「玩一玩」翻译:基于AForge.Net的扑克牌视频识别程序

Conmajia 2012-05-24 10:02:06
加精
看到有趣的东西,忍不住翻译过来了。

----------------



© 版权所有 野比 2012
原文地址:点击查看
作者:Nazmi Altun「土耳其」

源码下载:点我

demo下载:点我

介绍

(图片上的字:方块4,方块J,黑桃2)

用机器人配上扑克牌识别系统,就可以在二十一点一类的扑克游戏中扮演荷官或是人类玩家的角色。实现这样的程序同样也是学习计算机视觉和模式识别的好途径。
本文涉及到的AForge.NET框架技术有二值化、边缘检测、仿射变换、BLOB处理和模板匹配算法等。
需要注意的是,这篇文章和文中介绍的系统是针对英美扑克设计的,可能不适用于其他种类的扑克。然而,本文描述了扑克的检测和识别的基本方法。因此,具体的识别算法需要根据扑克牌型特点而加以变化。
这里有一个视频演示。
YouTube
直接访问 通过代理访问(在代理页面输入http://www.youtube.com/watch?v=dui3ftwsuhM然后访问)

(话说我传到优酷上了怎么总是「发布中」呢?)

扑克检测
我们需要检测图像(指采集到的视频画面,下同——野比注)上的扑克对象,以便能进行下一步的识别。为了完成检测,我们会用一些图像滤镜对视频画面进行处理。
第一步,将图像去色(即灰度化——野比注)。去色是将彩色图像转换成8bit图像的一种操作。我们需要将彩色图像转换为灰度图像以便对其进行二值化。
我们把彩色图像转为灰度图像后,对其进行二值化。二值化(阈值化)是将灰度图像转换为黑白图像的过程。本文使用Otsu的方法进行全局阈值化。

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类完成这项任务。该类利用连通区域标记算法统计并提取出图像中的独立对象(即扑克牌——野比注)。
// 从图像中提取宽度和高度大于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函数来计算之。
foreach (Blob blob in extractor.GetObjectsInformation())
{
// 获取扑克牌的边缘点
List< IntPoint > edgePoints = extractor.GetBlobsEdgePoints(blob);
// 利用边缘点,在原始图像上找到四角
List< IntPoint > corners = PointsCloud.FindQuadrilateralCorners(edgePoints);
}


(图片上的字:在图像上绘制边缘点、寻找每张扑克的角)
找到扑克牌的四角后,我们就可以从原始图像中提取出正常的扑克牌图像了。由上图可以看出,扑克牌可以横放。扑克牌是否横放是非常容易检测的。在扑克牌放下后,因为我们知道,牌的高度是大于宽度的,所以如果提取(转化)图像的宽度大于高度,那么牌必然是横放的。随后,我们用RotateFlip函数旋转扑克牌至正常位置。
注意,为了正确识别,所有的扑克应当具有相同的尺寸。不过,鉴于相机角度不同,扑克牌的尺寸是会变化的,这样容易导致识别失败。为了防止这样的问题,我们把所有变换后的扑克牌图像都调整为200x300(像素)大小。
// 用于从原始图像提取扑克牌
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
识别扑克牌
有好几种用于识别的技术用于识别扑克牌。本文用到的是基于牌型(如扑克牌上的形状)及模板匹配技术。扑克牌的花色和大小是分开识别的。我们可以这样枚举:
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类来表示识别到的扑克牌。这个类包括了牌的大小、花色、提取到的扑克牌图像和其在原始图像上的四角点。
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;
}
}
}

识别花色
标准的扑克牌花色有四种:黑桃、梅花、方块和红桃。其中方块和红桃是红色,黑桃和梅花是黑色。再有就是方块的宽度大于红桃,而梅花的宽度大于黑桃。这两个特点可以有助于我们识别花色。
识别颜色
首先,我们从识别颜色开始。正确识别出颜色,将帮助我们消除另外两种花色。我们将通过分析扑克牌图像的右上角来识别颜色。(作者强调过,本文基于他所选用的具体的扑克牌型,和印刷、牌面设计有关——野比注)
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,且红色分量不大于蓝色和绿色分量和,则认为该像素是黑色。
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)处理,找到最大图块,就可以从图块的大小上判断到底是人物牌还是数字牌了。
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; // 数字牌
}


所以我们不断的对扑克牌图像进行灰度变换、局部阈值化和边缘检测。注意我们使用局部阈值化而不是全局阈值化来消除照明不良的问题(即消除光线变换时,相机的自动白平衡造成的屏幕忽明忽暗现象——野比注)。

(图片上的字(上下牌相同):原始扑克图像,灰度化,布拉德利局部阈值化,边缘检测,提取最大图块)
正如你所看到的,人物牌最大图块几乎和整张扑克牌一样大,很容易区分。
前面提到过,出于性能上的考虑,我们将使用不同的识别技术对人物牌和数字牌进行识别。对于数字牌,我们直接提取派上最大图块并识别其宽度和颜色。
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),也不必过于关心性能。

...全文
4989 92 打赏 收藏 转发到动态 举报
写回复
用AI写文章
92 条回复
切换为时间正序
请发表友善的回复…
发表回复
ljasaa 2012-07-05
  • 打赏
  • 举报
回复
哪位朋友能帮我做一个安卓下的麻将识别程序。只识别13张牌 有兴趣谈功能与价格 13537549080
larissa523 2012-06-27
  • 打赏
  • 举报
回复
感谢分享。
yojinlin 2012-06-20
  • 打赏
  • 举报
回复
感謝分享。
christ 2012-06-20
  • 打赏
  • 举报
回复
有意思
  • 打赏
  • 举报
回复
很炫啊
hg2980986 2012-06-03
  • 打赏
  • 举报
回复
smamqm 2012-06-01
  • 打赏
  • 举报
回复
我下载的怎么报错一大堆?
jsp789 2012-06-01
  • 打赏
  • 举报
回复
很好很好
yojinlin 2012-05-31
  • 打赏
  • 举报
回复
感謝分享。
Johnny_Bao 2012-05-31
  • 打赏
  • 举报
回复
看不懂
馒头仔 2012-05-31
  • 打赏
  • 举报
回复
最近也在用AForge.NET框架搞项目
lyroot 2012-05-30
  • 打赏
  • 举报
回复
看样子花了不少心思,顶一下!多谢!
dan2323 2012-05-30
  • 打赏
  • 举报
回复
魔术一般!
向上一区 2012-05-30
  • 打赏
  • 举报
回复
学习了,有时间要好好学习,踩点了。
simona0805 2012-05-29
  • 打赏
  • 举报
回复
感谢楼主分享
alan817 2012-05-29
  • 打赏
  • 举报
回复
看不懂
Conmajia 2012-05-28
  • 打赏
  • 举报
回复
[Quote=引用 35 楼 的回复:]

http://v.youku.com/v_show/id_XMzg1MTAwOTky.html

楼主视屏挺酷。
[/Quote]

你这视频更酷,甘拜下风
w_b_b 2012-05-28
  • 打赏
  • 举报
回复
额外掐头去尾v巴特v未发放日公布
xiying12571 2012-05-28
  • 打赏
  • 举报
回复
厉害厉害啊
cq199 2012-05-28
  • 打赏
  • 举报
回复
[Quote=引用 63 楼 的回复:]
野比 发的东西都是奇葩!
[/Quote]顶!
加载更多回复(39)

110,539

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 C#
社区管理员
  • C#
  • Web++
  • by_封爱
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

让您成为最强悍的C#开发者

试试用AI创作助手写篇文章吧