本文源自我之前花了2天時(shí)間做的一個(gè)簡(jiǎn)單的車(chē)牌識別系統。那個(gè)項目,時(shí)間太緊,樣本也有限,達不到對方要求的95%識別率(主要對于車(chē)牌來(lái)說(shuō),D,0,O,I,1等等太相似了。然后,漢字的識別難度也不?。?,因此未被對方接受。在此放出,同時(shí)描述一下思路及算法。
全文分兩部分,第一部分講車(chē)牌識別及普通驗證碼這一類(lèi)識別的普通方法,第二部分講對類(lèi)似QQ驗證碼,Gmail驗證碼這一類(lèi)變態(tài)驗證碼的識別方法和思路。
一、車(chē)牌/驗證碼識別的普通方法
車(chē)牌、驗證碼識別的普通方法為:
(1) 將圖片灰度化與二值化
(2) 去噪,然后切割成一個(gè)一個(gè)的字符
(3) 提取每一個(gè)字符的特征,生成特征矢量或特征矩陣
(4) 分類(lèi)與學(xué)習。將特征矢量或特征矩陣與樣本庫進(jìn)行比對,挑選出相似的那類(lèi)樣本,將這類(lèi)樣本的值作為輸出結果。
下面借著(zhù)代碼,描述一下上述過(guò)程。因為更新SVN Server,我以前以bdb儲存的代碼訪(fǎng)問(wèn)不了,因此部分代碼是用Reflector反編譯過(guò)來(lái)的,望見(jiàn)諒。
(1) 圖片的灰度化與二值化
這樣做的目的是將圖片的每一個(gè)象素變成0或者255,以便以計算。同時(shí),也可以去除部分噪音。
圖片的灰度化與二值化的前提是bmp圖片,如果不是,則需要首先轉換為bmp圖片。
用代碼說(shuō)話(huà),我的將圖片灰度化的代碼(算法是在網(wǎng)上搜到的):


通過(guò)將圖片灰度化,每一個(gè)象素就變成了一個(gè)0-255的灰度值。
然后是將灰度值二值化為 0 或255。一般的處理方法是設定一個(gè)區間,比如,[a,b],將[a,b]之間的灰度全部變成255,其它的變成0。這里我采用的是網(wǎng)上廣為流行的自適應二值化算法。


灰度化與二值化之前的圖片:

灰度化與二值化之后的圖片:

注:對于車(chē)牌識別來(lái)說(shuō),這個(gè)算法還不錯。對于驗證碼識別,可能需要針對特定的網(wǎng)站設計特殊的二值化算法,以過(guò)濾雜色。 (2) 去噪,然后切割成一個(gè)一個(gè)的字符 上面這張車(chē)牌切割是比較簡(jiǎn)單的,從左到右掃描一下,碰見(jiàn)空大的,咔嚓一刀,就解決了。但有一些車(chē)牌,比如這張:

簡(jiǎn)單的掃描就解決不了。因此需要一個(gè)比較通用的去噪和切割算法。這里我采用的是比較樸素的方法: 將上面的圖片看成是一個(gè)平面。將圖片向水平方向投影,這樣有字的地方的投影值就高,沒(méi)字的地方投影得到的值就低。這樣會(huì )得到一根曲線(xiàn),像一個(gè)又一個(gè)山頭。下面是我手畫(huà)示意圖:

然后,用一根掃描線(xiàn)(上圖中的S)從下向上掃描。這個(gè)掃描線(xiàn)會(huì )與圖中曲線(xiàn)存在交點(diǎn),這些交點(diǎn)會(huì )將山頭分割成一個(gè)又一個(gè)區域。車(chē)牌圖片一般是7個(gè)字符,因此,當掃描線(xiàn)將山頭分割成七個(gè)區域時(shí)停止。然后根據這七個(gè)區域向水平線(xiàn)的投影的坐標就可以將圖片中的七個(gè)字符分割出來(lái)。 但是,現實(shí)是復雜的。比如,“川”字,它的水平投影是三個(gè)山頭。按上面這種掃描方法會(huì )將它切開(kāi)。因此,對于上面的切割,需要加上約束條件:每個(gè)山頭有一個(gè)中心線(xiàn),山頭與山頭的中心線(xiàn)的距離必需在某一個(gè)值之上,否則,則需要將這兩個(gè)山頭進(jìn)行合并。加上這個(gè)約束之后,便可以有效的切割了。 以上是水平投影。然后還需要做垂直投影與切割。這里的垂直投影與切割就一個(gè)山頭,因此好處理一些。 切割結果如下: 水平投影及切割代碼:


1 public static IList<Bitmap> Split(Bitmap map, int count)
2 {
3 if (count <= 0)
4 {
5 throw new ArgumentOutOfRangeException("Count 必須大于0.");
6 }
7 IList<Bitmap> resultList = new List<Bitmap>();
8 int x = map.Width;
9 int y = map.Height;
10 int splitBitmapMinWidth = 4;
11 int[] xNormal = new int[x];
12 for (int i = 0; i < x; i++)
13 {
14 for (int j = 0; j < y; j++)
15 {
16 if (map.GetPixel(i, j).R == CharGrayValue)
17 {
18 xNormal[i]++;
19 }
20 }
21 }
22 Pair pair = new Pair();
23 for (int i = 0; i < y; i++)
24 {
25 IList<Pair> pairList = new List<Pair>(count + 1);
26 for (int j = 0; j < x; j++)
27 {
28 if (xNormal[j] >= i)
29 {
30 if ((j == (x - 1)) && (pair.Status == PairStatus.Start))
31 {
32 pair.End = j;
33 pair.Status = PairStatus.End;
34 if ((pair.End - pair.Start) >= splitBitmapMinWidth)
35 {
36 pairList.Add(pair);
37 }
38 pair = new Pair();
39 }
40 else if (pair.Status == PairStatus.JustCreated)
41 {
42 pair.Start = j;
43 pair.Status = PairStatus.Start;
44 }
45 }
46 else if (pair.Status == PairStatus.Start)
47 {
48 pair.End = j;
49 pair.Status = PairStatus.End;
50 if ((pair.End - pair.Start) >= splitBitmapMinWidth)
51 {
52 pairList.Add(pair);
53 }
54 pair = new Pair();
55 }
56 if (pairList.Count > count)
57 {
58 break;
59 }
60 }
61 if (pairList.Count == count)
62 {
63 foreach (Pair p in pairList)
64 {
65 if (p.Width < (map.Width / 10))
66 {
67 int width = (map.Width / 10) - p.Width;
68 p.Start = Math.Max(0, p.Start - (width / 2));
69 p.End = Math.Min((int) (p.End + (width / 2)), (int) (map.Width - 1));
70 }
71 }
72 foreach (Pair p in pairList)
73 {
74 int newMapWidth = (p.End - p.Start) + 1;
75 Bitmap newMap = new Bitmap(newMapWidth, y);
76 for (int ni = p.Start; ni <= p.End; ni++)
77 {
78 for (int nj = 0; nj < y; nj++)
79 {
80 newMap.SetPixel(ni - p.Start, nj, map.GetPixel(ni, nj));
81 }
82 }
83 resultList.Add(newMap);
84 }
85 return resultList;
86 }
87 }
88 return resultList;
89 }
90
代碼中的 Pair,代表掃描線(xiàn)與曲線(xiàn)的一對交點(diǎn):


PairStatus代表Pair的狀態(tài)。具體哪個(gè)狀態(tài)是什么意義,我已經(jīng)忘了。


以上這一段代碼寫(xiě)的很辛苦,因為要處理很多特殊情況。那個(gè)PairStatus 也是為處理特殊情況引進(jìn)的。
垂直投影與切割的代碼簡(jiǎn)單一些,不貼了,見(jiàn)附后的dll的BitmapConverter.TrimHeight方法。
以上用到的是樸素的去噪與切割方法。有些圖片,尤其是驗證碼圖片,需要特別的去噪處理。具體操作方法就是,打開(kāi)CxImage(http://www.codeproject.com/KB/graphics/cximage.aspx),或者Paint.Net,用上面的那些圖片處理方法,看看能否有效去噪。記住自己的操作步驟,然后翻他們的源代碼,將其中的算法提取出來(lái)。還有什么細化啊,濾波啊,這些處理可以提高圖片的質(zhì)量。具體可參考ITK的代碼或圖像處理書(shū)籍。
(3) 提取每一個(gè)字符的特征,生成特征矢量或特征矩陣
將切割出來(lái)的字符,分割成一個(gè)一個(gè)的小塊,比如3×3,5×5,或3×5,或10×8,然后統計一下每小塊的值為255的像素數量,這樣得到一個(gè)矩陣M,或者將這個(gè)矩陣簡(jiǎn)化為矢量V。
通過(guò)以上3步,就可以將一個(gè)車(chē)牌中的字符數值化為矢量了。
(1)-(3)步具體的代碼流程如下: 然后,通過(guò)spliter.ValueList就可以獲得 Bitmap map0 的矢量表示。 (4) 分類(lèi) 分類(lèi)的原理很簡(jiǎn)單。用(Vij,Ci)表示一個(gè)樣本。其中,Vij是樣本圖片經(jīng)過(guò)上面過(guò)程數值化后的矢量。Ci是人肉眼識別這張圖片,給出的結果。Vij表明,有多個(gè)樣本,它們的數值化后的矢量不同,但是它們的結果都是Ci。假設待識別的圖片矢量化后,得到的矢量是V’。 直觀(guān)上,我們會(huì )有這樣一個(gè)思路,就是這張待識別的圖片,最像樣本庫中的某張圖片,那么我們就將它當作那張圖片,將它識別為樣本庫中那張圖片事先指定的字符。 在我們眼睛里,判斷一張圖片和另一張圖片是否相似很簡(jiǎn)單,但對于電腦來(lái)說(shuō),就很難判斷了。我們前面已經(jīng)將圖片數值化為一個(gè)個(gè)維度一樣的矢量,電腦是怎樣判斷一個(gè)矢量與另一個(gè)矢量相似的呢? 這里需要計算一個(gè)矢量與另一個(gè)矢量間的距離。這個(gè)距離越短,則認為這兩個(gè)矢量越相似。 我用 SampleVector<T> 來(lái)代表矢量: T代表數據類(lèi)型,可以為Int32,也可以為Double等更精確的類(lèi)型。 測量距離的公共接口為:IMetric 常用的是MinkowskiMetric。 MinkowskiMetric是普遍使用的測度。但不一定是最有效的量。因為它對于矢量V中的每一個(gè)點(diǎn)都一視同仁。而在圖像識別中,每一個(gè)點(diǎn)的重要性卻并不一樣,例如,Q和O的識別,特征在下半部分,下半部分的權重應該大于上半部分。對于這些易混淆的字符,需要設計特殊的測量方法。在車(chē)牌識別中,其它易混淆的有D和0,0和O,I和1。Minkowski Metric識別這些字符,效果很差。因此,當碰到這些字符時(shí),需要進(jìn)行特別的處理。由于當時(shí)時(shí)間緊,我就只用了Minkowski Metric。 我的代碼中,只實(shí)現了哪個(gè)最近,就選哪個(gè)。更好的方案是用K近鄰分類(lèi)器或神經(jīng)網(wǎng)絡(luò )分類(lèi)器。K近鄰的原理是,找出和待識別的圖片(矢量)距離最近的K個(gè)樣本,然后讓這K個(gè)樣本使用某種規則計算(投票),這個(gè)新圖片屬于哪個(gè)類(lèi)別(C);神經(jīng)網(wǎng)絡(luò )則將測量的過(guò)程和投票判決的過(guò)程參數化,使它可以隨著(zhù)樣本的增加而改變,是這樣的一種學(xué)習機。有興趣的可以去看《模式分類(lèi)》一書(shū)的第三章和第四章。 二、 變態(tài)字符的識別 有些字符變形很?chē)乐?,有的字符連在一起互相交叉,有的字符被掩蓋在一堆噪音海之中。對這類(lèi)字符的識別需要用上特殊的手段。 下面介紹幾種幾個(gè)經(jīng)典的處理方法,這些方法都是被證實(shí)對某些問(wèn)題很有效的方法: (1) 切線(xiàn)距離 (Tangent Distance):可用于處理字符的各種變形,OCR的核心技術(shù)之一。 (2) 霍夫變換(Hough Transform):對噪音極其不敏感,常用于從圖片中提取各種形狀。圖像識別中最基本的方法之一。 (3) 形狀上下文(Shape Context):將特征高維化,對形變不很敏感,對噪音也不很敏感。新世紀出現的新方法。 因為這幾種方法我均未編碼實(shí)現過(guò),因此只簡(jiǎn)單介紹下原理及主要應用場(chǎng)景。 (1) 切線(xiàn)距離 前面介紹了MinkowskiMetric。這里我們看看下面這張圖:一個(gè)正寫(xiě)的1與一個(gè)歪著(zhù)的1. 

1 
2 BitmapConverter.ToGrayBmp(bitmap); // 圖片灰度化
3 BitmapConverter.Binarizate(bitmap); // 圖片二值化
4 IList<Bitmap> mapList = BitmapConverter.Split(bitmap, DefaultCharsCount); // 水平投影然后切割
5 Bitmap map0 = BitmapConverter.TrimHeight(mapList[0], DefaultHeightTrimThresholdValue); // 垂直投影然后切割
6 ImageSpliter spliter = new ImageSpliter(map0);
7 spliter.WidthSplitCount = DefaultWidthSplitCount;
8 spliter.HeightSplitCount = DefaultHeightSplitCount;
9 spliter.Init();
10 

1 public class SampleVector<T>
2 {
3 protected T[] Vector { get; set; }
4 public Int32 Dimension { get { return Vector.Length; } }
5 ……
6 }
7 

1 public interface IMetric<TElement,TReturn>
2 {
3 TReturn Compute(SampleVector<TElement> v1, SampleVector<TElement> v2);
4 }
5 

1 /// <summary>
2 /// Minkowski 測度。
3 /// </summary>
4 public class MinkowskiMetric<TElement> : IMetric<TElement, Double>
5 {
6 public Int32 Scale { get; private set; }
7 public MinkowskiMetric(Int32 scale)
8 { Scale = scale; }
9
10 public Double Compute(SampleVector<TElement> v1, SampleVector<TElement> v2)
11 {
12 if (v1 == null || v2 == null) throw new ArgumentNullException();
13 if (v1.Dimension != v2.Dimension) throw new ArgumentException("v1 和 v2 的維度不等.");
14 Double result = 0;
15 for (int i = 0; i < v1.Dimension; i++)
16 {
17 result += Math.Pow(Math.Abs(Convert.ToDouble(v1[i]) - Convert.ToDouble(v2[i])), Scale);
18 }
19 return Math.Pow(result, 1.0 / Scale);
20 }
21 }
22
23 MetricFactory 負責生產(chǎn)各種維度的MinkowskiMetric:
24
25 public class MetricFactory
26 {
27 public static IMetric<TElement, Double> CreateMinkowskiMetric<TElement>(Int32 scale)
28 {
29 return new MinkowskiMetric<TElement>(scale);
30 }
31
32 public static IMetric<TElement, Double> CreateEuclideanMetric<TElement>()
33 {
34 return CreateMinkowskiMetric<TElement>(2);
35 }
36 }
37 
用MinkowskiMetric計算的話(huà),兩者的MinkowskiMetric很大。
然而,在圖像識別中,形狀形變是常事。理論上,為了更好地識別,我們需要對每一種形變都采足夠的樣,這樣一來(lái),會(huì )發(fā)現樣本數幾乎無(wú)窮無(wú)盡,計算量越來(lái)越大。
怎么辦呢?那就是通過(guò)計算切線(xiàn)距離,來(lái)代替直接距離。切線(xiàn)距離比較抽象,我們將問(wèn)題簡(jiǎn)化為二維空間,以便以理解。

上圖有兩條曲線(xiàn)。分別是兩個(gè)字符經(jīng)過(guò)某一形變后所產(chǎn)生的軌跡。V1和V2是2個(gè)樣本。V’是待識別圖片。如果用樣本之間的直接距離,比較哪個(gè)樣本離V’最近,就將V’當作哪一類(lèi),這樣的話(huà),就要把V’分給V1了。理論上,如果我們無(wú)限取樣的話(huà),下面那一條曲線(xiàn)上的某個(gè)樣本離V’最近,V’應該歸類(lèi)為V2。不過(guò),無(wú)限取樣不現實(shí),于是就引出了切線(xiàn)距離:在樣本V1,V2處做切線(xiàn),然后計算V’離這兩條切線(xiàn)的距離,哪個(gè)最近就算哪一類(lèi)。這樣一來(lái),每一個(gè)樣本,就可以代表它附近的一個(gè)樣本區域,不需要海量的樣本,也能有效的計算不同形狀間的相似性。 深入了解切線(xiàn)距離,可參考這篇文章。Transformation invariance in pattern recognition – tangent distance and tangent propagation (http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.32.9482)這篇文章。 (2) 霍夫變換 霍夫變換出自1962年的一篇專(zhuān)利。它的原理非常簡(jiǎn)單:就是坐標變換的問(wèn)題。

如,上圖中左圖中的直線(xiàn),對應著(zhù)有圖中k-b坐標系中的一個(gè)點(diǎn)。通過(guò)坐標變換,可以將直線(xiàn)的識別轉換為點(diǎn)的識別。點(diǎn)的識別就比直線(xiàn)識別簡(jiǎn)單的多。為了避免無(wú)限大無(wú)限小問(wèn)題,常用的是如下變換公式:

下面這張圖是wikipedia上一張霍夫變換的示意圖。左圖中的兩條直線(xiàn)變換后正對應著(zhù)右圖中的兩個(gè)亮點(diǎn)。

通過(guò)霍夫變換原理可以看出,它的抗干擾性極強極強:如果直線(xiàn)不是連續的,是斷斷續續的,變換之后仍然是一個(gè)點(diǎn),只是這個(gè)點(diǎn)的強度要低一些。如果一個(gè)直線(xiàn)被一個(gè)矩形遮蓋住了,同樣不影響識別。因為這個(gè)特征,它的應用性非常廣泛。 對于直線(xiàn),圓這樣容易被參數化的圖像,霍夫變換是最擅長(cháng)處理的。對于一般的曲線(xiàn),可通過(guò)廣義霍夫變換進(jìn)行處理。感興趣的可以google之,全是數學(xué)公式,看的人頭疼。
(3) 形狀上下文

圖像中的像素點(diǎn)不是孤立的,每個(gè)像素點(diǎn),處于一個(gè)形狀背景之下,因此,在提取特征時(shí),需要將像素點(diǎn)的背景也作為該像素點(diǎn)的特征提取出來(lái),數值化。
形狀上下文(Shape Context,形狀背景)就是這樣一種方法:假定要提取像素點(diǎn)O的特征,采用上圖(c)中的坐標系,以O點(diǎn)作為坐標系的圓心。這個(gè)坐標系將O點(diǎn)的上下左右切割成了12×5=60小塊,然后統計這60小塊之內的像素的特征,將其數值化為12×5的矩陣,上圖中的(d),(e),(f)便分別是三個(gè)像素點(diǎn)的Shape Context數值化后的結果。如此一來(lái),提取的每一個(gè)點(diǎn)的特征便包括了形狀特征,加以計算,威力甚大。來(lái)看看Shape Context的威力:

上圖中的驗證碼,對Shape Context來(lái)說(shuō)只是小Case。

看看這幾張圖。嘿嘿,硬是給識別出來(lái)了。 Shape Context是新出現的方法,其威力到底有多大目前還未見(jiàn)底。這篇文章是Shape context的必讀文章:Shape Matching and Object Recognitiom using shape contexts(http://www.cs.berkeley.edu/~malik/papers/BMP-shape.pdf)。最后那兩張驗證碼識別圖出自Greg Mori,Jitendra Malik的《Recognizing Objects in Adversarial Clutter:Breaking a Visual CAPTCHA》一文。 =========================================================== 附件:第一部分的代碼(vcr.zip). 3個(gè)dll文件,反編譯看的很清晰。源代碼反而沒(méi)dll好看,我就不放了。其中,Orc.Generics.dll是幾個(gè)泛型類(lèi),Orc.ImageProcess.Common.dll 對圖像進(jìn)行處理和分割,Orc.PatternRecognition.dll 是識別部分。
聯(lián)系客服