通过NDK实现Zxing二维码扫描放大功能

 在各个APP中几乎都能看见它的身影。二维码扫描虽然是一个普通的功能,但是在APP中占依然都放在很抢眼的位置。比如像淘宝、58这类APP主要解决PC端扫码登录,微信、支付宝这类主要使用支付,摩拜小蓝这类APP主要是用于扫码骑车。所以,能更快捷更方便的使用二维码,可以极大的提升用户体验。

##快捷与方便
 曾经或许会有那么一个寒冷的夜晚,你骑着刚共享单车飞驰在回家的路上,突然,街边飘来哈尔滨烤冷面的香气让你不知不觉的来到那个小摊面前,迟疑了很久,你说了一句:“老板,来一份。。。再。。。加个肠。”昏暗的灯光下、其他的食客中,老板熟练的抄起手中的家伙,你熟练掏出手机,打开扫一扫,老板的二维码在你的手里一次次对焦、一次次放大,“滴”的一声,付钱,取餐,嗝~。
 你有没有想过,为什么在扫描的过程中,被扫描的二维码会在扫描框里从模糊到清晰,然后再从清晰到模糊,周而复始?
 你有没有想过,为什么在稍远的距离,二维码扫描就会慢慢放大?
 是的,在这个寒冷的夜,为了能让你的手机更快速的识别出对方的二维码,我们做了很多。

##二维码解读

我们来看一下二维码扫描主要展示信息,主要分为功能图形和编码区格式。其中功能图形的位置探测图形、定位图形、矫正图形在定位的过程中起着举足轻重的地位。

##二维码扫描的源码解读
 从你拿出手机对准二维码直到扫描结束,整个过程都发生了什么?为了能让你生动的了解整个过程,我打算用下面的方式讲:
打开相机执行扫描->获取Byte流->灰度->二值化->霍夫变换->找到二维码定位点->识别->返回结果。

###打开相机执行扫描

private void initCamera(SurfaceHolder surfaceHolder) {
        if (surfaceHolder == null) {
            throw new IllegalStateException("No SurfaceHolder provided");
        }
        if (cameraManager.isOpen()) {
            Log.w(TAG, "initCamera() while already open -- late SurfaceView callback?");
            return;
        }
        try {
            cameraManager.openDriver(surfaceHolder);
            // Creating the handler starts the preview, which can also throw a
            // RuntimeException.
            if (handler == null) {
                handler = new CaptureActivityHandler(this, cameraManager, DecodeThread.ALL_MODE);
            }

            initCrop();
        } catch (IOException ioe) {
            Log.w(TAG, ioe);
            displayFrameworkBugMessageAndExit();
        } catch (RuntimeException e) {
            // Barcode Scanner has seen crashes in the wild of this variety:
            // java.?lang.?RuntimeException: Fail to connect to camera service
            Log.w(TAG, "Unexpected error initializing camera", e);
            displayFrameworkBugMessageAndExit();
        }
    }

###设置支持的二维码类型

目前二维码支持如下类型:
support-zxing-pic.png
代码在这里配置:

// The prefs can't change while the thread is running, so pick them up once here.
    if (decodeFormats == null || decodeFormats.isEmpty()) {
      SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
      decodeFormats = EnumSet.noneOf(BarcodeFormat.class);
      if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_1D_PRODUCT, true)) {
        decodeFormats.addAll(DecodeFormatManager.PRODUCT_FORMATS);
      }
      if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_1D_INDUSTRIAL, true)) {
        decodeFormats.addAll(DecodeFormatManager.INDUSTRIAL_FORMATS);
      }
      if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_QR, true)) {
        decodeFormats.addAll(DecodeFormatManager.QR_CODE_FORMATS);
      }
      if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_DATA_MATRIX, true)) {
        decodeFormats.addAll(DecodeFormatManager.DATA_MATRIX_FORMATS);
      }
      if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_AZTEC, false)) {
        decodeFormats.addAll(DecodeFormatManager.AZTEC_FORMATS);
      }
      if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_PDF417, false)) {
        decodeFormats.addAll(DecodeFormatManager.PDF417_FORMATS);
      }
    }
    hints.put(DecodeHintType.POSSIBLE_FORMATS, decodeFormats);

###获取相机预览的Byte流

public class PreviewCallback implements Camera.PreviewCallback {

	private static final String TAG = PreviewCallback.class.getSimpleName();

	private final CameraConfigurationManager configManager;
	private Handler previewHandler;
	private int previewMessage;

	public PreviewCallback(CameraConfigurationManager configManager) {
		this.configManager = configManager;
	}

	public void setHandler(Handler previewHandler, int previewMessage) {
		this.previewHandler = previewHandler;
		this.previewMessage = previewMessage;
	}

