这是来源于一个真实的案例,扫描仪扫描到的法律文件,需要切除白边,去掉边缘空白,这样看上去才更真实。如果采用人工操作,其成本和时间花费都太高了。我们希望通过一个程序,既准确又高效地完成任务。
其实本案例不需要扩展模块当中的特征提取就可以实现,运用基本的图像处理的知识,我们来尝试。
实现思路:通过边缘检测+轮廓检测或者直线检测最大外接矩形实现。
首先我们拿到一种倾斜的照片,我们在进行切边之前需要校正图片,即对图像进行旋转操作,程序如下:
#include <opencv2/opencv.hpp> #include<vector> #include <iostream> #include <math.h> using namespace cv; using namespace std; Mat src_img, gray_img, dst_img; int threshold_value = 100; // 定义阈值,全局变量 int max_level = 255; // 定义最大阈值,全局变量 const string output_win = "Contours Result"; const string roi_win = "Final Result"; void FindROI(int, void*); //声明函数,用于找到兴趣区域 void Check_Skew(); //声明函数,用于纠正倾斜 int main( ) { src_img = imread("456.png"); if (src_img.empty()) { printf("could not load the image...\n"); return -1; } namedWindow("原图", CV_WINDOW_AUTOSIZE); imshow("原图", src_img); Check_Skew(); //纠正倾斜 /* src_img = imread("2018.4.25.jpg"); if (src_img.empty()) { printf("could not load the image...\n"); return -1; } namedWindow(output_win, CV_WINDOW_AUTOSIZE); // 接下来提取兴趣区域 createTrackbar("Threshold:",output_win, &threshold_value, max_level, FindROI); FindROI(0, 0); */ waitKey(0); return 0; } void Check_Skew() { Mat canny_output; cvtColor(src_img, gray_img, COLOR_BGR2GRAY); //将原图转化为灰度图 Canny(gray_img, canny_output, threshold_value, threshold_value * 2, 3, false); // canny边缘检测 vector<vector<Point>> contours; vector<Vec4i> hireachy; findContours(canny_output, contours, hireachy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0)); // 找到所有轮廓 Mat drawImg = Mat::zeros(src_img.size(), CV_8UC3); float max_width = 0; // 定义最大宽度 float max_height = 0; // 定义最大高度 double degree = 0; // 定义旋转角度 for (auto t = 0; t < contours.size(); ++t) // 遍历每一个轮廓 { RotatedRect minRect = minAreaRect(contours[t]); // 找到每一个轮廓的最小外包旋转矩形,RotatedRect里面包含了中心坐标、尺寸以及旋转角度等信息 degree = abs(minRect.angle); if (degree > 0) { max_width = max(max_width, minRect.size.width); max_height = max(max_height, minRect.size.height); } } RNG rng(12345); for (auto t = 0; t < contours.size();++t) { RotatedRect minRect = minAreaRect(contours[t]); if (max_width == minRect.size.width && max_height == minRect.size.height) { degree = minRect.angle; // 保存目标轮廓的角度 Point2f pts[4]; minRect.points(pts); Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)); //产生随机颜色 for (int i = 0; i < 4; ++i) { line(drawImg, pts[i], pts[(i + 1) % 4], color, 2, 8, 0); } } } imshow("找到的矩形轮廓", drawImg); Point2f center(src_img.cols / 2, src_img.rows / 2); Mat rotm = getRotationMatrix2D(center, degree, 1.0); //获取仿射变换矩阵 Mat dst; warpAffine(src_img, dst, rotm, src_img.size(), INTER_LINEAR, 0, Scalar(255, 255, 255)); // 进行图像旋转操作 imwrite("123.png", dst); //将校正后的图像保存下来 imshow("Correct Image", dst); }
运行程序,我们可以看到:
再得到正立摆放的图像之后,我们就可以对其进行切变操作了,代码如下:
#include <opencv2/opencv.hpp> #include<vector> #include <iostream> #include <math.h> using namespace cv; using namespace std; Mat src_img, gray_img, dst_img; int threshold_value = 100; // 定义阈值,全局变量 int max_level = 255; // 定义最大阈值,全局变量 const string output_win = "Contours Result"; const string roi_win = "Final Result"; void FindROI(int, void*); //声明函数,用于找到兴趣区域 void Check_Skew(); //声明函数,用于纠正倾斜 int main( ) { /* src_img = imread("456.png"); if (src_img.empty()) { printf("could not load the image...\n"); return -1; } namedWindow("原图", CV_WINDOW_AUTOSIZE); imshow("原图", src_img); Check_Skew(); //纠正倾斜 */ src_img = imread("123.png"); if (src_img.empty()) { printf("could not load the image...\n"); return -1; } namedWindow(output_win, CV_WINDOW_AUTOSIZE); // 接下来提取兴趣区域 createTrackbar("Threshold:",output_win, &threshold_value, max_level, FindROI); FindROI(0, 0); waitKey(0); return 0; } /* void Check_Skew() { Mat canny_output; cvtColor(src_img, gray_img, COLOR_BGR2GRAY); //将原图转化为灰度图 Canny(gray_img, canny_output, threshold_value, threshold_value * 2, 3, false); // canny边缘检测 vector<vector<Point>> contours; vector<Vec4i> hireachy; findContours(canny_output, contours, hireachy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0)); // 找到所有轮廓 Mat drawImg = Mat::zeros(src_img.size(), CV_8UC3); float max_width = 0; // 定义最大宽度 float max_height = 0; // 定义最大高度 double degree = 0; // 定义旋转角度 for (auto t = 0; t < contours.size(); ++t) // 遍历每一个轮廓 { RotatedRect minRect = minAreaRect(contours[t]); // 找到每一个轮廓的最小外包旋转矩形,RotatedRect里面包含了中心坐标、尺寸以及旋转角度等信息 degree = abs(minRect.angle); if (degree > 0) { max_width = max(max_width, minRect.size.width); max_height = max(max_height, minRect.size.height); } } RNG rng(12345); for (auto t = 0; t < contours.size();++t) { RotatedRect minRect = minAreaRect(contours[t]); if (max_width == minRect.size.width && max_height == minRect.size.height) { degree = minRect.angle; // 保存目标轮廓的角度 Point2f pts[4]; minRect.points(pts); Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)); //产生随机颜色 for (int i = 0; i < 4; ++i) { line(drawImg, pts[i], pts[(i + 1) % 4], color, 2, 8, 0); } } } imshow("找到的矩形轮廓", drawImg); Point2f center(src_img.cols / 2, src_img.rows / 2); Mat rotm = getRotationMatrix2D(center, degree, 1.0); //获取仿射变换矩阵 Mat dst; warpAffine(src_img, dst, rotm, src_img.size(), INTER_LINEAR, 0, Scalar(255, 255, 255)); // 进行图像旋转 imwrite("2018.4.25.jpg", dst); imshow("Correct Image", dst); } */ void FindROI(int, void*) { printf("**************当前阈值:%d******************************\n", threshold_value); cvtColor(src_img, gray_img, COLOR_BGR2GRAY); //将原图转化为灰度图 Mat canny_output; Canny(gray_img, canny_output, threshold_value, threshold_value * 2, 3, false); // canny边缘检测 imshow("canny_output", canny_output); vector<vector<Point>> contours; vector<Vec4i> hireachy; findContours(canny_output, contours, hireachy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0)); // 调用API,找到轮廓 // 筛选contours中的轮廓,我们需要最大的那个轮廓 int min_width = src_img.cols*0.75; // 矩形的最小宽度 int min_height = src_img.rows*0.75; // 矩形的最小高度 RNG rng(12345); //定义一个随机数产生器,用来产生不同颜色的矩形框 Mat drawImage = Mat::zeros(src_img.size(), CV_8UC3); Rect bbox; for (auto t = 0; t < contours.size();++t) // 遍历每一个轮廓 { RotatedRect minRect = minAreaRect(contours[t]); // 找到每一个轮廓的最小外包旋转矩形,RotatedRect里面包含了中心坐标、尺寸以及旋转角度等信息 float degree = abs(minRect.angle); // 最小外包旋转矩形的旋转角度 if (minRect.size.width > min_width && minRect.size.height > min_height && minRect.size.width < (src_img.cols - 5)) //筛选最小外包旋转矩形 { printf("current angle : %f\n", degree); Mat vertices; // 定义一个4行2列的单通道float类型的Mat,用来存储旋转矩形的四个顶点 boxPoints(minRect, vertices); // 计算旋转矩形的四个顶点坐标 bbox = boundingRect(vertices); //找到输入点集的最小外包直立矩形,返回Rect类型 cout <<"最小外包矩形:"<< bbox << endl; Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)); //产生随机颜色 for (int i = 0; i < 4; i++) // 遍历每个旋转矩形的四个顶点坐标 { // 在相邻的顶点之间绘制直线 Mat p1 = vertices.row(i); // 使用成员函数row(i)和col(j)得到矩阵的第i行或者第j列,返回值仍然是一个单通道的Mat类型 int j = (i + 1) % 4; Mat p2 = vertices.row(j); Point p1_point = Point(p1.at<float>(0,0), p1.at<float>(0, 1)); //将Mat类型的顶点坐标转换为Point类型 Point p2_point = Point(p2.at<float>(0, 0), p2.at<float>(0, 1)); line(drawImage, p1_point, p2_point, color, 2, 8, 0); // 根据得到的四个顶点,通过连接四个顶点,将最小旋转矩形绘制出来 } } } imshow(output_win, drawImage); if (bbox.width > 0 && bbox.height > 0 ) { Mat roiImg = src_img(bbox); //从原图中截取兴趣区域 namedWindow(roi_win, CV_WINDOW_AUTOSIZE); imshow(roi_win, roiImg); } return; }
运行程序,可以看到:
改变阈值,可以看到:
效果还是蛮好的!