1. 程式人生 > >OpenCV探索之路(十六):圖像矯正技術深入探討

OpenCV探索之路(十六):圖像矯正技術深入探討

double gb2 教科書 長方形 strong fine open lines 導致

剛進入實驗室導師就交給我一個任務,就是讓我設計算法給圖像進行矯正。哎呀,我不太會圖像這塊啊,不過還是接下來了,硬著頭皮開幹吧!

那什麽是圖像的矯正呢?舉個例子就好明白了。

我的好朋友小明給我拍了這幾張照片,因為他的拍照技術不咋地,照片都拍得歪歪扭扭的,比如下面這些照片:

人民幣
技術分享

發票
技術分享

文本
技術分享

這些圖片讓人看得真不舒服!看個圖片還要歪脖子看,實在是太煩人了!我叫小明幫我掃描一下一本教科書,小明把每一頁書都拍成上面的文本那樣了。好氣啊那該怎麽辦呢?一頁一頁用PS來處理?1000頁的矯正啊,當然交給計算機去做!

真的,對於圖像矯正的問題,在圖像處理領域還真得多,比如人民幣的矯正、文本的矯正、車牌的矯正、身份證矯正等等。這些都是因為拍攝者總不可能100%正確地拍攝好圖片,這就要求我們通過後期的圖像處理技術將圖片還原好,才能進一步做後面的處理,比如數字分割啊數字識別啊,不然歪歪扭扭的文字數字,想識別出來估計就很難了。

上面幾個圖,我們在日常生活中遇到的可不少,因為拍攝時拍的不好,導致拍出來的圖片歪歪扭扭的,很不自然,那麽我們能不能把這些圖片盡可能地矯正過來呢?

OpenCV告訴我們,沒問題!工具我給你,算法你自己設計!

比如圖一,我要想將人民幣矯正,並且把人民幣整個摳出來保存,該怎麽做?那就涉及到了圖像的矯正和感興趣區域提取兩大技術了。

總的來說,要進行進行圖像矯正,至少有以下幾項知識儲備:

  • 輪廓提取技術
  • 霍夫變換知識
  • ROI感興趣區域知識

下面以人民幣矯正、發票矯正、文本矯正為例,一步步剖析如何實現圖像矯正。

首先分析如何矯正人民幣。

比如我們要矯正這張人民幣,思路應該是怎麽樣?
技術分享

首先分析這張圖的特點。

在這張圖裏,人民幣有一定的傾斜角度,但是角度不大;人民幣的背景是黑色的,而且人民幣的邊緣應該比較明顯。

沒錯,我們就抓住人民幣的的邊緣比較明顯來做文章!我們是不是可以先把人民幣的輪廓找出來(找出來的輪廓當然就是一個大大的矩形),然後用矩形去包圍它,得到他的旋轉角度,然後根據得到的角度進行旋轉,那樣不就可以實現矯正了嗎!

再詳細地總結處理步驟:

  1. 圖片灰度化
  2. 閾值二值化
  3. 檢測輪廓
  4. 尋找輪廓的包圍矩陣,並且獲取角度
  5. 根據角度進行旋轉矯正
  6. 對旋轉後的圖像進行輪廓提取
  7. 對輪廓內的圖像區域摳出來,成為一張獨立圖像

我把該矯正算法命名為基於輪廓提取的矯正算法,因為其關鍵技術就是通過輪廓來獲取旋轉角度。

#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
using namespace cv;
using namespace std;

//第一個參數:輸入圖片名稱;第二個參數:輸出圖片名稱
void GetContoursPic(const char* pSrcFileName, const char* pDstFileName)
{
    Mat srcImg = imread(pSrcFileName);
    imshow("原始圖", srcImg);
    Mat gray, binImg;
    //灰度化
    cvtColor(srcImg, gray, COLOR_RGB2GRAY);
    imshow("灰度圖", gray);
    //二值化
    threshold(gray, binImg, 100, 200, CV_THRESH_BINARY);
    imshow("二值化", binImg);

    vector<vector<Point> > contours;
    vector<Rect> boundRect(contours.size());
    //註意第5個參數為CV_RETR_EXTERNAL,只檢索外框  
    findContours(binImg, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); //找輪廓
    cout << contours.size() << endl;
    for (int i = 0; i < contours.size(); i++)
    {
        //需要獲取的坐標  
        CvPoint2D32f rectpoint[4];
        CvBox2D rect =minAreaRect(Mat(contours[i]));

        cvBoxPoints(rect, rectpoint); //獲取4個頂點坐標  
        //與水平線的角度  
        float angle = rect.angle;
        cout << angle << endl;

        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));
        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));
        //rectangle(binImg, rectpoint[0], rectpoint[3], Scalar(255), 2);
        //面積太小的直接pass
        if (line1 * line2 < 600)
        {
            continue;
        }

        //為了讓正方形橫著放,所以旋轉角度是不一樣的。豎放的,給他加90度,翻過來  
        if (line1 > line2) 
        {
            angle = 90 + angle;
        }

        //新建一個感興趣的區域圖,大小跟原圖一樣大  
        Mat RoiSrcImg(srcImg.rows, srcImg.cols, CV_8UC3); //註意這裏必須選CV_8UC3
        RoiSrcImg.setTo(0); //顏色都設置為黑色  
        //imshow("新建的ROI", RoiSrcImg);
        //對得到的輪廓填充一下  
        drawContours(binImg, contours, -1, Scalar(255),CV_FILLED);

        //摳圖到RoiSrcImg
        srcImg.copyTo(RoiSrcImg, binImg);


        //再顯示一下看看,除了感興趣的區域,其他部分都是黑色的了  
        namedWindow("RoiSrcImg", 1);
        imshow("RoiSrcImg", RoiSrcImg);

        //創建一個旋轉後的圖像  
        Mat RatationedImg(RoiSrcImg.rows, RoiSrcImg.cols, CV_8UC1);
        RatationedImg.setTo(0);
        //對RoiSrcImg進行旋轉  
        Point2f center = rect.center;  //中心點  
        Mat M2 = getRotationMatrix2D(center, angle, 1);//計算旋轉加縮放的變換矩陣 
        warpAffine(RoiSrcImg, RatationedImg, M2, RoiSrcImg.size(),1, 0, Scalar(0));//仿射變換 
        imshow("旋轉之後", RatationedImg);
        imwrite("r.jpg", RatationedImg); //將矯正後的圖片保存下來
    }

#if 1
    //對ROI區域進行摳圖

    //對旋轉後的圖片進行輪廓提取  
    vector<vector<Point> > contours2;
    Mat raw = imread("r.jpg");
    Mat SecondFindImg;
    //SecondFindImg.setTo(0);
    cvtColor(raw, SecondFindImg, COLOR_BGR2GRAY);  //灰度化  
    threshold(SecondFindImg, SecondFindImg, 80, 200, CV_THRESH_BINARY);
    findContours(SecondFindImg, contours2, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
    //cout << "sec contour:" << contours2.size() << endl;

    for (int j = 0; j < contours2.size(); j++)
    {
        //這時候其實就是一個長方形了,所以獲取rect  
        Rect rect = boundingRect(Mat(contours2[j]));
        //面積太小的輪廓直接pass,通過設置過濾面積大小,可以保證只拿到外框
        if (rect.area() < 600)
        {
            continue;
        }
        Mat dstImg = raw(rect);
        imshow("dst", dstImg);
        imwrite(pDstFileName, dstImg);
    }
#endif


}


void main()
{
    GetContoursPic("6.jpg", "FinalImage.jpg");
    waitKey();
}

效果依次如下:
原始圖
技術分享

二值化圖
技術分享

掩膜mask是這樣的
技術分享

旋轉矯正之後
技術分享

將人民幣區域摳出來
技術分享

該算法的效果還是很不錯的!那趕緊試試其他圖片,我把傾斜的發票圖像拿去試試。

原始圖
技術分享

傾斜矯正之後
技術分享

最後把目標區域摳出來,成為單獨的照片。
技術分享

上面的算法可以很好的處理人民幣和發票兩種情況的傾斜矯正,那文本矯正可以嗎?我趕緊試了一下,結果是失敗的。

原圖
技術分享

算法矯正後,還是原樣,矯正失敗。
技術分享

認真分析一下,還是很容易看出文本矯正失敗的原因的。

原因就在於,人民幣圖像和發票圖像他們有明顯的的邊界輪廓,而文本圖像沒有。文本圖像的背景是白色的,所以我們沒有辦法像人民幣發票那類有明顯邊界的矩形物體那樣,提取出輪廓並旋轉矯正。

經過深入分析可以看出,雖然文本類圖像沒有明顯的邊緣輪廓,但是他們有一個很重要的特征,那就是每一行文字都是呈現一條直線形狀,而且這些直線都是平行的!

對於這種情況,我想到了另一種方法:基於直線探測的矯正算法

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

  1. 用霍夫線變換探測出圖像中的所有直線
  2. 計算出每條直線的傾斜角,求他們的平均值
  3. 根據傾斜角旋轉矯正
  4. 最後根據文本尺寸裁剪圖片

然後給出OpenCV的實現算法:


#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
using namespace cv;
using namespace std;

#define ERROR 1234

//度數轉換
double DegreeTrans(double theta)
{
    double res = theta / CV_PI * 180;
    return res;
}


