针对的图形验证码需要具备元素之间无牵连的特点, 比如可以破解的有:

http://ykjcx.yundasys.com/zb1qBpg2.php

https://www.ttkdex.com/verificationCodeAction!getRandomPictrue.action

思路:

  1. 下载图片
  2. 二值化
  3. 降噪 (可不执行)
  4. 缩空白边 (可不执行)
  5. 纵向切割
  6. 序列化
  7. 匹配(采集,人工识别)

1. 下载图片

从网页上下载图片有多种方式, 这里使用 php 的 curl 函数, 使用curl函数的一个好处是可以通过响应头来判别图片的类型。


    private function downloadFile(string $url):bool
    {
        $ch = curl_init();
        curl_setopt($ch,CURLOPT_URL,$url);
        curl_setopt($ch, CURLOPT_POST, $this->config[self::IS_POST]);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, true);

        if ($this->config[self::HTTP_HEADER]) {
            curl_setopt($ch,CURLOPT_HTTPHEADER,$this->config[self::HTTP_HEADER]);
        }

        $resp = curl_exec($ch);
        $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
        $header = substr($resp, 0, $headerSize);
        $file_content = substr($resp, $headerSize);
        curl_close($ch);


        $pattern = "/Content-Type: images?\/(?<type>(?:" . implode('|', self::ALLOW_TYPE) . "))/";
        preg_match($pattern, $header,$matches );
        if(isset($matches['type']) && \in_array($matches['type'], self::ALLOW_TYPE)) {
            $this->imageType = $matches['type'];
        } else {
            return false;
        }

        $this->fileName = self::FILE_PATH . 'code-' . getmypid() . rand(1000, 9999) . '.' . $this->imageType;
        $fp = fopen($this->fileName, 'w');
        fwrite($fp, $file_content);
        fclose($fp);

        return true;
    }
    

2. 二值化

将图片资源转化为一个0和1组成的矩阵, 这一步至关重要, 二值化之后, 我们可以在 php 中将图片转化成数组进行处理。


    private function binary():array {
        if(!$ex = getimagesize($this->fileName)){
            return [];
        }

        $this->imageX = $ex[0];
        $this->imageY = $ex[1];

        switch (strtolower($this->imageType)) {
            case 'png':
                $im = imagecreatefrompng($this->fileName);
                break;
            default:
                $im = imagecreatefromjpeg($this->fileName);
                break;
        }

        $gray = array_fill(0, $this->imageY,
            array_fill(0, $this->imageX, 0)
        );

        // 灰度化
        foreach($gray as $y => &$row){
            foreach($row as $x => &$Y){
                $rgb = imagecolorat($im, $x, $y);
                $B = $rgb & 255;
                $G = ($rgb >> 8) & 255;
                $R = ($rgb >> 16) & 255;
                $Y = ($R * 19595 + $G * 38469 + $B * 7472) >> 16;
            }
        }
        unset($row, $Y);

        // 求域值 也可以联调手动设置阈值

        if ($this->config[self::CRUX]) {
            $crux = $this->config[self::CRUX];
        } else {
            $back = 127;
            do{
                $crux = $back;
                $s = $b = $l = $I = 0;
                foreach($gray as $row){
                    foreach($row as $Y){
                        if($Y < $crux){
                            $s += $Y;
                            $l++;
                        }else{
                            $b += $Y;
                            $I++;
                        }
                    }
                }
                $s = $l ? floor($s / $l) : 0;
                $b = $I ? floor($b / $I) : 0;
                $back = ($s + $b) >> 1;
            }while($crux != $back);
        }

        // 二值化
        $bin = $gray;
        foreach($bin as &$row){
            foreach($row as &$Y){
                $Y = $Y < $crux ? 0 : 1;
            }
        }
        return $bin;
    }

3. 降噪

