EasyPR--開發詳解(5)顏色定位與偏斜扭轉

 

  本篇文章介紹EasyPR裏新的定位功能:顏色定位與偏斜扭正。但願這篇文檔能夠幫助開發者與使用者更好的理解EasyPR的設計思想。html

  讓咱們先看一下示例圖片,這幅圖片中的車牌經過顏色的定位法進行定位並從偏斜的視角中扭正爲正視角(請看右圖的左上角)。git

 

圖1 新版本的定位效果  github

 

  下面內容會對這兩個特性的實現過程展開具體的介紹。首先介紹顏色定位的原理,而後是偏斜扭正的實現細節。安全

  因爲本文較長,爲方便讀者,如下是本文的目錄架構

  一.顏色定位ide

  1.1起源函數

  1.2方法測試

  1.3不足與改善網站

  二.偏斜扭正spa

  2.1分析

  2.2ROI截取

  2.3擴大化旋轉

  2.4偏斜判斷

  2.5仿射變換

  2.6總結

  三.總結

 

顏色定位

 1.起源

  在前面的介紹裏,咱們使用了Sobel查找垂直邊緣的方法,成功定位了許多車牌。可是,Sobel法最大的問題就在於面對垂直邊緣交錯的狀況下,沒法準確地定位車牌。例以下圖。爲了解決這個問題,能夠考慮使用顏色信息進行定位。

 

圖2 顏色定位與Sobel定位的比較

 

  若是將顏色定位與Sobel定位加以結合的話,可使車牌的定位準確率從75%上升到94%。

 

 2.方法

  關於顏色定位首先咱們想到的解決方案就是:利用RGB值來判斷。

  這個想法聽起來很天然:若是咱們想找出一幅圖像中的藍色部分,那麼咱們只須要檢查RGB份量(RGB份量由Red份量--紅色,Green份量--綠色,Blue份量--藍色共同組成)中的Blue份量就能夠了。通常來講,Blue份量是個0到255的值。若是咱們設定一個閾值,而且檢查每一個像素的Blue份量是否大於它,那咱們不就能夠得知這些像素是否是藍色的了麼?這個想法雖然很好,不過存在一個問題,咱們該怎麼來選擇這個閾值?這是第一個問題。

  即使咱們用一些方法決定了閾值之後,那麼下面的一個問題就會讓人抓狂,顏色是組合的,即使藍色屬性在255(這樣已經很‘藍’了吧),只要另外兩個份量配合(例如都爲255),你最後獲得的不是藍色,而是黑色。

  這還只是區分藍色的問題,黃色更麻煩,它是由紅色和綠色組合而成的,這意味着你須要考慮兩個變量的配比問題。這些問題讓選擇RGB顏色做爲判斷的難度大到難以接受的地步。所以必須另想辦法。

  爲了解決各類顏色相關的問題,人們發明了各類顏色模型。其中有一個模型,很是適合解決顏色判斷的問題。這個模型就是HSV模型。

圖3 HSV顏色模型

  HSV模型是根據顏色的直觀特性建立的一種圓錐模型。與RGB顏色模型中的每一個份量都表明一種顏色不一樣的是,HSV模型中每一個份量並不表明一種顏色,而分別是:色調(H),飽和度(S),亮度(V)。

  H份量是表明顏色特性的份量,用角度度量,取值範圍爲0~360,從紅色開始按逆時針方向計算,紅色爲0,綠色爲120,藍色爲240。S份量表明顏色的飽和信息,取值範圍爲0.0~1.0,值越大,顏色越飽和。V份量表明明暗信息,取值範圍爲0.0~1.0,值越大,色彩越明亮。

  H份量是HSV模型中惟一跟顏色本質相關的份量。只要固定了H的值,而且保持S和V份量不過小,那麼表現的顏色就會基本固定。爲了判斷藍色車牌顏色的範圍,能夠固定了S和V兩個值爲1之後,調整H的值,而後看顏色的變化範圍。經過一段摸索,能夠發現當H的取值範圍在200到280時,這些顏色均可以被認爲是藍色車牌的顏色範疇。因而咱們能夠用H份量是否在200與280之間來決定某個像素是否屬於藍色車牌。黃色車牌也是同樣的道理,經過觀察,能夠發現當H值在30到80時,顏色的值能夠做爲黃色車牌的顏色。

  這裏的顏色表來自於這個網站

  下圖顯示了藍色的H份量變化範圍。

 

圖4 藍色的H份量區間 

 

  下圖顯示了黃色的H份量變化範圍。 

 

 圖5 黃色的H份量區間  

 

  光判斷H份量的值是否就足夠了?

  事實上是不足的。固定了H的值之後,若是移動V和S會帶來顏色的飽和度和亮度的變化。當V和S都達到最高值,也就是1時,顏色是最純正的。下降S,顏色愈加趨向於變白。下降V,顏色趨向於變黑,當V爲0時,顏色變爲黑色。所以,S和V的值也會影響最終顏色的效果。

  咱們能夠設置一個閾值,假設S和V都大於閾值時,顏色才屬於H所表達的顏色。

  在EasyPR裏,這個值是0.35,也就是V屬於0.35到1且S屬於0.35到1的一個範圍,相似於一個矩形。對V和S的閾值判斷是有必要的,由於不少車牌周身的車身,都是H份量屬於200-280,而V份量或者S份量小於0.35的。經過S和V的判斷能夠排除車牌周圍車身的干擾。

 

        

圖6 V和S的區間 

 

  明確了使用HSV模型以及用閾值進行判斷之後,下面就是一個顏色定位的完整過程。

  第一步,將圖像的顏色空間從RGB轉爲HSV,在這裏因爲光照的影響,對於圖像使用直方圖均衡進行預處理;

  第二步,依次遍歷圖像的全部像素,當H值落在200-280之間而且S值與V值也落在0.35-1.0之間,標記爲白色像素,不然爲黑色像素;

  第三步,對僅有白黑兩個顏色的二值圖參照原先車牌定位中的方法,使用閉操做,取輪廓等方法將車牌的外接矩形截取出來作進一步的處理。

 

 圖7 藍色定位效果 

 

  以上就完成了一個藍色車牌的定位過程。咱們把對圖像中藍色車牌的尋找過程稱爲一次與藍色模板的匹配過程。代碼中的函數稱之爲colorMatch。通常說來,一幅圖像須要進行一次藍色模板的匹配,還要進行一次黃色模板的匹配,以此確保藍色和黃色的車牌都被定位出來。

  黃色車牌的定位方法與其相似,僅僅只是H閾值範圍的不一樣。事實上,黃色定位的效果通常好的出奇,能夠在很是複雜的環境下將車牌極爲準確的定位出來,這可能源於現實世界中黃色很是醒目的緣由。

 

  圖8 黃色定位效果 

  從實際效果來看,顏色定位的效果是很好的。在通用數據測試集裏,大約70%的車牌均可以被定位出來(一些顏色定位不了的,咱們能夠用Sobel定位處理)。

  在代碼中有些細節須要注意:

  一. opencv爲了保證HSV三個份量都落在0-255之間(確保一個char能裝的下),對H份量除以了2,也就是0-180的範圍,S和V份量乘以了255,將0-1的範圍擴展到0-255。咱們在設置閾值的時候須要參照opencv的標準,所以對參數要進行一個轉換。

  二. 是v和s取值的問題。對於暗的圖來講,取值過大容易漏,而對於亮的圖,取值太小則容易跟車身混淆。所以能夠考慮最適應的改變閾值。

  三. 是模板問題。目前的作法是針對藍色和黃色的匹配使用了兩個模板,而不是統一的模板。統一模板的問題在於擔憂藍色和黃色的干擾問題,例如黃色的車與藍色的牌的干擾,或者藍色的車和黃色牌的干擾,這裏面最典型的例子就是一個帶有藍色車牌的黃色出租車,在不少城市裏這已是「標準配置」。所以須要將藍色和黃色的匹配分別用不一樣的模板處理。

  瞭解完這三個細節之後,下面就是代碼部分。

    //! 根據一幅圖像與顏色模板獲取對應的二值圖
    //! 輸入RGB圖像, 顏色模板(藍色、黃色)
    //! 輸出灰度圖(只有0和255兩個值,255表明匹配,0表明不匹配)
    Mat colorMatch(const Mat& src, Mat& match, const Color r, const bool adaptive_minsv)
    {
        // S和V的最小值由adaptive_minsv這個bool值判斷
        // 若是爲true,則最小值取決於H值,按比例衰減
        // 若是爲false,則再也不自適應,使用固定的最小值minabs_sv
        // 默認爲false
        const float max_sv = 255;
        const float minref_sv = 64;

        const float minabs_sv = 95;

        //blue的H範圍
        const int min_blue = 100;  //100
        const int max_blue = 140;  //140

        //yellow的H範圍
        const int min_yellow = 15; //15
        const int max_yellow = 40; //40

        Mat src_hsv;
        // 轉到HSV空間進行處理,顏色搜索主要使用的是H份量進行藍色與黃色的匹配工做
        cvtColor(src, src_hsv, CV_BGR2HSV);

        vector<Mat> hsvSplit;
        split(src_hsv, hsvSplit);
        equalizeHist(hsvSplit[2], hsvSplit[2]);
        merge(hsvSplit, src_hsv);

        //匹配模板基色,切換以查找想要的基色
        int min_h = 0;
        int max_h = 0;
        switch (r) {
        case BLUE:
            min_h = min_blue;
            max_h = max_blue;
            break;
        case YELLOW:
            min_h = min_yellow;
            max_h = max_yellow;
            break;
        }

        float diff_h = float((max_h - min_h) / 2);
        int avg_h = min_h + diff_h;

        int channels = src_hsv.channels();
        int nRows = src_hsv.rows;
        //圖像數據列須要考慮通道數的影響;
        int nCols = src_hsv.cols * channels;

        if (src_hsv.isContinuous())//連續存儲的數據,按一行處理
        {
            nCols *= nRows;
            nRows = 1;
        }

        int i, j;
        uchar* p;
        float s_all = 0;
        float v_all = 0;
        float count = 0;
        for (i = 0; i < nRows; ++i)
        {
            p = src_hsv.ptr<uchar>(i);
            for (j = 0; j < nCols; j += 3)
            {
                int H = int(p[j]); //0-180
                int S = int(p[j + 1]);  //0-255
                int V = int(p[j + 2]);  //0-255

                s_all += S;
                v_all += V;
                count++;

                bool colorMatched = false;

                if (H > min_h && H < max_h)
                {
                    int Hdiff = 0;
                    if (H > avg_h)
                        Hdiff = H - avg_h;
                    else
                        Hdiff = avg_h - H;

                    float Hdiff_p = float(Hdiff) / diff_h;

                    // S和V的最小值由adaptive_minsv這個bool值判斷
                    // 若是爲true,則最小值取決於H值,按比例衰減
                    // 若是爲false,則再也不自適應,使用固定的最小值minabs_sv
                    float min_sv = 0;
                    if (true == adaptive_minsv)
                        min_sv = minref_sv - minref_sv / 2 * (1 - Hdiff_p); // inref_sv - minref_sv / 2 * (1 - Hdiff_p)
                    else
                        min_sv = minabs_sv; // add

                    if ((S > min_sv && S < max_sv) && (V > min_sv && V < max_sv))
                        colorMatched = true;
                }

                if (colorMatched == true) {
                    p[j] = 0; p[j + 1] = 0; p[j + 2] = 255;
                }
                else {
                    p[j] = 0; p[j + 1] = 0; p[j + 2] = 0;
                }
            }
        }

        //cout << "avg_s:" << s_all / count << endl;
        //cout << "avg_v:" << v_all / count << endl;

        // 獲取顏色匹配後的二值灰度圖
        Mat src_grey;
        vector<Mat> hsvSplit_done;
        split(src_hsv, hsvSplit_done);
        src_grey = hsvSplit_done[2];

        match = src_grey;

        return src_grey;
    }
View Code

  

 3.不足

  以上說明了顏色定位的設計思想與細節。那麼顏色定位是否是就是萬能的?答案是否認的。在色彩充足,光照足夠的狀況下,顏色定位的效果很好,可是在面對光線不足的狀況,或者藍色車身的狀況時,顏色定位的效果很糟糕。下圖是一輛藍色車輛,能夠看出,車牌與車身內容徹底重疊,沒法分割。

 

圖9 失效的顏色定位 

  碰到失效的顏色定位狀況時須要使用原先的Sobel定位法。

  目前的新版本使用了顏色定位與Sobel定位結合的方式。首先進行顏色定位,而後根據條件使用Sobel進行再次定位,增長整個系統的適應能力。

  爲了增強魯棒性,Sobel定位法能夠用兩階段的查找。也就是在已經被Sobel定位的圖塊中,再進行一次Sobel定位。這樣能夠增長準確率,但會下降了速度。一個折衷的方案是讓用戶決定一個參數m_maxPlates的值,這個值決定了你在一幅圖裏最多定位多少車牌。系統首先用顏色定位出候選車牌,而後經過SVM模型來判斷是不是車牌,最後統計數量。若是這個數量大於你設定的參數,則認爲車牌已經定位足夠了,不須要後一步處理,也就不會進行兩階段的Sobel查找。相反,若是這個數量不足,則繼續進行Sobel定位。

  綜合定位的代碼位於CPlateDectec中的的成員函數plateDetectDeep中,如下是plateDetectDeep的總體流程。

 圖10 綜合定位所有流程 

  有沒有顏色定位與Sobel定位都失效的狀況?有的。這種狀況下可能須要使用第三類定位技術--字符定位技術。這是EasyPR發展的一個方向,這裏不展開討論。

 

偏斜扭轉

  解決了顏色的定位問題之後,下面的問題是:在定位之後,咱們如何把偏斜過來的車牌扭正呢?

 

 

圖11 偏斜扭轉效果 

  這個過程叫作偏斜扭轉過程。其中一個關鍵函數就是opencv的仿射變換函數。但在具體實施時,有不少須要解決的問題。

 

 1.分析

  在任何新的功能開發以前,技術預研都是第一步。

  在這篇文檔介紹了opencv的仿射變換功能。效果見下圖。

圖12 仿射變換效果 

 

  仔細看下,貌似這個功能跟咱們的需求很類似。咱們的偏斜扭轉功能,說白了,就是把對圖像的觀察視角進行了一個轉換。

  不過這篇文章裏的代碼基原本自於另外一篇官方文檔。官方文檔裏還有一個例子,能夠矩形扭轉成平行四邊形。而咱們的需求正是將平行四邊形的車牌扭正成矩形。這麼說來,只要使用例子中對應的反函數,應該就能夠實現咱們的需求。從這個角度來看,偏斜扭轉功能夠實現。肯定了可行性之後,下一步就是思考如何實現。

  在原先的版本中,咱們對定位出來的區域會進行一次角度判斷,當角度小於某個閾值(默認30度)時就會進行全圖旋轉。

  這種方式有兩個問題:

  一是咱們的策略是對整幅圖像旋轉。對於opencv來講,每次旋轉操做都是一個矩形的乘法過程,對於很是大的圖像,這個過程是很是消耗計算資源的;

  二是30度的閾值沒法處理示例圖片。事實上,示例圖片的定位區域的角度是-50度左右,已經大於咱們的閾值了。爲了處理這樣的圖片,咱們須要把咱們的閾值增大,例如增長到60度,那麼這樣的結果是帶來候選區域的增多。

  兩個因素結合,會大幅度增長處理時間。爲了避免讓處理速度降低,必須想辦法規避這些影響。

  一個方法是再也不使用全圖旋轉,而是區域旋轉。其實咱們在獲取定位區域後,咱們並不須要定位區域之外的圖像。

  假若咱們能劃出一塊小的區域包圍定位區域,而後咱們僅對定位區域進行旋轉,那麼計算量就會大幅度下降。而這點,在opencv裏是能夠實現的,咱們對定位區域RotatedRect用boundingRect()方法獲取外接矩形,再使用Mat(Rect ...)方法截取這個區域圖塊,從而生成一個小的區域圖像。因而下面的全部旋轉等操做均可以基於這個區域圖像進行。

  在這些設計決定之後,下面就來思考整個功能的架構。

  咱們要解決的問題包括三類,第一類是正的車牌,第二類是傾斜的車牌,第三類是偏斜的車牌。前兩類是前面說過的,第三類是本次新增的功能需求。第二類傾斜車牌與第三類車牌的區別見下圖。

圖13 兩類不一樣的旋轉 

 

  經過上圖能夠看出,正視角的旋轉圖片的觀察角度仍然是正方向的,只是因爲路的不平或者攝像機的傾斜等緣由,致使矩形有必定傾斜。這類圖塊的特色就是在RotataedRect內部,車牌部分仍然是個矩形。偏斜視角的圖片的觀察角度是非正方向的,是從側面去看車牌。這類圖塊的特色是在RotataedRect內部,車牌部分再也不是個矩形,而是一個平行四邊形。這個特性決定了咱們須要區別的對待這兩類圖片。

  一個初步的處理思路就是下圖。

 

圖14 分析實現流程


  簡單來講,整個處理流程包括下面四步:

  1.感興趣區域的截取
  2.角度判斷
  3.偏斜判斷
  4.仿射變換 

  接下來按照這四個步驟依次介紹。

 

 2.ROI截取

  若是要使用區域旋轉,首先咱們必須從原圖中截取出一個包含定位區域的圖塊。

  opencv提供了一個從圖像中截取感興趣區域ROI的方法,也就是Mat(Rect ...)。這個方法會在Rect所在的位置,截取原圖中一個圖塊,而後將其賦值到一個新的Mat圖像裏。遺憾的是這個方法不支持RotataedRect,同時Rect與RotataedRect也沒有繼承關係。所以布不能直接調用這個方法。

  咱們可使用RotataedRect的boudingRect()方法。這個方法會返回一個RotataedRect的最小外接矩形,並且這個矩形是一個Rect。所以將這個Rect傳遞給Mat(Rect...)方法就能夠截取出原圖的ROI圖塊,並得到對應的ROI圖像。

  須要注意的是,ROI圖塊和ROI圖像的區別,當咱們給定原圖以及一個Rect時,原圖中被Rect包圍的區域稱爲ROI圖塊,此時圖塊裏的座標仍然是原圖的座標。當這個圖塊裏的內容被拷貝到一個新的Mat裏時,咱們稱這個新Mat爲ROI圖像。ROI圖像裏僅僅只包含原來圖塊裏的內容,跟原圖沒有任何關係。因此圖塊和圖像雖然顯示的內容同樣,但座標系已經發生了改變。在從ROI圖塊到ROI圖像之後,點的座標要計算一個偏移量。

  下一步的工做中能夠僅對這個ROI圖像進行處理,包括對其旋轉或者變換等操做。

  示例圖片中的截取出來的ROI圖像以下圖:

 

圖15 截取後的ROI圖像

  

  在截取中可能會發生一個問題。若是直接使用boundingRect()函數的話,在運行過程當中會常常發生這樣的異常。OpenCV Error: Assertion failed (0 <= roi.x && 0 <= roi.width && roi.x + roi.width <= m.cols && 0 <= roi.y && 0 <= roi.height && roi.y + roi.height <= m.rows) incv::Mat::Mat,以下圖。

 

圖16 不安全的外接矩形函數會拋出異常

 

  這個異常產生的緣由在於,在opencv2.4.8中(不清楚opencv其餘版本是否沒有這個問題),boundingRect()函數計算出的Rect的四個點的座標沒有作驗證。這意味着你計算一個RotataedRect的最小外接矩形Rect時,它可能會給你一個負座標,或者是一個超過原圖片外界的座標。因而當你把Rect做爲參數傳遞給Mat(Rect ...)的話,它會提示你所要截取的Rect中的座標越界了!

  解決方案是實現一個安全的計算最小外接矩形Rect的函數,在boundingRect()結果之上,對角點座標進行一次判斷,若是值爲負數,就置爲0,若是值超過了原始Mat的rows或cols,就置爲原始Mat的這些rows或cols。

  這個安全函數名爲calcSafeRect(...),下面是這個函數的代碼。

//! 計算一個安全的Rect
//! 若是不存在,返回false
bool CPlateLocate::calcSafeRect(const RotatedRect& roi_rect, const Mat& src, Rect_<float>& safeBoundRect)
{
    Rect_<float> boudRect = roi_rect.boundingRect();

    // boudRect的左上的x和y有可能小於0
    float tl_x = boudRect.x > 0 ? boudRect.x : 0;
    float tl_y = boudRect.y > 0 ? boudRect.y : 0;
    // boudRect的右下的x和y有可能大於src的範圍
    float br_x = boudRect.x + boudRect.width < src.cols ?
        boudRect.x + boudRect.width - 1 : src.cols - 1;
    float br_y = boudRect.y + boudRect.height < src.rows ?
        boudRect.y + boudRect.height - 1 : src.rows - 1;

    float roi_width = br_x - tl_x;
    float roi_height = br_y - tl_y;

    if (roi_width <= 0 || roi_height <= 0)
        return false;

    // 新建一個mat,確保地址不越界,以防mat定位roi時拋異常
    safeBoundRect = Rect_<float>(tl_x, tl_y, roi_width, roi_height);

    return true;
}
View Code

 

 3.擴大化旋轉

  好,當我經過calcSafeRect(...)獲取了一個安全的Rect,而後經過Mat(Rect ...)函數截取了這個感興趣圖像ROI之後。下面的工做就是對這個新的ROI圖像進行操做。

  首先是判斷這個ROI圖像是否要旋轉。爲了下降工做量,咱們不對角度在-5度到5度區間的ROI進行旋轉(注意這裏講的角度針對的生成ROI的RotataedRect,ROI自己是水平的)。由於這麼小的角度對於SVM判斷以及字符識別來講,都是沒有影響的。

  對其餘的角度咱們須要對ROI進行旋轉。當咱們對ROI進行旋轉之後,接着把轉正後的RotataedRect部分從ROI中截取出來。

  但很快咱們就會碰到一個新問題。讓咱們看一下下圖,爲何咱們截取出來的車牌區域最左邊的「川」字和右邊的「2」字發生了形變?爲了搞清這個緣由,做者仔細地研究了旋轉與截取函數,但很快發現了形變的根源在於旋轉後的ROI圖像。

  仔細看一下旋轉後的ROI圖像,是否左右兩側再也不完整,像是被截去了一部分?

 

圖17 旋轉後圖像被截斷 

 

  要想理解這個問題,須要理解opencv的旋轉變換函數的特性。做爲旋轉變換的核心函數,affinTransform會要求你輸出一個旋轉矩陣給它。這很簡單,由於咱們只須要給它一個旋轉中心點以及角度,它就能計算出咱們想要的旋轉矩陣。旋轉矩陣的得到是經過以下的函數獲得的:

  Mat rot_mat = getRotationMatrix2D(new_center, angle, 1);

  在獲取了旋轉矩陣rot_mat,那麼接下來就須要調用函數warpAffine來開始旋轉操做。這個函數的參數包括一個目標圖像、以及目標圖像的Size。目標圖像容易理解,大部分opencv的函數都會須要這個參數。咱們只要新建一個Mat便可。那麼目標圖像的Size是什麼?在通常的觀點中,假設咱們須要旋轉一個圖像,咱們給opencv一個原始圖像,以及我須要在某個旋轉點對它旋轉一個角度的需求,那麼opencv返回一個圖像給我便可,這個圖像的Size或者說大小應該是opencv返回給個人,爲何要我來告訴它呢?

  你能夠試着對一個正方形進行旋轉,仔細看看,這個正方形的外接矩形的大小會如何變化?當旋轉角度還小時,一切都還好,當角度變大時,明顯咱們看到的外接矩形的大小也在擴增。在這裏,外接矩形被稱爲視框,也就是我須要旋轉的正方形所須要的最小區域。隨着旋轉角度的變大,視框明顯增大。

 

圖18 矩形旋轉後所需視框增大  

 

  在圖像旋轉完之後,有三類點會得到不一樣的處理,一種是有原圖像對應點且在視框內的,這些點被正常顯示;一類是在視框內但找不到原圖像與之對應的點,這些點被置0值(顯示爲黑色);最後一類是有原圖像與之對應的點,但不在視框內的,這些點被悲慘的拋棄。

 

圖19 旋轉後三類不一樣點的命運 

  這就是旋轉後不一樣三類點的命運,也就是新生成的圖像中一些點呈現黑色(被置0),一些點被截斷(被拋棄)的緣由。若是把視框調整大點的話,就能夠大幅度減小被截斷點的數量。因此,爲了保證旋轉後的圖像不被截斷,所以咱們須要計算一個合理的目標圖像的Size,讓咱們的感興趣區域獲得完整的顯示。

  下面的代碼使用了一個極爲簡單的策略,它將原始圖像與目標圖像都進行了擴大化。首先新建一個尺寸爲原始圖像1.5倍的新圖像,接着把原始圖像映射到新圖像上,因而咱們獲得了一個顯示區域(視框)擴大化後的原始圖像。顯示區域擴大之後,那些在原圖像中沒有值的像素被置了一個初值。

  接着調用warpAffine函數,使用新圖像的大小做爲目標圖像的大小。warpAffine函數會將新圖像旋轉,並用目標圖像尺寸的視框去顯示它。因而咱們獲得了一個全部感興趣區域都被完整顯示的旋轉後圖像。

  這樣,咱們再使用getRectSubPix()函數就能夠得到想要的車牌區域了。

圖20 擴大化旋轉後圖像再也不被截斷

  如下就是旋轉函數rotation的代碼。

//! 旋轉操做
bool CPlateLocate::rotation(Mat& in, Mat& out, const Size rect_size, const Point2f center, const double angle)
{
    Mat in_large;
    in_large.create(in.rows*1.5, in.cols*1.5, in.type());

    int x = in_large.cols / 2 - center.x > 0 ? in_large.cols / 2 - center.x : 0;
    int y = in_large.rows / 2 - center.y > 0 ? in_large.rows / 2 - center.y : 0;

    int width = x + in.cols < in_large.cols ? in.cols : in_large.cols - x;
    int height = y + in.rows < in_large.rows ? in.rows : in_large.rows - y;

    /*assert(width == in.cols);
    assert(height == in.rows);*/

    if (width != in.cols || height != in.rows)
        return false;

    Mat imageRoi = in_large(Rect(x, y, width, height));
    addWeighted(imageRoi, 0, in, 1, 0, imageRoi);

    Point2f center_diff(in.cols/2, in.rows/2);
    Point2f new_center(in_large.cols / 2, in_large.rows / 2);

    Mat rot_mat = getRotationMatrix2D(new_center, angle, 1);

    /*imshow("in_copy", in_large);
    waitKey(0);*/

    Mat mat_rotated;
    warpAffine(in_large, mat_rotated, rot_mat, Size(in_large.cols, in_large.rows), CV_INTER_CUBIC);

    /*imshow("mat_rotated", mat_rotated);
    waitKey(0);*/

    Mat img_crop;
    getRectSubPix(mat_rotated, Size(rect_size.width, rect_size.height), new_center, img_crop);

    out = img_crop;

    /*imshow("img_crop", img_crop);
    waitKey(0);*/

    return true;

    
}
View Code

 

 4.偏斜判斷

  當咱們對ROI進行旋轉之後,下面一步工做就是把RotataedRect部分從ROI中截取出來,這裏可使用getRectSubPix方法,這個函數能夠在被旋轉後的圖像中截取一個正的矩形圖塊出來,並賦值到一個新的Mat中,稱爲車牌區域。

  下步工做就是分析截取後的車牌區域。車牌區域裏的車牌分爲正角度和偏斜角度兩種。對於正的角度而言,能夠看出車牌區域就是車牌,所以直接輸出便可。而對於偏斜角度而言,車牌是平行四邊形,與矩形的車牌區域不重合。

  如何判斷一個圖像中的圖形是不是平行四邊形?

  一種簡單的思路就是對圖像二值化,而後根據二值化圖像進行判斷。圖像二值化的方法有不少種,假設咱們這裏使用一開始在車牌定位功能中使用的大津閾值二值化法的話,效果不會太好。由於大津閾值是自適應閾值,在完整的圖像中二值出來的平行四邊形可能在小的局部圖像中就再也不是。最好的辦法是使用在前面定位模塊生成後的原圖的二值圖像,咱們經過一樣的操做就能夠在原圖中截取一個跟車牌區域對應的二值化圖像。

  下圖就是一個二值化車牌區域得到的過程。

