寫一個(gè)識(shí)別撲克牌花色和點(diǎn)數(shù)的小程序(一)

最近選修了數(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)后的圖片

然后將旋轉(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)代碼迟蜜。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市啡省,隨后出現(xiàn)的幾起案子娜睛,更是在濱河造成了極大的恐慌,老刑警劉巖卦睹,帶你破解...
    沈念sama閱讀 218,451評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件畦戒,死亡現(xiàn)場離奇詭異,居然都是意外死亡分预,警方通過查閱死者的電腦和手機(jī)兢交,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來笼痹,“玉大人配喳,你說我怎么就攤上這事〉矢桑” “怎么了晴裹?”我有些...
    開封第一講書人閱讀 164,782評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長救赐。 經(jīng)常有香客問我涧团,道長只磷,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,709評(píng)論 1 294
  • 正文 為了忘掉前任泌绣,我火速辦了婚禮钮追,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘阿迈。我一直安慰自己元媚,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評(píng)論 6 392
  • 文/花漫 我一把揭開白布苗沧。 她就那樣靜靜地躺著刊棕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪待逞。 梳的紋絲不亂的頭發(fā)上甥角,一...
    開封第一講書人閱讀 51,578評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音识樱,去河邊找鬼嗤无。 笑死,一個(gè)胖子當(dāng)著我的面吹牛怜庸,可吹牛的內(nèi)容都是我干的翁巍。 我是一名探鬼主播,決...
    沈念sama閱讀 40,320評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼休雌,長吁一口氣:“原來是場噩夢啊……” “哼灶壶!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起杈曲,我...
    開封第一講書人閱讀 39,241評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤驰凛,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后担扑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體恰响,經(jīng)...
    沈念sama閱讀 45,686評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評(píng)論 3 336
  • 正文 我和宋清朗相戀三年涌献,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了胚宦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,992評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡燕垃,死狀恐怖枢劝,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情卜壕,我是刑警寧澤您旁,帶...
    沈念sama閱讀 35,715評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站轴捎,受9級(jí)特大地震影響鹤盒,放射性物質(zhì)發(fā)生泄漏蚕脏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評(píng)論 3 330
  • 文/蒙蒙 一侦锯、第九天 我趴在偏房一處隱蔽的房頂上張望驼鞭。 院中可真熱鬧,春花似錦尺碰、人聲如沸终议。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至细燎,卻和暖如春两曼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背玻驻。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評(píng)論 1 270
  • 我被黑心中介騙來泰國打工悼凑, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人璧瞬。 一個(gè)月前我還...
    沈念sama閱讀 48,173評(píng)論 3 370
  • 正文 我出身青樓户辫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親嗤锉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子渔欢,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評(píng)論 2 355