1. 程式人生 > >基於opencv 識別、定位二維碼 (c++版)

基於opencv 識別、定位二維碼 (c++版)

前言 因工作需要,需要定點陣圖片中的二維碼;我遂查閱了相關資料,也學習了opencv開源庫。通過一番努力,終於很好的實現了二維碼定位。本文將講解如何使用opencv定位二維碼。

定位二維碼不僅僅是為了識別二維碼;還可以通過二維碼對影象進行水平糾正以及相鄰區域定位。定位二維碼,不僅需要影象處理相關知識,還需要分析二維碼的特性,本文先從二維碼的特性講起。

 

1 二維碼特性

二維碼在設計之初就考慮到了識別問題,所以二維碼有一些特徵是非常明顯的。

二維碼有三個“回“”字形圖案,這一點非常明顯。中間的一個點位於圖案的左上角,如果影象偏轉,也可以根據二維碼來糾正。

思考題:為什麼是三個點,而不是一個、兩個或四個點。

一個點:特徵不明顯,不易定位。不易定位二維碼傾斜角度。

兩個點:兩個點的次序無法確認,很難確定二維碼是否放正了。

四個點:無法確定4個點的次序,從而無法確定二維碼是否放正了。

識別二維碼,就是識別二維碼的三個點,逐步分析一下這三個點的特性

 1 每個點有兩個輪廓。就是兩個口,大“口”內部有一個小“口”,所以是兩個輪廓。

 2 如果把這個“回”放到一個白色的背景下,從左到右,或從上到下畫一條線。這條線經過的圖案黑白比例大約為:黑白比例為1:1:3:1:1。

 3 如何找到左上角的頂點?這個頂點與其他兩個頂點的夾角為90度。

通過上面幾個步驟,就能識別出二維碼的三個頂點,並且識別出左上角的頂點。

 

2 使用opencv識別二維碼

 1) 查詢輪廓,篩選出三個二維碼頂點

opencv一個非常重要的函式就是查詢輪廓,就是可以找到一個圖中的縮所有的輪廓,“回”字形圖案是一個非常的明顯的輪廓,很容易找到。

 1 int QrParse::FindQrPoint(Mat& srcImg, vector<vector<Point>>& qrPoint)
 2 {
 3     //彩色圖轉灰度圖
 4     Mat src_gray;
 5     cvtColor(srcImg, src_gray, CV_BGR2GRAY);
 6     namedWindow("src_gray");
 7     imshow("src_gray", src_gray);
 8 
 9     //二值化
10     Mat threshold_output;
11     threshold(src_gray, threshold_output, 0, 255, THRESH_BINARY | THRESH_OTSU);
12     Mat threshold_output_copy = threshold_output.clone();
13     namedWindow("Threshold_output");
14     imshow("Threshold_output", threshold_output);
15 
16     //呼叫查詢輪廓函式
17     vector<vector<Point> > contours;
18     vector<Vec4i> hierarchy;
19     findContours(threshold_output, contours, hierarchy, CV_RETR_TREE, CHAIN_APPROX_NONE, Point(0, 0));
20 
21     //通過黑色定位角作為父輪廓,有兩個子輪廓的特點,篩選出三個定位角
22     int parentIdx = -1;
23     int ic = 0;
24 
25     for (int i = 0; i < contours.size(); i++)
26     {
27         if (hierarchy[i][2] != -1 && ic == 0)
28         {
29             parentIdx = i;
30             ic++;
31         }
32         else if (hierarchy[i][2] != -1)
33         {
34             ic++;
35         }
36         else if (hierarchy[i][2] == -1)
37         {
38             ic = 0;
39             parentIdx = -1;
40         }
41 
43         //有兩個子輪廓才是二維碼的頂點
44         if (ic >= 2)
45         {
46             bool isQr = QrParse::IsQrPoint(contours[parentIdx], threshold_output_copy);
47 
48             //儲存找到的三個黑色定位角
49             if (isQr)
50                 qrPoint.push_back(contours[parentIdx]);
51 
52             ic = 0;
53             parentIdx = -1;
54         }
55     }
56 
57     return 0;
58 }

找到了兩個輪廓的圖元,需要進一步分析是不是二維碼頂點,用到如下函式:

bool QrParse::IsQrPoint(vector<Point>& contour, Mat& img)
{
    //最小大小限定
    RotatedRect rotatedRect = minAreaRect(contour);
    if (rotatedRect.size.height < 10 || rotatedRect.size.width < 10)
        return false;

    //將二維碼從整個圖上摳出來
    cv::Mat cropImg = CropImage(img, rotatedRect);
    int flag = i++;

    //橫向黑白比例1:1:3:1:1
    bool result = IsQrColorRate(cropImg, flag);
    return result;
}

黑白比例判斷函式:

  1 //橫向和縱向黑白比例判斷
  2 bool QrParse::IsQrColorRate(cv::Mat& image, int flag)
  3 {
  4     bool x = IsQrColorRateX(image, flag);
  5     if (!x)
  6         return false;
  7     bool y = IsQrColorRateY(image, flag);
  8     return y;
  9 }
 10 //橫向黑白比例判斷
 11 bool QrParse::IsQrColorRateX(cv::Mat& image, int flag)
 12 {
 13     int nr = image.rows / 2;
 14     int nc = image.cols * image.channels();
 15 
 16     vector<int> vValueCount;
 17     vector<uchar> vColor;
 18     int count = 0;
 19     uchar lastColor = 0;
 20 
 21     uchar* data = image.ptr<uchar>(nr);
 22     for (int i = 0; i < nc; i++)
 23     {
 24         vColor.push_back(data[i]);
 25         uchar color = data[i];
 26         if (color > 0)
 27             color = 255;
 28 
 29         if (i == 0)
 30         {
 31             lastColor = color;
 32             count++;
 33         }
 34         else
 35         {
 36             if (lastColor != color)
 37             {
 38                 vValueCount.push_back(count);
 39                 count = 0;
 40             }
 41             count++;
 42             lastColor = color;
 43         }
 44     }
 45 
 46     if (count != 0)
 47         vValueCount.push_back(count);
 48 
 49     if (vValueCount.size() < 5)
 50         return false;
 51 
 52     //橫向黑白比例1:1:3:1:1
 53     int index = -1;
 54     int maxCount = -1;
 55     for (int i = 0; i < vValueCount.size(); i++)
 56     {
 57         if (i == 0)
 58         {
 59             index = i;
 60             maxCount = vValueCount[i];
 61         }
 62         else
 63         {
 64             if (vValueCount[i] > maxCount)
 65             {
 66                 index = i;
 67                 maxCount = vValueCount[i];
 68             }
 69         }
 70     }
 71 
 72     //左邊 右邊 都有兩個值,才行
 73     if (index < 2)
 74         return false;
 75     if ((vValueCount.size() - index) < 3)
 76         return false;
 77 
 78     //黑白比例1:1:3:1:1
 79     float rate = ((float)maxCount) / 3.00;
 80 
 81     cout << "flag:" << flag << " ";
 82 
 83     float rate2 = vValueCount[index - 2] / rate;
 84     cout << rate2 << " ";
 85     if (!IsQrRate(rate2))
 86         return false;
 87 
 88     rate2 = vValueCount[index - 1] / rate;
 89     cout << rate2 << " ";
 90     if (!IsQrRate(rate2))
 91         return false;
 92 
 93     rate2 = vValueCount[index + 1] / rate;
 94     cout << rate2 << " ";
 95     if (!IsQrRate(rate2))
 96         return false;
 97 
 98     rate2 = vValueCount[index + 2] / rate;
 99     cout << rate2 << " ";
100     if (!IsQrRate(rate2))
101         return false;
102 
103     return true;
104 }
105 //縱向黑白比例判斷 省略
106 bool QrParse::IsQrColorRateY(cv::Mat& image, int flag)
bool QrParse::IsQrRate(float rate)
{
     //大概比例 不能太嚴格
    return rate > 0.6 && rate < 1.9;
}

 

2) 確定三個二維碼頂點的次序

 通過如下原則確定左上角頂點:二維碼左上角的頂點與其他兩個頂點的夾角為90度。

 1 // pointDest存放調整後的三個點,三個點的順序如下
 2 // pt0----pt1
 3 // 
 4 // pt2
 5 bool QrParse::AdjustQrPoint(Point* pointSrc, Point* pointDest)
 6 {
 7     bool clockwise;
 8     int index1[3] = { 2,1,0 };
 9     int index2[3] = { 0,2,1 };
10     int index3[3] = { 0,1,2 };
11 
12     for (int i = 0; i < 3; i++)
13     {
14         int *n = index1;
15         if(i==0)
16             n = index1;
17         else if (i == 1)
18             n = index2;
19         else 
20             n = index3;
21 
22         double angle = QrParse::Angle(pointSrc[n[0]], pointSrc[n[1]], pointSrc[n[2]], clockwise);
23         if (angle > 80 && angle < 99)
24         {
25             pointDest[0] = pointSrc[n[2]];
26             if (clockwise)
27             {
28                 pointDest[1] = pointSrc[n[0]];
29                 pointDest[2] = pointSrc[n[1]];
30             }
31             else
32             {
33                 pointDest[1] = pointSrc[n[1]];
34                 pointDest[2] = pointSrc[n[0]];
35             }
36             return true;
37         }
38     }
39     return true;
40 }

3)通過二維碼對圖片矯正。

圖片有可能是傾斜的,傾斜夾角可以通過pt0與pt1連線與水平線之間的夾角確定。二維碼的傾斜角度就是整個圖片的傾斜角度,從而可以對整個圖片進行水平矯正。

1 //二維碼傾斜角度
2 Point hor(pointAdjust[0].x+300,pointAdjust[0].y); //水平線
3 double qrAngle = QrParse::Angle(pointAdjust[1], hor, pointAdjust[0], clockwise);
4 
5 //以二維碼左上角點為中心 旋轉
6     Mat drawingRotation = Mat::zeros(Size(src.cols,src.rows), CV_8UC3);
7     double rotationAngle = clockwise? -qrAngle:qrAngle;
8     Mat affine_matrix = getRotationMatrix2D(pointAdjust[0], rotationAngle, 1.0);//求得旋轉矩陣
9     warpAffine(src, drawingRotation, affine_matrix, drawingRotation.size());

4)二維碼相鄰區域定位

一般情況下,二維碼在整個圖中的位置是確定的。識別出二維碼後,根據二維碼與其他圖的位置關係,可以很容易的定位別的圖元。

後記

作者通過查詢大量資料,仔細研究了二維碼的特徵,從而找到了識別二維碼的方法。網上也有許多識別二維碼的方法,但是不夠嚴謹。本文是將二維碼的多個特徵相結合來識別,這樣更準確。這種識別方法已應用在公司的產品中,識別效果還是非常好