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

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

开发环境

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。