利用Opencv识别电脑扑克牌详解(Android) 您所在的位置:网站首页 扑克牌机器设备吊牌图片 利用Opencv识别电脑扑克牌详解(Android)

利用Opencv识别电脑扑克牌详解(Android)

2024-06-30 13:07| 来源: 网络整理| 查看: 265

1.前言

Opencv 是一个很酷的计算机视觉库,作为CV技术的爱好者,我十分推崇Opencv,同时也向行业前辈致敬。本文将尽可能详细的介绍扑克牌识别实战的全部过程和代码原理,一方面练习自己的写作能力,另一方面希望可以帮助一些感兴趣的初学者更加直观的理解图像识别的过程(之一)。

这个小Demo也是我在学习过程中做的众多小项目之一,只是相对完善,可以单独运行不需要Opencv Manager,他目前只能用来识别电脑上的特定软件的页面,但如果了解了其中的原理,你就可以做更加通用的图像识别处理App,下面将贴出代码运行的最终效果和github 地址,欢迎fork和star。我未来也会一直不断学习CV方面的知识,欢迎大家留言交流扯皮。

在这里插入图片描述 GitHub 地址:https://github.com/woshiwzy/opencv_android

2.开发环境

AndroidStudio: 3.3.2 Opencv:3.4.2 Gradle :4.4

3.项目结构介绍

本项目主要包含opencv module 和 poker_rec module 其他的module 是我用来测试用的,可以不用管它们,扑克牌识别重要的源码在poker_rec model 中,运行的时候也是运行这个module。good_data.zip 是事先分类好的样本文件,运行的时候需要代码会把这个zip包拷贝到SD卡上面并且自动解压,在识别开始的时候会把这些样本图片转换成特征值用于识别。理论上并不需要这些原始的样本文件,只需要保留特征就行,这里是为了很好的演示算法同时也可以很直观的看到KNN算法的匹配过程(使用项目中红色箭头所指的图片来测试,不然没效果啊)。

在这里插入图片描述

4.知识扫盲

什么是机器学习?

我想来看这篇文章的人大多对机器学习都有一定的了解,我觉得没有必要非常学术话的解读这个意义。人的学习目的不就是为了认识更多的事物么,机器学习也是一样,就是让计算机通过学习类比来认识更多的事物。

在这里我们是让计算机认识图像,要让计算机认识事物,首先得教他,他学会了自然就认识了,所以我们准备了很多样本来告诉计算机什么是方块,什么是梅花等等,当样本足够多的时候,计算机通过类比自然就能区别它所看到的事物了。

机器学习算法有很多种,比如KNN,K-means,决策树,SVM,贝叶斯等,我们通过提取样本和目标的特征数据,再应用这些分类算法达到事物分类的目的,这样就简单的完成了一个机器学习的过程。当然机器学习不光用来分类,还有用来完成更多,更复杂的事情,目前图像识别领域的机器学习,千变万化的应用其实还是用来分类。所以图像分类还是图像识别的最基本,最重要的工作之一。

在图像识别领域如何来分类?

在任何情况对任何事物分类都需要有分类目标,比如一株植物是什么科,那么分类目标是这个植物,样本自然就是我们已经定义好的各种植物以及植物类别。一个人是谁,我们可以用ta的面部特征来分类,人脸就是一个需要分类的目标。同样图像分类我么首先要找到分类的目标,比如我们需要知道某张图里面是否有苹果,通常情况下我们需要把可能存在苹果的地方扣下来和苹果图片作为对比,通过对比当匹配度达到一定程度时我们就认为被扣下来图片区域就是苹果,这样的处理过程通常来讲叫做图像分割,是图像识别中不可或缺的过程,图像分割的效果直接影响图像识别的最终效果。为了解决这个过程,人们提出了很多算法来解决这个问题,在我看来图像分割任然是一个需要不断改进技术。碰巧在这个开源项目中用到的图像分割很简单,不需要知道太多其中的原理,也可以很好的完成这个任务。

当前图像识别领域有两类主要的图像识别手段;单步法和基于候选区识别。单步法比如yolo算法,他直接把未知图片传入到神经网络,不用查找候选区就可以识别目标物体。基于候选区方法则多一个过程,第一首先找到可能存在某个物体的候选区,第二步把这些候选区和已知的样本比对,如果匹配达到一定的程度就认为识别到某个物体。

单步法和基于候选区方式各有优缺点简单类比如下

基于候选区的算法优缺点如下:

更少的样本,更高效的运行速度,更容易理解的算法,更廉价的设备,但是有些情况无法用单步法解决或者效果非常差,本开源项目就是用的基于候选区方式来解决问题。

单步法优缺点:

更多的样本,单步法更多的使用神经网络,对设备性能要求高,能解决更加复杂的问题。

5.KNN算法在本例中的应用

KNN 算法是一种简单、直观、易于理解的分类方法。举一个非常通俗的例子,你要确定一个人是富人还是普通人,还是穷人,你可以看他经常和谁在一起(和谁的距离比较近)他就是那一类人,所谓物以类聚人以群分,就能很好的说明这个KNN的原始思想,这里就不介绍非常学术化的概念了。

在本例中我们会把所有的样本数据分成如下类别

0,1,2,3,4,5,6,7,8,9,方块,黑桃,梅花,黑桃,A,J,K,Q以及X这19个类别,每个类别下,我们分别收集了几十个样本,当我们抓取到一个未知的图片,想知道它到底属于哪一类,只需要和这些类别下的样本分别进行比较,这个未知的图片和哪一个类别下的图片更像,那么他就属于哪一个类,在本例中我们更加简化了这种这种算法,对于未知的图像我们只需要找到一个和它最接近的样本,这个样本的类别就定义为这个未知图像的类别。

这里为什么会有一个X文件夹呢?

想想一下如果抓到一个图像他和哪一类的图片都不想,那么极有可能他不是我们的要找的目标,这类东西我们我们不关心,就把它单独分成一类,我们可以称之为负样本,凡是不是我们想要的都分到这个类别,不影响我们得到正确的数据。

6.如何获取候选区 再谈单步法和两步法

前面我们说图像识别领域通常有两类方法:一类是单步法,另一类是基于候选区的两步法,常用的单步法多数是基于DNN的方式,能解决更为复杂的问题,但是对设备要求较高,也需要更多的样本,在很多自然场景下对单个类别的样本数据要求可能达到数千张,比较典型的算法是YOLO。基于候选区典型算法比如faster rcnn,目前也是比较成功的,但是目前相对而言YOLO优势更大,速度快,应用广,应用起来也比较简单。

上面提到的两类常用算法,都是基于深度神经网络的实现,这样做很多好处,比如更加准确,泛化能力更强等等。但是一旦用上了深度学习就意味着,你的程序会更加复杂,对硬件设备要求更高,否则在某些实时应用场合没有太大的意义,所以深度学习框架一般都是要支持GPU运算来提升速度的。然而在嵌入式设备上通常是没有GPU来加速的,在本例中也是一样,我们还是运用传统的算法来解决这个比较简单而实际的问题,而并不是通过构建神经网络来解决这个问题。

这样做由很多优势:一、针对这种单一环境下的图像识别处理,本身就只需要用传统算法解决问题,不必要复杂化,二、这种环境下我么得到的样本质量较高,只需要少量样本就可以解决问题,不需要太强的泛化能力也能搞定识别,三、简单的算法和处理过程能提高帧率,使用起来更流畅。事实证明这样做是非常明智的,尤其是在嵌入式设备上,效果是极好的。

到底用不用深度学习网络,我是这样看的:如果能用传统的算法解决问题就不用DNN的方式,优缺点前面已经提到过了,有些场合必须要用DNN的实现这无可厚非(使用DNN进行图像识别绝非必须的手段,只是很多现成的实现都是基于DNN的)。我觉得再CV领域很多论文都在堆砌网络来实现图像识别,说实话我并不觉得这是一个多么明智的方法,我觉得相比堆砌网络,更为重要的是图像分割技术,只有好的图像分割技术才能更加优雅的解决问题,模仿神经网络的方式到底是不是图像识别的终极方法,或者说是不是一个明智的方向这任然是一个疑问,不少学者已经对深度学习这个方向产生了质疑,认为这门学科是玄学,已经遇到了瓶颈,其实我也有类似的看法:-),深度学习貌似是学术界的时髦产物大家都在研究而已。

使用Opencv提取候选区域

这小节将结合实例源码以及Opencv API 来探讨两步法的第一步候选区域的提取。

在这里我们会用到Imgproc.findContours 函数,这个函数在做图像识别的时候经常会被调用到,他主要用来寻找联通区域(或者叫轮廓),他会根据颜色的灰度把输入图像分割成不同大小的小区域,大多数情况下这些区域就是我们要的候选区。比如在本例中,经过findContours 函数拿到的小区域可能就是我们要识别的扑克牌上面的数字或者花色。下面看一下这个函数的C++原型(直接看Demo代码会更好):

cv2.findContours(image, mode, method[, contours[, hierarchy[, offset ]]])

返回两个值:contours:hierarchy。

参数

第一个参数是寻找轮廓的图像;

第二个参数表示轮廓的检索模式,有四种(本文介绍的都是新的cv2接口):

cv2.RETR_EXTERNAL表示只检测外轮廓 cv2.RETR_LIST检测的轮廓不建立等级关系 cv2.RETR_CCOMP建立两个等级的轮廓,上面的一层为外边界,里面的一层为内孔的边界信息。如果内孔内还有一个连通物体,这个物体的边界也在顶层。 cv2.RETR_TREE建立一个等级树结构的轮廓。

第三个参数method为轮廓的近似办法

**cv2.CHAIN_APPROX_NONE存储所有的轮廓点,相邻的两个点的像素位置差不超过1**,即max(abs(x1-x2),abs(y2-y1))==1 cv2.CHAIN_APPROX_SIMPLE压缩水平方向,垂直方向,对角线方向的元素,只保留该方向的终点坐标,例如一个矩形轮廓只需4个点来保存轮廓信息 cv2.CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS使用teh-Chinl chain 近似算法

cv2.findContours()函数返回两个值,一个是轮廓本身,还有一个是每条轮廓对应的属性。

在这里我要重点介绍第二个参数,前两种 cv2.RETR_EXTERNAL和cv2.RETR_LIST,在我实践的过程中我发现只有在传入cv2.RETR_LIST的时候,才能得到足够多的轮廓,从参数的解释来看,这也是合理的,第三个参数选择显得不是那么重要,在本例中我选择了CHAIN_APPROX_SIMPLE,起始选择其他的参数,对本例来说效果上几乎没有差别。

需要注意的是在执行findContours函数的之前还做了一些其他的处理,最主要的就是灰度处理和二值化,其中二值化是findContours函数的要求,必须传入二值化图像,灰度处理则是减少图像干扰的的手段,详细代码见

com.myopencvdemo.PokerRecActivity 在这里插入图片描述

筛选候选区

上一节我们提到了函数 Imgproc类的

public static void findContours(Mat image, List contours, Mat hierarchy, int mode, int method)

函数,这个函数就是从图像中提取联通区域,简单的说它的作用就是把一些可能是一个整体的区域给连接起来,返回给调用者。无论是在牛逼的深度学习网络还是在简单的two-stage 传统的机器学习算法中,都是非常重要的步骤,这一步如果完美那么图像识别就成功80%了。

举一个例子 :

假如你要在一幅图片中识别一辆汽车:你传入一张含有汽车图片,通过调用findContours 得到的联通区域会有很多,可能有轮子,可能有车灯,引擎盖子,当然也可能有整车的区域(这种是理想情况),通过这种方式得到的的无数碎片区域,其中可能有你想要的区域也可能没有,比如你通过此方式只得到了引擎盖子的区域,但这种区域并不能准确的预测这是一辆车,你更不能很好的预测这辆车的准确位置,原因就是通过这种方式你找到的联通区域并不完美,最完美的区域就是整车的外接矩形框,当然这在实际处理的时候90%是办不到的,或者说你要完善这种算法才行,真遇到这种情况你需要借助深度学习算法框架比如YOLO,通过提供大量的数据集让算法自动去适应学习,找到完美的整车外接矩形框,当然这是另一个话题 了。

回到这个例子 :

很庆幸,我们能得到完美的矩形框。其中最大的原因就是这里的应用场景非常特殊,首先扑克的花色和数字是分开的,他们的外接矩形并不会连接到一起(扑克的白色背景起到了分割的作用)。其次是每一次摄像头捕捉到的图像相差不大(角度,亮度相差不大,字符特征都是固定的几种),而且我们还可以在软件上对客户进行引导,引导客户把相机的红色矩形框对准扑克牌,这能大量减少计算量,提高速度,且减小识别干扰。我们目前讨论的场景是非常简单的,但也仅限于识别这种单一的场景,如果要识别更加复杂的场景就需要根据实际情况修改算法,甚至使用DNN算法框架。我的一贯看法是如果能使用传统机器学习算法的两步法来解决问题的,尽量不要用DNN框架,DNN框架虽然能完美的解决很多问题,但是操作困难,硬件昂贵,而且应用场景收到了极大的限制,绝大多数情况下是不能在手机上流畅运行的,而且移动设备上通常装不了AI芯片,能装算力也非常有限。而且要让DNN框架工作起来,也需要大量的准备工作(采集样本),编码工作也并不会少,简单的说,杀鸡莫用牛刀。

上面说的这两段就是一个简单的知识铺垫:

我们现在来研究findContours函数,他的最有用的计算结果放在了contours参数中,这个参数是一个MatOfPoint 类型的List。一个MatOfPoint包含了很多点,这些点连起来才算是一个联通的区域。

List contours = new ArrayList(); Imgproc.findContours(dst, contours, new Mat(dst.rows(), dst.cols(), dst.type()), Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE);//CV_RETR_CCOMP, CV_CHAIN_APPROX_NONE

还有非常重要的一点就是 findContours 函数的参数选型,最重要的就是这个mode参数的选型,最好选择Imgproc.RETR_LIST,他能最大程度上返回的联通区域数量,其他的参数各位可以自行查看,这里不详细说,我们需要从尽可能多的联通区域(此时可以说是候选区了)找到可能是扑克牌的区域,候选区越多,我们就可能选得越准确。而不会漏掉一些本来是目标的区域。

在接下来文字当中将会结合具体代码, 讨论具体的筛选问题:

第一步: 通过boundingRect 函数将联通区域转换成矩形区域,这样方便于我们计算这块区域的高度,宽度,以及比例,面积等特征信息。针对我们当前的应用场景,太小的区域和太大的区域都不能要,因为他们并不是我们要找的区域,太小的区域,可能是一些小缝隙,太大的区域可能是大块的色区。高宽比例太小或者太大,也不是我们要找的区域,因为扑克牌的色号和数字总体趋近正方形,而且一般来说高度大于宽度,我们可以适当放大这一限制,比如高度是宽度的1-3倍都可以作为候选区,并且保存到磁盘上,进行第二步操作。 在这里插入图片描述 第二步 人工筛选,所谓人工智能,没有人的参与是不可能的,把第一步选出来候选区保存下来的文件进行人工分类,比如我们这里要识别数字和色号共18种情况(0,1,2,3,4,5,6,8,9,方块,黑桃,红桃,梅花,A,J,K,Q)那么我们需要建立19个文件夹如下 在这里插入图片描述 这里为什么会建立一个叫X的文件夹呢? 我们得到的候选区文件,之所有叫候选区,就是因为他们不是100%的准确,我们把准确的部分进行分类,不是我们想要的区域(不是这18种的任何一种)单独放到X文件夹, 比如0文件夹里面的图都是非常像0的候选区域小图(我们可以把已经分类好的候选区图像称为样本) 在这里插入图片描述 而X文件夹里什么形状都有,他并不是我们想要的18种的任何一种 在这里插入图片描述 这是因为我们在处理候选区时,我们并不能完全过滤非候选区,因为有些非候选区体现出来的特征和候选区非常像。不能简单的删除了之,在后面的算法还需要用到。先简单的说:在识别过程中我们也是先拿到候选区,再把每一个候选区的特征和已知的分类进行对比,它和谁的特征比较像我们就把它归为那一类,比如我们发现一个候选图和0文件夹中的某一个样本很像,我们就认为它是0,如果和黑桃文件夹里面的一个样本很像,我们就认为这个候选区是黑桃(这是机器学习算法中最简单的算法KNN基本思想,只是这里我们用的是极端情况就是取和候选区最接近的一个样本)。说白了就是针对每一个候选区都和所有的样本进行一次特征(后面再讲)比对,找出特征距离最小的那个样本,这个样本属于哪个类,这个候选区我们也认为它属于这个类。

到此为止X文件夹的作用也就很明白了,如果候选区的特征和X文件夹里面的某个样本特征非常接近(特征距离最小),那么这个样本就属于X,这个候选区不是我们18种当中的一种,我们不用管他。在很多场合我们称X文件夹中的这种样本为负样本,而其他的18中文件夹里面的样本称之为正样本。如果没有负样板,那么有可能出现这样一种情况:根据一些算法的计算结果(比如KNN),针对一个候选区,他本身不属于18种当中的一个,但是结果又需要取一个最相似的结果,这样就会取到18种当中的某一类,这个相似度会很低,所以结果本身就是错误或者极度不准确的(这样做是不科学的,除非再加一个相似度阀值,当样本中和候选区相似度最大的那个样本(想速度最大)相似度达度大于阀值,才作为结果,否则本次比对没有结果)。

