目录
一、什么是文档扫描
文档扫描步骤
二、使用的函数、变量介绍
变量介绍
函数介绍
三、实操
1 图像预处理
2 获取图像轮廓
3 提取并标记文档边缘
4 重新排序边缘
5 裁剪修饰边缘
一、什么是文档扫描
文档扫描即对采用不同视角所拍摄到的文本图像,以正视的形式将文本呈现。如下图:
所希望得到的是图片中的A4文档信息,经文档扫描后得到如下:
文档扫描步骤
1 将导入的图片经过转灰度、模糊降噪、canny边缘检测、膨胀等图像预处理
2 定义,轮廓变量,获取预处理后的图像轮廓
3 提取轮廓中的文本框部分,通过找最大外矩形轮廓函数
4 使用透视矩阵,使图像位于正视图,修剪边缘
二、使用的函数、变量介绍
变量介绍
vector 说明:
vector是向量类型,可以容纳许多类型的数据,因此也被称为容器(可以理解为动态数组,是封装好了的类)
进行vector操作前应添加头文件#include 具体可以查看
vector< Point >
vector容器里面放二维坐标点
vector<vector< Point >>
vector容器里面放了一个vector容器,子容器里放二维坐标点
vector< Vec4i > vector< int >
vector容器中放4维int向量
Point2f 二维坐标点
Rect 像素width * height from 位置(x*y)
RotateRect [angle,center[0,0],size[width*height]]
函数介绍
findContours(image, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
对findContours函数的具体内容可以查看findContours函数参数详解。
简单说明如下:
第一个image :为输入图像,可为灰度图,二值图常用,一般为经过Canny等边缘检测
第二个countors 定义为“vector<vector> 型向量,并且是一个双重向量,第一重向量对应轮廓元素,例如countours[i] 为第i个轮廓元素。且轮廓元素也为向量,保存的数据类型为Point类型。例如countours[i][j]为第i个轮廓的第J个特征点。
第三个 hierarchy 定义为vector < Vec4i >类型,即向量内每一个元素包括4个int型变量。vector分别表示第 i个轮廓的后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号。
第四个为int型的mode 定义轮廓检索模式
取值一:CV_RETR_EXTERNAL只检测最外围轮廓,包含在外围轮廓内的内围轮廓被忽略
取值二:CV_RETR_LIST 检测所有的轮廓,包括内围、外围轮廓,但是检测到的轮廓不建立等级关 系,彼此之间独立,没有等级关系,这就意味着这个检索模式下不存在父轮廓或内嵌轮廓, 所以hierarchy向量内所有元素的第3、第4个分量都会被置为-1。
取值三:CV_RETR_CCOMP 检测所有的轮廓,但所有轮廓只建立两个等级关系,外围为顶层,若外围 内的内围轮廓还包含了其他的轮廓信息,则内围内的所有轮廓均归属于顶层
取值四:CV_RETR_TREE, 检测所有轮廓,所有轮廓建立一个等级树结构。外层轮廓包含内层轮廓,内层轮廓还可以继续包含内嵌轮廓。
第五个参数:int型的method,定义轮廓的近似方法:
取值一:CV_CHAIN_APPROX_NONE 保存物体边界上所有连续的轮廓点到contours向量内
取值二:CV_CHAIN_APPROX_SIMPLE 仅保存轮廓的拐点信息,把所有轮廓拐点处的点保存入contours 向量内,拐点与拐点之间直线段上的信息点不予保留。
取值三和四: CV_CHAIN_APPROX_TC89_L1CV_CHAIN_APPROX_TC89_KCOS使用teh-Chinl chain 近似算法
第六个参数: Point偏移量 所有点相对移动一段距离,本项目未使用
approxPolyDP(contours[i], conPoly[i], 0.02 * peri, true);
void approxPolyDP(InputArray curve, OutputArray approxCurve, double epsilon, bool closed);//找出轮廓的多边形拟合曲线
第一个参数 InputArray curve:输入的点集
第二个参数OutputArray approxCurve:输出的点集,当前点集是能最小包容指定点集的。画出来即是一个多边形。
第三个参数double epsilon:指定的精度,也即是原始曲线与近似曲线之间的最大距离,根据周长来计算。
第四个参数bool closed:若为true,则说明近似曲线是闭合的;反之,若为false,则断开。
double arcLength( InputArray curve, bool closed );计算轮廓周长
InputArray类型的curve,输入的向量,二维点(轮廓顶点),可以为std::vector或Mat类型。
bool类型的closed,用于指示曲线是否封闭的标识符,一般设置为true
三、实操
1 图像预处理
Mat preProcessing(Mat img)
{
cvtColor(img, imgGray, COLOR_BGR2GRAY);
GaussianBlur(imgGray, imgBlur, Size(3, 3), 3, 0);
Canny(imgBlur, imgCanny, 25, 75);
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
dilate(imgCanny, imgDil, kernel);
//erode(imgDil, imgErode, kernel);
return imgDil;
}
2 获取图像轮廓
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(image, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
。
3 提取并标记文档边缘
vector<vector<Point>> conPoly(contours.size());
vector<Rect> boundRect(contours.size());
vector<Point> biggest;
int maxArea = 0;
for (int i = 0; i < contours.size(); i++)
{
int area = contourArea(contours[i]);
string objectType;
if (area > 1000)
{
float peri = arcLength(contours[i], true);
approxPolyDP(contours[i], conPoly[i], 0.02 * peri, true);
if (area > maxArea && conPoly[i].size() == 4) {
biggest = { conPoly[i][0],conPoly[i][1] ,conPoly[i][2] ,conPoly[i][3] };
maxArea = area;
}
}
}
return biggest;
}
4 重新排序边缘
vector<Point> reorder(vector<Point> points)
{
vector<Point> newPoints;
vector<int> sumPoints, subPoints;
for (int i = 0; i < 4; i++)
{
sumPoints.push_back(points[i].x + points[i].y);
subPoints.push_back(points[i].x - points[i].y);
}
//cout << sumPoints.begin() << endl;
newPoints.push_back(points[min_element(sumPoints.begin(), sumPoints.end())-sumPoints.begin()]); // 0
//cout << newPoints[0].x << endl;
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
cout << sizeof(newPoints) << endl;
return newPoints;
}
其中push_back为在容器后添加一个元素,min_elment和max_element分别为查找最大最小值。
5 绘制裁剪修饰边缘
得到透视投影后的图像
Mat getWarp(Mat img, vector<Point> points, float w, float h)
{
Point2f src[4] = { points[0],points[1],points[2],points[3] }; //初始变化四周
Point2f dst[4] = { {0.0f,0.0f},{w,0.0f},{0.0f,h},{w,h} }; //变化后的四周
Mat matrix = getPerspectiveTransform(src, dst); //得到变化矩阵
warpPerspective(img, imgWarp, matrix, Point(w, h)); //我,h为变化后的像素大小
return imgWarp;
}
得到图像结果如下:
可知四周仍有瑕疵,故对其修剪得到如下:
int cropVal = 5;
Rect roi(cropVal, cropVal, w-(2 * cropVal), h-(2 * cropVal));
imgCrop = imgWarp(roi);
最终得到运行结果如下:
最后代码如下:
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>
using namespace cv;
using namespace std;
/// Project 2 – Document Scanner //
Mat imgOriginal, imgGray, imgBlur, imgCanny, imgThre, imgDil, imgErode, imgWarp, imgCrop;
vector<Point> initialPoints, docPoints;
float w = 420, h = 596;
Mat preProcessing(Mat img)
{
cvtColor(img, imgGray, COLOR_BGR2GRAY);
GaussianBlur(imgGray, imgBlur, Size(3, 3), 3, 0);
Canny(imgBlur, imgCanny, 25, 75);
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
dilate(imgCanny, imgDil, kernel);
//erode(imgDil, imgErode, kernel);
return imgDil;
}
vector<Point> getContours(Mat image) {
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(image, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
//drawContours(img, contours, -1, Scalar(255, 0, 255), 2);
vector<vector<Point>> conPoly(contours.size());
vector<Rect> boundRect(contours.size());
vector<Point> biggest;
int maxArea = 0;
for (int i = 0; i < contours.size(); i++)
{
int area = contourArea(contours[i]);
//cout << area << endl;
string objectType;
if (area > 1000)
{
float peri = arcLength(contours[i], true);
approxPolyDP(contours[i], conPoly[i], 0.02 * peri, true);
if (area > maxArea && conPoly[i].size() == 4) {
//drawContours(imgOriginal, conPoly, i, Scalar(255, 0, 255), 5);
biggest = { conPoly[i][0],conPoly[i][1] ,conPoly[i][2] ,conPoly[i][3] };
maxArea = area;
}
//drawContours(imgOriginal, conPoly, i, Scalar(255, 0, 255), 2);
//rectangle(imgOriginal, boundRect[i].tl(), boundRect[i].br(), Scalar(0, 255, 0), 5);
}
}
return biggest;
}
void drawPoints(vector<Point> points, Scalar color)
{
for (int i = 0; i < points.size(); i++)
{
circle(imgOriginal, points[i], 10, color, FILLED);
putText(imgOriginal, to_string(i), points[i], FONT_HERSHEY_PLAIN, 4, color, 4);
}
}
vector<Point> reorder(vector<Point> points)
{
vector<Point> newPoints;
vector<int> sumPoints, subPoints;
for (int i = 0; i < 4; i++)
{
sumPoints.push_back(points[i].x + points[i].y);
subPoints.push_back(points[i].x - points[i].y);
}
//cout << sumPoints.begin() << endl;
newPoints.push_back(points[min_element(sumPoints.begin(), sumPoints.end())-sumPoints.begin()]); // 0
//cout << newPoints[0].x << endl;
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
cout << sizeof(newPoints) << endl;
return newPoints;
}
Mat getWarp(Mat img, vector<Point> points, float w, float h)
{
Point2f src[4] = { points[0],points[1],points[2],points[3] };
Point2f dst[4] = { {0.0f,0.0f},{w,0.0f},{0.0f,h},{w,h} };
Mat matrix = getPerspectiveTransform(src, dst);
warpPerspective(img, imgWarp, matrix, Point(w, h));
return imgWarp;
}
void main() {
string path = "resources/paper.jpg";
imgOriginal = imread(path);
resize(imgOriginal, imgOriginal, Size(), 0.5, 0.5);
// Preprpcessing – Step 1
imgThre = preProcessing(imgOriginal);
// Get Contours – Biggest – Step 2
initialPoints = getContours(imgThre);
//drawPoints(initialPoints, Scalar(0, 0, 255));
docPoints = reorder(initialPoints);
//drawPoints(docPoints, Scalar(0, 255, 0));
// Warp – Step 3
imgWarp = getWarp(imgOriginal, docPoints, w, h);
//Crop – Step 4
int cropVal = 5;
Rect roi(cropVal, cropVal, w-(2 * cropVal), h-(2 * cropVal));
imgCrop = imgWarp(roi);
imshow("Image", imgOriginal);
//imshow("Image Dilation", imgThre);
imshow("Image Warp", imgWarp);
imshow("Image Crop", imgCrop);
waitKey(0);
}