当前位置 : 主页 > 编程语言 > c语言 >

C++OpenCV实战之文档照片转换成扫描文件

来源:互联网 收集:自由互联 发布时间:2023-02-01
目录 一、背景 二、基础知识 三、方案一:自动检测点 1、读取图片文件(进行了指定尺寸缩放) 2、创建直线类并计算两条直线的交点 3、图像边缘检测Canny 4、通过霍夫变换进行直线检
目录
  • 一、背景
  • 二、基础知识
  • 三、方案一:自动检测点
    • 1、读取图片文件(进行了指定尺寸缩放)
    • 2、创建直线类并计算两条直线的交点
    • 3、图像边缘检测Canny
    • 4、通过霍夫变换进行直线检测
    • 5、求单应性矩阵
    • 6、降噪和二值化
  • 四、方案二:用户点选目标区域
    • 1、命令行解析
    • 2、鼠标事件
    • 3、主函数实现
    • 4、结果展示
  • 五、总结

    一、背景

    前段时间都是基于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、创建直线类并计算两条直线的交点

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

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

    可以参考这个网址中的数学公式: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实战之文档照片转换成扫描文件的详细内容,更多关于C++ OpenCV照片转扫描文件的资料请关注自由互联其它相关文章!

    上一篇:Matlab实现绘制雷达图(蜘蛛图)
    下一篇:没有了
    网友评论