圖21 二值化的車牌區域

 

  接下來就是對二值化車牌區域進行處理。爲了判斷二值化圖像中白色的部分是平行四邊形。一種簡單的作法就是從圖像中選擇一些特定的行。計算在這個行中,第一個全爲0的串的長度。從幾何意義上來看,這就是平行四邊形斜邊上某個點距離外接矩形的長度。

  假設咱們選擇的這些行位於二值化圖像高度的1/4,2/4,3/4處的話,若是是白色圖形是矩形的話,這些串的大小應該是相等或者相差很小的,相反若是是平行四邊形的話,那麼這些串的大小應該不等,而且呈現一個遞增或遞減的關係。經過這種不一樣,咱們就能夠判斷車牌區域裏的圖形,到底是矩形仍是平行四邊形。

  偏斜判斷的另外一個重要做用就是,計算平行四邊形傾斜的斜率,這個斜率值用來在下面的仿射變換中發揮做用。咱們使用一個簡單的公式去計算這個斜率,那就是利用上面判斷過程當中使用的串大小,假設二值化圖像高度的1/4,2/4,3/4處對應的串的大小分別爲len1,len2,len3,車牌區域的高度爲Height。一個計算斜率slope的計算公式就是:(len3-len1)/Height*2。

  Slope的直觀含義見下圖。

 

 

 圖22 slope的幾何含義

 

  須要說明的,這個計算結果在平行四邊形是右斜時是負值,而在左斜時則是正值。因而能夠根據slope的正負判斷平行四邊形是右斜或者左斜。在實踐中,會發生一些公式不能應對的狀況,例如像下圖這種狀況,斜邊的部分區域發生了內凹或者外凸現象。這種現象會致使len1,len2或者len3的計算有誤,所以slope也會不許。

 

圖23 內凹現象

  爲了實現一個魯棒性更好的計算方法,能夠用(len2-len1)/Height*4與(len3-len1)/Height*2二者之間更靠近tan(angle)的值做爲solpe的值(在這裏,angle表明的是原來RotataedRect的角度)。

  多采起了一個slope備選的好處是能夠避免單點的內凹或者外凸,但這仍然不是最好的解決方案。在最後的討論中會介紹一個其餘的實現思路。

  完成偏斜判斷與斜率計算的函數是isdeflection,下面是它的代碼。

//! 是否偏斜
//! 輸入二值化圖像,輸出判斷結果
bool CPlateLocate::isdeflection(const Mat& in, const double angle, double& slope)
{
    int nRows = in.rows;
    int nCols = in.cols;

    assert(in.channels() == 1);

    int comp_index[3];
    int len[3];

    comp_index[0] = nRows / 4;
    comp_index[1] = nRows / 4 * 2;
    comp_index[2] = nRows / 4 * 3;

    const uchar* p;
    
    for (int i = 0; i < 3; i++)
    {
        int index = comp_index[i];
        p = in.ptr<uchar>(index);

        int j = 0;
        int value = 0;
        while (0 == value && j < nCols)
            value = int(p[j++]);

        len[i] = j;
    }

    //cout << "len[0]:" << len[0] << endl;
    //cout << "len[1]:" << len[1] << endl;
    //cout << "len[2]:" << len[2] << endl;
    
    double maxlen = max(len[2], len[0]);
    double minlen = min(len[2], len[0]);
    double difflen = abs(len[2] - len[0]);
    //cout << "nCols:" << nCols << endl;

    double PI = 3.14159265;
    double g = tan(angle * PI / 180.0);

    if (maxlen - len[1] > nCols/32 || len[1] - minlen > nCols/32 ) {
        // 若是斜率爲正,則底部在下,反之在上
        double slope_can_1 = double(len[2] - len[0]) / double(comp_index[1]);
        double slope_can_2 = double(len[1] - len[0]) / double(comp_index[0]);
        double slope_can_3 = double(len[2] - len[1]) / double(comp_index[0]);

        /*cout << "slope_can_1:" << slope_can_1 << endl;
        cout << "slope_can_2:" << slope_can_2 << endl;
        cout << "slope_can_3:" << slope_can_3 << endl;*/
 
        slope = abs(slope_can_1 - g) <= abs(slope_can_2 - g) ? slope_can_1 : slope_can_2;

        /*slope = max(  double(len[2] - len[0]) / double(comp_index[1]),
            double(len[1] - len[0]) / double(comp_index[0]));*/
        
        //cout << "slope:" << slope << endl;
        return true;
    }
    else {
        slope = 0;
    }

    return false;
}
View Code

 

 5.仿射變換

  俗話說:行百里者半九十。前面已經作了如此多的工做,應該能夠實現偏斜扭轉功能了吧?但在最後的道路中,仍然有問題等着咱們。

  咱們已經實現了旋轉功能,而且在旋轉後的區域中截取了車牌區域,而後判斷車牌區域中的圖形是一個平行四邊形。下面要作的工做就是把平行四邊形扭正成一個矩形。

圖24 從平行四邊形車牌到矩形車牌

 

  首先第一個問題就是解決如何從平行四邊形變換成一個矩形的問題。opencv提供了一個函數warpAffine,就是仿射變換函數。注意,warpAffine不只可讓圖像旋轉(前面介紹過),也能夠進行仿射變換,真是一個多才多藝的函數。o

  經過仿射變換函數能夠把任意的矩形拉伸成其餘的平行四邊形。opencv的官方文檔裏給了一個示例,值得注意的是,這個示例演示的是把矩形變換爲平行四邊形,跟咱們想要的偏偏相反。但不要緊,咱們先看一下它的使用方法。

 

圖25 opencv官網上對warpAffine使用的示例

 

  warpAffine方法要求輸入的參數是原始圖像的左上點,右上點,左下點,以及輸出圖像的左上點,右上點,左下點。注意,必須保證這些點的對應順序,不然仿射的效果跟你預想的不同。經過這個方法介紹,咱們能夠大概看出,opencv須要的是三個點對(共六個點)的座標,而後創建一個映射關係,經過這個映射關係將原始圖像的全部點映射到目標圖像上。 

 