	@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");
		}
	}

}

//在这里通过相机框获取bitmap的byte流
PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(data, width, height);

BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));

###对bitmap的byte流解码

if (formats != null) {
  boolean addOneDReader =
	  formats.contains(BarcodeFormat.UPC_A) ||
	  formats.contains(BarcodeFormat.UPC_E) ||
	  formats.contains(BarcodeFormat.EAN_13) ||
	  formats.contains(BarcodeFormat.EAN_8) ||
	  formats.contains(BarcodeFormat.CODABAR) ||
	  formats.contains(BarcodeFormat.CODE_39) ||
	  formats.contains(BarcodeFormat.CODE_93) ||
	  formats.contains(BarcodeFormat.CODE_128) ||
	  formats.contains(BarcodeFormat.ITF) ||
	  formats.contains(BarcodeFormat.RSS_14) ||
	  formats.contains(BarcodeFormat.RSS_EXPANDED);
  // Put 1D readers upfront in "normal" mode
  readers.add(new MultiFormatOneDReader(hints));
  if (formats.contains(BarcodeFormat.QR_CODE)) {
	readers.add(new QRCodeReader(mActivity));
  }
  if (formats.contains(BarcodeFormat.DATA_MATRIX)) {
	readers.add(new DataMatrixReader());
  }
  if (formats.contains(BarcodeFormat.AZTEC)) {
	readers.add(new AztecReader());
  }
  if (formats.contains(BarcodeFormat.PDF_417)) {
	 readers.add(new PDF417Reader());
  }
  if (formats.contains(BarcodeFormat.MAXICODE)) {
	 readers.add(new MaxiCodeReader());
  }
  // At end in "try harder" mode
  readers.add(new MultiFormatOneDReader(hints));

解码,调用接口的实现类

private Result decodeInternal(BinaryBitmap image) throws NotFoundException {
    if (readers != null) {
      for (Reader reader : readers) {
        try {
          return reader.decode(image, hints);
        } catch (ReaderException re) {
          // continue
        }
      }
    }
    throw NotFoundException.getNotFoundInstance();
  }
// QRCodeReader.java
	@Override
	  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)) {
		  //二值化:image.getBlackMatrix()
		  BitMatrix bits = extractPureBits(image.getBlackMatrix());
		  decoderResult = decoder.decode(bits, hints);
		  points = NO_POINTS;
		} else {
		  //二值化:image.getBlackMatrix()
		  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;
	  }


	/**
	   * <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);
	  }

###定位特征点

	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();
		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) {
		  // Get a row of black/white values
		  clearCounts(stateCount);
		  int currentState = 0;
		  for (int j = 0; j < maxJ; j++) {
			if (image.get(j, i)) {
			  // Black pixel
			  if ((currentState & 1) == 1) { // Counting white pixels
				currentState++;
			  }
			  stateCount[currentState]++;
			} else { // White pixel
			  if ((currentState & 1) == 0) { // Counting black pixels
				if (currentState == 4) { // A winner?
				  if (foundPatternCross(stateCount)) { // Yes
					boolean confirmed = handlePossibleCenter(stateCount, i, j);
					if (confirmed) {
					  iSkip = 2;
					  if (hasSkipped) {
						done = haveMultiplyConfirmedCenters();
					  } else {
						int rowSkip = findRowSkip();
						if (rowSkip > stateCount[2]) {
						  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);
	  }

	private static ResultPoint[] expandSquare(ResultPoint[] cornerPoints, float oldSide, float newSide) {
		float ratio = newSide / (2 * oldSide);
		float dx = cornerPoints[0].getX() - cornerPoints[2].getX();
		float dy = cornerPoints[0].getY() - cornerPoints[2].getY();
		float centerx = (cornerPoints[0].getX() + cornerPoints[2].getX()) / 2.0f;
		float centery = (cornerPoints[0].getY() + cornerPoints[2].getY()) / 2.0f;

		ResultPoint result0 = new ResultPoint(centerx + ratio * dx, centery + ratio * dy);
		ResultPoint result2 = new ResultPoint(centerx - ratio * dx, centery - ratio * dy);

		dx = cornerPoints[1].getX() - cornerPoints[3].getX();
		dy = cornerPoints[1].getY() - cornerPoints[3].getY();
		centerx = (cornerPoints[1].getX() + cornerPoints[3].getX()) / 2.0f;
		centery = (cornerPoints[1].getY() + cornerPoints[3].getY()) / 2.0f;
		ResultPoint result1 = new ResultPoint(centerx + ratio * dx, centery + ratio * dy);
		ResultPoint result3 = new ResultPoint(centerx - ratio * dx, centery - ratio * dy);

		return new ResultPoint[]{result0, result1, result2, result3};
	  }

###返回结果

	protected final DetectorResult processFinderPatternInfo(FinderPatternInfo info)
		  throws NotFoundException, FormatException {

		FinderPattern topLeft = info.getTopLeft();
		FinderPattern topRight = info.getTopRight();
		FinderPattern bottomLeft = info.getBottomLeft();

		float moduleSize = calculateModuleSize(topLeft, topRight, bottomLeft);
		if (moduleSize < 1.0f) {
		  throw NotFoundException.getNotFoundInstance();
		}
		int dimension = computeDimension(topLeft, topRight, bottomLeft, moduleSize);
		Version provisionalVersion = Version.getProvisionalVersionForDimension(dimension);
		int modulesBetweenFPCenters = provisionalVersion.getDimensionForVersion() - 7;

		AlignmentPattern alignmentPattern = null;
		// Anything above version 1 has an alignment pattern
		if (provisionalVersion.getAlignmentPatternCenters().length > 0) {

		  // Guess where a "bottom right" finder pattern would have been
		  float bottomRightX = topRight.getX() - topLeft.getX() + bottomLeft.getX();
		  float bottomRightY = topRight.getY() - topLeft.getY() + bottomLeft.getY();

		  // Estimate that alignment pattern is closer by 3 modules
		  // from "bottom right" to known top left location
		  float correctionToTopLeft = 1.0f - 3.0f / modulesBetweenFPCenters;
		  int estAlignmentX = (int) (topLeft.getX() + correctionToTopLeft * (bottomRightX - topLeft.getX()));
		  int estAlignmentY = (int) (topLeft.getY() + correctionToTopLeft * (bottomRightY - topLeft.getY()));

		  // Kind of arbitrary -- expand search radius before giving up
		  for (int i = 4; i <= 16; i <<= 1) {
			try {
			  alignmentPattern = findAlignmentInRegion(moduleSize,
				  estAlignmentX,
				  estAlignmentY,
				  i);
			  break;
			} catch (NotFoundException re) {
			  // try next round
			}
		  }
		}

		PerspectiveTransform transform =
			createTransform(topLeft, topRight, bottomLeft, alignmentPattern, dimension);

		BitMatrix bits = sampleGrid(image, transform, dimension);

		ResultPoint[] points;
		if (alignmentPattern == null) {
		  points = new ResultPoint[]{bottomLeft, topLeft, topRight};
		} else {
		  points = new ResultPoint[]{bottomLeft, topLeft, topRight, alignmentPattern};
		}
		return new DetectorResult(bits, points);
	  }

##问题来了
在阅读源码之后,我们发现这里面并没有解读放大相关的功能,这是为什么呢?
是的,因为Zxing库本来就没有提供放大的功能,那么我们想实现放大功能应该怎么做?
对,答案就是利用相机的放大功能setZoom来放大相机即可。
那么,什么时机放大呢?

###相机放大功能和二维码建立关系
为了解决这个放大时机的问题,我们再来回顾一下源码。
有这样一段源码,它的作用是找到定位点:

		FinderPattern[] patternInfo = selectBestPatterns();
		ResultPoint.orderBestPatterns(patternInfo);

		return new FinderPatternInfo(patternInfo);
	  }

	private static ResultPoint[] expandSquare(ResultPoint[] cornerPoints, float oldSide, float newSide) {
		float ratio = newSide / (2 * oldSide);
		float dx = cornerPoints[0].getX() - cornerPoints[2].getX();
		float dy = cornerPoints[0].getY() - cornerPoints[2].getY();
		float centerx = (cornerPoints[0].getX() + cornerPoints[2].getX()) / 2.0f;
		float centery = (cornerPoints[0].getY() + cornerPoints[2].getY()) / 2.0f;

		ResultPoint result0 = new ResultPoint(centerx + ratio * dx, centery + ratio * dy);
		ResultPoint result2 = new ResultPoint(centerx - ratio * dx, centery - ratio * dy);

		dx = cornerPoints[1].getX() - cornerPoints[3].getX();
		dy = cornerPoints[1].getY() - cornerPoints[3].getY();
		centerx = (cornerPoints[1].getX() + cornerPoints[3].getX()) / 2.0f;
		centery = (cornerPoints[1].getY() + cornerPoints[3].getY()) / 2.0f;
		ResultPoint result1 = new ResultPoint(centerx + ratio * dx, centery + ratio * dy);
		ResultPoint result3 = new ResultPoint(centerx - ratio * dx, centery - ratio * dy);

		return new ResultPoint[]{result0, result1, result2, result3};

那么,我们是不是利用这个定位点,就可以和相机扫描框的间距搞一些事情就可以了?
事实上,网上也有很多例子是这么做的。
行,既然已经有了,那我们直接拿来用就可以了吧?
NONONO,事情并不是那么简单:
bucunzai.png

##58APP二维码扫描的应用
因为58APP,用的不是Java实现的。核心解码流程使用的是C++,通过JNI编译生成的so,然后再打入到58APP里的。
兵来将挡水来土掩,行,既然java实现的思路已经有了,我们就用C++再写一遍怎么了,谁让咱也是科班出身。

###找到核心C++实现
我们发现,是在这里执行的decode,然后也是调用了decodeWithState,然后将得到的结果返回给java。
仔细跟进去,发现逻辑差不多,只不过一个是用java,一个是用C++。

jbyte *yuvData = env->GetByteArrayElements(yuvData_, NULL);

    std::string codeResult = "";
    try {
        ......
		省略部分代码
        // Perform the decoding.
        MultiFormatReader reader;
        reader.setHints(hints);
        Ref<Result> result(reader.decodeWithState(image));

        // Output the result.
        codeResult = result->getText()->getText();
        cout << result->getText()->getText() << endl;
    } catch (zxing::Exception& e) {
        cerr << "Error: " << e.what() << endl;
    }

    env->ReleaseByteArrayElements(yuvData_, yuvData, 0);

    return env->NewStringUTF(codeResult.c_str());

###QRCodeReader.cpp核心实现

下面就是decode使用c++的核心实现,因为前面已经讲过了Java的实现,所以这里找到C++的代码也是非常容易的
这里我们看到,有一句

	Ref<DetectorResult> detectorResult(detector.detect(hints));

它的意思就是通过detect得到了detectorResult,然后通过下面的,

	ArrayRef< Ref<ResultPoint> > points (detectorResult->getPoints());

得到了points,即定位点;
一开始我想着把这个定位点独立出一个方法,然后再通过回调的形式传递给Java层,这样做的好处就是我们可以通过Java来实现放大功能。但是因为我们相机预览的速度是特别快的,所以这样做其实并不合适。于是,把相机的参数传递进去,然后在C++里面完成对于相机的放大。

QRCodeReader::QRCodeReader() :decoder_() {
		}
		//TODO: see if any of the other files in the qrcode tree need tryHarder
		Ref<Result> QRCodeReader::decode(Ref<BinaryBitmap> image, DecodeHints hints) {
			Detector detector(image->getBlackMatrix());
			Ref<DetectorResult> detectorResult(detector.detect(hints));
			ArrayRef< Ref<ResultPoint> > points (detectorResult->getPoints());
			Ref<DecoderResult> decoderResult(decoder_.decode(detectorResult->getBits()));
			Ref<Result> result(
							   new Result(decoderResult->getText(), decoderResult->getRawBytes(), points, BarcodeFormat::QR_CODE));
			return result;
		}

###传递Java的Camera对象
主要是在原来的基础上多传递了扫描框的位置和Camera这个对象:

	xxx_QbarNative_decode(JNIEnv *env, jobject instance, jbyteArray bytes_, jint width,
										  jint height, jint cropLeft, jint cropRight, jint cropTop,
										  jint cropWidth, jint cropHeight, jboolean reverseHorizontal,
										  jobject camera, jobject parameters)

###放大代码使用C++实现
最后,根据参数进行实现:简单的说就是判断二维码定位点的距离,然后用这个距离和相框之间的距离做个比对,小于一半,我们递增。

	//执行检测逻辑,检测得到执行放大照相机逻辑,否则执行解析逻辑
	if(NULL != resultPoints && !(resultPoints->empty())){
		//省去部分核心代码

		if(isZoomSupported){
		//这里做了简化,主要解决放大倍数过多,使得二维码突然不在扫描框里。
			if (len <= frameWidth / 2){
				if(zoom < maxZoom){
					zoom = zoom + 8;
				}else{
					zoom = maxZoom;
				}
				jmethodID setZoomMth = env->GetMethodID(parametersCls, "setZoom", "(I)V");
				env->CallVoidMethod(parameters, setZoomMth, zoom);

				jmethodID setParametersMth = env->GetMethodID(camearCls, "setParameters", "(Landroid/hardware/Camera$Parameters;)V");
				env->CallVoidMethod(camera, setParametersMth, parameters);

				env->ReleaseByteArrayElements(bytes_, yuvData, 0);
				string ss = strStream.str();
			}
		}
	}

###导出so,打入58APP
最后,在demo中运行一下,看看效果。
1557793019816360 (1).gif

##心得
其实最终解决这个问题不难,重点在于发现问题,解决问题的过程学到了什么。