OpenCV C++案例实战五《答题卡识别》

2022-08-21 09:56:19

前言

本文将使用OpenCV C++ 进行答题卡识别。

一、图像矫正

请添加图片描述
原图如图所示。我们拿到图像首先要进行图像预处理。本文目的是进行答题卡选项识别。所以,第一步我们需要将答题卡区域切割出来以进行后续识别工作。在上一篇文章我已经做过图像矫正案例OpenCV C++案例实战四《图像透视矫正》,详细内容大家可以参考,这里就不再赘述。

1.源码

voidAnswer::GetWarpImg(Mat src, Mat& WarpImg){
	Mat gray;cvtColor(src, gray, COLOR_BGR2GRAY);

	Mat blur;GaussianBlur(gray, blur,Size(3,3),0);

	Mat canny;Canny(blur, canny,100,200);

	vector<vector<Point>>contours;findContours(canny, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);//找到最大矩形轮廓int index=0;double maxArea=0;for(int i=0; i< contours.size(); i++){double area=contourArea(contours[i]);if(maxArea< area){
			maxArea= area;
			index= i;}}//多边形近似
	vector<vector<Point>>conPloy(contours.size());double peri=arcLength(contours[index],true);approxPolyDP(contours[index], conPloy[index],0.02*peri,true);//找到矩形四个顶点
	vector<Point>srcPts;for(int i=0; i< conPloy[index].size(); i++){
		srcPts.push_back(Point(conPloy[index][i].x, conPloy[index][i].y));}int width= src.cols/2;int height= src.rows/2;//将矩形四个顶点按T_L, B_L, B_R, T_R区分int T_L, B_L, B_R, T_R;for(int i=0; i< srcPts.size(); i++){if(srcPts[i].x< width&& srcPts[i].y< height){
			T_L= i;}if(srcPts[i].x< width&& srcPts[i].y> height){
			B_L= i;}if(srcPts[i].x> width&& srcPts[i].y> height){
			B_R= i;}if(srcPts[i].x> width&& srcPts[i].y< height){
			T_R= i;}}/*
	变换后,图像的长和宽应该变为:
	长 = max(变换前左边长,变换前右边长)
	宽 = max(变换前上边长,变换前下边长)
	设变换后图像的左上角位置为原点位置。
	*/double upWidth=EuDis(srcPts[T_R], srcPts[T_L]);double downWidth=EuDis(srcPts[B_R], srcPts[B_L]);double maxWidth=max(upWidth, downWidth);double leftHeight=EuDis(srcPts[B_L], srcPts[T_L]);double rightHeight=EuDis(srcPts[B_R], srcPts[T_R]);double maxHeight=max(leftHeight, rightHeight);

	Point2f AffineSrcPts[4]={Point2f(srcPts[T_L]),Point2f(srcPts[T_R]),Point2f(srcPts[B_L]),Point2f(srcPts[B_R])};

	Point2f AffineDstPts[4]={Point2f(0,0),Point2f(maxWidth,0),Point2f(0, maxHeight),Point2f(maxWidth, maxHeight)};

	Mat M;//计算仿射变换矩阵
	M=getPerspectiveTransform(AffineSrcPts, AffineDstPts);//对加载图形进行仿射变换操作warpPerspective(src, WarpImg, M,Point(maxWidth, maxHeight));}

请添加图片描述
如图就是经图像透视矫正切割出来的答题卡区域。接下来,我们就需要对该图进行后续识别处理。

二、获取选项区域

1.扣出每题选项

我对于该案例的处理思路是:先对原图进行透视矫正;然后将每一题号选项都切割出来;最后对这些切割出来的选项一一识别。

	Mat gray;cvtColor(WarpImg, gray, COLOR_BGR2GRAY);

	Mat bin;threshold(gray, bin,0,255, THRESH_BINARY_INV| THRESH_OTSU);//使用 Size(15, 5)闭操作目的是为了将每一题选项连接起来。
	Mat close;
	Mat kernel=getStructuringElement(MORPH_RECT,Size(15,5));morphologyEx(bin, close, MORPH_CLOSE, kernel);

请添加图片描述
如图所示,经过上述图像预处理,我们就可以利用轮廓筛选出每一题号选项。

	vector<vector<Point>>contours;findContours(close, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);

	RotatedRect rect;
	Rect box;for(int i=0; i< contours.size(); i++){
		Mat mask=Mat::zeros(WarpImg.size(), WarpImg.type());
		mask=Scalar::all(255);double area=contourArea(contours[i]);if(area>1000){
			rect=minAreaRect(contours[i]);

			box= rect.boundingRect();double ratio= rect.size.height/ rect.size.width;//将每一选项都单独抠出来放进AnswerROI容器,以便后续识别。if(ratio>0.1){//rectangle(WarpImg, Rect(box.tl(), box.br()), Scalar(0, 255, 0), 2);

				Mat ROI=WarpImg(Rect(box.tl(), box.br()));

				ROI.copyTo(mask(box));

				AnswerROI.push_back(mask);}}}

请添加图片描述
如图为扣出的题号选项,这里只展示其中之一。接下来我们对每一题的选项进行识别就可以了。

