首页 > 其他分享 >OpenCV 图像矫正技术深入探讨

OpenCV 图像矫正技术深入探讨

时间:2024-08-28 11:24:32浏览次数:12  
标签:矫正 Mat RoiSrcImg 深入探讨 OpenCV 图像 轮廓 CV

刚进入实验室导师就交给我一个任务,就是让我设计算法给图像进行矫正。哎呀,我不太会图像这块啊,不过还是接下来了,硬着头皮开干吧!

那什么是图像的矫正呢?举个例子就好明白了。

我的好朋友小明给我拍了这几张照片,因为他的拍照技术不咋地,照片都拍得歪歪扭扭的,比如下面这些照片:

人民币

在这里插入图片描述

发票
在这里插入图片描述

文本
在这里插入图片描述

这些图片让人看得真不舒服!看个图片还要歪脖子看,实在是太烦人了!我叫小明帮我扫描一下一本教科书,小明把每一页书都拍成上面的文本那样了。好气啊那该怎么办呢?一页一页用PS来处理?1000页的矫正啊,当然交给计算机去做!

真的,对于图像矫正的问题,在图像处理领域还真得多,比如人民币的矫正、文本的矫正、车牌的矫正、身份证矫正等等。这些都是因为拍摄者总不可能100%正确地拍摄好图片,这就要求我们通过后期的图像处理技术将图片还原好,才能进一步做后面的处理,比如数字分割啊数字识别啊,不然歪歪扭扭的文字数字,想识别出来估计就很难了。

上面几个图,我们在日常生活中遇到的可不少,因为拍摄时拍的不好,导致拍出来的图片歪歪扭扭的,很不自然,那么我们能不能把这些图片尽可能地矫正过来呢?

OpenCV告诉我们,没问题!工具我给你,算法你自己设计!

比如图一,我要想将人民币矫正,并且把人民币整个抠出来保存,该怎么做?那就涉及到了图像的矫正和感兴趣区域提取两大技术了。

总的来说,要进行进行图像矫正,至少有以下几项知识储备:

轮廓提取技术
霍夫变换知识
ROI感兴趣区域知识
下面以人民币矫正、发票矫正、文本矫正为例,一步步剖析如何实现图像矫正。

首先分析如何矫正人民币。

比如我们要矫正这张人民币,思路应该是怎么样?
在这里插入图片描述

首先分析这张图的特点。

在这张图里,人民币有一定的倾斜角度,但是角度不大;人民币的背景是黑色的,而且人民币的边缘应该比较明显。

没错,我们就抓住人民币的的边缘比较明显来做文章!我们是不是可以先把人民币的轮廓找出来(找出来的轮廓当然就是一个大大的矩形),然后用矩形去包围它,得到他的旋转角度,然后根据得到的角度进行旋转,那样不就可以实现矫正了吗!

再详细地总结处理步骤:

图片灰度化
阈值二值化
检测轮廓
寻找轮廓的包围矩阵,并且获取角度
根据角度进行旋转矫正
对旋转后的图像进行轮廓提取
对轮廓内的图像区域抠出来,成为一张独立图像
我把该矫正算法命名为基于轮廓提取的矫正算法,因为其关键技术就是通过轮廓来获取旋转角度。

#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
using namespace cv;
using namespace std;

