首先推荐几篇有关验证码识别的文章,觉得不错
一、思路
碰见一个验证码,如果我们想要识别它,我们需要的是做什么呢?
我们先观察几个验证码............
我们用人眼去观察,会很显然的认出验证码所包含的字符,那么人眼的“识别机理”是什么呢?
大概是验证码图片字符的背景的颜色区别吧,试想,如果字符和背景没有颜色区别,我们能够判断验证码吗,很显然不能。
所以,我们就可以从人出发。
先从图片的颜色着手,即图片的RGB信息。
RGB色彩模式是工业界的一种颜色标准,是通过对红(R)、绿(G)、蓝(B)三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色的,RGB即是代表红、绿、蓝三个通道的颜色,这个标准几乎包括了人类视力所能感知的所有颜色,是目前运用最广的颜色系统之一。
定义函数取得RGB信息
1 //代码本来是一个类,现在拆开来写的,有可能有不严谨的地方,大家可以看得懂就好了 2 3 /* 4 *取得图片路径和图片尺寸 5 */ 6 $this->ImagePath = $Image; 7 $this->ImageSize = getimagesize($Image); 8 9 /* 10 *获取图像标识符,保存到ImageInfo,只能处理bmp,png,jpg图片 11 *ImageCreateFromBmp是我自己定义的函数,最后会给出 12 */ 13 function getInfo(){ 14 $filetype = substr($this->ImagePath,-3); 15 if($filetype == ‘bmp‘){ 16 $this->ImageInfo = $this->ImageCreateFromBmp($this->ImagePath); 17 }elseif($filetype == ‘jpg‘){ 18 $this->ImageInfo = imagecreatefromjpeg($this->ImagePath); 19 }elseif($filetype == ‘png‘){ 20 $this->ImageInfo = imagecreatefrompng($this->ImagePath); 21 } 22 } 23 24 /*获取图片RGB信息*/ 25 function getRgb(){ 26 $rgbArray = array(); 27 $res = $this->ImageInfo; 28 $size = $this->ImageSize; 29 $wid = $size[‘0‘]; 30 $hid = $size[‘1‘]; 31 for($i=0; $i < $hid; ++$i){ 32 for($j=0; $j < $wid; ++$j){ 33 $rgb = imagecolorat($res,$j,$i); 34 $rgbArray[$i][$j] = imagecolorsforindex($res, $rgb); 35 } 36 } 37 return $rgbArray; 38 }
二、二值化
因为人眼可以分别出验证码,所以验证码的RGB信息就会有一定的特点,这时候需要我们观察一下,直接打印RGB数组是不好观察的…………,好多数啊
在php实现验证码的识别(初级篇)中,作者的判断依据是
无论验证数字颜色如何变化,该数字的 RGB 值总有一个值小于 125
我们先获取他的灰度,再判断
1 /* 2 *获取灰度信息 3 */ 4 function getGray(){ 5 $grayArray = array(); 6 $size = $this->ImageSize; 7 $rgbarray = $this->getRgb(); 8 $wid = $size[‘0‘]; 9 $hid = $size[‘1‘]; 10 for($i=0; $i < $hid; ++$i){ 11 for($j=0; $j < $wid; ++$j){ 12 $grayArray[$i][$j] = (299*$rgbarray[$i][$j][‘red‘]+587*$rgbarray[$i][$j][‘green‘]+144*$rgbarray[$i][$j][‘blue‘])/1000; 13 } 14 } 15 return $grayArray; 16 }
然后我们根据灰度信息,打印图片,注意不是打印灰度信息
1 /*根据灰度信息打印图片*/ 2 function printByGray(){ 3 $size = $this->ImageSize; 4 $grayArray = $this->getGray(); 5 $wid = $size[‘0‘]; 6 $hid = $size[‘1‘]; 7 for($k=0;$k<25;$k++){ 8 echo $k."\n"; 9 for($i=0; $i < $hid; ++$i){ 10 for($j=0; $j < $wid; ++$j){ 11 if($grayArray[$i][$j] < $k*10){ 12 echo ‘■‘; 13 }else{ 14 echo ‘□‘; 15 } 16 } 17 echo "|\n"; 18 } 19 echo "---------------------------------------------------------------------------------------------------------------\n"; 20 } 21 22 }
注意到,从$grayArray[$i][$j] < 80就会有显然的输出,我们观察选择了一个恰当的阈值,得到一个0101的数组,我们已经将我们的图片转化为了字符(1)和背景(0),即二值化。
1 /* 2 *根据自定义的规则,获取二值化二维数组 3 *@return 图片高*宽的二值数组(0,1) 4 */ 5 function getErzhi(){ 6 $erzhiArray = array(); 7 $size = $this->ImageSize; 8 $grayArray = $this->getGray(); 9 $wid = $size[‘0‘]; 10 $hid = $size[‘1‘]; 11 for($i=0; $i < $hid; ++$i){ 12 for($j=0; $j <$wid; ++$j){ 13 if( $grayArray[$i][$j] < 90 ){ 14 $erzhiArray[$i][$j]=1; 15 }else{ 16 $erzhiArray[$i][$j]=0; 17 } 18 } 19 } 20 return $erzhiArray; 21 }
三、去除噪点
但是我们发现有一些小点影响了我们的判断
我们可以注意到这些事干扰噪点,但是如果我们是机器的话,我们如何判断这些点是否是字符呢?
所以接下来,我们需要将这些字符去除。
我们判断,如果一个黑点的上下左右的八个点全部是白,我们就认为它是噪点,并予以清除,赋为白
1 /* 2 *二值化图片降噪 3 *@param $erzhiArray二值化数组 4 */ 5 function reduceZao($erzhiArray){ 6 $data = $erzhiArray; 7 $gao = count($erzhiArray); 8 $chang = count($erzhiArray[‘0‘]); 9 10 $jiangzaoErzhiArray = array(); 11 12 for($i=0;$i<$gao;$i++){ 13 for($j=0;$j<$chang;$j++){ 14 $num = 0; 15 if($data[$i][$j] == 1) 16 { 17 // 上 18 if(isset($data[$i-1][$j])){ 19 $num = $num + $data[$i-1][$j]; 20 } 21 // 下 22 if(isset($data[$i+1][$j])){ 23 $num = $num + $data[$i+1][$j]; 24 } 25 // 左 26 if(isset($data[$i][$j-1])){ 27 $num = $num + $data[$i][$j-1]; 28 } 29 // 右 30 if(isset($data[$i][$j+1])){ 31 $num = $num + $data[$i][$j+1]; 32 } 33 // 上左 34 if(isset($data[$i-1][$j-1])){ 35 $num = $num + $data[$i-1][$j-1]; 36 } 37 // 上右 38 if(isset($data[$i-1][$j+1])){ 39 $num = $num + $data[$i-1][$j+1]; 40 } 41 // 下左 42 if(isset($data[$i+1][$j-1])){ 43 $num = $num + $data[$i+1][$j-1]; 44 } 45 // 下右 46 if(isset($data[$i+1][$j+1])){ 47 $num = $num + $data[$i+1][$j+1]; 48 } 49 } 50 51 if($num < 1){ 52 $jiangzaoErzhiArray[$i][$j] = 0; 53 }else{ 54 $jiangzaoErzhiArray[$i][$j] = 1; 55 } 56 } 57 } 58 return $jiangzaoErzhiArray; 59 60 }
我们发现噪点消失了。
四、分割
这个时候,我们就需要对单一数字字母进行操作了,我们先将数字提取出来。
有些验证码字符相连,特别难!!!
我们分别从左到右,从右到左,从上到下,从下到上,进行扫描,去除白点,找到边框。
1 /* 2 *归一化处理,针对一个个的数字,即去除字符周围的白点 3 *@param $singleArray 二值化数组 4 */ 5 function getJinsuo($singleArray){ 6 $dianCount = 0; 7 $rearr = array(); 8 9 $gao = count($singleArray); 10 $kuan = count($singleArray[‘0‘]); 11 12 $dianCount = 0; 13 $shangKuang = 0; 14 $xiaKuang = 0; 15 $zuoKuang = 0; 16 $youKuang = 0; 17 //从上到下扫描 18 for($i=0; $i < $gao; ++$i){ 19 for($j=0; $j < $kuan; ++$j){ 20 if( $singleArray[$i][$j] == 1){ 21 $dianCount++; 22 } 23 } 24 if($dianCount>1){ 25 $shangKuang = $i; 26 $dianCount = 0; 27 break; 28 } 29 } 30 //从下到上扫描 31 for($i=$gao-1; $i > -1; $i--){ 32 for($j=0; $j < $kuan; ++$j){ 33 if( $singleArray[$i][$j] == 1){ 34 $dianCount++; 35 } 36 } 37 if($dianCount>1){ 38 $xiaKuang = $i; 39 $dianCount = 0; 40 break; 41 } 42 } 43 //从左到右扫描 44 for($i=0; $i < $kuan; ++$i){ 45 for($j=0; $j < $gao; ++$j){ 46 if( $singleArray[$j][$i] == 1){ 47 $dianCount++; 48 } 49 } 50 if($dianCount>1){ 51 $zuoKuang = $i; 52 $dianCount = 0; 53 break; 54 } 55 } 56 //从右到左扫描 57 for($i=$kuan-1; $i > -1; --$i){ 58 for($j=0; $j < $gao; ++$j){ 59 if( $singleArray[$j][$i] == 1){ 60 $dianCount++; 61 } 62 } 63 if($dianCount>1){ 64 $youKuang = $i; 65 $dianCount = 0; 66 break; 67 } 68 } 69 for($i=0;$i<$xiaKuang-$shangKuang+1;$i++){ 70 for($j=0;$j<$youKuang-$zuoKuang+1;$j++){ 71 $rearr[$i][$j] = $singleArray[$shangKuang+$i][$zuoKuang+$j]; 72 } 73 } 74 return $rearr; 75 }
然后从左到右扫描,找到字符的分割
返回三维数组,每一维就是一个字符。
1 /* 2 *切割成三维数组,每个小数字在一个数组里面 3 *只适用四个数字一起的数组 4 *@param 经过归一化处理的二值化数组 5 */ 6 function cutSmall($erzhiArray){ 7 $doubleArray = array(); 8 $jieZouyou = array(); 9 10 $gao = count($erzhiArray); 11 $kuan = count($erzhiArray[‘0‘]); 12 13 $jie = 0; 14 $s = 0; 15 $jieZouyou[$s] = 0; 16 $s++; 17 //从左到右扫描 18 19 for($i=0; $i < $kuan;){ 20 for($j=0; $j < $gao; ++$j){ 21 $jie = $jie + $erzhiArray[$j][$i]; 22 } 23 //如果有一列全部是白,设置$jieZouyou,并且跳过中间空白部分 24 if($jie == 0){ 25 $jieZouyou[$s] = $i+1; 26 do{ 27 $n = ++$i; 28 $qian = 0; 29 $hou = 0; 30 for($m=0; $m < $gao; ++$m){ 31 $qian = $qian + $erzhiArray[$m][$n]; 32 $hou = $hou + $erzhiArray[$m][$n+1]; 33 } 34 $jieZouyou[$s+1] = $n+1; 35 } 36 //当有两列同时全部为白,说明有间隙,循环,知道间隙没有了 37 while($qian == 0 && $hou == 0); 38 $s+=2; 39 $i++; 40 }else{ 41 $i++; 42 } 43 44 $jie = 0; 45 } 46 $jieZouyou[] = $kuan; 47 //极端节点数量,(应该是字符个数)*2 48 $jieZouyouCount = count($jieZouyou); 49 50 for($k=0;$k<$jieZouyouCount/2;$k++){ 51 for($i=0; $i < $gao; $i++){ 52 for($j=0; $j < $jieZouyou[$k*2+1]-$jieZouyou[$k*2]-1; ++$j){ 53 $doubleArray[$k][$i][$j] = $erzhiArray[$i][$j+$jieZouyou[$k*2]]; 54 } 55 } 56 57 } 58 return $doubleArray; 59 }
五、倾斜调整
我们发现第三个9有一点倾斜,
我们需要将倾斜的图片“正”过来
人怎么处理的呢,先眼睛观察“倾斜了多少度”,然后把图片扭过来多少度,并且观察->负反馈->大脑传递扭转角度时刻在发生,最后图片就“正”过来了。
人是怎么观察“倾斜”的,以上面的“2”做例子,可能是右上方(左下方)的黑色比左上方(右下方)的多?
我们建立X轴正向向下,Y轴向右的直角坐标系
我们计算每一层的黑点的分布中点坐标,得到一系列离散点,计算这些点所在的直线(线性回归方程的计算,),公式y = b*x+a,
竟然有用到这个公式的一天!!!
大概就是一条倾斜的直线了,通过直线计算直线倾斜角度,然后转这么多的角度,图片应该就“正”了吧。
其中a,b的计算如下
1 /* 2 *定义求线性回归A和B的函数 3 *@param $zuobiaoArray坐标的三维数组 4 */ 5 function getHuigui($zuobiaoArray){ 6 $y8 = 0; 7 $x8 = 0; 8 $x2 = 0; 9 $xy = 0; 10 $geshu = count($zuobiaoArray); 11 for($i=0;$i<$geshu;$i++){ 12 $y8 = $y8+$zuobiaoArray[$i][‘y‘]; 13 $x8 = $x8+$zuobiaoArray[$i][‘x‘]; 14 $xy = $xy+$zuobiaoArray[$i][‘y‘]*$zuobiaoArray[$i][‘x‘]; 15 $x2 = $x2 + $zuobiaoArray[$i][‘x‘]*$zuobiaoArray[$i][‘x‘];; 16 } 17 $y8 = $y8/$geshu; 18 $x8 = $x8/$geshu; 19 20 $b = ($xy-$geshu*$y8*$x8)/($x2-$geshu*$x8*$x8); 21 $a = $y8-$b*$x8; 22 $re[‘a‘] = $a; 23 $re[‘b‘] = $b; 24 return $re; 25 //y = b * x + a 26 }
怎么转角?
1、可以直接对图片进行操作,但是发现有比较大的失真,就没有继续了。
2、或者,对黑点白点的坐标进行操作……
这就是三角函数了,好长时间不碰三角函数,都差点忘记了。
定义函数
1 /* 2 *定义转化坐标的函数 3 *@param $x x坐标即$i 4 *@param $y y坐标,即j 5 *@param $b 线性回归方程的b参数 6 */ 7 function getNewZuobiao($x,$y,$b){ 8 if($x == 0){ 9 if($y>0){ 10 $xianJiao = M_PI/2; 11 }elseif($y<0){ 12 $xianJiao = -M_PI/2; 13 }else{ 14 $p[‘x‘] = 0; 15 $p[‘y‘] = 0; 16 return $p; 17 } 18 }else{ 19 $xianJiao = atan($y/$x); 20 } 21 $jiao =$xianJiao-atan($b); 22 $chang = sqrt($x*$x+$y*$y); 23 $p[‘x‘] = $chang*cos($jiao); 24 $p[‘y‘] = $chang*sin($jiao); 25 return $p; 26 }
转角吧
1 /* 2 *对【单个】数字的二值化二维数组进行倾斜调整 3 *@param $singleArray 高*宽的二值数组(0,1) 4 */ 5 function singleSlopeAdjust($singleErzhiArray){ 6 $slopeArray = array(); 7 $gao = count($singleErzhiArray); 8 $chang = count($singleErzhiArray[‘0‘]); 9 10 //初始化$slopeArray 11 for($i=0;$i<$gao*4;$i++){ 12 for($j=0;$j<$chang*4;$j++){ 13 $slopeArray[$i][$j] = 0; 14 } 15 } 16 17 //初始化中心坐标(是数组的下标) 18 $centerXfoalt = ($gao-1)/2; 19 $centerYfoalt = ($chang-1)/2; 20 $centerX = ceil($centerXfoalt); 21 $centerY = ceil($centerYfoalt); 22 23 //初始化图片倾斜诶角度 24 /*斜率的计算!!!!!,回归方程*/ 25 //从上到下扫描,计算中点,求得一串坐标($i,$ava) 26 for($i=0;$i<$gao;$i++){ 27 $Num = 0; 28 $Amount = 0; 29 for($j=0;$j<$chang;$j++){ 30 if($singleErzhiArray[$i][$j] == 1){ 31 $Num = $Num+$j; 32 $Amount++; 33 } 34 } 35 if($Amount == 0){ 36 $Ava[$i] = $chang/2; 37 }else{ 38 $Ava[$i] = $Num/$Amount; 39 } 40 } 41 42 43 //计算线性回归方程的b与a 44 $zuo = array(); 45 for($j=0;$j<count($Ava);$j++){ 46 $zuo[$j][‘x‘] = $j; 47 $zuo[$j][‘y‘] = $Ava[$j]; 48 } 49 $res = $this->getHuigui($zuo); 50 $zuoB = $res[‘b‘]; 51 52 53 for($i=0;$i<$gao;$i++){ 54 for($j=0;$j<$chang;$j++){ 55 if($singleErzhiArray[$i][$j] == 1){ 56 $splodeZuobiao = $this->getNewZuobiao($i,$j,$zuoB); 57 $splodeX = $splodeZuobiao[‘x‘]; 58 $splodeY = $splodeZuobiao[‘y‘]; 59 $slopeArray[$splodeX+$gao][$splodeY+$chang] = 1; 60 } 61 } 62 } 63 64 //将预处理的数组空白清理 65 $slopeArray = $this->getJinsuo($slopeArray); 66 return $slopeArray; 67 }
看到正了一些
六、统一大小
上文中因为各种操作,每个字符大小不一,我们需要统一大小
七、特征值的建立
有很多方法
1、逐像素特征提取法
这是一种最简单的特征提取方法。它可以对图像进行逐行逐列的扫描,当遇到黑色像素时取其特征值为1,遇到白色像素时取其特征值为0,这样当扫描结束后就获得一个维数与图像中的像素点的个数相同的特征向量矩阵。
这种方法提取的信息量最大,但是它的缺点也很明显,就是适应性不强。
2、骨架特征提取法
两幅图像由于它们的线条的粗细不同,使得两幅图像差别很大,但是将它们的线条进行细化后,统一到相同的宽度,如一个像素宽时,这是两幅图像的差距就不那么明显。利用图形的骨架作为特征来进行数码识别,就使得识别有了一定的适应性。一般使用细化的方法来提取骨架,细化的算法有很多,如Hilditch算法、Rosenfeld算法等。对经过细化的图像利用EveryPixel函数进行处理就可以得到细化后图像的特征向量矩阵。骨架特征提取的方法对于线条粗细不同的数码有一定的适应性,但是图像一旦出现偏移就难以识别。
3、微结构法
微结构法将图像分为几个小块,统计每个小块的像素分布。本文提取出汉字的39个特征,存储在数组f[0]~f[38]中。具体算法可分为四步:
步骤一:把字符平均分成9份,如图4.1所示,给每一份编号如图4.2,统计每一份内黑色像素的个数,存储在数字tz[0]~tz[9]中,统计在行方向和列方向上每一份内的黑色像素个数和与之相邻的一份内黑色像素个数的比值作为一个特征,例如:行方向上提取特征f[0]=tz[1]/
tz[0],f[1]=tz[2]/ tz[1],f[2]=tz[0]/ tz[2],…,f[8]=tz[6]/ tz[8];列方向上f[9]=tz[3]/
tz[0],f[10]=tz[6]/ tz[3],f[11]=tz[0]/ tz[6],…,f[17]=tz[2]/ tz[8],共18个特征。
步骤二:把字符横向分成三份,如图4.3所示,统计每一份内的黑色像素个数,每一份内的黑色像素个数与前一份内黑色像素个数的比值作为一个特征,f[18]=tz[10]/
tz[9],f[19]=tz[11]/ tz[10],f[20]=tz[9]/
tz[11];把字符纵向分成三份,如图4.4所示,统计每一份内的黑色像素个数,每一份内的黑色像素个数与前一份内黑色像素个数的比值作为一个特征,f[21]=tz[13]/
tz[12],f[22]=tz[14]/ tz[13],f[23]=tz[12]/ tz[14];共六个特征。
步骤三:如图4.5,在竖直方向上找出三列,统计在该列中跳变点的个数,即相邻点像素值从0变到255的次数,共三个特征,记为f[24],f[25],f[26];在水平方向上找出三行列,统计在该行中跳变点的个数,即相邻点象素值从0变到255的次数,共三个特征,记为f[27],f[28],f[29]。
图4.5
步骤四:把每一份内黑色象素的个数tz[0]~tz[9],作为9个特征,记为:f[30]~f[38]。
这样得到汉字的共39个特征,根据这些特征就可以区分每个车牌汉字,进行识别。
我们使用最简单的逐像素特征提取法。
多多增加数据库,识别率会增加的
八、识别验证码
对于一个新的验证码,进行上文操作,然后对比数据库就可以了
1 /* 2 *进行匹配 3 *@param $Image 图片路径 4 */ 5 public function run($Image){ 6 $data = array(‘‘,‘‘,‘‘,‘‘); 7 $result=""; 8 $bilu = ‘‘; 9 $maxarr = ‘‘; 10 11 //提取特征 12 $this->prepare($Image); 13 $yuanshi = $this->getErzhi(); 14 $yijijiangzao = $this->reduceZao($yuanshi); 15 $small = $this->cutSmall($yijijiangzao); 16 for($k=0;$k<4;$k++){ 17 $tianchong = $this->tianChong($small[$k]); 18 $tiaozhenjiaodu = $this->singleSlopeAdjust($tianchong); 19 $tongyidaxiao = $this->tongyiDaxiao($tiaozhenjiaodu); 20 for($i=0;$i<20;$i++){ 21 for($j=0;$j<20;$j++){ 22 $data[$k] .= $tongyidaxiao[$i][$j]; 23 } 24 } 25 } 26 27 // 进行关键字匹配 28 foreach($data as $numKey => $numString) 29 { 30 31 $max = 0; 32 $num = 0; 33 foreach($this->Keys as $value => $key) 34 { 35 similar_text($value, $numString,$percent); 36 if($percent > $max) 37 { 38 $max = $percent; 39 $num = $key; 40 $zim = $value; 41 } 42 if($max>95){ 43 break; 44 } 45 } 46 $result .=$num; 47 $maxarr[] = $max; 48 } 49 // 查找最佳匹配数字 50 $re = $maxarr; 51 $re[] = $result; 52 return $re; 53 //return $result.‘|max|一:‘.$maxarr[‘0‘].‘|二:‘.$maxarr[‘1‘].‘|三:‘.$maxarr[‘2‘].‘|四:‘.$maxarr[‘3‘]; 54 }
试试:
PHP之验证码识别