圖26 warpAffine須要的三個對應座標點

 

  再回來看一下咱們的需求,咱們的目標是把車牌區域中的平行四邊形映射爲一個矩形。讓咱們作個假設,若是咱們選取了車牌區域中的平行四邊形車牌的三個關鍵點,而後再肯定了咱們但願將車牌扭正成的矩形的三個關鍵點的話,咱們是否就能夠實現從平行四邊形車牌到矩形車牌的扭正?

  讓咱們畫一幅圖像來看看這個變換的做用。有趣的是,把一個平行四邊形變換爲矩形會對包圍平行四邊形車牌的區域帶來影響。

  例以下圖中,藍色的實線表明扭轉前的平行四邊形車牌,虛線表明扭轉後的。黑色的實線表明矩形的車牌區域,虛線表明扭轉後的效果。能夠看到,當藍色車牌被扭轉爲矩形的同時,黑色車牌區域則被扭轉爲平行四邊形。

  注意,當車牌區域扭變爲平行四邊形之後,須要顯示它的視框增大了。跟咱們在旋轉圖像時碰到的情形同樣。

 

 圖27 平行四邊形的扭轉帶來的變化

 

  讓咱們先實際嘗試一下仿射變換吧。

  根據仿射函數的須要,咱們計算平行四邊形車牌的三個關鍵點座標。其中左上點的值(xdiff,0)中的xdiff就是根據車牌區域的高度height與平行四邊形的斜率slope計算獲得的:

xidff = Height * abs(slope)

  

  爲了計算目標矩形的三個關鍵點座標,咱們首先須要把扭轉後的原點座標調整到平行四邊形車牌區域左上角位置。見下圖。

 

圖28 原圖像的座標計算

  依次推算關鍵點的三個座標。它們應該是

        plTri[0] = Point2f(0 + xiff, 0);
        plTri[1] = Point2f(width - 1, 0);
        plTri[2] = Point2f(0, height - 1);

        dstTri[0] = Point2f(xiff, 0);
        dstTri[1] = Point2f(width - 1, 0);
        dstTri[2] = Point2f(xiff, height - 1);

  

  根據上圖的座標,咱們開始進行一次仿射變換的嘗試。

  opencv的warpAffine函數不會改變變換後圖像的大小。而咱們給它傳遞的目標圖像的大小僅會決定視框的大小。不過此次咱們不用擔憂視框的大小,由於根據圖27看來,哪怕視框跟原始圖像同樣大,咱們也足夠顯示扭正後的車牌。

  看看仿射的效果。暈,好像效果不對,視框的大小是足夠了,可是圖像往右偏了一些,致使最右邊的字母沒有顯示全。

 

圖29 被偏移的車牌區域

 

  此次的問題再也不是目標圖像的大小問題了,而是視框的偏移問題。仔細觀察一下咱們的視框,假若咱們想把車牌所有顯示的話,視框往右偏移一段距離,是否是就能夠解決這個問題呢?爲保證新的視框中心可以正好與車牌的中心重合,咱們能夠選擇偏移xidff/2長度。正以下圖所顯示的同樣。

 

 圖30 考慮偏移的座標計算

 

  視框往右偏移的含義就是目標圖像Mat的原點往右偏移。若是原點偏移的話,那麼仿射後圖像的三個關鍵點的座標要從新計算,都須要減去xidff/2大小。

  從新計算的映射點座標爲下:

        plTri[0] = Point2f(0 + xiff, 0);
        plTri[1] = Point2f(width - 1, 0);
        plTri[2] = Point2f(0, height - 1);

        dstTri[0] = Point2f(xiff/2, 0);
        dstTri[1] = Point2f(width - 1 - xiff + xiff/2, 0);
        dstTri[2] = Point2f(xiff/2, height - 1);

 

  再試一次。果真,視框被調整到咱們但願的地方了,咱們能夠看到全部的車牌區域了。此次解決的是warpAffine函數帶來的視框偏移問題。

 

圖31 完整的車牌區域

 

  關於座標調整的另外一個理解就是當中心點保持不變時,平行四邊形扭正爲矩形時剛好是左上的點往左偏移了xdiff/2的距離,左下的點往右偏移了xdiff/2的距離,造成一種對稱的平移。可使用ps或者inkspace相似的矢量製圖軟件看看「斜切」的效果, 

  如此一來,就完成了偏斜扭正的過程。須要注意的是,向左傾斜的車牌的視框偏移方向與向右傾斜的車牌是相反的。咱們能夠用slope的正負來判斷車牌是左斜仍是右斜。

 

  6.總結

  經過以上過程,咱們成功的將一個偏斜的車牌通過旋轉變換等方法扭正過來。

  讓咱們回顧一下偏斜扭正過程。咱們須要將一個偏斜的車牌扭正,爲了達成這個目的咱們首先須要對圖像進行旋轉。由於旋轉是個計算量很大的函數,因此咱們須要考慮再也不用全圖旋轉,而是區域旋轉。在旋轉過程當中,會發生圖像截斷問題,因此須要使用擴大化旋轉方法。旋轉之後,只有偏斜視角的車牌才須要扭正,正視角的車牌不須要,所以還須要一個偏斜判斷過程。如此一來,偏斜扭正的過程須要旋轉,區域截取,擴大化,偏斜判斷等等過程的協助,這就是整個流程中有這麼多步須要處理的緣由。

  下圖從另外一個視角回顧了偏斜扭正的過程,主要說明了偏斜扭轉中的兩次「截取」過程。

