写一个识别扑克牌花色和点数的小程序(一)

最近选修了数字图像处理,课程设计选择写了一个可以识别扑克牌花色和点数的小程序,来跟大家分享一下,请多多指教。

开发环境

  • VS2019
  • OpenCV4.1.0
  • QT5.13

最终效果

检测单张扑克牌
检测多张扑克牌

码代码过程

总共包含5个cpp文件

classfiy.cpp 里面包括了对扑克牌各种处理的函数

main.cpp 创建QT界面

PokerClassify.cpp 写一个界面并和相关函数衔接起来

rotation.cpp 将各种角度的扑克牌旋转成正的扑克牌

templateMatching.cpp 进行模板匹配


classfiy.cpp

通过扑克牌的长宽比判断是否是一个扑克牌

int ifPorker(vector<Point> approx) {
   //规定多边形的面积,以及是一个凸多边形
   if (approx.size() == 4 && fabs(contourArea(approx)) > 1000 && isContourConvex(approx)) {
       //按照比例剔除不是扑克牌的矩形
       double length, width;
       double regular_rate = 0.715;
       double rate, error = 0.03;
       int maxLength;
       length = fabs(sqrt((approx[3].x - approx[0].x) * (approx[3].x - approx[0].x) + (approx[3].y - approx[0].y) * (approx[3].y - approx[0].y)));
       width = fabs(sqrt((approx[0].x - approx[1].x) * (approx[0].x - approx[1].x) + (approx[0].y - approx[1].y) * (approx[0].y - approx[1].y)));
       /*cout << width << " " << length << endl;*/
       if (length > width) {
           rate = width / length;
           maxLength = length;
       }
       else if (length < width) {
           rate = length / width;
           maxLength = width;
       }

       if (fabs(rate - regular_rate) < error && maxLength < 3000) {
           //cout << rate << endl;
           return 1;
       }
   }
}

查找图片里的扑克牌,主要参考了opencv官方案例里的findSquare案例。用在不同灰度级下用轮廓检测检测出矩形边缘,然后保存矩形顶点坐标。

bool findPorker(Mat& image, vector<vector<Point>>& squares) {
   squares.clear();

   Mat gray, pyr, timg, gray0(image.size(), CV_8U);
   //对图片进行下采样再上采样,其实是一个去噪的过程
   pyrDown(image, pyr, Size(image.cols / 2, image.rows / 2));//先下采样
   pyrUp(pyr, timg, image.size());//上采样
   //查找轮廓
   vector<vector<Point>> contours;
   vector<vector<Vec4i>> hierarchy;
   vector<vector<Point>> tempContours;
   for (int channel = 0; channel < 3; channel++) {
       int channels[] = { channel, 0 };
       //分离通道
       mixChannels(&timg, 1, &gray0, 1, channels, 1);
       for (int l = 0; l < N; l++)
       {
           if (l == 0)
           {
               Canny(gray0, gray, 0, 50, 5);
               dilate(gray, gray, Mat(), Point(-1, -1));
           }
           else
           {
               gray = gray0 >= (l + 1) * 255 / N;
           }
           findContours(gray, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);
           //第i个轮廓的后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号
           //findContours(bin, contours, hierarchy,RETR_EXTERNAL, CHAIN_APPROX_NONE);
           //RETR_EXTERNAL=0只检查外围轮廓,RETR_LIST检查所有轮廓
           //RETR_CCOMP检查外围轮廓,有两个等级关系 RETR_TREE 检查所有的轮廓,建一个树形结构
           //CHAIN_APPROX_NONE=1保存物体边界上所有的轮廓点到contours
           vector<Point> approx;
           for (size_t i = 0; i < contours.size(); i++) {
               //对图像轮廓进行多边形拟合,对点集进行逼近 采用的是道格拉斯-普客算法
               approxPolyDP(contours[i], approx, arcLength(contours[i], true) * 0.02, true);
               //规定多边形的面积,以及是一个凸多边形
               if (ifPorker(approx) == 1) {
                   tempContours.push_back(approx);
               }
           }
       }
       simpleROI(tempContours, squares);
       //drawPorkerROI(image, squares);
       return true;
   }
}

因为分了不同的灰度级查找矩形,所以图像中同一个矩形可能会有多个矩形框。在这里我通过判断顶点的距离去掉重复的矩形框。

int compareROI(vector<Point>rect1, vector<Point>rect2) {
   int error1, error2, error3, error4;
   int Error = 15;
   bool ifPorker = true;
   //起始点不是扑克牌的左上角点的,估计是异常数据了

   error1 = sqrt((rect1[0].x - rect2[0].x) * (rect1[0].x - rect2[0].x) + (rect1[0].y - rect2[0].y) * (rect1[0].y - rect2[0].y));
   error2 = sqrt((rect1[1].x - rect2[1].x) * (rect1[1].x - rect2[1].x) + (rect1[1].y - rect2[1].y) * (rect1[1].y - rect2[1].y));
   error3 = sqrt((rect1[2].x - rect2[2].x) * (rect1[2].x - rect2[2].x) + (rect1[2].y - rect2[2].y) * (rect1[2].y - rect2[2].y));
   error4 = sqrt((rect1[3].x - rect2[3].x) * (rect1[3].x - rect2[3].x) + (rect1[3].y - rect2[3].y) * (rect1[3].y - rect2[3].y));
   if ((error1 < Error) && (error2 < Error) && (error3 < Error) && (error4 < Error)) {
       return 1;
   }
}
void simpleROI(vector<vector<Point>> contours, vector<vector<Point>>& squares) {
   squares.push_back(contours[0]);
   for (int i = 1; i < contours.size(); i++) {
       int flag = 0;//如果在squares中找到近似的多边形,则不计入ROI中
       for (int j = 0; j < squares.size(); j++) {
           if (compareROI(contours[i], squares[j]) == 1) {
               flag = 1;
           }
       }
       if (flag == 0) {
           squares.push_back(contours[i]);
       }
   }
}

画出找到的矩形

void drawPorkerROI(Mat image, vector<vector<Point>> contours) {
   //画出矩形
   int porkerNumber = 0;
   for (size_t i = 0; i < contours.size(); i++)
   {
       porkerNumber++;
       const Point* p = &contours[i][0];
       int n = (int)contours[i].size();
       polylines(image, &p, &n, 1, true, Scalar(0, 255, 0), 3, LINE_AA);
   }
   //cout << porkerNumber << endl;
   /*namedWindow("image", WINDOW_NORMAL);
   imshow("image", image);*/
}

绘制出扑克牌

void drawPorker(vector<Mat>porkers) {
   for (int i = 0; i < porkers.size(); i++) {
       string name = "porker" + to_string(i);
       //namedWindow(name, WINDOW_NORMAL);
       //imshow(name, porkers[i]);
   }
}

效果如下图所示(发现自己把poker写成porker了,不碍事不碍事(〃′o`))


找到的扑克牌

将找到的扑克牌进行旋转操作

void rotatePorkers(vector<vector<Point>>squares, vector<Mat>& porkers) {
   for (int i = 0; i < squares.size(); i++) {
       float angle;
       Mat src = porkers[i];
       calculationAngle(squares[i], angle);
       rotate_arbitrarily_angle(src, porkers[i], angle);
   }
}

旋转的效果如下,旋转的代码在写一个识别扑克牌花色和点数的小程序(二)中会写

旋转后的图片

然后将旋转后的扑克牌组合成一张图片再识别一次,将扑克牌完整的和背景分开(想不出别的更简单的办法了),效果如下
切割出的扑克牌

最后一步救赎对扑克牌的花色点数区域进行切割然后识别

void identifyPorkers(vector<Mat>porkers,vector<String> &answer) {
   answer.clear();
   vector<Mat>indentifyROI;
   //对切割出来的扑克牌同一大小,并保存要识别的范围
   for (int i = 0; i < porkers.size(); i++) {
       resize(porkers[i], porkers[i], Size(171, 264), 1);
       //按照上述设定的大小 规定的剪裁范围
       vector<Point> coordinate = { {0,0},{0,70},{30,70},{30,0} };
       Rect boundRect = boundingRect(coordinate);
       Mat imageROI = porkers[i](boundRect);
       indentifyROI.push_back(imageROI);
   }
   //drawPorker(indentifyROI);
   //针对每个剪裁出来的小块,将数字和花色切割开进行识别
   Mat num;
   Mat suit;
   vector<Mat> numModels;
   vector<Mat> suitModels;
   vector<string>files;
   string numPath = "num";//数字模板包
   string suitPath = "suits";//花色模板包
   //加载模板图片
   loadImage(numModels, files, numPath);
   loadImage(suitModels, files, suitPath);
   //preModel(models);
   vector<string>allNum;
   vector<string>allSuit;
   for (int i = 0; i < indentifyROI.size(); i++) {
       vector<Point> coordinateNum = { {5,0},{5,40},{30,40},{30,0} };
       vector<Point> coordinateSuit = { {5,40},{5,70},{30,70},{30,40} };
       string number;
       string suitValue;
       string suitName;

       Rect boundRectNum = boundingRect(coordinateNum);
       Rect boundRectSuit = boundingRect(coordinateSuit);
       num = indentifyROI[i](boundRectNum);
       suit = indentifyROI[i](boundRectSuit);
       //string nameNum = "porkerNum" + to_string(i);
       //imshow(nameNum, num);
       //string nameSuit = "porkerSuit" + to_string(i);
       //imshow(nameSuit, suit);
       returnNum(num, numModels, number);
       returnSuit(suit, suitModels, suitValue);
       answer.push_back(suitValue); 
       answer.push_back(number);
   }
}

写到这这个小程序已经成功了一半,接下来我们在旋转的效果如下,旋转的代码在写一个识别扑克牌花色和点数的小程序(二)
)中会写中再补充一些关于旋转扑克牌,切割扑克牌和识别花色和点数的相关代码。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343