这个算法可以扩展, 目前代码实现仅仅是邻域降噪, 也就是根据当前元素周边的几个元素, 如果孤立便判别为噪点进行移除, 实际情况需要不断的尝试分析领域的范围以及 定义为孤立的阈值, 对于小图,通常8领域就可以,也就是包括元素点共9个点, 阈值设置为 ceil(1/9) = 2 就可以,大图需要不断尝试。
这只是针对点的处理,对于有些验证码上面的线,需要使用其他算法。

    private function removeNoise(array $bin):array {

        if (empty($bin) || !$this->config[self::REMOVE_NOISE]) {
            return $bin;
        }

        $size = floor((sqrt($this->config[self::NOISE_AREA]) - 1 ) / 2 );
        for($i=0; $i<$this->imageY; $i++) {
            for($j=0; $j<$this->imageX; $j++) {
                $count = 0;
                $sum = 0;
                for ($ti=$i-$size;$ti<=$i+$size;$ti++) {
                    for ($tj=$j-$size;$tj<=$j+$size;$tj++) {
                        $count++;
                        if(isset($bin[$ti][$tj]) && $bin[$ti][$tj] != $this->config[self::BACKGROUND]) {
                            $sum++;
                        }
                    }
                }
                if( $sum/$count <= $this->config[self::NOISE_WEIGHT] ) {
                    $bin[$i][$j] = $this->config[self::BACKGROUND];
                }

            }
        }

        if ($this->config[self::NOISE_IMG]) {
            $retName = str_replace('.', '_noise.', $this->fileName);
            $this->createImg($bin, $retName);
        }

        return $bin;
    }

4. 缩边

去掉空白边的目的是避免元素的不齐导致同一个元素在数据存储中存在多条,增加识别的工作量。

    private function reduceSide(array $bin):array {
        if (empty($bin) || !$this->config[self::REDUCE_SIDE] ) {
            return $bin;
        }
        $dealFunc = function(&$bin, $background) {
            $size = \count($bin);
            for($i=0; $i<$size; $i++) {
                if(\array_sum($bin[$i]) == \count($bin[$i]) * $background ) {
                    unset($bin[$i]);
                } else {
                    break;
                }
            }

            for($i=$size-1; $i>=0; $i--) {
                if(\array_sum($bin[$i]) == \count($bin[$i]) * $background ) {
                    unset($bin[$i]);
                } else {
                    break;
                }
            }
        };
        $dealFunc($bin, $this->config[self::BACKGROUND]);
        $bin = $this->arrayTransposition(array_values($bin));
        $dealFunc($bin, $this->config[self::BACKGROUND]);
        $bin = $this->arrayTransposition(array_values($bin));
        return array_values($bin);
    } 

5. 纵向切割

对于大部分图形验证码而言 , 生成的时候都是一个一个的元素画上去的, 这些元素之间通常都有间隔, 而且这些元素通常都是通用, 这也是这里识别的原理所在。

   
    private function splitRow(array $bin):array {
        $mark = 0;
        $ret = $tmp = [];

        foreach ($bin as $k=>$v) {

            $sum = array_sum($v);
            $cnt = \count($v);

            if ($this->config[self::BACKGROUND]) {
                if( $mark == 0 ) {
                    if($sum != $cnt) {
                        $mark = 1;
                        $tmp[] = $v;
                    }
                } else {
                    if($sum < $cnt) {
                        $tmp[] = $v;
                    } else {
                        $ret[] = $tmp;
                        $tmp = [];
                        $mark = 0;
                    }
                }
            } else {
                if( $mark == 0 ) {
                    if($sum != 0) {
                        $mark = 1;
                        $tmp[] = $v;
                    }
                } else {
                    if($sum > 0) {
                        $tmp[] = $v;
                    } else {
                        $ret[] = $tmp;
                        $tmp = [];
                        $mark = 0;
                    }
                }

            }
        }

        $tmp && $ret[] = $tmp;
        return $ret;
    }

5. 序列化


foreach ($ret as $k => $v) {
    $ret[$k] = md5(json_encode($v));
}

6. 采集

    private function createImg(array $bin, string $fileName) {
        $img = imagecreate(count($bin[0]), count($bin));
        $rgb = array(
            imagecolorallocate($img, 255, 255, 255),
            imagecolorallocate($img, 0, 0, 0)
        );

        $x = $y = 0;
        foreach($bin as $row){
            do{
                imagesetpixel($img, $x, $y, $rgb[$row[$x]]);
            }while(isset($row[++$x]));
            $x = 0;
            $y++;
        }

        switch (strtolower($this->imageType)) {
            case 'png':
                imagepng($img, $fileName);
                break;
            default :
                imagejpeg($img, $fileName);
                break;
        }
    }

代码示例

