最近選修了數(shù)字圖像處理,課程設(shè)計(jì)選擇寫了一個(gè)可以識(shí)別撲克牌花色和點(diǎn)數(shù)的小程序傅蹂,來跟大家分享一下,請多多指教算凿。
開發(fā)環(huán)境
- VS2019
- OpenCV4.1.0
- QT5.13
最終效果
碼代碼過程
總共包含5個(gè)cpp文件
classfiy.cpp 里面包括了對(duì)撲克牌各種處理的函數(shù)
main.cpp 創(chuàng)建QT界面
PokerClassify.cpp 寫一個(gè)界面并和相關(guān)函數(shù)銜接起來
rotation.cpp 將各種角度的撲克牌旋轉(zhuǎn)成正的撲克牌
templateMatching.cpp 進(jìn)行模板匹配
classfiy.cpp
通過撲克牌的長寬比判斷是否是一個(gè)撲克牌
int ifPorker(vector<Point> approx) {
//規(guī)定多邊形的面積份蝴,以及是一個(gè)凸多邊形
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案例氓轰。用在不同灰度級(jí)下用輪廓檢測檢測出矩形邊緣婚夫,然后保存矩形頂點(diǎn)坐標(biāo)。
bool findPorker(Mat& image, vector<vector<Point>>& squares) {
squares.clear();
Mat gray, pyr, timg, gray0(image.size(), CV_8U);
//對(duì)圖片進(jìn)行下采樣再上采樣署鸡,其實(shí)是一個(gè)去噪的過程
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個(gè)輪廓的后一個(gè)輪廓案糙、前一個(gè)輪廓镐躲、父輪廓、內(nèi)嵌輪廓的索引編號(hào)
//findContours(bin, contours, hierarchy,RETR_EXTERNAL, CHAIN_APPROX_NONE);
//RETR_EXTERNAL=0只檢查外圍輪廓侍筛,RETR_LIST檢查所有輪廓
//RETR_CCOMP檢查外圍輪廓,有兩個(gè)等級(jí)關(guān)系 RETR_TREE 檢查所有的輪廓撒穷,建一個(gè)樹形結(jié)構(gòu)
//CHAIN_APPROX_NONE=1保存物體邊界上所有的輪廓點(diǎn)到contours
vector<Point> approx;
for (size_t i = 0; i < contours.size(); i++) {
//對(duì)圖像輪廓進(jìn)行多邊形擬合,對(duì)點(diǎn)集進(jìn)行逼近 采用的是道格拉斯-普客算法
approxPolyDP(contours[i], approx, arcLength(contours[i], true) * 0.02, true);
//規(guī)定多邊形的面積匣椰,以及是一個(gè)凸多邊形
if (ifPorker(approx) == 1) {
tempContours.push_back(approx);
}
}
}
simpleROI(tempContours, squares);
//drawPorkerROI(image, squares);
return true;
}
}
因?yàn)榉至瞬煌幕叶燃?jí)查找矩形,所以圖像中同一個(gè)矩形可能會(huì)有多個(gè)矩形框端礼。在這里我通過判斷頂點(diǎn)的距離去掉重復(fù)的矩形框禽笑。
int compareROI(vector<Point>rect1, vector<Point>rect2) {
int error1, error2, error3, error4;
int Error = 15;
bool ifPorker = true;
//起始點(diǎn)不是撲克牌的左上角點(diǎn)的,估計(jì)是異常數(shù)據(jù)了
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中找到近似的多邊形蛤奥,則不計(jì)入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]);
}
}
效果如下圖所示(發(fā)現(xiàn)自己把poker寫成porker了佳镜,不礙事不礙事(〃′o`))
將找到的撲克牌進(jìn)行旋轉(zhuǎn)操作
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);
}
}
旋轉(zhuǎn)的效果如下,旋轉(zhuǎn)的代碼在寫一個(gè)識(shí)別撲克牌花色和點(diǎn)數(shù)的小程序(二)中會(huì)寫
然后將旋轉(zhuǎn)后的撲克牌組合成一張圖片再識(shí)別一次凡桥,將撲克牌完整的和背景分開(想不出別的更簡單的辦法了)蟀伸,效果如下
最后一步救贖對(duì)撲克牌的花色點(diǎn)數(shù)區(qū)域進(jìn)行切割然后識(shí)別
void identifyPorkers(vector<Mat>porkers,vector<String> &answer) {
answer.clear();
vector<Mat>indentifyROI;
//對(duì)切割出來的撲克牌同一大小,并保存要識(shí)別的范圍
for (int i = 0; i < porkers.size(); i++) {
resize(porkers[i], porkers[i], Size(171, 264), 1);
//按照上述設(shè)定的大小 規(guī)定的剪裁范圍
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);
//針對(duì)每個(gè)剪裁出來的小塊缅刽,將數(shù)字和花色切割開進(jìn)行識(shí)別
Mat num;
Mat suit;
vector<Mat> numModels;
vector<Mat> suitModels;
vector<string>files;
string numPath = "num";//數(shù)字模板包
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);
}
}
寫到這這個(gè)小程序已經(jīng)成功了一半啊掏,接下來我們在旋轉(zhuǎn)的效果如下,旋轉(zhuǎn)的代碼在寫一個(gè)識(shí)別撲克牌花色和點(diǎn)數(shù)的小程序(二)
)中會(huì)寫中再補(bǔ)充一些關(guān)于旋轉(zhuǎn)撲克牌衰猛,切割撲克牌和識(shí)別花色和點(diǎn)數(shù)的相關(guān)代碼迟蜜。