ZXing源碼解析四:如何識別圖片中的二維碼

不知道你們在用ZXing做爲掃碼庫的時候,有沒有想過「ZXing是怎麼從相機捕獲的每一幀圖片中獲取到二維碼並解析的呢?」,若是你思考過而且已經從源碼中知道了答案,那麼這篇文章你就不必讀下去了,若是你思考過殊不知道答案,那麼這篇文章就是爲你準備的,相信你讀事後會有一個清晰的答案。java

  爲了避免那麼突兀,仍是先跟着源碼來一步步的講解,先來看怎樣獲取到相機捕獲到的圖片的數據的。git

獲取相機捕獲到的數據

  由於前面的文章已經分析過ZXing解碼的步驟了,這裏就重點看下,相機捕獲到圖像的後續步驟,源碼以下github

public void restartPreviewAndDecode() {
    if (state == State.SUCCESS) {
      state = State.PREVIEW;
      cameraManager.requestPreviewFrame(decodeThread.getHandler(), R.id.decode);
      activity.drawViewfinder();
    }
  }
複製代碼

上面的代碼是在CaptureActivityHandler構造方法中調用的,也就是在CaptureActivityHandler實例化的時候調用。而後,調用到了cameraManagerrequestPreviewFrame方法,代碼以下數組

/** * A single preview frame will be returned to the handler supplied. The data will arrive as byte[] * in the message.obj field, with width and height encoded as message.arg1 and message.arg2, * respectively. * * @param handler The handler to send the message to. * @param message The what field of the message to be sent. */
  public synchronized void requestPreviewFrame(Handler handler, int message) {
    OpenCamera theCamera = camera;
    if (theCamera != null && previewing) {
      previewCallback.setHandler(handler, message);
      theCamera.getCamera().setOneShotPreviewCallback(previewCallback);
    }
  }
複製代碼

如今來分析一下上面的代碼,重點來看下這句app

theCamera.getCamera().setOneShotPreviewCallback(previewCallback);
複製代碼

這句代碼的做用就是設置一個預覽幀的回調,意思就是相機每捕獲一幀數據就會調用,這裏設置的previewCallback中的方法,經分析,最終調用previewCallback中的方法是public void onPreviewFrame(byte[] data, Camera camera),這裏的第一個參數就是每一幀圖像的數據即byte數組。Android 中Google支持的Camera Preview CallBack的YUV經常使用格式有兩種:一種是NV21,一種是YV12,Android通常默認使用的是YCbCR_420_sp(NV21),固然,也能夠經過下面的代碼來設置本身須要的格式。post

Camera.Parameters parameters = camera.getParameters();
parameters.setPreviewFormat(ImageFormat.NV21);
camera.setParameters(parameters);
複製代碼

ZXing庫中並無設置格式,因此這裏默認的是NV21格式。那麼問題來了,NV21究竟是什麼意思呢?欲知詳情,請繼續閱讀下文性能

YUV圖片格式詳解

YUV是一種顏色編碼方法,和它等同的還有 RGB 顏色編碼方法。測試

  RGB 圖像中,每一個像素點都有紅、綠、藍三個原色,其中每種原色都佔用 8 bit,也就是一個字節,那麼一個像素點也就佔用 24 bit,也就是三個字節。一張 1280 * 720 大小的圖片,就佔用 1280 * 720 * 3 / 1024 / 1024 = 2.63 MB 存儲空間。    YUV顏色編碼採用的是 明亮度 和 色度 來指定像素的顏色。其中,Y 表示明亮度(Luminance、Luma),而U和V表示色度(Chrominance、Chroma)。而色度又定義了顏色的兩個方面:色調和飽和度。ui

  上文的NV21YV12是YUV存儲格式。編碼

  • NV21格式屬於YUV420SP類型。它也是先存儲了Y份量,但接下來並非再存儲全部的U或者V份量,而是把UV 份量交替連續存儲。
  • YV12格式屬於YUV420P類型,即先存儲Y份量,再存儲U、V份量,YV12是先Y再V後U。

關於YUV格式的介紹,網上有一篇比較好的文章,點擊這裏查看。對YUV格式有必定的瞭解以後,繼續來分析源碼,看下,是怎樣從圖片中識別二維碼的。

識別圖片中的二維碼

  上文已經知道,相機每獲取一幀的數據都會回調PreviewCallback類中的onPreviewFrame方法,在此方法中,利用Handler的機制,將圖片轉換成的字節數組傳遞給了DecodeHandler類,而後調用了decode方法,代碼以下

