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

二維碼的生成有其通用的編碼規(guī)范,上圖所示為一個二維碼的基本組成部分。 除了尋象圖形、校正圖形及定位圖形用于幫助定位外,格式信息包含糾錯水平、掩碼類型等信息,識別的過程就是根據(jù)這些信息對數(shù)據(jù)及糾錯碼字區(qū)域進(jìn)程解碼,當(dāng)然解碼涉及的算法比較復(fù)雜。文章重點在于對二維碼識別及解析流程作梳理,算法的部分暫時不做深究。
獲取相機(jī)預(yù)覽幀
對二維碼有了一個大概的認(rèn)知接下來就可以愉(jian)快(nan)地分析ZXing源碼了。去github把代碼clone下來之后,對于安卓項目只須編譯android和core這兩個包下的源碼即可,如圖編譯后的項目結(jié)構(gòu)如下

幾個核心類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);
}

最終返回DecoderResult,終于見到了我們熟悉的字符串text。取到這個結(jié)果之后,比如一個url,就可以打開系統(tǒng)瀏覽器或者進(jìn)行自定義處理了。
參考文章:
二維碼的生成細(xì)節(jié)和原理
如何筆算解碼二維碼
zxing源碼分析——QR碼部分
機(jī)器視覺的圖像二值化詳細(xì)分析