圖32 偏斜扭正全過程

 

  1. 首先咱們獲取RotatedRect,而後對每一個RotatedRect獲取外界矩形,也就是ROI區域。外接矩形的計算有可能得到不安全的座標,所以須要使用安全的獲取外界矩形的函數。
  2. 獲取安全外接矩形之後,在原圖中截取這部分區域,並放置到一個新的Mat裏,稱之爲ROI圖像。這是本過程當中第一次截取,使用Mat(Rect ...)函數。
  3. 接下來對ROI圖像根據RotatedRect的角度展開旋轉,旋轉的過程當中使用了放大化旋轉法,以此防止車牌區域被截斷。
  4. 旋轉完之後,咱們把已經轉正的RotatedRect部分截取出來,稱之爲車牌區域。這是本過程當中第二次截取,與第一次不一樣,此次截取使用getRectSubPix()方法。
  5. 接下里使用偏斜判斷函數來判斷車牌區域裏的車牌是不是傾斜的。
  6. 若是是,則繼續使用仿射變換函數wrapAffine來進行扭正處理,處理過程當中要注意三個關鍵點的座標。
  7. 最後使用resize函數將車牌區域統一化爲EasyPR的車牌大小。

  整個過程有一個統一的函數--deskew。下面是deskew的代碼。

//! 抗扭斜處理
int CPlateLocate::deskew(const Mat& src, const Mat& src_b, vector<RotatedRect>& inRects, vector<CPlate>& outPlates)
{

    for (int i = 0; i < inRects.size(); i++)
    {
        RotatedRect roi_rect = inRects[i];

        float r = (float)roi_rect.size.width / (float)roi_rect.size.height;
        float roi_angle = roi_rect.angle;

        Size roi_rect_size = roi_rect.size;
        if (r < 1) {
            roi_angle = 90 + roi_angle;
            swap(roi_rect_size.width, roi_rect_size.height);
        }
        
        if (roi_angle - m_angle < 0 && roi_angle + m_angle > 0)
        {
            Rect_<float> safeBoundRect;
            bool isFormRect = calcSafeRect(roi_rect, src, safeBoundRect);
            if (!isFormRect)
                continue;

            Mat bound_mat = src(safeBoundRect);
            Mat bound_mat_b = src_b(safeBoundRect);

            Point2f roi_ref_center = roi_rect.center - safeBoundRect.tl();
            
            Mat deskew_mat;
            if ((roi_angle - 5 < 0 && roi_angle + 5 > 0)  || 90.0 == roi_angle || -90.0 == roi_angle) 
            {
                deskew_mat = bound_mat;
            } 
            else
            {
                // 角度在5到60度之間的,首先須要旋轉 rotation
                Mat rotated_mat;
                Mat rotated_mat_b;

                if (!rotation(bound_mat, rotated_mat, roi_rect_size, roi_ref_center, roi_angle))
                    continue;    

                if (!rotation(bound_mat_b, rotated_mat_b, roi_rect_size, roi_ref_center, roi_angle))
                    continue;

                // 若是圖片偏斜,還須要視角轉換 affine
                double roi_slope = 0;
                
                if (isdeflection(rotated_mat_b, roi_angle, roi_slope))
                {
                    //cout << "roi_angle:" << roi_angle << endl;
                    //cout << "roi_slope:" << roi_slope << endl;
                    affine(rotated_mat, deskew_mat, roi_slope);
                }
                else
                    deskew_mat = rotated_mat;
            }

            Mat plate_mat;
            plate_mat.create(HEIGHT, WIDTH, TYPE);

            if (deskew_mat.cols >= WIDTH || deskew_mat.rows >= HEIGHT)
                resize(deskew_mat, plate_mat, plate_mat.size(), 0, 0, INTER_AREA);
            else
                resize(deskew_mat, plate_mat, plate_mat.size(), 0, 0, INTER_CUBIC);
            
            /*if (1)
            {
                imshow("plate_mat", plate_mat);
                waitKey(0);
                destroyWindow("plate_mat");
            }*/
            

            CPlate plate;
            plate.setPlatePos(roi_rect);
            plate.setPlateMat(plate_mat);
            outPlates.push_back(plate);

        }
    }
    return 0;
}
View Code

 

  最後是改善建議:

  角度偏斜判斷時能夠用白色區域的輪廓來肯定平行四邊形的四個點,而後用這四個點來計算斜率。這樣算出來的斜率的可能魯棒性更好。

 

總結

  本篇文檔介紹了顏色定位與偏斜扭轉等功能。其中顏色定位屬於做者一直想作的定位方法,而偏斜扭轉則是做者之前認爲不可能解決的問題。這些問題如今都基本被攻克了,並在這篇文檔中闡述,但願這篇文檔能夠幫助到讀者。

  做者但願能在這片文檔中不只傳遞知識,也傳授我在摸索過程當中積累的經驗。由於光知道怎麼作並不能加深對車牌識別的認識,只有經歷過失敗,瞭解哪些思想嘗試過,碰到了哪些問題,是如何解決的,才能幫助讀者更好地認識這個系統的內涵。

 

  最後,做者很感謝可以閱讀到這裏的讀者。若是看完以爲好的話,還請輕輕點一下贊,大家的鼓勵就是做者繼續行文的動力。

 

  對EasyPR作下說明:EasyPR,一個開源的中文車牌識別系統,代碼託管在github。其次,在前面的博客文章中,包含EasyPR至今的開發文檔與介紹。在後續的文章中,做者會介紹EasyPR中字符分割與識別等相關內容,歡迎繼續閱讀。

 

版權說明:

 

  本文中的全部文字,圖片,代碼的版權都是屬於做者和博客園共同全部。歡迎轉載,可是務必註明做者與出處。任何未經容許的剽竊以及爬蟲抓取都屬於侵權,做者和博客園保留全部權利。

 

參考文獻:

  1.http://blog.csdn.net/xiaowei_cqu/article/details/7616044

  2.http://docs.opencv.org/doc/tutorials/imgproc/imgtrans/warp_affine/warp_affine.html

相關文章
相關標籤/搜索