Android二維碼識別

什么是二維碼

Android系統(tǒng)最常用的二維碼開源庫ZXing,借助ZXing來深入了解一下二維碼識別機(jī)制。在這之前有必要了解二維碼的組成,以最常用的QRCode(快速識別二維碼)為例,


二維碼構(gòu)成.png

二維碼的生成有其通用的編碼規(guī)范,上圖所示為一個二維碼的基本組成部分。 除了尋象圖形、校正圖形及定位圖形用于幫助定位外,格式信息包含糾錯水平、掩碼類型等信息,識別的過程就是根據(jù)這些信息對數(shù)據(jù)及糾錯碼字區(qū)域進(jìn)程解碼,當(dāng)然解碼涉及的算法比較復(fù)雜。文章重點在于對二維碼識別及解析流程作梳理,算法的部分暫時不做深究。

獲取相機(jī)預(yù)覽幀

對二維碼有了一個大概的認(rèn)知接下來就可以愉(jian)快(nan)地分析ZXing源碼了。去github把代碼clone下來之后,對于安卓項目只須編譯android和core這兩個包下的源碼即可,如圖編譯后的項目結(jié)構(gòu)如下


項目結(jié)構(gòu).png

幾個核心類CaptureActivity、CaptureActivityHandler、DecodeThread、DecodeHandler、QRCodeReader、PlanarYUVLuminanceSource、HybridBinarizer、BitMatrix、Detector、Decoder

在項目入口CaptureActivity里對相機(jī)進(jìn)行初始化

private void initCamera(SurfaceHolder surfaceHolder) {
   ...
      cameraManager.openDriver(surfaceHolder);
      // Creating the handler starts the preview, which can also throw a RuntimeException.
      if (handler == null) {
        handler = new CaptureActivityHandler(this, decodeFormats, decodeHints, characterSet, cameraManager);
      }
    ...

在CaptureActivityHandler構(gòu)造方法里初始化并開啟子線程DecodeThread,后面可以看到DecodeThread里構(gòu)建了一個消息處理器DecodeHandler,開始監(jiān)聽獲取到的每一幀圖像。

CaptureActivityHandler(CaptureActivity activity,
                         Collection<BarcodeFormat> decodeFormats,
                         Map<DecodeHintType,?> baseHints,
                         String characterSet,
                         CameraManager cameraManager) {
    this.activity = activity;
    decodeThread = new DecodeThread(activity, decodeFormats, baseHints, characterSet,
        new ViewfinderResultPointCallback(activity.getViewfinderView()));
    decodeThread.start();
    state = State.SUCCESS;

    // Start ourselves capturing previews and decoding.
    this.cameraManager = cameraManager;
    //開始掃描流程
    cameraManager.startPreview();
    restartPreviewAndDecode();
  }

private void restartPreviewAndDecode() {
    ...
      cameraManager.requestPreviewFrame(decodeThread.getHandler(), R.id.decode);
     ...
  }

相機(jī)開始掃描,并為相機(jī)設(shè)置預(yù)覽幀回調(diào),這里的handler就是DecodeHandler,消息id為R.id.decode,相機(jī)在獲取到一幀圖像后會發(fā)送消息,由DecodeHandler處理。

public synchronized void requestPreviewFrame(Handler handler, int message) {
    OpenCamera theCamera = camera;
    if (theCamera != null && previewing) {
      //DecodeHandler   R.id.decode
      previewCallback.setHandler(handler, message);
      theCamera.getCamera().setOneShotPreviewCallback(previewCallback);
    }
  }

下面為PreviewCallback的回調(diào)處理,thePreviewHandler即為DecodeHandler

@Override
  public void onPreviewFrame(byte[] data, Camera camera) {
    Point cameraResolution = configManager.getCameraResolution();
    Handler thePreviewHandler = previewHandler;
    if (cameraResolution != null && thePreviewHandler != null) {
      Message message = thePreviewHandler.obtainMessage(previewMessage, cameraResolution.x,
          cameraResolution.y, data);
      message.sendToTarget();
      previewHandler = null;
    } else {
      Log.d(TAG, "Got preview callback, but no handler or resolution available");
    }
  }

解析預(yù)覽幀

預(yù)覽到的一幀圖像將會回調(diào)到DecodeHandler,在DecodeHandler里開始解析這一幀圖像。在解析前首先會對數(shù)據(jù)源進(jìn)行封裝,對于計算機(jī)而言一般需要用矩陣來表示一個二維圖像,圖像的每一個像素都只是這個矩陣中的一個元素??梢酝ㄟ^PlanarYUVLuminanceSource這個類將相機(jī)的yuv數(shù)據(jù)源抽象成矩陣的形式,以便后續(xù)的解析。

private void decode(byte[] data, int width, int height) {
   ...
    Result rawResult = null;
    PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(data, width, height);
    if (source != null) {
      BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
      try {
        //尋找相應(yīng)的解碼器
        rawResult = multiFormatReader.decodeWithState(bitmap);
      } catch (ReaderException re) {
        // continue
      } finally {
        multiFormatReader.reset();
      }
    ...
    }

multiFormatReader.decodeWithState(bitmap)這里會遍歷各種格式的解碼器,直到找到相應(yīng)的解碼器,對于二維碼而言對應(yīng)的解碼器是QRCodeReader,到這里開始解碼的核心步驟,

@Override
  public final Result decode(BinaryBitmap image, Map<DecodeHintType,?> hints)
      throws NotFoundException, ChecksumException, FormatException {
    DecoderResult decoderResult;
    ResultPoint[] points;
    ...
      //1.對yuv數(shù)據(jù)進(jìn)行二值化處理,最終返回01矩陣
      DetectorResult detectorResult = new Detector(image.getBlackMatrix()).detect(hints);
      //2.根據(jù)二維碼生成標(biāo)準(zhǔn)進(jìn)行解碼
      decoderResult = decoder.decode(detectorResult.getBits(), hints);
      points = detectorResult.getPoints();
    ...
二值化

可以看到上述代碼注釋1處image.getBlackMatrix,這里實際上會調(diào)用到HybridBinarizer.getBlackMatrix。HybridBinarizer可以翻譯為二值化器。經(jīng)過二值化處理后的圖像會由灰度或彩色圖像轉(zhuǎn)換為黑白圖像。二值化其實是為了簡化圖像表現(xiàn)形式,使圖像的形狀和輪廓更清晰,減少干擾信息,更易于計算機(jī)提取關(guān)鍵部分進(jìn)行處理。對于二維碼識別來說,轉(zhuǎn)換成黑白圖像后也剛好對應(yīng)二維碼的黑白像素塊。二值化的過程即通過一定的算法找到一個閾值,若當(dāng)前像素點小于這個閾值則取黑,然后將像素點在矩陣中的位置保存到BitMatrix。可以看到BitMatrix中矩陣的存取方式,x為行,y為列,返回true代表這一點為黑。

/**
   * <p>Gets the requested bit, where true means black.</p>
   *獲取指定像素點是否為黑
   * @param x The horizontal component (i.e. which column)
   * @param y The vertical component (i.e. which row)
   * @return value of given bit in matrix
   */
  public boolean get(int x, int y) {
    int offset = y * rowSize + (x / 32);
    return ((bits[offset] >>> (x & 0x1f)) & 1) != 0;
  }

  /**
   * <p>Sets the given bit to true.</p>
   *將黑色像素點置為true
   * @param x The horizontal component (i.e. which column)
   * @param y The vertical component (i.e. which row)
   */
  public void set(int x, int y) {
    int offset = y * rowSize + (x / 32);
    bits[offset] |= 1 << (x & 0x1f);
  }
定位二維碼區(qū)域

在對捕捉到的圖像進(jìn)行二值化處理之后,開始對圖像中二維碼的進(jìn)行定位。仍然看上述代碼注釋1處,后半部分Detector.detect方法,會首先定位到三個尋象圖形及校正符的位置,進(jìn)而找到圖片中二維碼的位置。尋像圖形水平方向黑/白/黑/白黑的比例為1:1:3:1:1,按照這個比例找到尋象圖形的大概位置。

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);
    //根據(jù)定位圖形,尋找校正圖形及數(shù)據(jù)區(qū)域
    return processFinderPatternInfo(info);
  }
解碼

拿到0、1矩陣后就可以根據(jù)二維碼生成規(guī)范進(jìn)行反向解碼。

private DecoderResult decode(BitMatrixParser parser, Map<DecodeHintType,?> hints)
      throws FormatException, ChecksumException {
    //讀取版本信息
    Version version = parser.readVersion();
    //讀取糾錯水平
    ErrorCorrectionLevel ecLevel = parser.readFormatInformation().getErrorCorrectionLevel();

    // Read codewords  去掩碼后的數(shù)據(jù)區(qū)
    byte[] codewords = parser.readCodewords();
    // Separate into data blocks  獲取數(shù)據(jù)塊
    DataBlock[] dataBlocks = DataBlock.getDataBlocks(codewords, version, ecLevel);

    // Count total number of data bytes
    int totalBytes = 0;
    for (DataBlock dataBlock : dataBlocks) {
      totalBytes += dataBlock.getNumDataCodewords();
    }
    byte[] resultBytes = new byte[totalBytes];
    int resultOffset = 0;

    // Error-correct and copy data blocks together into a stream of bytes  
    //編碼時對數(shù)據(jù)碼進(jìn)行分組,也就是分成不同的Block,然后對各個Block進(jìn)行糾錯編碼
    for (DataBlock dataBlock : dataBlocks) {
      byte[] codewordBytes = dataBlock.getCodewords();
      int numDataCodewords = dataBlock.getNumDataCodewords();
      correctErrors(codewordBytes, numDataCodewords);
      for (int i = 0; i < numDataCodewords; i++) {
        resultBytes[resultOffset++] = codewordBytes[i];
      }
    }

    // Decode the contents of that stream of bytes
    return DecodedBitStreamParser.decode(resultBytes, version, ecLevel, hints);
  }

拿到版本、糾錯水平這些輔助信息后開始數(shù)據(jù)區(qū)進(jìn)行解碼,二維碼支持的類型主要有數(shù)字編碼、字符編碼、字節(jié)編碼、日文編碼,還有其他的混合編碼以及一些特殊用途的編碼。

static DecoderResult decode(byte[] bytes,
                              Version version,
                              ErrorCorrectionLevel ecLevel,
                              Map<DecodeHintType,?> hints) throws FormatException {
    BitSource bits = new BitSource(bytes);
    StringBuilder result = new StringBuilder(50);
    List<byte[]> byteSegments = new ArrayList<>(1);
    int symbolSequence = -1;
    int parityData = -1;

    try {
      CharacterSetECI currentCharacterSetECI = null;
      boolean fc1InEffect = false;
      Mode mode;
      do {
        // While still another segment to read...
        if (bits.available() < 4) {
          // OK, assume we're done. Really, a TERMINATOR mode should have been recorded here
          mode = Mode.TERMINATOR;
        } else {
          mode = Mode.forBits(bits.readBits(4)); // mode is encoded by 4 bits
        }
        switch (mode) {
          case TERMINATOR:
            break;
          case FNC1_FIRST_POSITION:
          case FNC1_SECOND_POSITION:
            // We do little with FNC1 except alter the parsed result a bit according to the spec
            fc1InEffect = true;
            break;
          case STRUCTURED_APPEND://混合編碼
            if (bits.available() < 16) {
              throw FormatException.getFormatInstance();
            }
            // sequence number and parity is added later to the result metadata
            // Read next 8 bits (symbol sequence #) and 8 bits (parity data), then continue
            symbolSequence = bits.readBits(8);
            parityData = bits.readBits(8);
            break;
          case ECI://特殊字符集
            // Count doesn't apply to ECI
            int value = parseECIValue(bits);
            currentCharacterSetECI = CharacterSetECI.getCharacterSetECIByValue(value);
            if (currentCharacterSetECI == null) {
              throw FormatException.getFormatInstance();
            }
            break;
          case HANZI:
            // First handle Hanzi mode which does not start with character count
            // Chinese mode contains a sub set indicator right after mode indicator
            int subset = bits.readBits(4);
            int countHanzi = bits.readBits(mode.getCharacterCountBits(version));
            if (subset == GB2312_SUBSET) {
              decodeHanziSegment(bits, result, countHanzi);
            }
            break;
          default:
            // "Normal" QR code modes:
            // How many characters will follow, encoded in this mode?
            int count = bits.readBits(mode.getCharacterCountBits(version));
            switch (mode) {
              case NUMERIC://數(shù)字
                decodeNumericSegment(bits, result, count);
                break;
              case ALPHANUMERIC://字符,字母和數(shù)字組成
                decodeAlphanumericSegment(bits, result, count, fc1InEffect);
                break;
              case BYTE://字節(jié)   比如漢字,通常使用這種
                decodeByteSegment(bits, result, count, currentCharacterSetECI, byteSegments, hints);
                break;
              case KANJI://日語
                decodeKanjiSegment(bits, result, count);
                break;
              default:
                throw FormatException.getFormatInstance();
            }
            break;
        }
      } while (mode != Mode.TERMINATOR);
    } catch (IllegalArgumentException iae) {
      // from readBits() calls
      throw FormatException.getFormatInstance();
    }

    return new DecoderResult(bytes,
                             result.toString(),
                             byteSegments.isEmpty() ? null : byteSegments,
                             ecLevel == null ? null : ecLevel.toString(),
                             symbolSequence,
                             parityData);
  }
image.png

最終返回DecoderResult,終于見到了我們熟悉的字符串text。取到這個結(jié)果之后,比如一個url,就可以打開系統(tǒng)瀏覽器或者進(jìn)行自定義處理了。

參考文章:
二維碼的生成細(xì)節(jié)和原理
如何筆算解碼二維碼
zxing源碼分析——QR碼部分
機(jī)器視覺的圖像二值化詳細(xì)分析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容