Example of PHP CAPTCHA Recognition

Here's an example of CAPTCHA recognition using PHP. The recognition process includes image binarization, noise reduction, compensation, segmentation, skew correction, database construction, and matching. Finally, sample code will be provided that can directly run the recognition.

Brief Description

The CAPTCHA to be recognized is relatively simple, with no overlapping characters, but it may feature bolded fonts to varying degrees, as well as a skew of approximately 0-30 degrees. The number of characters can also vary between 4-5. Generally, using Python for CAPTCHA recognition is relatively simple. For further reference, you can read the following articles:
CAPTCHA Recognition in Qiangzhi Education System using OpenCV
CAPTCHA Recognition in Qiangzhi Education System using Tensorflow CNN

Binarization

Images are composed of individual pixels, with each pixel having quantifiable RGB color values. Based on the colors of the CAPTCHA, the threshold for the three colors is adjusted to filter out the background and characters, setting the background to 1 and the characters to 0.

// Binarization
    private static function binaryImage($image){
        $img = [];
        for($y = 0;$y < self::$width;$y++) {
            for($x =0;$x < self::$height;$x++) {
                if($y === 0 || $x === 0 || $y === self::$width - 1 || $x === self::$height - 1){
                    $img[$x][$y] = 1;
                    continue;
                }
                $rgb = imagecolorat($image, $y, $x);
                $rgb = imagecolorsforindex($image, $rgb);
                if($rgb['red'] < 255 && $rgb['green'] < 230 && $rgb['blue'] < 220) {
                    $img[$x][$y] = 0;
                } else {
                    $img[$x][$y] = 1;
                }
            }
        }
        return $img;
    }
