C++ 纯 OpenCV 实现扑克牌实时识别
网上有很多用OpenCV或其他工具实现扑克牌或者简单的数字识别demo,但都讲的不够简洁清晰或者提供的代码太混乱,所以我自己用OpenCV实现了一下这个demo,识别效果还比较好,所以把过程和代码都放出来,可以一起学习交流。
完成一张扑克牌的识别主要步骤有:
- 从摄像头获取扑克牌图片
(旧代码)二值化后查找图片最外层轮廓,并截取出轮廓内部的图片,即拍摄的扑克牌- 使用霍夫线检测和旋转来标定扑克牌位置并截取,比通过查找轮廓标定更准确,对背景环境要求也更低(更新代码见github)
- 使用漫水填充算法把扑克牌四周的多余的背景变成和扑克牌牌面背景一样的白色像素
- 此时图片只剩白色背景以及黑色的扑克牌数字、花色、头像等,再查找最左上角轮廓并截取出,这就是扑克牌的数字
- 从余下的图片中再查找最左边的轮廓并截取出,这就是扑克牌的花色
- 可以将数字和花色都预先保存下来,进行一些处理,放到 KNN 里进行训练得到预测模型
- 得到模型后就可以从1开始一套走下来自动预测识别扑克牌了
一些说明:
9. 模型训练用的图片我保存在 TrainSample 目录下,图片命名见如下数组里(下划线后接0~39,我的TrainNum取了40,每组图片有40张,可以根据需要修改。):
string NumName[] = { "1_", "2_", "3_", "4_", "5_", "6_", "7_", "8_", "9_", "10_", "j_", "q_", "k_" };
string FlagName[] = { "hongtao_", "fangkuai_", "meihua_", "heitao_" };
10. 模型训练我是用的基本像素进行训练,预测效果蛮不错;但如果对预测准确率要求高的话,可以使用HOG特征去训练,准确率可以有一定提升。
11. 扑克牌最好放到一张大白纸上或黑色不反光的背景下拍摄,识别效果比较好,然后扑克牌需要摆放的比较正才能正确的提取并识别,我没有做图片的霍夫线检测和旋转,如果有需要可以自己实现一下,应该不难。
12. github上最新代码已加入霍夫线检测和旋转,识别更简单,准确率更高。
13. 我只实现了一张图片里只有一张扑克牌的情况,有多张扑克牌的话,步骤应该差不多,主要就是截取各个扑克牌的轮廓。
14. 可以实现下直接在显示的原始图上显示识别出的数字和花色,只要找到轮廓边界然后用putText()函数输入字符即可。
15. 完整工程代码可以到我的GitHub下载:https://github.com/hfq0219/PredictCard.git
下面开始贴代码,代码比较简单,就没有写详细的注释,稍微有些C++知识和OpenCV入门就能看懂:
//PredictCard.h
//Author: fengqi
#ifndef PREDICT_CARD
#define PREDICT_CARD
#include <iostream>
#include <string>
#include <opencv2\opencv.hpp>
using namespace std;
using namespace cv;
using namespace cv::ml;
void train_pixel(); //train the knn model for classifying.
int splitCard(Mat &m, int i, string num_, string flag_); //split the card to detect the number and flag.
int findCard(Mat &m); //find the card region which read from camera.
int predictNum(Mat &m); //predict the num of card.
int predictFlag(Mat &m); //predict the flag of card.
/**
reference: this function is the [main-function] to predict the card-picture which sended from the caller.
- first, findCard() to determine the card region;
- then, splitCard() to detect the flag and number of the card;
- last, if split succeed, call predictFlag() and predictNum() to predict the card using knn-model
which trained before.
function: scan(Mat &,int &,int &);
parameter: m - the Mat read from outside which includes the card.
suit - the flag of card, return to the caller if detected the flag correctly.
rank - the number of card, return to the caller if detected the number correctly.
return: void
*/
void scan(Mat &m,int &suit, int &rank){
//----------从单张图像找出数字和花色,比如从摄像头采集的图像---------------------
int flag = findCard(m);
if (flag == 0){
Mat m = imread("card.jpg", 0);
int result = splitCard(m, 1, "num", "flag");
if (result == 0){
cout << "截取成功,按任意键进行预测,ctrl-c 退出..." << endl;
waitKey();
Mat flag = imread("flag.jpg", 0);
Mat num = imread("num.jpg", 0);
suit = predictFlag(flag);
rank = predictNum(num);
}
else{
cout << "识别失败,请重新输入图片" << endl;
destroyWindow("num");
destroyWindow("flag");
suit = -1;
rank = -1;
return;
}
}
}
void train_pixel()
{
Mat NumData, NumLabels, FlagData, FlagLabels;
int trainNum = 40;
string NumName[] = { "1_", "2_", "3_", "4_", "5_", "6_", "7_", "8_", "9_", "10_", "j_", "q_", "k_" };
string FlagName[] = { "hongtao_", "fangkuai_", "meihua_", "heitao_" };
for (int i = 0; i < trainNum * 13; i++){
Mat img, tmp;
string path = "TrainSample\\";
path.append(NumName[i / trainNum]).append(to_string((i%trainNum))).append(".jpg");
img = imread(path, 0);
resize(img, tmp, Size(30, 40));
NumData.push_back(tmp.reshape(0, 1)); //序列化后放入特征矩阵
NumLabels.push_back(i / trainNum + 1); //对应的标注
}
NumData.convertTo(NumData, CV_32F); //uchar型转换为cv_32f
//使用KNN算法
int K = 5;
Ptr<TrainData> tData = TrainData::create(NumData, ROW_SAMPLE, NumLabels);
Ptr<KNearest> NumModel = KNearest::create();
NumModel->setDefaultK(K);
NumModel->setIsClassifier(true);
NumModel->train(tData);
NumModel->save("./num_knn_pixel.yml");
//-----------------------------------------------------------------------------------------------
for (int i = 0; i < trainNum * 4; i++){
Mat img, tmp;
string path = "TrainSample\\";
path.append(FlagName[i / trainNum]).append(to_string((i%trainNum))).append(".jpg");
img = imread(path, 0);
resize(img, tmp, Size(30, 30));
FlagData.push_back(tmp.reshape(0, 1)); //序列化后放入特征矩阵
FlagLabels.push_back(i / trainNum + 1); //对应的标注
}
FlagData.convertTo(FlagData, CV_32F); //uchar型转换为cv_32f
//使用KNN算法
int L = 5;
Ptr<TrainData> tFlag = TrainData::create(FlagData, ROW_SAMPLE, FlagLabels);
Ptr<KNearest> FlagModel = KNearest::create();
FlagModel->setDefaultK(L);
FlagModel->setIsClassifier(true);
FlagModel->train(tFlag);
FlagModel->save("./flag_knn_pixel.yml");
}
int findCard(Mat &m){
Mat gray, bin;
cvtColor(m, gray, COLOR_BGR2GRAY);
imshow("gray", gray);
//################################################
threshold(gray, bin, 80, 255, THRESH_BINARY); //---对光照和环境要求较高,阈值设置合适值-----------------
//################################################
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(bin, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
vector<vector<Point>>::iterator It;
//-------------------找出最外层轮廓,即扑克牌的轮廓--------------------------------
int up = 2000, down = 0, right = 0, left = 2000;
for (It = contours.begin(); It < contours.end(); It++){
Rect rect = boundingRect(*It);
Point tl = rect.tl();
Point br = rect.br();
if (up>tl.y) up = tl.y;
if (down < br.y) down = br.y;
if (left>tl.x) left = tl.x;
if (right < br.x) right = br.x;
}
if (up == 2000 || left == 2000) return -1;
Mat card;
card = gray(Range(up + 10, down), Range(left + 2, right)); //切割掉左上角一定区域的背景,便于定位数字和花色
//----------漫水填充,去掉扑克牌外面的黑色像素,只留下黑色的数字、花色以及白色背景------------------
threshold(card, card, 150, 255, THRESH_BINARY); //注意阈值选取
floodFill(card, Point(0, 0), Scalar(255, 255, 255));
floodFill(card, Point(0, card.rows - 1), Scalar(255, 255, 255));
floodFill(card, Point(card.cols - 1, 0), Scalar(255, 255, 255));
floodFill(card, Point(card.cols - 1, card.rows - 1), Scalar(255, 255, 255));
imwrite("card.jpg", card);
imshow("card", card);
return 0;
}
int splitCard(Mat &m, int i, string num_path, string flag_path){
//-------------------------找轮廓确定数字和花色的位置----------------------------------------------
threshold(m, m, 150, 255, THRESH_BINARY_INV); //注意阈值选取,与上一步重复了
//-------------------------找最左上角的一块联通区域,即扑克牌的数字---------------------------------
vector<vector<Point>> contours_Num;
vector<Vec4i> hierarchy_Num;
findContours(m, contours_Num, hierarchy_Num, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
vector<vector<Point>>::iterator It_Num;
int up = 1000, down = 1000, right = 1000, left = 1000;
for (It_Num = contours_Num.begin(); It_Num < contours_Num.end(); It_Num++){
Rect rect = boundingRect(*It_Num);
Point tl = rect.tl();
Point br = rect.br();
if (up > tl.y) up = tl.y;
if (down > br.y) down = br.y;
if (left > tl.x && (br.x - tl.x > 20)){
left = tl.x; right = br.x;
}
}
if (down - up < 25 || right - left < 20) return -1; //分辨率低于25*20判断为提取数字失败
if (down - up > 50 || right - left > 50) return -1; //分辨率高于50*50判断为提取数字失败
Mat num = m(Range(up, down), Range(left, right));
Mat tmp = m(Range(down, m.rows), Range(left, right));
imshow("num", num);
//-------------------------截去数字区域,从余下区域用同样方法提取花色--------------------------------
vector<vector<Point>> contours_Flag;
vector<Vec4i> hierarchy_Flag;
findContours(tmp, contours_Flag, hierarchy_Flag, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
vector<vector<Point>>::iterator It_Flag;
up = 1000; down = 1000; right = 1000; left = 1000;
for (It_Flag = contours_Flag.begin(); It_Flag < contours_Flag.end(); It_Flag++){
Rect rect = boundingRect(*It_Flag);
Point tl = rect.tl();
Point br = rect.br();
if (left>tl.x && (br.x - tl.x > 20)) {
left = tl.x;
right = br.x;
up = tl.y;
down = br.y;
}
}
if (down - up < 25) return -1; //分辨率低于25*20判断为提取花色失败
Mat flag = tmp(Range(up, down), Range(left, right));
imshow("flag", flag);
//imwrite(num_path.append(to_string(i)).append(".jpg"), num); //训练数据保存
//imwrite(flag_path.append(to_string(i)).append(".jpg"), flag);
imwrite(num_path.append(".jpg"), num); //预测数据保存
imwrite(flag_path.append(".jpg"), flag);
return 0;
}
int predictNum(Mat &m){
Ptr<KNearest> model_pixel = Algorithm::load<KNearest>("num_knn_pixel.yml");
Mat temp;
resize(m, temp, Size(30, 40));
Mat vec1;
vec1.push_back(temp.reshape(0, 1));
vec1.convertTo(vec1, CV_32F);
int r1 = model_pixel->predict(vec1); //对所有行进行预测
switch (r1){
case 1:
cout << "A"; break;
case 11:
cout << "J"; break;
case 12:
cout << "Q"; break;
case 13:
cout << "K"; break;
default:
cout << r1; break;
}
cout << endl;
return r1;
}
int predictFlag(Mat &m){
Ptr<KNearest> model_pixel = Algorithm::load<KNearest>("flag_knn_pixel.yml");
Mat temp;
resize(m, temp, Size(30, 30));
Mat vec1;
vec1.push_back(temp.reshape(0, 1));
vec1.convertTo(vec1, CV_32F);
int r1 = model_pixel->predict(vec1); //对所有行进行预测
cout << "识别的扑克牌是:";
switch (r1){
case 1:
cout << "红桃 "; break;
case 2:
cout << "方块 "; break;
case 3:
cout << "梅花 "; break;
case 4:
cout << "黑桃 "; break;
default:
break;
}
return r1;
}
#endif
下面是主调函数:
#include <iostream>
#include <opencv2\opencv.hpp>
#include "PredictCard.h"
using namespace std;
int main(){
VideoCapture capture(0);
int suit = 0, rank = 0;
while (1){
Mat m;
capture >> m;
scan(m,suit, rank);
cout << "suit: " << suit << ", rank: " << rank << endl;
waitKey();
}
return 0;
}
我的实验结果: