联系我们
简单又实用的WordPress网站制作教学
当前位置:网站首页 > 程序开发学习 > 正文

ZXING扫描本地图片失败竟然是没有Options的问题?

作者:访客发布时间:2023-12-13分类:程序开发学习浏览:87


导读:还是搬砖,这个讲道理,应该周末就写了的,但是,周末太忙了,就断更了两天.先说业务诉求及其问题,业务诉求就是从本地相册选择一张图片,然后识别其中的二维码及其条形码,因为使用的第三方M...

还是搬砖,这个讲道理,应该周末就写了的,但是,周末太忙了,就断更了两天.

先说业务诉求及其问题,业务诉求就是从本地相册选择一张图片,然后识别其中的二维码及其条形码,因为使用的第三方Maven,所以这个看起来很简单,就是直接获取到图片的绝对路径,然后丢给Maven的VIEW即可。一套代码行云流水,然后测试提BUG了,同一个图片扫码可以,保存到相册就不行?那么到底是哪里的问题呢?

正文

话说,要想知道问题的本质,那么就需要知道如何调用Zxing去扫描.

如何使用Zxing扫描

先导入Maven。

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-安卓进行描述了。

创建视图与设置监听

创建很简单,在xml里面cn.bingoogolapple.qrcode.zxing.ZXing.ZXingView写上这个视图,同时约束下相对位置。设置监听需要创建一个代理对象:

private val delegate = object : QRCodeView.Delegate {
    override fun onScanQRCodeSuccess(result: String?) {
        // 获取到了扫码结果
    }

    override fun onCameraAmbientBrightnessChanged(isDark: Boolean) {
    }

    override fun onScanQRCodeOpenCameraError() {
       
    }
}

设置代理对象:

binding.zxingview.setDelegate(delegate)

获取权限

扫描是需要获取相机权限的,而相机权限属于高危权限,需要动态获取,所以我们在on Start简单粗暴的处理一下:

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 这里需要获取到权限的回调,然后开启相机,开启扫码。
        }
    }
}

因为是演示。所以逻辑不严谨.当获取到权限之后,我们就开启扫码.

设置本地图片

binding.zxingview.decodeQRCode()

这个函数有两个入参,一个是位图,一个是图片的绝对路径。讲道理,感觉是完美的.然后就是图片识别不出来,On ScanQRCodeSuccess的回调永远没有,当然当我们把二维码或者条形码截图到只有二维码或者条形码的区域的时候,他就可以识别了。那么应该如何排查呢?

开始排查问题

既然不知道问题是啥.那么就重新自己写一遍,可能就会发现问题,好的,基于这个原因,我开始在我演示工程上调试这个问题。

因为懒发现的问题

因为是Demo工程,我根本就没有些MediaStore代码,所以我采用了一个最简单粗暴的方式,将有问题的图片导入到res目录下,众所皆知,BitmapFactory.decodeResource()可以获取到一个位图。然后发现可以识别出来.所以问题是啥?通过阅读BitmapFactory.decdeResource()的源码发现了下面代码:

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

他竟然偷偷的创建了一个选项?

大模型反馈:设置选项与不设置选项的区别

在安卓中,获取位图对象时,可以使用位图工厂类提供的解码文件()方法。这个方法可以接受一个文件路径作为参数,返回一个位图对象。位图工厂。选项是一个可选的参数,用于指定解码图像时的选项。通过设置位图工厂。选项对象的属性,可以控制解码的图像的大小、颜色配置、是否进行缩放等操作。

如果不使用位图工厂。选项参数、解码文件()方法会使用默认选项解码图像,返回的位图对象的尺寸将与原始图像相同,颜色配置为argb_8888.

如果使用位图工厂。选项参数,你可以通过设置in JustDecodeBound属性为True来只获取图像的尺寸信息而不实际解码图像。这可以节省内存和cpu资源。

因此,使用位图工厂。选项参数可以让你更精确地控制解码的图像的大小和颜色配置,以及进行其他优化。如果不设置位图工厂。选项参数,你将获得默认的解码选项。

BitmapFactory.decdeResource()中Options到底生效了什么?

可以看到,总共设置了两个值,一个是在密度中,一个是在目标密度中。当然还有一些是默认值.

密度不大

