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


前言

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

一、图像矫正

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

1.源码

void Answer::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.源码

void  Answer::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区域所有像素点个数
int get_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;
}


//找到像素点最多区域的索引
int getMaxIndex(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.源码

void Answer::GetAnswer(cv::Mat&WarpImg, vector<Mat>&AnswerROI, vector<int>Answers, int &Score)
{
    
    
	const double Total = 5;  //题目数量
	double Count = 0;       //答对数量
	vector<int>Results;

	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);

		vector<int>Answer;

		//计算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);

		Answer = {
    
     A,B,C,D,E };

		//找到像素点最多区域的索引
		int Index = getMaxIndex(Answer);
		
		Results.push_back(Index);

		//将选项在原图圈出来
		vector<vector<Point>>contours;
		findContours(bin, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);

		reverse(contours.begin(), contours.end());

		for (int j = 0; j < contours.size(); j++)
		{
    
    
			double area = contourArea(contours[j]);

			if (area > 200)
			{
    
    

				if (Answers[i] == Index)
				{
    
    
					drawContours(WarpImg, contours, Index, Scalar(0, 255, 0), 2);
				}
				else
				{
    
    
					drawContours(WarpImg, contours, Answers[i], Scalar(0, 0, 255), 2);
				}

			}

		}

	}

	//统计得分
	for (int i = 0; i < Total; i++)
	{
    
    
		if (Results[i] == Answers[i])
		{
    
    
			Count++;
		}
	}

	Score = (Count / Total) * 100;

}

4.效果

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


总结

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

以上就是我对整个案例是思路以及处理手法,如果大家有更好地想法欢迎讨论。最后附上源码与素材图像。

在这里插入图片描述

百度网盘下载
链接:https://pan.baidu.com/s/1kt6IR3op1x90LMrLjWGpQA
提取码:reug

猜你喜欢

转载自blog.csdn.net/Zero___Chen/article/details/121300567