zoukankan      html  css  js  c++  java
  • OpenCV 图像矫正技术深入探讨

    刚进入实验室导师就交给我一个任务,就是让我设计算法给图像进行矫正。哎呀,我不太会图像这块啊,不过还是接下来了,硬着头皮开干吧!

    那什么是图像的矫正呢?举个例子就好明白了。

    我的好朋友小明给我拍了这几张照片,因为他的拍照技术不咋地,照片都拍得歪歪扭扭的,比如下面这些照片:

    人民币

    发票

    文本

    这些图片让人看得真不舒服!看个图片还要歪脖子看,实在是太烦人了!我叫小明帮我扫描一下一本教科书,小明把每一页书都拍成上面的文本那样了。好气啊那该怎么办呢?一页一页用PS来处理?1000页的矫正啊,当然交给计算机去做!

    真的,对于图像矫正的问题,在图像处理领域还真得多,比如人民币的矫正、文本的矫正、车牌的矫正、身份证矫正等等。这些都是因为拍摄者总不可能100%正确地拍摄好图片,这就要求我们通过后期的图像处理技术将图片还原好,才能进一步做后面的处理,比如数字分割啊数字识别啊,不然歪歪扭扭的文字数字,想识别出来估计就很难了。

    上面几个图,我们在日常生活中遇到的可不少,因为拍摄时拍的不好,导致拍出来的图片歪歪扭扭的,很不自然,那么我们能不能把这些图片尽可能地矫正过来呢?

    OpenCV告诉我们,没问题!工具我给你,算法你自己设计!

    比如图一,我要想将人民币矫正,并且把人民币整个抠出来保存,该怎么做?那就涉及到了图像的矫正和感兴趣区域提取两大技术了。

    总的来说,要进行进行图像矫正,至少有以下几项知识储备:

    • 轮廓提取技术
    • 霍夫变换知识
    • ROI感兴趣区域知识

    下面以人民币矫正、发票矫正、文本矫正为例,一步步剖析如何实现图像矫正。

    首先分析如何矫正人民币。

    比如我们要矫正这张人民币,思路应该是怎么样?

    首先分析这张图的特点。

    在这张图里,人民币有一定的倾斜角度,但是角度不大;人民币的背景是黑色的,而且人民币的边缘应该比较明显。

    没错,我们就抓住人民币的的边缘比较明显来做文章!我们是不是可以先把人民币的轮廓找出来(找出来的轮廓当然就是一个大大的矩形),然后用矩形去包围它,得到他的旋转角度,然后根据得到的角度进行旋转,那样不就可以实现矫正了吗!

    再详细地总结处理步骤:

    1. 图片灰度化
    2. 阈值二值化
    3. 检测轮廓
    4. 寻找轮廓的包围矩阵,并且获取角度
    5. 根据角度进行旋转矫正
    6. 对旋转后的图像进行轮廓提取
    7. 对轮廓内的图像区域抠出来,成为一张独立图像

    我把该矫正算法命名为基于轮廓提取的矫正算法,因为其关键技术就是通过轮廓来获取旋转角度。

      1 #include "opencv2/imgproc.hpp"
      2 #include "opencv2/highgui.hpp"
      3 #include <iostream>
      4 using namespace cv;
      5 using namespace std;
      6 
      7 //第一个参数:输入图片名称;第二个参数:输出图片名称
      8 void GetContoursPic(const char* pSrcFileName, const char* pDstFileName)
      9 {
     10     Mat srcImg = imread(pSrcFileName);
     11     imshow("原始图", srcImg);
     12     Mat gray, binImg;
     13     //灰度化
     14     cvtColor(srcImg, gray, COLOR_RGB2GRAY);
     15     imshow("灰度图", gray);
     16     //二值化
     17     threshold(gray, binImg, 100, 200, CV_THRESH_BINARY);
     18     imshow("二值化", binImg);
     19 
     20     vector<vector<Point> > contours;
     21     vector<Rect> boundRect(contours.size());
     22     //注意第5个参数为CV_RETR_EXTERNAL,只检索外框  
     23     findContours(binImg, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); //找轮廓
     24     cout << contours.size() << endl;
     25     for (int i = 0; i < contours.size(); i++)
     26     {
     27         //需要获取的坐标  
     28         CvPoint2D32f rectpoint[4];
     29         CvBox2D rect =minAreaRect(Mat(contours[i]));
     30 
     31         cvBoxPoints(rect, rectpoint); //获取4个顶点坐标  
     32         //与水平线的角度  
     33         float angle = rect.angle;
     34         cout << angle << endl;
     35 
     36         int line1 = sqrt((rectpoint[1].y - rectpoint[0].y)*(rectpoint[1].y - rectpoint[0].y) + (rectpoint[1].x - rectpoint[0].x)*(rectpoint[1].x - rectpoint[0].x));
     37         int line2 = sqrt((rectpoint[3].y - rectpoint[0].y)*(rectpoint[3].y - rectpoint[0].y) + (rectpoint[3].x - rectpoint[0].x)*(rectpoint[3].x - rectpoint[0].x));
     38         //rectangle(binImg, rectpoint[0], rectpoint[3], Scalar(255), 2);
     39         //面积太小的直接pass
     40         if (line1 * line2 < 600)
     41         {
     42             continue;
     43         }
     44 
     45         //为了让正方形横着放,所以旋转角度是不一样的。竖放的,给他加90度,翻过来  
     46         if (line1 > line2) 
     47         {
     48             angle = 90 + angle;
     49         }
     50 
     51         //新建一个感兴趣的区域图,大小跟原图一样大  
     52         Mat RoiSrcImg(srcImg.rows, srcImg.cols, CV_8UC3); //注意这里必须选CV_8UC3
     53         RoiSrcImg.setTo(0); //颜色都设置为黑色  
     54         //imshow("新建的ROI", RoiSrcImg);
     55         //对得到的轮廓填充一下  
     56         drawContours(binImg, contours, -1, Scalar(255),CV_FILLED);
     57 
     58         //抠图到RoiSrcImg
     59         srcImg.copyTo(RoiSrcImg, binImg);
     60 
     61 
     62         //再显示一下看看,除了感兴趣的区域,其他部分都是黑色的了  
     63         namedWindow("RoiSrcImg", 1);
     64         imshow("RoiSrcImg", RoiSrcImg);
     65 
     66         //创建一个旋转后的图像  
     67         Mat RatationedImg(RoiSrcImg.rows, RoiSrcImg.cols, CV_8UC1);
     68         RatationedImg.setTo(0);
     69         //对RoiSrcImg进行旋转  
     70         Point2f center = rect.center;  //中心点  
     71         Mat M2 = getRotationMatrix2D(center, angle, 1);//计算旋转加缩放的变换矩阵 
     72         warpAffine(RoiSrcImg, RatationedImg, M2, RoiSrcImg.size(),1, 0, Scalar(0));//仿射变换 
     73         imshow("旋转之后", RatationedImg);
     74         imwrite("r.jpg", RatationedImg); //将矫正后的图片保存下来
     75     }
     76 
     77 #if 1
     78     //对ROI区域进行抠图
     79 
     80     //对旋转后的图片进行轮廓提取  
     81     vector<vector<Point> > contours2;
     82     Mat raw = imread("r.jpg");
     83     Mat SecondFindImg;
     84     //SecondFindImg.setTo(0);
     85     cvtColor(raw, SecondFindImg, COLOR_BGR2GRAY);  //灰度化  
     86     threshold(SecondFindImg, SecondFindImg, 80, 200, CV_THRESH_BINARY);
     87     findContours(SecondFindImg, contours2, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
     88     //cout << "sec contour:" << contours2.size() << endl;
     89 
     90     for (int j = 0; j < contours2.size(); j++)
     91     {
     92         //这时候其实就是一个长方形了,所以获取rect  
     93         Rect rect = boundingRect(Mat(contours2[j]));
     94         //面积太小的轮廓直接pass,通过设置过滤面积大小,可以保证只拿到外框
     95         if (rect.area() < 600)
     96         {
     97             continue;
     98         }
     99         Mat dstImg = raw(rect);
    100         imshow("dst", dstImg);
    101         imwrite(pDstFileName, dstImg);
    102     }
    103 #endif
    104 
    105 
    106 }
    107 
    108 
    109 void main()
    110 {
    111     GetContoursPic("6.jpg", "FinalImage.jpg");
    112     waitKey();
    113 }

    效果依次如下:
    原始图

    二值化图

    掩膜mask是这样的

    旋转矫正之后

    将人民币区域抠出来

    该算法的效果还是很不错的!那赶紧试试其他图片,我把倾斜的发票图像拿去试试。

    原始图

    倾斜矫正之后

    最后把目标区域抠出来,成为单独的照片。

    上面的算法可以很好的处理人民币和发票两种情况的倾斜矫正,那文本矫正可以吗?我赶紧试了一下,结果是失败的。

    原图

    算法矫正后,还是原样,矫正失败。

    认真分析一下,还是很容易看出文本矫正失败的原因的。

    原因就在于,人民币图像和发票图像他们有明显的的边界轮廓,而文本图像没有。文本图像的背景是白色的,所以我们没有办法像人民币发票那类有明显边界的矩形物体那样,提取出轮廓并旋转矫正。

    经过深入分析可以看出,虽然文本类图像没有明显的边缘轮廓,但是他们有一个很重要的特征,那就是每一行文字都是呈现一条直线形状,而且这些直线都是平行的!

    对于这种情况,我想到了另一种方法:基于直线探测的矫正算法

    首先介绍一下我的算法思路:

    1. 用霍夫线变换探测出图像中的所有直线
    2. 计算出每条直线的倾斜角,求他们的平均值
    3. 根据倾斜角旋转矫正
    4. 最后根据文本尺寸裁剪图片

    然后给出OpenCV的实现算法:

      1 #include "opencv2/imgproc.hpp"
      2 #include "opencv2/highgui.hpp"
      3 #include <iostream>
      4 using namespace cv;
      5 using namespace std;
      6 
      7 #define ERROR 1234
      8 
      9 //度数转换
     10 double DegreeTrans(double theta)
     11 {
     12     double res = theta / CV_PI * 180;
     13     return res;
     14 }
     15 
     16 
     17 //逆时针旋转图像degree角度(原尺寸)    
     18 void rotateImage(Mat src, Mat& img_rotate, double degree)
     19 {
     20     //旋转中心为图像中心    
     21     Point2f center;
     22     center.x = float(src.cols / 2.0);
     23     center.y = float(src.rows / 2.0);
     24     int length = 0;
     25     length = sqrt(src.cols*src.cols + src.rows*src.rows);
     26     //计算二维旋转的仿射变换矩阵  
     27     Mat M = getRotationMatrix2D(center, degree, 1);
     28     warpAffine(src, img_rotate, M, Size(length, length), 1, 0, Scalar(255,255,255));//仿射变换,背景色填充为白色  
     29 }
     30 
     31 //通过霍夫变换计算角度
     32 double CalcDegree(const Mat &srcImage, Mat &dst)
     33 {
     34     Mat midImage, dstImage;
     35 
     36     Canny(srcImage, midImage, 50, 200, 3);
     37     cvtColor(midImage, dstImage, CV_GRAY2BGR);
     38 
     39     //通过霍夫变换检测直线
     40     vector<Vec2f> lines;
     41     HoughLines(midImage, lines, 1, CV_PI / 180, 300, 0, 0);//第5个参数就是阈值,阈值越大,检测精度越高
     42     //cout << lines.size() << endl;
     43 
     44     //由于图像不同,阈值不好设定,因为阈值设定过高导致无法检测直线,阈值过低直线太多,速度很慢
     45     //所以根据阈值由大到小设置了三个阈值,如果经过大量试验后,可以固定一个适合的阈值。
     46 
     47     if (!lines.size())
     48     {
     49         HoughLines(midImage, lines, 1, CV_PI / 180, 200, 0, 0);
     50     }
     51     //cout << lines.size() << endl;
     52 
     53     if (!lines.size())
     54     {
     55         HoughLines(midImage, lines, 1, CV_PI / 180, 150, 0, 0);
     56     }
     57     //cout << lines.size() << endl;
     58     if (!lines.size())
     59     {
     60         cout << "没有检测到直线!" << endl;
     61         return ERROR;
     62     }
     63 
     64     float sum = 0;
     65     //依次画出每条线段
     66     for (size_t i = 0; i < lines.size(); i++)
     67     {
     68         float rho = lines[i][0];
     69         float theta = lines[i][1];
     70         Point pt1, pt2;
     71         //cout << theta << endl;
     72         double a = cos(theta), b = sin(theta);
     73         double x0 = a*rho, y0 = b*rho;
     74         pt1.x = cvRound(x0 + 1000 * (-b));
     75         pt1.y = cvRound(y0 + 1000 * (a));
     76         pt2.x = cvRound(x0 - 1000 * (-b));
     77         pt2.y = cvRound(y0 - 1000 * (a));
     78         //只选角度最小的作为旋转角度
     79         sum += theta;
     80 
     81         line(dstImage, pt1, pt2, Scalar(55, 100, 195), 1, LINE_AA); //Scalar函数用于调节线段颜色
     82 
     83         imshow("直线探测效果图", dstImage);
     84     }
     85     float average = sum / lines.size(); //对所有角度求平均,这样做旋转效果会更好
     86 
     87     cout << "average theta:" << average << endl;
     88 
     89     double angle = DegreeTrans(average) - 90;
     90 
     91     rotateImage(dstImage, dst, angle);
     92     //imshow("直线探测效果图2", dstImage);
     93     return angle;
     94 }
     95 
     96 
     97 void ImageRecify(const char* pInFileName, const char* pOutFileName)
     98 {
     99     double degree;
    100     Mat src = imread(pInFileName);
    101     imshow("原始图", src);
    102     Mat dst;
    103     //倾斜角度矫正
    104     degree = CalcDegree(src,dst);
    105     if (degree == ERROR)
    106     {
    107         cout << "矫正失败!" << endl;
    108         return;
    109     }
    110     rotateImage(src, dst, degree);
    111     cout << "angle:" << degree << endl;
    112     imshow("旋转调整后", dst);
    113 
    114     Mat resulyImage = dst(Rect(0, 0, dst.cols, 500)); //根据先验知识,估计好文本的长宽,再裁剪下来
    115     imshow("裁剪之后", resulyImage);
    116     imwrite("recified.jpg", resulyImage); 
    117 }
    118 
    119 
    120 int main()
    121 {
    122     ImageRecify("correct2.jpg", "FinalImage.jpg");
    123     waitKey();
    124     return 0;
    125 }

    看看效果。这是原始图

    直线探测的效果。

    矫正之后的效果。

    我们发现矫正之后的图像有较多留白,影响观看,所以需要进一步裁剪,保留文字区域。

    赶紧再试多一张。

    原始图

    直线探测

    矫正效果

    进一步裁剪

    可以看出,基于直线探测的矫正算法在文本处理上效果真的很不错!

    最后总结一下两个算法的应用场景:

      • 基于轮廓提取的矫正算法更适用于车牌、身份证、人民币、书本、发票一类矩形形状而且边界明显的物体矫正。

      • 基于直线探测的矫正算法更适用于文本类的矫正。

  • 相关阅读:
    超出宽度...显示
    javascript 浏览器兼容 统计输入字数
    javascript常用工具类
    全局inline-block
    如何设计一套通用消息交互系统
    浅谈.NETfarmwork
    windows 安装gitlab
    阿里云ACP 云计算专业考试分享 2021最新
    Linux将一般用户加入sudo组
    RabbitMQ基础学习
  • 原文地址:https://www.cnblogs.com/ybqjymy/p/14177331.html
Copyright © 2011-2022 走看看