<?php
/**
 * Created by Ogenes.
 * User: yihuaiyuan
 * Date: 2019/5/16
 * Time: 2:22 PM
 */

namespace service\verificationCodeCracking;


use artisan\db;

class Recognition
{

    private const FILE_PATH = '/Users/yihuaiyuan/www/personal/file/';

    private const ALLOW_TYPE = [
        'jpeg',
        'png',
        'Jpeg',
    ];

    private const
        TB = 'verification_code',
        DB = 'v_home';

    private
        $fileName = '',//临时文件名
        $imageType = 'jpeg',//图片格式
        $imageX,//图片横向
        $imageY;//图片纵向

    
    //可配置项
    public const
        BACKGROUND = 0,
        IS_POST = 1,
        MARK = 2,
        INIT_IMG = 3,
        BIN_IMG = 4,
        CRUX = 5,
        REDUCE_SIDE = 6,
        REMOVE_NOISE = 7,
        NOISE_IMG = 8,
        NOISE_AREA = 9,
        NOISE_WEIGHT = 10,
        SPLIT_IMG = 11,
        HTTP_HEADER = 12;

    private $config = [
        self::BACKGROUND => 0,//二值化之后的图片背景 0白底黑字,1黑底白字
        self::IS_POST => false,//下载文件的请求方式
        self::MARK => '',//数据存储标记位
        self::INIT_IMG => false,//是否保存原图,未完全匹配强制为true
        self::BIN_IMG => false,//是否保存二值化之后的图片
        self::CRUX => 0,//二值化的阈值,不设置时自动求
        self::REDUCE_SIDE => true,//是否需要切边
        self::REMOVE_NOISE => false,//是否需要降噪
        self::NOISE_IMG => false,//是否保存降噪之后的图片,不降噪无意义
        self::NOISE_AREA => 9,//噪点比对区域,不降噪无意义
        self::NOISE_WEIGHT => 0.1,//噪点权重,不降噪无意义
        self::SPLIT_IMG => false,//是否保存切割之后的小图,未匹配强制为true
        self::HTTP_HEADER => [],//下载图片用到的请求头部
    ];

    private static $instance;

    public static function exec(string $url,  array $config):array {
        if (!self::$instance instanceof Recognition) {
            self::$instance = new Recognition();
        }

        self::$instance->config = $config + self::$instance->config;

        return self::$instance->cracking($url);
    }

    public static function test(string $fileName, array $config):array {

        if (!self::$instance instanceof Recognition) {
            self::$instance = new Recognition();
        }
        self::$instance->config = $config + self::$instance->config;
        self::$instance->imageType = substr($fileName, strpos($fileName, '.')+1);
        self::$instance->fileName = $fileName;

        return self::$instance->init();

    }

    private function cracking(string $url):array {

        $this->downloadFile($url);
        return $this->init();
    }

    private function init():array {

        if(!$this->fileName || !file_exists($this->fileName)) {
            return [];
        }

        $bin = $this->binary();
        if ($this->config[self::BIN_IMG]) {
            $retName = str_replace('.', '_bin.', $this->fileName);
            $this->createImg($bin, $retName);
        }

        $bin = $this->removeNoise($bin);
        $bin = $this->reduceSide($bin);

        //填充,待完善
        //$bin = $this->filling($bin);

        if(empty($bin)) {
            return [];
        }

        $ret = $this->splitRow($this->arrayTransposition($bin));

        $result = [];
        $whole = 1;
        foreach ($ret as $k=>$bin) {
            $bin = $this->reduceSide($bin);
            $key = md5(json_encode($bin));
            $where['key'] = $key;
            $this->config[self::MARK] && $where['mark'] = $this->config[self::MARK];

            $list = $this->getData($where);

            $char = $list['val'] ?? '';
            if ($char || $char === '0') {

                if ($char !== '.') {
                    $result[$k] = $char;
                }

                if (!$this->config[self::SPLIT_IMG]) {
                    continue;
                }

            } else {
                $result[$k] = '';
            }

            $whole = 0;
            $bin = $this->arrayTransposition($bin);
            $retName = str_replace('.', '_'. $k . $key . '.', $this->fileName);
            $this->createImg($bin, $retName);

            $data = [
                'filename' => $retName,
                'img' => $this->fileName,
            ];
            $this->saveData($where, $data);
        }

        //默认切割之后的图片存在时保留原图
        if($whole && !$this->config[self::INIT_IMG]) {
            unlink($this->fileName);
        }

        return array_values($result);
    }

    private function filling(array $bin):array {
        //test
        $retName = str_replace('.', '_reduce.', $this->fileName);
        $this->createImg($bin, $retName);

        $indexes = [];
        foreach ($bin as $k=>$v) {
            $indexes[$k]['size'] = 0;
            if (\current($v) == $this->config[self::BACKGROUND]) {
                $mark = 0;//背景色开始,
            } else {
                $mark = 1;
            }
            foreach ($v as $key=>$value) {
                switch ($mark) {
                    case 0:
                        if ($value != $this->config[self::BACKGROUND]) {
                            $mark = 1;
                        }
                        break;
                    case 1:
                        if ($value == $this->config[self::BACKGROUND]) {
                            $mark = 0;
                            $indexes[$k][] = $key;
                            $indexes[$k]['size']++;
                        }
                        break;
                }
            }
        }

        logInfo($indexes);
        $indexCount = count($indexes);
        $sizes = array_column($indexes,  'size');
        $minKey = array_search( min($sizes), $sizes);
        $minSize = $sizes[$minKey];

        $ret = [];
        for($i=0; $i<$indexCount; $i++) {
            unset($indexes[$i]['size']);
            if($i == $minKey) {
                $ret[$i] = $indexes[$i];
            } else {
                $standardSpaces = $ret[$i-1] ?? $indexes[$minKey];

                for ($j=0; $j<$minSize; $j++) {
                    $standard = $standardSpaces[$j];//应随时调整,根据上一次的值

                    $tmpDiff = abs($standard - current($indexes[$i]));
                    $ret[$i][$j] = array_shift($indexes[$i]);

                    foreach ($indexes[$i] as $key=>$value) {
                        if (abs($value-$standard) < $tmpDiff) {
                            $ret[$i][$j] = $value;
                            $tmpDiff = abs($value-$standard);
                            unset($indexes[$i][$key]);
                        } else {
                            break;
                        }
                    }
                }
            }
        }

        foreach ($bin as $k=>$v){
            $spaces = $ret[$k];
            foreach ($spaces as $sk=>$space) {
                $space = $space + $sk * 5;
                $bin[$k] = array_merge(array_slice($bin[$k], 0, $space), array_fill(0,5, 1), array_slice($bin[$k], $space));
            }
            echo PHP_EOL;
        }

        $retName = str_replace('.', '_fill.', $this->fileName);
        $this->createImg($bin, $retName);

        return $bin;
    }

    private function downloadFile(string $url):bool
    {
        $ch = curl_init();
        curl_setopt($ch,CURLOPT_URL,$url);
        curl_setopt($ch, CURLOPT_POST, $this->config[self::IS_POST]);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, true);

        if ($this->config[self::HTTP_HEADER]) {
            curl_setopt($ch,CURLOPT_HTTPHEADER,$this->config[self::HTTP_HEADER]);
        }

        $resp = curl_exec($ch);
        $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
        $header = substr($resp, 0, $headerSize);
        $file_content = substr($resp, $headerSize);
        curl_close($ch);


        $pattern = "/Content-Type: images?\/(?<type>(?:" . implode('|', self::ALLOW_TYPE) . "))/";
        preg_match($pattern, $header,$matches );
        if(isset($matches['type']) && \in_array($matches['type'], self::ALLOW_TYPE)) {
            $this->imageType = $matches['type'];
        } else {
            return false;
        }

        $this->fileName = self::FILE_PATH . 'code-' . getmypid() . rand(1000, 9999) . '.' . $this->imageType;
        $fp = fopen($this->fileName, 'w');
        fwrite($fp, $file_content);
        fclose($fp);

        return true;
    }

    private function binary():array {
        if(!$ex = getimagesize($this->fileName)){
            return [];
        }

        $this->imageX = $ex[0];
        $this->imageY = $ex[1];

        switch (strtolower($this->imageType)) {
            case 'png':
                $im = imagecreatefrompng($this->fileName);
                break;
            default:
                $im = imagecreatefromjpeg($this->fileName);
                break;
        }

        $gray = array_fill(0, $this->imageY,
            array_fill(0, $this->imageX, 0)
        );

        // 灰度化
        foreach($gray as $y => &$row){
            foreach($row as $x => &$Y){
                $rgb = imagecolorat($im, $x, $y);
                $B = $rgb & 255;
                $G = ($rgb >> 8) & 255;
                $R = ($rgb >> 16) & 255;
                $Y = ($R * 19595 + $G * 38469 + $B * 7472) >> 16;
            }
        }
        unset($row, $Y);

        // 求域值 也可以联调手动设置阈值

        if ($this->config[self::CRUX]) {
            $crux = $this->config[self::CRUX];
        } else {
            $back = 127;
            do{
                $crux = $back;
                $s = $b = $l = $I = 0;
                foreach($gray as $row){
                    foreach($row as $Y){
                        if($Y < $crux){
                            $s += $Y;
                            $l++;
                        }else{
                            $b += $Y;
                            $I++;
                        }
                    }
                }
                $s = $l ? floor($s / $l) : 0;
                $b = $I ? floor($b / $I) : 0;
                $back = ($s + $b) >> 1;
            }while($crux != $back);
        }

        // 二值化
        $bin = $gray;
        foreach($bin as &$row){
            foreach($row as &$Y){
                $Y = $Y < $crux ? 0 : 1;
            }
        }
        return $bin;
    }

    private function removeNoise(array $bin):array {

        if (empty($bin) || !$this->config[self::REMOVE_NOISE]) {
            return $bin;
        }

        $size = floor((sqrt($this->config[self::NOISE_AREA]) - 1 ) / 2 );
        for($i=0; $i<$this->imageY; $i++) {
            for($j=0; $j<$this->imageX; $j++) {
                $count = 0;
                $sum = 0;
                for ($ti=$i-$size;$ti<=$i+$size;$ti++) {
                    for ($tj=$j-$size;$tj<=$j+$size;$tj++) {
                        $count++;
                        if(isset($bin[$ti][$tj]) && $bin[$ti][$tj] != $this->config[self::BACKGROUND]) {
                            $sum++;
                        }
                    }
                }
                if( $sum/$count <= $this->config[self::NOISE_WEIGHT] ) {
                    $bin[$i][$j] = $this->config[self::BACKGROUND];
                }

            }
        }

        if ($this->config[self::NOISE_IMG]) {
            $retName = str_replace('.', '_noise.', $this->fileName);
            $this->createImg($bin, $retName);
        }

        return $bin;
    }

    private function arrayTransposition(array $arr):array {

        if (!($arr && $arr[0])) {
            return [];
        }
        $ret = [];
        $col = \count($arr);
        $row = \count($arr[0]);

        for ($i=0; $i<$col; $i++) {
            for($j=0;$j<$row;$j++) {
                $ret[$j][$i] = $arr[$i][$j];
            }
        }
        return $ret;
    }

    private function reduceSide(array $bin):array {
        if (empty($bin) || !$this->config[self::REDUCE_SIDE] ) {
            return $bin;
        }
        $dealFunc = function(&$bin, $background) {
            $size = \count($bin);
            for($i=0; $i<$size; $i++) {
                if(\array_sum($bin[$i]) == \count($bin[$i]) * $background ) {
                    unset($bin[$i]);
                } else {
                    break;
                }
            }

            for($i=$size-1; $i>=0; $i--) {
                if(\array_sum($bin[$i]) == \count($bin[$i]) * $background ) {
                    unset($bin[$i]);
                } else {
                    break;
                }
            }
        };
        $dealFunc($bin, $this->config[self::BACKGROUND]);
        $bin = $this->arrayTransposition(array_values($bin));
        $dealFunc($bin, $this->config[self::BACKGROUND]);
        $bin = $this->arrayTransposition(array_values($bin));
        return array_values($bin);
    }

    private function splitRow(array $bin):array {
        $mark = 0;
        $ret = $tmp = [];

        foreach ($bin as $k=>$v) {

            $sum = array_sum($v);
            $cnt = \count($v);

            if ($this->config[self::BACKGROUND]) {
                if( $mark == 0 ) {
                    if($sum != $cnt) {
                        $mark = 1;
                        $tmp[] = $v;
                    }
                } else {
                    if($sum < $cnt) {
                        $tmp[] = $v;
                    } else {
                        $ret[] = $tmp;
                        $tmp = [];
                        $mark = 0;
                    }
                }
            } else {
                if( $mark == 0 ) {
                    if($sum != 0) {
                        $mark = 1;
                        $tmp[] = $v;
                    }
                } else {
                    if($sum > 0) {
                        $tmp[] = $v;
                    } else {
                        $ret[] = $tmp;
                        $tmp = [];
                        $mark = 0;
                    }
                }

            }
        }

        $tmp && $ret[] = $tmp;
        return $ret;
    }

    private function createImg(array $bin, string $fileName) {
        $img = imagecreate(count($bin[0]), count($bin));
        $rgb = array(
            imagecolorallocate($img, 255, 255, 255),
            imagecolorallocate($img, 0, 0, 0)
        );

        $x = $y = 0;
        foreach($bin as $row){
            do{
                imagesetpixel($img, $x, $y, $rgb[$row[$x]]);
            }while(isset($row[++$x]));
            $x = 0;
            $y++;
        }

        switch (strtolower($this->imageType)) {
            case 'png':
                imagepng($img, $fileName);
                break;
            default :
                imagejpeg($img, $fileName);
                break;
        }
    }

    private function saveData(array $where, array $data):bool {
        $data['update_at'] = date('Y-m-d H:i:s');
        $db = db::connect(self::DB);
        if($db->table(self::TB)->getOne($where)) {
            $ret = $db->table(self::TB)->update($where, $data);
        } else {
            $data['create_at'] = date('Y-m-d H:i:s');
            $ret = $db->table(self::TB)->insert(array_merge($where, $data));
        }
        return (bool)$ret;
    }

    private function getData(array $where):array {
        $ret = db::connect(self::DB)->table(self::TB)->getOne($where);
        db::connect(self::DB)->table(self::TB)->incr($where, 'weight');
        return \is_array($ret) ? $ret : [];
    }
}

