答题卡的识别

目录

1.问题描述

2.解决思路

3.代码实现

4.相关资料


该博客整编于:https://www.pyimagesearch.com/

1.问题描述

现在,我们需要识别一张简易的答题卡,如图1-1所示。

                                                                                       图1-1 简易答题卡

最终的识别结果如图1-2所示。其中,选对的答案用绿色表示,错选的用红色表示。

那么在答题卡识别的问题中有哪些待续解决的问题呢?我的理解是这样的:

1.答题卡区域的分割问题:想要进行答题卡识别总得先把答题卡区域和环境区域分割出来吧。

2.答题卡纸张背景和答案的分离问题:我们需要的只有答案的区域,因此需要解决答案和答题卡背景的分割问题。

3.轮廓的筛选问题:筛选出我们想要的轮廓,排除那些不需要的轮廓信息。

4.轮廓的排序和定位问题:如何对轮廓进行行和列的定位,这很重要。

5.检测答题者所选择的选项:检测漏选、多选的情况。

2.解决思路

2.1 答题卡区域的分割问题

在这里,由于环境色的一致性,我们使用了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算子计算图

2.2 分割答题卡的纸张和答题区域

首先,我们要找到答题卡轮廓区域的边界,利用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<vector<Point>> contours;
	findContours(canny, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
	vector<Point>result_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<Point> 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<Point2f> 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<Point2f> Pt;
	src.convertTo(src,CV_32F);
	Pt = (vector<Point2f>) 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.3 轮廓的筛选问题

首先,在对轮廓进行少筛选之前,最好对轮廓进行膨胀运算,这是为了增加轮廓的稳定性,防止如图2.4所示的情况:

                                                                                       图2-4 轮廓的不完整性

          给定轮廓的筛选条件,宽度和高度同时大于20;

	//轮廓筛选
	//1.改善轮廓
	Mat element = getStructuringElement(MORPH_RECT, Size(3, 3));
	dilate(target,target,element);
	//2.筛选轮廓
	vector<vector<Point>> target_contour;
	vector<vector<Point>> 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  筛选出所有想要的轮廓

2.4 轮廓的排序问题

如何对轮廓进行排序,这是个很重要的问题,在这里我们使用了计算圆心的方式,依据圆心的位置来确认答题卡轮廓的位置:

	//轮廓的排序问题
	//1.计算所有外接圆基本数据
	vector<float> radius(selected_contour.size());
	vector<Point2f> 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<vector<vector<Point>>> classed_contours;
	classed_contours.resize(5,vector<vector<Point>>(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.绘制并验证
	vector<Scalar>color;
	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 轮廓的分类结果(用不同的颜色表示)

2.5 检测答题者所作出的选择,并检测漏选和多选的情况

我采用了二维数组的方式来对当前的答案进行统计,用蓝色绘制正确答案,红色绘制错误的答案:

//检测答题者的选项,并检查多选和漏选
	//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<vector<Rect>> re_rect;
	re_rect.resize(5,vector<Rect>(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<float>(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<float>(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 最终的检测图

3.代码实现

全部的实现代码:

#include<iostream>
#include<opencv2/opencv.hpp>

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<vector<Point>> contours;
	findContours(canny, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
	vector<Point>result_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<Point> 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<Point2f> 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<Point2f> Pt;
	src.convertTo(src,CV_32F);
	Pt = (vector<Point2f>) 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<vector<Point>> target_contour;
	vector<vector<Point>> 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<float> radius(selected_contour.size());
	vector<Point2f> 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<vector<vector<Point>>> classed_contours;
	classed_contours.resize(5,vector<vector<Point>>(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.绘制并验证
	vector<Scalar>color;
	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<vector<Rect>> re_rect;
	re_rect.resize(5,vector<Rect>(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<float>(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<float>(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;
}

4.相关资料

1.禾路的博客园:

https://www.cnblogs.com/jsxyhelu/p/9790979.html

2.opencv convertTo用法:

https://blog.csdn.net/qq_22764813/article/details/52135686

3.vector<Point> 到 vector<Point2f>的类型转化:

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

 

 

猜你喜欢

转载自blog.csdn.net/qiao_lili/article/details/83176480