zxing 一維碼部分深入分析與實際應用,識別卡片數量,Android數卡器


  打算修改zxing 源碼應用到其它方面,所以最近花了點時間閱讀其源碼,無意中找到這篇博客,條碼掃描二維碼掃描——ZXing android 簡化源碼分析 對過程的分析還是可以參考的.原作者給出的一個基本的UML序列圖:

 (圖像引用自http://blog.csdn.net/doonly2009/article/details/12175997)

結合上面的序列圖,本文將本zxing 一維碼部分的源碼進行解析,有不對的地方忘大家給予指正,所有內容僅供大家參考.更正上圖的一個小錯誤,DecodeThead 是被CaptureActivityHandler 調用 decodeThread.start()方法啟動的,而不是在構造方法中觸發的. 

部分一 ,環境搭建  2014.1.14 

  從這里下載工程文件,導入到Eclipse中(我的環境,windows,eclipse).這個工程文件是把一些代碼打包成了jar文件,這反而不利於文件的分析.我們這里利用從官網下載的源碼重新建立一個庫工程文件,方便我們的代碼分析.

1,core.jar 文件打包過程 .

  這里的core.jar  和網上的zxing.jar core.jar 類似 ,不過網上下載的都是簡化過的.過程如下:  

  ①,新建android 工程 ,不需要勾選 Create activty

  ②,右鍵工程中的src  -->new-->packages   命名為 com.google.zxing 

  ③,右鍵 com.google.zxing --> import --> File System -->找到 zxing 源碼  .. java\com\google\zxing 即可.

  ④,右鍵工程 選擇 android 標簽,勾選 Is Library 如圖.

  

 

打開你的工程文件,可以看到生成的jar文件了.如圖:

  

2,使用自己的 core.jar文件

  打開之前下載的工程文件(已經導入到eclipse)刪除之前引用的 zxing.jar 文件 .右鍵BarCodeTest 工程文件-->Properties-->Android 選項    -->在Libarary 選項中添加 . 這里會自動找到剛創建的包工程文件.

  

強調一點,若其它工程文件引用這個jar工程文件,則這個jar工程文件必須是打開狀態.經過測試 zxing 2.3  和 1.6 版本的 core 文件都可以在上面下載到的簡化工程使用.環境配置完成 ,下面將進行核心代碼的分析.

部分二,源碼分析 2014.1.21

  經過一段時間閱讀和分析源碼,下面以程序執行的大體順序進行源碼的分析.我們從獲取一幀數據開始分析,流程圖如下:

  

 

這里按這個流程圖進行代碼的分析. 

1 ,過程①,獲取最原始的數據,數據存儲在 byte[] data 中.

2,過程②,通過傳遞 handler, 當有消息時,會自動跳轉到 public void handleMessage(Message message) {}處執行.

 DecodeHandler.handleMessage(Message message) 在restartPreviewAndDecode()方法中被傳遞,其過程如下圖:

     

   zxing中主要采用 Message  來傳遞消息 ,這里有兩個繼承handler 的類,分別為CaptureActivityHandlerDecodeHandler.消息實例的  創建分別在:

 1 public void onPreviewFrame(byte[] data, Camera camera) {
 2     Point cameraResolution = configManager.getCameraResolution();
 3     if (!useOneShotPreviewCallback) {
 4       camera.setPreviewCallback(null);
 5     }
 6     if (previewHandler != null) {
 7         //從這里傳遞 message 參數  ,創建Message對象,其Handler.obtainMessage可以調用Message.obtain來創建消息。
 8       Message message = previewHandler.obtainMessage(previewMessage, cameraResolution.x,
 9           cameraResolution.y, data);
10       //Sends this Message to the Handler specified by getTarget(). 
11       //Throws a null pointer exception if this field has not been set. 
12       //who use the getTarget() function?
13       //通過Message.sendToTarget向消息隊列插入消息;
14       message.sendToTarget();
15       previewHandler = null;
16     } else {
17       Log.d(TAG, "Got preview callback, but no handler for it");
18     }
19   }

  和,位於 com.zxing.decoding.DecodeHandler 類中的方法

1 private void decode(byte[] data, int width, int height) {
2 
3         .........
4          Message message = Message.obtain(activity.getHandler(), R.id.decode_succeeded, rawResult);
5         .........
6 }

  這樣當有message 到來時便會自動調用 相應類中的handleMessage() 方法,實現對消息的處理.

3,過程③,這里實現對數據的解碼,如果解碼不成功,則進行下次解碼,否則將會發送消息給CaptureActivityHandler 對象.

4,過程④,這里是實現解碼的核心方法,包括數據的二值化和最內層的解碼方法 Result result = decodeRow(rowNumber, row, hints),因為我這里只分析一維碼的部分,所以預先設定的解碼類型,這里會跳轉到 這個類MultiFormatUPCEANReader中的decodeRow 方法.

       OneDReader.doDecode(BinaryBitmap image, Map<DecodeHintType, ?> hints) throws NotFoundException 對每一次獲取的幀進行最大15次的解碼,這里的解碼只是針對一行數據.從中間區域開始,分別對上下依次獲取的行數據進行解碼,如果解碼成功則返回.

強調一點,zxing 不對獲取的圖片數據進行旋轉,雖然支持旋轉.但是支持對轉動180 的一維碼的解碼,代碼如下:

 1 if (attempt == 1) { // trying again?
 2           row.reverse(); // reverse the row and continue
 3           // This means we will only ever draw result points *once* in the life of this method
 4           // since we want to avoid drawing the wrong points after flipping the row, and,
 5           // don't want to clutter with noise from every single row scan -- just the scans
 6           // that start on the center line.
 7           if (hints != null && hints.containsKey(DecodeHintType.NEED_RESULT_POINT_CALLBACK)) {
 8             Map<DecodeHintType,Object> newHints = new EnumMap<DecodeHintType,Object>(DecodeHintType.class);
 9             newHints.putAll(hints);
10             newHints.remove(DecodeHintType.NEED_RESULT_POINT_CALLBACK);
11             hints = newHints;
12           }
13         }

 

 這里主要的就是這句 row.reverse().

 

5,過程⑤,zxing 對行數據二值化的代碼如:

 

 1 public BitArray getBlackRow(int y, BitArray row) throws NotFoundException {
 2     LuminanceSource source = getLuminanceSource();
 3     int width = source.getWidth();
 4     if (row == null || row.getSize() < width) {
 5       row = new BitArray(width);
 6     } else {
 7       row.clear();
 8     }
 9 
10     initArrays(width);
11     //在這里 luminances  被賦值
12     //y=180;
13     byte[] localLuminances = source.getRow(y, luminances);
14     //統計並建立直方圖
15     int[] localBuckets = buckets;
16     /*---------modify here ,just for analysis .--------------------*/
17     int i_row[] = new int [width];
18     for(int x=0;x<width;x++){
19         i_row[x]=localLuminances[x] & 0xff;
20     }
21     /*-------------------------------------------------------------*/
22     for (int x = 0; x < width; x++) {
23       int pixel = localLuminances[x] & 0xff;
24       localBuckets[pixel >> LUMINANCE_SHIFT]++;
25     }
26     //According to the histogram, find the threshold.
27     int blackPoint = estimateBlackPoint(localBuckets);
28     //Based on threshold ,set 1 bit.  21.1.2014
29     int left = localLuminances[0] & 0xff;
30     int center = localLuminances[1] & 0xff;
31     for (int x = 1; x < width - 1; x++) {
32       int right = localLuminances[x + 1] & 0xff;
33       // A simple -1 4 -1 box filter with a weight of 2.
34       int luminance = ((center << 2) - left - right) >> 1;
35       if (luminance < blackPoint) {
36         row.set(x);
37       }
38       left = center;
39       center = right;
40     }
41     return row;
42   }

 

代碼的基本過程包括,統計並建立直方圖-->根據直方圖找到白與黑的中間閥值-->大於閥值的為白色,反之黑色.在這個方法中, int luminance = ((center << 2) - left - right) >> 1; 這句代碼我起初理解為對圖像進行降噪處理,后來經過數學推導不合理,所以便在zxing group 進行了提問,zxing 成員給予了熱心的回復:

 

6,過程⑥,zxing 黑白閥值的尋找比較巧妙,其代碼如下 :

 

 1  private static int estimateBlackPoint(int[] buckets) throws NotFoundException {
 2     // Find the tallest peak in the histogram.
 3     int numBuckets = buckets.length;
 4     int maxBucketCount = 0;
 5     int firstPeak = 0;
 6     int firstPeakSize = 0;
 7     //找到  數組中最多像素點的個數(maxBucketCount)  和  對應的 序號,序號其實就是亮度值(區間)firstPeak.
 8     //這里的 firstPeak 即可能是  黑色的區間,也可能是白色的區間.
 9     for (int x = 0; x < numBuckets; x++) {
10       if (buckets[x] > firstPeakSize) {
11         firstPeak = x;
12         firstPeakSize = buckets[x];
13       }
14       if (buckets[x] > maxBucketCount) {
15         maxBucketCount = buckets[x];
16       }
17     }
18 
19     // Find the second-tallest peak which is somewhat far from the tallest peak.
20     int secondPeak = 0;
21     int secondPeakScore = 0;
22     for (int x = 0; x < numBuckets; x++) {
23       int distanceToBiggest = x - firstPeak;
24       // Encourage more distant second peaks by multiplying by square of distance.
25       int score = buckets[x] * distanceToBiggest * distanceToBiggest;
26       if (score > secondPeakScore) {
27         secondPeak = x;
28         secondPeakScore = score;
29       }
30     }
31 
32     // Make sure firstPeak corresponds to the black peak.
33     if (firstPeak > secondPeak) {
34       int temp = firstPeak;
35       firstPeak = secondPeak;
36       secondPeak = temp;
37     }
38 
39     // If there is too little contrast in the image to pick a meaningful black point, throw rather
40     // than waste time trying to decode the image, and risk false positives.
41     if (secondPeak - firstPeak <= numBuckets >> 4) {
42       throw NotFoundException.getNotFoundInstance();
43     }
44 
45     // Find a valley between them that is low and closer to the white peak.
46     int bestValley = secondPeak - 1;
47     int bestValleyScore = -1;
48     for (int x = secondPeak - 1; x > firstPeak; x--) {
49       int fromFirst = x - firstPeak;
50       //這里找 到兩個峰比較遠,同時又點少的區間.這里  fromFist 采用平方的形式,可以理解更接近於白色.
51       int score = fromFirst * fromFirst * (secondPeak - x) * (maxBucketCount - buckets[x]);
52       if (score > bestValleyScore) {
53         bestValley = x;
54         bestValleyScore = score;
55       }
56     }
57 
58     return bestValley << LUMINANCE_SHIFT;
59   }

 

代碼還是比較清晰的,因為對於一維碼和二維碼,圖像的主要顏色為黑白色,所以直方圖呈現雙峰結構.知道這個特性,其算法思想也就明子了.基本過程:根據直方圖找到最多的像素區域--->根據 int score = buckets[x] * distanceToBiggest*distanceToBiggest 這個評分,找到第二個像素區域.可以理解為距離白色(黑色)比較遠且個數比較多的像素區域.--->根據 int score = fromFirst * fromFirst * (secondPeak - x) * (maxBucketCount - buckets[x]);這個評分,找到距離兩個峰比較遠且比較少的區域為閥值. 這里fromFist 采用平方的形式,可以理解更接近於白色.距離比重越大,其閥值便越靠近另外一個峰.

代碼分析基本到這里,我這里將對zxing 原碼進行修改,進行二次開發應用到其它方面.

2014.2.13

  經過一段時間的程序開發,基於android 的數卡程序終於完工了,為了解化程序的開發,這里手機照的照片再打印出來,如下圖:

 

 下面的日志顯示了程序的識別效果,識別左上角的卡片.

  02-13 18:22:13.438: D/Correct rate(30802): Successful frequency 4.171029.
  02-13 18:22:13.478: D/Correct rate(30802): The times of success 985,The times of failed 6,The correct rate 0.99394554.
  02-13 18:22:13.478: D/Correct rate(30802): Successful frequency 4.174507.
  02-13 18:22:13.569: D/Correct rate(30802): The times of success 986,The times of failed 6,The correct rate 0.9939516.
  02-13 18:22:13.569: D/Correct rate(30802): Successful frequency 4.177081.

從日志里可以看出,識別能夠得到不錯的效果. 使用時要求卡片明亮清潔,掃描背景最好為黑灰色.卡片數量大於3 張小於50張.

 程序算法及思想:

  程序先根據二值化后的數據,先均勻先取5 行-->找總亮度值最小的前3行-->根據第2和3行號  取2行數據 -->找到這兩行的起始位置

   -->從起始位置找 白色卡片的數量(這里是經過二值后的數據) -->  如果最終兩行計算的數據相等則返回結果 和結束位置.

本人面向對象的編程能力實在不敢恭維,android 程序寫出了 c 語言風格.所以我這里便不公開源碼,代碼寫的很亂.

這里本人上傳 打包的apk 程序和圖片素材.

 安裝包和測試文件安裝包和測試圖片4.rar

算法經過優化之后的安裝包和測試文件5.rar

功能展示

 最佳性能測試視頻. 距離圖片50cm ,純黑白條件下,白色條紋寬3mm 縫隙為 0.8mm

 

 

博文為本人所寫,轉載請表明出處,博客園夢工廠2012.

推薦閱讀

  http://blog.csdn.net/doonly2009/article/details/12175997

  http://kuangjianwei.blog.163.com/blog/static/190088953201361015055110/

  http://www.cnblogs.com/zdwillie/p/3331250.html

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM