目录
1.问题描述
2.解决思路
3.代码实现
4.相关资料
现在,我们需要识别一张简易的答题卡,如图1-1所示。
图1-1 简易答题卡
最终的识别结果如图1-2所示。其中,选对的答案用绿色表示,错选的用红色表示。
那么在答题卡识别的问题中有哪些待续解决的问题呢?我的理解是这样的:
1.答题卡区域的分割问题:想要进行答题卡识别总得先把答题卡区域和环境区域分割出来吧。
2.答题卡纸张背景和答案的分离问题:我们需要的只有答案的区域,因此需要解决答案和答题卡背景的分割问题。
3.轮廓的筛选问题:筛选出我们想要的轮廓,排除那些不需要的轮廓信息。
4.轮廓的排序和定位问题:如何对轮廓进行行和列的定位,这很重要。
5.检测答题者所选择的选项:检测漏选、多选的情况。
在这里,由于环境色的一致性,我们使用了canny边缘检测算子,检测出答题卡的边界信息。
分割代码如下:
Mat answerSheet = imread("answerSheet.png");
//灰度转化
Mat gray;
cvtColor(answerSheet,gray,CV_BGR2GRAY);
//进行高斯滤波
Mat blurred;
GaussianBlur(gray,blurred,Size(3,3),0);
//进行canny边缘检测
Mat canny;
Canny(blurred,canny,75,200);
计算的图像如图2-1:
图 2-1 canny算子计算图
首先,我们要找到答题卡轮廓区域的边界,利用DP算法计算出轮廓的角点,最后基于透视变化对图像进行矫正,即转化为鸟瞰图。实现的代码如下:
//排序算子
bool sortBy_x( Point &a, Point &b)
{
return a.x < b.x;
}
bool sortBy_y( Point &a, Point &b)
{
return a.y < b.y;
}
//寻找矩形边界
vector> contours;
findContours(canny, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
vectorresult_contour;
if (contours.size() == 1)
{
result_contour = contours[0];
}
else
{
int max = -1;
int index = -1;
for (int i = 0; i < contours.size(); i++)
{
int tem = arcLength(contours[i], true);
if (tem > max) max = tem;
index = i;
}
result_contour = contours[index];
}
//使用DP算法拟合答题卡的几何轮廓,保存点集pts并顺时针排序
vector pts;
approxPolyDP(result_contour,pts,(int)arcLength(result_contour,true)*0.02,true);
if (pts.size() != 4) return 1;
sort(pts.begin(), pts.end(), sortBy_x);
sort(pts.begin(), pts.end(), sortBy_y);
//进行透视变换
//1.确定变化尺寸的宽度
int width;
int width1 = (pts[0].x - pts[1].x)*(pts[0].x - pts[1].x) + (pts[0].y - pts[1].y)*(pts[0].y - pts[1].y);
int width2= (pts[2].x - pts[3].x)*(pts[2].x - pts[3].x) + (pts[2].y - pts[3].y)*(pts[2].y - pts[3].y);
if (width1 > width2) width = sqrt(width1);
else width = sqrt(width2);
//2.确定变化尺寸的高度
int height;
int height1 = (pts[0].x - pts[3].x)*(pts[0].x - pts[3].x) + (pts[0].y - pts[3].y)*(pts[0].y - pts[3].y);
int height2 = (pts[2].x - pts[1].x)*(pts[2].x - pts[1].x) + (pts[2].y - pts[1].y)*(pts[2].y - pts[1].y);
if (height1 > height2) height= sqrt(height1);
else height = sqrt(height2);
//3.计算透视变换矩阵
vector Pts(4);
Pts[0]=(Point2f(0,0));
Pts[1]=(Point2f(width-1, 0));
Pts[2]=(Point2f(width-1, height-1));
Pts[3]=(Point2f(0, height-1));
//4.计算透视变换矩阵
//4.1类型转化
Mat src = Mat(pts);
vector Pt;
src.convertTo(src,CV_32F);
Pt = (vector) src;
//4.2计算M矩阵
Mat M = getPerspectiveTransform(Pt,Pts);
//5.进行透视变换
Mat birdMat;
warpPerspective(answerSheet,birdMat,M,Size(width,height));
最终的结果如图2-2所示:
图2-2 计算的鸟瞰图
随后,我们要将答题卡的图形信息找出来,在这里采用OTSU阈值分割的方法:
//OTSU阈值分割
Mat gray_birdMat;
cvtColor(birdMat,gray_birdMat,CV_BGR2GRAY);
Mat target;
threshold(gray_birdMat, target,0,255,CV_THRESH_BINARY_INV | CV_THRESH_OTSU);
分割的结果如图2-3所示:
图2.3 otsu分割结果
首先,在对轮廓进行少筛选之前,最好对轮廓进行膨胀运算,这是为了增加轮廓的稳定性,防止如图2.4所示的情况:
图2-4 轮廓的不完整性
给定轮廓的筛选条件,宽度和高度同时大于20;
//轮廓筛选
//1.改善轮廓
Mat element = getStructuringElement(MORPH_RECT, Size(3, 3));
dilate(target,target,element);
//2.筛选轮廓
vector> target_contour;
vector> selected_contour;
findContours(target,target_contour,RETR_EXTERNAL,CHAIN_APPROX_SIMPLE);
for (auto m : target_contour)
{
Rect rect = boundingRect(m);
double k = (double)rect.height / rect.width;
if (rect.height > 20 && rect.width > 20 )
{
selected_contour.push_back(m);
}
}
//3.验证结果
Mat answerSheet_con=target.clone();
cvtColor(answerSheet_con,answerSheet_con,CV_GRAY2BGR);
drawContours(answerSheet_con,selected_contour,-1,Scalar(0,0,255),2);
这样,我们便筛选出了所有想要的轮廓,用红色标出,如图2-5所示:
图2-5 筛选出所有想要的轮廓
如何对轮廓进行排序,这是个很重要的问题,在这里我们使用了计算圆心的方式,依据圆心的位置来确认答题卡轮廓的位置:
//轮廓的排序问题
//1.计算所有外接圆基本数据
vector radius(selected_contour.size());
vector center(selected_contour.size());
for (int i = 0; i < selected_contour.size();i++)
{
minEnclosingCircle(selected_contour[i],center[i],radius[i]);
}
//2.计算x轴分割间隔
int x_min = 999;
int x_max = -1;
int x_interval = 0;
for (auto m : center)
{
if (m.x < x_min) x_min = m.x;
if (m.x > x_max) x_max = m.x;
}
x_interval = (x_max - x_min) / 4;
//3.计算y轴分割间隔
int y_min = 999;
int y_max = -1;
int y_interval = 0;
for (auto m : center)
{
if (m.y < y_min) y_min = m.y;
if (m.y > y_max) y_max = m.y;
}
y_interval = (y_max - y_min) / 4;
//4.分类
vector>> classed_contours;
classed_contours.resize(5,vector>(5));
int thresh_x = x_interval / 2;
int thresh_y = y_interval / 2;
for (int i = 0; i < center.size();i++)
{
Point point = center[i];
int index_x = round((point.x - x_min) / x_interval);
int index_y= round((point.y - y_min) / y_interval);
classed_contours[index_y][index_x] = selected_contour[i];
}
//5.绘制并验证
vectorcolor;
color.push_back(Scalar(0,0,255));
color.push_back(Scalar(255, 0, 255));
color.push_back(Scalar(0, 255, 255));
color.push_back(Scalar(255, 0, 0));
color.push_back(Scalar(0, 255, 0));
Mat test_result = target.clone();
cvtColor(test_result, test_result,CV_GRAY2BGR);
for (int i = 0; i < 5; i++)
{
drawContours(test_result,classed_contours[i],-1,color[i],2);
}
最后的轮廓分类结果如图2-6所示:
图2-6 轮廓的分类结果(用不同的颜色表示)
我采用了二维数组的方式来对当前的答案进行统计,用蓝色绘制正确答案,红色绘制错误的答案:
//检测答题者的选项,并检查多选和漏选
//1.给定正确的选项 1-5 BCECB
int result_count[5][5] = { 0 };
result_count[0][1] = 1;
result_count[1][2] = 1;
result_count[2][4] = 1;
result_count[3][2] = 1;
result_count[4][1] = 1;
//2.检测答题者的选项
//2.1 确定答题区域非零点的数目
vector> re_rect;
re_rect.resize(5,vector(5));
Mat count_roi(Size(5, 5), CV_32FC1, Scalar(0));
int min_count = 999;
int max_count = -1;
for (int ii = 0; ii < 5; ii++)
{
for (int jj = 0; jj < 5; jj++)
{
re_rect[ii][jj] = boundingRect(classed_contours[ii][jj]);
Mat tem = target(re_rect[ii][jj]);
int count = countNonZero(tem);
if (count > max_count) max_count = count;
if (count < min_count) min_count = count;
count_roi.at(ii,jj)=count;
}
}
int mean = (max_count+min_count) / 8;
Mat option_diff = abs(count_roi - max_count);
//2.2判断选项结果,存储在数组result_count中
for (int ii = 0; ii < 5; ii++)
{
for (int jj = 0; jj < 5; jj++)
{
if (option_diff.at(ii, jj) < mean) result_count[ii][jj]++;
}
}
Mat label_answer = birdMat.clone();
for (int ii = 0; ii < 5; ii++)
{
bool no_Answer = false;
bool several_Answer = false;
bool wrong_Answer = false;
int row_sum = 0;
int count_no_zero = 0;
for (int m : result_count[ii])
{
row_sum += m;
if (m != 0)count_no_zero++;
}
if (row_sum == 1) no_Answer = true;
if (row_sum >= 2 && count_no_zero > 1) several_Answer = true;
if (row_sum == 2 && count_no_zero == 2) wrong_Answer = true;
//2.3 标记错误答案(红色),标记正确答案(蓝色)
if (several_Answer)
{
for (int i = 0; i < 5; i++)
{
if (result_count[ii][i] == 1)
drawContours(label_answer, classed_contours[ii], i, Scalar(0, 0, 255));
}
}
if (wrong_Answer)
{
for (int i = 0; i < 5; i++)
{
if (result_count[ii][i] == 1)
drawContours(label_answer, classed_contours[ii], i, Scalar(0, 0, 255));
}
}
}
drawContours(label_answer, classed_contours[0], 1, Scalar(255, 0, 0));
drawContours(label_answer, classed_contours[1], 2, Scalar(255, 0, 0));
drawContours(label_answer, classed_contours[2], 4, Scalar(255, 0, 0));
drawContours(label_answer, classed_contours[3], 2, Scalar(255, 0, 0));
drawContours(label_answer, classed_contours[4], 1, Scalar(255, 0, 0));
最终的效果图如图2-7所示:
图2-7 最终的检测图
全部的实现代码:
#include
#include
using namespace cv;
using namespace std;
bool sortBy_x( Point &a, Point &b)
{
return a.x < b.x;
}
bool sortBy_y( Point &a, Point &b)
{
return a.y < b.y;
}
int main()
{
Mat answerSheet = imread("answerSheet.png");
//灰度转化
Mat gray;
cvtColor(answerSheet,gray,CV_BGR2GRAY);
//进行高斯滤波
Mat blurred;
GaussianBlur(gray,blurred,Size(3,3),0);
//进行canny边缘检测
Mat canny;
Canny(blurred,canny,75,200);
//寻找矩形边界
vector> contours;
findContours(canny, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
vectorresult_contour;
if (contours.size() == 1)
{
result_contour = contours[0];
}
else
{
int max = -1;
int index = -1;
for (int i = 0; i < contours.size(); i++)
{
int tem = arcLength(contours[i], true);
if (tem > max) max = tem;
index = i;
}
result_contour = contours[index];
}
//使用DP算法拟合答题卡的几何轮廓,保存点集pts并顺时针排序
vector pts;
approxPolyDP(result_contour,pts,(int)arcLength(result_contour,true)*0.02,true);
if (pts.size() != 4) return 1;
sort(pts.begin(), pts.end(), sortBy_x);
sort(pts.begin(), pts.end(), sortBy_y);
//进行透视变换
//1.确定变化尺寸的宽度
int width;
int width1 = (pts[0].x - pts[1].x)*(pts[0].x - pts[1].x) + (pts[0].y - pts[1].y)*(pts[0].y - pts[1].y);
int width2= (pts[2].x - pts[3].x)*(pts[2].x - pts[3].x) + (pts[2].y - pts[3].y)*(pts[2].y - pts[3].y);
if (width1 > width2) width = sqrt(width1);
else width = sqrt(width2);
//2.确定变化尺寸的高度
int height;
int height1 = (pts[0].x - pts[3].x)*(pts[0].x - pts[3].x) + (pts[0].y - pts[3].y)*(pts[0].y - pts[3].y);
int height2 = (pts[2].x - pts[1].x)*(pts[2].x - pts[1].x) + (pts[2].y - pts[1].y)*(pts[2].y - pts[1].y);
if (height1 > height2) height= sqrt(height1);
else height = sqrt(height2);
//3.计算透视变换矩阵
vector Pts(4);
Pts[0]=(Point2f(0,0));
Pts[1]=(Point2f(width-1, 0));
Pts[2]=(Point2f(width-1, height-1));
Pts[3]=(Point2f(0, height-1));
//4.计算透视变换矩阵
//4.1类型转化
Mat src = Mat(pts);
vector Pt;
src.convertTo(src,CV_32F);
Pt = (vector) src;
//4.2计算M矩阵
Mat M = getPerspectiveTransform(Pt,Pts);
//5.进行透视变换
Mat birdMat;
warpPerspective(answerSheet,birdMat,M,Size(width,height));
//OTSU阈值分割
Mat gray_birdMat;
cvtColor(birdMat,gray_birdMat,CV_BGR2GRAY);
Mat target;
threshold(gray_birdMat, target,0,255,CV_THRESH_BINARY_INV | CV_THRESH_OTSU);
//轮廓筛选
//1.改善轮廓
Mat element = getStructuringElement(MORPH_RECT, Size(3, 3));
dilate(target,target,element);
//2.筛选轮廓
vector> target_contour;
vector> selected_contour;
findContours(target,target_contour,RETR_EXTERNAL,CHAIN_APPROX_SIMPLE);
for (auto m : target_contour)
{
Rect rect = boundingRect(m);
double k = (double)rect.height / rect.width;
if (rect.height > 20 && rect.width > 20 )
{
selected_contour.push_back(m);
}
}
//3.验证结果
Mat answerSheet_con=target.clone();
cvtColor(answerSheet_con,answerSheet_con,CV_GRAY2BGR);
drawContours(answerSheet_con,selected_contour,-1,Scalar(0,0,255),2);
//轮廓的排序问题
//1.计算所有外接圆基本数据
vector radius(selected_contour.size());
vector center(selected_contour.size());
for (int i = 0; i < selected_contour.size();i++)
{
minEnclosingCircle(selected_contour[i],center[i],radius[i]);
}
//2.计算x轴分割间隔
int x_min = 999;
int x_max = -1;
int x_interval = 0;
for (auto m : center)
{
if (m.x < x_min) x_min = m.x;
if (m.x > x_max) x_max = m.x;
}
x_interval = (x_max - x_min) / 4;
//3.计算y轴分割间隔
int y_min = 999;
int y_max = -1;
int y_interval = 0;
for (auto m : center)
{
if (m.y < y_min) y_min = m.y;
if (m.y > y_max) y_max = m.y;
}
y_interval = (y_max - y_min) / 4;
//4.分类
vector>> classed_contours;
classed_contours.resize(5,vector>(5));
int thresh_x = x_interval / 2;
int thresh_y = y_interval / 2;
for (int i = 0; i < center.size();i++)
{
Point point = center[i];
int index_x = round((point.x - x_min) / x_interval);
int index_y= round((point.y - y_min) / y_interval);
classed_contours[index_y][index_x] = selected_contour[i];
}
//5.绘制并验证
vectorcolor;
color.push_back(Scalar(0,0,255));
color.push_back(Scalar(255, 0, 255));
color.push_back(Scalar(0, 255, 255));
color.push_back(Scalar(255, 0, 0));
color.push_back(Scalar(0, 255, 0));
Mat test_result = target.clone();
cvtColor(test_result, test_result,CV_GRAY2BGR);
for (int i = 0; i < 5; i++)
{
drawContours(test_result,classed_contours[i],-1,color[i],2);
}
//检测答题者的选项,并检查多选和漏选
//1.给定正确的选项 1-5 BCECB
int result_count[5][5] = { 0 };
result_count[0][1] = 1;
result_count[1][2] = 1;
result_count[2][4] = 1;
result_count[3][2] = 1;
result_count[4][1] = 1;
//2.检测答题者的选项
//2.1 确定答题区域非零点的数目
vector> re_rect;
re_rect.resize(5,vector(5));
Mat count_roi(Size(5, 5), CV_32FC1, Scalar(0));
int min_count = 999;
int max_count = -1;
for (int ii = 0; ii < 5; ii++)
{
for (int jj = 0; jj < 5; jj++)
{
re_rect[ii][jj] = boundingRect(classed_contours[ii][jj]);
Mat tem = target(re_rect[ii][jj]);
int count = countNonZero(tem);
if (count > max_count) max_count = count;
if (count < min_count) min_count = count;
count_roi.at(ii,jj)=count;
}
}
int mean = (max_count+min_count) / 8;
Mat option_diff = abs(count_roi - max_count);
//2.2判断选项结果,存储在数组result_count中
for (int ii = 0; ii < 5; ii++)
{
for (int jj = 0; jj < 5; jj++)
{
if (option_diff.at(ii, jj) < mean) result_count[ii][jj]++;
}
}
Mat label_answer = birdMat.clone();
for (int ii = 0; ii < 5; ii++)
{
bool no_Answer = false;
bool several_Answer = false;
bool wrong_Answer = false;
int row_sum = 0;
int count_no_zero = 0;
for (int m : result_count[ii])
{
row_sum += m;
if (m != 0)count_no_zero++;
}
if (row_sum == 1) no_Answer = true;
if (row_sum >= 2 && count_no_zero > 1) several_Answer = true;
if (row_sum == 2 && count_no_zero == 2) wrong_Answer = true;
//2.3 标记错误答案(红色),标记正确答案(蓝色)
if (several_Answer)
{
for (int i = 0; i < 5; i++)
{
if (result_count[ii][i] == 1)
drawContours(label_answer, classed_contours[ii], i, Scalar(0, 0, 255));
}
}
if (wrong_Answer)
{
for (int i = 0; i < 5; i++)
{
if (result_count[ii][i] == 1)
drawContours(label_answer, classed_contours[ii], i, Scalar(0, 0, 255));
}
}
}
drawContours(label_answer, classed_contours[0], 1, Scalar(255, 0, 0));
drawContours(label_answer, classed_contours[1], 2, Scalar(255, 0, 0));
drawContours(label_answer, classed_contours[2], 4, Scalar(255, 0, 0));
drawContours(label_answer, classed_contours[3], 2, Scalar(255, 0, 0));
drawContours(label_answer, classed_contours[4], 1, Scalar(255, 0, 0));
return 0;
}
1.禾路的博客园:
https://www.cnblogs.com/jsxyhelu/p/9790979.html
2.opencv convertTo用法:
https://blog.csdn.net/qq_22764813/article/details/52135686
3.vector
https://stackoverflow.com/questions/7386210/convert-opencv-2-vectorpoint2i-to-vectorpoint2f
4.opencv 中的Rect类:
https://blog.csdn.net/qq_30214939/article/details/65648273
5.opencv中copyTo的应用:
https://www.cnblogs.com/phoenixdsg/p/8420716.html
6.开辟二维的vector矢量:
https://blog.csdn.net/zchlww/article/details/44678757