————致力于用代码改变世界

OpenCV实战案例——校正+切边[C++]

0.前言
本文以实战案例为背景,讲述如何使用计算机图形学知识完成需求,实现最终效果。本文包含实战案例素材以及过程代码讲解,方便读者理解。
1.案例需求
某公司打算开发一款用于提取学生作业本的程序,学生用手机拍摄自己的作业上传到程序,程序进行处理最终提取出作业本区域方便老师批改。
下图(图1-1)为某学生提交的作业本俯拍图片。

该公司希望该程序将图片裁剪校正使其达到方便教师批改的大小。最终效果图如下(图1-2)所示。

2.处理思想

  • 由于环境因素,学生上传的图片可能存在较多的噪点,不利于计算机处理,故可以采取高斯模糊进行降噪处理,方便后续提取特征。
  • 为了更方便提取图像特征,应将图像灰度化、二值化,使其尽可能显示出图片边缘纹理特征,对于处理期间的噪声点可以采用形态学操作消除。
  • 通过需求结果的特点,可以使用轮廓查找findContours()函数查找二值图中的轮廓,根据大小、型状等外显特征进行过滤。
  • 考虑到学生上传的图片可能存在倾斜情况,而普通的矩形查找(boundingRect函数)难以胜任该工作,所以应选择能够衡量角度特征的矩形查找函数,故本文选取minAreaRect()函数查找最小外接矩形。
  • 获取矩形大小、角度等特征后,可以通过仿射变换校正图片,ROI(感兴趣区域)提取获取最终结果。

3.代码实现

点击查看代码
//读取图像
Mat mSrc = imread(path1, ImreadModes::IMREAD_COLOR);
imshow("源图像", mSrc);
上述代码读取了图片加载到内存中并显示,显示结果如下图(图3-1)所示。

点击查看代码
//高斯模糊
Mat mGaussian;
GaussianBlur(mSrc, mGaussian, Size(3, 3), 1);

//转灰度
Mat mGray;
cvtColor(mGaussian, mGray, ColorConversionCodes::COLOR_BGR2GRAY);

//二值化
Mat mBin;
threshold(mGray, mBin, 244, 255, ThresholdTypes::THRESH_TOZERO);

//膨胀
Mat mDilateKernal = getStructuringElement(MorphShapes::MORPH_RECT, Size(3, 3));
dilate(mBin, mBin, mDilateKernal, Point(-1, -1));
imshow("二值图", mBin);
上述代码将源图像高斯模糊去噪,然后转为灰度图像,再通过阈值将灰度图像转变成二值图像,最后通过膨胀操作处理细节,使图像中特征不明显区域特征加强,方便提取轮廓。getStructuringElement函数用于获取核矩阵,常见的核矩阵有十字型、矩形、圆形等。最终显示结果如下图(图3-2)所示。

通过图3-2可以明显看到学生的作业本轮廓,但由于背景干扰,仍出现大量的噪点无法去除。

点击查看代码
//查找轮廓
vector<vector<Point>> vvContours;
vector<Vec4i> vHierarchy;
findContours(mBin, vvContours, vHierarchy, RetrievalModes::RETR_EXTERNAL, ContourApproximationModes::CHAIN_APPROX_SIMPLE);

//绘制轮廓
RNG rng(0);
Mat mContoursImg=mSrc.clone();
for (int a = 0; a < vvContours.size(); ++a)
{
	drawContours(mContoursImg, vvContours, a, Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)), 1, LineTypes::LINE_AA);
}
imshow("轮廓", mContoursImg);

上述代码使用findContours()函数查找出了所有的轮廓并绘制,每个轮廓的颜色随机生成,其中变量vHierarchy并没有使用到,显示结果如下图(图3-3)所示。

