查看: 3995|回复: 0

[手机开发] 二维码扫描的坑:本地图片二维码扫描的优化(zxing和zbar)

发表于 2018-1-31 08:00:00

前面:
前面写了选取本地图片进行二维码扫描的文章:
《基于zxing-android-embedded:3.5.0实现扫描二维码(本地图片中解析二维码)》
逻辑实现相对简单,但是在测试的时候会发现,有很多网站上生成的二维码都识别不出来,比如加上个性化定制的二维码,或者是生成的二维码图片经过拍照或其他处理,所以就要进行优化。

先看一下典型的识别不出来的图片

图片描述图片描述

自己做过的尝试

为什么要写这个而不直接写解决方法呢?因为我怕读者按照这些不生效的方法(至少我自己证明不生效)上越走越远。
(1)从用摄像头扫描二维码得到的灵感,从本地图片中选取图片识别二维码时做一个类似zooming的功能,直白讲就是:直接识别失败时,将图片缩小,图片加边框,再进行识别;识别失败了再缩小,再加边框,继续识别...直到到达某一阈值,报失败!
图片描述
——最后证明这种方法实现上麻烦,而且效果不是非常明显,一个字:low!
(2)黑白化图片
简单讲,就是把图片里面模糊的点去掉,就是说,黑就是黑,白就是白。
——最后证明,完全不起作用!
(3)使用zbar替代zxing进行本地图片识别
这个方法是管用的,虽然zbar已经很久没有更新了(多久呢?2012年在github上有个fork是最新的),但是依然不影响它的性能。这主要归因于它的实现是纯c实现,算法上也是比zxing更好一些。
——这个方案从识别成功率来看有很大提升(但还是有个别图片识别不了),但是在使用zbar的过程中,发现zbar识别本地图片是直接解析YUV数据,从这点得出灵感,让zxing也解析YUV,这样就催生最终的实现。

3.最终方案

因为从本地图片拿到的bitmap是ARGB进行存储的,所以要先转化出YUV数据,然后先采用zxing解析图片的YUV数据,识别失败了使用zbar解析。
具体来看步骤:
(1)从本地图片中拿到的bitmap,通过算法拿到它的YUV数据
(2)把YUV数据塞给zxing解析
(3)解析不成功的情况下,使用zbar
先看一下总体的实现,接下来会贴出具体方法的实现

  1. static String scanImage(Uri uri) {
  2. String qrCode = "";
  3. try {
  4. Bitmap bitmap = MediaStore.Images.Media.getBitmap(HtscApplication.getGlobalContext().getContentResolver(), uri);
  5. bitmap = getSmallerBitmap(bitmap);
  6. if (bitmap != null) {
  7. int bitmapWidth = bitmap.getWidth();
  8. int bitmapHeight = bitmap.getHeight();
  9. // 1.将bitmap的RGB数据转化成YUV420sp数据
  10. byte[] bmpYUVBytes = Bmp2YUV.getBitmapYUVBytes(bitmap);
  11. // 2.塞给zxing进行decode
  12. qrCode = decodeYUVByZxing(bmpYUVBytes, bitmapWidth, bitmapHeight);
  13. if (HtscStringUtil.isEmpty(qrCode)) {
  14. // 3.识别不成功,使用zbar进行decode
  15. qrCode = decodeYUVByZbar(bmpYUVBytes, bitmapWidth, bitmapHeight);
  16. }
  17. bitmap.recycle();
  18. bitmap = null;
  19. }
  20. } catch (IOException e) {
  21. e.printStackTrace();
  22. }
  23. return qrCode;
  24. }
复制代码
4.从Uri拿到bitmap,再拿到YUV420sp数据

