最近选修了数字图像处理,课程设计选择写了一个可以识别扑克牌花色和点数的小程序,来跟大家分享一下,请多多指教。
开发环境
- 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);
}
}
写到这这个小程序已经成功了一半,接下来我们在旋转的效果如下,旋转的代码在写一个识别扑克牌花色和点数的小程序(二)
)中会写中再补充一些关于旋转扑克牌,切割扑克牌和识别花色和点数的相关代码。