1111111111111111111111111111011111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111100000000011111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111000000000111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111110000000000111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111110000000000111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111100000011111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111100100111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111111111111111111111111111111111111111111111111111111100111111111111111111111111111111111111111111111111000111111111111111111111111111111111111111111100000011111111111111111111111111111111111111
1111111111111111000000000000001111111111111111111111111111110000000011111111100000001111111111111111111111000000000000111111111111111111111111111111111111110000000000011111111111111111111111111111111111000000000000001111111111111111111111111111111111
1111111111111111100000100000001111111111111111111111111111110000000111111111000000001111111111111111111110000000000000001111111111111111111111111111111111100000000000000111111111111111111111111111111110000000000000000111111111111111111111111111111111
1111111111111111100000000000001111111111111111111111111111110000000111111111000000001111111111111111111100000000000000000111111111111111111111111111111110000000000000000011111111111111111111111111111100000000000000000001111111111111111111111111111111
1111111111111111100000000011101111111111111111111111111111110000100111111111000000011111111111111111111000000111110000000111111111111111111111111111111110000000000000000001111111111111111111111111111000000000000000000001111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111100000100111111111000000011111111111111111111000011111111100000111111111111111111111111111111100000000000000000000111111111111111111111111111000000000010000000000111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111100000101111111111000000011111111111111111110000011111111110000011111111111111111111111111111000000001111110000000111111111111111111111111110000000111111110000000111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111100000001111111111000000011111111111111111110000011111111110000011111111111111111111111111111001000011111110000000011111111111111111111111110000000111111110000000011111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111100000001111111110000000011111111111111111110000011111111111111111111111111111111111111111110000000011111111000000011111111111111111111111110000000111111111001111111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111000000001111111110000000111111111111111111111000001111111111111111111111111111111111111111110000000111111111000000011111111111111111111111110000000000111111111111111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111000000011111111110000000111111111111111111111000000011111111111111111111111111111111111111110100000111111111100000001111111111111111111111111000000000001111111111111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111000000011111111110000000111111111111111111111000000000001111111111111111111111111111111111100000000001010111100000001111111111111111111111111000000000000000011111111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111000000011111111100000000111111111111111111111100000000000001111111111111111111111111111111100000000000000000000000001111111111111111111111111000000000000000000011111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111000000011111111100000001111111111111111111111110000000001000001111111111111111111111111111100000000000010110000011001111111111111111111111111100000000000000000000111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111000000011111111100000001111111111111111111111111100000000000000011111111111111111111111111100000000000000010000110001111111111111111111111111110000000000000110000111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111110000000011111111100000001111111111111111111111111111100000000000011111111111111111111111111110000000000000000000000001111111111111111111111111111100000000000000000011111111111111111111111111111
1111111111111111111100000111111111111111111111111111111110000000111111111000000001111111111111111111111111111111110000000001111111111111111111111111110000000111111111111111111111111111111111111111111111111110000000000000011111111111111111111111111111
1111111111111111111100000111111111111111111111111111111110000000111111111000000001111111111111111111111111111111111110000001111111111111111111111111110000000111111111111111111111111111111111111111111111111111110000000000011111111111111111111111111111
1111111111111111111100000111111111111111111111111111111100000001111111111000000001111111111111111111111110111111111111000001111111111111111111111111110000000111111111011111111111111111111111111111111111101111111111000000001111111111111111111111111111
1111111111111111111100000111111111111111111111111111111100000001111111110000000011111111111111111111110000111111111111100001111111111111111111111111110000000011111111000001011111111111111111111111100000000111111111001000011111111111111111111111111111
1111111111111111111100000111111111111111111111111111111100000001111111100000000011111111111111111111110000011111111111100001111111111111111111111111111000000011111110000000011111111111111111111111100000000111111111001000011111111111111111111111111111
1111111111111111111100000111111111111111111111111111111100000000111111000000000011111111111111111111110000011111111111000001111111111111111111111111111000000001111110000000011111111111111111111111100000000011111111000100011111111111111111111111111111
1111111111111111111100000111111111111111111111111111111100000000001000000000000111111111111111111111111000001111111110000011111111111111111111111111111000000000000000000000111111111111111111111111110000000000000000000000111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111110000000000000000000000111111111111111111111111000000001111100000011111111111111111111111111111100000100000000000000111111111111111111111111111000000000000000000000111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111110000000000000011000100111111111111111111111111100000000000000000011111111111111111111111111111110000000000000000001111111111111111111111111111100000000000000000001111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111000000000000110000000111111111111111111111111110001000010000001111111111111111111111111111111111100000000000000011111111111111111111111111111110000000000000000011111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111100000000001110000000111111111111111111111111111000000000000011111111111111111111111111111111111110000000000001111111111111111111111111111111111000000000000001111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111000011111111111111111111111111111111111111111111100000111111111111111111111111111111111111111111111000011111111111111111111111111111111111111111110000111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111

Noise Reduction and Compensation

CAPTCHAs often contain some noise, which typically consists of isolated points or occasionally a few single-pixel points forming interference lines. When reducing noise, it's necessary to eliminate these noisy points and interference lines. I adopted the approach of extracting the values of the four surrounding pixels for each pixel, and if two or more of these surrounding pixels are background (i.e., 1), then it's considered a noisy point and set to background.
During binarization, it's inevitable that some small character pixels will be filtered into the background. In such cases, it's necessary to compensate for these characters. I also used the same approach of calculating the values of the four surrounding pixels, and if two or more of them are characters (i.e., 0), then the pixel is considered a character pixel and set accordingly.