private void decode(byte[] data, int width, int height) {
    long start = System.nanoTime();
    //...省略部分代碼

    Result rawResult = null;
    PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(data, width, height);
    if (source != null) {
      BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source));
      try {
        rawResult = multiFormatReader.decodeWithState(bitmap);
      } catch (ReaderException re) {
        // continue
        Log.e(TAG, "decode: 沒有發現二維碼" );
      } finally {
        multiFormatReader.reset();
      }
    }

    //...省略部分代碼
  }
複製代碼

這部分代碼能夠說是ZXing解碼的核心代碼了,如今一點點的來分析,先看

PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(data, width, height);
複製代碼

這句代碼,實例化了PlanarYUVLuminanceSource對象,主要的目的是獲取掃碼框中的圖像的數據。在將圖像進行二值化的時候會調用此對象中的方法,稍後會在源碼中介紹。 再看這句代碼

BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source));
複製代碼

這句代碼,嗯,先看new GlobalHistogramBinarizer(source)這句代碼,GlobalHistogramBinarizer圖像的數據就是在這個類中進行二值化的,固然還有一個HybridBinarizer類,這個類也是將圖像二值化的,那主要的區別是什麼呢?主要的區別就是HybridBinarizer類處理的比GlobalHistogramBinarizer精確,可是處理的速度較慢,推薦在性能比較好的手機上使用,而GlobalHistogramBinarizer處理的不太精確,若有陰影的化,可能處理的圖片就會有問題,可是速度較快,推薦在性能不太好的手機上使用。 這裏,咱們用的是GlobalHistogramBinarizer來對圖像進行二值化處理,由於,通過我測試發現,這個速度快點。

  再來看整句的代碼,就是實例化了BinaryBitmap類,而後將GlobalHistogramBinarizer對象注入。

  下面的代碼就是從圖像中發現二維碼並解析,代碼以下

try {
        rawResult = multiFormatReader.decodeWithState(bitmap);
      } catch (ReaderException re) {
        // continue
        Log.e(TAG, "decode: 沒有發現二維碼" );
      } finally {
        multiFormatReader.reset();
      }
複製代碼

跟蹤下去,發現最終會調用QRCodeReader類中的decode(BinaryBitmap image, Map<DecodeHintType,?> hints)方法。代碼以下

public final Result decode(BinaryBitmap image, Map<DecodeHintType,?> hints) throws NotFoundException, ChecksumException, FormatException {
    DecoderResult decoderResult;
    ResultPoint[] points;
    if (hints != null && hints.containsKey(DecodeHintType.PURE_BARCODE)) {
      BitMatrix bits = extractPureBits(image.getBlackMatrix());
      decoderResult = decoder.decode(bits, hints);
      points = NO_POINTS;
    } else {
      // 會進入這段代碼
      DetectorResult detectorResult = new Detector(image.getBlackMatrix()).detect(hints);
      decoderResult = decoder.decode(detectorResult.getBits(), hints);
      points = detectorResult.getPoints();
    }

    // If the code was mirrored: swap the bottom-left and the top-right points.
    if (decoderResult.getOther() instanceof QRCodeDecoderMetaData) {
      ((QRCodeDecoderMetaData) decoderResult.getOther()).applyMirroredCorrection(points);
    }

    Result result = new Result(decoderResult.getText(), decoderResult.getRawBytes(), points, BarcodeFormat.QR_CODE);
    List<byte[]> byteSegments = decoderResult.getByteSegments();
    if (byteSegments != null) {
      result.putMetadata(ResultMetadataType.BYTE_SEGMENTS, byteSegments);
    }
    String ecLevel = decoderResult.getECLevel();
    if (ecLevel != null) {
      result.putMetadata(ResultMetadataType.ERROR_CORRECTION_LEVEL, ecLevel);
    }
    if (decoderResult.hasStructuredAppend()) {
      result.putMetadata(ResultMetadataType.STRUCTURED_APPEND_SEQUENCE,
                         decoderResult.getStructuredAppendSequenceNumber());
      result.putMetadata(ResultMetadataType.STRUCTURED_APPEND_PARITY,
                         decoderResult.getStructuredAppendParity());
    }
    return result;
  }
複製代碼

來看

DetectorResult detectorResult = new Detector(image.getBlackMatrix()).detect(hints);
複製代碼

這句代碼。image.getBlackMatrix()就是調用GlobalHistogramBinarizer類中的getBlackMatrix方法,其中的代碼就不看了,getBlackMatrix方法的主要做用就是將圖片進行二值化的處理,二值化的關鍵就是定義出黑白的界限,咱們的圖像已經轉化爲了灰度圖像,每一個點都是由一個灰度值來表示,就須要定義出一個灰度值,大於這個值就爲白(0),低於這個值就爲黑(1)。具體的處理方法以下

