利用计算机视觉识别扑克牌花色、点数

2024-03-26 06:30

本文主要是介绍利用计算机视觉识别扑克牌花色、点数,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

© 2012 Conmajia, 2011 Nazmi Altun
Init. 24 May 2012
SN: 125.1

本中文版已获原作者 Nazmi Altun 授权。

下载资源:
源代码(148.6 KB)、Demo(3.1 MB)

简介

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

机器人搭配上扑克牌识别系统,即可在二十一点之类的扑克游戏中扮演荷官或者玩家的角色。实现这样的程序同样也是学习计算机视觉和模式识别的好路子。

本文涉及到关于 AForge.NET 框架的技术有二值化、边缘检测、仿射变换、BLOB 处理和模板匹配算法等。

需要注意的是,本文介绍的系统针对英美扑克设计,可能不适用于其他种类的扑克(如日本、朝鲜传统花牌——译者注)。当然,本文详述了扑克牌检测和识别的基本方法,具体的识别算法按特定的牌型特点据此修改即可。

这里有一个 演示视频。

扑克检测

我们需要检测视频图像上的扑克对象,以便能进行下一步的识别。为了完成检测,我们会用一些图像滤镜对视频画面进行预处理。

第一步,将图像去色,即灰度化。去色是将彩色图像转换成 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] 像素之间的图块。这有助于我们区分出图像中其他物体(如果有的话)。根据测试环境的不同,我们需要改变滤镜参数。例如,假设地面和相机之间距离增大,则图像中的扑克牌会变小。此时,我们需要相应的改变最小、最大宽度和高度参数。

现在,我们可以通过调用 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); // 归一化(重设大小)扑克牌.....
}

(图片上的字:使用 QuariterialTransformation 类从原始图像提取出的扑克牌。该类基于每张牌的四个角进行变换)

到目前为止,我们已经找到了原始图像上每张扑克牌的四角,并从图像中提取出了扑克牌,还调整到统一的尺寸。现在,我们可以开始进行识别了。

识别扑克牌

有好几种用于识别的技术用于识别扑克牌。本文用到的是基于牌型(如扑克牌上的形状)及模板匹配技术。扑克牌的花色和大小是分开识别的。我们可以这样枚举:

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);
}

(图片上的字:裁剪扑克牌右上角、再次裁剪前次结果的底部)

裁剪了扑克牌右上角后,我们得到一张 30 x 60 像素的图像。但是该图像同时包含了花色和大小。因为我们只是分析花色,所以再次裁剪下半部分,得到 30 x 30 像素的图像。

现在,我们可以遍历图像中红色像素和黑色像素的总数。如果一个像素的红色分量比蓝色分量和绿色分量的总和还打,就可以认为该像素是红色。如果红、绿、蓝分量小于 50,且红色分量不大于蓝色和绿色分量和,则认为该像素是黑色。

关于处理 Bitmap 过程中的锁定与解锁,可以参见《GDI+ 位图读写速度测试》一文。——译者注

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) // 红绿蓝均小于50totalBlack++; // 认为是黑色}}}finally {bmp.UnlockBits(imageData); // 解锁}
}
if (totalRed > totalBlack) // 红色占优
color = 'R'; // 设置颜色为红,否则默认黑色
return color;

(图片上的字:(左边)总红色=82 总黑色=0;(右边)总红色=0 总黑色=80)

注意 .NET 的 Bitmap.GetPixel() 函数运行缓慢,所以我们使用了指针来遍历像素。

区分人物牌和数字牌

识别了颜色后,我们需要确定扑克牌是否是人物牌。人物牌的牌面为 J、Q、K。人物牌和数字牌之间有一个很突出的特点,即数字牌牌面有很多花色符号指示其大小,而人物牌很好辨认,其牌面有人物头像。我们可以简单的设定一个大个的花色形状来分析扑克,而不是对其使用复杂的模板匹配算法。这样,识别数字牌就可以变得更快。

为了找出一张扑克牌到底是人物牌还是数字牌非常简单。人物牌上面有大的人物图,而数字牌没有。如果我们对牌进行边缘检测和图块处理,找到最大图块,就可以从图块的大小上判断到底是人物牌还是数字牌了。

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;  // 数字牌
}

我们不断的对扑克牌图像进行灰度变换、局部阈值化和边缘检测。注意我们使用局部阈值化而不是全局阈值化来消除照明不良的问题。

(图片上的字:原始扑克图像、灰度、Bradley 局部阈值、边缘检测、提取最大图块)

正如你所看到的,人物牌最大图块几乎和整张扑克牌一样大,很容易区分。

前面提到过,出于性能上的考虑,我们将使用不同的识别技术对人物牌和数字牌进行识别。对于数字牌,我们直接提取派上最大图块并识别其宽度和颜色。

private Suit ScanSuit(Bitmap suitBmp, char color) {Bitmap temp = commonSeq.Apply(suitBmp);// 提取最大图块ExtractBiggestBlob extractor = new ExtractBiggestBlob();temp = extractor.Apply(temp);Suit suit = Suit.NOT_RECOGNIZED;// 判断花色if (color == 'R')suit = temp.Width >= 55 ? Suit.Diamonds : Suit.Hearts;if (color == 'B')suit = temp.Width <= 48 ? Suit.Spades : Suit.Clubs;return suit;
}

(图片上的字:宽度 = 52 px、43 px、47 px、57 px)

上述测试最大误差 2 像素。一般来说,因为我们把扑克牌尺寸都调整到了 200 x 300 像素,所以测试的结果都会是相同的大小。

人物牌牌面上没有类似数字牌的最大花色图像,只有角上的小花色图。这就是为什么我们会裁剪扑克图像的右上角并对其应用模板匹配算法来识别花色。

二值化模板图像已经包含在了本文提供的下载资源中。

AForge.NET 提供了一个叫做 ExhaustiveTemplateMatching 的类实现穷尽模板匹配算法。该类对原始图进行完全扫描,用相应的模板对每个像素进行比较。尽管该算法的性能不佳,但我们只是用于一个小区域(30 x 60),也不必过于关心性能。

private Suit ScanFaceSuit(Bitmap bmp, char color) {Bitmap clubs, diamonds, spades, hearts; // 花色模板// 载入模板资源clubs = PlayingCardRecognition.Properties.Resources.Clubs;diamonds = PlayingCardRecognition.Properties.Resources.Diamonds;spades = PlayingCardRecognition.Properties.Resources.Spades;hearts = PlayingCardRecognition.Properties.Resources.Hearts;// 用0.8的相似度阈值初始化模板匹配类ExhaustiveTemplateMatching templateMatching = new ExhaustiveTemplateMatching(0.8f);Suit suit = Suit.NOT_RECOGNIZED;if (color == 'R') {if (templateMatching.ProcessImage(bmp, hearts).Length > 0)suit = Suit.Hearts; //匹配红桃if (templateMatching.ProcessImage(bmp, diamonds).Length > 0)suit = Suit.Diamonds; // 匹配方块}else {if (templateMatching.ProcessImage(bmp,spades).Length > 0)suit = Suit.Spades; // 匹配黑桃if (templateMatching.ProcessImage(bmp, clubs).Length > 0)suit = Suit.Clubs; // 匹配梅花}return suit;
}

(图片上的字:模板匹配?是/否)

当然,模板不能 100% 匹配样本,所以我们使用 0.8(80%)的相似度阈值。

识别大小

识别大小和识别花色类似,也是单独对人物牌和数字牌进行识别。由于数字牌可以只靠计算牌面上的花色图块数量就可以识别,而不用模板匹配,所以利用简单的图像滤镜就可以完成任务。

下面所示的 ScanRank 函数过滤小图块(小于 30 像素长或宽)并计算剩余的图块数。

private Rank ScanRank(Bitmap cardImage) {Rank rank = Rank.NOT_RECOGNIZED;int total = 0;Bitmap temp = commonSeq.Apply(cardImage); // 应用滤镜BlobCounter blobCounter = new BlobCounter();blobCounter.FilterBlobs = true;// 过滤小图块blobCounter.MinHeight = blobCounter.MinWidth = 30;blobCounter.ProcessImage(temp);total = blobCounter.GetObjectsInformation().Length; // 获取总数rank = (Rank)total; // 转换成大小(枚举类型)	return rank;
}

(图片上的字:边缘检测、过滤宽高小于 30 像素的图块、剩余图块总数为 10,即扑克的点数)

所以,数字牌不用模板匹配算法或是 OCR 即可识别。但是,对人物卡,我们需要再次使用模板匹配进行识别。

private Rank ScanFaceRank(Bitmap bmp) {Bitmap j, k, q; // 人物牌人物模板 4// 载入资源j = PlayingCardRecognition.Properties.Resources.J;k = PlayingCardRecognition.Properties.Resources.K;q = PlayingCardRecognition.Properties.Resources.Q;// 用0.75进行初始化ExhaustiveTemplateMatching templateMatchin =new ExhaustiveTemplateMatching(0.75f);Rank rank = Rank.NOT_RECOGNIZED;if (templateMatchin.ProcessImage(bmp, j).Length > 0) // Jrank = Rank.Jack;if (templateMatchin.ProcessImage(bmp, k).Length > 0)// Krank = Rank.King;if (templateMatchin.ProcessImage(bmp, q).Length > 0)// Qrank = Rank.Queen;return rank;
}

由于识别难度较大,这次我们使用 0.75(75%)作为相似度阈值。

已知问题

本文给出的实例代码,只能识别分开的扑克牌,无法应对重叠牌。另一个已知问题是光线环境变化常造成识别错误。

结论

本文用到的图像用例来自 AForge.NET 框架。AForge.NET 为机器视觉和机器学习领域的开发者提供了大量有用的特性。对我来说,它同样非常简单。

本文还可提高,例如如何在牌还没有分放置的时候就进行识别。另一种提升是用这套系统做成 AI 二十一点玩家。

历史

7th, Oct., 2011: 初稿

许可

本文及附带的源文件代码、文件均以 CPOL 许可发布。

关于作者

Nazmi Altun,来自土耳其的软件开发者

© 2012 Conmajia, 2011 Nazmi Altun

这篇关于利用计算机视觉识别扑克牌花色、点数的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/847558

相关文章

阿里开源语音识别SenseVoiceWindows环境部署

SenseVoice介绍 SenseVoice 专注于高精度多语言语音识别、情感辨识和音频事件检测多语言识别: 采用超过 40 万小时数据训练,支持超过 50 种语言,识别效果上优于 Whisper 模型。富文本识别:具备优秀的情感识别,能够在测试数据上达到和超过目前最佳情感识别模型的效果。支持声音事件检测能力,支持音乐、掌声、笑声、哭声、咳嗽、喷嚏等多种常见人机交互事件进行检测。高效推

计算机视觉工程师所需的基本技能

一、编程技能 熟练掌握编程语言 Python:在计算机视觉领域广泛应用,有丰富的库如 OpenCV、TensorFlow、PyTorch 等,方便进行算法实现和模型开发。 C++:运行效率高,适用于对性能要求严格的计算机视觉应用。 数据结构与算法 掌握常见的数据结构(如数组、链表、栈、队列、树、图等)和算法(如排序、搜索、动态规划等),能够优化代码性能,提高算法效率。 二、数学基础

Clion不识别C代码或者无法跳转C语言项目怎么办?

如果是中文会显示: 此时只需要右击项目,或者你的源代码目录,将这个项目或者源码目录标记为项目源和头文件即可。 英文如下:

《计算机视觉工程师养成计划》 ·数字图像处理·数字图像处理特征·概述~

1 定义         从哲学角度看:特征是从事物当中抽象出来用于区别其他类别事物的属性集合,图像特征则是从图像中抽取出来用于区别其他类别图像的属性集合。         从获取方式看:图像特征是通过对图像进行测量或借助算法计算得到的一组表达特性集合的向量。 2 认识         有些特征是视觉直观感受到的自然特征,例如亮度、边缘轮廓、纹理、色彩等。         有些特征需要通

【python计算机视觉编程——7.图像搜索】

python计算机视觉编程——7.图像搜索 7.图像搜索7.1 基于内容的图像检索(CBIR)从文本挖掘中获取灵感——矢量空间模型(BOW表示模型)7.2 视觉单词**思想****特征提取**: 创建词汇7.3 图像索引7.3.1 建立数据库7.3.2 添加图像 7.4 在数据库中搜索图像7.4.1 利用索引获取获选图像7.4.2 用一幅图像进行查询7.4.3 确定对比基准并绘制结果 7.

参会邀请 | 第二届机器视觉、图像处理与影像技术国际会议(MVIPIT 2024)

第二届机器视觉、图像处理与影像技术国际会议(MVIPIT 2024)将于2024年9月13日-15日在中国张家口召开。 MVIPIT 2024聚焦机器视觉、图像处理与影像技术,旨在为专家、学者和研究人员提供一个国际平台,分享研究成果,讨论问题和挑战,探索前沿技术。诚邀高校、科研院所、企业等有关方面的专家学者参加会议。 9月13日(周五):签到日 9月14日(周六):会议日 9月15日(周日

【python计算机视觉编程——8.图像内容分类】

python计算机视觉编程——8.图像内容分类 8.图像内容分类8.1 K邻近分类法(KNN)8.1.1 一个简单的二维示例8.1.2 用稠密SIFT作为图像特征8.1.3 图像分类:手势识别 8.2贝叶斯分类器用PCA降维 8.3 支持向量机8.3.2 再论手势识别 8.4 光学字符识别8.4.2 选取特征8.4.3 多类支持向量机8.4.4 提取单元格并识别字符8.4.5 图像校正

Python计算机视觉编程 第十章

目录 一、OpenCv基础知识 1.读取和写入图像 2.颜色空间 3.显示图像和结果 二、处理视频 1.输入视频 2.将视频读取到NumPy数组中 三、跟踪 1.光流 2.Lucas-Kanade算法 一、OpenCv基础知识 OpenCV 自带读取、写入图像函数以及矩阵操作和数学库。 1.读取和写入图像 import cv2# 读取图像im = c

BERN2(生物医学领域)命名实体识别与命名规范化工具

BERN2: an advanced neural biomedical named entity recognition and normalization tool 《Bioinformatics》2022 1 摘要 NER和NEN:在生物医学自然语言处理中,NER和NEN是关键任务,它们使得从生物医学文献中自动提取实体(如疾病和药物)成为可能。 BERN2:BERN2是一个工具,

行为智能识别摄像机

行为智能识别摄像机 是一种结合了人工智能技术和监控摄像技术的先进设备,它能够通过深度学习算法对监控画面进行实时分析,自动识别和分析监控画面中的各种行为动作。这种摄像机在安防领域有着广泛的应用,可以帮助监控人员及时发现异常行为,并采取相应的措施。 行为智能识别摄像机可以有效预防盗窃事件。在商场、超市等公共场所安装这种摄像机,可以通过识别异常行为等情况,及时报警并阻止不安全行为的发生