前面通過各類去除噪點、干擾線,驗證碼圖片如今已經只有兩個部分,若是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像素投影法的運行結果圖:


