概述

长按图片辨认二维码在移动端是很常见的操作,长按后需求对图片进行辨认,而且将二维码中所包含的数据解码出来。在咱们的事务场景中,是经过点击图片进入大图预览页面。长按大图预览的图片,会辨认图片中的二维码,而且显现有跳转按钮,提示用户能够跳转二维码对应的页面。

可是,在现有事务场景中,要求图片中二维码不能在视觉上占有太大的方位,所以只能以很小的尺寸显现在下面。为了更好的配合公司现有事务,确保对图片中二维码的辨认率,所以需求对二维码辨认进行优化。

优化方案

二维码识别率优化

方案整体分为勘探和辨认两个中心流程,勘探流程首要由图画处理算法,以及OpenCV来完结,辨认流程首要由系统AVFoundation库的CIDetector来完结。先将二维码地点区域勘探出来,随后对这个区域进行辨认增强的处理,以完结含糊、较小的二维码的辨认。

勘探流程

由于不是每一张图片上都有二维码,勘探的含义在于,查找图片中是否有二维码,以及二维码在图片中的方位。从而进行后续的针对性处理。以下,任何一步勘探有成果,都将进入辨认流程中,而且将勘探到的方位传给辨认办法。

  1. 第一次勘探。转灰度图,经过OpenCVcvtColor函数,将四通道的RGBA图片,转化为单通道的灰度图。
  2. 第2次勘探。经过算法进行直方图均衡化(非自适应,而且约束对比度),意图是让图片内概括清晰。
  3. 第三次勘探。经过算法进行伽马变换,意图是增强图画对比度。
  4. 第四次勘探。将原始灰度图的下面20%的右半部分,clone到一个新的Mat目标中,而且进行3.5倍的resize。将得到后的灰度大图调用detect进行勘探。
  5. 第五次勘探。将原始灰度图的下面20%的左半部分,clone到一个新的Mat目标中,而且进行3.5倍的resize。将得到后的灰度大图调用detect进行勘探。
  6. 勘探完毕。在二维码的定位图形、码元等中心信息没有受损的前提下,这时候根本断定这张图片上没有二维码。

需求注意的是,在勘探方案中,为什么选取下面左右两头的20%侧重进行勘探。是由于依据对公司实践事务的调研,绝大多数的二维码都是在图片的右下角方位,其次是左下角。以公司事务为例,现在没见过将二维码放在图片中心的场景。

辨认流程

  1. 勘探到二维码后,会将当时用来勘探的图片,以及勘探到的二维码区域传递给辨认办法。传递过来的图片,可能是resize后的大图,对这些大图辨认率会更高。
  2. 从传递过来的图片上,依据二维码地点区域的坐标系,将二维码地点的部分,重绘到一个新的位图目标上。为了确保辨认作用,会在重绘时加上一个15的外边距,以确保二维码Quiet Zone的特性。
  3. 用重绘后的二维码,调用AVFoundationCIDetector进行辨认,并获取辨认后的字符串。

为什么不把勘探和辨认都交给OpenCV来做。这是由于经过我的测验,我发现OpenCV的辨认率较低,远不如CIDetector的辨认率。所以,只将勘探部分交给OpenCV,但不让OpenCV去辨认二维码。

依据OpenCVdetect函数的源码进行查看,发现OpenCV的勘探是基于定位符号进行勘探的,分为横向和纵向两个方向进行勘探,运用OpenCV进行勘探是不错的选择。可是OpenCV辨认率并不高,所以用OpenCV进行勘探,结合AVFoundation进行辨认的方案,是一个比较不错的策略。

代码完结

方案整体代码量较大,这儿列出了一些首要办法的代码完结。勘探办法内部完结,会进行不同程度的增强扫描和辨认。方案中运用了一些OpenCVAPI,能够经过OpenCV官方文档了解下API的定义和调用。

生成灰度图

把传入的图片转为OpenCV能够辨认的Mat的灰度图,灰度图色彩通道只有一个,在进行后续计算上,会节省许多性能。随后进入后续的勘探部分,勘探到二维码后,会将二维码拼接事务参数,并在主线程中回来给调用方。

办法的中心逻辑有两部分,一是将UIImage转为OpenCV类型的Mat目标,二是将四个色彩通道的彩色图,转为单个色彩通道的灰度图。二值图和灰色图是不同的,灰色图是单个通道,每个单位占8位,表明范围是0~255。二值图只有0~1的展现,所以辨认速度会相对快一些。

这儿简要讲一下色彩通道的概念,CV_8UC4表明RGBA四色彩通道,占用32位空间。相同的,也有三色彩通道RGB,以及两个通道和单通道,两个通道的CV_8UC2我没用过,不知道什么场景下会需求。cvtColor函数是OpenCV中进行色彩空间转化的函数,能够将彩色图修改为灰度图。

CGFloat rows = sourceImage.size.height;
CGFloat cols = sourceImage.size.width;
cv::Mat cvMat(rows, cols, CV_8UC4);
CGColorSpaceRef colorSpace = CGImageGetColorSpace(sourceImage.CGImage);
CGContextRef context = CGBitmapContextCreate(cvMat.data,
                                             cols,
                                             rows,
                                             8,
                                             cvMat.step[0],
                                             colorSpace,
                                             kCGImageAlphaNoneSkipLast | kCGBitmapByteOrderDefault);
if (context == NULL) {
    return cvMat;
}
CGContextDrawImage(context, CGRectMake(0, 0, cols, rows), sourceImage.CGImage);
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
cv::Mat grayMat;
cv::cvtColor(cvMat, grayMat, cv::COLOR_BGR2GRAY);
return grayMat;

resize

下面代码是对左下和右下两个部分进行勘探的逻辑,代码中scale代表局部扩大的倍数。依据公司事务,先从右下角进行扩大及勘探。扩大需求获取到像素的行数和列数,这些Mat提供了对应的API

下面是中心逻辑的整理。

  1. 经过rowRangecolRange获取到需求增强扫描的像素,例如右下角区域的横排和竖排的像素。
  2. 调用clone将像素取出,并赋值Mat
  3. 调用resize函数扩大到对应的倍数。

需求注意的是,在下面办法的第二段,对右下角进行了强制辨认。是一个容错处理,属于“闭眼辨认”。第一步无论是否有勘探的成果,都会对右下角进行二维码的辨认。由于在测验中,关于非常小的二维码,会呈现关于resize后的灰度图,勘探不到可是能辨认到的状况。所以,增加了右下角没有勘探到,但仍然进行强制辨认的逻辑。

CGFloat scale = 3.5f;
cv::Mat copyMat = grayMat.rowRange(grayMat.rows * 0.8, grayMat.rows).colRange(grayMat.cols * 0.5, grayMat.cols).clone();
cv::Mat resizeMat;
cv::resize(copyMat, resizeMat, cv::Size(grayMat.cols * scale * 0.5, grayMat.rows * 0.2 * scale));
NSString *result = [self qrcodeQRCodeForGrayMat:resizeMat];
/// 闭眼辨认
if (!result.length) {
    UIImage *image = [self UIImageFromCVMat:resizeMat];
    result = [self readQRCodeWithImage:image
                      detectorAccuracy:CIDetectorAccuracyHigh];
}
if (!result.length) {
    copyMat = grayMat.rowRange(grayMat.rows * 0.8, grayMat.rows).colRange(grayMat.cols * 0.f, grayMat.cols * 0.5).clone();
    cv::resize(copyMat, resizeMat, cv::Size(grayMat.cols * scale * 0.5, grayMat.rows * 0.2 * scale));
    result = [self qrcodeQRCodeForGrayMat:resizeMat];
}
return result;

辨认

辨认办法会接纳传入的二维码坐标系,裁剪出二维码的方位并做相应的处理,随后交给AVFoundation去辨认。当勘探不届时,不会进入辨认的环节。

当勘探到二维码时,能够经过output获取二维码的四个点,次序是左上、右上、右下、左下,顺时针存储。在原有的四个点外面,加上15个单位的Quiet Zone,随后会将这部分图片裁剪出来,并交给AVFoundation去辨认。加入Quiet Zone的原因在于,有Quiet Zone的二维码,辨认率会比没有的好。

