基于AForge.Net框架的扑克牌识别

原文:基于AForge.Net框架的扑克牌识别

© 版权所有 野比 2012

原文地址:点击查看

作者:Nazmi Altun

Nazmi Altun著,野比 译

 下载源代码 - 148.61 KB

 下载demo - 3.1 MB

介绍

(图片上的字:方块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

© 版权所有 野比 2012

(全文完)

时间: 2024-10-11 04:52:20

基于AForge.Net框架的扑克牌识别的相关文章

基于Aforge的物体运动识别-入门

基于Aforge的物体运动识别-入门篇chatbot人工智能机器人开发,提供教学视频>>>   0 收藏(2) 本文来自http://blog.csdn.net/hellogv/ ,引用必须注明出处! 最近看到越来越多人在做物体运动识别(例如:"第六感"中的指套),而且我最近也有点闲空,所以也来玩玩.....大多数人都是用Opencv来做,那我就不做重复的工作了,换个别的开源类库~~~Aforge. 来自百度知道的Aforge介绍:AForge.NET 是一个专门为开

人工神经网络简介和单层网络实现AND运算--AForge.NET框架的使用(五)

原文:人工神经网络简介和单层网络实现AND运算--AForge.NET框架的使用(五) 前面4篇文章说的是模糊系统,它不同于传统的值逻辑,理论基础是模糊数学,所以有些朋友看着有点迷糊,如果有兴趣建议参考相关书籍,我推荐<模糊数学教程>,国防工业出版社,讲的很全,而且很便宜(我买成7元钱). 人工神经网络的简介 人工神经网络是一种应用类似于大脑神经突触联接的结构进行信息处理的数学模型.它是一种运算模型,由大量神经元和相互的连接组成,每个神经元代表一种特定的输出函数,称为激励函数(activati

模糊语意变数、规则和模糊运算--AForge.NET框架的使用(二)

原文:模糊语意变数.规则和模糊运算--AForge.NET框架的使用(二) 语意变数(Linguistic Variable) 语意变数存储了数个语意量(标签),每个语意量包含一个识别名和模糊集合.在宣告陈述时每个语意量只能和在同一变数中的语意比较. 举个很简单的例子,我们有一个名为temperature的语意变数,它包含4个语意量,名为cold.cool.warm.hot,这也是各自的标签名,同时它们还有各自的隶属度函数. 那么我们就在接下来的系统中使用诸如temperature is hot

简述人脸特异性识别&amp;&amp;一个基于LBP和SVM的人脸识别小例子

原谅我用图片,MAC在Safari里给文章进行图文排版太麻烦啦~ 本文适合初入计算机视觉和模式识别方向的同学们观看~ 文章写得匆忙,加上博主所知甚少,有不妥和勘误请指出并多多包涵. 本文Demo的代码由HZK编写,特征点由月神和YK选择和训练. 转载请注明 copyleft by sciencefans, 2014 为了方便大家学习,附上高维LBP的核心代码 1 ################################################### 2 # 3 # 4 # NO

jquery:基于js的框架

jquery:基于js的框架 框架: 1.方便使用 2.解决了兼容性的问题 官方网站jQuery.com(2点几版本的不支持IE6.7.8) this 当前环境下指代的对象 闭包(closer) 1.可以让局部变量持续的保存下来 在一个函数(对象)里面定义一个函数,这个内部函数可以访问外面函数的局部变量,而且可以让外面的变量引用,可以持续保存局部变量 一个函数里面必须包含一个函数,而且要引用外面函数的变量,里面的函数必须要返回出去,可以使用相同名称的变量 缺点:对内存耗需比较大,可能会导致内存泄

基于QT和OpenCV的人脸识别系统

1 系统方案设计 1.1 引言 人脸是一个常见而复杂的视觉模式,人脸所反映的视觉信息在人与人的交流和交往中有着重 要的作用和意义,对人脸进行处理和分析在视频监控.出入口控制.视频会议以及人机交互等领 域都有着广泛的应用前景,因此是模式识别和计算机视觉领域持续的研究热点. 本系统在 FriendlyARM Tiny6410 开发板基础上,利用 OpenCV 计算机视觉库和 QT 图形库,通 过普通的 USB 摄像头实现了自动人脸识别,准确率较高,方便易用. 1.2 系统总体架构 "人脸识别&quo

[Android_HTML5]基于PhoneGap(Cordova)框架的HTML5开发

PhoneGap是一套能让你使用HTML5轻松调用本地API接口和发布应用到商店的应用开发平台.官方说有低成本,低开发周期,轻量化等优点,这些咱暂时也没法证明,略过不表.但是有一条跨平台,却是很明显的优势.因为它采用HTML5+JavaScript的模式来开发应用.PhoneGap用JavaScript统一封装了几大平台的本地api(Andriod,IOS,WP8/7,WINRT)等等..这样的话从一个平台移植到另外一个平台只需要把HTML代码跟JS原封不动的拿过去,打包一下就可以了.Phone

MVC4 基于 Unity Ioc 框架的 ControllerFactory

首先引入Untiy框架. 可以在NuGet程序包 管理器中直接安装. 新建 继承DefaultControllerFactory  的UnityControllerFactory: 重写虚方法GetControllerInstance. public class UnityControllerFactory : DefaultControllerFactory { public IUnityContainer UnityContainer { get; private set; } public

基于PhoneGap3.4框架的iOS插件的实现

Phonegap 提供了iOS 设备的基础特性接口来供HTML页面调用,但是这些基础接口不能满足我们的一些特殊需求,所以有时候我们需要开发插件来扩展其功能.基于PhoneGap3.4框架的iOS插件开发,主要分以下几个步骤: 1)搭建PhoneGap3.4的iOS开发环境,搭建步骤参考此文章. 2)编写.h头文件,示例代码如下: #import <Cordova/CDVPlugin.h> @interface CDVLogin : CDVPlugin - (void)login:(CDVInv