2.源码

voidAnswer::GetAnswerArea(Mat&WarpImg, vector<Mat>&AnswerROI){
	
	Mat gray;cvtColor(WarpImg, gray, COLOR_BGR2GRAY);

	Mat bin;threshold(gray, bin,0,255, THRESH_BINARY_INV| THRESH_OTSU);//使用 Size(15, 5)闭操作目的是为了将每一题选项连接起来。
	Mat close;
	Mat kernel=getStructuringElement(MORPH_RECT,Size(15,5));morphologyEx(bin, close, MORPH_CLOSE, kernel);

	vector<vector<Point>>contours;findContours(close, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);

	RotatedRect rect;
	Rect box;for(int i=0; i< contours.size(); i++){
		Mat mask=Mat::zeros(WarpImg.size(), WarpImg.type());
		mask=Scalar::all(255);double area=contourArea(contours[i]);if(area>1000){
			rect=minAreaRect(contours[i]);

			box= rect.boundingRect();double ratio= rect.size.height/ rect.size.width;//将每一选项都单独抠出来放进AnswerROI容器,以便后续识别。if(ratio>0.1){//rectangle(WarpImg, Rect(box.tl(), box.br()), Scalar(0, 255, 0), 2);

				Mat ROI=WarpImg(Rect(box.tl(), box.br()));

				ROI.copyTo(mask(box));

				AnswerROI.push_back(mask);}}}reverse(AnswerROI.begin(), AnswerROI.end());}

三、获取答案

1.思路

请添加图片描述
由于之前我们已经将图像透视矫正,并且将每一题号选项都一一抠出来作为一张新图像存储。所以,这里我们可以将每个选项按A,B,C,D,E划分区域,然后计算每个选项区域像素点个数,选项涂抹区域必定是像素点最多的,由此我们可以判定出每一题号的选项。

2.辅助函数

//计算ABCDE区域所有像素点个数intget_pixsum(Mat image,int pixstart,int pixend){int sum=0;for(int i= pixstart; i< pixend; i++){for(int j=0; j< image.rows; j++){if(image.at<uchar>(j, i)!=0){
				sum++;}}}return sum;}//找到像素点最多区域的索引intgetMaxIndex(vector<int>Answer){int max=0;int index=-1;for(int i=0; i< Answer.size(); i++){if(Answer[i]> max){
			max= Answer[i];
			index= i;}}return index;}

3.源码

voidAnswer::GetAnswer(Mat&WarpImg, vector<Mat>&AnswerROI, vector<int>Answers,int&Score){
	vector<int>Results;//正确结果选项索引
	vector<vector<Point>>ResultContours;//正确结果选项轮廓for(int i=0; i< AnswerROI.size(); i++){
		Mat gray;cvtColor(AnswerROI[i], gray, COLOR_BGR2GRAY);

		Mat bin;threshold(gray, bin,0,255, THRESH_BINARY_INV| THRESH_OTSU);//计算ABCDE区域所有像素点个数int A=get_pixsum(bin,60,110);int B=get_pixsum(bin,110,160);int C=get_pixsum(bin,160,210);int D=get_pixsum(bin,210,260);int E=get_pixsum(bin,260,310);

		vector<int>Answer={ A,B,C,D,E};//找到像素点最多区域的索引int Index=getMaxIndex(Answer);
		
		Results.push_back(Index);//正确结果选项索引//获取选项轮廓
		vector<vector<Point>>contours;
		vector<vector<Point>>AnswerContours;findContours(bin, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);//由于提取到的轮廓是倒序的,故reverse反转一下reverse(contours.begin(), contours.end());for(int j=0; j< contours.size(); j++){//通过面积条件,只提取选项(ABCDE)轮廓if(contourArea(contours[j])>200){
				AnswerContours.push_back(contours[j]);}}

		ResultContours.push_back(AnswerContours[Index]);//正确结果选项轮廓}//结果绘制for(int i=0; i< Answers.size(); i++){if(Answers[i]== Results[i]){//答题正确绘制绿色drawContours(WarpImg, ResultContours, i,Scalar(0,255,0),2);}else{//答题错误绘制红色drawContours(WarpImg, ResultContours, i,Scalar(0,0,255),2);}}//统计得分double Count=0;//答对数量for(int i=0; i< Answers.size(); i++){if(Results[i]== Answers[i]){
			Count++;}}

	Score=(Count/ Answers.size())*100;}

4.效果

请添加图片描述
这里我设置的正确答案为:B,E,A,C,D。故只答对4题,得80分。


总结

本文使用OpenCV C++ 进行答题卡识别,关键步骤有以下几点。
1、图像透视矫正,将答题卡区域正确切割出来。
2、将每一题号分别抠出来存为新图像,待后续识别。
3、对每一题号确定其A,B,C,D,E选项区域,统计其像素点个数,故而匹配选项。

以上就是我对整个案例是思路以及处理手法,如果大家有更好地想法欢迎讨论。

  • 作者:Zero___Chen
  • 原文链接:https://blog.csdn.net/Zero___Chen/article/details/121300567
    更新时间:2022-08-21 09:56:19