前面通过各类去除噪点、干扰线,验证码图片如今已经只有两个部分,若是pixel为白就是背景,若是pixel为黑就为字符。正如前面流畅所提到的同样,为了字符的识别,这里须要将图片上的字符一个一个“扣”下来,获得单个的字符,接下来再进行OCR识别。php
字符分割能够说是图像验证码识别最关键的一步,由于分割的正确与否直接关系到最后的结果,若是4个字符分割成了3个,即使后面的识别算法识别率达到100%,结果也是错的。固然,前面预处理若是作得够好,干扰因素可以有效的去除,而没有影响到字符的pixel,那么分割来说要容易得多。反过来,若是前面的干扰因素都没有去除掉,那么分割出来的可能就不是字符了。算法
字符的粘连是分割的难点,这一点也能够做为验证码安全系数的标准,若是验证码上的几个字符彻底是分开的,那么能够保证字符分割成功率百分之百,这样验证码破解的难度就下降了不少,好比下面的字符:安全

这个就是CSDN的验证码,通过二值化和降噪获得的图片,能够看到这里图片已经很是干净,没有一点多余的信息,字符之间没有重叠的部分,分割起来毫无难度。ide
固然,大多数IT巨头的网页验证码里地字符都是粘连在一块儿的,好比谷歌的验证码:spa

谷歌的验证码不只粘连成都很大,并且字符扭曲地也特别厉害,因此破解起来那是难度很是大了对象
至于图片分割,我再这里介绍两种简单地方法。图片
1、 泛水填充法ci
泛水填充法在前面降噪的地方就提到过,主要思路仍是连通域的思想。对于相互之间没有粘连的字符验证码,直接对图片进行扫描,遇到一个黑的pixel就对其进行泛水填充,全部与其连通的字符都被标记出来,所以一个独立的字符就可以找到了。这个方法优势是效率高,时间复杂度是O(N),N为像素的个数;并且不用考虑图片的大小、相邻字符间隔以及字符在图片中得位置等其余任何因素,任何验证码图片只要字符相互是独立的,不须要对其余任何阀值作预处理,直接就操做;用这种方法分割正确率很是高,几乎不会出现分割错误的状况。可是缺点也很致命:那就是字符之间必须彻底隔离,没有粘连的部分,不然会将两个字符误认为一个字符。get
代码以下:it
[cpp] view plain copy
- for (i = 0; i < nWidth; ++i)
- for (j = 0; j < nHeight; ++j)
- {
- if ( !getPixel(i,j) )
- {
- //FloodFill each point in connect area using different color
- floodFill(m_Mat,cvPoint(i,j),cvScalar(color));
- color++;
- }
- }
-
- int ColorCount[256] = { 0 };
- for (i = 0; i < nWidth; ++i)
- {
- for (j = 0; j < nHeight; ++j)
- {
- //caculate the area of each area
- if (getPixel(i,j) != 255)
- {
- ColorCount[getPixel(i,j)]++;
- }
- }
- }
- //get rid of noise point
- for (i = 0; i < nWidth; ++i)
- {
- for (j = 0; j < nHeight; ++j)
- {
- if (ColorCount[getPixel(i,j)] <= nMin_area)
- {
- setPixel(i,j,WHITE);
- }
- }
- }
-
- int k = 1;
- int minX,minY,maxX,maxY;
- vector<Image> vImage;
- while( ColorCount[k] )
- {
- if (ColorCount[k] > nMin_area)
- {
- minX = minY = 100;
- maxX = maxY = -1;
- //get the rect of each charactor
- for (i = 0; i < nWidth; ++i)
- {
- for (j = 0; j < nHeight; ++j)
- {
- if(getPixel(i,j) == k)
- {
- if(i < minX)
- minX = i;
- else if(i > maxX)
- maxX = i;
- if(j < minY)
- minY = j;
- else if(j > maxY)
- maxY = j;
- }
- }
- }
- //copy to each standard mat
- Mat *ch = new Mat(HEIGHT,WIDTH,CV_8U,WHITE);
- int m,n;
- m = (WIDTH - (maxX-minX))/2;
- n = (HEIGHT - (maxY-minY))/2;
- for (i = minX; i <= maxX; ++i)
- {
- for (j = minY; j <= maxY; ++j)
- {
- if(getPixel(i,j) == k)
- {
- *(ch->data+ch->step[0]*(n+j-minY)+m+(i-minX)) = BLACK;
- }
- }
- <span style="white-space:pre"> </span>}
这段代码就是使用泛水填充法,每次扫到一个连通域就把连通域全部的pixel的灰度值改成0-255之间的一个值,好比第一个是254,下一个是253...接下来再对每个灰度值(即每个连通域)的pixel出现的X,Y坐标的最大、最小的值记录下来,这样就获得了每一个字符的最小外包矩形,最后将这个最小外包矩形所有复制到固定大小的一个单独的Mat对象中,这个对象存储的就是一个固定分辨率大小的表现为单独字符的图片。
分割的效果能够见下面的图:


能够看到,分割效果很是好。
2、X像素投影法
对于粘连的字符,也并不是没有方法分割。一个方法就是将两个粘连的验证码一刀切开,从哪里切?固然是从粘连的薄弱的地方切。前面提到过图片的像素就像一个二维的矩阵,对每个x值,统计全部x值为这个值的pixel中黑色的数目,直观来说就是统计每一条竖线上黑色点的数目。显而易见的是,若是这一条线为背景,那么这一条线确定都是白色的,那么黑色点的数目为0,若是一条竖线通过字符,那么这条竖线上的黑色点数目确定很多。
对于彻底独立的两个字符之间,确定有黑色点数目为0的竖线,可是若是粘连,那么不会有黑色点数为0的竖线存在,可是字符粘连最薄弱的地方必定是黑色点数目最少的那条竖线,所以切就要从这个地方切。
在代码的实现的过程当中,能够先从左到右扫描一遍,统计投影到每一个X值的黑色点的数目,而后设定一个阀值范围,这个阀值大概就是一个字符的宽度。从左到右,先找到第一个x黑色点投影不为0的x值,而后在这个x值加上大概一个字符宽度的大小找到x投影数目最小的x值,这两个x值分割出来就是一个字符了。
这个方法的特色就是可以分割粘连的字符,可是缺点就是容易分割不干净,可能会出现分割错误的状况,另外就是须要提供相应的阀值。
代码以下:
[cpp] view plain copy
- void Image::xProjectDivide(int nMin_thsd,int nMax_thsd)
- {
- int i,j;
- int nWidth = getWidth();
- int nHeight = getHeight();
- int *xNum = new int[nWidth];
-
- //inital the x-projection-num
- memset(xNum,0,nWidth*sizeof(int));
-
- //compute the black pixel num in X coordinate
- for (j = 0; j < nHeight; ++j)
- for (i = 0; i < nWidth; ++i)
- {
- if ( getPixel(i,j) == BLACK ) xNum[i]++;
- }
- /*-----------------show x project map-------------------*/
- Mat xProjectResult(nHeight/2,nWidth,CV_8U,Scalar(WHITE));
-
- for (i = 0; i < xProjectResult.cols-1; ++i)
- {
- int begin,end;
- if(xNum[i] > xNum[i+1])
- {
- begin = xNum[i+1];
- end = xNum[i];
- }
- else {
- begin = xNum[i];
- end = xNum[i+1];
- }
- for (j = begin; j <= end; ++j)
- {
- *(xProjectResult.data+xProjectResult.step[0]*(nHeight/2 - j - 1)+i) = BLACK;
- }
- }
-
- std::cout << "The porject of BLACK pixel in X coordinate is in the window" << std::endl;
- namedWindow("xProjectResult");
- imshow("xProjectResult",xProjectResult);
- waitKey();
- /*-----------------show x project map-------------------*/
-
- /*-------------------divide the map---------------------*/
- vector<int> vPoint;
- int nMin,nIndex;
- if (xNum[0] > BOUNDRY_NUM) vPoint.push_back(0);
- for(i = 1;i < nWidth-1 ;)
- {
- if( xNum[i] < BOUNDRY_NUM)
- {
- i++;
- continue;
- }
- vPoint.push_back(i);
- //find minimum between the min_thsd and max_thsd
- nIndex = i+nMin_thsd;
- nMin = xNum[nIndex];
- for(j = nIndex;j<i+nMax_thsd;j++)
- {
- if (xNum[j] < nMin)
- {
- nMin = xNum[j];
- nIndex = j;
- }
- }
- vPoint.push_back(nIndex);
- i = nIndex + 1;
- }
- if (xNum[nWidth-1] > BOUNDRY_NUM) vPoint.push_back(nWidth-1);
-
- //save the divided characters in map vector
- int ch_width = nWidth / (vPoint.size()/2) + EXPAND_WIDTH;
- vector<Image> vImage;
- for (j = 0; j < (int)vPoint.size(); j += 2)
- {
- Mat *mCharacter = new Mat(nHeight,ch_width,CV_8U,Scalar(WHITE));
- for (i = 0; i < nHeight; ++i)
- memcpy(mCharacter->data+i*ch_width+EXPAND_WIDTH/2,m_Mat.data+i*nWidth+vPoint.at(j),vPoint.at(j+1)-vPoint.at(j));
- Image::ContoursRemoveNoise(*mCharacter,2.5);
- Mat *mResized = new Mat(SCALE,SCALE,CV_8U);
- resize(*mCharacter,*mResized,cv::Size(SCALE,SCALE),0,0,CV_INTER_AREA);
- Image iCh(*mResized);
- vImage.push_back(iCh);
- delete mCharacter;
- }
- //show divided characters
- char window_name[12];
- for (i = 0; i < (int)vImage.size(); ++i)
- {
- sprintf(window_name,"Character%d",i);
- //vImage.at(i).NaiveRemoveNoise(1.0f);
- vImage.at(i).ShowInWindow(window_name);
- }
-
- delete []xNum;
- }
代码首先统计每一个x坐标对应的黑色点的数目,而后根据参数提供的阀值,找到字符之间的分割点,而后将分割点入栈,若是有4个字符,就入栈8个边界。最后每次出栈两个x值,将这两个x值之间的全部像素都拷贝到一个新的Mat对象中去,这样就获得了一个独立的字符图片。
下面给出X像素投影法的运行结果图:


