如何識別高級的驗證碼

http://sebug.net/paper/pst_WebZine/pst_WebZine_0x02/html/PSTZine_0x02_0x09.html


                          ==Ph4nt0m Security Team==
 
                       Issue 0x02, Phile #0x09 of 0x0A
 

|=---------------------------------------------------------------------------=|
|=-----------------------=[  如何識別高級的驗證碼  ]=------------------------=|
|=---------------------------------------------------------------------------=|
|=---------------------------------------------------------------------------=|
|=----------------------=[      By moonblue333     ]=------------------------=|
|=-------------------=[  <moonblue333_at_hotmail.com>  ]=--------------------=|
|=---------------------------------------------------------------------------=|


一、驗證碼的基本知識

    1. 驗證碼的主要目的是強制人機交互來抵禦機器自動化攻擊的。
    
    2. 大部分的驗證碼設計者並不得要領,不瞭解圖像處理,機器視覺,模式識別,人工智能
的基本概念。
    
    3. 利用驗證碼,可以發財,當然要犯罪:比如招商銀行密碼只有6位,驗證碼形同虛設,計
算機很快就能破解一個有錢的賬戶,很多帳戶是可以網上交易的。
    
    4. 也有設計的比較好的,比如Yahoo,Google,Microsoft等。而國內Tencent的中文驗證
碼雖然難,但算不上好。

二、人工智能,模式識別,機器視覺,圖像處理的基本知識

    1)主要流程:
    
    比如我們要從一副圖片中,識別出驗證碼;比如我們要從一副圖片中,檢測並識別出一張
人臉。 大概有哪些步驟呢?

    1.圖像採集:驗證碼呢,就直接通過HTTP抓HTML,然後分析出圖片的url,然後下載保存就
可以了。 如果是人臉檢測識別,一般要通過視屏採集設備,採集回來,通過A/D轉操作,存爲
數字圖片或者視頻頻。
    
    2.預處理:檢測是正確的圖像格式,轉換到合適的格式,壓縮,剪切出ROI,去除噪音,灰度
化,轉換色彩空間這些。
    
    3.檢測:車牌檢測識別系統要先找到車牌的大概位置,人臉檢測系統要找出圖片中所有
的人臉(包括疑似人臉);驗證碼識別呢,主要是找出文字所在的主要區域。
    
    4.前處理:人臉檢測和識別,會對人臉在識別前作一些校正,比如面內面外的旋轉,扭曲
等。我這裏的驗證碼識別,“一般”要做文字的切割
    
    5.訓練:通過各種模式識別,機器學習算法,來挑選和訓練合適數量的訓練集。不是訓練
的樣本越多越好。過學習,泛化能力差的問題可能在這裏出現。這一步不是必須的,有些識
別算法是不需要訓練的。
    
    6.識別:輸入待識別的處理後的圖片,轉換成分類器需要的輸入格式,然後通過輸出的類
和置信度,來判斷大概可能是哪個字母。識別本質上就是分類。

    2)關鍵概念:
    
    圖像處理:一般指針對數字圖像的某種數學處理。比如投影,鈍化,銳化,細化,邊緣檢測,
二值化,壓縮,各種數據變換等等。
    
    1.二值化:一般圖片都是彩色的,按照逼真程度,可能很多級別。爲了降低計算複雜度,
方便後續的處理,如果在不損失關鍵信息的情況下,能將圖片處理成黑白兩種顏色,那就最好
不過了。
    
    2.細化:找出圖像的骨架,圖像線條可能是很寬的,通過細化將寬度將爲1,某些地方可能
大於1。不同的細化算法,可能有不同的差異,比如是否更靠近線條中間,比如是否保持聯通
行等。
    
    3.邊緣檢測:主要是理解邊緣的概念。邊緣實際上是圖像中圖像像素屬性變化劇烈的地
方。可能通過一個固定的門限值來判斷,也可能是自適應的。門限可能是圖像全局的,也可
能是局部的。不能說那個就一定好,不過大部分時候,自適應的局部的門限可能要好點。被
分析的,可能是顏色,也可能是灰度圖像的灰度。

    機器視覺:利用計算機來模式實現人的視覺。 比如物體檢測,定位,識別。按照對圖像
理解的層次的差別,分高階和低階的理解。

    模式識別:對事物或者現象的某種表示方式(數值,文字,我們這裏主要想說的是數值),
通過一些處理和分析,來描述,歸類,理解,解釋這些事物,現象及其某種抽象。

    人工智能:這種概念比較寬,上面這些都屬於人工智能這個大的方向。簡單點不要過分
學院派的理解就是,把人類的很“智能”的東西給模擬出來協助生物的人來處理問題,特別是
在計算機裏面。

三、常見的驗證碼的破解分析
    
    以http://libcaca.zoy.org/wiki/PWNtcha這裏PWNtcha項目中的資料爲例分析,各種驗
證碼的破解。(方法很多,僅僅從我個人乍看之下覺得可行的方法來分析)
    
    1)Authimage
    使用的反破解技巧: 
    
    1.不連續的點組成字符
    2.有一定程度的傾斜

    設計不好的地方:
    
    1.通過縱橫的直方圖投影,可以找到字幕區域
    2.通過Hough變換,適當的參數,可以找到近似的橫線,可以做傾斜矯正
    3.字符串的傾斜式面內的,沒有太多的破解難度
    4.字母寬度一定,大小一定
    
    2)Clubic
    使用的反破解技巧: 
    
    1.字符是手寫體

    設計不好的地方:
    
    1.檢測切割階段沒有任何技術含量,屬於設計的比較醜的
    2.只有數字,而且手寫體變化不大
    3.表面看起來對識別階段有難度,仔細分析,發現幾乎不用任何高級的訓練識別算法,就
固定的招某些像素點是否有色彩就夠了

    3)linuxfr.org
    使用的反破解技巧: 

    1.背景顏色塊
    2.前景的橫線或矩形

    設計不好的地方:

    1.背景色是單一色塊,有形狀,通過Region-Growth區域增長來很容易把背景給去掉
    2.前景色是標準的線條,色彩單一
    3.字母無粘連
    4.都是印刷體

    4)Ourcolony
    使用的反破解技巧: 
    
    1.設計的太低級,不屑於去評價

    設計不好的地方:

    1.這種驗證碼,設計的最醜,但還是能把菜鳥搞定,畢竟學計算機的少,搞這個破解的更
少,正所謂隔行如隔山

    5)LiveJournal
    使用的反破解技巧: 

    1.這個設計略微好點,使用個隨機噪音,而且作爲前景
    2.字母位置粗細都有變化

    設計不好的地方:

    1.字母沒有粘連
    2.噪音類型單一
    3.通過在X軸的直方圖投影,能準確分割字幕
    4.然後在Y周作直方圖投影,能準確定位高度
    5.識別階段,都是印刷體,簡單地很

四、網上的一些高級驗證碼
    
    1)ICQ
    2)IMDb
    3)MS MVPS
 
    4)MVN Forum
 
    這些類型是被很多人認爲比較難得類型,分析一下可以發現,字符檢測,定位和分割都不
是難。 唯一影響識別率的是IMDBb和MVPS這兩類,字體變形略大。
    
    總體來說,這些類型的破解也不難,很容易做到50%以上的識別率。

五、高級驗證碼的破解分析
    
    時間關係,我簡單介紹如何利用圖像處理和模式識別技術,自動識別比較高級的驗證碼。
(以風頭正勁的Google爲例)
    1)至少從目前的AI的發展程度看,沒有簡單的做法能自動處理各種不同的驗證碼,即使
