背景
- 在一次旅行中,在车上看到旁边坐着的两个同学用手机玩数独。但是当我凑过去的时候,被嘲笑了。。。
求解思路
- 找出图片中哪个格子是有数字的,并记录数字处于格子的什么位置
- 有数字的格子中,数字是多少
- 根据识别出来的数字和记录的位置初始化一个数独数组,并求解。
找出哪个格子有数字并记录位置
首先确定数独在哪个位置,即ROI 区域的寻找
- 这个比较简单,使用windows的画图软件,找出数独的左上角和右下角的坐标就可以了。
在ROI区域中寻找带有数字的格子
- 在opencv中提供一个强大的寻找轮廓的函数:findContours()。具体代码及注释
//contours是由一系列点构成的矩形,每个图形中又包括许多轮廓
//contours[i]:表示第 i 个轮廓,
//contours[i][i]:表示第 i 个轮廓的第 i 个点
vector<vector<Point> > contours;
//hierarchy[i][0] :后一个轮廓序号, 没有则-1
//hierarchy[i][1] :前一个轮廓序号, 没有则-1
//hierarchy[i][2] :子轮廓的序号, 没有则-1
//hierarchy[i][3] :父轮廓的序号,没有则 -1
vector<Vec4i> hierarchy;
//建立层级关系
findContours(roibin, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));
for (size_t i = 0; i < contours.size(); i++)
{ //当前轮廓有子轮廓,并且当前轮廓的面积大于10000
if (hierarchy[i][2] != -1 && contourArea(contours[i])>10000)
{//当前轮廓的子轮廓点的序号,即是数字外围的轮廓
int childIndex = hierarchy[i][2];
Rect recttemp = boundingRect(contours[i]);//第 i 个轮廓的轮廓
Rect numberbox = boundingRect(contours[childIndex]);//第 i 个轮廓的子轮廓,即包围数字
rect.push_back(numberbox);
pos.push_back(Point(recttemp.br().x / recttemp.width -1 , recttemp.br().y / recttemp.height - 1));
}
}
- 效果
识别数字是多少
制作样本数据
- 这里使用knn算法识别数字,但是是印刷体的数字。首先应找一些图片作为样本,为了提高效率和准确率,直接使用上面提取到的数字图片即可,但是要注意将所有照片的规格要统一一下。
vector<vector<Point> > contours;
vector<Vec4i> hierarchy;
findContours(roibin, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));
for (size_t i = 0; i < contours.size(); i++)
{//当前轮廓有子轮廓,并且当前轮廓的面积大于10000
if (hierarchy[i][2] != -1 && contourArea(contours[i])>10000)
{//当前轮廓的子轮廓点的序号,即是数字外围的轮廓
int childIndex = hierarchy[i][2];
Rect recttemp = boundingRect(contours[childIndex]);//第 i 个轮廓的字轮廓
string filename = to_string(i) + ".jpg";
Mat temp = roi(recttemp);
resize(temp, temp, Size(60, 60));
cvtColor(temp, temp, COLOR_BGR2GRAY);
imwrite(filename, temp);
}
}
- 将上述提取的数字分别放到9个文件夹中,使用这些作为样本。
训练数据
for (size_t i = 0; i < 10; i++)
{
string filename = samplefloderpath + "\\" + to_string(i);
string imgtype = ".jpg";
vector<string> imgvector;
getFilesName(filename, imgtype, imgvector);
for (size_t j = 0; j < imgvector.size(); j++)
{
Mat tmp = imread(imgvector[j], IMREAD_GRAYSCALE);
data.push_back(tmp.reshape(0,1));/*将图像转换成行向量,每张图像就是一个行向量,有多少张图片该矩阵中就有多少行*/
labels.push_back((int)i);//对应的标签
trainNum++;
}
}
data.convertTo(data, CV_32F); //uchar型转换为cv_32f
int samplesNum = data.rows;
Mat trainData, trainLabels;
trainData = data(Range(0, trainNum), Range::all());
trainLabels = labels(Range(0, trainNum), Range::all());
//使用KNN算法
int K = 5;
Ptr<TrainData> tData = TrainData::create(trainData, ROW_SAMPLE, trainLabels);
Ptr<KNearest> model = KNearest::create();
model->setDefaultK(K);
model->setIsClassifier(true);
model->train(tData);
识别数字
Mat sample = imread("xx.jpg",IMREAD_GRAYSCALE);//灰度图
sample = sample.reshape(0, 1);//还是要将数据转换成行向量
sample.convertTo(sample, CV_32F);//这里一定要做类型转换,否则会报错
int res = int(model->predict(sample));
递归求解数独
按照数独的规则检查每个格子可以填写的数字
bool CheckCell(int(*sudoku)[9], int index, int value)
{
int row = index / 9;
int col = index % 9;
//检查行
for (int i = 0; i < 9; i++)
{
if (sudoku[row][i] == value) return false;
}
//检查列
for (int i = 0; i < 9; i++)
{
if (sudoku[i][col] == value) return false;
}
//检查小格子
int box_row = row / 3;
int box_col = col / 3;
int box_row_start = box_row * 3;
int box_row_to = box_row * 3 + 3;
int box_col_start = box_col * 3;
int box_col_to = box_col * 3 + 3;
for (int i = box_row_start; i < box_row_to; i++)
{
for (int j = box_col_start; j < box_col_to; j++)
{
if (sudoku[i][j] == value)
return false;
}
}
//都正确返回true
return true;
}
递归求解
//求解数独,从第 0 个位置开始填写就可以了
void DFS_Slove(int(*sudoku)[9], int index)
{
static bool flag = false;
if (index > 80)
{
flag = true;
return;
}
else
{
int row = index / 9;
int col = index % 9;
if (sudoku[row][col] != 0)
{
DFS_Slove(sudoku, index + 1);
}
else
{
for (int i = 1; i <= 9; i++)
{
if (CheckCell(sudoku, index, i) == true)
{
sudoku[row][col] = i;
DFS_Slove(sudoku, index + 1);
if (flag == true) return;
sudoku[row][col] = 0;
}
}
}
}
return;
}
最终效果
样本数据和代码下载地址
直接点击
https://download.csdn.net/download/mengxiangpeng123/10606576