NDK 開發實戰 - 微信公衆號二維碼檢測

關於二維碼識別,咱們通常都是用的 Zxing 或者 Zbar ,但它們的識別率其實不是很高,有些狀況下是失靈的,好比下面這兩張圖: android

騰訊 Buggly
同窗給的

使用開源庫 Zxing 掃描以上兩張二維碼,有一張死活不識別。使用微信是能夠的,你們能夠用支付寶試試(不行),那碰到這種狀況到底該怎麼辦呢?哈哈,此次終於有用武之地了,咱們琢磨着來優化一把。git

咱們在微信公衆號都用過這麼一個功能,長按一張圖片,若是該圖片包含有二維碼,會彈出識別圖中二維碼,若是該圖片不含有二維碼,則不會彈出識別二維碼這個選項。說到這裏咱們大概應該知曉了,識別二維碼其實分爲兩步,**第一步是發現截取二維碼區域,第二步是識別截取到的二維碼區域。**那麼 zxing 和支付寶究竟是哪一步出了問題呢?首先咱們來看一下第一步發現截取二維碼區域。github

二維碼事例

上圖是一張經常使用的二維碼事例圖,有三個比較重要的區域,分別是左上,右上和左下,咱們只要能找到這三個特定的區域,就能斷定圖片中包含有二維碼。接下來咱們來分析一下思路:算法

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 沒法識別也是正常的,由於我們在寫代碼的時候根本沒考慮這麼個狀況。那麼咱們怎麼才能作到識別圓形的特徵呢?考驗咱們的時候到了,咱們能想到三種解決方案:bash

1. 再寫一套識別圓形特徵的代碼 2. 借鑑人臉識別的方案,採用訓練樣本的方式識別 3. 換一種檢查方案,只寫一套代碼微信

人臉識別在下期文章中會寫到,訓練樣本的方式比較麻煩,若是以前沒接觸過,那麼須要必定的時間成本,但這種方案應該是最好的。再寫一套圓形識別的代碼,感受維護困難,做爲一個有靈魂的工程師總以爲彆扭。那這裏咱們就採用第三種方案了,其實知識點也就那麼多,仍是那句話多培養咱們分析解決問題的能力框架

咱們仔細觀察,他們其實仍是有不少共同點,咱們對其進行輪廓篩選的時候會發現,都是一個大輪廓裏面套兩個小輪廓。具體流程以下:優化

1. 對其進行輪廓查找 2. 對查找的到的輪廓進行初步過濾 3. 判斷是不是一個大輪廓套兩個小輪廓且符合特徵規則(面積比例判斷) 4. 截取二維碼區域 5. 識別二維碼ui

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;
}
複製代碼

處理結果

開發中咱們最喜歡作的就是拿過來直接用,但最好仍是明白其中的原理,由於咱們沒法判定開發中會出什麼幺蛾子。像微信這樣的大廠天然得本身這一套,其實好的框架可以拿過來優化優化,我的認爲就已經差很少了。固然以上寫法在某些特定場景下,可能仍是會存在些許漏洞,這就靠咱們不斷的去琢磨優化了。

視頻地址:pan.baidu.com/s/1m7Epc4TV… 視頻密碼:5g3z

相關文章
相關標籤/搜索