自己來實現一個簡易的OCR


來做個簡易的字符識別 ,既然是簡易的 那么我們就不能用任何的第三方庫 。啥谷歌的 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 點安裝即可。 然后就可以進行識別了 。

 運行結果:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM