OpenCV文档矫正
需求 将一个斜着拍摄的文档矫正成正的,如图所示:
文章图片
【OpenCV|OpenCV文档矫正】
文章图片
思路
- 读取原始图像,若图像太大可以先进行缩放处理,并获取原始图像的宽和高
- 对图像进行预处理得到边缘,依次进行灰度处理、高斯模糊、边缘检测、膨胀、腐蚀。
- 找到最大的轮廓,并提取角点
- 进行降噪处理:检测轮廓面积,只保留大于阈值面积的轮廓
- 计算每个轮廓的周长,使用DP算法计算出轮廓点的个数,规则为周长*0.02
- 找到图像中面积最大的,且角点为4的轮廓
- 将找到的四个角点排列成一个固定的顺序,排列后的顺序为:左上角-右上角-左下角-右下角
- 将每个点的xy坐标值相加(x+y),左上角的点的坐标和应该是最小的,右下角的点的坐标和应该是最大的
- 将每个点的xy坐标值相减(x-y),左下角的点的坐标差应该是最小的,右上角的点的坐标差应该是最大的
- 重新排列四个角点
- 进行透视变换
- 根据变换前及变换后的四个角点,创建变换矩阵
- 根据变换矩阵对图像进行透视变换
- 若透视变换后有一些毛边,按需要进行裁剪,裁剪后重新调整比例
- 创建一个矩形用来裁剪,并设定四周裁剪5像素
- 裁剪后重新调整图像宽高
- 显示变换后图像
#include
#include
#include
#include using namespace cv;
using namespace std;
// 一些定义
Mat image_origin,// 原始图像
image_gray,// 灰度处理后的图像
image_blur,// 高斯模糊处理后的图像
image_canny,// 边缘检测后的图像
image_dilate,// 膨胀后的图像
image_erode,// 腐蚀后的图像
image_preprocess, // 预处理后的图像
image_trans,// 透视变换后的图像
image_crop;
// 裁剪后的图像vector origin_points,// 重新排列前的角点
reorder_points;
// 重新排列后的角点int origin_width = 0, origin_height = 0;
/*
* 函数功能:预处理,依次进行灰度处理、高斯模糊、边缘检测、膨胀、腐蚀。
* 输入:图像,是否显示(0-不显示 1-显示每一步处理后的图像 2-只显示最终图像)
* */
Mat PreProcess(const Mat& image, int display)
{
// 灰度处理
cvtColor(image, image_gray, COLOR_BGR2GRAY);
// 高斯模糊
GaussianBlur(image_gray, image_blur, Size(3, 3), 3, 0);
// 边缘检测(边缘检测前对图像进行一次高斯模糊)
Canny(image_blur, image_canny, 50, 150);
// 膨胀和腐蚀(有时进行边缘检测的时候,没有被完全填充,或者无法正确检测,可以用膨胀和腐蚀)
// 创建一个用于膨胀和腐蚀的内核,后面的数字越大膨胀的越多(数字要用奇数)
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
// 膨胀
dilate(image_canny, image_dilate, kernel);
// 腐蚀
//erode(image_dilate, image_erode, kernel);
// 显示预处理效果
if(display == 1)
{
imshow("灰度处理后的图像", image_gray);
imshow("高斯模糊后的图像", image_blur);
imshow("边缘检测后的图像", image_canny);
imshow("膨胀后的图像", image_dilate);
//imshow("腐蚀后的图像", image_erode);
}
else if(display == 2)
{
imshow("预处理后的图像", image_dilate);
} return image_dilate;
}/*
* 函数功能:找到面积最大的轮廓
* 输入:源图像
* 输出:最大轮廓的四个角点数组
* */
vector GetMaxContour(const Mat& img_input)
{
/*
* contours是一个双重向量,向量内每个元素保存了一组由连续的Point点构成的点的集合的向量,每一组Point点集就是一个轮廓。有多少轮廓,向量contours就有多少元素。
* 相当于创建了这样一个向量{{Point(),Point()},{},{}}
* */
vector contours;
/*
* hierarchy向量内每个元素保存了一个包含4个int整型的数组。向量hiararchy内的元素和轮廓向量contours内的元素是一一对应的,向量的容量相同。
* hierarchy向量内每一个元素的4个int型变量——hierarchy[i][0] ~ hierarchy[i][3],分别表示第i个轮廓的后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号。
* 如果当前轮廓没有对应的后一个轮廓、前一个轮廓、父轮廓或内嵌轮廓的话,则hierarchy[i][0] ~ hierarchy[i][3]的相应位被设置为默认值-1。
* */
vector hierarchy;
/*
* findContours找到轮廓
* 第一个参数:单通道图像矩阵,可以是灰度图,但更常用的是二值图像,一般是经过Canny、拉普拉斯等边缘检测算子处理过的二值图像;
* 第二个参数:contours (前文介绍过)
* 第三个参数:hierarchy(前文介绍过)
* 第四个参数:轮廓的检索模式
*取值一:CV_RETR_EXTERNAL 只检测最外围轮廓,包含在外围轮廓内的内围轮廓被忽略
*取值二:CV_RETR_LIST检测所有的轮廓,包括内围、外围轮廓,但是检测到的轮廓不建立等级关系,彼此之间独立,没有等级关系,这就意味着这个检索模式下不存在父轮廓或内嵌轮廓,所以hierarchy向量内所有元素的第3、第4个分量都会被置为-1,具体下文会讲到
*取值三:CV_RETR_CCOMP检测所有的轮廓,但所有轮廓只建立两个等级关系,外围为顶层,若外围内的内围轮廓还包含了其他的轮廓信息,则内围内的所有轮廓均归属于顶层
*取值四:CV_RETR_TREE检测所有轮廓,所有轮廓建立一个等级树结构。外层轮廓包含内层轮廓,内层轮廓还可以继续包含内嵌轮廓。
* 第五个参数:轮廓的近似方法
*取值一:CV_CHAIN_APPROX_NONE保存物体边界上所有连续的轮廓点到contours向量内
*取值二:CV_CHAIN_APPROX_SIMPLE 仅保存轮廓的拐点信息,把所有轮廓拐点处的点保存入contours向量内,拐点与拐点之间直线段上的信息点不予保留
*取值三和四:CV_CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS使用teh-Chinl chain 近似算法
* 第六个参数:Point偏移量,所有的轮廓信息相对于原始图像对应点的偏移量,相当于在每一个检测出的轮廓点上加上该偏移量,且Point可以是负值。不填为默认不偏移Point()
* */
findContours(img_input, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
/*
* drawContours绘出轮廓
* 第一个参数:指明在哪幅图像上绘制轮廓。image为三通道才能显示轮廓
* 第二个参数:contours
* 第三个参数:指定绘制哪条轮廓,如果是-1,则绘制其中的所有轮廓
* 第四个参数:轮廓线颜色
* 第五个参数:轮廓线的宽度,如果是-1(FILLED),则为填充
* */
// // 不全输出,在下文只输出角点
// drawContours(image, contours, -1, Scalar(255, 0, 255), 2);
// 定义轮廓,大小与contours相同,但内层向量中只有角点(例如三角形就是3,四边形就是4,圆形可能七八个)
vector corners_contours(contours.size());
// 定义边界框,大小与contours相同
vector bounding_box(contours.size());
vector biggest_contours;
double max_area = 0;
for (int i = 0;
i < contours.size();
i++)
{
// 检测轮廓面积
double contour_area = contourArea(contours[i]);
//cout << area << endl;
// 假设图像中有噪声,需要将其过滤,只保留面积大于1000的轮廓
if (contour_area > 1000)
{
// 计算每个轮廓的周长
double contour_perimeter = arcLength(contours[i], true);
// 使用DP算法计算出轮廓点的个数,规则为周长*0.02
approxPolyDP(contours[i], corners_contours[i], 0.02 * contour_perimeter, true);
// 找到图像中面积最大的,且角点为4的轮廓
if (contour_area > max_area && corners_contours[i].size() == 4 ) {//drawContours(image_origin, conPoly, i, Scalar(255, 0, 255), 5);
biggest_contours = { corners_contours[i][0],corners_contours[i][1] ,corners_contours[i][2] ,corners_contours[i][3] };
max_area = contour_area;
}//// 只绘制角点之间的边框线,Debug用,取消注释可以看到检测出的所有边界框
//drawContours(image_origin, corners_contours, i, Scalar(255, 0, 255), 2);
//rectangle(image_origin, bounding_box[i].tl(), bounding_box[i].br(), Scalar(0, 255, 0), 5);
}
} // 返回最大的轮廓
return biggest_contours;
}/*
* 函数功能:绘制一些点
* 输入:点集,颜色
* */
void DrawPoints(vector points, const Scalar& color)
{
for (int i = 0;
i < points.size();
i++)
{
circle(image_origin, points[i], 10, color, FILLED);
putText(image_origin, to_string(i), points[i], FONT_HERSHEY_PLAIN, 4, color, 4);
}
}/*
* 函数功能:重新排列四个角点的顺序
* 最终顺序为: 01
*23
*数组中为左上角-右上角-左下角-右下角
* */
vector ReorderPoints(vector points)
{
vector newPoints;
vectorsumPoints, subPoints;
// OpenCV中左上顶点为(0,0),右为x轴正向,下为y轴正向。
for (int i = 0;
i < 4;
i++)
{
// 将每个点的xy坐标值相加(x+y),左上角的点的坐标和应该是最小的,右下角的点的坐标和应该是最大的
sumPoints.push_back(points[i].x + points[i].y);
// 将每个点的xy坐标值相减(x-y),左下角的点的坐标差应该是最小的,右上角的点的坐标差应该是最大的
subPoints.push_back(points[i].x - points[i].y);
} // 重新排列
newPoints.push_back(points[min_element(sumPoints.begin(), sumPoints.end()) - sumPoints.begin()]);
// 0 和的最小值
newPoints.push_back(points[max_element(subPoints.begin(), subPoints.end()) - subPoints.begin()]);
// 1 差的最大值
newPoints.push_back(points[min_element(subPoints.begin(), subPoints.end()) - subPoints.begin()]);
// 2 差的最小值
newPoints.push_back(points[max_element(sumPoints.begin(), sumPoints.end()) - sumPoints.begin()]);
// 3 和的最大值 return newPoints;
}/*
* 函数功能:
* 输入:源图像,四个角点的集合(角点的顺序为,左上角-右上角-左下角-右下角),输出的宽,输出的高
* 输出:透视变换后的图像
* */
Mat PerspectiveTrans(const Mat& img, vector points, float width, float height )
{
// 前面经过重新排列,四个角点的顺序为:左上角-右上角-左下角-右下角
Point2f src[4] = { points[0],points[1],points[2],points[3] };
// 变换后的四个角点
Point2f dst[4] = { {0.0f,0.0f},{width,0.0f},{0.0f,height},{width,height} };
// 创建变换矩阵
Mat matrix = getPerspectiveTransform(src, dst);
// 透视变换
warpPerspective(img, image_trans, matrix, Point(width, height));
return image_trans;
}int main()
{
// 1.读取原始图像
string path = "res/image_origin.jpg";
image_origin = imread(path);
// // 若图像太大可以先进行缩放处理
// resize(image_origin, image_origin, Size(), 0.5, 0.5);
// 获取原始图像的宽和高
origin_width= image_origin.size().width;
origin_height = image_origin.size().height;
// 2.对图像进行预处理得到边缘,依次进行灰度处理、高斯模糊、边缘检测、膨胀、腐蚀。
image_preprocess = PreProcess(image_origin, 0);
// 3.找到最大的轮廓,并提取角点
origin_points = GetMaxContour(image_preprocess);
// DrawPoints(origin_points, Scalar(0, 0, 255));
// 红色
// 此时发现,角点的顺序不固定,为了后面进行透视变换时与代码中变换后点集的顺序相同,需要将其排列成一个固定的顺序,排列后的顺序为:左上角-右上角-左下角-右下角
reorder_points = ReorderPoints(origin_points);
// DrawPoints(reorder_points, Scalar(0, 255, 0));
//绿色 // 4.透视变换
image_trans = PerspectiveTrans(image_origin, reorder_points, origin_width, origin_height);
// 透视变换后有一些毛边,若需要可以进行裁剪
// 四周裁剪5像素
int cropVal= 5;
// 创建一个矩形用来裁剪
Rect roi(cropVal, cropVal, origin_width - (2 * cropVal), origin_height - (2 * cropVal));
image_crop = image_trans(roi);
// 裁剪后重新调整比例
resize(image_crop, image_crop, Size(origin_width, origin_height));
// 5.显示并输出变换后图像
imshow("源图像", image_origin);
imshow("最终图像", image_crop);
imwrite("res/image_output.jpg", image_crop);
waitKey(0);
}
效果
文章图片
推荐阅读
- 计算机视觉教程|计算机视觉教程2-7(天使与恶魔?图文详解图像形态学运算(附代码))
- opencv|opencv(15)---图像膨胀腐蚀
- 人工智能|OpenCV之形态学操作(消除文章批注)
- 计算机视觉|OpenCV之图像轮廓(绘制图像轮廓)
- 竞赛|高级数据结构(树状数组以及逆序对求解)
- C++|蓝桥杯算法提高欧拉函数--复杂度为O(n log n)
- 《C++要笑着学》|【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针
- c++|c++类的默认成员函数
- C++|C++ 网络套接字编程TCP和UDP实例