利用包围轮廓和仿射变换进行图像倾斜校正

作者 Xiangyu Pu 日期 2017-06-25
利用包围轮廓和仿射变换进行图像倾斜校正

本文所展示的是利用OpenCV来进行图像的倾斜校正,由于图像背景复杂,需要旋转的角度未知,我们大致需要完成以下几个步骤:

  1. 对图像进行二值化处理,方便检测出目标的轮廓
  2. 计算图像的矩,绘制轮廓,并利用minAreaRect()函数寻找最小面积的包围矩形
  3. 计算旋转矩阵,并通过旋转矩阵来完成最终的倾斜校正

实现细节

代码如下:

#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>
using namespace cv;
using namespace std;
//空洞填充
void fillHole(const Mat srcBw, Mat &dstBw)
{
Size m_Size = srcBw.size();
Mat Temp = Mat::zeros(m_Size.height + 2, m_Size.width + 2, srcBw.type());//延展图像
srcBw.copyTo(Temp(Range(1, m_Size.height + 1), Range(1, m_Size.width + 1)));
cv::floodFill(Temp, Point(0, 0), Scalar(255));
Mat cutImg;//裁剪延展的图像
Temp(Range(1, m_Size.height + 1), Range(1, m_Size.width + 1)).copyTo(cutImg);
dstBw = srcBw | (~cutImg);
}
int main()
{
Mat src = imread("rotImage.jpg");
if (!src.data)
{
printf("读取图片错误! \n");
return false;
}
imshow("原图像", src);
Mat src1 = src.clone();
//图像预处理
Mat dst, gray, edge;
dst.create(src1.size(), src1.type());
cvtColor(src1, gray, COLOR_BGR2GRAY);
blur(gray, edge, Size(3, 3));
Mat bw;
cv::threshold(edge, bw, 120, 250, 0);
//填充图像
Mat bwFill;
fillHole(bw, bwFill);
imshow("二值化处理后的图像", bwFill);
//找到轮廓
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(bwFill, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);
//计算矩
vector<Moments> mu(contours.size());
for (unsigned int i = 0; i < contours.size(); i++)
{
mu[i] = moments(contours[i], false);
}
//计算中心矩
vector<Point2f> mc(contours.size());
for (unsigned int i = 0; i < contours.size(); i++)
{
mc[i] = Point2f(static_cast<float>(mu[i].m10 / mu[i].m00), static_cast<float>(mu[i].m01 / mu[i].m00));
}
//绘制轮廓
Mat drawing = Mat::zeros(bwFill.size(), CV_8UC3);
RNG g_rng(12345);
for (unsigned int i = 0; i< contours.size(); i++)
{
Scalar color = Scalar(g_rng.uniform(0, 255), g_rng.uniform(0, 255), g_rng.uniform(0, 255));//随机生成颜色值
drawContours(drawing, contours, i, color, 2, 8, hierarchy, 0, Point());//绘制外层和内层轮廓
circle(drawing, mc[i], 4, color, -1, 8, 0);//绘制圆
}
//绘制外框矩形
RotatedRect box;
Point2f vertex[4];
for (unsigned int i = 0; i < contours.size(); i++)
{
box = minAreaRect(Mat(contours[i]));//寻找最小面积的包围矩形
box.points(vertex);
for (int i = 0; i < 4; i++)
line(drawing, vertex[i], vertex[(i + 1) % 4], Scalar(100, 200, 211), 2, CV_AA);//绘制出最小面积的包围矩形
}
imshow("绘制的矩形框", drawing);
cout <<"需要校正的角度为:"<< box.angle;
// 计算旋转矩阵
Mat rotMat, rotateImage;
rotMat = getRotationMatrix2D(box.center, box.angle,1);
// 旋转图像
warpAffine(src, rotateImage, rotMat, src.size());
// 显示到窗口中
imshow("校正后的图像", rotateImage);
waitKey(0);
return 0;
}

运行结果:

原图像

orinal_image

二值化处理后的图像

threshold_image

寻找的最小面积的包围矩形

minAreaRect_image

得到的需要校正的角度

angle_image

完成校正的图像

rotCorrect_image

具体算法思路

为了区别背景和目标物体,首先要对图像进行二值化处理,其中使用了一个空洞填充算法fillHole(),这里借鉴了冈萨雷斯书上的集合运算方法。在本例中这不是必须的,但考虑到通用性,空洞填充能更利于进一步处理,减少未知BUG。其大致思路如下:

  1. 设原图像为A,首先A向外延展一到两个像素,并将值填充为背景色(0),标记为B。
  2. 使用floodFill函数将B的大背景填充,填充值为前景色(255),种子点为(0,0)即可(步骤一可以确保(0,0)点位于大背景),标记为C。
  3. 将填充好的图像裁剪为原图像大小(去掉延展区域),标记为D。
  4. 将D取反与A相加即得填充的图像,E=A|(~D)。

下一步是找到目标物的轮廓,这里不再赘述。

第三步是使用OpenCV中的minAreaRect()函数返回一个最小面积的包围矩形RotateRect,并绘制外框矩形。

RotateRect类成员包括了angle、center等,这是下一步的关键数据。关于minAreaRect()和RotateRect类的说明可参考:

Stack Overflow - MinAreaRect angles

RotatedRect和CvBox2D的角度疑云

最后一步是使用以上数据得到旋转矩阵,并使用旋转矩阵和warpAffine()函数进行校正得到最后的图像。

warpAffine()函数实际上是一种仿射变换,关于仿射变换的原理可参考:

OpenCV官方文档 - 仿射变换

总结

这套算法对平面倾斜的图像有较好的处理效果,若图像为透视倾斜,则需要考虑其他的解决思路,因本例暂未涉及,所以不再赘述。