我这里写成一个工具类,具体实现网上一搜一堆,出自yuv420sp spec,还是比较权威,也比较好理解:

  1. class Bmp2YUV {
  2. /**
  3. * 得到bitmap的YUV数据
  4. *
  5. * @param sourceBmp 原始bitmap
  6. * @return yuv数据
  7. */
  8. public static byte[] getBitmapYUVBytes(Bitmap sourceBmp) {
  9. if (null != sourceBmp) {
  10. int inputWidth = sourceBmp.getWidth();
  11. int inputHeight = sourceBmp.getHeight();
  12. int[] argb = new int[inputWidth * inputHeight];
  13. sourceBmp.getPixels(argb, 0, inputWidth, 0, 0, inputWidth, inputHeight);
  14. byte[] yuv = new byte[inputWidth
  15. * inputHeight
  16. + ((inputWidth % 2 == 0 ? inputWidth : (inputWidth + 1)) * (inputHeight % 2 == 0 ? inputHeight
  17. : (inputHeight + 1))) / 2];
  18. encodeYUV420SP(yuv, argb, inputWidth, inputHeight);
  19. sourceBmp.recycle();
  20. return yuv;
  21. }
  22. return null;
  23. }
  24. /**
  25. * 将bitmap里得到的argb数据转成yuv420sp格式
  26. * 这个yuv420sp数据就可以直接传给MediaCodec, 通过AvcEncoder间接进行编码
  27. *
  28. * @param yuv420sp 用来存放yuv429sp数据
  29. * @param argb 传入argb数据
  30. * @param width bmpWidth
  31. * @param height bmpHeight
  32. */
  33. private static void encodeYUV420SP(byte[] yuv420sp, int[] argb, int width, int height) {
  34. // 帧图片的像素大小
  35. final int frameSize = width * height;
  36. // Y的index从0开始
  37. int yIndex = 0;
  38. // UV的index从frameSize开始
  39. int uvIndex = frameSize;
  40. // YUV数据, ARGB数据
  41. int Y, U, V, a, R, G, B;
  42. ;
  43. int argbIndex = 0;
  44. // ---循环所有像素点,RGB转YUV---
  45. for (int j = 0; j < height; j++) {
  46. for (int i = 0; i < width; i++) {
  47. // a is not used obviously
  48. a = (argb[argbIndex] & 0xff000000) >> 24;
  49. R = (argb[argbIndex] & 0xff0000) >> 16;
  50. G = (argb[argbIndex] & 0xff00) >> 8;
  51. B = (argb[argbIndex] & 0xff);
  52. argbIndex++;
  53. // well known RGB to YUV algorithm
  54. Y = ((66 * R + 129 * G + 25 * B + 128) >> 8) + 16;
  55. U = ((-38 * R - 74 * G + 112 * B + 128) >> 8) + 128;
  56. V = ((112 * R - 94 * G - 18 * B + 128) >> 8) + 128;
  57. Y = Math.max(0, Math.min(Y, 255));
  58. U = Math.max(0, Math.min(U, 255));
  59. V = Math.max(0, Math.min(V, 255));
  60. // NV21 has a plane of Y and interleaved planes of VU each
  61. // sampled by a factor of 2
  62. // meaning for every 4 Y pixels there are 1 V and 1 U. Note the
  63. // sampling is every other
  64. // pixel AND every other scanline.
  65. // ---Y---
  66. yuv420sp[yIndex++] = (byte) Y;
  67. // ---UV---
  68. if ((j % 2 == 0) && (i % 2 == 0)) {
  69. yuv420sp[uvIndex++] = (byte) V;
  70. yuv420sp[uvIndex++] = (byte) U;
  71. }
  72. }
  73. }
  74. }
  75. }
复制代码
5.用zxing解析图片的YUV420sp数据

这里从zbar得到的灵感,在zxing解析时使用的source(LuminanceSource)是PlanarYUVLuminanceSource,看具体实现

  1. private static String decodeYUVByZxing(byte[] bmpYUVBytes, int bmpWidth, int bmpHeight) {
  2. String zxingResult = "";
  3. // Both dimensions must be greater than 0
  4. if (null != bmpYUVBytes && bmpWidth > 0 && bmpHeight > 0) {
  5. try {
  6. PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(bmpYUVBytes, bmpWidth,
  7. bmpHeight, 0, 0, bmpWidth, bmpHeight, true);
  8. BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source));
  9. Reader reader = new QRCodeReader();
  10. Result result = reader.decode(binaryBitmap);
  11. if (null != result) {
  12. zxingResult = result.getText();
  13. }
  14. } catch (Exception e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. return zxingResult;
  19. }
复制代码

注意:
(1)bitmap的宽和高必须大于0,否则报exception;bmpYUVBytes不能为空,而且长度要够reverse,否则得到PlanarYUVLuminanceSource源时报exception,加上try就行了。
(2)BinaryBitmap生成时的Binarizer有些人说要用GlobalHistogramBinarizer,但是其实不是的,看一下GlobalHistogramBinarizer的注释就知道了,而且我自己做过尝试,不会提升。

  1. /**
  2. * This Binarizer implementation uses the old ZXing global histogram approach. It is suitable
  3. * for low-end mobile devices which don't have enough CPU or memory to use a local thresholding
  4. * algorithm. However, because it picks a global black point, it cannot handle difficult shadows
  5. * and gradients.
  6. *
  7. * Faster mobile devices and all desktop applications should probably use HybridBinarizer instead.
  8. *
  9. * @author dswitkin@google.com (Daniel Switkin)
  10. * @author Sean Owen
  11. */
复制代码
6.zxing不行用zbar

zbar的实现如下。

  1. private static String decodeYUVByZbar(byte[] bmpYUVBytes, int bmpWidth, int bmpHeight) {
  2. String zbarResult = "";
  3. // Both dimensions must be greater than 0
  4. if (null != bmpYUVBytes && bmpWidth > 0 && bmpHeight > 0) {
  5. ZBarDecoder decoder = new ZBarDecoder();
  6. zbarResult = decoder.decodeRaw(bmpYUVBytes, bmpWidth, bmpHeight);
  7. }
  8. Log.e("HtscCodeScanningUtil", "decode by zbar, result = " + zbarResult);
  9. return zbarResult;
  10. }
复制代码

注意:
同样的bitmap宽和高必须大于,否则exception,但是bmpYUVBytes为空,其实无所谓,但是肯定识别不出来。

7.总结

没什么好总结的,因为结合了两个平台的优点(本来zxing不能识别的用zbar可以,zbar不行的zxing却可以),最终证明这个方案的识别成功率有相当大的提升,但是也不排除那些分辨率极低的拍来拍去的图片识别不了。
贴一些资料大家可以自己去看(如果你有兴趣)
zbar源码解析和算法相关:
http://blog.csdn.net/sunflower_boy/article/details/50783179
http://blog.csdn.net/u013738531/article/details/54574262
bitmap ARGB转YUV420sp:
https://stackoverflow.com/questions/5960247/convert-bitmap-array-to-yuv-ycbcr-nv21
http://blog.csdn.net/juxuny/article/details/38444607



回复

使用道具 举报