NDK 開發實戰 - 微信公眾號二維碼檢測
關於二維碼識別,我們一般都是用的 Zxing 或者 Zbar ,但它們的識別率其實並不高,有很多情況下都是失靈的,比如下面這兩張圖:

騰訊 Buggly

同學給的
使用開源庫 Zxing 掃描以上兩張二維碼,有一張死活不識別。使用微信是可以的,大家可以用支付寶試試(不行),那碰到這種情況到底該怎麼辦呢?哈哈,這次終於有用武之地了,我們琢磨著來優化一把。
我們在微信公眾號都用過這麼一個功能,長按一張圖片,如果該圖片包含有二維碼,會彈出識別圖中二維碼,如果該圖片不含有二維碼,則不會彈出識別二維碼這個選項。說到這裡我們大概應該知曉了,識別二維碼其實分為兩步, 第一步是發現擷取二維碼區域,第二步是識別擷取到的二維碼區域。 那麼 zxing 和支付寶到底是哪一步出了問題呢?首先我們來看一下第一步發現擷取二維碼區域。

二維碼事例
上圖是一張常用的二維碼事例圖,有三個比較重要的區域,分別是左上,右上和左下,我們只要能找到這三個特定的區域,就能判定圖片中包含有二維碼。接下來我們來分析一下思路:
1. 對其進行輪廓查詢
2. 對查詢的到的輪廓進行初步過濾
3. 判斷是否符合二維碼的特徵規則
4. 擷取二維碼區域
5. 識別二維碼
//判斷 X 方向上是否符合規則 bool isXVerify(const Mat& qrROI){ ... 程式碼省略 // 判斷 x 方向從左到右的畫素比例 // 黑:白:黑:白:黑 = 1:1:3:1:1 } //判斷 Y 方向上是否符合規則 bool isYVerify(const Mat& qrROI){ ... 程式碼省略 // y 方向上也可以按照 isXVerify 方法判斷 // 但我們也可以適當的寫簡單一些 // 白色畫素 * 2 < 黑色畫素 && 黑色像 < 4 * 白色畫素 } int main(){ Mat src = imread("C:/Users/hcDarren/Desktop/android/code1.png"); if (!src.data){ printf("imread error!"); return -1; } imshow("src", src); // 對影象進行灰度轉換 Mat gary; cvtColor(src, gary, COLOR_BGR2GRAY); // 二值化 threshold(gary, gary, 0, 255, THRESH_BINARY | THRESH_OTSU); imshow("threshold", gary); // 1. 對其進行輪廓查詢 vector<vector<Point> > contours; findContours(gary, contours, RETR_LIST, CHAIN_APPROX_SIMPLE); for (int i = 0; i < contours.size(); i++) { // 2. 對查詢的到的輪廓進行初步過濾 double area = contourArea(contours[i]); // 2.1 初步過濾面積 7*7 = 49 if (area < 49){ continue; } RotatedRect rRect = minAreaRect(contours[i]); float w = rRect.size.width; float h = rRect.size.height; float ratio = min(w, h) / max(w, h); // 2.2 初步過濾寬高比大小 if (ratio > 0.9 && w< gary.cols/2 && h< gary.rows/2){ Mat qrROI = warpTransfrom(gary, rRect); // 3. 判斷是否符合二維碼的特徵規則 if (isYVerify(qrROI) && isXVerify(qrROI)) { drawContours(src, contours, i, Scalar(0, 0, 255), 4); } } } imshow("src", src); imwrite("C:/Users/hcDarren/Desktop/android/code_result.jpg", src); waitKey(0); return 0; }

處理結果
程式碼是非常簡單的,關鍵是我們要善於學會去分析,多多培養解決問題的能力,只要知道實現思路,其他一切都不是問題了。那麼有意思的就來了,當掃描第二張圖的時候,我們發現死活都識別不了。那麼細心的同學就可能明白了,我們上面的程式碼是按照正方形的特徵來識別的,而第二張圖是圓形的特徵,因此 Zxing 無法識別也是正常的,因為咱們在寫程式碼的時候根本沒考慮這麼個情況。那麼我們怎麼才能做的去識別圓形特徵的呢?考驗我們的時候到了,我們能想到三種解決方案:
1. 再寫一套識別圓形特徵的程式碼
2. 借鑑人臉識別的方案,採用訓練樣本的方式識別
3. 換一種檢查方案,只寫一套程式碼
人臉識別在下期文章中會寫到,訓練樣本的方式比較麻煩,如果之前沒接觸過,那麼需要一定的時間成本,但這種方案應該是最好的。再寫一套圓形識別的程式碼,感覺維護困難,作為有靈魂的工程師總覺得彆扭。那這裡我們就採用第三種方案了,其實知識點也就那麼多,還是那句話 多培養我們分析解決問題的能力 。
我們仔細觀察,他們其實還是有很多共同點,我們對其進行輪廓篩選的時候會發現,都是一個大輪廓裡面套兩個小輪廓。具體流程如下:
1. 對其進行輪廓查詢
2. 對查詢的到的輪廓進行初步過濾
3. 判斷是否是一個大輪廓套兩個小輪廓且符合特徵規則(面積比例判斷)
4. 擷取二維碼區域
5. 識別二維碼
extern "C" JNIEXPORT jobject JNICALL Java_com_darren_ndk_day76_MainActivity_clipQrBitmap(JNIEnv *env, jobject instance, jobject bitmap) { Mat src; cv_helper::bitmap2mat(env, bitmap, src); // 對影象進行灰度轉換 Mat gary; cvtColor(src, gary, COLOR_BGR2GRAY); // 二值化 threshold(gary, gary, 0, 255, THRESH_BINARY | THRESH_OTSU); // 1. 對其進行輪廓查詢 vector<Vec4i> hierarchy; vector<vector<Point> > contours; vector<vector<Point> > contoursRes; /* 引數說明:https://blog.csdn.net/guduruyu/article/details/69220296 輸入影象image必須為一個2值單通道影象 contours引數為檢測的輪廓陣列,每一個輪廓用一個point型別的vector表示 hiararchy引數和輪廓個數相同,每個輪廓contours[ i ]對應4個hierarchy元素hierarchy[ i ][ 0 ] ~hierarchy[ i ][ 3 ], 分別表示後一個輪廓、前一個輪廓、父輪廓、內嵌輪廓的索引編號,如果沒有對應項,該值設定為負數。 mode表示輪廓的檢索模式 CV_RETR_EXTERNAL 表示只檢測外輪廓 CV_RETR_LIST 檢測的輪廓不建立等級關係 CV_RETR_CCOMP 建立兩個等級的輪廓,上面的一層為外邊界,裡面的一層為內孔的邊界資訊。如果內孔內還有一個連通物體,這個物體的邊界也在頂層。 CV_RETR_TREE 建立一個等級樹結構的輪廓。具體參考contours.c這個demo method為輪廓的近似辦法 CV_CHAIN_APPROX_NONE 儲存所有的輪廓點,相鄰的兩個點的畫素位置差不超過1,即max(abs(x1-x2),abs(y2-y1))==1 CV_CHAIN_APPROX_SIMPLE 壓縮水平方向,垂直方向,對角線方向的元素,只保留該方向的終點座標,例如一個矩形輪廓只需4個點來儲存輪廓資訊 CV_CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS 使用teh-Chinl chain 近似演算法 offset表示代表輪廓點的偏移量,可以設定為任意值。對ROI影象中找出的輪廓,並要在整個影象中進行分析時,這個引數還是很有用的。 */ findContours(gary, contours, hierarchy, CV_RETR_TREE, CHAIN_APPROX_NONE, Point(0, 0)); int tCC = 0; // 臨時用來累加的子輪廓計數器 int pId = -1;// 父輪廓的 index for (int i = 0; i < contours.size(); i++) { if (hierarchy[i][2] != -1 && tCC == 0) { pId = i; tCC++; } else if (hierarchy[i][2] != -1) {// 有父輪廓 tCC++; } else if (hierarchy[i][2] == -1) {// 沒有父輪廓 tCC = 0; pId = -1; } // 找到了兩個子輪廓 if (tCC >= 2) { contoursRes.push_back(contours[pId]); tCC = 0; pId = -1; } } // 找到過多的符合特徵輪廓,對其進行篩選 if (contoursRes.size() > FEATURE_NUMBER) { contoursRes = filterContours(gary, contoursRes); } // 沒有找到符合的條件 if (contoursRes.size() < FEATURE_NUMBER) { return NULL; } for (int i = 0; i < contoursRes.size(); ++i) { drawContours(src, contoursRes, i, Scalar(255, 0, 0), 2); } // 裁剪二維碼,交給 zxing 或者 zbar 處理即可 cv_helper::mat2bitmap(env, src, bitmap); return bitmap; }

處理結果
開發中我們最喜歡做的就是拿過來直接用,但最好還是明白其中的原理,因為我們無法斷定開發中會出什麼么蛾子。像微信這樣的大廠自然得自己這一套,其實好的框架能夠優化優化,個人認為就已經差不多了。當然以上寫法在某些特定場景下,可能還是會存在些許漏洞,這就靠我們不斷的去琢磨優化了。
視訊地址: https://pan.baidu.com/s/1m7Epc4TVNs8fSXi2ifXkhQ
視訊密碼:5g3z