美文网首页ios iOS开发Objective-C
iOS 二维码有效区域rectOfInterest详解

iOS 二维码有效区域rectOfInterest详解

作者: S型身材的猪 | 来源:发表于2017-12-08 22:07 被阅读937次

    demo

    前言

    • 关于二维码的有效区域,在开发中遇到的人可能并不是很多,大多数情况都是直接用第三方,但是当你真正自己去尝试写的时候,你会发现二维码的有效区域是一个很令人捉摸不定的问题,其实很多基于系统的第三方并没有解决这个问题,它们都是全屏扫描。
    • 网上有一些关于rectOfInterest属性的解释,但是经过我自己的核对,发现他们说的并不是很精确,甚至可以说是错误的。我觉得我还是有必要跟大家分享一下,在网上你真的再也找不到这么详细的有关rectOfInterest的解释。

    影响rectOfInterest的因素

    rectOfInterest跟2个属性息息相关:一个是AVCaptureSession(会话对象)的sessionPreset属性,另一个是AVCaptureVideoPreviewLayer(预览图层)的videoGravity属性。在这里,我简单讲一下这2个属性的意义:

    sessionPreset属性

    该属性是设置图像音频等输出分辨率,大约一共有11个:

    // 完整的图像分辨率输出,不支持音频
    NSString *const AVCaptureSessionPresetPhoto; 
    // 最高分辨率,根据设备系统自动选择最高分辨率
    NSString *const AVCaptureSessionPresetHigh;
    // 中等分辨率,根据设备系统自动选择中等分辨率
    NSString *const AVCaptureSessionPresetMedium;
    // 最低分辨率,根据设备系统自动选择最低分辨率
    NSString *const AVCaptureSessionPresetLow;
    // 以352x288分辨率输出
    NSString *const AVCaptureSessionPreset352x288;
    // 以640x480分辨率输出
    NSString *const AVCaptureSessionPreset640x480;
    // 以1280x720分辨率输出
    NSString *const AVCaptureSessionPreset1280x720;
    // 以1920x1080分辨率输出
    NSString *const AVCaptureSessionPreset1920x1080;
    // 以960x540分辨率输出
    NSString *const AVCaptureSessionPresetiFrame960x540;
    // 以1280x720分辨率输出
    NSString *const AVCaptureSessionPresetiFrame1280x720;
    // 不去控制音频与视频输出设置,而是通过已连接的捕获设备的 activeFormat 来反过来控制 capture session 的输出质量等级
    NSString *const AVCaptureSessionPresetInputPriority;
    
    videoGravity属性

    该属性共有3个值:如果你不了解,我建议你先去熟悉一下UIView的contentMode属性,光了解没有用,必须知道它的原理以及计算方式

    // 保持原始比例,自适应最小的bounds,不足的会有留白;类似于UIView的contentMode属性的UIViewContentModeScaleAspectFit.
    AVLayerVideoGravityResizeAspect;
    
    // 保持原始比例,填充整个bounds,多余的会被剪掉,类似于UIView的contentMode属性的UIViewContentModeScaleAspectFill.
    AVLayerVideoGravityResizeAspectFill;
    
    // 拉伸直到填充整个bounds,类似于UIView的contentMode属性的UIViewContentModeScaleToFill.
    AVLayerVideoGravityResize
    

    正题

    一般的,扫描区域就是预览视图previewLayer的frame对应的矩形框,一般是设置全屏。如果我们想要设置一个有效区域怎么办,如同支付宝、微信等将扫码区域限制在一个小正方形内。这就要用到输出流AVCaptureMetadataOutput的一个rectOfInterest属性。

    rectOfInterest默认为(0,0,1,1);
    

    大家应该提出质疑:为什么宽高才为1?这也太小了吧,然而这个区域却是全屏。这是肿么肥四呢? 聪明的你应该猜到了,rectOfInterest肯定是经过某种转化而来,而且x,y, w, h的范围均在0~1之间。究竟是如何转化的,且听我慢慢说给你听:
    假如在手机屏幕中,我想限制有效扫描区域在矩形框(10,10,100,100)内,是不是这样设置:

    metadataOutput.rectOfInterest = CGRectMake(10, 10, 100, 100);
    

    这样对吗?肯定不对咯,因为还没有转化为0~1的范围内呢。
    好的,我们一起来转化一下,由于图像都是显示在预览视图previewLayer中,所以自然是通过previewLayer的frame来转化.

    假设previewLayer的frame为全屏,记为:
    preViewRect = CGRectMake(0,0,kScreenW,kScreenH);
    有效扫描区域为
    validRect =  CGRectMake(x, y, w, h);
    转化后:
    rectOfInterest = (x / kScreenW, y / kScreenH, w / kScreenW, h / kScreenH)
    

    到此,转化结束!就这样完了吗?还早着呢!

    我问大家一个问题:矩形框rectOfInterest=(0,0,1,1)应该在屏幕的哪个位置?
    大家应该会回答在屏幕的左上方,没错,不仅是你,就连官方文档的解释都是这样说的,官方文档说:

    rectOfInterest中的origin如果为(0,0),表示在图像的左上方;如果为(1,1),表示 在未经过旋转的图像的右下方。这很符合我们的想象。
    

    好。如果按照我们的想象或者官方文档所说,我们设置的有效区域:(10 ,10 , 100 ,100)应该会偏左上方。然而,结果并非如此,显示结果是这样的:

    红色矩形框代表扫码区域
    17298B562720FB2280DC530FD12022EA.jpg

    有没有发现,显示结果和我们想的完全相反,偏右上角,也就是说:

    核心句子:

    实际显示在我们肉眼看到的屏幕中的坐标原点,应该是在右上角,这就好比是小明在照镜子,假如小明真人的左脸颊有一颗痣,那么在镜子中,痣应该是在右脸颊。我们所想的rectOfInterest,都是镜中的rectOfInterest。
    

    既然我们已经知道了坐标原点,那么我们想让扫描区域(10 ,10 , 100 ,100)显示在左上方,就不是难事了。


    17298B562720FB2280DC530FD12022EA.jpg

    如上图:左边红色矩形框就是我们实际要的扫描区域所在位置,最关键是要求出图中蓝点相对原点(右上角)的坐标。

    蓝点的坐标(相对右上角)为:
    x = (kScreenW-(100+10)) / kScreenW;
    y = 10 / kScreenH; 
    
    // 除以kScreenW和kScreenH是转化比例
    

    // 由此,我们可以推导出一个转化公式:

    设 有效区域为
    validRect = CGRectMake(x, y, w, h);
    预览图层的frame为
    preViewRect = CGRectMake(0, 0, kScreenW, kScreenH);
    那么
    rectOfInterest = CGRectMake((kScreenW-(w+x)) / kScreenW, y / kScreenH, w / kScreenW, h / kScreenH);
    

    到了这里, 离成功似乎很近了,但是很遗憾, 漫长的路才刚起步!用此公式代入计算,发现扫码区域完全不对,好桑心,为什么会这样?于是猜想: AVCapture输出的图片大小都是横着的,而iPhone的屏幕是竖着的,那么我把它旋转90°呢:
    旋转90°也就意味着x与y互换,w和h互换,即:rectOfInterest的x, y, w , h 应该对应y, x , h, w;转换如下:

    有一定正确性的转化公式:

    设 有效区域为
    validRect = CGRectMake(x, y, w, h);
    预览图层的frame为
    preViewRect = CGRectMake(0, 0, kScreenW, kScreenH);
    那么
    rectOfInterest = CGRectMake(y / kScreenH, (kScreenW-(w+x)) / kScreenW, h / kScreenH, w / kScreenW);
    

    // 这个公式上升了一个级别,有了一定的正确性,但是它太“死”了,不够灵活,也就是说,假如我随意更换设备,随意修改sessionPreset和videoGravity属性的话,此公式计算出来的扫描区域是不准确的。这下该怎么办,我差点就要放弃了,到这里就结束算了,但是心里总感觉有点希望,于是彻夜都在想这个问题。
    大家还记得我开篇讲的sessionPreset和videoGravity属性吧,在这里,这俩属性就要闪亮登场了。

    核心句子:

    rectOfInterest是相对图像大小的比例,而不是相对设备或者预览图层AVCaptureVideoPreviewLayer的比例
    

    既然是相对图像,由于图像的输出有多种模式,这些模式通过AVCaptureVideoPreviewLayer的videoGravity属性设置,如AVLayerVideoGravityResizeAspectFill;由于这些模式的设置,导致图像会被裁减、留白或者拉伸,所以我们计算出来的结果是相对图像而言的,我们需要将其转化到预览图层AVCaptureVideoPreviewLayer上来。所以我开始要求大家去熟悉一下UIView的contentMode模式。
    我不废话了,我直接上转化过程,我将其封装成了一个方法.

    最终的万能转化公式:(本文核心)

    // 该方法中,_preViewLayer指的是AVCaptureVideoPreviewLayer的实例对象,_session是会话对象,_metadataOutput是扫码输出流
    - (void)coverToMetadataOutputRectOfInterestForRect:(CGRect)cropRect {
        CGSize size = _previewLayer.bounds.size;
        CGFloat p1 = size.height/size.width;
        CGFloat p2 = 0.0;
    
        if ([_session.sessionPreset isEqualToString:AVCaptureSessionPreset1920x1080]) {
            p2 = 1920./1080.;
        }
        else if ([_session.sessionPreset isEqualToString:AVCaptureSessionPreset352x288]) {
            p2 = 352./288.;
        }
        else if ([_session.sessionPreset isEqualToString:AVCaptureSessionPreset1280x720]) {
            p2 = 1280./720.;
        }
        else if ([_session.sessionPreset isEqualToString:AVCaptureSessionPresetiFrame960x540]) {
            p2 = 960./540.;
        }
        else if ([_session.sessionPreset isEqualToString:AVCaptureSessionPresetiFrame1280x720]) {
            p2 = 1280./720.;
        }
        else if ([_session.sessionPreset isEqualToString:AVCaptureSessionPresetHigh]) {
            p2 = 1920./1080.;
        }
        else if ([_session.sessionPreset isEqualToString:AVCaptureSessionPresetMedium]) {
            p2 = 480./360.;
        }
        else if ([_session.sessionPreset isEqualToString:AVCaptureSessionPresetLow]) {
            p2 = 192./144.;
        }
        else if ([_session.sessionPreset isEqualToString:AVCaptureSessionPresetPhoto]) { // 暂时未查到具体分辨率,但是可以推导出分辨率的比例为4/3
             p2 = 4./3.;
        }
        else if ([_session.sessionPreset isEqualToString:AVCaptureSessionPresetInputPriority]) {
            p2 = 1920./1080.;
        }
        else if (@available(iOS 9.0, *)) {
            if ([_session.sessionPreset isEqualToString:AVCaptureSessionPreset3840x2160]) {
                p2 = 3840./2160.;
            }
        } else {
            
        }
        if ([_previewLayer.videoGravity isEqualToString:AVLayerVideoGravityResize]) {
            _metadataOutput.rectOfInterest = CGRectMake((cropRect.origin.y)/size.height,(size.width-(cropRect.size.width+cropRect.origin.x))/size.width, cropRect.size.height/size.height,cropRect.size.width/size.width);
        } else if ([_previewLayer.videoGravity isEqualToString:AVLayerVideoGravityResizeAspectFill]) {
            if (p1 < p2) {
                CGFloat fixHeight = size.width * p2;
                CGFloat fixPadding = (fixHeight - size.height)/2;
                _metadataOutput.rectOfInterest = CGRectMake((cropRect.origin.y + fixPadding)/fixHeight,
                                                            (size.width-(cropRect.size.width+cropRect.origin.x))/size.width,
                                                            cropRect.size.height/fixHeight,
                                                            cropRect.size.width/size.width);
            } else {
                CGFloat fixWidth = size.height * (1/p2);
                CGFloat fixPadding = (fixWidth - size.width)/2;
                _metadataOutput.rectOfInterest = CGRectMake(cropRect.origin.y/size.height,
                                                            (size.width-(cropRect.size.width+cropRect.origin.x)+fixPadding)/fixWidth,
                                                            cropRect.size.height/size.height,
                                                            cropRect.size.width/fixWidth);
            }
        } else if ([_previewLayer.videoGravity isEqualToString:AVLayerVideoGravityResizeAspect]) {
            if (p1 > p2) {
                CGFloat fixHeight = size.width * p2;
                CGFloat fixPadding = (fixHeight - size.height)/2;
                _metadataOutput.rectOfInterest = CGRectMake((cropRect.origin.y + fixPadding)/fixHeight,
                                                            (size.width-(cropRect.size.width+cropRect.origin.x))/size.width,
                                                            cropRect.size.height/fixHeight,
                                                            cropRect.size.width/size.width);
            } else {
                CGFloat fixWidth = size.height * (1/p2);
                CGFloat fixPadding = (fixWidth - size.width)/2;
                _metadataOutput.rectOfInterest = CGRectMake(cropRect.origin.y/size.height,
                                                            (size.width-(cropRect.size.width+cropRect.origin.x)+fixPadding)/fixWidth,
                                                            cropRect.size.height/size.height,
                                                            cropRect.size.width/fixWidth);
            }
        }
    }
    

    上面那个公式就是最终的转化公式,有一点要声明一下,当开发者设置输出分辨率为AVCaptureSessionPresetHigh、AVCaptureSessionPresetMedium、AVCaptureSessionPresetLow、AVCaptureSessionPresetPhoto等不确定性分辨率时,我都是默认给了一个对应的明确的分辨率,例如AVCaptureSessionPresetHigh我计算时采用的是1920x1080,因为我测试时是采用的iPhone6s,其他机型未必是这个分辨率,所以当分辨率取决于设备时,你自己需要根据设备的不同去修改一下。本人没有那么多真机,所以我无法给出通用的答案。

    metadataOutputRectOfInterestForRect方法

    我想有人肯定一直在怀疑,为什么不用系统自带的metadataOutputRectOfInterestForRect方法,这个方法就是我上面那个公式的功能啊,甚至更权威。但是,试了就知道,metadataOutputRectOfInterestForRect在输入流格式发生变化之前设置是无效的,你需要监听一个通知:AVCaptureInputPortFormatDescriptionDidChangeNotification,在通知方法中调用metadataOutputRectOfInterestForRect才起作用,或者你开启扫码startRunning之后再设置也行,这些做法确实也能计算出扫码有效区域,但是会卡顿,开启扫描之后,总是会卡一下,才开始扫描,这非常影响用户体验,所以不建议使用。

    作者寄语

    你可以用我的公式和采用系统的metadataOutputRectOfInterestForRect方法的转化结果对比一下,你会发现结果的差距非常微妙,只有零点零零几的误差

    相关文章

      网友评论

        本文标题:iOS 二维码有效区域rectOfInterest详解

        本文链接:https://www.haomeiwen.com/subject/npmyixtx.html