基于C++的OpenCV项目实战——文档照片转换成扫描文件

一、背景

前段时间都是基于Python的OpecCV进行一些学习和实践,但小的知识点并没有应用到实际的项目中;并且基于Python的版本的移植性、效率性都较差,在包含硬件的项目中往往都是采用基于C++的版本;

因此本次项目实战专题主要是基于C++的版本,并且从大的任务中剖析小的知识点,实际项目中算法的选型也是比较难的部分,根据需求和任务选用不同的算法,这才是真的掌握了知识点;

本次项目是照片转扫描文件,可以参考下面网址中的案例:

https://www.camscanner.com/

这已经是一个落地且成熟的项目了,下面将结合多个知识点进行实现;

二、基础知识

首先需要明确实现该任务的几个步骤:

1、视角转换:不规则四边形转变为矩形(使用透视变换算法);

2、背景降噪:去掉图中的一些噪声点(使用中值滤波算法);

3、颜色变换:二值化图像,使得呈现黑白扫描图(使用自适应高斯阈值算法);

投影变换算法介绍:

图1代表仿射变换,只需要6个自由度,并且约束条件是原来平行的线依旧平行,只需三对点对就可以求得未知参数值;

图4代表投影变换,需要四对点对,有8个自由度,可以将任意四边形变换为指定的四边形形状;

降噪算法介绍:

中值滤波算法示意图:

找到滑窗中中值的数,替换中间区域的数值;

原理上是将一些较亮、较暗的点进行降噪,也就是降噪的作用;

像高斯滤波和均值滤波,考虑到一个全局信息,是起到一个平滑的作用;

三、方案一:自动检测点

1、读取图片文件(进行了指定尺寸缩放)

// 读取图片
Mat readFile(String imagePath, int minEdge = 1080) {Mat image = imread(imagePath);int width = image.size().width;int height = image.size().height;int minline = min(width, height);float ratio = minEdge * 1.0 / minline;    // 得到缩放比例int processWith = width * ratio;int peocessHeight = height * ratio;Mat resultImg;     // 保存处理后图像resize(image, resultImg, Size(processWith, peocessHeight));return resultImg;
}

这里再定义一个显示图像的方法:

// 显示图片
void visualize(String winName, Mat image) {namedWindow(winName, WINDOW_NORMAL);imshow(winName, image);waitKey(0);
}

2、创建直线类并计算两条直线的交点

先定义一个直线类,包含两个端点;

// 编写一个直线类
class Line {
public:Point p1;Point p2;Point center;Line(Point p, Point q) : p1(p), p2(q) {     // 构造函数center = Point((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);}
};

假设已知两条直线上的两点,怎么求得交点呢?

可以参考这个网址中的数学公式:https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection

// 返回两条直线的交点
Point2f linesIntersect(Line lin1, Line lin2) {// 这里直接根据网上的数学公式求得int x1 = lin1.p1.x, y1 = lin1.p1.y;int x2 = lin1.p2.x, y2 = lin1.p2.y;int x3 = lin2.p1.x, y3 = lin2.p1.y;int x4 = lin2.p2.x, y4 = lin2.p2.y;float D = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);if (fabs(D) > 1e-6) {return Point2f(((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / D,((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / D);}return Point2f(-1, -1);
}

3、图像边缘检测Canny

Mat canny, gray;
cvtColor(image, gray, COLOR_BGR2GRAY);
double threshold_low = threshold(gray, canny, 0, 255, THRESH_BINARY | cv::THRESH_OTSU);
Canny(gray, canny, threshold_low, threshold_low * 2);
return canny;

注意:进行Canny边缘检测的效果和图像的大小有关,可以适当对大图进行缩放;

4、通过霍夫变换进行直线检测

检测到的直线分为两类,一类是水平线,一类是竖直线;

为了得到外边缘框的直线,可以先根据直线的外接矩形长宽比分为水平和竖直线,再根据中点的位置,找到边缘直线;

如上图所示,如果x>y,则将该直线分为水平线,如果y>x,则将该直线划分为水平线;

随后再根据中心点的坐标大小确定边缘线;

// 进行霍夫直线检测,保存所有检测到的直线,并且确保直线大于4条
vector<Vec4i> lines;
// 这里的参数需要根据图像大小等因素进行微调
HoughLinesP(canny, lines, 1, CV_PI / 180, 80, height/5, 200);
if (lines.size() < 4) {cout << "Find only" << lines.size() << "lines, return directly" << endl;
}// 将直线分为水平线和垂直线
vector<Line> horizontals, verticals;
Mat tmp = image.clone();
for (auto v : lines) {double w = fabs(v[0] - v[2]), h = fabs(v[1] - v[3]);Line tmp_line(Point(v[0], v[1]), Point(v[2], v[3]));if (w > h) horizontals.push_back(tmp_line);else verticals.push_back(tmp_line);// 下面两行代码是实现绘制直线//line(tmp, Point(v.val[0], v.val[1]), Point(v.val[2], v.val[3]), Scalar(255, 0, 0), 8);//visualize("hough test", tmp);
}// 确保水平线和垂直线至少有两条
if (horizontals.size() < 2 || verticals.size() < 2) {cout << "Not enough edge lines... " << endl;}// 将水平和垂直线按中心点位置进行排序,这里的两个排序函数需要自己实现
sort(horizontals.begin(), horizontals.end(), cmpHeight);
sort(verticals.begin(), verticals.end(), cmpWidth);// 绘制出找到的直线
line(tmp, horizontals[0].p1, horizontals[0].p2, Scalar(255, 0, 0), 8);
line(tmp, horizontals[horizontals.size()-1].p1, horizontals[horizontals.size() - 1].p2, Scalar(255, 0, 0), 8);
line(tmp, verticals[0].p1, verticals[0].p2, Scalar(255, 0, 0), 8);
line(tmp, verticals[verticals.size()-1].p1, verticals[verticals.size() - 1].p2, Scalar(255, 0, 0), 8);
visualize("hough test", tmp);

排序函数的实现:

// 对水平线进行排序
bool cmpHeight(const Line& l1, const Line& l2) {return l1.center.y < l2.center.y;
}// 对垂直线进行排序
bool cmpWidth(const Line& l1, const Line& l2) {return l1.center.x < l2.center.x;
}

5、求单应性矩阵

现在已知图像上目标的四个点坐标,通过点对关系,求得二者之间的单应性变换矩阵;

int dst_width = 1080, dst_height = 1920;
vector<Point2f> dst_pts;
dst_pts.push_back(Point(0, 0));
dst_pts.push_back(Point(dst_width - 1, 0));
dst_pts.push_back(Point(0, dst_height - 1));
dst_pts.push_back(Point(dst_width - 1, dst_height - 1));Mat warpedImg = Mat::zeros(dst_height, dst_width, CV_8UC3);
Mat homo = getPerspectiveTransform(ori_pts, dst_pts);
warpPerspective(image, warpedImg, homo, warpedImg.size());visualize("result", warpedImg);

结果图:

6、降噪和二值化

降噪采用中值滤波,阈值过滤采用自适应的二值化;

void postProcess(Mat& image) {medianBlur(image, image, 7);cvtColor(image, image, COLOR_BGR2GRAY);threshold(image, image, 0, 255, THRESH_BINARY | cv::THRESH_OTSU);
}

四、方案二:用户点选目标区域

方案一是完全基于自动化的方式,用户只需要传入图像,程序会自动选择合适的区域;

优点在于其节省了用户的人工成本,使得程序更为简便;

缺点在于算法具有局限性,对背景及区域选取有要求,比如不能在区域外出现干扰物体,也无法满足用户的一些特别需求,比如选定大区域中的小区域;

方案二的优势在于其强大的灵活性,用户可以根据自己的需求选择相应的区域,程序将对所选区域进行转换;

1、命令行解析

加入命令行参数的功能,用户可以从命令行传入参数;

int main(int argc, char** argv)
{const String keys ="{help h usage ? |      | print this message   }""{path           |D: / project / OpenCV / card.jpg| path to file         }";// 采用opencv命令行解析的方式CommandLineParser myParser(argc, argv, keys);String path = myParser.get<String>("path");cout << path << endl;
}

2、鼠标事件

主要实现用户点击鼠标时的一些交互功能:

// 这几个参数为默认参数
void onMouse(int event, int x, int y, int flags, void* userdata) {// 当点数为四个时,判断当前用户鼠标选取的拖动点是哪一个if (srcPts.size() == 4) {for (int i = 0; i < 4; i++) {Point2f v = srcPts[i];if ((event == EVENT_LBUTTONDOWN) & (abs(v.x - x) < 20) & (abs(v.y - y) < 20)) {isDragging = true;drag_index = i;}}}// 用户点击鼠标左键时,加入点else if (event == EVENT_LBUTTONDOWN) {srcPts.push_back(Point2f(x, y));}// 取消拖动if (event == EVENT_LBUTTONUP) {isDragging = false;}// 如果鼠标移动并且一直按着,就改变原来的点if ((event == EVENT_MOUSEMOVE) && isDragging) {srcPts[drag_index].x = x;srcPts[drag_index].y = y;}
}

3、主函数实现

定义了鼠标函数之后,需要将其中的操作在窗口进行展示:

namedWindow(winNameOri, WINDOW_NORMAL);
namedWindow(winNameRes, WINDOW_NORMAL);
setMouseCallback(winNameOri, onMouse, nullptr);
bool done = false;
while (!done) {if (srcPts.size() < 4) {img = oriImg.clone();for (int i = 0; i < srcPts.size(); i++) {circle(img, srcPts[i], 10, Scalar(255, 255, 0), 5);putText(img, labels[i].c_str(), srcPts[i], FONT_HERSHEY_SIMPLEX, 1, Scalar(255, 255, 255), 2);}imshow(winNameOri, img);}if (srcPts.size() == 4) {img = oriImg.clone();for (int i = 0; i < 4; i++) {circle(img, srcPts[i], 10, Scalar(255, 255, 0), 5);line(img, srcPts[i], srcPts[(i + 1) % 4], Scalar(0, 255, 0), 5);putText(img, labels[i].c_str(), srcPts[i], FONT_HERSHEY_SIMPLEX, 1, Scalar(255, 255, 255), 2);}imshow(winNameOri, img);}

4、结果展示

后面的求取单应性矩阵以及降噪和二值化同方案一一致,在这里就不进行展示了;

结果图:

五、总结

本次项目是基于C++实现的,后续我也用Python进行了代码的转换,但在直线检测部分用相同函数却得到不同的效果,这个问题还没有进行解决;

采用C++进行编写代码,能够对整个项目的每个变量以及流程更加熟悉,本次项目可以说是一个多个知识点的集合项目,不仅仅包含边缘检测、直线检测、图像变换等,其中也有很多值得思考的小点;

后续会继续更新项目,想要源码或者有问题的可以一起探讨;

基于C++的OpenCV项目实战——文档照片转换成扫描文件相关推荐

  1. 使用poi根据模版生成word文档并转换成PDF文件(可处理doc文件与docx文件版)

    该篇文章是<使用poi根据模版生成word文档并转换成PDF文件>后续解决传入文件为doc文档或docx的处理方法 /*** 根据模板生成word** @param path 模板的路径* ...

  2. PPT文档怎么转换成PDF文件?告诉你3种好用的方法

    不知道经常需要进行演讲的小伙伴们,你们平时在制作完PPT文稿后,将文稿发送到其它设备上的时候,会不会出现格式错乱的问题呢?如果会的话也不用太过担心,我们只要将PPT文件转换为PDF格式就可以了,这是因 ...

  3. 使用poi根据模版生成word文档并转换成PDF文件

    一.首先制作word模版(这里需要注意的是文件后缀是docx不能是doc),${xxxx}是一会要替换的内容 关于为何必须是docx后缀可以看这篇文章https://www.cnblogs.com/c ...

  4. Jacob Java程序把Word文档直接转换成Html文件

    2019独角兽企业重金招聘Python工程师标准>>> Jacob是Java和Windows下的Com桥,通过它我们可以在Java程序中调用COM组件.如果你的JDK是1.4,那你需 ...

  5. 手机word文档怎么转换成pdf?分享两种方法

    手机word文档怎么转换成pdf?在如今信息化的时代,电子文档已经成为人们日常办公不可或缺的一部分.随着科技的不断进步,电子文档的格式也在不断发展.PDF作为电子文档的一种重要格式,被广泛使用.那么, ...

  6. 如何把caj文档免费转换成Word格式

    现在知网下载的文档基本都是caj格式的文档,我们会发现,这种格式的文档不容易进行编辑,就算在caj里面内容识别也只能识别有限的部分,那么如何能够比较方便的讲caj转换Word文档进行编辑呢?目前市面上 ...

  7. 【实战】OpenCV+Python项目实战--文档扫描OCR识别

    文章目录 1 准备工作(python) 1.1 np.diff用法 1.2 tesseract和pytesseract安装 2代码实现 2.1 文档提取与摆正 2.2 OCR扫描 1 准备工作(pyt ...

  8. OpenCV OCR实战 文档扫描与文字检测

    本文讲述使用OpenCV- python以及easyocr库实现文档扫描与文字检测的思路和具体实现过程. 目录 知识准备 项目概述 实现过程 代码讲解 1.读入图片并进行预处理(灰度转换,高斯滤波) ...

  9. 项目实战——文档扫描OCR识别

    扫描全能王的实现,maybe 目录 一.文档扫描 1.引入所需要的库 2.图像的读取与预处理 读取图像 图像reszie, 图像灰度化.滤波.边缘检测. 3.轮廓检测 4.透视与二值变换 二.文字识别 ...

最新文章

  1. 解决org.apache.hadoop.io.nativeio.NativeIOException: 当文件已存在时,无法创建该文件。
  2. axios vue 加载效果动画_vue+axios+element ui 实现全局loading加载示例
  3. python自动化是什么意思_python appium自动化是什么?
  4. 【Latex】分数写法区别
  5. launchpad不用图标_Launchpad Manager,一款非常方便的启动台图标管理工具
  6. 吴恩达机器学习学习笔记第一章:绪论初识机器学习
  7. python的any函数_Python any()函数
  8. 电赛机器视觉——激光点定位
  9. 高通模式9008模式linux,高通芯片如何进入9008模式深度救砖
  10. 我的实用小软件(持续更新)
  11. 前端vs图片:2 图片深度、图片分类等基本信息
  12. codevs 切糕 网络流
  13. 在bandwagon上架设web服务器
  14. Codeforces869B The Eternal Immortality
  15. jvm调优二:jvm内存模型剖析和参数设置
  16. 纯静态网页设计鞋服包包 鞋子 童装 服装网店商城html模板.rar(含源码+论文)
  17. 从《波斯语课》电影,思考当下紧张的形势,该如何准备面试?
  18. hydra和medusa使用教程
  19. (LeetCode)Java 求解无重复字符的最长子串
  20. Ext2文件系统—文件读写

热门文章

  1. 附注:友链检测限制检测前100条数据
  2. ListView Item侧滑菜单
  3. loki 日志管理的安装部署使用
  4. Windows系统资源管理器无法启动的问题
  5. horizo虚拟机_您的PC能否运行Horizo​​n零黎明:完整版?
  6. UART串口初始化波特率9600,8N1,发送o,k两个字符,画出通信协议,课程归纳
  7. 华为mate40pro和小米10pro的区别
  8. 关于使用java程序从excel或者数据库中取出建表规则数据,自动生成数据库建表语句(适用于批量建数据库表)
  9. Qt信号发送过快,槽函数处理不过来解决方法
  10. oracle 磁盘io满了,一次磁盘IO高的处理