在 GlobalHistogramBinarizer中,是從圖像中均勻取5行(覆蓋整個圖像高度),每行取中間五分之四做爲樣本;以灰度值爲X軸,每一個灰度值的像素個數爲Y軸創建一個直方圖,從直方圖中取點數最多的一個灰度值,而後再去給其餘的灰度值進行分數計算,按照點數乘以與最多點數灰度值的距離的平方來進行打分,選分數最高的一個灰度值。接下來在這兩個灰度值中間選取一個區分界限,取的原則是儘可能靠近中間而且要點數越少越好。界限有了之後就容易了,與整幅圖像的每一個點進行比較,若是灰度值比界限小的就是黑,在新的矩陣中將該點置1,其他的就是白,爲0。

上面一句的代碼,調用了Detector中的detect(Map<DecodeHintType,?> hints)方法,代碼以下

/** * <p>Detects a QR Code in an image.</p> * * @param hints optional hints to detector * @return {@link DetectorResult} encapsulating results of detecting a QR Code * @throws NotFoundException if QR Code cannot be found * @throws FormatException if a QR Code cannot be decoded */
  public final DetectorResult detect(Map<DecodeHintType,?> hints) throws NotFoundException, FormatException {

    resultPointCallback = hints == null ? null :
        (ResultPointCallback) hints.get(DecodeHintType.NEED_RESULT_POINT_CALLBACK);

    FinderPatternFinder finder = new FinderPatternFinder(image, resultPointCallback);
    FinderPatternInfo info = finder.find(hints);

    return processFinderPatternInfo(info);
  }
複製代碼

從這段代碼的註釋中能夠得知,這個方法的做用就是「封裝檢測二維碼的結果」,若是沒有發現二維碼就會拋出NotFoundException異常,若是不能解析二維碼就會拋出FormatException異常。如今,咱們來看怎樣找到圖像中的二維碼的。

二維碼的特徵

  在介紹發現圖片中二維碼的方法以前,先來看下二維碼的特色,以下圖

二維碼在設計之初就考慮到了識別問題,因此二維碼有一些特徵是很是明顯的。

二維碼有三個「回「字形圖案,這一點很是明顯。中間的一個點位於圖案的左上角,若是圖像偏轉,也能夠根據二維碼來糾正。

識別二維碼,就是識別二維碼的三個點,逐步分析一下這三個點的特性

  1. 每一個點有兩個輪廓。就是兩個口,大「口」內部有一個小「口」,因此是兩個輪廓。
  2. 若是把這個「回」放到一個白色的背景下,從左到右,或從上到下畫一條線。這條線通過的圖案黑白比例大約爲:黑白比例爲1:1:3:1:1。以下圖
  3. 如何找到左上角的頂點?這個頂點與其餘兩個頂點的夾角爲90度。

經過上面幾個步驟,就能識別出二維碼的三個頂點,而且識別出左上角的頂點。

ZXing識別圖像中的二維碼

  上面已經介紹了二維碼的特徵,也介紹了怎樣發現二維碼的「回」字,如今,咱們來看下ZXing是怎麼識別圖片中的二維碼的,主要的代碼以下

final FinderPatternInfo find(Map<DecodeHintType,?> hints) throws NotFoundException {
    boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER);
    int maxI = image.getHeight();
    int maxJ = image.getWidth();
    // 在圖像中尋找黑白像素比例爲1:1:3:1:1
    int iSkip = (3 * maxI) / (4 * MAX_MODULES);
    if (iSkip < MIN_SKIP || tryHarder) {
      iSkip = MIN_SKIP;
    }

    boolean done = false;
    int[] stateCount = new int[5];
    for (int i = iSkip - 1; i < maxI && !done; i += iSkip) {
      // 獲取一行的黑白像素值
      clearCounts(stateCount);
      int currentState = 0;
      for (int j = 0; j < maxJ; j++) {
        if (image.get(j, i)) {
          // 黑色像素
          if ((currentState & 1) == 1) { // Counting white pixels
            currentState++;
          }
          stateCount[currentState]++;
        } else { // 白色像素
          if ((currentState & 1) == 0) { // Counting black pixels
            if (currentState == 4) { // A winner?
              if (foundPatternCross(stateCount)) { // Yes 是不是二維碼左上角的回字
                boolean confirmed = handlePossibleCenter(stateCount, i, j);
                if (confirmed) {
                  // Start examining every other line. Checking each line turned out to be too
                  // expensive and didn't improve performance.
                  iSkip = 2;
                  if (hasSkipped) {
                    done = haveMultiplyConfirmedCenters();
                  } else {
                    int rowSkip = findRowSkip();
                    if (rowSkip > stateCount[2]) {
                      // Skip rows between row of lower confirmed center
                      // and top of presumed third confirmed center
                      // but back up a bit to get a full chance of detecting
                      // it, entire width of center of finder pattern
                      // Skip by rowSkip, but back off by stateCount[2] (size of last center
                      // of pattern we saw) to be conservative, and also back off by iSkip which
                      // is about to be re-added
                      i += rowSkip - stateCount[2] - iSkip;
                      j = maxJ - 1;
                    }
                  }
                } else {
                  shiftCounts2(stateCount);
                  currentState = 3;
                  continue;
                }
                // Clear state to start looking again
                currentState = 0;
                clearCounts(stateCount);
              } else { // No, shift counts back by two
                shiftCounts2(stateCount);
                currentState = 3;
              }
            } else {
              stateCount[++currentState]++;
            }
          } else { // Counting white pixels
            stateCount[currentState]++;
          }
        }
      }
      if (foundPatternCross(stateCount)) {
        boolean confirmed = handlePossibleCenter(stateCount, i, maxJ);
        if (confirmed) {
          iSkip = stateCount[0];
          if (hasSkipped) {
            // Found a third one
            done = haveMultiplyConfirmedCenters();
          }
        }
      }
    }
    FinderPattern[] patternInfo = selectBestPatterns();
    ResultPoint.orderBestPatterns(patternInfo);
    return new FinderPatternInfo(patternInfo);
  }