能力很強,那麼系統自然也十分複雜強大。所以,要想在很簡單的算法實現比較高級的驗證
碼破解,必須分析不同驗證碼算法的特點:

    作爲一般的圖像處理和計算機視覺,會考慮色彩,紋理,形狀等直接的特徵,同時也考慮
直方圖,灰度等統計特徵,還考慮FFT,Wavelet等各種變換後的特徵。但最終目標都是
Dimension Reduction(降維)然後利於識別,不僅僅是速度的考慮。從圖像的角度看,很多系
統都考慮轉換爲灰度級甚者黑白圖片。
 
    Google的圖片可以看出,顏色變化是虛晃一槍,不存在任何處理難度。難度是字體變形
和字符粘連。
 
    如果能成功的分割字符,那麼後期識別無論是用SVM等分類算法,還是分析筆順比劃走向
來硬識別,都相對好做。
 
    2)圖像處理和粘連分割
    
    代碼中的part1目錄主要完成圖像預處理和粘連字符分割
    001:將圖像從jpg等格式轉換爲位圖便於處理
    002:採用Fix/Adaptive的Threshold門限算法,將圖片Bin-Value二值化。
    (可用003算法)
    003:採用OSTU分水嶺算法,將圖片Bin-Value二值化。
    (更通用,大部分時候效果更好)
    005:獲取ROI感興趣的區域。
    006:Edge Trace邊緣跟蹤。
    007:Edge Detection邊界檢測。
    008:Thin細化去骨架。
    009:做了一些Tidy整理。
  (這個一般要根據特定的Captcha算法調整)
    010:做切割,注意圖片中紅色的交叉點。
    011:將邊緣檢測和骨幹交叉點監測的圖像合併。
  (合併過程可以做分析: 比如X座標偏移門限分析,交叉點區域紋理分析,線條走勢分析,
等等各種方法,找出更可能的切分點和分離後部件的組合管理。)
    代碼:(代碼質量不高,從其他項目拷貝過來,簡單修改的。)

    查看代碼(./pstzine_09_01.txt)

    注: 在這裏,我們可以看到,基本的部件(字母是分割開了,但可以造成統一字母的被切
割成多個Component。 一種做法是:利用先驗知識,做分割; 另外一種做法是,和第二部分的
識別結合起來。 比如按照從左至右,嘗試增加component來識別,如果不能識別而且
component的總寬度,總面積還比較小,繼續增加。 當然不排除拒識的可能性。 )

    3)字符部件組合和識別。

    part2的代碼展示了切割後的字母組合,和基於svm的字符識別的訓練和識別過程。
Detection.cpp中展示了ImageSpam檢測過程中的一些字符分割和組合,layout的分析和利用
的簡單技術。 而Google的驗證碼的識別,完全可以不用到,僅做參考。

    SVM及使用:

    本質上,SVM是一個分類器,原始的SVM是一個兩類分類的分類器。可以通過1:1或者1:n
的方式來組合成一個多類分類的分類器。 天生通過核函數的使用支持高維數據的分類。從
幾何意義上講,就是找到最能表示類別特徵的那些向量(支持向量SV),然後找到一條線,能最
大化分類的Margin。

    libSVM是一個不錯的實現。
    
    訓練間斷和識別階段的數據整理和歸一化是一樣的。 這裏的簡單做法是:
    
    首先:
    
    #define SVM_MAX  +0.999
    #define SVM_MIN  +0.001

    其次:
    
    掃描黑白待識別字幕圖片的每個像素,如果爲0(黑色,是字母上的像素),那麼svm中該位
置就SVM_MAX,反之則反。

    最後:

    訓練階段,在svm的input的前面,爲該類打上標記,即是那一個字母。
    識別階段,當然這個類別標記是SVM分類出來。

    注意:

    如果是SVM菜鳥,最好找一個在SVM外邊做了包裝的工具,比如樣本選擇,交叉驗證,核函