// Noise Reduction Compensation
    private static function noiseReduce($img) {
        $xCount = count($img[0]);
        $yCount = count($img); 
        for ($i=1; $i < $yCount-1 ; $i++) { 
            for ($k=1; $k < $xCount-1; $k++) { 
                if($img[$i][$k] === 0){
                    $countOne = $img[$i][$k-1] + $img[$i][$k+1] + $img[$i+1][$k] + $img[$i-1][$k];
                    if($countOne > 2) $img[$i][$k] = 1;
                } 
                if($img[$i][$k] === 1){
                    $countZero = $img[$i][$k-1] + $img[$i][$k+1] + $img[$i+1][$k] + $img[$i-1][$k];
                    if($countZero < 2) $img[$i][$k] = 0;
                } 
            }
        }
        return $img;
    }
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111100000000111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111000000000111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111110000000000111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111110000000000111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111100000011111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111111111111111111111111111111111111111111111111111111100111111111111111111111111111111111111111111111111000111111111111111111111111111111111111111111100000011111111111111111111111111111111111111
1111111111111111100000000000001111111111111111111111111111110000000111111111100000001111111111111111111111000000000000111111111111111111111111111111111111110000000000011111111111111111111111111111111111000000000000001111111111111111111111111111111111
1111111111111111100000000000001111111111111111111111111111110000000111111111000000001111111111111111111110000000000000001111111111111111111111111111111111100000000000000111111111111111111111111111111110000000000000000111111111111111111111111111111111
1111111111111111100000000000001111111111111111111111111111110000000111111111000000001111111111111111111100000000000000000111111111111111111111111111111110000000000000000011111111111111111111111111111100000000000000000001111111111111111111111111111111
1111111111111111100000000011111111111111111111111111111111110000000111111111000000011111111111111111111000000111110000000111111111111111111111111111111110000000000000000001111111111111111111111111111000000000000000000001111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111100000000111111111000000011111111111111111111000011111111100000111111111111111111111111111111100000000000000000000111111111111111111111111111000000000000000000000111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111100000001111111111000000011111111111111111110000011111111110000011111111111111111111111111111000000001111110000000111111111111111111111111110000000111111110000000111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111100000001111111111000000011111111111111111110000011111111110000011111111111111111111111111111000000011111110000000011111111111111111111111110000000111111110000000111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111100000001111111110000000011111111111111111110000011111111111111111111111111111111111111111110000000011111111000000011111111111111111111111110000000111111111001111111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111000000001111111110000000111111111111111111111000001111111111111111111111111111111111111111110000000111111111000000011111111111111111111111110000000000111111111111111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111000000011111111110000000111111111111111111111000000011111111111111111111111111111111111111110000000111111111100000001111111111111111111111111000000000001111111111111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111000000011111111110000000111111111111111111111000000000001111111111111111111111111111111111100000000000000111100000001111111111111111111111111000000000000000011111111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111000000011111111100000000111111111111111111111100000000000001111111111111111111111111111111100000000000000000000000001111111111111111111111111000000000000000000011111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111000000011111111100000001111111111111111111111110000000000000001111111111111111111111111111100000000000000000000010001111111111111111111111111100000000000000000000111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111000000011111111100000001111111111111111111111111100000000000000011111111111111111111111111100000000000000000000000001111111111111111111111111110000000000000000000111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111110000000011111111100000001111111111111111111111111111100000000000011111111111111111111111111110000000000000000000000001111111111111111111111111111100000000000000000011111111111111111111111111111
1111111111111111111100000111111111111111111111111111111110000000111111111000000001111111111111111111111111111111110000000001111111111111111111111111110000000111111111111111111111111111111111111111111111111110000000000000011111111111111111111111111111
1111111111111111111100000111111111111111111111111111111110000000111111111000000001111111111111111111111111111111111110000001111111111111111111111111110000000111111111111111111111111111111111111111111111111111110000000000011111111111111111111111111111
1111111111111111111100000111111111111111111111111111111100000001111111111000000001111111111111111111111111111111111111000001111111111111111111111111110000000111111111111111111111111111111111111111111111111111111111000000011111111111111111111111111111
1111111111111111111100000111111111111111111111111111111100000001111111110000000011111111111111111111110000111111111111100001111111111111111111111111110000000011111111000000011111111111111111111111100000000111111111000000011111111111111111111111111111
1111111111111111111100000111111111111111111111111111111100000001111111100000000011111111111111111111110000011111111111100001111111111111111111111111111000000011111110000000011111111111111111111111100000000111111111000000011111111111111111111111111111
1111111111111111111100000111111111111111111111111111111100000000111111000000000011111111111111111111110000011111111111000001111111111111111111111111111000000001111110000000011111111111111111111111100000000011111111000000011111111111111111111111111111
1111111111111111111100000111111111111111111111111111111100000000000000000000000111111111111111111111111000001111111110000011111111111111111111111111111000000000000000000000111111111111111111111111110000000000000000000000111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111110000000000000000000000111111111111111111111111000000001111100000011111111111111111111111111111100000000000000000000111111111111111111111111111000000000000000000000111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111110000000000000010000000111111111111111111111111100000000000000000011111111111111111111111111111110000000000000000001111111111111111111111111111100000000000000000001111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111000000000000110000000111111111111111111111111110000000000000001111111111111111111111111111111111100000000000000011111111111111111111111111111110000000000000000011111111111111111111111111111111
1111111111111111111100000111111111111111111111111111111111100000000001110000000111111111111111111111111111000000000000011111111111111111111111111111111111110000000000001111111111111111111111111111111111000000000000001111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111000011111111111111111111111111111111111111111111100000111111111111111111111111111111111111111111111000011111111111111111111111111111111111111111110000111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111

