作者:森碼
連結:
https://www.jianshu.com/p/e2866af44236
本文由作者授權發布。
1. 為什麼要做這個庫?相信大家在平常的生活中,如果遇到掃碼的場景第一個想到的應該就是微信了,可以說微信使用二維碼打開了移動網際網路的另一扇大門,並且在掃碼體驗上及其優秀,本該有一定要求的掃碼過程,在經過微信的優化之後,讓用戶在使用時擁有了一種『隨意性』,像拍一張照片一樣簡單,像發一句消息隨意,像擺弄一件玩具一樣有趣。
有了這樣的『標杆』存在,大家在潛意識裡面也都有了標準,你們的掃一掃為什麼不好用?為什麼要識別這麼久?甚至我對準了也識別不出來?擺在我們面前的是各種用戶的不滿,解決這些問題就成了我們必須要面對的情形。
2. 選型二維碼處理,繞不開的就是ZXing和ZBar了,ZXing作為老牌的識別庫已經"孵化"出了包括js、Python、C++、PHP等各個語言的lib,同時Android版本也一直在更新,但是ZBar作為C的處理者,上次的更新已經是7年前了。
為了讓二維碼的識別儘量的快,並且對圖像處理有更多的可能性,考量之後我們選用了更具活力的zxing-cpp,選用了它來作為我們的底層處理庫。
https://github.com/nu-book/zxing-cpp
3. 相機的處理原始圖像的獲取至關重要,倘若這一步走不好,其他的處理再好也於事無補,對於從來沒有接觸這一領域的自己來說,踏遍Android相機的坑不知要花多少時間,好在已經有優秀的開源庫,這裡特別感謝BGAQRCode-Android的開源庫,操縱攝像頭的一些重要功能,比如自動對焦、觸摸對焦、放大縮小等都已經具備,自己也只是在其之上做了一些小改進,比如GroupView的改進、加入傳感器對焦、線程池處理等等。有了這些之後,我們就可以開始處理數據了。
https://github.com/bingoogolapple/BGAQRCode-Android
Android的相機獲取到的數據並非我們平常認為的RGB數據,而是視頻採集中的經常使用的NV21格式即YUV,所以在獲取到這些數據之後是無法直接使用的。
1. 格式轉換
要轉格式首先我們先要了解NV21在內存中是什麼樣子的。
YUV420
不同於我們平常的圖片格式,比如png的圖片,圖片由一個一個像素點構成,400 * 800的圖片就有320000個像素,每一個像素對應一個ARGB,即4個字節,分別表示(透明度,紅色色值,綠色色值,藍色色值),就是我們平常見到的(0,255,255,255)一個像素的內存是連在一起的,但是YUV不同於我們『認知』上的格式,這3個數值分別代表的是(明亮度,色度,濃度),一個很有意思的知識是:YUV的發明是由於彩色電視與黑白電視的過渡時期。
https://zh.wikipedia.org/wiki/YUV
YUV的組成
有了一個大概的了解之後,我們就可以把攝像頭的數據轉化為我們需要的數據,其實只要根據公式來推倒就可以了,但是了解原理能讓我們更好的理解。
YUV->RGB
2. 算法優化
對於二維碼來說,它是一個個黑白的點組成的,其實並不需要多麼色彩斑斕的裝飾,一張灰度圖或許是更好的選擇,一般的圖像處理,都是輪詢所有像素數據,對於一個或者一組數據進行處理。一個YUV轉化為RGB的算法就是要拿出所有像素,然後各種轉換,這無疑是一種浪費。一個更好的選擇是把原圖像直接轉化為灰度圖。
for (int i = top; i < height + top; ++i) {
srcIndex += left;
for (int j = left; j < left + width; ++j, ++desIndex, ++srcIndex) {
p = data[srcIndex] & 0xFF;
pixels[desIndex] = 0xff000000 | p << 16 | p << 8 | p;
}
srcIndex += margin;
}
3. 圖片裁剪
我們知道,通常二維碼識別的界面都有一個『框』,這個框並不是可有可無的,它不僅能告訴用戶我們正在掃描,二維碼應該放在這裡面,更能在我們處理時成倍的提高處理效率,在測試的過程中,裁剪和沒有經過裁剪的圖片處理一般要相差4-5倍的時間。
一張600 * 600的圖片識別要50-80ms;而一張完整照片,比如1920 * 1080的圖片,通常要經過200ms以上的時間處理,如果所有的圖片都不經過截取,那麼想要提升整體的識別效率是很困難的。
圖片裁剪是一個非常必要的操作,加上我們上面的灰度轉換,兩個操作合二為一,得到灰度圖的同時也裁剪了圖像。經過簡單的處理之後,這個圖像的「質量」是很高的。
截取後的圖像只有原圖像的1/5,更利於我們去處理數據,至此我們的圖像已經準備好了。
void ImageUtil::convertNV21ToGrayAndScale(int left, int top, int width, int height, int rowWidth,
const jbyte *data, int *pixels) {
int p;
int desIndex = 0;
int bottom = top + height;
int right = left + width;
int srcIndex = top * rowWidth;
int marginRight = rowWidth - right;
for (int i = top; i < bottom; ++i) {
srcIndex += left;
for (int j = left; j < right; ++j, ++desIndex, ++srcIndex) {
p = data[srcIndex] & 0xFF;
pixels[desIndex] = 0xff000000u | p << 16u | p << 8u | p;
}
srcIndex += marginRight;
}
}
1. 解析二維碼
有了充足的準備,二維碼的識別已經是水到渠成的事情了,根據轉化好的數據,生成HybridBinarizer對象,通過MultiFormatReader即可解析。
2. 識別流程優化
在一些Demo中,二維碼處理流程通常是使用setOneShotPreviewCallback作為相機數據的處理,即一幀畫面處理完再處理下一幀(兩幀不一定是相連的),這樣的處理會造成兩個問題.
首先:相機獲取的畫面不一定是完全對焦好的,一般我們拿出手機都有一個對焦的動作,中間可能只有50%的畫面是可用的,這種情況下可能會丟失清晰的圖像而處理了模糊的圖像;
其次這種串行的處理也是對機能的浪費,現在的手機處理連續的圖像是綽綽有餘的;
最後,這樣的處理流程是不受我們控制的,只能來一張處理一張。
在流程改進中我使用了setPreviewCallback的回調,並統一加入線程池處理。這裡我可以控制一秒之內處理多少幀圖像,在測試中是300ms處理一幀(不同機型處理的速度不盡相同,為了避免線程池隊列過長,選擇了較低的處理速度,後期可以根據機型來動態設置處理間隔),為了加速處理,這4幀是識別框內的數據。
同時,為了能快速識別簡單的二維碼,每4幀處理完之後加入一幀全屏處理,這一幀可以作為識別圖像明亮度的主幀,也可以在二維碼超出識別框時,繼續識別數據。有了這個改動,就可以做到點擊掃一掃,抬手就能得到結果。
但是這個掃碼的距離實在不能讓人滿意,我們常用的掃一掃通常都會有一個放大的操作,而這個操作是掃碼優化中也是非常關鍵的一步。
3. 放大優化
想要進一步的優化我們就得更進一步的研究二維碼了,二維碼的生成細節和原理和二維碼(QR code)基本結構及生成原理有詳細的解釋,這裡我們發現左上、左下、右上三個位置探測圖形,在二維碼的解碼過程中,其實是分幾個步驟的,首先就是要定位這個二維碼確認其位置,然後才能取出裡面的數據,而這個定位的點就是這三個。
在距離二維碼較遠時,可能無法解析出完整的數據,但是卻能定位這個二維碼,通過定位點的信息,我們可以進行放大的操作,從而獲取到更加精確的圖像數據,也更有利於我們解析。
二維碼結構
void tryZoom(BarcodeReader.Result result) {
int len = 0;
float[] points = result.getPoints();
if (points.length > 3) {
float point1X = points[0];
float point1Y = points[1];
float point2X = points[2];
float point2Y = points[3];
float xLen = Math.abs(point1X - point2X);
float yLen = Math.abs(point1Y - point2Y);
len = (int) Math.sqrt(xLen * xLen + yLen * yLen);
}
handleAutoZoom(len);
}
4. 與微信的對比
微信的掃一掃可以說是秒級的處理,特別是在iOS的設備上,更不可思議的是它好像沒有距離的限制。經過我們的優化之後,我們的二維碼可以在50cm內解析出來,但是與微信相差的還是太遠,我們需要更好的處理圖像數據,來定位二維碼。
識別距離
二維碼的識別中,距離 是一個非常關鍵的制約條件,通常在30cm-40cm內是一定可以識別出來的,但是超過這個距離獲取到的圖像就會比較模糊,如果攝像頭的解析度不高識別率也會下降,如果超過這個閾值,識別算法就只能定位數據而無法解析數據,比如上圖中的B點,這裡我們加入自動放大就可以解決,但是超過這個距離呢?
我們就需要手機移動了。如果有一種方案,可以像在B點時一樣,雖然無法獲取到數據但是可以得到二維碼的位置、大小呢?要做到這個,OpenCV是一個不二之選。
說到圖像處理,我們大致有兩套方案,
方案一:處理圖像數據,獲取圖像輪廓,算法檢測二維碼位置。
方案二:機器學習,直接定位二維碼。
兩者其實都是可行的,只是在難易度方面的差異,我們首先嘗試了機器學習的方案,奈何自己學的還比較淺,收集到的樣本數據也不夠,訓練出來的模型也不太理想,比如一個沒有二維碼的畫面會檢查出好幾個,又比如有的時候又要離的特別近才能識別出來,這又違背了我們的本意。所以我選擇了方案一,雖然聽起來沒那麼高大上了,但是在實際的測試中也完全能達到預期水平。
當圖像即無法解析出數據也無法定位到二維碼時,我們採用OpenCV去處理圖像。因為之前已經進行過灰度處理了,這裡可以直接進行Canny化,然後執行findContours方法獲取輪廓信息,之後過濾輪廓信息,判斷點與點之間的距離,得到二維碼的位置信息。
(以上的過程看似簡單,其實進行了很多嘗試,包括二值化,毛邊去除、調節亮度、對比度處理,直接獲取點信息等等,這裡感謝
https://blog.csdn.net/jia20003/article/details/77348170
和https://blog.csdn.net/zwx1995zwx/article/details/79171979
的圖像過濾算法,作為一個圖像處理的門外漢真的學到很多)
canny化之後的圖像
識別二維碼輪廓
定位二維碼輪廓,紅色框框是自動生成的
拿到這些信息之後,我們就可以遵循在B點時的處理邏輯,直接放大圖像獲取數據。
你可能會想為什麼不直接截取圖像,這樣就不用費時費力再進行一輪識別,其實這裡也想到過,但是得到的數據精度丟失實在太多,我嘗試用微信去識別截取得到的二維碼,微信也無法檢測出來,這樣的處理對於簡單的二維碼或許可行,但是對於稍微複雜的二維碼或者我們所要解決的問題來說是遠遠不夠的.
在加入OpenCV之後,我們的識別距離擴大了一倍,得到的效果比預期的還要好。
識別效果展示
待完善的功能aar過大,因為有OpenCV的加入,aar文件有7.7M。
不支持生成二維碼(將會在近期加入)。
擴展性、可定製性不夠,這個可以慢慢加入。
有時候會放的太大,試試縮小功能。
註:不同手機識別效率其實不盡相同,攝像頭越好,識別效率越高。
源碼地址
https://github.com/devilsen/CZXing
歡迎Star,歡迎Fork,歡迎和我一起開發。
在不到一個月的時間裡完成了主要功能(主要是自己在業餘時間完成的),感謝那些無私奉獻的博主之餘也感受到開源的便利和偉大,我能做的也只是用開源來回饋各位。
最後以牛頓的一句話來結尾吧,「我之所以站得高,是因為我站在巨人的肩膀上。」
推薦閱讀:
掃一掃 關注我的公眾號
如果你想要跟大家分享你的文章,歡迎投稿~
┏(^0^)┛明天見!