數選擇這些,讓程序自動選擇和分析。

    代碼:通過ReginGrowth來提取單個單個的字符,然後開始識別。

    查看代碼(./pstzine_09_02.txt)
#include "SpamImage.h"
#include "svm-predict.h"
#include <algorithm>
#include <string>
#include <stdio.h>

#ifndef MAX
#define MAX(x,y) (((x) > (y)) ? (x) : (y))
#endif

#ifndef ABS
#define ABS(x) ((x<0) ? (-x) : (x))
#endif

bool x_more_than(const XBlock & m1, const XBlock & m2)
{
	return m1.x < m2.x;
};
void Layout::insert(int i,int x,int y)
{
    layout.insert(std::map<int,Point>::value_type(i,Point(x,y)));
};
void Layout::compute(Config& config,std::map<int,std::string>& lines,std::string& final)
{
	std::map<int,Point>::iterator it;
	std::vector<XBlock> xList;
	//
	int newFile = 1;
	while(layout.size() > 0)
	{
		int startY = -1;
		int startX = -1;
		int startI = -1;
		for(it=layout.begin();it!=layout.end();it++)
		{
			int i = (*it).first;
			Point xy=(*it).second;
			int x=xy.x;
			int y=xy.y;
			if(y > startY || startY == -1)
			{
				startY = y;
				startX = x;
				startI = i;
			}
		}
		//
		for(it=layout.begin();it!=layout.end();it++)
		{
			int i = (*it).first;
			Point xy=(*it).second;
			int x=xy.x;
			int y=xy.y;
		}
		//
		xList.clear();
		for(it=layout.begin();it!=layout.end();it++)
		{			
			int i = (*it).first;
			Point xy=(*it).second;
			int x=xy.x;
			int y=xy.y;
			if(y > startY - 12)
			{
				XBlock xBlock(i,x,y);
				xList.push_back(xBlock);
			}
		}
		//
		std::sort(xList.begin(), xList.end(), x_more_than);
		//
		for(int i=0;i<xList.size();i++)
		{
			XBlock xBlock=xList[i];
			layout.erase(xBlock.i);
			//
			char output='?';
			std::map<int,std::string>::iterator li = lines.find(xBlock.i);
			if(li!=lines.end())
			{
				const char* line = (*li).second.c_str();
				//printf("%s\n",line);
				output = predict_take((char*)line);
				//printf("output1=%c\n",output);
				char temp[2];
				temp[0]=output;
				temp[1]=0;
				final.append(temp);
				//printf("final=%s\n",final.c_str());
			}
			else
			{
				printf("Error case 1\n");
			}
			if(config.trainData)
			{
				char zFile[MAX_PATH];
				sprintf(zFile,"%s\\Z%08d.bmp",config.midstPath,xBlock.i);
				char aFile[MAX_PATH];
				sprintf(aFile,"%s\\A%08d(%c).bmp",config.midstPath,newFile,output);
				rename(zFile,aFile);
				//printf("%s --> %s\n\n",zFile,aFile);
			}
			//
			newFile = newFile + 1;
		}
	}
};

Project::Project(char* fileName)
{
	FILE* fp=fopen(fileName,"r");
	if(!fp)
	{
		printf("Can not load chararters project file.");
		return;
	}
	Charater* oneChar;
	while(true)
	{
		char flag;
		int result = fscanf(fp,"%c",&flag);
		if(result <=0)
		{
			break;
		}
		else
		{
			std::map<char,Charater>::iterator li = chars.find(flag);
			if(li != chars.end())
			{
				oneChar=&((*li).second);
			}
			else
			{
				oneChar=new Charater();
			}
			int size = 0;
			fscanf(fp,"(%d)",&size);
			int data;
			double diff = 0.0;
			std::string line;
			char buff[256];
			for(int i=0;i<size;i++)
			{
				fscanf(fp,"%d:",&data);
				sprintf(buff,"%d",data);
				line.append(buff);
			}			
			//printf("flag=%c  line=%s\n",flag,line.c_str());
			oneChar->lines.push_back(line);
			fscanf(fp,"\n",buff);
		}
		chars.insert(std::map<char,Charater>::value_type(flag,*oneChar));
	}
	if(fp)
	{
		fclose(fp);
		fp=NULL;
	}	
};

RegionGrow::RegionGrow(int maxWidth,int maxHeight)
{
	nMaxWidth = maxWidth;
	nMaxHeight = maxHeight;
	//
	pucRegion = new unsigned char[maxWidth * maxHeight];
	//
	pbMirror = new bool*[maxHeight];
	for(int cy=0;cy<maxHeight;cy++)
	{
		pbMirror[cy] = new bool[maxWidth];
		for(int cx=0;cx<maxWidth;cx++)
		{			
			pbMirror[cy][cx] = true;
		}
	}
	//
	pnGrowQueueX = new int[maxWidth*maxHeight];
	pnGrowQueueY = new int[maxWidth*maxHeight];
};
RegionGrow::~RegionGrow()
{
	delete []pnGrowQueueX;
	delete []pnGrowQueueY;
	pnGrowQueueX = NULL ;
	pnGrowQueueY = NULL ;
	//
	for (int dy=0;dy<nMaxHeight;dy++) 
	{
		delete[] pbMirror[dy];
	}
	delete[] pbMirror;
	//
	delete []pucRegion;
	pucRegion = NULL  ;
};

bool RegionGrow::isNeighbor(RGBQUAD sourceCS,RGBQUAD targetCS,int average)
{
	int sourceGray=(sourceCS.rgbRed+sourceCS.rgbGreen+sourceCS.rgbBlue)/3.0;
	int targetGray=(targetCS.rgbRed+targetCS.rgbGreen+targetCS.rgbBlue)/3.0;	
	if( abs(sourceGray - targetGray) < 256/4 )
	{
		return true;
	}
	else
	{
		return false;
	}
};
void RegionGrow::recognizeSave(std::map<int,std::string> &lines,unsigned char* pUnRegion,int nWidth,int nHeight,int nLeftX,int nLeftY,int nRightX,int nRightY,Config& config,int saveName,char* line)
{
	if(line != NULL)
	{
		sprintf(line,"%d ",saveName);
		int index = 1;
		for(int y=nLeftY;y<=nRightY;y++)
		{
			for(int x=nLeftX;x<=nRightX;x++)
			{
				if(pUnRegion[y*nWidth+x] == 1)
				{
					sprintf(line,"%s%d:%lf ",line,index++,SVM_MAX);
				}
				else
				{
					sprintf(line,"%s%d:%lf ",line,index++,SVM_MIN);
				}
			}
		}
		lines.insert(std::map<int,std::string>::value_type(saveName,line));
	}
    //
	if(config.trainData)
	{
		CxImage image;
		int nWidthROI = nRightX-nLeftX+1;
		int nHeightROI = nRightY-nLeftY+1;
		image.Create(nWidthROI,nHeightROI,24,CXIMAGE_SUPPORT_BMP);
		RGBQUAD rgbSet;
		for(int sy=nLeftY;sy<=nRightY;sy++)
		{
			for(int sx=nLeftX;sx<=nRightX;sx++)
			{
				if(pUnRegion[sy*nWidth+sx] == 1)
				{
					rgbSet.rgbRed=255;
					rgbSet.rgbGreen=0;
					rgbSet.rgbBlue=0;
				}
				else
				{
                    rgbSet.rgbRed=0;
					rgbSet.rgbGreen=0;
					rgbSet.rgbBlue=0;
				}
				image.SetPixelColor(sx-nLeftX,sy-nLeftY,rgbSet);
			}
		}
		char file[MAX_PATH];
		if(line == NULL)
		{
			static int notText = 1;
			sprintf(file,"%s\\N%08d.bmp",config.midstPath,notText++);
		}
		else
		{
		    sprintf(file,"%s\\Z%08d.bmp",config.midstPath,saveName);
		}
		image.Save(file,CXIMAGE_SUPPORT_BMP);
	}
}
void RegionGrow::runRegionGrow(CxImage* cxImage,int nWidth,int nHeight,Config& config,Project &project,std::string& final) 
{
#define ROI_X_LEFT  1
#define ROI_X_RIGHT  1
#define ROI_Y_LEFT  1
#define ROI_Y_RIGHT  1
	
	//static int nDn = 4;
	//static int nDx[]={-1,+0,+1,+0};
	//static int nDy[]={+0,+1,+0,-1};
	
    static int nDn = 8;
	static int nDx[]={-1,+0,+1,+0, -1,-1,+1,+1};
	static int nDy[]={+0,+1,+0,-1, +1,-1,+1,-1};
	
    //static int nDn = 20;
	//static int nDx[]={-1,+0,+1,+0, -1,-1,+1,+1, -2,+2,-2,+2,-2,+2,+0,+0,-1,-1,+1,+2};
	//static int nDy[]={+0,+1,+0,-1, +1,-1,+1,-1, +0,+0,+1,+1,-1,-1,+2,-2,+2,-2,+1,-2};
	
	if(nWidth <= ROI_X_LEFT+ROI_X_RIGHT || nHeight <= ROI_Y_LEFT+ROI_Y_RIGHT)
	{
		printf("The image must be bigger than %d x %d (width * height)!\n",(ROI_X_LEFT+ROI_X_RIGHT),(ROI_Y_LEFT+ROI_Y_RIGHT));
		exit(1);
	}

	int nLocAvg = 0;
	for(int cy=nHeight-ROI_Y_RIGHT;cy>ROI_Y_LEFT;cy--)
	{
		for(int cx=ROI_X_LEFT;cx<nWidth-ROI_X_RIGHT;cx++)
		{
            RGBQUAD rgbCS = cxImage->GetPixelColor(cx,cy);
			RGBQUAD yuvCS = CxImage::RGBtoXYZ(rgbCS); 
			int gray = (yuvCS.rgbRed + yuvCS.rgbGreen + yuvCS.rgbBlue) / 3.0;
			RGBQUAD gryCS;
			gryCS.rgbRed = gray;
			gryCS.rgbGreen = gray;
			gryCS.rgbBlue = gray;
			cxImage->SetPixelColor(cx,cy,gryCS);			
			nLocAvg = nLocAvg + gray;
		}
	}
	nLocAvg /= ( (nHeight-ROI_Y_RIGHT-ROI_Y_LEFT) * (nWidth-ROI_X_RIGHT-ROI_X_LEFT) ) ;

	int nPixel = 0;
	int nLeftX = 0;
	int nLeftY = 0;
	int nRightX = 0;
	int nRightY = 0;
    int debugFile=1;
	std::map<int,std::string> lines;
	
	for(int my=nHeight-ROI_Y_RIGHT;my>ROI_Y_LEFT;my--)
	{
		for(int mx=ROI_X_LEFT;mx<nWidth-ROI_X_RIGHT;mx++)
		{
			if(pbMirror[my][mx])
			{
				memset(pucRegion,0,sizeof(unsigned char)* nWidth * nHeight);
				nPixel = 1;
                nLeftX = mx;
				nLeftY = my;
				nRightX = mx;
				nRightY = my;
				int nStart = 0 ;
				int nEnd   = 0 ;
				pnGrowQueueX[nEnd] = mx;
				pnGrowQueueY[nEnd] = my;
				int nCurrX ;
				int nCurrY ;
				int xx;
				int yy;
				int k ;
				while (nStart<=nEnd)
				{
					nCurrX = pnGrowQueueX[nStart];
					nCurrY = pnGrowQueueY[nStart];
					for (k=0;k<nDn;k++)	
					{	
						xx = nCurrX+nDx[k];
						yy = nCurrY+nDy[k]; 
						if ((xx < nWidth) && (xx>=0) && (yy<nHeight) && (yy>=0) && (pucRegion[yy*nWidth+xx]==0) )
						{
							if(isNeighbor(cxImage->GetPixelColor(xx,yy),cxImage->GetPixelColor(nCurrX,nCurrY),nLocAvg))
							{
								nEnd++;
								pnGrowQueueX[nEnd] = xx;
								pnGrowQueueY[nEnd] = yy;
								pucRegion[yy*nWidth+xx] = 1;
								nPixel++;
								if(xx < nLeftX) 
								{
									nLeftX=xx;
								}
								else if(xx > nRightX)
								{
									nRightX=xx;
								}
								if(yy < nLeftY) 
								{
									nLeftY=yy;
								}
								else if(yy > nRightY)
								{
									nRightY=yy;
								}
                                pbMirror[yy][xx] = false;
								pbMirror[nCurrY][nCurrX] = false;   //FAST
							}
						}
					}
					nStart++;
				}		
				const static int TOO_SMALL = 11;
				const static int TOO_HIGH = 19;
				const static int TOO_SHORT = 6;				
				if(nPixel < TOO_SMALL)   //面積太小
				{
					//printf("xxx: found no-text region case: too small (pixels: %d<%d)\n",nPixel,TOO_SMALL);
					//recognizeSave(lines,pucRegion,nWidth,nHeight,nLeftX,nLeftY,nRightX,nRightY,config,debugFile,NULL);
					continue;
				}
				else if(nRightY-nLeftY > TOO_HIGH)  //太高
				{
					//printf("xxx: found no-text region case: too high (height: %d>%d)\n",nRightY-nLeftY,TOO_HIGH);
					//recognizeSave(lines,pucRegion,nWidth,nHeight,nLeftX,nLeftY,nRightX,nRightY,config,debugFile,NULL);
					continue;
				}
				else if(nRightY-nLeftY < TOO_SHORT)  //太矮
				{
					//printf("xxx: found no-text region case: too short (height: %d<%d)\n",nRightY-nLeftY,TOO_SHORT);
					//recognizeSave(lines,pucRegion,nWidth,nHeight,nLeftX,nLeftY,nRightX,nRightY,config,debugFile,NULL);
					continue;
				}
				else if( (nRightX-nLeftX) >= (nRightY-nLeftY) * 1.6 )  //寬大於高
				{
					//printf("???: found merged block: (%d,%d) --> (%d,%d)\n",nLeftX,nLeftY,nRightX,nRightY);
					//預切
					int nWidthROI = nRightX-nLeftX+1;
					int nHeightROI = nRightY-nLeftY+1;
					int aLeftY=nLeftY;
					int aRightY=nRightY;
					int aLeftX=nLeftX;
					int aRightX=nLeftX+nHeightROI-1;   // *1.1
					while(true)
					{
						int aW=aRightX-aLeftX+1;
						int aH=aRightY-aLeftY+1;
						char* line = new char[aW*aH*32];
						memset(line, 0, aW*aH*32);
						recognizeSave(lines,pucRegion,nWidth,nHeight,aLeftX,aLeftY,aRightX,aRightY,config,debugFile,line);
						layout.insert(debugFile,nLeftX,nLeftY);
						debugFile = debugFile + 1;
						//識別
						char output = predict_take(line);
						delete line;
						//X投影 (上輪廓 + 下輪廓)
						int* projectX =  new int[aW];
						int* projectXScaled =  new int[aW];
						for(int px1=aLeftX,index=0;px1<=aRightX;px1++,index++)
						{
							projectX[index] = 0;
							for(int py1=aLeftY;py1<=aRightY;py1++)
							{
								if(pucRegion[py1*nWidth+px1] == 1)
								{
									projectX[index] = projectX[index]+1;
								}
							}
							//5-scale
							projectXScaled[index] = (int)( (double)projectX[index] / (double)aH * 5.0 );
						}
						//輪廓
						Charater oneChar;
						std::map<char,Charater>::iterator li = project.chars.find(output);
						if(li != project.chars.end())
						{
							oneChar=(*li).second;			
						}
						int matchedSize = 0;
						double matchedDiff = aW * 5.0;		
						for(int c=0;c<oneChar.lines.size();c++)
						{
							const char* line=oneChar.lines[c].c_str();
							int size=strlen(line);
							double diff = 0.0;
							for(int i=0;i<size && i<aW;i++)
							{
								char temp[2];
								temp[0]=line[i];
								temp[1]=0;
								int data=atoi(temp);
								diff = diff + abs(projectXScaled[i]-data);
								//printf("project=%d  current=%d    diff=%lf\n",projectXScaled[i],data,diff);
							}
							//需要設計這裏的評價函數 size/aW, size/matchedSize, diff/matchedDiff
							if(diff < matchedDiff)
							{
								matchedDiff = diff;
								matchedSize = size;
							}
						}
						delete projectXScaled;
						delete projectX;
						//printf("matchedSize=%d  matchedDiff=%lf\n",matchedSize,matchedDiff);
						//
						if(matchedSize == 0)
						{
							matchedSize = nHeightROI;
						}
						aLeftX=aLeftX+matchedSize;
						aRightX=aLeftX+nHeightROI-1;	//*1.1
						if(aLeftX >= nRightX-1)
						{
							break;
						}
						if(aRightX > nRightX)
						{
							aRightX=nRightX;
						}
					}				
				}
				else
				{
					//printf("vvv: found ok-text region case: other condition\n");
					int aW=nRightX-nLeftX+1;
					int aH=nRightY-nLeftY+1;
					char* line = new char[aW*aH*32];
					memset(line, 0, aW*aH*32);					
					RegionGrow::recognizeSave(lines,pucRegion,nWidth,nHeight,nLeftX,nLeftY,nRightX,nRightY,config,debugFile,line);
					layout.insert(debugFile,nLeftX,nLeftY);
					debugFile =  debugFile + 1;
					delete line;
				}
			}
		}
	}
    layout.compute(config,lines,final);
};

六、對驗證碼設計的一些建議 1.在噪音等類型的使用上,盡力讓字符和用來混淆的前景和背景不容易區分。盡力讓壞人(噪音)長得和好人(字母)一樣。 2.特別好的驗證碼的設計,要盡力發揮人類擅長而AI算法不擅長的。 比如粘連字符的分割和手寫體(通過印刷體做特別的變形也可以)。 而不要一味的去加一些看起來比較複雜的噪音或者其他的花哨的東西。即使你做的足夠複雜,但如果人也難識別,顯然別人認爲你是沒事找抽型的。 3. 從專業的機器視覺的角度說,驗證碼的設計,一定要讓破解者在識別階段,反覆在低階視覺和高階視覺之間多反覆幾次才能識別出來。 這樣可以大大降低破解難度和破解的準確率。七、個人鄭重申明 1.這個問題,本身是人工智能,計算機視覺,模式識別領域的一個難題。我是蝦米,菜得不能再菜的那種。作爲破解者來說,是出於劣勢地位。要做的很好,是很難得。總體來說,我走的是比較學院派的線路,能真正的破解難度比較高的驗證碼,不同於網上很多不太入流的破解方法。我能做的只有利用有限的知識,拋磚引玉而已。 很多OCR的技術,特別是離線手寫體中文等文字識別的技術,個人瞭解有限的很,都不敢在這裏亂寫。 2.希望不要把這種技術用於非法用途。-EOF-



發佈了110 篇原創文章 · 獲贊 79 · 訪問量 84萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章