Segmentation

As this captcha is not connected, the segmentation of the characters is relatively simple. Vertically, the start and end positions of the segmented characters are counted, and then placed into an array after segmentation. The horizontal spaces are removed, and similarly, the start and end rows with '0' values are counted, and then segmented, keeping only the characters.

// Image cropping function
    private static function cutImg($img){
        $xCount = count($img[0]);
        $yCount = count($img);
        $xFilter = [];
        for($x = 0;$x < $xCount;$x++) {
            $filter = true;
            for($y = 0;$y < $yCount;$y++)  $filter = $filter && ($img[$y][$x] === 1);
            if($filter) $xFilter[] = $x;
        }
        $xImage = array_values(array_diff(range(0, $xCount-1), $xFilter));
        $wordImage = [];
        $preX = $xImage[0] - 1;
        $wordCount = 0;
        foreach($xImage as $xKey => $x) {
            if($x != ($preX + 1))  $wordCount++;
            $preX = $x;
            for($y = 0;$y < $yCount;$y++) $wordImage[$wordCount][$y][] = $img[$y][$x];
        }
        $cutImg = [];
        foreach($wordImage as $i => $image) {
            $xCount = count($image[0]);
            $yCount = count($image);
            $start = 0;
            for ($j=0; $j < $yCount; ++$j) { 
                $stopFlag = false;
                for ($k=0; $k < $xCount; ++$k) { 
                    if ($image[$j][$k] === 0) {
                        $start = $j;
                        $stopFlag = true;
                        break;
                    }
                }
                if($stopFlag) break;
            }
            $stop = $yCount-1;
            for ($j=$yCount-1; $j >= 0; --$j) { 
                $stopFlag = false;
                for ($k=0; $k < $xCount; ++$k) { 
                    if ($image[$j][$k] === 0) {
                        $stop = $j;
                        $stopFlag = true;
                        break;
                    }
                }
                if($stopFlag) break;
            }
            for ($k=$start; $k <= $stop ; ++$k) { 
                $cutImg[$i][] = $image[$k];
            }
            // self::showImg($cutImg[$i]);
            $cutImg[$i] = self::adjustImg($cutImg[$i]);
            // self::showImg($cutImg[$i]);
        }
        return $cutImg;
    }
1111111111111000001111111
1111111100000000000001111
1111111000000000000000011
1111110000000000000000011
1111100000000000000000001
1111000000001111000000001
1110000000011111100000000
1110000000111111110000000
1111111111111111110000000
1111111111111111110000000
1111111111111111100000001
1111111100000000000000001
1111100000000000000000001
1110000000000000000000001
1100000000000000000000001
1000000000000111100000011
1000000001111111000000011
1000000011111111000000011
0000000111111111000000011
0000000111111110000000111
0000000111111100000000111
0000000011111000000000111
1000000001100000000000111
1000000000000000000000111
1000000000000000000000111
1100000000000010000000111
1111000000001110000000111
1111100001111111111111111

Skew Correction

I've tried two approaches for skew correction, one is using linear regression, the other one is using projection.

Linear Regression

With linear regression, I obtained the coordinates of the midpoint of character pixel points on each line, used the least squares method to fit the curve, and obtained a slope, which is equivalent to obtaining the skew angle of the character. Then, I corrected the skew of the character based on the slope. This method works quite well for characters like 'n', but not so well for characters like 'j'.

