© 版權所有 Conmajia 2012
原文地址:點擊查看
作者:Nazmi Altun
Nazmi Altun著,Conmajia 譯
介紹

(圖片上的字:方塊4,方塊J,黑桃2)
用機器人配上撲克牌識別系統,就可以在二十一點一類的撲克游戲中扮演荷官或是人類玩家的角色。實現這樣的程序同樣也是學習計算機視覺和模式識別的好途徑。
本文涉及到的AForge.NET框架技術有二值化、邊緣檢測、仿射變換、BLOB處理和模板匹配算法等。
需要注意的是,這篇文章和文中介紹的系統是針對英美撲克設計的,可能不適用於其他種類的撲克。然而,本文描述了撲克的檢測和識別的基本方法。因此,具體的識別算法需要根據撲克牌型特點而加以變化。
這里有一個視頻演示。
(已經搬運到優酷——野比注)
© 版權所有 野比 2012
撲克檢測
我們需要檢測圖像(指采集到的視頻畫面,下同——野比注)上的撲克對象,以便能進行下一步的識別。為了完成檢測,我們會用一些圖像濾鏡對視頻畫面進行處理。
第一步,將圖像去色(即灰度化——野比注)。去色是將彩色圖像轉換成8bit圖像的一種操作。我們需要將彩色圖像轉換為灰度圖像以便對其進行二值化。
我們把彩色圖像轉為灰度圖像后,對其進行二值化。二值化(閾值化)是將灰度圖像轉換為黑白圖像的過程。本文使用Otsu的方法進行全局閾值化。
1 Bitmap temp = source.Clone() as Bitmap; // 復制原始圖像 2 3 FiltersSequence seq = new FiltersSequence(); 4 seq.Add(Grayscale.CommonAlgorithms.BT709); // 添加灰度濾鏡 5 seq.Add(new OtsuThreshold()); // 添加二值化濾鏡 6 temp = seq.Apply(source); // 應用濾鏡

(圖片上的字:原始圖像、灰度圖像、二值(黑白)圖像)
有了二值圖像后,就可以用BLOB處理法檢測撲克牌了。我們使用AForge.Net的BlobCounter類完成這項任務。該類利用連通區域標記算法統計並提取出圖像中的獨立對象(即撲克牌——野比注)。
1 // 從圖像中提取寬度和高度大於150的blob 2 BlobCounter extractor = new BlobCounter(); 3 extractor.FilterBlobs = true; 4 extractor.MinWidth = extractor.MinHeight = 150; 5 extractor.MaxWidth = extractor.MaxHeight = 350; 6 extractor.ProcessImage(temp);
執行完上述代碼后,BlobCounter類會濾掉(去除)寬度和高度不在[150,350]像素之間的斑點(blob,即圖塊blob,圖像中的獨立對象。以下將改稱圖塊——野比注)。這有助於我們區分出圖像中其他物體(如果有的話)。根據測試環境的不同,我們需要改變濾鏡參數。例如,假設地面和相機之間距離增大,則圖像中的撲克牌會變小。此時,我們需要相應的改變最小、最大寬度和高度參數。
現在,我們可以通過調用extractor.GetObjectsInformation()方法得到所有圖塊的信息(邊緣點、矩形區域、中心點、面積、完整度,等等)。然而,我們只需要圖塊的邊緣點來計算矩形區域中心點,並通過調用PointsCloud.FindQuadriteralCorners函數來計算之。
1 foreach (Blob blob in extractor.GetObjectsInformation()) 2 { 3 // 獲取撲克牌的邊緣點 4 List< IntPoint > edgePoints = extractor.GetBlobsEdgePoints(blob); 5 // 利用邊緣點,在原始圖像上找到四角 6 List< IntPoint > corners = PointsCloud.FindQuadrilateralCorners(edgePoints); 7 }

(圖片上的字:在圖像上繪制邊緣點、尋找每張撲克的角)
找到撲克牌的四角后,我們就可以從原始圖像中提取出正常的撲克牌圖像了。由上圖可以看出,撲克牌可以橫放。撲克牌是否橫放是非常容易檢測的。在撲克牌放下后,因為我們知道,牌的高度是大於寬度的,所以如果提取(轉化)圖像的寬度大於高度,那么牌必然是橫放的。隨后,我們用RotateFlip函數旋轉撲克牌至正常位置。
注意,為了正確識別,所有的撲克應當具有相同的尺寸。不過,鑒於相機角度不同,撲克牌的尺寸是會變化的,這樣容易導致識別失敗。為了防止這樣的問題,我們把所有變換后的撲克牌圖像都調整為200x300(像素)大小。
1 // 用於從原始圖像提取撲克牌 2 QuadrilateralTransformation quadTransformer = new QuadrilateralTransformation(); 3 // 用於調整撲克牌大小 4 ResizeBilinear resizer = new ResizeBilinear(CardWidth, CardHeight); 5 6 foreach (Blob blob in extractor.GetObjectsInformation()) 7 { 8 // 獲取撲克牌邊緣點 9 List<IntPoint> edgePoints = extractor.GetBlobsEdgePoints(blob); 10 // 利用邊緣點,在原始圖像上找到四角 11 List<IntPoint> corners = PointsCloud.FindQuadrilateralCorners(edgePoints); 12 Bitmap cardImg = quadTransformer.Apply(source); // 提取撲克牌圖像 13 14 if (cardImg.Width > cardImg.Height) // 如果撲克牌橫放 15 cardImg.RotateFlip(RotateFlipType.Rotate90FlipNone); // 旋轉之 16 cardImg = resizer.Apply(cardImg); // 歸一化(重設大小)撲克牌 17 ..... 18 }

(圖片上的字:使用QuadriteralTransformation類從原始圖像提取出的撲克牌。該類利用每張牌的四角進行變換。)
到目前為止,我們已經找到了原始圖像上每張撲克牌的四角,並從圖像中提取出了撲克牌,還調整到統一的尺寸。現在,我們可以開始進行識別了。
© 版權所有 野比 2012
識別撲克牌
有好幾種用於識別的技術用於識別撲克牌。本文用到的是基於牌型(如撲克牌上的形狀)及模板匹配技術。撲克牌的花色和大小是分開識別的。我們可以這樣枚舉:
1 public enum Rank 2 { 3 NOT_RECOGNIZED = 0, 4 Ace = 1, 5 Two, 6 Three, 7 Four, 8 Five, 9 Six, 10 Seven, 11 Eight, 12 Nine, 13 Ten, 14 Jack, 15 Queen, 16 King 17 } 18 public enum Suit 19 { 20 NOT_RECOGNIZED = 0, 21 Hearts, 22 Diamonds, 23 Spades, 24 Clubs 25 }
我們還將創建如下的Card類來表示識別到的撲克牌。這個類包括了牌的大小、花色、提取到的撲克牌圖像和其在原始圖像上的四角點。
1 public class Card 2 { 3 // 變量 4 private Rank rank; // 大小 5 private Suit suit; // 花色 6 private Bitmap image; // 提取出的圖像 7 private Point[] corners ;// 四角點 8 9 // 屬性 10 public Point[] Corners 11 { 12 get { return this.corners; } 13 } 14 public Rank Rank 15 { 16 set { this.rank = value; } 17 } 18 public Suit Suit 19 { 20 set { this.suit = value; } 21 } 22 public Bitmap Image 23 { 24 get { return this.image; } 25 } 26 // 構造函數 27 public Card(Bitmap cardImg, IntPoint[] cornerIntPoints) 28 { 29 this.image = cardImg; 30 31 // 將AForge.IntPoint數組轉化為System.Drawing.Point數組 32 int total = cornerIntPoints.Length; 33 corners = new Point[total]; 34 35 for(int i = 0 ; i < total ; i++) 36 { 37 this.corners[i].X = cornerIntPoints[i].X; 38 this.corners[i].Y = cornerIntPoints[i].Y; 39 } 40 } 41 }
© 版權所有 野比 2012
識別花色
標准的撲克牌花色有四種:黑桃、梅花、方塊和紅桃。其中方塊和紅桃是紅色,黑桃和梅花是黑色。再有就是方塊的寬度大於紅桃,而梅花的寬度大於黑桃。這兩個特點可以有助於我們識別花色。
識別顏色
首先,我們從識別顏色開始。正確識別出顏色,將幫助我們消除另外兩種花色。我們將通過分析撲克牌圖像的右上角來識別顏色。(作者強調過,本文基於他所選用的具體的撲克牌型,和印刷、牌面設計有關——野比注)
1 public Bitmap GetTopRightPart() 2 { 3 if (image == null) 4 return null; 5 Crop crop = new Crop(new Rectangle(image.Width - 37, 10, 30, 60)); 6 7 return crop.Apply(image); 8 }

(圖片上的字:裁剪 撲克圖像右上角、再次裁剪前次圖像的底部)
裁剪了撲克牌右上角后,我們得到一張30x60像素的圖像。但是該圖像同時包含了花色和大小。因為我們只是分析花色,所以再次裁剪下半部分,得到30x30像素的圖像。
現在,我們可以遍歷圖像中紅色像素和黑色像素的總數。如果一個像素的紅色分量比藍色分量和綠色分量的總和還打,就可以認為該像素是紅色。如果紅、綠、藍分量小於50,且紅色分量不大於藍色和綠色分量和,則認為該像素是黑色。
1 char color = 'B'; 2 // 開始,鎖像素 3 BitmapData imageData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), 4 ImageLockMode.ReadOnly, bmp.PixelFormat); 5 int totalRed = 0; 6 int totalBlack = 0; 7 8 unsafe 9 { 10 // 統計紅與黑 11 try 12 { 13 UnmanagedImage img = new UnmanagedImage(imageData); 14 15 int height = img.Height; 16 int width = img.Width; 17 int pixelSize = (img.PixelFormat == PixelFormat.Format24bppRgb) ? 3 : 4; 18 byte* p = (byte*)img.ImageData.ToPointer(); 19 20 // 逐行 21 for (int y = 0; y < height; y++) 22 { 23 // 逐像素 24 for (int x = 0; x < width; x++, p += pixelSize) 25 { 26 int r = (int)p[RGB.R]; // 紅 27 int g = (int)p[RGB.G]; // 綠 28 int b = (int)p[RGB.B]; // 藍 29 30 if (r > g + b) // 紅 > 綠 + 藍 31 totalRed++; // 認為是紅色 32 33 if (r <= g + b && r < 50 && g < 50 && b < 50) // 紅綠藍均小於50 34 totalBlack++; // 認為是黑色 35 } 36 } 37 } 38 finally 39 { 40 bmp.UnlockBits(imageData); // 解鎖 41 } 42 } 43 if (totalRed > totalBlack) // 紅色占優 44 color = 'R'; // 設置顏色為紅,否則默認黑色 45 return color;

注意.NET的Bitmap.GetPixel()函數運行緩慢,所以我們使用了指針來遍歷像素。
區分人物牌和數字牌
識別了顏色后,我們需要確定撲克牌是否是人物牌。人物牌的牌面為J、Q、K。人物牌和數字牌之間有一個很突出的特點,即數字牌牌面有很多花色符號指示其大小,而人物牌很好辨認,其牌面有人物頭像。我們可以簡單的設定一個大個的花色形狀來分析撲克,而不是對其使用復雜的模板匹配算法。這樣,識別數字牌就可以變得更快。
為了找出一張撲克牌到底是人物牌還是數字牌非常簡單。人物牌上面有大的人物圖,而數字牌沒有。如果我們對牌進行邊緣檢測和圖塊(BLOB)處理,找到最大圖塊,就可以從圖塊的大小上判斷到底是人物牌還是數字牌了。
1 private bool IsFaceCard(Bitmap bmp) 2 { 3 FiltersSequence commonSeq = new FiltersSequence(); 4 commonSeq.Add(Grayscale.CommonAlgorithms.BT709); 5 commonSeq.Add(new BradleyLocalThresholding()); 6 commonSeq.Add(new DifferenceEdgeDetector()); 7 8 Bitmap temp = this.commonSeq.Apply(bmp); 9 ExtractBiggestBlob extractor = new ExtractBiggestBlob(); 10 temp = extractor.Apply(temp); // 提取最大圖塊 11 12 if (temp.Width > bmp.Width / 2) // 如果寬度大於整個牌的一般寬 13 return true; // 人物牌 14 15 return false; // 數字牌 16 }
所以我們不斷的對撲克牌圖像進行灰度變換、局部閾值化和邊緣檢測。注意我們使用局部閾值化而不是全局閾值化來消除照明不良的問題(即消除光線變換時,相機的自動白平衡造成的屏幕忽明忽暗現象——野比注)。

(圖片上的字(上下牌相同):原始撲克圖像,灰度化,布拉德利局部閾值化,邊緣檢測,提取最大圖塊)
正如你所看到的,人物牌最大圖塊幾乎和整張撲克牌一樣大,很容易區分。
前面提到過,出於性能上的考慮,我們將使用不同的識別技術對人物牌和數字牌進行識別。對於數字牌,我們直接提取派上最大圖塊並識別其寬度和顏色。
1 private Suit ScanSuit(Bitmap suitBmp, char color) 2 { 3 Bitmap temp = commonSeq.Apply(suitBmp); 4 //Extract biggest blob on card 5 ExtractBiggestBlob extractor = new ExtractBiggestBlob(); 6 temp = extractor.Apply(temp); //Biggest blob is suit blob so extract it 7 Suit suit = Suit.NOT_RECOGNIZED; 8 9 //Determine type of suit according to its color and width 10 if (color == 'R') 11 suit = temp.Width >= 55 ? Suit.Diamonds : Suit.Hearts; 12 if (color == 'B') 13 suit = temp.Width <= 48 ? Suit.Spades : Suit.Clubs; 14 15 return suit; 16 }

上述測試最大誤差2像素。一般來說,因為我們把撲克牌尺寸都調整到了200x300像素,所以測試的結果都會是相同的大小。
人物牌牌面上沒有類似數字牌的最大花色圖像,只有角上的小花色圖。這就是為什么我們會裁剪撲克圖像的右上角並對其應用模板匹配算法來識別花色。
在項目資源文件中有二值化模板圖像。(參見項目源代碼——野比注)
AForge.NET還提供了一個叫做ExhaustiveTemplateMatching的類,實現了窮盡模板匹配算法。該類對原始圖進行完全掃描,用相應的模板對每個像素進行比較。盡管該算法的性能不佳,但我們只是用於一個小區域(30x60),也不必過於關心性能。
1 private Suit ScanFaceSuit(Bitmap bmp, char color) 2 { 3 Bitmap clubs, diamonds, spades, hearts; // 花色模板 4 5 // 載入模板資源 6 clubs = PlayingCardRecognition.Properties.Resources.Clubs; 7 diamonds = PlayingCardRecognition.Properties.Resources.Diamonds; 8 spades = PlayingCardRecognition.Properties.Resources.Spades; 9 hearts = PlayingCardRecognition.Properties.Resources.Hearts; 10 11 // 用0.8的相似度閾值初始化模板匹配類 12 ExhaustiveTemplateMatching templateMatching = new ExhaustiveTemplateMatching(0.8f); 13 Suit suit = Suit.NOT_RECOGNIZED; 14 15 if (color == 'R') // 如果是紅色 16 { 17 if (templateMatching.ProcessImage(bmp, hearts).Length > 0) 18 suit = Suit.Hearts; //匹配紅桃 19 if (templateMatching.ProcessImage(bmp, diamonds).Length > 0) 20 suit = Suit.Diamonds; // 匹配方塊 21 } 22 else // 如果是黑色 23 { 24 if (templateMatching.ProcessImage(bmp,spades).Length > 0) 25 suit = Suit.Spades; // 匹配黑桃 26 if (templateMatching.ProcessImage(bmp, clubs).Length > 0) 27 suit = Suit.Clubs; // 匹配梅花 28 } 29 return suit; 30 }

(圖片上的字:上面是,模板匹配?是。下面是,模板匹配?否)
當然,模板不能100%匹配樣本,所以我們使用0.8(80%)的相似度閾值。
識別大小
識別大小和識別花色類似,也是單獨對人物牌和數字牌進行識別。由於數字牌可以只靠計算牌面上的花色圖塊數量就可以識別,而不用模板匹配,所以利用簡單的圖像濾鏡就可以完成任務。
下面所示的ScanRank函數過濾小圖塊(小於30像素長或寬)並計算剩余的圖塊數。
1 private Rank ScanRank(Bitmap cardImage) 2 { 3 Rank rank = Rank.NOT_RECOGNIZED; 4 5 int total = 0; 6 Bitmap temp = commonSeq.Apply(cardImage); // 應用濾鏡 7 BlobCounter blobCounter = new BlobCounter(); 8 blobCounter.FilterBlobs = true; 9 // 過濾小圖塊 10 blobCounter.MinHeight = blobCounter.MinWidth = 30; 11 blobCounter.ProcessImage(temp); 12 13 total = blobCounter.GetObjectsInformation().Length; // 獲取總數 14 rank = (Rank)total; // 轉換成大小(枚舉類型) 15 16 return rank; 17 }

(圖片上的字:邊緣檢測,過濾寬高小於30像素的圖塊,剩余圖塊總數為10,即為撲克牌的點數)
所以,數字牌不用模板匹配算法或是OCR即可識別。但是,對人物卡,我們需要再次使用模板匹配進行識別。
1 private Rank ScanFaceRank(Bitmap bmp) 2 { 3 Bitmap j, k, q; // 人物牌人物模板 4 // 載入資源 5 j = PlayingCardRecognition.Properties.Resources.J; 6 k = PlayingCardRecognition.Properties.Resources.K; 7 q = PlayingCardRecognition.Properties.Resources.Q; 8 9 10 // 用0.75進行初始化 11 ExhaustiveTemplateMatching templateMatchin = 12 new ExhaustiveTemplateMatching(0.75f); 13 Rank rank = Rank.NOT_RECOGNIZED; 14 15 if (templateMatchin.ProcessImage(bmp, j).Length > 0) // J 16 rank = Rank.Jack; 17 if (templateMatchin.ProcessImage(bmp, k).Length > 0)// K 18 rank = Rank.King; 19 if (templateMatchin.ProcessImage(bmp, q).Length > 0)// Q 20 rank = Rank.Queen; 21 22 return rank; 23 }

由於識別難度較大,這次我們使用0.75(75%)作為相似度閾值。
已知問題
本文的實現,只能識別分開的撲克牌(沒有重疊——野比注)。另一個已知問題是光線環境變化常造成識別錯誤。
© 版權所有 野比 2012
結論
本文用到的圖像用例來自AForge.NET框架。AForge.NET為機器視覺和機器學習領域的開發者提供了大量有用的特性。對我來說,它同樣非常簡單。
本文還可提高,例如如何在牌還沒有分放置的時候就進行識別。另一種提升是用這套系統做成AI二十一點玩家。
歷史
* 7th, Oct., 2011: 初稿
許可
本文及附帶的源文件代碼和文件,遵循代碼計划網站開源許可(CPOL)
關於作者
Nazmi Altun
Softeare Developer
Turkey
© 版權所有 Conmajia 2012
(全文完)
