您的位置:首页 > 编程语言 > C语言/C++

图像验证码识别(七)——字符分割

2016-03-01 14:37 555 查看
前面经过各种去除噪点、干扰线,验证码图片现在已经只有两个部分,如果pixel为白就是背景,如果pixel为黑就为字符。正如前面流畅所提到的一样,为了字符的识别,这里需要将图片上的字符一个一个“扣”下来,得到单个的字符,接下来再进行OCR识别。

字符分割可以说是图像验证码识别最关键的一步,因为分割的正确与否直接关系到最后的结果,如果4个字符分割成了3个,即便后面的识别算法识别率达到100%,结果也是错的。当然,前面预处理如果做得够好,干扰因素能够有效的去除,而没有影响到字符的pixel,那么分割来讲要容易得多。反过来,如果前面的干扰因素都没有去除掉,那么分割出来的可能就不是字符了。

字符的粘连是分割的难点,这一点也可以作为验证码安全系数的标准,如果验证码上的几个字符完全是分开的,那么可以保证字符分割成功率百分之百,这样验证码破解的难度就降低了很多,比如下面的字符:



这个就是CSDN的验证码,经过二值化和降噪得到的图片,可以看到这里图片已经非常干净,没有一点多余的信息,字符之间没有重叠的部分,分割起来毫无难度。

当然,大多数IT巨头的网页验证码里地字符都是粘连在一起的,比如谷歌的验证码:



谷歌的验证码不仅粘连成都很大,而且字符扭曲地也特别厉害,所以破解起来那是难度非常大了

至于图片分割,我再这里介绍两种简单地方法。

一、 泛水填充法

泛水填充法在前面降噪的地方就提到过,主要思路还是连通域的思想。对于相互之间没有粘连的字符验证码,直接对图片进行扫描,遇到一个黑的pixel就对其进行泛水填充,所有与其连通的字符都被标记出来,因此一个独立的字符就能够找到了。这个方法优点是效率高,时间复杂度是O(N),N为像素的个数;而且不用考虑图片的大小、相邻字符间隔以及字符在图片中得位置等其他任何因素,任何验证码图片只要字符相互是独立的,不需要对其他任何阀值做预处理,直接就操作;用这种方法分割正确率非常高,几乎不会出现分割错误的情况。但是缺点也很致命:那就是字符之间必须完全隔离,没有粘连的部分,否则会将两个字符误认为一个字符。

代码如下:

[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对象中,这个对象存储的就是一个固定分辨率大小的表现为单独字符的图片。

分割的效果可以见下面的图:





可以看到,分割效果非常好。

二、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像素投影法的运行结果图:





内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  c++ opencv 验证码识别