來做個簡易的字符識別 ,既然是簡易的 那么我們就不能用任何的第三方庫 。啥谷歌的 tesseract-ocr, opencv 之類的 那些玩意是叼 至少圖像處理 機器視覺這類課題對我這種高中沒畢業的人來說是一座高山 對於大多數程序員都應該算難度不小吧。 但是我們這里 這么簡陋的功能 還用那些玩意 作為一個程序員的自我修養 你還玩個球。管他代碼寫得咋個low 效率咋個低 被高手嗤之以鼻也好 其實那些高手也就那樣 把你的代碼走起來 ,這是一件很好玩的事情。 以前一直覺着這玩意挺神奇 什么OCR optical character Recognition 高大上,這三個單詞一直記不住 。好了正題:
二值化和對象分割
拿到圖像 首先二值化 就是用一種無腦的方式把淺色的背景去掉變成純白色,書上都是說二值化 這樣說感覺是要叼一些 專業一些 那么我也這樣說了。圖像上的像素數據都是一堆無意義的離散的數據。那么第一步就是要把這些離散的像素數據組織成有邏輯的 數據 也就是對象分割了,一塊整的圖片 把他分割成一個個的字符 小圖片。 網上看到別人用投影直方圖的方式 這樣做可以很容易 分割一行排的字符。 但是我原來還想做一個簡易的“數細胞”的算法 干脆就一並實現了吧 正好這里也可以用得上 ,數細胞明白否 就是一副白紙上 一坨 一坨的 每一坨的形狀都不一樣 我們要用程序判斷它總共有多少坨 只要是連在一起 哪怕是一根細線連着的 都算一坨。 當然也可以分割開 涉及到形態學 啥的 這里面太深奧了 暫時我還沒准備深入研究 。 基於他的原理你們也知道了 不能判斷小寫字母i 這樣的 因為一點加一豎 的方式 。這也是為啥那些成熟的OCR軟件里都容易把掃描文本里比較粗糙有毛邊的i 識別成 1 加 ' 。 好 我們就用這種方式 只是為了演示原理 我們這里也只准備進行數字識別, 正好數字0~9 每一個字符也都是連着的。
我們還是用我原來的巡路用過的算法 擴散大法 ,書面叫廣度搜索 本來在原來是用來進行路徑聯通測試的,說明這玩意的用處還挺多的 威力無窮啊。 就這樣隨便從黑坨里取一個像素 作為種子 就像一滴水一樣 讓他去擴散 污染整個池塘。什么時候返回 也很簡單 當觸角不能再延伸了 自然就返回了。 污染后把整個池塘刪除 放到 邏輯數據集里去 ,然后又從所有黑色像素里取一個種子像素 如此往復就把這一堆離散的像素點變得有意義了,我們一個個的字符也分割出來了 並且還有個好處 單個字符的每個像素點我們都知曉 進而可以計算字符的像素面積 ,這就可以把小的噪點過濾掉 然后 還可以定位每個字符的位置 寬 高。上面的做法效率是很低的 尤其字符面積過大 ,其實正統的做法應該是使用邊緣查找,邊緣查找的原理: 假設從上下左右有四堵牆往中間推 把遇到的所有第一個黑色像素 確定為邊緣。 然后找一個像素 八方向查找 依次連城一個路徑 直到找到起始點 則連成一個完整的閉塞區域,當然這個東西也不是那么簡單的 比如遇到238這樣的 ,任何東西運行都要有嚴密而行得通的理論支持。
對象分割的部分核心代碼:
1 public Bitmap objSegmentation() 2 { 3 if (stu > Status.readyToTransform) 4 return sourceImg; 5 else if (stu == Status.waitSourceImg) 6 return null; 7 8 if (sourceImg == null) 9 return null; 10 11 bool Over = false; 12 while (Over == false) 13 { 14 //取得一個種子像素 15 node pxs = null; 16 foreach (var item in blackPixs) 17 { 18 if (item.accessed == false) 19 { 20 pxs = item; 21 break; 22 } 23 } 24 25 //根據種子像素找出被污染的區域 並把對應的位置設置為已訪問 26 //設置第一個節點 27 startPoint = new Point(pxs.x, pxs.y); 28 zouguo = new Dictionary<int, List<node>>(); 29 int qibu = 0; 30 List<node> stepOne = new List<node>(); 31 stepOne.Add(new node() { parent = startPoint, current = startPoint }); 32 zouguo.Add(qibu, stepOne); 33 qibu++; 34 35 //進行廣度搜索 直到搜索完一片區域為止 36 bool isgogogo = false; 37 do 38 { 39 isgogogo = besideOf(qibu - 1); 40 qibu++; 41 //if (qibu > 10) 42 // break; 43 } while (isgogogo); 44 45 //遍歷當前被腐蝕的那一片區域 46 //並把所有節點添加到一個線性數組里去 47 48 int top = height - 1; 49 int bottom = 0; 50 int left = cols - 1; 51 int right = 0; 52 53 RegionOfObj bedestory = new RegionOfObj(); 54 bedestory.pixs = new List<Point>(); 55 foreach (var item in zouguo.Values) 56 { 57 foreach (var item2 in item) 58 { 59 bedestory.pixs.Add(item2.current); 60 //找出黑色像素里已經被腐蝕過的 把標示設置為已訪問 61 for (int i = 0; i < blackPixs.Count; i++) 62 { 63 if (item2.current.X == blackPixs[i].x && item2.current.Y == blackPixs[i].y) 64 { 65 blackPixs[i].accessed = true; 66 if (blackPixs[i].x > right) 67 right = blackPixs[i].x; 68 if (blackPixs[i].x < left) 69 left = blackPixs[i].x; 70 if (blackPixs[i].y < top) 71 top = blackPixs[i].y; 72 if (blackPixs[i].y > bottom) 73 bottom = blackPixs[i].y; 74 } 75 } 76 } 77 } 78 79 Rectangle rec = new Rectangle(left, top, right - left + 1, bottom - top + 1); 80 81 82 bedestory.rect = rec; 83 //往最終呈現數據里加入結果 84 groupedObj.Add(bedestory); 85 86 //直到黑色像素所有的區域都被訪問 就退出 87 Over = true; 88 foreach (var item in blackPixs) 89 { 90 if (item.accessed == false) 91 { 92 Over = false; 93 break; 94 } 95 } 96 //break; 97 } 98 99 100 stu = Status.readyToRecognition; 101 return sourceImg; 102 }
模板匹配
然后就是進行識別了 網上隨便一找 都知道是用 模板匹配的方式,翻了兩本書 也都是說的用這種方式。要說的話這確實沒啥技術含量 挺簡單的,就是簡單的像素比對 差異化的像素占總像素比過大則認為不匹配 。 我們也不是無腦的拿固定大小的模板圖片去比對 既然我們字符都分割定位了 寬高都知道,首先 我們的模板字符是比較大 比較清晰的 然后縮放到分割字符的大小 然后才進行像素比對。
模板匹配部分核心代碼:
1 public string recognition() 2 { 3 if (stu == Status.waitSourceImg) 4 return ""; 5 else if (stu > Status.readyToRecognition) 6 return recognition_result; 7 else if (stu == Status.readyToTransform) 8 objSegmentation(); 9 10 11 //如果沒有模板文件 則生成他 12 if (File.Exists("0.png") == false || File.Exists("1.png") == false || File.Exists("2.png") == false || 13 File.Exists("3.png") == false || File.Exists("4.png") == false || File.Exists("5.png") == false || 14 File.Exists("6.png") == false || File.Exists("7.png") == false || File.Exists("8.png") == false || 15 File.Exists("9.png") == false) 16 createTempleFile(); 17 18 19 //載入模板 20 Image[] templateImg = new Image[10]{ 21 Image.FromFile("0.png"),Image.FromFile("1.png"),Image.FromFile("2.png"),Image.FromFile("3.png"),Image.FromFile("4.png"), 22 Image.FromFile("5.png"),Image.FromFile("6.png"),Image.FromFile("7.png"),Image.FromFile("8.png"),Image.FromFile("9.png")}; 23 24 GraphicsUnit uu = GraphicsUnit.Pixel; 25 string result = ""; 26 for (int i = 0; i < groupedObj.Count; i++)//遍歷所有對象 27 { 28 float mach = 0.000f; 29 string chr_tmp = " "; 30 for (int j = 0; j < templateImg.Length; j++)//0-9每個字符進行比對 31 { 32 //處理等比例縮放 算了也不用等比例了。 33 Bitmap scaleImg = new Bitmap(groupedObj[i].rect.Width, groupedObj[i].rect.Height); 34 Graphics gph = Graphics.FromImage(scaleImg); 35 gph.Clear(Color.White); 36 gph.DrawImage(templateImg[j], scaleImg.GetBounds(ref uu), templateImg[j].GetBounds(ref uu), GraphicsUnit.Pixel); 37 38 float mach_tmp = 0; 39 for (int k = 0; k < scaleImg.Height; k++) 40 { 41 for (int l = 0; l < scaleImg.Width; l++) 42 { 43 Color tmp_cor = scaleImg.GetPixel(l, k); 44 Color trg_cor = sourceImg.GetPixel(groupedObj[i].rect.Location.X + l, groupedObj[i].rect.Location.Y + k); 45 if (tmp_cor.R == trg_cor.R && tmp_cor.G == trg_cor.G && tmp_cor.B == trg_cor.B)//如果像素匹配上 46 mach_tmp += 1; 47 } 48 } 49 if ((mach_tmp / (float)(groupedObj[i].rect.Width * groupedObj[i].rect.Height)) > mach) 50 { 51 mach = (mach_tmp / (float)(groupedObj[i].rect.Width * groupedObj[i].rect.Height)); 52 chr_tmp = j.ToString(); 53 } 54 } 55 if (mach < 0.6f) 56 result += "?"; 57 else 58 result += chr_tmp; 59 } 60 recognition_result = result; 61 stu = Status.complete; 62 return result; 63 }
本來准備把模板跟目標區域進行等比例縮放的,后來仔細一想算了這不是多事嗎 並且這樣還有一個好處 ,就是高度進行壓縮了的字符也可以識別出來。 搞完了 看得出來 我們這個只算是最初級最初級的 只能夠去識別那種解放前水平的驗證碼。現在的驗證碼也不是那么好識別的 做驗證碼的人只要大概了解識別原理 都可以給識別的人制造成倍的難度 ,對於現在的有些驗證碼 即使是高手 做自動識別都不是那么容易的。
不要問我這可不可以用來識別身份證號 之類的 。我可以負責的告訴你 肯定是可以的 。身份證號識別那個本身難度就是比較低的。 首先身份證號 的位置 在整個身份證版面中 都是固定的 把那一塊截取出來 進行處理就可以了 ,然后 身份證號所使用的字體叫 "OCR-B 10 BT" 我也不知道啥意思 意思是專利於進行OCR識別的字體?OCR-B: An isO recognized machine-readable typeface that is designed to be more legible to humans than OCR-A 這種字體電腦上是沒有的 需要進行安裝下 打開OCR-B 10 BT.ttf 點安裝即可。 然后就可以進行識別了 。
運行結果: