要对从相册选择图片进行扫码进行优化,因为之前使用的方法很多图片会扫描不出来
对图片进行扫描要先对图片进行采样,将占用内存较大的图片采样获取小的版本防止在加载图片时发生OOM。如果只是采样有的图片仍然不能直接被识别出来,所以需要对图片进行缩放,这样能够将二维码识别出来。
本文是在不改动zxing源码的基础上通过外面编写代码来提高识别率,但是有的图片识别可能会比较慢,没有微信的那么快。代码使用kotlin编写。
简单的扫描版本
对图片进行简单采样
fun getBitmapSameAsCI(path: String): Bitmap? {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(path, options)
options.inJustDecodeBounds = false
val fixWidth = 400
var sampleSize = (options.outWidth / fixWidth + options.outHeight / fixWidth) / 2
if (sampleSize < 0) {
sampleSize = 1
}
options.inSampleSize = sampleSize
return BitmapFactory.decodeFile(path, options)
}
使用RGBLuminanceSource进行扫描的代码,直接对采样后的图片进行扫码不进行缩放
fun scanFromBitmap(bitmap: Bitmap): Result? {
val width = bitmap.width
val height = bitmap.height
val pixels = IntArray(width * height)
bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
var source = RGBLuminanceSource(width, height, pixels)
var result: Result?
try {
result = QRCodeReader().decode(BinaryBitmap(HybridBinarizer(source)), HINTS)
return result
} catch (e: Exception) {
if (decodeMultiSource) {
try {
result = QRCodeReader().decode(BinaryBitmap(GlobalHistogramBinarizer(source)), HINTS)
return result
} catch (e2: Throwable) {
e2.printStackTrace()
}
}
} catch (e: Exception) {
// do nothing
}
return null
}
上面的扫码能够对普通图片进行扫码,但是一些大的图片和过于小的含有二维码的图片就扫描不出来了
使用PlannarLumianceSource进行扫码
很多blog都说采样这个能优化扫码,但是根据我的测试结果好像并没有,甚至扫描成功率更差,不过还是贴出来,供大家参考。
bitmap获取YUV数据方法1
public static byte[] convertBitmapToYUV(Bitmap image) {
int w = image.getWidth();
int h = image.getHeight();
int[] rgb = new int[w * h];
byte[] yuv = new byte[w * h];
image.getPixels(rgb, 0, w, 0, 0, w, h);
populateYUVLuminanceFromRGB(rgb, yuv, w, h);
return yuv;
}
private static void populateYUVLuminanceFromRGB(int[] rgb, byte[] yuv420sp, int width, int height) {
for (int i = 0; i < width * height; i++) {
float red = (rgb[i] >> 16) & 0xff;
float green = (rgb[i] >> 8) & 0xff;
float blue = (rgb[i]) & 0xff;
int luminance = (int) ((0.257f * red) + (0.504f * green) + (0.098f * blue) + 16);
yuv420sp[i] = (byte) (0xff & luminance);
}
}
bitmap获取YUV数据方法2
public static byte[] getBitmapYUVBytes(Bitmap sourceBmp) {
if (null != sourceBmp) {
int inputWidth = sourceBmp.getWidth();
int inputHeight = sourceBmp.getHeight();
int[] argb = new int[inputWidth * inputHeight];
sourceBmp.getPixels(argb, 0, inputWidth, 0, 0, inputWidth, inputHeight);
byte[] yuv = new byte[inputWidth
* inputHeight
+ ((inputWidth % 2 == 0 ? inputWidth : (inputWidth + 1)) * (inputHeight % 2 == 0 ? inputHeight
: (inputHeight + 1))) / 2];
encodeYUV420SP(yuv, argb, inputWidth, inputHeight);
sourceBmp.recycle();
return yuv;
}
return null;
}
/**
* 将bitmap里得到的argb数据转成yuv420sp格式
* 这个yuv420sp数据就可以直接传给MediaCodec, 通过AvcEncoder间接进行编码
*
* @param yuv420sp 用来存放yuv429sp数据
* @param argb 传入argb数据
* @param width bmpWidth
* @param height bmpHeight
*/
private static void encodeYUV420SP(byte[] yuv420sp, int[] argb, int width, int height) {
// 帧图片的像素大小
final int frameSize = width * height;
// Y的index从0开始
int yIndex = 0;
// UV的index从frameSize开始
int uvIndex = frameSize;
// YUV数据, ARGB数据
int Y, U, V, a, R, G, B;
int argbIndex = 0;
// ---循环所有像素点,RGB转YUV---
for (int j = 0; j < height; j++) {
for (int i = 0; i < width; i++) {
// a is not used obviously
a = (argb[argbIndex] & 0xff000000) >> 24;
R = (argb[argbIndex] & 0xff0000) >> 16;
G = (argb[argbIndex] & 0xff00) >> 8;
B = (argb[argbIndex] & 0xff);
argbIndex++;
// well known RGB to YUV algorithm
Y = ((66 * R + 129 * G + 25 * B + 128) >> 8) + 16;
U = ((-38 * R - 74 * G + 112 * B + 128) >> 8) + 128;
V = ((112 * R - 94 * G - 18 * B + 128) >> 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));
// NV21 has a plane of Y and interleaved planes of VU each
// sampled by a factor of 2
// meaning for every 4 Y pixels there are 1 V and 1 U. Note the
// sampling is every other
// pixel AND every other scanline.
// ---Y---
yuv420sp[yIndex++] = (byte) Y;
// ---UV---
if ((j % 2 == 0) && (i % 2 == 0)) {
yuv420sp[uvIndex++] = (byte) V;
yuv420sp[uvIndex++] = (byte) U;
}
}
}
}
上面两个转换bitmap到yuv是在网上找到的方法,哪个正确并不知道,通过比较发现方法1要比方法2速度更快,在获取二维码的时候成功率也更高,但是看转换而成的二维码图片方法2更符合对yuv的描述,因为方法1转换成的图片是绿色的。
再贴一个yuv数据转换成图片的方法
public static Bitmap convertYUVToBitmap(byte[] data, int width, int height){
ByteArrayOutputStream out = new ByteArrayOutputStream();
YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21, width, height, null);
yuvImage.compressToJpeg(new Rect(0, 0, width, height), 50, out);
byte[] imageBytes = out.toByteArray();
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
}
使用YUVLuminance进行扫码
fun scanFromBitmap(bitmap: Bitmap): Result? {
val yuvData = getBitmapYUVBytes(bitmap)
var source = PlanarYUVLuminanceSource(yuvData, bitmap.width, bitmap.height, 0, 0, bitmap.width, bitmap.height, false)
var result: Result?
try {
result = QRCodeReader().decode(BinaryBitmap(HybridBinarizer(source)), HINTS)
return result
} catch (e: Exception) {
if (decodeMultiSource) {
try {
result = QRCodeReader().decode(BinaryBitmap(GlobalHistogramBinarizer(source)), HINTS)
return result
} catch (e2: Throwable) {
e2.printStackTrace()
}
}
} catch (e: Exception) {
// do nothing
}
return null
}
优化的扫描版本
这里对扫描的优化比较简单,就是通过不断的变换采样参数和缩放菜蔬来获取最佳扫描图片然后提高了扫码成功率。因为通过固定的采样和缩放参数往往很多图片都不能成功识别,所以再不考虑时间的基础上基本都能够识别到二维码。
val loopScaleMax = 6
val loopScaleMin = 1
val loopScaleStep = 1
private fun scanByLoopSample(path: String, s: Int):Result?{
var scanResult: Result?
for (s in sampleMaxValue downTo sampleMinValue step loopSampleStep) {
// s:采样的大小
val bitmap = AlbumUtil.getBitmapByPath(path, true, s * 100)
scanResult = decodeByLoopScale(bitmap)
if (scanResult != null) {
return scanResult
}
}
}
private fun decodeByLoopScale(bitmap: Bitmap): Result? {
var scanResult: Result? = null
for (i in loopScaleMin..loopScaleMax step loopScaleStep) {
if (scaleStep * i > bitmap!!.width * sizeTime && scaleStep * i > bitmap.height * sizeTime) {
break
}
if (i == 0 && bitmap.width > 2000 && bitmap.height > 2000) {
scalePosition = 0
scanResult = scanFromPic(bitmap, bitmap.width, bitmap.height, callback)
} else {
scalePosition = i
scanResult = scanFromPic(bitmap, scaleStep * (i), scaleStep * (i), callback)
}
if (scanResult != null) {
return scanResult
}
}
return scanResult
}
fun scanFromPic(bitmap: Bitmap?, fixWidth: Int, fixHeight: Int): Result? {
if (bitmap == null) {
return null
}
var scale = 1f
if (fixWidth < bitmap.width) {
scale = fixWidth.toFloat() / Math.max(bitmap.width, bitmap.height)
} else {
scale = fixWidth.toFloat() / Math.max(bitmap.width, bitmap.height)
}
val newBitmap = Bitmap.createScaledBitmap(bitmap,
(bitmap.width * scale).toInt(),
(bitmap.height * scale).toInt(), false)
scaledBitmap = newBitmap
return scanFromPicImpl(newBitmap, callback)
}
fun scanFromPicImpl(bitmap: Bitmap): Result? {
val width = bitmap.width
val height = bitmap.height
val pixels = IntArray(width * height)
bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
var source = RGBLuminanceSource(width, height, pixels)
var result: Result?
try {
result = QRCodeReader().decode(BinaryBitmap(HybridBinarizer(source)), HINTS)
return result
} catch (e: Exception) {
if (decodeMultiSource) {
try {
result = QRCodeReader().decode(BinaryBitmap(GlobalHistogramBinarizer(source)), HINTS)
return result
} catch (e2: Throwable) {
e2.printStackTrace()
}
}
} catch (e: Exception) {
}
return null
}
通过循环采样和循环缩放,最终找到能够识别出二维码的最佳参数。这种方法比较笨,对于没有二维码的图片也要经过这样的计算,所以如果再实际使用中最好定制一个时间,超过时间就终止扫码。在测试过程中发现有的图片的识别超级慢而且同样大小的图片有的采样从1000开始比较容易识别有的从200开始比较容易识别。