二维码实现和优化

由于项目需要,我对接二维码模块,经过自己的优化,目前效果还不错,网上给出的方案大多数都是复制过来的,没有经过深度思考。所以我决定纪录下来自己优化的地方,供以后自己参考。

项目地址

github-vanda-伍中联-QrCode

二维码模块

二维码这块经过了初步的封装处理,没有Activity,没有Fragment,只有View,最终使用二维码的形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
private QrCodeView mQrCodeView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

mQrCodeView = new QrCodeView(this, null);
mQrCodeView.registerResultCallback(this);
mQrCodeView.setText(getString(R.string.qrcode_desc));

setContentView(mQrCodeView);
}

@Override
public void onPause() {
super.onPause();
if (mQrCodeView != null) {
mQrCodeView.onPause();
}
}

@Override
public void onResume() {
super.onResume();
if (mQrCodeView != null) {
mQrCodeView.onResume();
}
}

@Override
public void onDestroy() {
super.onDestroy();
if (mQrCodeView != null) {
mQrCodeView.onDestroy();
mQrCodeView = null;
}
}

@Override
public void onResultCallback(Result result) {
if (result != null) {
if (mQrCodeView != null) {
mQrCodeView.onPause();
}
new android.support.v7.app.AlertDialog.Builder(this).setTitle(result.getText())
.setCancelable(true)
.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialogInterface) {
if (mQrCodeView != null) {
mQrCodeView.onResume();
}
}
}).create().show();
}
}
  1. 二维码使用了zxing库,我保留了二维码和条形码的和一些常用的格式
  2. 大多数项目要求是竖屏进行扫描,本优化是针对竖屏下的优化

相机的拍摄图像的原始数据图像

相机回调的原始数据图像是横着的。回调代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void onPreviewFrame(byte[] data, Camera camera) {
Point cameraResolution = configManager.getCameraResolution();
if (!useOneShotPreviewCallback) {
camera.setPreviewCallback(null);
}
if (previewHandler != null) {
Message message = previewHandler.obtainMessage(previewMessage, cameraResolution.x,
cameraResolution.y, data);
message.sendToTarget();
previewHandler = null;
} else {
Log.d(TAG, "Got preview callback, but no handler for it");
}
}

byte[] data这个是预览图中的相机的原始数据,这个数据是比较大的,原始图像如下:

这个是原始图像,这个图像是横着的,然后我们需要对这张图片做出相应的处理。

如何处理相机获取来的图片

我Google了很多关于二维码竖屏下的实现方案,给出网上一大片的实现方案(这种方案没有仔细思考,也反映了国内程序员们的急躁):

  1. 图片是横着的,那么就需要将相机图像摆正
  2. 显示在手机屏幕上摆正只是相机显示,最终回调的图片数据还是横着的,所以还需要转换图像为竖屏的(这个数据量是比较大的)
  3. 根据二维码框对应手机屏幕位置找到图片相对位置
  4. 找到对应的位置进行裁剪
  5. 交给解码库解码

我针对以上的点做出代码上的说明:

  1. 图片是横着的,那么就需要将相机图像摆正,需要找到正确的相机方向,这段代码就是找到相机方向
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/**
* Reads, one time, values from the camera that are needed by the app.
*/
void initFromCameraParameters(OpenCamera camera) {
Camera.Parameters parameters = camera.getCamera().getParameters();
WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = manager.getDefaultDisplay();

int displayRotation = display.getRotation();
int cwRotationFromNaturalToDisplay;
switch (displayRotation) {
case Surface.ROTATION_0:
cwRotationFromNaturalToDisplay = 0;
break;
case Surface.ROTATION_90:
cwRotationFromNaturalToDisplay = 90;
break;
case Surface.ROTATION_180:
cwRotationFromNaturalToDisplay = 180;
break;
case Surface.ROTATION_270:
cwRotationFromNaturalToDisplay = 270;
break;
default:
// Have seen this return incorrect values like -90
if (displayRotation % 90 == 0) {
cwRotationFromNaturalToDisplay = (360 + displayRotation) % 360;
} else {
throw new IllegalArgumentException("Bad rotation: " + displayRotation);
}
}
Log.i(TAG, "Display at: " + cwRotationFromNaturalToDisplay);

int cwRotationFromNaturalToCamera = camera.getOrientation();
Log.i(TAG, "Camera at: " + cwRotationFromNaturalToCamera);

// Still not 100% sure about this. But acts like we need to flip this:
if (camera.getFacing() == CameraFacing.FRONT) {
cwRotationFromNaturalToCamera = (360 - cwRotationFromNaturalToCamera) % 360;
Log.i(TAG, "Front camera overriden to: " + cwRotationFromNaturalToCamera);
}

/*
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String overrideRotationString;
if (camera.getFacing() == CameraFacing.FRONT) {
overrideRotationString = prefs.getString(Config.KEY_FORCE_CAMERA_ORIENTATION_FRONT, null);
} else {
overrideRotationString = prefs.getString(Config.KEY_FORCE_CAMERA_ORIENTATION, null);
}
if (overrideRotationString != null && !"-".equals(overrideRotationString)) {
Log.i(TAG, "Overriding camera manually to " + overrideRotationString);
cwRotationFromNaturalToCamera = Integer.parseInt(overrideRotationString);
}
*/

cwRotationFromDisplayToCamera =
(360 + cwRotationFromNaturalToCamera - cwRotationFromNaturalToDisplay) % 360;
Log.i(TAG, "Final display orientation: " + cwRotationFromDisplayToCamera);
if (camera.getFacing() == CameraFacing.FRONT) {
Log.i(TAG, "Compensating rotation for front camera");
cwNeededRotation = (360 - cwRotationFromDisplayToCamera) % 360;
} else {
cwNeededRotation = cwRotationFromDisplayToCamera;
}
Log.i(TAG, "Clockwise rotation from display to camera: " + cwNeededRotation);

Point theScreenResolution = new Point();
display.getSize(theScreenResolution);
screenResolution = theScreenResolution;
Log.i(TAG, "Screen resolution in current orientation: " + screenResolution);
cameraResolution = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution);
Log.i(TAG, "Camera resolution: " + cameraResolution);
bestPreviewSize = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution);
Log.i(TAG, "Best available preview size: " + bestPreviewSize);

boolean isScreenPortrait = screenResolution.x < screenResolution.y;
boolean isPreviewSizePortrait = bestPreviewSize.x < bestPreviewSize.y;

if (isScreenPortrait == isPreviewSizePortrait) {
previewSizeOnScreen = bestPreviewSize;
} else {
previewSizeOnScreen = new Point(bestPreviewSize.y, bestPreviewSize.x);
}
Log.i(TAG, "Preview size on screen: " + previewSizeOnScreen);
}

设置相机的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void setDesiredCameraParameters(OpenCamera camera, boolean safeMode) {
Camera theCamera = camera.getCamera();
Camera.Parameters parameters = theCamera.getParameters();

Log.d(TAG, "Setting preview size: " + cameraResolution);
parameters.setPreviewSize(cameraResolution.x, cameraResolution.y);
setFlash(parameters);
setZoom(parameters);
theCamera.setDisplayOrientation(cwNeededRotation);
theCamera.setParameters(parameters);

Camera.Size afterSize = parameters.getPreviewSize();
if (afterSize != null && (bestPreviewSize.x != afterSize.width || bestPreviewSize.y != afterSize.height)) {
Log.w(TAG, "Camera said it supported preview size " + bestPreviewSize.x + 'x' + bestPreviewSize.y +
", but after setting it, preview size is " + afterSize.width + 'x' + afterSize.height);
bestPreviewSize.x = afterSize.width;
bestPreviewSize.y = afterSize.height;
}
}

这样就能够正确的显示相机拍摄的图片。

  1. 显示在手机屏幕上摆正只是相机显示,最终回调的图片数据还是横着的,所以还需要转换图像为竖屏的(这个数据量是比较大的),这个在竖屏中绝大都是进行数据转换(横屏图像数据转换成竖屏),代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* Decode the data within the viewfinder rectangle, and time how long it took. For efficiency,
* reuse the same reader objects from one decode to the next.
*
* @param data The YUV preview frame.
* @param width The width of the preview frame.
* @param height The height of the preview frame.
*/
private void decode(byte[] data, int width, int height) {
long start = System.currentTimeMillis();
Result rawResult = null;
Handler handler = mIScanCallback.getScanHandler();
try {

if (null == mRotatedData) {
mRotatedData = new byte[width * height];
} else {
if (mRotatedData.length < width * height) {
mRotatedData = new byte[width * height];
}
}
Arrays.fill(mRotatedData, (byte) 0);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
if (x + y * width >= data.length) {
break;
}
mRotatedData[x * height + height - y - 1] = data[x + y * width];
}
}
int tmp = width; // Here we are swapping, that's the difference to #11
width = height;
height = tmp;
}