$img = [
    [1,1,1,1,1,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,1,1],
    [1,1,1,1,1,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,1],
    [1,1,1,1,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0],
    [1,1,1,1,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0],
    [1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,0,0,0,0],
    [1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0],
    [1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],
    [1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],
    [1,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],
    [1,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0],
    [1,1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0],
    [1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1],
    [1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,1],
    [1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,1],
    [1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,1],
    [1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,1],
    [1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1,1],
    [1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1,1],
    [1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,1,1],
    [1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,1,1],
    [1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,1,1],
    [0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1,1,1],
    [0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1,1,1],
    [0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,1,1,1],
    [0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,1,1,1],
];
ImgIdenfy::showImg($img);
$mixX = 0.0;
$mixY = 0.0;
$mixXX = 0.0;
$mixXY = 0.0;
$yCount = count($img);
$xCount = count($img[0]);
foreach($img as $i => $line) {
    $x = 0;
    $xValidCount = 0;
    foreach($line as $k => $unit) {
        if($unit === 0) {
            $x += $k;
            ++$xValidCount;
        }
    }
    if($xValidCount) {
        $pointX = $x/$xValidCount;
        $pointY = $yCount - $i;
        $mixX += $pointX;
        $mixY += $pointY;
        $mixXX += ($pointX*$pointX);
        $mixXY += ($pointX*$pointY);
    }
}
$linearK = -($mixXY - $mixX*$mixY/$yCount) / ($mixXX - $mixX*$mixX/$yCount);
// if($linearK > -1 && $linearK < 1) return $img;
$whirlImg = [];
foreach($img as $i => $line) {
    $pointY = $i;
    if(!isset($whirlImg[$pointY])) $whirlImg[$pointY]=[];
    foreach($line as $pointX => $unit) {
        if(!isset($whirlImg[$pointY][$pointX])) $whirlImg[$pointY][$pointX]=1;
        // $newY = (int)($pointY*sqrt(1+$linearK*$linearK)/$linearK);
        $newY = (int)($pointY);
        $newX = (int)($pointX-$pointY/$linearK);
        if($newX >= 0 && $newX < $xCount && $newY >= 0 && $newY < $yCount) $whirlImg[$newY][$newX] = $unit;
    }
}

$finishedImg = [];
for ($i=0; $i < $xCount; ++$i) { 
    for($k=0; $k < $yCount; ++$k) {
        if($whirlImg[$k][$i] !== 1){
            for($y = 0;$y < $yCount;++$y) $finishedImg[$y][] = $whirlImg[$y][$i];
            break;
        }
    }
}
ImgIdenfy::showImg($finishedImg);
111110000111100000000011
111110000111000000000001
111100001110000000000000
111100000000000111100000
111100000000111111110000
111100000001111111110000
111100000011111111110000
111000000111111111110000
111000000111111111110000
111000001111111111110000
111000001111111111100000
111000011111111111100000
110000011111111111100001
110000011111111111000001
110000111111111111000001
110000111111111111000001
110000111111111111000001
100000111111111111000011
100000111111111111000011
100000111111111110000011
100001111111111110000011
100001111111111110000011
000011111111111110000111
000011111111111110000111
000011111111111100000111
000011111111111100000111

10000111100000000011
10000111000000000001
00001110000000000000
00000000000111100000
00000000111111110000
10000000111111111000
10000001111111111000
00000011111111111000
00000011111111111000
00000111111111111000
10000011111111111000
10000111111111111000
00000111111111111000
00000111111111110000
00001111111111110000
10000111111111111000
10000111111111111000
00000111111111111000
00000111111111111000
00000111111111110000
10000111111111111000
10000111111111111000
00001111111111111000
00001111111111111000
00001111111111110000
10000111111111111000

Projection Method

As the direct linear fitting method does not work well for some characters, I adopted the projection method. When a character is rotated, its width will inevitably increase. Therefore, I attempted to rotate the character within a certain range and obtained the character with the minimum width during the rotation process, which is the corrected character. Since directly rotating the vertical characters according to the slope is not feasible as 'tan90°' does not exist, it's difficult to define the counterclockwise rotation range. Therefore, I first transpose the character array, then rotate it clockwise within the range of '-0.5-0.5' of the slope, and then transpose it back. In the implementation process, there is a considerable amount of repetitive calculation, which mainly requires mathematical deduction. Additionally, if the character width changes from small to large during the rotation process, the reverse calculation or stopping the calculation can be performed, similar to a gradient descent method. Besides, I didn't use matrix operations. If matrix operations are used, the implementation will be simpler. In PHP, there are machine learning libraries like PHP-ML, which provide methods for matrix operations. Of course, you can also directly use PHP-ML for neural network training.

// whirl
    private static function whirl($img, $yCount, $xCount, $linearK){
        $whirlImg = [];
        foreach($img as $i => $line) {
            $pointY = $yCount - $i - 1;
            if(!isset($whirlImg[$pointY])) $whirlImg[$pointY]=[];
            foreach($line as $pointX => $unit) {
                if(!isset($whirlImg[$pointY][$pointX])) $whirlImg[$pointY][$pointX]=1;
                $newY = (int)($pointY - $pointX*$linearK);
                $newX = (int)($pointX);
                if($unit === 0 && ($newY < 0 || $newY >= $yCount)) return [$yCount+1, $img];
                if($newX >= 0 && $newX < $xCount && $newY >= 0 && $newY < $yCount) $whirlImg[$newY][$newX] = $unit;
            }
        }
        $cutImg = [];
        $height = $yCount;
        foreach ($whirlImg as $j => $line) {
            foreach ($line as $k => $v) {
                if($v !== 1) {
                    --$height;
                    break;
                }
            }
        }
        return [$yCount - $height, $whirlImg];
    }

    // Tilt adjustment
    private static function adjustImg($img){
        $reverseImg = [];
        $yCount = count($img);
        $xCount = count($img[0]);
        for ($i=0; $i < $yCount; ++$i) { 
            $pointY = $yCount - $i - 1;
            for($k=0; $k < $xCount; ++$k) {
                $reverseImg[$k][$i] = $img[$pointY][$k];
            }
        }
        list($yCount,$xCount) = [$xCount,$yCount];
        $min = $yCount;
        $minImg = $reverseImg;
        for ($k= -0.5 ; $k <= 0.5; $k = $k + 0.05) { 
            list($tempMin, $tempMinImg) = self::whirl($reverseImg, $yCount, $xCount, $k);
            if($tempMin < $min) {
                $min = $tempMin;
                $minImg = $tempMinImg;
            }
        }
        $removedImg = [];
        foreach ($minImg as $j => $line) {
            foreach ($line as $k => $v) {
                if($v !== 1) {
                    $removedImg[] = $line;
                    break;
                }
            }
        }
        $reverseImg = [];
        $xCount = count($removedImg[0]);
        $yCount = count($removedImg);
        $reverseImg = [];
        for ($i=0; $i < $xCount; ++$i) { 
            for($k=0; $k < $yCount; ++$k) {
                $pointX = $xCount - $i - 1;
                $reverseImg[$i][$k] = $removedImg[$k][$pointX];
            }
        }
        return $reverseImg;
    }
1111111111111000001111111
1111111100000000000001111
1111111000000000000000011
1111110000000000000000011
1111100000000000000000001
1111000000001111000000001
1110000000011111100000000
1110000000111111110000000
1111111111111111110000000
1111111111111111110000000
1111111111111111100000001
1111111100000000000000001
1111100000000000000000001
1110000000000000000000001
1100000000000000000000001
1000000000000111100000011
1000000001111111000000011
1000000011111111000000011
0000000111111111000000011
0000000111111110000000111
0000000111111100000000111
0000000011111000000000111
1000000001100000000000111
1000000000000000000000111
1000000000000000000000111
1100000000000010000000111
1111000000001110000000111
1111100001111111111111111

111111111110000011111111
111111000000000000011111
111110000000000000000111
111100000000000000000111
111000000000000000000011
110000000011110000000011
100000000111111000000001
100000001111111100000001
111111111111111110000000
111111111111111110000000
111111111111111100000001
111111100000000000000001
111100000000000000000001
110000000000000000000001
100000000000000000000001
000000000000111100000011
000000001111111000000011
000000011111111000000011
000000011111111100000001
000000011111111000000011
000000011111110000000011
000000001111100000000011
100000000110000000000011
100000000000000000000011
100000000000000000000011
110000000000001000000011
111100000000111000000011
111110000111111111111111