vector<cv::Point> output;
if (output.empty()) {
    return nil;
}
/// 左上
cv::Point point0 = output[0];
/// 右下
cv::Point point2 = output[2];
CGRect rect = CGRectMake(point0.x, point0.y, point2.x - point0.x, point2.y - point0.y);
CGRect insetRect = CGRectInset(rect, -15, -15);
/// rect合法性查看
if (insetRect.origin.x > 0 && insetRect.origin.y > 0 && insetRect.size.width > 0 && insetRect.size.height > 0) {
    rect = insetRect;
}
UIImage *image = [self UIImageFromCVMat:grayMat];
image = [self drawInRect:rect sourceImage:image];
NSString *result = [self readQRCodeWithImage:image
                            detectorAccuracy:CIDetectorAccuracyLow];
return result;

Mat转化

iOS中进行OpenCV开发的过程中,UIImageMat的彼此转化是经常需求的。UIImageMat在生成灰度图的过程中已经触及,下面的办法是将Mat转为UIImage。中心逻辑便是将Matdata数据,经过CGImageCreate函数创立CGImage完结的。

CGColorSpaceRef colorSpace;
CGBitmapInfo bitmapInfo;
size_t elemsize = cvMat.elemSize();
if (elemsize == 1) {
    colorSpace = CGColorSpaceCreateDeviceGray();
    bitmapInfo = kCGImageAlphaNone | kCGBitmapByteOrderDefault;
}
else {
    colorSpace = CGColorSpaceCreateDeviceRGB();
    bitmapInfo = kCGBitmapByteOrder32Host;
    bitmapInfo |= (elemsize == 4) ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNone;
}
NSData *data = [NSData dataWithBytes:cvMat.data length:elemsize * cvMat.total()];
CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
/// 依据Mat创立CGImage
CGImageRef imageRef = CGImageCreate(cvMat.cols,           // width
                                    cvMat.rows,           // height
                                    8,                    // bits per component
                                    8 * cvMat.elemSize(), // bits per pixel
                                    cvMat.step[0],        // bytesPerRow
                                    colorSpace,           // colorspace
                                    bitmapInfo,           // bitmap info
                                    provider,             // CGDataProviderRef
                                    NULL,                 // decode
                                    false,                // should interpolate
                                    kCGRenderingIntentDefault // intent
                                    );
UIImage *finalImage = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
CGDataProviderRelease(provider);
CGColorSpaceRelease(colorSpace);
return finalImage;

收益

计算方案

二维码辨认率的计算比较困难,由于场景是在长按图片辨认二维码,但图片中是否有二维码,咱们并不知道。而且,用户长按操作可能是为了保存图片,并非辨认二维码。

所以,数据剖析经过A/B测的方法进行,经过分桶方法完结,两个桶分别为50%的用户量。经过这种方法,能够得出一个根本精确的辨认数据。

收益数据

数据从两个维度来计算,辨认速度和辨认率,数据如下。

  • 优化方案相对原有方案,辨认速度提升6.8%
  • 优化方案相对原有方案,辨认率提升12%

辨认速度OpenCV表现并不是很明显,由于仅仅经过灰度图的方法提升了辨认速度,但假如到了勘探的阶段,对速度还是有一定影响的,收益为正数就很不错了。

辨认率提升还是不错的,由于是A/B方案的对比,咱们并不知道单个方案的实在辨认率。但经过咱们自测,运营常用的几个存在杂乱二维码的账号中,挨个试二维码的辨认,自测辨认率为100%

展望

除了灰度图,也考虑过经过OTSU算法将灰度图转为二值图,可是发现二值图在图片清晰度不够的状况下,定位符号比较含糊的方位会被转化为白色。这样,定位符号就会缺一个角,不能构成特定比例,导致辨认率呈现明显下降。已然二值图的方案都不可行,那就别考虑辨认概括了,所以就乖乖的用灰度图做辨认了。

后续方案加入中值滤波后,再进行OTSU算法做二值化,这样转化后的二值图作用会好许多。