byte[] data的数据长度在3110400,长度百万级别,那么将图片数据转换成竖屏需要大概在56ms (三星S6+)。

  1. 根据二维码框对应手机屏幕位置找到图片相对位置,通过以上步骤,我们将图片竖直了,接下来就是需要进行进行坐标映射,通过屏幕的扫描框映射到图片的具体框选坐标,然后裁剪数据:

相对坐标代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* Like {@link #getFramingRect} but coordinates are in terms of the preview frame,
* not UI / screen.
*
* @return {@link Rect} expressing barcode scan area in terms of the preview size
*/
public synchronized Rect getFramingRectInPreview() {
if (framingRectInPreview == null) {
Rect framingRect = getFramingRect();
if (framingRect == null) {
return null;
}
Rect rect = new Rect(framingRect);
Point cameraResolution = configManager.getCameraResolution();
Point screenResolution = configManager.getScreenResolution();
if (cameraResolution == null || screenResolution == null) {
// Called early, before init even finished
return null;
}
rect.left = rect.left * cameraResolution.y / screenResolution.x;
rect.right = rect.right * cameraResolution.y / screenResolution.x;
rect.top = rect.top * cameraResolution.x / screenResolution.y;
rect.bottom = rect.bottom * cameraResolution.x / screenResolution.y;

framingRectInPreview = rect;
}
return framingRectInPreview;
}

上面的代码是计算图片二维码框的位置,然后进行裁剪。

针对上面步骤的优化

总结下面几点:

  1. 图片横着先不进行数据转换
  2. 将手机屏幕的扫码框先进行横着图片的坐标映射
  3. 主动裁剪出数据区域
  4. 将二维码区域数据裁剪

针对第一点,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* Decode the data within the viewfinder rectangle, and time how long it took. For efficiency,
* reuse the same reader objects from one decode to the next.
*
* @param data The YUV preview frame.
* @param width The width of the preview frame.
* @param height The height of the preview frame.
*/
private void decode(byte[] data, int width, int height) {
long start = System.currentTimeMillis();
Result rawResult = null;
Handler handler = mIScanCallback.getScanHandler();
try {
PlanarYUVLuminanceSource source = mIScanCallback.buildLuminanceSource(data, width, height);
if (source != null) {
BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source));//
try {
rawResult = multiFormatReader.decodeWithState(bitmap);
} catch (ReaderException re) {
// continue
} finally {
multiFormatReader.reset();
}
}

if (rawResult != null) {
// Don't log the barcode contents for security.
long end = System.currentTimeMillis();
Log.d(TAG, "Found barcode in " + (end - start) + " ms");
if (handler != null) {
Message message = Message.obtain(handler, DECODE_SUCCEEDED, rawResult);
//为了提高扫码速度,去除这里的生成bitmap的操作
// Bundle bundle = new Bundle();
// bundleThumbnail(source, bundle);
// message.setData(bundle);
message.sendToTarget();
}
} else {
if (handler != null) {
Message message = Message.obtain(handler, DECODE_FAILED);
message.sendToTarget();
}
}
} catch (ArrayIndexOutOfBoundsException e) {
if (handler != null) {
Message message = Message.obtain(handler, DECODE_FAILED);
message.sendToTarget();
}
}
}
  1. 映射横屏下的扫码框的坐标,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* Like {@link #getFramingRect} but coordinates are in terms of the preview frame,
* not UI / screen.
*
* @return {@link Rect} expressing barcode scan area in terms of the preview size
*/
public synchronized Rect getFramingRectInPreview() {
if (framingRectInPreview == null) {
Rect framingRect = getFramingRect();
if (framingRect == null) {
return null;
}
Rect rect = new Rect(framingRect);
Point cameraResolution = configManager.getCameraResolution();
Point screenResolution = configManager.getScreenResolution();
if (cameraResolution == null || screenResolution == null) {
// Called early, before init even finished
return null;
}

int left, top, right, bottom;

//坐标转换, 我们二维码扫描是竖屏,图像是横屏,所以需要计算二维码所处的相对坐标
left = rect.top * cameraResolution.x / screenResolution.y;
top = rect.left * cameraResolution.y / screenResolution.x;
right = left + rect.height() * cameraResolution.x / screenResolution.y;
bottom = top + rect.width() * cameraResolution.y / screenResolution.x;


//对应一些相机在竖屏下需要旋转270 才能够正常图像,但是图像是横屏的,这个图像也是调转的,所以也需要转换
if (getCWNeededRotation() == 270) {
int width = right - left;
left = cameraResolution.x - left - width;
right = left + width;
}

rect.left = left;
rect.top = top;
rect.right = right;
rect.bottom = bottom;

framingRectInPreview = rect;
}
return framingRectInPreview;
}
  1. 裁剪映射图片扫码框的数据区域,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
private byte[] mOriginData;
private byte[] mRotatedData;

/**
* A factory method to build the appropriate LuminanceSource object based on the format
* of the preview buffers, as described by Camera.Parameters.
*
* @param data A preview frame.
* @param width The width of the image.
* @param height The height of the image.
* @return A PlanarYUVLuminanceSource instance.
*/
public PlanarYUVLuminanceSource buildLuminanceSource(byte[] data, int width, int height) {
Rect rect = getFramingRectInPreview();
if (rect == null) {
return null;
}

int previewH = rect.height();
int previewW = rect.width();
int size = previewH * previewW;

if (null == mOriginData) {
mOriginData = new byte[size];
mRotatedData = new byte[size];
} else {
if (mOriginData.length < size) {
mOriginData = new byte[size];
mRotatedData = new byte[size];
}
}

int inputOffset = rect.top * width + rect.left;

// If the width matches the full width of the underlying data, perform a single copy.
if (width == previewW) {
System.arraycopy(data, inputOffset, mOriginData, 0, size);
}

// Otherwise copy one cropped row at a time.
for (int y = 0; y < previewH; y++) {
int outputOffset = y * previewW;
System.arraycopy(data, inputOffset, mOriginData, outputOffset, previewW);
inputOffset += width;
}

for (int y = 0; y < previewH; y++) {
for (int x = 0; x < previewW; x++) {
if (x + y * previewW >= mOriginData.length) {
break;
}
mRotatedData[x * previewH + previewH - y - 1] = mOriginData[x + y * previewW];
}
}
int tmp = previewW; // Here we are swapping, that's the difference to #11
previewW = previewH;
previewH = tmp;

// Go ahead and assume it's YUV rather than die.
return new PlanarYUVLuminanceSource(mRotatedData, previewW, previewH, 0, 0,
previewW, previewH, false);
}

这个步骤是,先裁剪出横屏的二维码区域数据,然后在旋转90度,数据交换的大小为419904,处理时间在6ms (三星S6+),这里扫码框是相对比较大的,是屏幕宽度的0.6,扫码框越小越快。

其他点的优化

二维码的对焦速度也影响扫码速度,生成bitmap也需要时间。针对这2点,我也做出了一些优化:

  1. 相比之前,官方是使用一个AsyncTask,然后使用线程Sleep的方式来控制对焦时间,AsyncTask本身是一个线程池,显然是比较浪费也不优雅的方式;修改方案是服用本身存在的CaptureHandler,在开始生成预览图(requestPreviewFrame(decodeThread.getHandler(), DECODE))之前发起对焦。
  2. 识别成功后,会生出一个bitmap,这个较耗大概18ms

针对上面2点,做出代码上的解释:

1
2
3
4
5
6
7
8
9
10
//对焦成功后的回调,和下次对焦的消息,这样完全不需要官方给出的AsyncTask,来对焦
public void onAutoFocus(boolean success, Camera camera) {
if (autoFocusHandler != null) {
Message message = autoFocusHandler.obtainMessage(autoFocusMessage, success);
autoFocusHandler.sendMessageDelayed(message, AUTOFOCUS_INTERVAL_MS);
autoFocusHandler = null;
} else {
Log.d(TAG, "Got auto-focus callback, but no handler for it");
}
}

不生成bitmap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (rawResult != null) {
// Don't log the barcode contents for security.
long end = System.currentTimeMillis();
Log.d(TAG, "Found barcode in " + (end - start) + " ms");
if (handler != null) {
Message message = Message.obtain(handler, DECODE_SUCCEEDED, rawResult);
//为了提高扫码速度,去除这里的生成bitmap的操作
// Bundle bundle = new Bundle();
// bundleThumbnail(source, bundle);
// message.setData(bundle);
message.sendToTarget();
}
} else {
if (handler != null) {
Message message = Message.obtain(handler, DECODE_FAILED);
message.sendToTarget();
}
}
}

以上就是现阶段的优化,当然还有存在的继续优化的空间。