Database Construction

After correcting the CAPTCHA, it's necessary to build a feature matching database. Here, I directly used the binarized array converted into a string as the entire feature and wrote it into a feature matching array. Then, I manually entered the codes. If the recognized character does not match the manually entered character, it was added to the feature matching array. Then, the character array was serialized and stored in a file. After that, the serialized string was compressed and stored in a file. I extracted about 150 character feature codes, occupying about 8KB. Note that I used PHP as a script, configured the environment variables, inserted empty data, and then used php Build.php to start extracting the feature codes.

// Write empty serialized array
// $info = serialize([]);
// $library = fopen("library", "w+");
// fwrite($library,gzcompress($info));
// fclose($library);
$library = fopen("library", "r+");
$info = fread($library, filesize("library"));
if (!$info) {
    $charMap = [];
} else {
    $charMap = unserialize(gzuncompress($info));
}
while (1) {
    $img = imagecreatefromjpeg("http://grdms.sdust.edu.cn:8081/security/jcaptcha.jpg");
    imagejpeg($img, "v.jpg");
    list($result, $imgStringArr) = ImgIdenfy::build($img, $charMap, 250, 100);
    echo ($result . "\n");
    $input = fgets(STDIN);
    if (isset($input[0]) && $input[0] === "$") {
        break;
    }
    $n = strlen($input) - 2;
    for ($i = 0; $i < $n; $i++) {
        if (!isset($result[$i]) || $input[$i] !== $result[$i]) {
            $charMap[$input[$i] . mt_rand(1, 10000)] = $imgStringArr[$i];
        }
    }
    echo count($charMap) . "\n";
    ftruncate($library, 0);
    rewind($library);
    fwrite($library, gzcompress(serialize($charMap)));
}
fclose($library);

Match

Because all the feature information is directly stored in the file, you can directly use a loop to compare the values of the strings. To improve accuracy, I align the first 0 of the two comparison strings and then iterate through, obtaining the number of identical characters. In addition, because the lengths of the strings being compared are different, I multiply the length information of the string by a certain weight and include it as part of the similarity. Of course, in PHP, the similar_text function is provided for comparing string similarities. Using this function will improve recognition rate, but because the string length is too long, the comparison matching time is relatively slow. To balance time consumption and accuracy, I still chose the self-matching method.

// Comparison
    private static function comparedText($s1, $s2){
        $s1N = strlen($s1);
        $s2N = strlen($s2);
        $i = -1;
        $k = -1;
        $percent = -abs($s1N - $s2N) * 0.1;
        while (++$i < $s1N && $s1[$i]) {}
        while (++$k < $s2N && $s2[$k]) {}
        while ($i < $s1N && $k < $s2N) ($s1[$i++] === $s2[$k++]) ? $percent++ : "";
        return $percent;
        // $percent = 0;
        // $N = $s1N < $s2N ? $s1N : $s2N;
        // for ($i = 0; $i < $N; ++$i) { 
        //     ($s1[$i] === $s2[$i]) ? $percent++ : "";
        // }
        // return $percent;
    }
// Match
private static function matchCode($imgGroup, $charMap){
    $record = "";
    $imgStringArr = [];
    foreach ($imgGroup as $img) {
        $maxMatch = 0;
        $tempRecord = "";
        $s = ImgIdenfy::getString($img);
        foreach ($charMap as $key => $value) {
            // similar_text(ImgIdenfy::getString($img),$value,$percent);
            $percent = self::comparedText($s , $value);
            if($percent > $maxMatch){
                $maxMatch = $percent;
                $tempRecord = $key[0];
            }
        }
        $record = $record.$tempRecord;
        $imgStringArr[] = $s;
    }
    return [$record, $imgStringArr];
}

Example Code

If you think it's good, give it a star 😃 https://github.com/WindrunnerMax/Example