OpenCV图像矫正技术深入探讨

转自:https://www.cnblogs.com/skyfsm/p/6902524.html

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

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

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

人民币

发票

文本

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

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

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

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

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

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

  • 轮廓提取技术
  • 霍夫变换知识
  • ROI感兴趣区域知识

下面以人民币矫正、发票矫正、文本矫正为例,一步步剖析如何实现图像矫正。

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

比如我们要矫正这张人民币,思路应该是怎么样?

首先分析这张图的特点。

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

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

再详细地总结处理步骤:

  1. 图片灰度化
  2. 阈值二值化
  3. 检测轮廓
  4. 寻找轮廓的包围矩阵,并且获取角度
  5. 根据角度进行旋转矫正
  6. 对旋转后的图像进行轮廓提取
  7. 对轮廓内的图像区域抠出来,成为一张独立图像

我把该矫正算法命名为基于轮廓提取的矫正算法,因为其关键技术就是通过轮廓来获取旋转角度。

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

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

	cvBoxPoints(rect, rectpoint); <span class="hljs-comment">//获取4个顶点座标  </span>
	<span class="hljs-comment">//与水平线的角度  </span>
	<span class="hljs-keyword">float</span> angle = rect.angle;
	<span class="hljs-built_in">cout</span> &lt;&lt; angle &lt;&lt; <span class="hljs-built_in">endl</span>;

	<span class="hljs-keyword">int</span> line1 = <span class="hljs-built_in">sqrt</span>((rectpoint[<span class="hljs-number">1</span>].y - rectpoint[<span class="hljs-number">0</span>].y)*(rectpoint[<span class="hljs-number">1</span>].y - rectpoint[<span class="hljs-number">0</span>].y) + (rectpoint[<span class="hljs-number">1</span>].x - rectpoint[<span class="hljs-number">0</span>].x)*(rectpoint[<span class="hljs-number">1</span>].x - rectpoint[<span class="hljs-number">0</span>].x));
	<span class="hljs-keyword">int</span> line2 = <span class="hljs-built_in">sqrt</span>((rectpoint[<span class="hljs-number">3</span>].y - rectpoint[<span class="hljs-number">0</span>].y)*(rectpoint[<span class="hljs-number">3</span>].y - rectpoint[<span class="hljs-number">0</span>].y) + (rectpoint[<span class="hljs-number">3</span>].x - rectpoint[<span class="hljs-number">0</span>].x)*(rectpoint[<span class="hljs-number">3</span>].x - rectpoint[<span class="hljs-number">0</span>].x));
	<span class="hljs-comment">//rectangle(binImg, rectpoint[0], rectpoint[3], Scalar(255), 2);</span>
	<span class="hljs-comment">//面积太小的直接pass</span>
	<span class="hljs-keyword">if</span> (line1 * line2 &lt; <span class="hljs-number">600</span>)
	{
		<span class="hljs-keyword">continue</span>;
	}

	<span class="hljs-comment">//为了让正方形横着放,所以旋转角度是不一样的。竖放的,给他加90度,翻过来  </span>
	<span class="hljs-keyword">if</span> (line1 &gt; line2) 
	{
		angle = <span class="hljs-number">90</span> + angle;
	}

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

	<span class="hljs-comment">//抠图到RoiSrcImg</span>
	srcImg.copyTo(RoiSrcImg, binImg);


	<span class="hljs-comment">//再显示一下看看,除了感兴趣的区域,其他部分都是黑色的了  </span>
	namedWindow(<span class="hljs-string">"RoiSrcImg"</span>, <span class="hljs-number">1</span>);
	imshow(<span class="hljs-string">"RoiSrcImg"</span>, RoiSrcImg);

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

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

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

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

#endif

}

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

效果依次如下:
原始图

二值化图

掩膜mask是这样的

旋转矫正之后

将人民币区域抠出来

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

原始图

倾斜矫正之后

最后把目标区域抠出来,成为单独的照片。

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

原图

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

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

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

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

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

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

  1. 用霍夫线变换探测出图像中的所有直线
  2. 计算出每条直线的倾斜角,求他们的平均值
  3. 根据倾斜角旋转矫正
  4. 最后根据文本尺寸裁剪图片

然后给出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.colssrc.cols + src.rowssrc.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, <span class="hljs-number">50</span>, <span class="hljs-number">200</span>, <span class="hljs-number">3</span>);
cvtColor(midImage, dstImage, CV_GRAY2BGR);

<span class="hljs-comment">//通过霍夫变换检测直线</span>
<span class="hljs-built_in">vector</span>&lt;Vec2f&gt; lines;
HoughLines(midImage, lines, <span class="hljs-number">1</span>, CV_PI / <span class="hljs-number">180</span>, <span class="hljs-number">300</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>);<span class="hljs-comment">//第5个参数就是阈值,阈值越大,检测精度越高</span>
<span class="hljs-comment">//cout &lt;&lt; lines.size() &lt;&lt; endl;</span>

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

<span class="hljs-keyword">if</span> (!lines.size())
{
	HoughLines(midImage, lines, <span class="hljs-number">1</span>, CV_PI / <span class="hljs-number">180</span>, <span class="hljs-number">200</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>);
}
<span class="hljs-comment">//cout &lt;&lt; lines.size() &lt;&lt; endl;</span>

<span class="hljs-keyword">if</span> (!lines.size())
{
	HoughLines(midImage, lines, <span class="hljs-number">1</span>, CV_PI / <span class="hljs-number">180</span>, <span class="hljs-number">150</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>);
}
<span class="hljs-comment">//cout &lt;&lt; lines.size() &lt;&lt; endl;</span>
<span class="hljs-keyword">if</span> (!lines.size())
{
	<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"没有检测到直线!"</span> &lt;&lt; <span class="hljs-built_in">endl</span>;
	<span class="hljs-keyword">return</span> ERROR;
}

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

	line(dstImage, pt1, pt2, Scalar(<span class="hljs-number">55</span>, <span class="hljs-number">100</span>, <span class="hljs-number">195</span>), <span class="hljs-number">1</span>, LINE_AA); <span class="hljs-comment">//Scalar函数用于调节线段颜色</span>

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

<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"average theta:"</span> &lt;&lt; average &lt;&lt; <span class="hljs-built_in">endl</span>;

<span class="hljs-keyword">double</span> angle = DegreeTrans(average) - <span class="hljs-number">90</span>;

rotateImage(dstImage, dst, angle);
<span class="hljs-comment">//imshow("直线探测效果图2", dstImage);</span>
<span class="hljs-keyword">return</span> 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(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, dst.cols, <span class="hljs-number">500</span>)); <span class="hljs-comment">//根据先验知识,估计好文本的长宽,再裁剪下来</span>
imshow(<span class="hljs-string">"裁剪之后"</span>, resulyImage);
imwrite(<span class="hljs-string">"recified.jpg"</span>, resulyImage); 

}

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

看看效果。这是原始图

直线探测的效果。

矫正之后的效果。

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

赶紧再试多一张。

原始图

直线探测

矫正效果

进一步裁剪

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

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

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章