一句话:有了负样本,增加了识别结果的准确性。

7.处理筛选区

先看源代码图片 在这里插入图片描述 在242行通过宽度,高度,宽高比筛选出可能的候选区,并保存在rects中,在需要的地方,按照rects中保存的矩形框裁剪下来。如果选中了学习模式的Checkbox就把这些候选区座位图片保存到SD卡上,这些图片就需要人工分类后,分别放到19个文件夹中,就是最终的样本。样本越多,样本越准确,识别的结果也越准确,但是跟多的样本也会导致速度的更慢。

在273行开始,就是真正的进入识别过程了

在这里插入图片描述 识别过程有如下步骤

1.初始化机器学习引擎(在App启动的时候做了这件事)

这个初始化,首先是把样本图片按照设定好的参数转换成向量特征,识别过程中并不是对比的图片而是对比的特征,这一点需要牢记,如果是对比图片那就是另一种算法了。所以其实我们整理好识别的图片后保存它们的特征就好了,特征比图片占用的存储空间小很多。在机器学习的生产环境是不会把样本转换成特征的过程放到最终的程序中的,最终的程序只负责把特征加载到机器学习引擎中用于计算。我这里之所以每次都要从图片转换为特征,就是为了我方便添加样本文件到文件夹中,添加好了重启一下App就行了,再走一下初始化过程,而不需要单独处理特征文件(实际上整个过程都没有涉及到特征文件)。

2.把所有的候选区每张图都转换成特征值,轮流放到机器学习引擎中得到一个最佳的匹配

只有KNN才这么干,其他的学习算法大多数都不会这么做

3.整理结果

对于本例,需要把结果集进行整理,比如排列顺序,计算花色,确定扑克牌上的10 的位置

在297行中保存了所有的识别结果,接下来就需要整理了,整理算法如下

第一步:按照裁剪的起始位置进行排序(花色和数字分成两组),把所有的结果都进行排序,得到正确的顺序

第二步:处理扑克牌为10的结果,因为10是1和0分开的,在识别的时候是2个结果,如果排序后发现,1之后是0,就合并成一个结果为10

第三步:本例中只考虑了18中情况,也没有考虑大小王的情况,分组成2组后分别是(A,2,3,4,5,6,7,8,9,10,J,Q,K),花色组是(红桃,黑桃,方块,梅花).

第四步:判定识别结果是否成功 识别成功的条件就是识别出所有的花色和字符组合,本例中识别的是13张牌,所以必须要识别出13张牌,才算成功,由于识别光线,角度等原因,不可能每次都能识别成功,所以要用红色框引导用户对齐识别区域。这样一来能帮助对齐识别区域,二来减少计算的图片大小,提升速度。

所以:对于本例的结果是得到字符分组和花色分组都是13个元素才算成功,因为是按照起始位置排序的,当13个字符和13个花色都确定的情况下,那么两组数据都是一一对应的正确结果了。

8.过程梳理

才开始练习写作,经验和文笔都是比较差的,在这里再次梳理一下从采集样本到图像识别结果的全部过程。

1.需求

识别扑克牌:识别字符和和花色,这里没有考虑大小王。

2.采集样本

2.1.使用Opencv查找联通区域 2.2.把这些联通区域转换成矩形 2.3.通过矩形的大小,长宽,筛选出候选区 2.4.人工分类,分成正样本和负样本

3.数据整理与识别

3.1 归一化 在计算特征之前,必须保证所有的样本是一样大小,这样可以得到的特征纬度是一样的,这个过程可以在采集样本的时候做,就是把所有样本resize成一样大小的图片,也可以在在计算特征的时候,先resize成一样的大小再计算特征。

3.2 特征化

把所有的样本都计算成特征值,等待传入AI引擎

3.3 识别 把所有的候选区先resize成归一化大小,再计算出特征,然后应用到特征引擎识别。

注意: 1.本例使用特征算法是是HOG特征,这里没有细讲,感兴趣的同学自动搜索学习

9.完结

写个文章好费时间,结果还没写好,见谅,回过头来发现好多细节没写清楚…

使用深度学习算法框架,比如Darknet也能很好的解决问题,而且更灵活,但是在手机上是绝对做不到实时,而且对这种小目标识别也比较困难,重要的是这种小东西用DNN简直是牛刀杀鸡…

完了… 谢谢各位读者…



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有