//第一个参数:输入图片名称;第二个参数:输出图片名称
void GetContoursPic(const char* pSrcFileName, const char* pDstFileName)
{
	Mat srcImg = imread(pSrcFileName);
	imshow("原始图", srcImg);
	Mat gray, binImg;
	//灰度化
	cvtColor(srcImg, gray, COLOR_RGB2GRAY);
	imshow("灰度图", gray);
	//二值化
	threshold(gray, binImg, 100, 200, CV_THRESH_BINARY);
	imshow("二值化", binImg);

vector<vector<Point> > contours;
vector<Rect> boundRect(contours.size());
//注意第5个参数为CV_RETR_EXTERNAL,只检索外框  
findContours(binImg, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); //找轮廓
cout << contours.size() << endl;
for (int i = 0; i < contours.size(); i++)
{
	//需要获取的坐标  
	CvPoint2D32f rectpoint[4];
	CvBox2D rect =minAreaRect(Mat(contours[i]));

	cvBoxPoints(rect, rectpoint); //获取4个顶点坐标  
	//与水平线的角度  
	float angle = rect.angle;
	cout << angle << endl;

	int line1 = sqrt((rectpoint[1].y - rectpoint[0].y)*(rectpoint[1].y - rectpoint[0].y) + (rectpoint[1].x - rectpoint[0].x)*(rectpoint[1].x - rectpoint[0].x));
	int line2 = sqrt((rectpoint[3].y - rectpoint[0].y)*(rectpoint[3].y - rectpoint[0].y) + (rectpoint[3].x - rectpoint[0].x)*(rectpoint[3].x - rectpoint[0].x));
	//rectangle(binImg, rectpoint[0], rectpoint[3], Scalar(255), 2);
	//面积太小的直接pass
	if (line1 * line2 < 600)
	{
		continue;
	}

	//为了让正方形横着放,所以旋转角度是不一样的。竖放的,给他加90度,翻过来  
	if (line1 > line2) 
	{
		angle = 90 + angle;
	}

	//新建一个感兴趣的区域图,大小跟原图一样大  
	Mat RoiSrcImg(srcImg.rows, srcImg.cols, CV_8UC3); //注意这里必须选CV_8UC3
	RoiSrcImg.setTo(0); //颜色都设置为黑色  
	//imshow("新建的ROI", RoiSrcImg);
	//对得到的轮廓填充一下  
	drawContours(binImg, contours, -1, Scalar(255),CV_FILLED);

	//抠图到RoiSrcImg
	srcImg.copyTo(RoiSrcImg, binImg);


	//再显示一下看看,除了感兴趣的区域,其他部分都是黑色的了  
	namedWindow("RoiSrcImg", 1);
	imshow("RoiSrcImg", RoiSrcImg);

	//创建一个旋转后的图像  
	Mat RatationedImg(RoiSrcImg.rows, RoiSrcImg.cols, CV_8UC1);
	RatationedImg.setTo(0);
	//对RoiSrcImg进行旋转  
	Point2f center = rect.center;  //中心点  
	Mat M2 = getRotationMatrix2D(center, angle, 1);//计算旋转加缩放的变换矩阵 
	warpAffine(RoiSrcImg, RatationedImg, M2, RoiSrcImg.size(),1, 0, Scalar(0));//仿射变换 
	imshow("旋转之后", RatationedImg);
	imwrite("r.jpg", RatationedImg); //将矫正后的图片保存下来
}

#if 1
//对ROI区域进行抠图

//对旋转后的图片进行轮廓提取  
vector<vector<Point> > contours2;
Mat raw = imread("r.jpg");
Mat SecondFindImg;
//SecondFindImg.setTo(0);
cvtColor(raw, SecondFindImg, COLOR_BGR2GRAY);  //灰度化  
threshold(SecondFindImg, SecondFindImg, 80, 200, CV_THRESH_BINARY);
findContours(SecondFindImg, contours2, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
//cout << "sec contour:" << contours2.size() << endl;

for (int j = 0; j < contours2.size(); j++)
{
	//这时候其实就是一个长方形了,所以获取rect  
	Rect rect = boundingRect(Mat(contours2[j]));
	//面积太小的轮廓直接pass,通过设置过滤面积大小,可以保证只拿到外框
	if (rect.area() < 600)
	{
		continue;
	}
	Mat dstImg = raw(rect);
	imshow("dst", dstImg);
	imwrite(pDstFileName, dstImg);
}
#endif


}


void main()
{
	GetContoursPic("6.jpg", "FinalImage.jpg");
	waitKey();
}

效果依次如下:
原始图
在这里插入图片描述

二值化图
在这里插入图片描述

掩膜mask是这样的

在这里插入图片描述

旋转矫正之后
在这里插入图片描述

将人民币区域抠出来
在这里插入图片描述

该算法的效果还是很不错的!那赶紧试试其他图片,我把倾斜的发票图像拿去试试。

原始图
在这里插入图片描述

倾斜矫正之后

在这里插入图片描述

最后把目标区域抠出来,成为单独的照片。
在这里插入图片描述

上面的算法可以很好的处理人民币和发票两种情况的倾斜矫正,那文本矫正可以吗?我赶紧试了一下,结果是失败的。

原图
在这里插入图片描述

算法矫正后,还是原样,矫正失败。

在这里插入图片描述

认真分析一下,还是很容易看出文本矫正失败的原因的。

原因就在于,人民币图像和发票图像他们有明显的的边界轮廓,而文本图像没有。文本图像的背景是白色的,所以我们没有办法像人民币发票那类有明显边界的矩形物体那样,提取出轮廓并旋转矫正。

经过深入分析可以看出,虽然文本类图像没有明显的边缘轮廓,但是他们有一个很重要的特征,那就是每一行文字都是呈现一条直线形状,而且这些直线都是平行的!

对于这种情况,我想到了另一种方法:基于直线探测的矫正算法。

首先介绍一下我的算法思路:

用霍夫线变换探测出图像中的所有直线
计算出每条直线的倾斜角,求他们的平均值
根据倾斜角旋转矫正
最后根据文本尺寸裁剪图片
然后给出OpenCV的实现算法:

#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
using namespace cv;
using namespace std;

#define ERROR 1234

//度数转换
double DegreeTrans(double theta)
{
	double res = theta / CV_PI * 180;
	return res;
}


//逆时针旋转图像degree角度(原尺寸)    
void rotateImage(Mat src, Mat& img_rotate, double degree)
{
	//旋转中心为图像中心    
	Point2f center;
	center.x = float(src.cols / 2.0);
	center.y = float(src.rows / 2.0);
	int length = 0;
	length = sqrt(src.cols*src.cols + src.rows*src.rows);
	//计算二维旋转的仿射变换矩阵  
	Mat M = getRotationMatrix2D(center, degree, 1);
	warpAffine(src, img_rotate, M, Size(length, length), 1, 0, Scalar(255,255,255));//仿射变换,背景色填充为白色  
}

//通过霍夫变换计算角度
double CalcDegree(const Mat &srcImage, Mat &dst)
{
	Mat midImage, dstImage;

Canny(srcImage, midImage, 50, 200, 3);
cvtColor(midImage, dstImage, CV_GRAY2BGR);

//通过霍夫变换检测直线
vector<Vec2f> lines;
HoughLines(midImage, lines, 1, CV_PI / 180, 300, 0, 0);//第5个参数就是阈值,阈值越大,检测精度越高
//cout << lines.size() << endl;

//由于图像不同,阈值不好设定,因为阈值设定过高导致无法检测直线,阈值过低直线太多,速度很慢
//所以根据阈值由大到小设置了三个阈值,如果经过大量试验后,可以固定一个适合的阈值。

if (!lines.size())
{
	HoughLines(midImage, lines, 1, CV_PI / 180, 200, 0, 0);
}
//cout << lines.size() << endl;

if (!lines.size())
{
	HoughLines(midImage, lines, 1, CV_PI / 180, 150, 0, 0);
}
//cout << lines.size() << endl;
if (!lines.size())
{
	cout << "没有检测到直线!" << endl;
	return ERROR;
}

float sum = 0;
//依次画出每条线段
for (size_t i = 0; i < lines.size(); i++)
{
	float rho = lines[i][0];
	float theta = lines[i][1];
	Point pt1, pt2;
	//cout << theta << endl;
	double a = cos(theta), b = sin(theta);
	double x0 = a*rho, y0 = b*rho;
	pt1.x = cvRound(x0 + 1000 * (-b));
	pt1.y = cvRound(y0 + 1000 * (a));
	pt2.x = cvRound(x0 - 1000 * (-b));
	pt2.y = cvRound(y0 - 1000 * (a));
	//只选角度最小的作为旋转角度
	sum += theta;

	line(dstImage, pt1, pt2, Scalar(55, 100, 195), 1, LINE_AA); //Scalar函数用于调节线段颜色

	imshow("直线探测效果图", dstImage);
}
float average = sum / lines.size(); //对所有角度求平均,这样做旋转效果会更好

cout << "average theta:" << average << endl;

double angle = DegreeTrans(average) - 90;

rotateImage(dstImage, dst, angle);
//imshow("直线探测效果图2", dstImage);
return angle;
}


void ImageRecify(const char* pInFileName, const char* pOutFileName)
{
	double degree;
	Mat src = imread(pInFileName);
	imshow("原始图", src);
	Mat dst;
	//倾斜角度矫正
	degree = CalcDegree(src,dst);
	if (degree == ERROR)
	{
		cout << "矫正失败!" << endl;
		return;
	}
	rotateImage(src, dst, degree);
	cout << "angle:" << degree << endl;
	imshow("旋转调整后", dst);

Mat resulyImage = dst(Rect(0, 0, dst.cols, 500)); //根据先验知识,估计好文本的长宽,再裁剪下来
imshow("裁剪之后", resulyImage);
imwrite("recified.jpg", resulyImage); 
}


int main()
{
	ImageRecify("correct2.jpg", "FinalImage.jpg");
	waitKey();
	return 0;
}

看看效果。这是原始图
在这里插入图片描述

直线探测的效果。
在这里插入图片描述

矫正之后的效果。
在这里插入图片描述

我们发现矫正之后的图像有较多留白,影响观看,所以需要进一步裁剪,保留文字区域。

在这里插入图片描述

赶紧再试多一张。

原始图
在这里插入图片描述

直线探测
在这里插入图片描述

矫正效果

在这里插入图片描述

进一步裁剪
在这里插入图片描述

可以看出,基于直线探测的矫正算法在文本处理上效果真的很不错!

最后总结一下两个算法的应用场景:

基于轮廓提取的矫正算法更适用于车牌、身份证、人民币、书本、发票一类矩形形状而且边界明显的物体矫正。

基于直线探测的矫正算法更适用于文本类的矫正。

标签:矫正,Mat,RoiSrcImg,深入探讨,OpenCV,图像,轮廓,CV
From: https://blog.csdn.net/m0_37302966/article/details/141635485

相关文章

  • OpenCV Mat和IplImage访问像素的方法总结
    在opencv的编程中,遍历访问图像元素是经常遇到的操作,掌握其方法非常重要,无论是Mat类的像素访问,还是IplImage结构体的访问的方法,都必须扎实掌握,毕竟,图像处理本质上就是对像素的各种操作,访问元素就是各种图像处理算法的第一步。首先先看看图像的是怎么存储的。单通道图像多......
  • CSEC:香港城市大学提出SOTA曝光矫正算法 | CVPR 2024
    CSEC:香港城市大学提出SOTA曝光矫正算法|CVPR2024 在光照条件不佳下捕获的图像可能同时包含过曝和欠曝。目前的方法主要集中在调整图像亮度上,这可能会加剧欠曝区域的色调失真,并且无法恢复过曝区域的准确颜色。论文提出通过学习估计和校正这种色调偏移,来增强既有过曝又有欠......
  • CSEC:香港城市大学提出SOTA曝光矫正算法 | CVPR 2024
    在光照条件不佳下捕获的图像可能同时包含过曝和欠曝。目前的方法主要集中在调整图像亮度上,这可能会加剧欠曝区域的色调失真,并且无法恢复过曝区域的准确颜色。论文提出通过学习估计和校正这种色调偏移,来增强既有过曝又有欠曝的图像。先通过基于UNet的网络推导输入图像的增亮和变暗......
  • Yololov5+Pyqt5+Opencv 实时城市积水报警系统
    在现代城市生活中,积水问题不仅影响交通和人们的日常生活,还可能对城市基础设施造成潜在的威胁。为了快速、准确地识别和应对积水问题,使用计算机视觉技术进行智能积水检测成为一个重要的解决方案。在这篇博客中,我将带你一步步实现一个基于YOLOv5的积水检测系统,帮助你轻松应对城市......
  • OpenCV(cv::circle())
    目录1.函数2.示例3.说明4.使用场景cv::circle()是OpenCV提供的一个函数,用于在图像上绘制圆形。它非常适用于在图像处理任务中标记特定的点或区域。这个函数具有多种参数,允许你根据需要控制圆的颜色、位置、半径和边界厚度。1.函数voidcv::circle(InputOutputArrayi......
  • 基于OpenCV-Python实现人脸识别-----摄像头捕获人脸图像显示中文乱码问题
    基于OpenCV-Python实现人脸识别时,为了使图像上显示识别到人员的中文名字,做了几次尝试,使用PIL.Image和OpenCV图像格式相互转换解决:使用OpenCV将图片灰度化,对加载的灰度化图使用分类器中的detectMultiScale()函数查找目标人脸,并使用for循环实现矩形框和圆形框框住查找到的人脸。......
  • OpenCV(VS2022配置OpenCV开发环境)
    目录1.下载OpenCV2.添加环境变量3.添加项目属性表4.配置DeBug属性表5.新的项目中快速配置6.配置Release属性表1.下载OpenCV访问:https://opencv.org/releases/2.添加环境变量添加环境变量%opencv%\build\x64\vc15\bin其中%opencv%为你自己的opencv文件夹的位置。......
  • 探索OpenCV:图像处理基础与实践
    探索OpenCV:图像处理基础与实践前言图像读取基础安装OpenCV库读取彩色与灰度图像RGB颜色模型颜色通道解析单通道图像显示感兴趣区域(ROI)图像处理进阶技巧图像打码图像组合图像缩放结语前言  在当今数字化时代,图像不仅是我们日常生活中不可或缺的一部分,也是科学研......
  • OpenCV(cv::addWeighted()、cv::threshold())
    目录1.cv::addWeighted()函数定义:参数详解:公式:例子:2.cv::threshold()函数定义:参数详解:返回值:例子:3.总结:1.cv::addWeighted()cv::addWeighted()是OpenCV中用于将两幅图像按指定的权重进行加权求和的函数。主要用途包括图像融合、过渡效果生成等。函数定义:voidcv::add......
  • OpenCV开发笔记(七十九):基于Stitcher类实现全景图片拼接
    前言  一个摄像头视野不大的时候,我们希望进行两个视野合并,这样让正视的视野增大,从而可以看到更广阔的标准视野。拼接的方法分为两条路,第一条路是stitcher类,第二条思路是特征点匹配。  本篇使用stitcher匹配,进行两张图来视野合并拼接。 Demo   两张图拼接过......