基于opencv答题卡识别

2022-08-20 12:56:33

1.问题描述:

模拟考试答题卡的识别:如下图所示的一张答题卡,需要自动识别并标记出考生选择的选项,以及标记出正确答案。考生选择的选项用蓝色标记,标准答案用绿色标记。

                                                        

2.解决思路:

2.1 首先打开相机拍摄一张答题卡

//用相机拍一张答题卡
	
	VideoCapture cap(0);
	if (!cap.isOpened())
	{
		cout << "open failed !";

	}
	Mat frame, gray;
	cap.read(frame);
	cvtColor(frame, gray, COLOR_BGR2GRAY);
	imwrite("C:\\Users\\86176\\Downloads\\visionimage\\testdetect.jpg", gray);
	imshow("frame", frame);
	waitKey(0);

2.2对拍摄的答题卡图像进行预处理

颜色空间转换--------直方图均衡进行图像增强------高斯模糊去噪------canny边缘检测

Mat img = imread("C:\\Users\\86176\\Downloads\\visionimage\\testdetect.jpg");
	Mat binary, gry, equalhist;
	cvtColor(img, gry, COLOR_BGR2GRAY);
	equalizeHist(gry, equalhist);
	//imshow("eq", equalhist);
	GaussianBlur(equalhist, equalhist, Size(3, 3),10,20);
	Canny(equalhist, binary, 50, 300);
	imshow("can", binary);

2.3进行轮廓检测,筛选出答题卡纸张的区域,便于接下来的透视变换

vector<vector<Point>> bigcontour;
	vector<double>areas;
	double max = 0;
	int index = -1;
	findContours(binary, bigcontour, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
	cout << bigcontour.size() << endl;
	for (int i = 0; i < bigcontour.size(); ++i)
	{
		double area = contourArea(bigcontour[i]);
		
		if (area > max)
		{
			max = area;
			index = i;
		}
		areas.push_back(area);
		


		//drawContours(img, bigcontour, i, Scalar( 0,0,200), 2);

	}
	cout << max << endl;
	cout << index << endl;

 筛选出最大的轮廓后,对该轮廓进行多边形逼近;从而得到透视变换前的四个顶点坐标;再计算出透视变化矩形的宽高,最后进行透视变换,将答题卡区域提取出来:

Mat result;
	Rect rec=boundingRect(bigcontour[index]);
	vector<Point2f>app;
	approxPolyDP(bigcontour[index], app, 10, true);
	//cout << app << endl;
	int curwidth = sqrt(
		pow((app[0].x - app[3].x), 2) + pow((app[0].y - app[3].y), 2)
	);
	int curheight = sqrt(
		pow((app[0].x - app[1].x), 2) + pow((app[0].y - app[1].y), 2)
	);

	vector<Point2f>currs;
	currs.emplace_back(Point(55, 30));
	currs.emplace_back(Point(55, 30+curheight));
	currs.emplace_back(Point(55+curwidth, 30+curheight));
	currs.emplace_back(Point(55 + curwidth, 30));

	Mat  pers = getPerspectiveTransform(app, currs);
	Mat perspect;

	warpPerspective(img, perspect, pers, img.size());
	

	Mat out;
	Rect roi = Rect(55, 30, curwidth, curheight);
	perspect(roi).copyTo(out);

 2.4采用OTSU阈值分割的方法将答题卡的图形信息找出来

Mat otsu, outgray, outdial;
	cvtColor(out, outgray,COLOR_BGR2GRAY);
	threshold(outgray, otsu, 100, 220, THRESH_OTSU|THRESH_BINARY_INV);
	imshow("otsu", otsu);

	//对图像进行膨胀
	Mat mod = getStructuringElement(MorphShapes::MORPH_RECT, Size(5, 5));
	morphologyEx(otsu, outdial, MORPH_DILATE, mod);

 膨胀后的图形区域:

 2.5再进行轮廓筛选,筛选出答题区域:

//再进行轮廓检测
	vector<vector<Point>>contoursagin, selectcontours;
	findContours(outdial, contoursagin, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
	for (auto m : contoursagin)
	{
		Rect bound = boundingRect(m);
		if ((bound.height < 45 && bound.height>20) && (bound.width < 45 && bound.width>20))
		{
			selectcontours.push_back(m);
			
			
		}
	}
	vector<Point2f>circlecenters;
	vector<Point2f>line1center;
	vector<Point2f >line2center;
	vector<Point2f>line3center;
	vector<Point2f>line4center;
	vector<Point2f>line5center;

	vector<vector<Point>>line1contour;
	vector<vector<Point>>line2contour;
	vector<vector<Point>>line3contour;
	vector<vector<Point>>line4contour;
	vector<vector<Point>>line5contour;


	vector<float>circiers;


	for (int n=0;n<selectcontours.size();++n)
	{
		//画出所有选项的轮廓
		drawContours(out, selectcontours, n, Scalar(200, 0, 0), 2);
		//确定每个轮廓外接圆圆心
		Point2f center;
		float circler;
		minEnclosingCircle(selectcontours[n], center, circler);
		circiers.push_back(circler);
		if (center.y < 85)
		{
			line1center.push_back(center);
			line1contour.push_back(selectcontours[n]);
			drawContours(out, selectcontours, n, Scalar(200, 0, 0), 2);
			
		}
		else if (center.y < 145)
		{
			line2center.push_back(center);
			line2contour.push_back(selectcontours[n]);
			drawContours(out, selectcontours, n, Scalar(0, 200, 0), 2);
		}
		else if (center.y < 205)
		{
			line3center.push_back(center);
			line3contour.push_back(selectcontours[n]);
			drawContours(out, selectcontours, n, Scalar( 0, 0,200), 2);
		}
		else if (center.y < 265)
		{
			line4center.push_back(center);
			line4contour.push_back(selectcontours[n]);
			drawContours(out, selectcontours, n, Scalar(20, 0, 50), 2);
		}
		else if (center.y < 325)
		{
			line5center.push_back(center);
			line5contour.push_back(selectcontours[n]);
			drawContours(out, selectcontours, n, Scalar(200, 200, 0), 2);
		}

                        

 2.6根据轮廓外接圆圆心的x,y坐标对轮廓进行排序:

//对每一行排序
	sort(line1center.begin(), line1center.end(), [](const Point2f& center1, const Point2f& center2)
		{
			return (center1.x < center2.x);
		});
	sort(line2center.begin(), line2center.end(), [](const Point2f& center1, const Point2f& center2)
		{
			return (center1.x < center2.x);
		});
	sort(line3center.begin(), line3center.end(), [](const Point2f& center1, const Point2f& center2)
		{
			return (center1.x < center2.x);
		});
	sort(line4center.begin(), line4center.end(), [](const Point2f& center1, const Point2f& center2)
		{
			return (center1.x < center2.x);
		});
	sort(line5center.begin(), line5center.end(), [](const Point2f& center1, const Point2f& center2)
		{
			return (center1.x < center2.x);
		});
	
	vector<Point2f>sortedcenter;
	sortedcenter.insert(sortedcenter.end(), line1center.begin(), line1center.end());
	sortedcenter.insert(sortedcenter.end(), line2center.begin(), line2center.end());
	sortedcenter.insert(sortedcenter.end(), line3center.begin(), line3center.end());
	sortedcenter.insert(sortedcenter.end(), line4center.begin(), line4center.end());
	sortedcenter.insert(sortedcenter.end(), line5center.begin(), line5center.end());

	float meanrr=0;
	float tempr=0;
	for (int k=0;k<circiers.size();++k)
	{ 
		tempr += circiers[k];
		meanrr = tempr / circiers.size();
		
	}

2.7已知答题卡选项的标准答案是A,DE,BE,C,D;用绿色标记标准答案:


	//答题卡答案A,DE,BE,C,D
	int answer[5][5] = {
		1,0,0,0,0,
		0,0,0,1,1,
		0,1,0,0,1,
		0,0,1,0,0,
		0,0,0,1,0
	};
	//用绿色标记处标准答案
	Mat resultout, selectout,studentsel;
	perspect(roi).copyTo(resultout);
	perspect(roi).copyTo(selectout);
	perspect(roi).copyTo( studentsel);


	for (int ii = 0; ii < 5; ++ii)
	{
		for (int jj = 0; jj < 5; ++jj)
		{
			if (answer[ii][jj] == 1)
			{
				Point2f temp = sortedcenter[ii * 5 + jj];
				circle(resultout, temp, meanrr, Scalar(0, 200, 0), 2);
			}
		}
	}

 2.8最后标记考生答题的选项:

//用蓝色标记考生答案
	Mat erodM = getStructuringElement(0, Size(17,17));
	erode(outdial, selectout, erodM);
	
	vector<vector<Point>>contours;
	vector<Point2f>tempcenters;

	findContours(selectout, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
	for (int t = 0; t < contours.size(); ++t)
	{
		Point2f tempcenter;
		float tempr;
		minEnclosingCircle(contours[t], tempcenter, tempr);
		circle(studentsel, tempcenter, meanrr, Scalar(200, 0, 0),2);
	}
	//cout << contours.size() << endl;

	//cout << selectcontours.size();

	imshow("outdial", outdial);
	imshow("out", out);
	imshow("res", resultout);
	imshow("studentsel" , studentsel);

	
	//imshow("img", img);
		//imshow("gry", gry);
	waitKey(0); 
}

3.全部代码实现:

void VisionTest:: testdetect()
{
	//用相机拍一张答题卡
	/*
	VideoCapture cap(0);
	if (!cap.isOpened())
	{
		cout << "open failed !";

	}
	Mat frame, gray;
	cap.read(frame);
	cvtColor(frame, gray, COLOR_BGR2GRAY);
	imwrite("C:\\Users\\86176\\Downloads\\visionimage\\testdetect.jpg", gray);
	imshow("frame", frame);
	waitKey(0);*/

	Mat img = imread("C:\\Users\\86176\\Downloads\\visionimage\\testdetect.jpg");
	Mat binary, gry, equalhist;
	cvtColor(img, gry, COLOR_BGR2GRAY);
	equalizeHist(gry, equalhist);
	//imshow("eq", equalhist);
	GaussianBlur(equalhist, equalhist, Size(3, 3),10,20);
	Canny(equalhist, binary, 50, 300);
	imshow("can", binary);
	vector<vector<Point>> bigcontour;
	vector<double>areas;
	double max = 0;
	int index = -1;
	findContours(binary, bigcontour, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
	cout << bigcontour.size() << endl;
	for (int i = 0; i < bigcontour.size(); ++i)
	{
		double area = contourArea(bigcontour[i]);
		
		if (area > max)
		{
			max = area;
			index = i;
		}
		areas.push_back(area);
		//drawContours(img, bigcontour, i, Scalar( 0,0,200), 2);

	}
	cout << max << endl;
	cout << index << endl;
	Mat result;
	Rect rec=boundingRect(bigcontour[index]);
	vector<Point2f>app;
	approxPolyDP(bigcontour[index], app, 10, true);
	//cout << app << endl;
	int curwidth = sqrt(
		pow((app[0].x - app[3].x), 2) + pow((app[0].y - app[3].y), 2)
	);
	int curheight = sqrt(
		pow((app[0].x - app[1].x), 2) + pow((app[0].y - app[1].y), 2)
	);

	vector<Point2f>currs;
	currs.emplace_back(Point(55, 30));
	currs.emplace_back(Point(55, 30+curheight));
	currs.emplace_back(Point(55+curwidth, 30+curheight));
	currs.emplace_back(Point(55 + curwidth, 30));
	Mat  pers = getPerspectiveTransform(app, currs);
	Mat perspect;
	warpPerspective(img, perspect, pers, img.size());
	

	Mat out;
	Rect roi = Rect(55, 30, curwidth, curheight);
	perspect(roi).copyTo(out);
	imshow("roi", perspect(roi));
	

	Mat otsu, outgray, outdial;
	cvtColor(out, outgray,COLOR_BGR2GRAY);
	threshold(outgray, otsu, 100, 220, THRESH_OTSU|THRESH_BINARY_INV);
	imshow("otsu", otsu);

	//对图像进行膨胀
	Mat mod = getStructuringElement(MorphShapes::MORPH_RECT, Size(5, 5));
	morphologyEx(otsu, outdial, MORPH_DILATE, mod);
	
	//再进行轮廓检测
	vector<vector<Point>>contoursagin, selectcontours;
	findContours(outdial, contoursagin, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
	for (auto m : contoursagin)
	{
		Rect bound = boundingRect(m);
		if ((bound.height < 45 && bound.height>20) && (bound.width < 45 && bound.width>20))
		{
			selectcontours.push_back(m);
			
			
		}
	}
	vector<Point2f>circlecenters;
	vector<Point2f>line1center;
	vector<Point2f >line2center;
	vector<Point2f>line3center;
	vector<Point2f>line4center;
	vector<Point2f>line5center;

	vector<vector<Point>>line1contour;
	vector<vector<Point>>line2contour;
	vector<vector<Point>>line3contour;
	vector<vector<Point>>line4contour;
	vector<vector<Point>>line5contour;

	vector<float>circiers;

	for (int n=0;n<selectcontours.size();++n)
	{
		//画出所有选项的轮廓
		drawContours(out, selectcontours, n, Scalar(200, 0, 0), 2);
		//确定每个轮廓外接圆圆心
		Point2f center;
		float circler;
		minEnclosingCircle(selectcontours[n], center, circler);
		circiers.push_back(circler);
		if (center.y < 85)
		{
			line1center.push_back(center);
			line1contour.push_back(selectcontours[n]);
			drawContours(out, selectcontours, n, Scalar(200, 0, 0), 2);
			
		}
		else if (center.y < 145)
		{
			line2center.push_back(center);
			line2contour.push_back(selectcontours[n]);
			drawContours(out, selectcontours, n, Scalar(0, 200, 0), 2);
		}
		else if (center.y < 205)
		{
			line3center.push_back(center);
			line3contour.push_back(selectcontours[n]);
			drawContours(out, selectcontours, n, Scalar( 0, 0,200), 2);
		}
		else if (center.y < 265)
		{
			line4center.push_back(center);
			line4contour.push_back(selectcontours[n]);
			drawContours(out, selectcontours, n, Scalar(20, 0, 50), 2);
		}
		else if (center.y < 325)
		{
			line5center.push_back(center);
			line5contour.push_back(selectcontours[n]);
			drawContours(out, selectcontours, n, Scalar(200, 200, 0), 2);
		}
	}
	//对每一行排序
	sort(line1center.begin(), line1center.end(), [](const Point2f& center1, const Point2f& center2)
		{
			return (center1.x < center2.x);
		});
	sort(line2center.begin(), line2center.end(), [](const Point2f& center1, const Point2f& center2)
		{
			return (center1.x < center2.x);
		});
	sort(line3center.begin(), line3center.end(), [](const Point2f& center1, const Point2f& center2)
		{
			return (center1.x < center2.x);
		});
	sort(line4center.begin(), line4center.end(), [](const Point2f& center1, const Point2f& center2)
		{
			return (center1.x < center2.x);
		});
	sort(line5center.begin(), line5center.end(), [](const Point2f& center1, const Point2f& center2)
		{
			return (center1.x < center2.x);
		});
	
	vector<Point2f>sortedcenter;
	sortedcenter.insert(sortedcenter.end(), line1center.begin(), line1center.end());
	sortedcenter.insert(sortedcenter.end(), line2center.begin(), line2center.end());
	sortedcenter.insert(sortedcenter.end(), line3center.begin(), line3center.end());
	sortedcenter.insert(sortedcenter.end(), line4center.begin(), line4center.end());
	sortedcenter.insert(sortedcenter.end(), line5center.begin(), line5center.end());
	float meanrr=0;
	float tempr=0;
	for (int k=0;k<circiers.size();++k)
	{ 
		tempr += circiers[k];
		meanrr = tempr / circiers.size();
		
	}

	//答题卡答案A,DE,BE,C,D
	int answer[5][5] = {
		1,0,0,0,0,
		0,0,0,1,1,
		0,1,0,0,1,
		0,0,1,0,0,
		0,0,0,1,0
	};
	//用绿色标记处标准答案
	Mat resultout, selectout,studentsel;
	perspect(roi).copyTo(resultout);
	perspect(roi).copyTo(selectout);
	perspect(roi).copyTo( studentsel);
	for (int ii = 0; ii < 5; ++ii)
	{
		for (int jj = 0; jj < 5; ++jj)
		{
			if (answer[ii][jj] == 1)
			{
				Point2f temp = sortedcenter[ii * 5 + jj];
				circle(resultout, temp, meanrr, Scalar(0, 200, 0), 2);
			}
		}
	}
	//用蓝色标记考生答案
	Mat erodM = getStructuringElement(0, Size(17,17));
	erode(outdial, selectout, erodM);
	vector<vector<Point>>contours;
	vector<Point2f>tempcenters;
	findContours(selectout, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
	for (int t = 0; t < contours.size(); ++t)
	{
		Point2f tempcenter;
		float tempr;
		minEnclosingCircle(contours[t], tempcenter, tempr);
		circle(studentsel, tempcenter, meanrr, Scalar(200, 0, 0),2);
	}
	cout << contours.size() << endl;
	imshow("outdial", outdial);
	imshow("out", out);
	imshow("res", resultout);
	imshow("studentsel" , studentsel);
	waitKey(0); 
}

  • 作者:Callmegodyu、
  • 原文链接:https://blog.csdn.net/m0_57747965/article/details/123445645
    更新时间:2022-08-20 12:56:33