点击查看代码
//查找最小外接矩形
vector<RotatedRect> vRotatedRects;
for (int a = 0; a < vvContours.size(); ++a)
{
	vRotatedRects.emplace_back(minAreaRect(vvContours[a]));
}
对于每一个轮廓都生成一个最小外接矩形并保存在vRotatedRects向量中。
点击查看代码
//过滤不合格的
vector<RotatedRect> vGoodRotatedRects;
for (int a = 0; a < vRotatedRects.size(); ++a)
{
	Size2f sz_rect = vRotatedRects.at(a).size;
	if (sz_rect.width >= 100 && sz_rect.height>=100)
	{
		vGoodRotatedRects.push_back(vRotatedRects.at(a));
	}
}
通过minAreaRect()函数得到了大量的最小外接矩形,但是仅有少数是符合实际情况,所以需要对找到的轮廓进行过滤。上述代码根据轮廓的大小特征进行过滤,过滤掉宽度小于100且高度小于100的矩形,当然,仅设置阈值为100是比较随意的,读者也可以添加其他判断条件过滤,例如宽高比例等。
点击查看代码
//绘制矩形
Mat mRectsImg = mSrc.clone();
Point2f* p1 = new Point2f[4];
vGoodRotatedRects.front().points(p1);
Scalar color(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
for (int a = 0; a < 4; ++a)
{
	line(mRectsImg, p1[a], p1[(a + 1) % 4], color,3);
}
delete p1;
p1 = nullptr;
imshow("最小外接矩形", mRectsImg);
上述代码绘制过滤后的最小外接矩形,其中(a + 1) % 4可以实现线条的首尾相连效果,最终显示效果如下图(图3-4)所示。

通过图3-4可以明显的看到学生上传的作业本轮廓被提出出来了,与最终预期结果又进一步。

点击查看代码
//绘制十字坐标系、方向
Mat mDirectionImg= mRectsImg.clone();
float fAngle = vGoodRotatedRects.front().angle;
Point2f p2fCenter = vGoodRotatedRects.front().center;
cout << "angle:" << fAngle << ",center:" << p2fCenter << endl;
//x轴
line(mDirectionImg, Point(p2fCenter.x - 200, p2fCenter.y), Point(p2fCenter.x + 200, p2fCenter.y), Scalar(0, 0, 255), 2);
//y轴
line(mDirectionImg, Point(p2fCenter.x, p2fCenter.y-200), Point(p2fCenter.x, p2fCenter.y+200), Scalar(0, 0, 255), 2);
imshow("坐标", mDirectionImg);
上述代码以作业本中心点为(0,0)点绘制直角坐标系(红线),绿色标注是笔者后期添加上的,用于后续分析理解,θ值就是控制台输出的angle度数。最终结果如下图(图3-5)所示。 控制台输出结果如下所示:

angle:41.3478,center:[365.829, 209.149]

具体angle的实际含义可以参考笔者的另一篇文章:https://www.cnblogs.com/hello-nullptr/p/18240905

对于图3-5,若将图像中的作业本校正(使黑色签字笔笔尖垂直向下),则需要将整幅图像逆时针旋转angle度即可,本文通过以下代码实现校正。

点击查看代码
//校正
Mat mRotationKernal= getRotationMatrix2D(p2fCenter, fAngle, 1.0);
Mat mCorrectionImg ;
warpAffine(mSrc, mCorrectionImg, mRotationKernal, mSrc.size());
imshow("校正", mCorrectionImg);
通过执行上述代码,输出结果如下图(图3-6)所示。

学生提交的作业图片成功被校正了,接下来仅需提取感兴趣区域(ROI)即可。

点击查看代码
//提取ROI区域
Size sz_rect=vGoodRotatedRects.front().size;
Rect rRoi(p2fCenter.x - (sz_rect.width / 2), p2fCenter.y - (sz_rect.height / 2), sz_rect.width, sz_rect.height);
Mat mRoiImg(mCorrectionImg, rRoi);
imshow("ROI", mRoiImg);
上述代码提取了作业本区域,输出结果如下图(图3-7)所示。

至此结束。

4.完整代码

点击查看代码
	//读取图像
	Mat mSrc = imread(path1, ImreadModes::IMREAD_COLOR);
	imshow("源图像", mSrc);

	//高斯模糊
	Mat mGaussian;
	GaussianBlur(mSrc, mGaussian, Size(3, 3), 1);

	//转灰度
	Mat mGray;
	cvtColor(mGaussian, mGray, ColorConversionCodes::COLOR_BGR2GRAY);

	//二值化
	Mat mBin;
	threshold(mGray, mBin, 244, 255, ThresholdTypes::THRESH_TOZERO);

	//膨胀
	Mat mDilateKernal = getStructuringElement(MorphShapes::MORPH_RECT, Size(3, 3));
	dilate(mBin, mBin, mDilateKernal, Point(-1, -1));
	imshow("二值图", mBin);

	//查找轮廓
	vector<vector<Point>> vvContours;
	vector<Vec4i> vHierarchy;
	findContours(mBin, vvContours, vHierarchy, RetrievalModes::RETR_EXTERNAL, ContourApproximationModes::CHAIN_APPROX_SIMPLE);

	//绘制轮廓
	RNG rng(0);
	Mat mContoursImg=mSrc.clone();
	for (int a = 0; a < vvContours.size(); ++a)
	{
		drawContours(mContoursImg, vvContours, a, Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)), 1, LineTypes::LINE_AA);
	}
	imshow("轮廓", mContoursImg);

	//查找最小外接矩形
	vector<RotatedRect> vRotatedRects;
	for (int a = 0; a < vvContours.size(); ++a)
	{
		vRotatedRects.emplace_back(minAreaRect(vvContours[a]));
	}

	//过滤不合格的
	vector<RotatedRect> vGoodRotatedRects;
	for (int a = 0; a < vRotatedRects.size(); ++a)
	{
		Size2f sz_rect = vRotatedRects.at(a).size;
		if (sz_rect.width >= 100 && sz_rect.height>=100)
		{
			vGoodRotatedRects.push_back(vRotatedRects.at(a));
		}
	}

	//绘制矩形
	Mat mRectsImg = mSrc.clone();
	Point2f* p1 = new Point2f[4];
	vGoodRotatedRects.front().points(p1);
	Scalar color(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
	for (int a = 0; a < 4; ++a)
	{
		line(mRectsImg, p1[a], p1[(a + 1) % 4], color,3);
	}
	delete p1;
	p1 = nullptr;
	imshow("最小外接矩形", mRectsImg);

	//绘制十字坐标系、方向
	Mat mDirectionImg= mRectsImg.clone();
	float fAngle = vGoodRotatedRects.front().angle;
	Point2f p2fCenter = vGoodRotatedRects.front().center;
	cout << "angle:" << fAngle << ",center:" << p2fCenter << endl;
	//x轴
	line(mDirectionImg, Point(p2fCenter.x - 200, p2fCenter.y), Point(p2fCenter.x + 200, p2fCenter.y), Scalar(0, 0, 255), 2);
	//y轴
	line(mDirectionImg, Point(p2fCenter.x, p2fCenter.y-200), Point(p2fCenter.x, p2fCenter.y+200), Scalar(0, 0, 255), 2);
	imshow("坐标", mDirectionImg);

	//校正
	Mat mRotationKernal= getRotationMatrix2D(p2fCenter, fAngle, 1.0);
	Mat mCorrectionImg ;
	warpAffine(mSrc, mCorrectionImg, mRotationKernal, mSrc.size());
	imshow("校正", mCorrectionImg);

	//提取ROI区域
	Size sz_rect=vGoodRotatedRects.front().size;
	Rect rRoi(p2fCenter.x - (sz_rect.width / 2), p2fCenter.y - (sz_rect.height / 2), sz_rect.width, sz_rect.height);
	Mat mRoiImg(mCorrectionImg, rRoi);
	imshow("ROI", mRoiImg);
posted @ 2024-06-11 10:21  hello_nullptr  阅读(167)  评论(0编辑  收藏  举报