前言 因工作需要,需要定位圖片中的二維碼;我遂查閱了相關資料,也學習了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 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 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)二維碼相鄰區域定位
一般情況下,二維碼在整個圖中的位置是確定的。識別出二維碼后,根據二維碼與其他圖的位置關系,可以很容易的定位別的圖元。
后記
作者通過查找大量資料,仔細研究了二維碼的特征,從而找到了識別二維碼的方法。網上也有許多識別二維碼的方法,但是不夠嚴謹。本文是將二維碼的多個特征相結合來識別,這樣更准確。這種識別方法已應用在公司的產品中,識別效果還是非常好的。