不知道你們在用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
實例化的時候調用。而後,調用到了cameraManager
的requestPreviewFrame
方法,代碼以下數組
/** * 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是一種顏色編碼方法,和它等同的還有 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
上文的NV21
和YV12
是YUV存儲格式。編碼
關於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
異常。如今,咱們來看怎樣找到圖像中的二維碼的。
在介紹發現圖片中二維碼的方法以前,先來看下二維碼的特色,以下圖
二維碼在設計之初就考慮到了識別問題,因此二維碼有一些特徵是很是明顯的。二維碼有三個「回「字形圖案,這一點很是明顯。中間的一個點位於圖案的左上角,若是圖像偏轉,也能夠根據二維碼來糾正。
識別二維碼,就是識別二維碼的三個點,逐步分析一下這三個點的特性
經過上面幾個步驟,就能識別出二維碼的三個頂點,而且識別出左上角的頂點。
上面已經介紹了二維碼的特徵,也介紹了怎樣發現二維碼的「回」字,如今,咱們來看下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去
尋找最靠近右下角的那個校訂符,尋找方法與尋找定位符的方法基本相同,若是找到就返回校訂符的中心座標,若是沒有找到也不要緊,解碼程序能夠繼續。
經過上面的兩步就能夠判斷相機獲取的圖像幀中是否有二維碼了,若是有二維碼則進行二維碼的解析,沒有二維碼就拋出異常,而後繼續解析下一幀圖像數據。
經過上文的講解和源碼的分析,咱們能夠知道判斷圖像幀中是否有二維碼須要通過如下幾步:
若是在步驟「4」中找到了校訂符,則說明這一幀圖片中含有二維碼,能夠進行二維碼的解析,不然就拋出異常,繼續解析下一幀圖像的數據。
沒有看源碼以前,我是比較迷茫的,不知道怎樣才能判斷圖片中是否有二維碼,雖然知道能夠根據二維碼中的「回」字來判斷,可是不知道怎麼找到「回」字呀!閱讀源碼後才知道,能夠將圖片進行「二值化」處理,再根據黑白像素的比例來找到「回」字,感受學到了不少。因此呢,在咱們不知道某個庫的某個功能是怎樣實現的時候,最好的解決辦法就是閱讀源碼,答案都在源碼中。
在研究源碼的時候刪除了好多與解析二維碼無關的代碼,最後的代碼在這裏。
該系列文章:
本文已由公衆號「AndroidShared」首發