而in Density则表示原始图像的像素密度.它不是用来表示图像的物理密度(即每英寸的像素数),而是用来计算图像在缩放时的密度比例.例如,如果一个图像的像素密度是100dpi、而目标设备的像素密度是200dpi、那么在缩放这个图像时、安卓系统将自动按照2:1的比例进行缩放。

目标密度

在目标密度表示当需要将图像绘制到屏幕上时,希望达到的目标像素密度中。这是一个非常重要的参数,因为如果原始图像的像素密度与目标密度不匹配,可能会导致图像在屏幕上显示时出现模糊或拉伸等问题.

例如,如果一个图像的像素密度是100dpi(每英寸100像素),而目标设备的像素密度是200dpi(每英寸200像素),那么在绘制这个图像时,安卓系统将自动进行缩放,使得图像在目标设备上看起来更大.但是,如果原始图像的像素密度是200dpi、而目标设备的像素密度是100dpi、那么在绘制这个图像时、安卓系统将自动进行缩放,使得图像在目标设备上看起来更小。 因此,通过设置in TargetDensity,可以确保在绘制图像时,图像的大小不会因为像素密度的不同而出现偏差.

应该怎么去理解

我认为Options是作用是保证图片不变形,不会拉伸或者被压缩。

那么我们自己创建一个选项,相册图片可以识别吗?

答案是肯定的.Options不仅仅是用于设置上面几个参数,还可以设置的很多,比如说,我们做大图的加载就需要设置Options用于获取到图片的宽高,然后加载局部内容。下面贴简单的Options的创建。

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
}

扩展

学到了什么?还应该学习什么?我们知道安卓相机过来的数据是YUV数据,格式一般都是nv21。安卓BGAQRCode-所以我们看是如何做到使用zxing相机识别和图形识别的.至于为什么需要扩展,通常识别来说,如果失败了就需要多次识别的,也不是说多次调用查看上的识别不好,如果如果能在协程中一趟写完是最好的。

相机识别

我们知道,这个相机的数据是有一个回调接口的,那么我们就直接定位到那里.OnPreviewFrame()。最终定位到zxingview的进程Data()函数中,我们直接贴关键代码:

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官方文档可以看到,他这里是调用了mMultiFormReader进行解码.同时使用了平面YUV亮度源对象。优先调用了全局历史二元化,当全局历史二元化,解析不到结果的时候调用了杂化二元化。我们想要自己写相机扫码也得这么写.

全局组织图二元化和混合二元化的区别

全球组织图二元化和杂化二元化都是中兴库中的二元化类,用于图像二值化处理,但两者在实现方式和应用场景上存在一些差异。

全局组织图二元化是ZXing库早期版本中使用的全局直方图二值化方法,适用于低端移动设备.它通过拾取全局黑点来确定合适的阈值,对cpu和内存的占用率较低,但是无法很好地处理有阴影或渐变的复杂图像。因此,对于二维码解析的准确性相对较低.

HYBYBINARIZER是针对GLOBALSTORGRAM Binarizer的不足进行改进的类,它通过采用局部阈值算法来优化二维码解析.虽然相对于全局组织图Binarizer来说,它的解析速度略慢,但效率相当高。它的主要优势在于对二维码的识别率更高,尤其适合处理有阴影、光线差或其他复杂背景的二维码图片. 总之,GlobalOrganram Binarizer适用于对性能要求较高,而对识别准确性要求较低的场景;而Hybridge Binarizer则更适合对识别准确性要求较高,对性能要求相对较低的场景.

位图识别

位图识别就更容易找了。

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

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

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
}

总结

整体上来讲,涉及到的知识点蛮多的.

  • Zinging如何识别数据的
  • 权限申请与获取
  • 位图工厂与选项
  • Rgb与yuv(yuv420或nv21)的转换
  • 安卓相机预览相关的知识
  • 如果说,优化走位图、那么对位图的处理,裁剪,缩放等都要了解。
  • 如果说,优化走yuv,那么yuv数据的裁剪,缩放也得了解。

当然了,排查问题,最重要的还是心要静.实在找不到,先准备信息去请教,答案往往在准备信息里面.


标签:图片扎兴选项


程序开发学习排行
最近发表
网站分类
标签列表