还是搬砖,这个讲道理,应该周末就写了的,但是,周末太忙了,就断更了两天。
先说业务诉求及其问题,业务诉求就是从本地相册选择一张图片,然后识别其中的二维码及其条形码,因为使用的第三方maven,所以这个看起来很简单,就是直接获取到图片的绝对路径,然后丢给maven 的view即可。一套代码行云流水,然后测试提bug了,同一个图片扫码可以,保存到相册就不行?那么到底是哪里的问题呢?
正文
话说,要想知道问题的本质,那么就需要知道如何调用zxing去扫描。
如何使用zxing 扫描
先导入maven。
aidl
implementation 'com.google.zxing:core:3.3.3'
// 或者导入这个
implementation 'com.github.bingoogolapple.BGAQRCode-Android:zxing:1.3.8'
implementation 'com.github.bingoogolapple.BGAQRCode-Android:zbar:1.3.8'
这个砖就是用的 com.github.bingoogolapple.BGAQRCode-Android:zxing:1.3.8
,所以上面zing就可以不导入了。这个也是基于zxing进行封装了一个UI层,这个相当好用,推荐一下,所以我们就直接基于BGAQRCode-Android进行描述了。
创建view与设置监听
创建很简单,在xml 里面cn.bingoogolapple.qrcode.zxing.ZXingView 写上这个view,同时约束下相对位置。设置监听需要创建一个代理对象:
kotlin
private val delegate = object : QRCodeView.Delegate {
override fun onScanQRCodeSuccess(result: String?) {
// 获取到了扫码结果
}
override fun onCameraAmbientBrightnessChanged(isDark: Boolean) {
}
override fun onScanQRCodeOpenCameraError() {
}
}
设置代理对象:
csharp
binding.zxingview.setDelegate(delegate)
获取权限
扫描是需要获取相机权限的,而相机权限属于高危权限,需要动态获取,所以我们在onStart简单粗暴的处理一下:
scss
override fun onStart() {
super.onStart()
lifecycleScope.launch {
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED
) {
// 权限被授予
binding.zxingview.startCamera()
binding.zxingview.startSpotAndShowRect()
} else {
ActivityCompat.requestPermissions(
requireActivity(),
arrayOf(Manifest.permission.CAMERA),
)
// todo 这里需要获取到权限的回调,然后开启相机,开启扫码。
}
}
}
因为是demo。所以逻辑不严谨。当获取到权限之后,我们就开启扫码。
设置本地图片
scss
binding.zxingview.decodeQRCode()
这个函数有两个入参,一个是bitmap,一个是图片的绝对路径。讲道理,感觉是完美的。然后就是图片识别不出来,onScanQRCodeSuccess的回调永远没有,当然当我们把二维码或者条形码截图到只有二维码或者条形码的区域的时候,他就可以识别了。那么应该如何排查呢?
开始排查问题
既然不知道问题是啥。那么就重新自己写一遍,可能就会发现问题,OK,基于这个原因,我开始在我demo工程上调试这个问题。
因为懒发现的问题
因为是demo工程,我根本就没有些mediaStore 代码,所以我采用了一个最简单粗暴的方式,将有问题的图片导入到res目录下,众所皆知,BitmapFactory.decodeResource()
可以获取到一个bitmap。然后发现可以识别出来。所以问题是啥? 通过阅读BitmapFactory.decodeResource()的源码发现了下面代码:
less
public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
@Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
validate(opts);
if (opts == null) {
opts = new Options();
}
if (opts.inDensity == 0 && value != null) {
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density;
}
}
if (opts.inTargetDensity == 0 && res != null) {
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
他竟然偷偷的创建了一个options?
大模型反馈:设置options与不设置options的区别
在Android中,获取Bitmap对象时,可以使用BitmapFactory类提供的decodeFile()方法。这个方法可以接受一个文件路径作为参数,返回一个Bitmap对象。BitmapFactory.Options是一个可选的参数,用于指定解码图像时的选项。通过设置BitmapFactory.Options对象的属性,可以控制解码的图像的大小、颜色配置、是否进行缩放等操作。
如果不使用BitmapFactory.Options参数,decodeFile()方法会使用默认选项解码图像,返回的Bitmap对象的尺寸将与原始图像相同,颜色配置为ARGB_8888。
如果使用BitmapFactory.Options参数,你可以通过设置inJustDecodeBounds属性为true来只获取图像的尺寸信息而不实际解码图像。这可以节省内存和CPU资源。
因此,使用BitmapFactory.Options参数可以让你更精确地控制解码的图像的大小和颜色配置,以及进行其他优化。如果不设置BitmapFactory.Options参数,你将获得默认的解码选项。
BitmapFactory.decodeResource() 中options到底生效了什么?
可以看到,总共设置了两个值,一个是 inDensity,一个是inTargetDensity。当然还有一些是默认值。
inDensity
而inDensity则表示原始图像的像素密度。它不是用来表示图像的物理密度(即每英寸的像素数),而是用来计算图像在缩放时的密度比例。例如,如果一个图像的像素密度是100dpi,而目标设备的像素密度是200dpi,那么在缩放这个图像时,Android系统将自动按照2:1的比例进行缩放。
inTargetDensity
inTargetDensity表示当需要将图像绘制到屏幕上时,希望达到的目标像素密度。这是一个非常重要的参数,因为如果原始图像的像素密度与目标密度不匹配,可能会导致图像在屏幕上显示时出现模糊或拉伸等问题。
例如,如果一个图像的像素密度是100dpi(每英寸100像素),而目标设备的像素密度是200dpi(每英寸200像素),那么在绘制这个图像时,Android系统将自动进行缩放,使得图像在目标设备上看起来更大。但是,如果原始图像的像素密度是200dpi,而目标设备的像素密度是100dpi,那么在绘制这个图像时,Android系统将自动进行缩放,使得图像在目标设备上看起来更小。 因此,通过设置inTargetDensity,可以确保在绘制图像时,图像的大小不会因为像素密度的不同而出现偏差。
应该怎么去理解
我认为options是作用是保证图片不变形,不会拉伸或者被压缩。
那么我们自己创建一个options,相册图片可以识别吗?
答案是肯定的。options 不仅仅是用于设置上面几个参数,还可以设置的很多,比如说,我们做大图的加载就需要设置options 用于获取到图片的宽高,然后加载局部内容。下面贴简单的options 的创建。
ini
private fun getOptions(): BitmapFactory.Options{
val opts = BitmapFactory.Options()
val value = TypedValue()
if (opts.inDensity == 0) {
val density: Int = value.density
if (density == TypedValue.DENSITY_DEFAULT) {
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density
}
}
if (opts.inTargetDensity == 0) {
opts.inTargetDensity = resources.displayMetrics.densityDpi
}
return opts
}
扩展
学到了什么?还应该学习什么?我们知道Android 相机过来的数据是YUV数据,格式一般都是nv21。所以我们看BGAQRCode-Android是如何做到使用zxing相机识别和图形识别的。至于为什么需要扩展,通常识别来说,如果失败了就需要多次识别的,也不是说多次调用view上的识别不好,如果如果能在协程中一趟写完是最好的。
相机识别
我们知道,这个相机的数据是有一个回调接口的,那么我们就直接定位到那里。onPreviewFrame() 。最终定位到zxingview的processData() 函数中,我们直接贴关键代码:
ini
try {
PlanarYUVLuminanceSource source;
scanBoxAreaRect = mScanBoxView.getScanBoxAreaRect(height);
if (scanBoxAreaRect != null) {
source = new PlanarYUVLuminanceSource(data, width, height, scanBoxAreaRect.left, scanBoxAreaRect.top, scanBoxAreaRect.width(),
scanBoxAreaRect.height(), false);
} else {
source = new PlanarYUVLuminanceSource(data, width, height, 0, 0, width, height, false);
}
rawResult = mMultiFormatReader.decodeWithState(new BinaryBitmap(new GlobalHistogramBinarizer(source)));
if (rawResult == null) {
rawResult = mMultiFormatReader.decodeWithState(new BinaryBitmap(new HybridBinarizer(source)));
if (rawResult != null) {
BGAQRCodeUtil.d("GlobalHistogramBinarizer 没识别到,HybridBinarizer 能识别到");
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
mMultiFormatReader.reset();
}
结合zxing 官方文档可以看到,他这里是调用了mMultiFormatReader进行解码。同时使用了PlanarYUVLuminanceSource对象。优先调用了GlobalHistogramBinarizer,当GlobalHistogramBinarizer解析不到结果的时候调用了HybridBinarizer。我们想要自己写相机扫码也得这么写。
GlobalHistogramBinarizer和HybridBinarizer的区别
GlobalHistogramBinarizer和HybridBinarizer都是ZXing库中的Binarizer类,用于图像二值化处理,但两者在实现方式和应用场景上存在一些差异。
GlobalHistogramBinarizer是ZXing库早期版本中使用的全局直方图二值化方法,适用于低端移动设备。它通过拾取全局黑点来确定合适的阈值,对CPU和内存的占用率较低,但是无法很好地处理有阴影或渐变的复杂图像。因此,对于二维码解析的准确性相对较低。
HybridBinarizer是针对GlobalHistogramBinarizer的不足进行改进的类,它通过采用局部阈值算法来优化二维码解析。虽然相对于GlobalHistogramBinarizer来说,它的解析速度略慢,但效率相当高。它的主要优势在于对二维码的识别率更高,尤其适合处理有阴影、光线差或其他复杂背景的二维码图片。 总之,GlobalHistogramBinarizer适用于对性能要求较高,而对识别准确性要求较低的场景;而HybridBinarizer则更适合对识别准确性要求较高,对性能要求相对较低的场景。
bitmap识别
bitmap 识别就更容易找了。
arduino
public static String syncDecodeQRCode(Bitmap bitmap) {
Result result;
RGBLuminanceSource source = null;
try {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
int[] pixels = new int[width * height];
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
source = new RGBLuminanceSource(width, height, pixels);
result = new MultiFormatReader().decode(new BinaryBitmap(new HybridBinarizer(source)), ALL_HINT_MAP);
return result.getText();
} catch (Exception e) {
e.printStackTrace();
if (source != null) {
try {
result = new MultiFormatReader().decode(new BinaryBitmap(new GlobalHistogramBinarizer(source)), ALL_HINT_MAP);
return result.getText();
} catch (Throwable e2) {
e2.printStackTrace();
}
}
return null;
}
}
优化方向
网络上很多zxing 的优化方向来着,大致都是转yuv格式。主要是基于性能考虑,一个图片能否识别出来,取决于图片清晰度与正确性,起码不能干模糊了,也不能变形了,因为zxing的识别是基于黑白点阵的(这么说可能有问题),我们保证图片的清晰度与不变形的基础上进行多次识别。比如说微信的识别,他看起来就是先定位,然后去识别的。 比如说裁剪掉非二维码或条形码区域,然后获取到图片,重新识别,zxing 提供的是开始位置,就是左上角的位置。 多次识别有一个好处,就是可以判断这个结果是否正确,如果多次一致那么就返回正确的就行了。
YUV 方向
至于为什么是这个方向,因为YUV是颜色值本身就没有RGB丰富,但是转黑白点阵肯定是更快的,因为数据量小了1/3,如果丢弃部分颜色数据,少的更多。如果对于yuv 裁剪比较熟悉,在能保证不变形和清晰的的情况下,这种肯定是最快的,因为用于识别,所以有些数据可以丢弃。
丢弃部分颜色值转yuv
scss
fun convertBitmapToYUV(image: Bitmap): ByteArray {
val w = image.width
val h = image.height
val rgb = IntArray(w * h)
val yuv = ByteArray(w * h)
image.getPixels(rgb, 0, w, 0, 0, w, h)
for (i in 0 until w * h) {
val red = (rgb[i] shr 16 and 0xff).toFloat()
val green = (rgb[i] shr 8 and 0xff).toFloat()
val blue = (rgb[i] and 0xff).toFloat()
val luminance = (0.257f * red + 0.504f * green + 0.098f * blue + 16).toInt()
yuv[i] = (0xff and luminance).toByte()
}
return yuv
}
全部转yuv
ini
fun getBitmapYUVBytes(sourceBmp: Bitmap): ByteArray {
val inputWidth = sourceBmp.width
val inputHeight = sourceBmp.height
val argb = IntArray(inputWidth * inputHeight)
sourceBmp.getPixels(argb, 0, inputWidth, 0, 0, inputWidth, inputHeight)
val yuv = ByteArray(
inputWidth
* inputHeight
+ (if (inputWidth % 2 == 0) inputWidth else inputWidth + 1) * if (inputHeight % 2 == 0) inputHeight else inputHeight + 1 / 2
)
// 帧图片的像素大小
val frameSize = inputWidth * inputHeight
// Y的index从0开始
var yIndex = 0
// UV的index从frameSize开始
var uvIndex = frameSize
// YUV数据, ARGB数据
var Y: Int
var U: Int
var V: Int
var a: Int
var R: Int
var G: Int
var B: Int
var argbIndex = 0
// ---循环所有像素点,RGB转YUV---
for (j in 0 until inputHeight) {
for (i in 0 until inputWidth) {
// a is not used obviously
a = (argb[argbIndex] and -0x1000000) shr 24
R = (argb[argbIndex] and 0xff0000) shr 16
G = (argb[argbIndex] and 0xff00) shr 8
B = (argb[argbIndex] and 0xff)
argbIndex++
// well known RGB to YUV algorithm
Y = (((66 * R) + (129 * G) + (25 * B) + 128) shr 8) + 16
U = (((-38 * R - 74 * G) + (112 * B) + 128) shr 8) + 128
V = (((112 * R) - (94 * G) - (18 * B) + 128) shr 8) + 128
Y = Math.max(0, Math.min(Y, 255))
U = Math.max(0, Math.min(U, 255))
V = Math.max(0, Math.min(V, 255))
yuv[yIndex++] = Y.toByte()
// ---UV---
if ((j % 2 == 0) && (i % 2 == 0)) {
yuv[uvIndex++] = V.toByte()
yuv[uvIndex++] = U.toByte()
}
}
}
sourceBmp.recycle()
return yuv
}
总结
整体上来讲,涉及到的知识点蛮多的。
- zxing如何识别数据的
- 权限申请与获取
- BitmapFactory与options
- RGB与YUV(yuv420或nv21)的转换
- Android 相机预览相关的知识
- 如果说,优化走bitmap,那么对bitmap 的处理,裁剪,缩放等都要了解。
- 如果说,优化走yuv,那么yuv 数据的裁剪,缩放也得了解。
当然了,排查问题,最重要的还是心要静。实在找不到,先准备信息去请教,答案往往在准备信息里面。