介紹
(圖片上的字:方塊4,方塊J,黑桃2)
用機器人配上撲克牌識別系統,就可以在二十一點一類的撲克游戲中扮演荷官或是人類玩家的角色。實現這樣的程序同樣也是學習計算機視覺和模式識別的好途徑。
本文涉及到的AForge.NET框架技術有二值化、邊緣檢測、仿射變換、BLOB處理和模板匹配算法等。
需要注意的是,這篇文章和文中介紹的系統是針對英美撲克設計的,可能不適用於其他種類的撲克。然而,本文描述了撲克的檢測和識別的基本方法。因此,具體的識別算法需要根據撲克牌型特點而加以變化。
這裡有一個視頻演示。
YouTube
(話說我傳到優酷上了怎麼總是「發布中」呢?)
撲克檢測
我們需要檢測圖像(指采集到的視頻畫面,下同——野比注)上的撲克對象,以便能進行下一步的識別。為了完成檢測,我們會用一些圖像濾鏡對視頻畫面進行處理。
第一步,將圖像去色(即灰度化——野比注)。去色是將彩色圖像轉換成8bit圖像的一種操作。我們需要將彩色圖像轉換為灰度圖像以便對其進行二值化。
我們把彩色圖像轉為灰度圖像後,對其進行二值化。二值化(閾值化)是將灰度圖像轉換為黑白圖像的過程。本文使用Otsu的方法進行全局閾值化。
-
C# code
-
Bitmap temp
=
source.Clone()
as
Bitmap;
//
復制原始圖像
FiltersSequence seq
=
new
FiltersSequence(); seq.Add(Grayscale.CommonAlgorithms.BT709);
//
添加灰度濾鏡
seq.Add(
new
OtsuThreshold());
//
添加二值化濾鏡
temp
=
seq.Apply(source);
//
應用濾鏡
(圖片上的字:原始圖像、灰度圖像、二值(黑白)圖像)
有了二值圖像後,就可以用BLOB處理法檢測撲克牌了。我們使用AForge.Net的BlobCounter類完成這項任務。該類利用連通區域標記算法統計並提取出圖像中的獨立對象(即撲克牌——野比注)。
-
C# code
-
//
從圖像中提取寬度和高度大於150的blob
BlobCounter extractor
=
new
BlobCounter(); extractor.FilterBlobs
=
true
; extractor.MinWidth
=
extractor.MinHeight
=
150
; extractor.MaxWidth
=
extractor.MaxHeight
=
350
; extractor.ProcessImage(temp);
執行完上述代碼後,BlobCounter類會濾掉(去除)寬度和高度不在[150,350]像素之間的斑點(blob,即圖塊blob,圖像中的獨立對象。以下將改稱圖塊——野比注)。這有助於我們區分出圖像中其他物體(如果有的話)。根據測試環境的不同,我們需要改變濾鏡參數。例如,假設地面和相機之間距離增大,則圖像中的撲克牌會變小。此時,我們需要相應的改變最小、最大寬度和高度參數。
現在,我們可以通過調用extractor.GetObjectsInformation()方法得到所有圖塊的信息(邊緣點、矩形區域、中心點、面積、完整度,等等)。然而,我們只需要圖塊的邊緣點來計算矩形區域中心點,並通過調用PointsCloud.FindQuadriteralCorners函數來計算之。
-
C# code
-
foreach
(Blob blob
in
extractor.GetObjectsInformation()) {
//
獲取撲克牌的邊緣點
List
<
IntPoint
>
edgePoints
=
extractor.GetBlobsEdgePoints(blob);
//
利用邊緣點,在原始圖像上找到四角
List
<
IntPoint
>
corners
=
PointsCloud.FindQuadrilateralCorners(edgePoints); }
(圖片上的字:在圖像上繪制邊緣點、尋找每張撲克的角)
找到撲克牌的四角後,我們就可以從原始圖像中提取出正常的撲克牌圖像了。由上圖可以看出,撲克牌可以橫放。撲克牌是否橫放是非常容易檢測的。在撲克牌放下後,因為我們知道,牌的高度是大於寬度的,所以如果提取(轉化)圖像的寬度大於高度,那麼牌必然是橫放的。隨後,我們用RotateFlip函數旋轉撲克牌至正常位置。
注意,為了正確識別,所有的撲克應當具有相同的尺寸。不過,鑑於相機角度不同,撲克牌的尺寸是會變化的,這樣容易導致識別失敗。為了防止這樣的問題,我們把所有變換後的撲克牌圖像都調整為200x300(像素)大小。
-
C# code
-
//
用於從原始圖像提取撲克牌
QuadrilateralTransformation quadTransformer
=
new
QuadrilateralTransformation();
//
用於調整撲克牌大小
ResizeBilinear resizer
=
new
ResizeBilinear(CardWidth, CardHeight);
foreach
(Blob blob
in
extractor.GetObjectsInformation()) {
//
獲取撲克牌邊緣點
List
<
IntPoint
>
edgePoints
=
extractor.GetBlobsEdgePoints(blob);
//
利用邊緣點,在原始圖像上找到四角
List
<
IntPoint
>
corners
=
PointsCloud.FindQuadrilateralCorners(edgePoints); Bitmap cardImg
=
quadTransformer.Apply(source);
//
提取撲克牌圖像
if
(cardImg.Width
>
cardImg.Height)
//
如果撲克牌橫放
cardImg.RotateFlip(RotateFlipType.Rotate90FlipNone);
//
旋轉之
cardImg
=
resizer.Apply(cardImg);
//
歸一化(重設大小)撲克牌
..... }
(圖片上的字:使用QuadriteralTransformation類從原始圖像提取出的撲克牌。該類利用每張牌的四角進行變換。)
到目前為止,我們已經找到了原始圖像上每張撲克牌的四角,並從圖像中提取出了撲克牌,還調整到統一的尺寸。現在,我們可以開始進行識別了。
© 版權所有 野比 2012
識別撲克牌
有好幾種用於識別的技術用於識別撲克牌。本文用到的是基於牌型(如撲克牌上的形狀)及模板匹配技術。撲克牌的花色和大小是分開識別的。我們可以這樣枚舉:
-
C# code
-
public
enum
Rank { NOT_RECOGNIZED
=
0
, Ace
=
1
, Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King }
public
enum
Suit { NOT_RECOGNIZED
=
0
, Hearts, Diamonds, Spades, Clubs }
我們還將創建如下的Card類來表示識別到的撲克牌。這個類包括了牌的大小、花色、提取到的撲克牌圖像和其在原始圖像上的四角點。
-
C# code
-
public
class
Card {
//
變量
private
Rank rank;
//
大小
private
Suit suit;
//
花色
private
Bitmap image;
//
提取出的圖像
private
Point[] corners ;
//
四角點
//
屬性
public
Point[] Corners {
get
{
return
this
.corners; } }
public
Rank Rank {
set
{
this
.rank
=
value; } }
public
Suit Suit {
set
{
this
.suit
=
value; } }
public
Bitmap Image {
get
{
return
this
.image; } }
//
構造函數
public
Card(Bitmap cardImg, IntPoint[] cornerIntPoints) {
this
.image
=
cardImg;
//
將AForge.IntPoint數組轉化為System.Drawing.Point數組
int
total
=
cornerIntPoints.Length; corners
=
new
Point[total];
for
(
int
i
=
0
; i
<
total ; i
++
) {
this
.corners[i].X
=
cornerIntPoints[i].X;
this
.corners[i].Y
=
cornerIntPoints[i].Y; } } }
識別花色
標准的撲克牌花色有四種:黑桃、梅花、方塊和紅桃。其中方塊和紅桃是紅色,黑桃和梅花是黑色。再有就是方塊的寬度大於紅桃,而梅花的寬度大於黑桃。這兩個特點可以有助於我們識別花色。
識別顏色
首先,我們從識別顏色開始。正確識別出顏色,將幫助我們消除另外兩種花色。我們將通過分析撲克牌圖像的右上角來識別顏色。(作者強調過,本文基於他所選用的具體的撲克牌型,和印刷、牌面設計有關——野比注)
-
C# code
-
public
Bitmap GetTopRightPart() {
if
(image
==
null
)
return
null
; Crop crop
=
new
Crop(
new
Rectangle(image.Width
-
37
,
10
,
30
,
60
));
return
crop.Apply(image); }
(圖片上的字:裁剪 撲克圖像右上角、再次裁剪前次圖像的底部)
裁剪了撲克牌右上角後,我們得到一張30x60像素的圖像。但是該圖像同時包含了花色和大小。因為我們只是分析花色,所以再次裁剪下半部分,得到30x30像素的圖像。
現在,我們可以遍歷圖像中紅色像素和黑色像素的總數。如果一個像素的紅色分量比藍色分量和綠色分量的總和還打,就可以認為該像素是紅色。如果紅、綠、藍分量小於50,且紅色分量不大於藍色和綠色分量和,則認為該像素是黑色。
-
C# code
-
char
color
=
'
B
'
;
//
開始,鎖像素
BitmapData imageData
=
bmp.LockBits(
new
Rectangle(
0
,
0
, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat);
int
totalRed
=
0
;
int
totalBlack
=
0
;
unsafe
{
//
統計紅與黑
try
{ UnmanagedImage img
=
new
UnmanagedImage(imageData);
int
height
=
img.Height;
int
width
=
img.Width;
int
pixelSize
=
(img.PixelFormat
==
PixelFormat.Format24bppRgb)
?
3
:
4
;
byte
*
p
=
(
byte
*
)img.ImageData.ToPointer();
//
逐行
for
(
int
y
=
0
; y
<
height; y
++
) {
//
逐像素
for
(
int
x
=
0
; x
<
width; x
++
, p
+=
pixelSize) {
int
r
=
(
int
)p[RGB.R];
//
紅
int
g
=
(
int
)p[RGB.G];
//
綠
int
b
=
(
int
)p[RGB.B];
//
藍
if
(r
>
g
+
b)
//
紅 > 綠 + 藍
totalRed
++
;
//
認為是紅色
if
(r
<=
g
+
b
&&
r
<
50
&&
g
<
50
&&
b
<
50
)
//
紅綠藍均小於50
totalBlack
++
;
//
認為是黑色
} } }
finally
{ bmp.UnlockBits(imageData);
//
解鎖
} }
if
(totalRed
>
totalBlack)
//
紅色佔優
color
=
'
R
'
;
//
設置顏色為紅,否則默認黑色
return
color;
注意.NET的Bitmap.GetPixel()函數運行緩慢,所以我們使用了指針來遍歷像素。
區分人物牌和數字牌
識別了顏色後,我們需要確定撲克牌是否是人物牌。人物牌的牌面為J、Q、K。人物牌和數字牌之間有一個很突出的特點,即數字牌牌面有很多花色符號指示其大小,而人物牌很好辨認,其牌面有人物頭像。我們可以簡單的設定一個大個的花色形狀來分析撲克,而不是對其使用復雜的模板匹配算法。這樣,識別數字牌就可以變得更快。
為了找出一張撲克牌到底是人物牌還是數字牌非常簡單。人物牌上面有大的人物圖,而數字牌沒有。如果我們對牌進行邊緣檢測和圖塊(BLOB)處理,找到最大圖塊,就可以從圖塊的大小上判斷到底是人物牌還是數字牌了。
-
C# code
-
private
bool
IsFaceCard(Bitmap bmp) { FiltersSequence commonSeq
=
new
FiltersSequence(); commonSeq.Add(Grayscale.CommonAlgorithms.BT709); commonSeq.Add(
new
BradleyLocalThresholding()); commonSeq.Add(
new
DifferenceEdgeDetector()); Bitmap temp
=
this
.commonSeq.Apply(bmp); ExtractBiggestBlob extractor
=
new
ExtractBiggestBlob(); temp
=
extractor.Apply(temp);
//
提取最大圖塊
if
(temp.Width
>
bmp.Width
/
2
)
//
如果寬度大於整個牌的一般寬
return
true
;
//
人物牌
return
false
;
//
數字牌
}
所以我們不斷的對撲克牌圖像進行灰度變換、局部閾值化和邊緣檢測。注意我們使用局部閾值化而不是全局閾值化來消除照明不良的問題(即消除光線變換時,相機的自動白平衡造成的屏幕忽明忽暗現象——野比注)。
(圖片上的字(上下牌相同):原始撲克圖像,灰度化,布拉德利局部閾值化,邊緣檢測,提取最大圖塊)
正如你所看到的,人物牌最大圖塊幾乎和整張撲克牌一樣大,很容易區分。
前面提到過,出於性能上的考慮,我們將使用不同的識別技術對人物牌和數字牌進行識別。對於數字牌,我們直接提取派上最大圖塊並識別其寬度和顏色。
-
C# code
-
private
Suit ScanSuit(Bitmap suitBmp,
char
color) { Bitmap temp
=
commonSeq.Apply(suitBmp);
//
Extract biggest blob on card
ExtractBiggestBlob extractor
=
new
ExtractBiggestBlob(); temp
=
extractor.Apply(temp);
//
Biggest blob is suit blob so extract it
Suit suit
=
Suit.NOT_RECOGNIZED;
//
Determine type of suit according to its color and width
if
(color
==
'
R
'
) suit
=
temp.Width
>=
55
?
Suit.Diamonds : Suit.Hearts;
if
(color
==
'
B
'
) suit
=
temp.Width
<=
48
?
Suit.Spades : Suit.Clubs;
return
suit; }
上述測試最大誤差2像素。一般來說,因為我們把撲克牌尺寸都調整到了200x300像素,所以測試的結果都會是相同的大小。
人物牌牌面上沒有類似數字牌的最大花色圖像,只有角上的小花色圖。這就是為什麼我們會裁剪撲克圖像的右上角並對其應用模板匹配算法來識別花色。
在項目資源文件中有二值化模板圖像。(參見項目源代碼——野比注)
AForge.NET還提供了一個叫做ExhaustiveTemplateMatching的類,實現了窮盡模板匹配算法。該類對原始圖進行完全掃描,用相應的模板對每個像素進行比較。盡管該算法的性能不佳,但我們只是用於一個小區域(30x60),也不必過於關心性能。