//逆時針旋轉圖像degree角度(原尺寸)    
void rotateImage(Mat src, Mat& img_rotate, double degree)
{
    //旋轉中心為圖像中心    
    Point2f center;
    center.x = float(src.cols / 2.0);
    center.y = float(src.rows / 2.0);
    int length = 0;
    length = sqrt(src.cols*src.cols + src.rows*src.rows);
    //計算二維旋轉的仿射變換矩陣  
    Mat M = getRotationMatrix2D(center, degree, 1);
    warpAffine(src, img_rotate, M, Size(length, length), 1, 0, Scalar(255,255,255));//仿射變換,背景色填充為白色  
}

//通過霍夫變換計算角度
double CalcDegree(const Mat &srcImage, Mat &dst)
{
    Mat midImage, dstImage;

    Canny(srcImage, midImage, 50, 200, 3);
    cvtColor(midImage, dstImage, CV_GRAY2BGR);

    //通過霍夫變換檢測直線
    vector<Vec2f> lines;
    HoughLines(midImage, lines, 1, CV_PI / 180, 300, 0, 0);//第5個參數就是閾值,閾值越大,檢測精度越高
    //cout << lines.size() << endl;

    //由於圖像不同,閾值不好設定,因為閾值設定過高導致無法檢測直線,閾值過低直線太多,速度很慢
    //所以根據閾值由大到小設置了三個閾值,如果經過大量試驗後,可以固定一個適合的閾值。

    if (!lines.size())
    {
        HoughLines(midImage, lines, 1, CV_PI / 180, 200, 0, 0);
    }
    //cout << lines.size() << endl;

    if (!lines.size())
    {
        HoughLines(midImage, lines, 1, CV_PI / 180, 150, 0, 0);
    }
    //cout << lines.size() << endl;
    if (!lines.size())
    {
        cout << "沒有檢測到直線!" << endl;
        return ERROR;
    }

    float sum = 0;
    //依次畫出每條線段
    for (size_t i = 0; i < lines.size(); i++)
    {
        float rho = lines[i][0];
        float theta = lines[i][1];
        Point pt1, pt2;
        //cout << theta << endl;
        double a = cos(theta), b = sin(theta);
        double x0 = a*rho, y0 = b*rho;
        pt1.x = cvRound(x0 + 1000 * (-b));
        pt1.y = cvRound(y0 + 1000 * (a));
        pt2.x = cvRound(x0 - 1000 * (-b));
        pt2.y = cvRound(y0 - 1000 * (a));
        //只選角度最小的作為旋轉角度
        sum += theta;

        line(dstImage, pt1, pt2, Scalar(55, 100, 195), 1, LINE_AA); //Scalar函數用於調節線段顏色

        imshow("直線探測效果圖", dstImage);
    }
    float average = sum / lines.size(); //對所有角度求平均,這樣做旋轉效果會更好

    cout << "average theta:" << average << endl;

    double angle = DegreeTrans(average) - 90;

    rotateImage(dstImage, dst, angle);
    //imshow("直線探測效果圖2", dstImage);
    return angle;
}


void ImageRecify(const char* pInFileName, const char* pOutFileName)
{
    double degree;
    Mat src = imread(pInFileName);
    imshow("原始圖", src);
    Mat dst;
    //傾斜角度矯正
    degree = CalcDegree(src,dst);
    if (degree == ERROR)
    {
        cout << "矯正失敗!" << endl;
        return;
    }
    rotateImage(src, dst, degree);
    cout << "angle:" << degree << endl;
    imshow("旋轉調整後", dst);

    Mat resulyImage = dst(Rect(0, 0, dst.cols, 500)); //根據先驗知識,估計好文本的長寬,再裁剪下來
    imshow("裁剪之後", resulyImage);
    imwrite("recified.jpg", resulyImage); 
}


int main()
{
    ImageRecify("correct2.jpg", "FinalImage.jpg");
    waitKey();
    return 0;
}

看看效果。這是原始圖
技術分享

直線探測的效果。
技術分享

矯正之後的效果。
技術分享

我們發現矯正之後的圖像有較多留白,影響觀看,所以需要進一步裁剪,保留文字區域。

技術分享

趕緊再試多一張。

原始圖
技術分享

直線探測
技術分享

矯正效果
技術分享

進一步裁剪
技術分享

可以看出,基於直線探測的矯正算法在文本處理上效果真的很不錯!

最後總結一下兩個算法的應用場景:

    • 基於輪廓提取的矯正算法更適用於車牌、身份證、人民幣、書本、發票一類矩形形狀而且邊界明顯的物體矯正。

    • 基於直線探測的矯正算法更適用於文本類的矯正。

OpenCV探索之路(十六):圖像矯正技術深入探討