複製代碼

上面的代碼主要作了下面的事

一、尋找定位符

  在圖像中每隔iSkip就採樣一行,

int iSkip = (3 * maxI) / (4 * MAX_MODULES);
複製代碼

  在這一行中將連續的相同顏色的像素個數計入數組中,數組長度爲5位,即去找黑\白\黑\白\黑的圖像(如開始檢測到黑色計入數組[0],直到檢測到白色以前都將數組[0]的值+1;檢測到白色了就開始在數組[1]中計數,以此類推)。填滿5位後檢測這5位中像素個數是否比例爲1:1:3:1:1(能夠有50%的偏差範圍),若是知足條件就說明找到了定位符的大概位置,將這個圖像交給handlePossibleCenter方法去找到定位符的中心點,方法是先從垂直方向檢測是否知足定位符的條件,如知足就定出Y軸的中心點座標值,而後用這個座標值去再次檢測水平方向是否知足定位符條件,如知足就定出X軸的中心點座標值。至此就找到了一個定位符的中心座標。

  按照上面所說的步驟找出全部三個定位符的中心座標,接下來開始定位三個定位符在符號中的位置,即左上(B點)、左下(A點)、右上(C點)三個位置。先經過兩兩之間的距離定出哪一個是左上那一點(左上那點到其餘兩點的距離應該相差不遠),而後經過計算BA、BC向量的叉乘定出A和C兩點。

二、尋找校訂符

  經過ABC三點的座標計算出校訂符的可能位置,而後交給AlignmentPatternFinder去尋找最靠近右下角的那個校訂符,尋找方法與尋找定位符的方法基本相同,若是找到就返回校訂符的中心座標,若是沒有找到也不要緊,解碼程序能夠繼續。

經過上面的兩步就能夠判斷相機獲取的圖像幀中是否有二維碼了,若是有二維碼則進行二維碼的解析,沒有二維碼就拋出異常,而後繼續解析下一幀圖像數據。

總結

  經過上文的講解和源碼的分析,咱們能夠知道判斷圖像幀中是否有二維碼須要通過如下幾步:

  1. 獲取圖像幀的數據,格式爲YUV;
  2. 將二維碼掃碼框中的圖像數據進行灰度化處理;
  3. 將灰度化後的圖像進行二值化處理;
  4. 根據二維碼的特徵尋找定位符;
  5. 尋找二維碼的校訂符。

若是在步驟「4」中找到了校訂符,則說明這一幀圖片中含有二維碼,能夠進行二維碼的解析,不然就拋出異常,繼續解析下一幀圖像的數據。

結束語

  沒有看源碼以前,我是比較迷茫的,不知道怎樣才能判斷圖片中是否有二維碼,雖然知道能夠根據二維碼中的「回」字來判斷,可是不知道怎麼找到「回」字呀!閱讀源碼後才知道,能夠將圖片進行「二值化」處理,再根據黑白像素的比例來找到「回」字,感受學到了不少。因此呢,在咱們不知道某個庫的某個功能是怎樣實現的時候,最好的解決辦法就是閱讀源碼,答案都在源碼中。

  在研究源碼的時候刪除了好多與解析二維碼無關的代碼,最後的代碼在這裏

  該系列文章:

ZXing源碼解析一:讓源碼跑起來
ZXing源碼解析二:掌握解碼步驟
ZXing源碼解析三:相機的配置與數據的處理

本文已由公衆號「AndroidShared」首發

歡迎關注個人公衆號
掃碼關注公衆號,回覆「獲取資料」有驚喜
相關文章
相關標籤/搜索