http://www.cnblogs.com/zhenyulu/category/47600.html
我做的《筆跡鑑別》是與文字無關的筆跡鑑別,簡單的說就是你提供給我多個人手寫的“一二三四”,然後再提供給我其中一個人寫的“五六七八”,我就可以通過程序判斷究竟是誰寫的。待識別的文字與我手頭掌握的文字資料可以是不同的漢字,這就是所謂的與文字無關的筆跡鑑別。當然僅僅提供四五個漢字是不行的,需要提前準備大量的筆跡素材纔可以。
我主要採用“紋理識別”的方式進行筆跡鑑別,也就是將筆跡看作是某種紋理(就像布紋、木紋一樣),紋理相同的就認爲是筆跡相同。而目前紋理識別我使用的是“加窗傅立葉變換”Gabor變換,利用Gabor變換提取不同頻率、不同方向的筆跡特徵,最後使用KNN或SVM(支持向量機)對待測樣本進行類別判別。
基本步驟如下:
** 筆跡圖像預處理
1、 筆跡圖像掃描2、 去除稿紙中的分割線,轉換成黑白二值圖(目前使用PhotoShop實現)
3、 中值濾波,去除圖片中的椒鹽噪聲(目前使用MatLab實現)
4、 傾斜校正(儘管可以使用一些現成的算法,但目前使用手工傾斜校正)
** 文字切分、紋理製作
5、 行切分、字切分(根據象素的統計信息進行切分,對於漢字中常見的左右結構以及偏旁部首等設計了偏旁部首合併策略,確保漢字的完整性。此部分自己編程實現)6、 紋理圖像的製作(對切分下來的漢字將文字長、寬歸一化,製作紋理圖像,自己編程實現)
** Gabor變換,提取紋理特徵
7、 對紋理圖像進行Gabor變換(自己編程實現。由於在時域進行二維離散卷積需要大量的運算時間,因此我通過二維傅立葉變換將其轉換到頻域求乘法,實驗表明卷積求解效率提高了近50倍),提取紋理特徵(一64維向量)。8、 對Gabor變換產生的結果進行數據庫存貯,以備將來識別使用(爲了簡便起見,我目前使用VFP,如果將來數據量再大的話,可以考慮使用SQL Server等數據庫)。
** 對待測樣本進行鑑別
9、 對待處理樣本採用同樣的處理方法提取紋理特徵,然後使用KNN臨近聚類的方法或SVM進行分類。(KNN自己編程實現,SVM使用現成的LibSVM。當然也有C#版的LibSVM可用來融入自己的程序中)實驗表明(我只採集了9個人的筆跡),識別率可以達到91.67%以上,採取某種措施後,9人筆跡鑑別的成功率可達到100%(目前由於筆跡採集有限,才達到100%,隨着筆跡樣本的增加,成功率可能有所下降)。
我會在隨後的文章中將以上步驟中的關鍵技術和關鍵代碼放上來供大家參考。
一、文字切割
字跡在經過初始處理後,被製作成黑白二值圖保存。這個步驟比較簡單,可以使用PhotoShop等工具進行處理。剩下的工作就是從字跡中將一個一個的漢字摘出來,用來製作紋理圖片。我採用的方法是通過字切割的方式,當然也有一些文獻採用另外的較簡單的方式進行處理(比如只是去掉行間、文字間的空白)。
1、行切割
對於得到的黑白二值圖進行統計處理。統計黑白點陣圖中每行中黑色像素的數量,得到一統計向量,該向量中極小值所對應的位置就應當是分行的地方。代碼相對簡單,不再贅述。
2、字切割
行切割完成後,就需要將每行中的文字切割下來,這次是對每行文字的黑色像素進行縱向統計,黑色像素數量極小的地方就有可能是分字的地方。之所以說“可能”是因爲有很多漢字是左右結構(比如“朋”字),在兩個“月”字中間的區域對應的黑色像素數量也很小,甚至是0,因此需要採取某種偏旁部首合併策略,將可能的左右結構漢字重新合併到一起,作爲一個漢字處理。另外在進行字切割時還應當將可能是標點符號的字符去掉,防止標點符號影響字跡的識別。
我的程序中首先對初步字切割的結果進行判別,剔除標點符號。標點符號往往寬度較小,同時像素統計值比較少。然後,採用自右向左的順序進行偏旁部首合併(之所以選擇自右向左是因爲漢字中左窄右寬的文字比重較大,自右向左合併的成功性較大),如果經嘗試合併後的文字寬度與字跡中平均文字寬度差不多的化,就進行合併處理。
當然有時候需要進行文字切分,當發現某個切分結果過寬時,就可能預示兩個漢字捱得太緊,需要進一步切分開。
經過一番文字切分、偏旁部首合併策略的的調整,我的程序能夠較成功的將字跡中的文字逐一切割下來。切割結果如下圖所示:
3、關鍵代碼
在文字切割部分中用到的關鍵代碼主要是黑白圖像的讀取代碼。我程序中主要使用的是PixelFormat.Format1bppIndexed格式的PNG圖像,一個二進制位對應一個像素點,這樣比PixelFormat.Format1bppIndexed格式的BMP文件要節省磁盤空間。有關此類圖像的處理技巧,可以參考:《Using the LockBits method to access image data》。這裏將部分行切分時用到的代碼放上來:
#region SetIndexedPixel and GetIndexedPixel for Format1bppIndexed png file protected void SetIndexedPixel(int x,int y,BitmapData bmd, bool pixel) { int index=y*bmd.Stride+(x>>3); byte p=Marshal.ReadByte(bmd.Scan0,index); byte mask=(byte)(0x80>>(x&0x7)); if(pixel) p |=mask; else p &=(byte)(mask^0xff); Marshal.WriteByte(bmd.Scan0,index,p); } private bool GetIndexedPixel(int x, int y, BitmapData bmd) { int index = y * bmd.Stride + (x>>3); byte p = Marshal.ReadByte(bmd.Scan0,index); byte mask=(byte)(0x80>>(x&0x7)); if(((int)(p & mask))== 0) return true; else return false; } #endregion private void CalcBlackDotsOfLine() { Bitmap bm = new Bitmap(this.ImageFileName); BlackDotsOfLine = new int[bm.Height]; BitmapData bmdn=bm.LockBits(new Rectangle(0,0,bm.Width,bm.Height), ImageLockMode.ReadOnly, PixelFormat.Format1bppIndexed); for(int y=0; y < bm.Height; y++) for(int x=0; x < bm.Width; x++) if(this.GetIndexedPixel(x, y, bmdn)) BlackDotsOfLine[y]++; bm.UnlockBits(bmdn); }
之所以使用LockBits方法是因爲這樣處理速度比較快,如果速度並不是很重要的因素的話,我建議使用Bitmap對象的SetPixel方法和GetPixel方法。
二、紋理製作
1、紋理製作
文字切割完成後,就需要製作紋理圖像了。我這裏主要參考了“劉宏 李錦濤 崔國勤 唐勝,基於SVM和紋理的筆跡鑑別方法,計算機輔助設計與圖形學學報,Vol15(12),pp1479-1484”一文,將文字縮放至16×16點陣大小,並拼接成384×384規格的圖片,每幅圖片可以切割成9個128×128大小的圖片作爲訓練樣本。待測樣本製作成256×256大小,可以切割成4個128×128大小的紋理圖片。下面是一張訓練樣本圖片和兩張待測樣本圖片:
訓練樣本(384×384大小)
待測樣本(256×256大小)
紋理製作好後就可以使用Gabor變換程序進行變換提取筆跡特徵了。Gabor變換將在下一部分再做介紹。
2、關鍵代碼
紋理製作過程中的關鍵代碼主要是圖像的縮放操作,將切割下來的文字縮放成16×16點陣並且進行拼接。這方面的資料很多,包括博客園在內的很多網站在對大圖片進行顯示之前都要進行尺寸處理。我這裏將我程序中的關鍵代碼放上來(略經刪截):
private void BeginProcess() { Image imgSrc = this.spbSrc.PicBox.Image, imgDest; Rectangle destRect, srcRect; if(this.cboSize.SelectedIndex == 0) imgDest = new Bitmap(384, 384, PixelFormat.Format24bppRgb); else imgDest = new Bitmap(256, 256, PixelFormat.Format24bppRgb); Graphics g = Graphics.FromImage(imgDest); g.CompositingQuality = CompositingQuality.HighSpeed; g.SmoothingMode = SmoothingMode.HighSpeed; g.InterpolationMode = InterpolationMode.Bilinear; int current = 0; for(int y=0; y < imgDest.Height; y+=CharSize) for(int x = 0; x < imgDest.Width; x+=CharSize) { destRect = new Rectangle(x, y, CharSize, CharSize); srcRect = (Rectangle)CharsRectangle[current]; g.DrawImage(imgSrc, destRect, srcRect, GraphicsUnit.Pixel); } }
其中CharSize是一常量,值爲16。
一、二維卷積運算
Gabor變換的本質實際上還是對二維圖像求卷積。因此二維卷積運算的效率就直接決定了Gabor變換的效率。在這裏我先說說二維卷積運算以及如何通過二維傅立葉變換提高卷積運算效率。在下一步分內容中我們將此應用到Gabor變換上,抽取筆跡紋理的特徵。
1、離散二維疊加和卷積
關於離散二維疊加和卷積的運算介紹的書籍比較多,我這裏推薦William K. Pratt著,鄧魯華 張延恆 等譯的《數字圖像處理(第3版)》,其中第7章介紹的就是這方面的運算。爲了便於理解,我用下面幾個圖來說明離散二維疊加和卷積的求解過程。
A可以理解成是待處理的筆跡紋理,B可以理解成Gabor變換的核函數,現在要求A與B的離散二維疊加捲積,我們首先對A的右邊界和下邊界填充0(zero padding),然後將B進行水平翻轉和垂直翻轉,如下圖:
然後用B中的每個值依次乘以A中相對位置處的值並進行累加,結果填入相應位置處(注意紅圈位置)。通常二維卷積的結果比A、B的尺寸要大。如下圖所示:
2、快速傅立葉變換卷積
根據傅立葉變換理論,對圖像進行二維卷積等價於對圖像的二維傅立葉變換以及核函數的二維傅立葉變換在頻域求乘法。通過二維傅立葉變換可以有效提高卷積的運算效率。但在進行傅立葉變換時一定要注意“卷繞誤差效應”,只有正確對原有圖像以及卷積核填補零後,才能得到正確的卷積結果。關於這部分內容可以參考William K. Pratt著,鄧魯華 張延恆 等譯的《數字圖像處理(第3版)》第9章的相關內容,此處就不再贅述。
目前網上可以找到開源C#版的快速傅立葉變換代碼(Exocortex.DSP),我使用的是1.2版,2.0版似乎只能通過CVS從SourceForge上籤出, 並且功能沒有什麼太大改變。將Exocortex.DSP下載下來後,將源代碼包含在自己的項目中,然後就可以利用它裏面提供的複數運算以及傅立葉變換功能了。爲了測試通過傅立葉變換求卷積的有效性,特編寫以下代碼:
using System; using Exocortex.DSP; class MainEntry { static void Main() { fftConv2 c = new fftConv2(); c.DoFFTConv2(); } } public class fftConv2 { double[,] kernel = {{-1, 1}, {0, 1}}; double[,] data = {{10,5,20,20,20}, {10,5,20,20,20}, {10,5,20,20,20}, {10,5,20,20,20}, {10,5,20,20,20}}; Complex[] Kernel = new Complex[8*8]; Complex[] Data = new Complex[8*8]; Complex[] Result = new Complex[8*8]; private void Init() { for(int y=0; y<2; y++) for(int x=0; x<2; x++) Kernel[y*8+x].Re = kernel[y,x]; for(int y=0; y<5; y++) for(int x=0; x<5; x++) Data[y*8+x].Re = data[y,x]; } public void DoFFTConv2() { Init(); Fourier.FFT2(Data, 8, 8, FourierDirection.Forward); Fourier.FFT2(Kernel, 8, 8, FourierDirection.Forward); for(int i=0; i<8*8; i++) Result[i] = Data[i] * Kernel[i] / (8*8); Fourier.FFT2(Result, 8, 8, FourierDirection.Backward); for(int y=0; y<6; y++) { for(int x=0; x<6; x++) Console.Write("{0,8:F2}", Result[y*8+x].Re); Console.WriteLine(); } } }
程序的運行結果與離散二維疊加和卷積的運算結果完全相同。
由於卷積結果與原始輸入圖片的大小是不一樣的,存在着所謂“邊界”,在我的實際應用程序中,爲了避免這些“邊界”對結果過多的影響,我採用的是居中陣列定義,並且從卷積結果中只截取需要的那部分內容,確保和原始圖片的大小完全一致,如下圖:
這就需要對卷積的傅立葉求法做些微小的調整,具體調整辦法就不說了,主要是座標的變換,將示例代碼貼上來供大家參考:
using System; using Exocortex.DSP; class MainEntry { static void Main() { CenterfftConv2 s = new CenterfftConv2(); s.CommonMethod(); s.DoFFTConv2(); } } public class CenterfftConv2 { double[,] kernel = {{0, 1, 0}, {1, 2, 0}, {0, 0, 3}}; double[,] data = new double[12,12]; Complex[] Kernel = new Complex[16*16]; Complex[] Data = new Complex[16*16]; Complex[] Result = new Complex[16*16]; public CenterfftConv2() { Random r = new Random(); for(int y=0; y<12; y++) for(int x=0; x<12; x++) data[y,x] = r.NextDouble(); for(int y=0; y<3; y++) for(int x=0; x<3; x++) Kernel[y*16+x].Re = kernel[y,x]; for(int y=1; y<13; y++) for(int x=1; x<13; x++) Data[y*16+x].Re = data[y-1,x-1]; } public void DoFFTConv2() { Console.WriteLine(" ========= By FFT2Conv2 Method ========="); Fourier.FFT2(Data, 16, 16, FourierDirection.Forward); Fourier.FFT2(Kernel, 16, 16, FourierDirection.Forward); for(int i=0; i<16*16; i++) Result[i] = Data[i] * Kernel[i] / (16*16); Fourier.FFT2(Result, 16, 16, FourierDirection.Backward); for(int y=2; y<14; y++) { for(int x=2; x<14; x++) Console.Write("{0,5:F2}", Result[y*16+x].GetModulus()); Console.WriteLine(); } } public void CommonMethod() { double real = 0; Console.WriteLine(" ========== Direct Transform ==========="); for(int y=0; y < 12; y++) { for(int x=0; x < 12; x++) { for(int y1=0; y1 < 3; y1++) for(int x1=0; x1 < 3; x1++) { // (kernel.Length-1)/2 = 1 if(((y - 1 + y1)>=0) && ((y - 1 + y1)<12) && ((x - 1 + x1)>=0) && ((x - 1 + x1)<12)) { real += data[y - 1 + y1, x - 1 + x1] * kernel[2 - x1, 2 - y1]; } } Console.Write("{0,5:F2}", real); real=0; } Console.WriteLine(); } Console.WriteLine("\n"); } }
有了此部分的基礎知識後,我們就要步入筆跡識別中最核心的部分Gabor變換,提取筆跡的特徵了。
二、Gabor函數
Gabor變換屬於加窗傅立葉變換,Gabor函數可以在頻域不同尺度、不同方向上提取相關的特徵。另外Gabor函數與人眼的生物作用相仿,所以經常用作紋理識別上,並取得了較好的效果。二維Gabor函數可以表示爲:
其中:
v的取值決定了Gabor濾波的波長,u的取值表示Gabor核函數的方向,K表示總的方向數。參數決定了高斯窗口的大小,這裏取。程序中取4個頻率(v=0, 1, ..., 3),8個方向(即K=8,u=0, 1, ... ,7),共32個Gabor核函數。不同頻率不同方向的Gabor函數可通過下圖表示:
圖片來源:GaborFilter.html
圖片來源:http://www.bmva.ac.uk/bmvc/1997/papers/033/node2.html
三、代碼實現
Gabor函數是復值函數,因此在運算過程中要分別計算其實部和虛部。代碼如下:
private void CalculateKernel(int Orientation, int Frequency) { double real, img; for(int x = -(GaborWidth-1)/2; x<(GaborWidth-1)/2+1; x++) for(int y = -(GaborHeight-1)/2; y<(GaborHeight-1)/2+1; y++) { real = KernelRealPart(x, y, Orientation, Frequency); img = KernelImgPart(x, y, Orientation, Frequency); KernelFFT2[(x+(GaborWidth-1)/2) + 256 * (y+(GaborHeight-1)/2)].Re = real; KernelFFT2[(x+(GaborWidth-1)/2) + 256 * (y+(GaborHeight-1)/2)].Im = img; } } private double KernelRealPart(int x, int y, int Orientation, int Frequency) { double U, V; double Sigma, Kv, Qu; double tmp1, tmp2; U = Orientation; V = Frequency; Sigma = 2 * Math.PI * Math.PI; Kv = Math.PI * Math.Exp((-(V+2)/2)*Math.Log(2, Math.E)); Qu = U * Math.PI / 8; tmp1 = Math.Exp(-(Kv * Kv * ( x*x + y*y)/(2 * Sigma))); tmp2 = Math.Cos(Kv * Math.Cos(Qu) * x + Kv * Math.Sin(Qu) * y) - Math.Exp(-(Sigma/2)); return tmp1 * tmp2 * Kv * Kv / Sigma; } private double KernelImgPart(int x, int y, int Orientation, int Frequency) { double U, V; double Sigma, Kv, Qu; double tmp1, tmp2; U = Orientation; V = Frequency; Sigma = 2 * Math.PI * Math.PI; Kv = Math.PI * Math.Exp((-(V+2)/2)*Math.Log(2, Math.E)); Qu = U * Math.PI / 8; tmp1 = Math.Exp(-(Kv * Kv * ( x*x + y*y)/(2 * Sigma))); tmp2 = Math.Sin(Kv * Math.Cos(Qu) * x + Kv * Math.Sin(Qu) * y) - Math.Exp(-(Sigma/2)); return tmp1 * tmp2 * Kv * Kv / Sigma; }
有了Gabor核函數後就可以採用前文中提到的“離散二維疊加和卷積”或“快速傅立葉變換卷積”的方法求解Gabor變換,並對變換結果求均值和方差作爲提取的特徵。32個Gabor核函數對應32次變換可以提取64個特徵(包括均值和方差)。由於整個變換過程代碼比較複雜,這裏僅提供測試代碼供下載。該代碼僅計算了一個101×101尺寸的Gabor函數變換,得到均值和方差。代碼採用兩種卷積計算方式,從結果中可以看出,快速傅立葉變換卷積的效率是離散二維疊加和卷積的近50倍。
代碼下載請點 >>>> 這裏 。注意,代碼中沒有包含Exocortex.DSP,請測試者到相應網站上下載幷包含在自己的項目中。
解壓縮後,裏面有一"GaborTest.png"文件,程序中默認路徑是“D:\”,請將此圖片放置到此路徑下。(程序代碼在Visual Studio .net 2003下調試通過)。
一、k-NN法
這種概率密度函數估計的方法是這樣的:在以特徵向量x爲中心的一個鄰域裏,固定落入鄰域中的樣本的個數k(n)。這可以通過下面的方法實現:在一個合適的距離尺度下,逐漸增大包圍x點的區域體積,直到有k個樣本點落入這個區域中。這就是x周圍離它最近的k(n)個樣本。在這k(n)個樣本中,數量最多的種類就可以看作樣本x的類型。當然k的選取也很重要。隨着k的增加,k-NN的錯誤率將逐漸貼近貝葉斯錯誤率。
在進行k-NN聚類之前首先要對Gabor變換的結果數據歸一化,以確保結果運算的有效性。具體說就是讓每個測量數據的取值在[0, 1]或[-1, 1]之間。舉例來說:假設有兩類數據,一類數據的中心位於座標(-1, 10000)處,另一類數據的中心位於(0,9996)處,有一待測樣本,座標值是(-1,9997),它應當屬於拿類呢?如果計算歐氏距離的化,該樣本屬於第二類。但仔細分析可知,該樣本應當屬於第一類,因爲縱座標的值過於大(其實待測樣本的縱座標只比訓練樣本有萬分之幾的變化,完全可以忽略不計)。現在我們將縱座標縮小10000倍,歸一到[-1, 1]上,則兩類待測樣本座標變爲(-1,1)和(0,0.9996),待測樣本座標爲(-1,0.9997),顯然屬於第一類。
至於k-NN的代碼實現相對來說比較簡單。我採用了鏈表的方式。鏈表爲有序定長的鏈表,設x爲待測樣本,依次計算x與p個訓練樣本的距離(我採用的是歐氏距離)。將該結果依次和鏈表中各元素比較,如果小於某一節點,則將新結果插入到該節點之前,並刪除鏈表中最後一個節點。這樣,當完成x與所有p個訓練樣本的距離計算後,鏈表中就記錄了和它最近的k個樣本。我們通過判別這k個樣本中數量最多的種類完成對x類型的估計。
代碼相對簡單,這裏就不再佔用空間貼代碼了。下面我們看看SVM分類。
二、SVM
SVM分類器通常具有較高的分類精度。我這裏不想過多的去說SVM是怎麼回事,只是提供一種使用SVM進行判別的方法。我使用的是開源的LibSVM實現SVM分類。Google上輸入LIBSVM可以很容易的找到代碼下載。我使用的是C#版(不過是2.6版),也可以使用C++的2.81版。下面我說說如何使用2.81版中帶的編譯好的程序完成聚類工作。該版本支持多類判別。
1、數據準備工作
首先對Gabor變換結果進行處理,生成符合SVM處理規範的文本格式。關於格式的更多說明可以參考軟件使用手冊。另外此步可以不用歸一化,因爲LibSVM工具中提供了scale工具,可以自動完成數據的歸一化處理工作。
2、程序配置工作
LibSVM 2.81版下載下來後並不能直接操作,還需要一些輔助工作,否則在默認判別的方式下工作判別精度可能非常低。關於此方面的更多內容可以參考《A Practical Guide to Support Vector Classification》一文,該PDF文檔可以從LibSVM的網站上下載到。
在使用LibSVM之前首先要安裝Pathon,Pathon 2.4可以從Pathon的網站上下載到。
緊接着就是需要裝“pgnuplot.exe”,LibSVM使用它完成參數搜索時的繪圖工作,該程序沒有包含在LibSVM 2.81版的壓縮包中,需要自己到網上搜索並下載。另外在LibSVM 2.81中grid.py代碼裏默認pgnuplot.exe的路徑是“c:\tmp\gnuplot\bin\pgnuplot.exe”,你可以將“pgnuplot.exe”放到該路徑下或修改grid.py代碼指向你自己的路徑。
所有這些準備工作完成後,就可以進行SVM分類工作了。
3、SVM分類
使用SVM分類可以執行“libsvm-2.81\tools”目錄下的easy.py程序,該程序提供了一套默認的、精度較高的SVM分類算法。
在DOS窗口下輸入:C:\Python\Python easy.py Train.txt Test.txt就可以利用Train.txt中的數據進行訓練,然後對Test.txt中的數據進行判別。
*** 結果 ***
本人採集了多人的筆跡,經過紋理製作、Gabor變換提取出了相應的特徵。在使用KNN與SVM對待測樣本進行聚類時均取得了較高的識別精度。通過優化,10人筆跡的測試精度可以達到100%,效果還是很不錯的。
2006年2月
======= 全文完 =======