数据存储结构是:

CREATE TABLE `verification_code` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `key` varchar(32) NOT NULL COMMENT 'md5的key',
  `val` varchar(10) DEFAULT '' COMMENT '对应值',
  `mark` varchar(255) DEFAULT '' COMMENT '标记位',
  `weight` int(5) DEFAULT '1' COMMENT '权重',
  `filename` varchar(255) DEFAULT NULL COMMENT '对应文件路径',
  `img` varchar(255) DEFAULT NULL COMMENT '源文件路径',
  `create_at` datetime DEFAULT NULL COMMENT '第一次采集时间',
  `update_at` datetime DEFAULT NULL COMMENT '最近一次采集时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

破解:


$url = '某二维码获取链接';
$config = [
    Recognition::BACKGROUND => 1,
    Recognition::MARK => 'ttkdex',
    Recognition::INIT_IMG => true,
//    Recognition::BIN_IMG => true,
//    Recognition::CRUX => 0,
//    Recognition::REDUCE_SIDE => true,
//    Recognition::REMOVE_NOISE => false,
//    Recognition::NOISE_IMG => false,
//    Recognition::NOISE_AREA => 9,
//    Recognition::NOISE_WEIGHT => 0.1,
    Recognition::SPLIT_IMG => true,
];
$ret = Recognition::exec($url, $config);
print_r($ret);

获取到的验证码原图为:

原图

处理之后的图片为:

b 3 5 2

存储记录为:

存储记录

注意 这里的val字段是采集之后人工识别的, 在不断的采集记录过程中, 按照权重,从高到低人工识别标记, 考虑到每个网站所用的字符元素样式其实有限, 所以采集的越多,越久,识别成功率越高。
当然这里也可以使用ocr的api来做识别, 切割之后的字符通常识别率也很高。但是基于前面说到的情景,对单个的站点,依靠人工也不会耗费太多精力。

识别结果为:

Array
(
    [0] => b
    [1] => 3
    [2] => 5
    [3] => 2
)

补充

对于有粘连的验证码,上面的方法是不行的, 例如:

http://www.php.cn/captcha.html?t=1559025014690

开始想根据页面的元素的个数来分离,实现后发现不可行, 因为元素倾斜之后无法再同一行进行计算。 还有一个思路就是根据不同元素的颜色的不同,所以灰度值也是不同的, 降噪之后根据灰度值进行填充,达到分离的目的,这个应该是可行的。 后续有时间再补充。

标签: none

添加新评论