[iOS] 图文讲解原生二维码有效扫描区域 rectOfInterest
在使用原生的 AVFoundation
框架实现二维码扫描的时候, 需要注意一下两个方面:
- 启动相机的卡顿问题;
- 有效扫描区域的问题; 本文主要针对这两个问题进行讲解.
1. 启动扫描卡顿
在Push
到二维码扫描页时, 一般在初始化扫描视图的时候就开始启动session
:
[self.session startRunning]
但是这样会有一个问题, 就是点击扫描按钮的时候, 按钮会1秒多的卡顿, 然后才会Push
到扫描页;
针对这个问题, 有的人是在Push
到扫描页的时候, 才去启动 session
, 但是这样会有1 – 2秒的等待时间, 才会出现正常的扫描画面;
针对此问题, 解决的方式很简单, 依然在初始化的时候启动 session
, 但是, 需要异步去启动:
[self.activity startAnimating];dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);dispatch_async(queue, ^{if (self.session.isRunning == NO) {NSLog(@"startRunning");[self.session startRunning];}dispatch_async(dispatch_get_main_queue(), ^{[self.activity stopAnimating];});});
这样, 在push
的过程中就在准备session
, 可以很大程度上缩短等待时间, 甚至, 在push
到扫描页的时候, 几乎看不到等待时间.
需要注意
如果使用了 属性观察者模式, 其方法是异步执行的, 需要返回到主线程, 例如这里我监听了属性 running
:
[self.session addObserver:self forKeyPath:@"running" options:NSKeyValueObservingOptionNew context:nil];
根据这个来开始/停止扫描线的动画, 这时候, 就需要回到主线程执行:
- (void)observeValueForKeyPath:(NSString *)keyPathofObject:(id)objectchange:(NSDictionary *)changecontext:(void *)context{dispatch_async(dispatch_get_main_queue(), ^{if ([object isKindOfClass:[AVCaptureSession class]]) {if ([keyPath isEqualToString:@"running"]) {BOOL isRunning = ((AVCaptureSession *)object).isRunning;if (isRunning) {[self startAnimate];}else{[self stopAnimate];}}}});
}
2. AVCaptureMetadataOutput 有效扫描区域 rectOfInterest
首先, 需要知道 rectOfInterest
所在的坐标系, 其坐标原点是图像的左上角, 注意, 这里是图像的左上角, 不是设备的左上角, 其值介于 0 – 1, 如果设置为 CGRectMake(0, 0, 1, 1)
将是全屏幕扫描; 所以需要将我们的扫描框的坐标(相对于设备), 转换为 0–1 之间的值(相对于图像).
2.1. 影响因素
要想准确计算其区域, 就需要知道影响因素, rectOfInterest
的影响因素有以下两个:
AVCaptureSession
的sessionPreset
属性
AVCaptureVideoPreviewLayer
的videoGravity
属性
前者为图像的分辨率, 不同的值生成不同分辨率的图像;
后者为预览时的图像填充模式, 类似于 UIView
的 contentMode
属性;
sessionPreset
// 完整的图像分辨率输出,不支持音频
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
// 保持原始比例,自适应最小的bounds,不足的会有留白;类似于UIView的contentMode属性的UIViewContentModeScaleAspectFit.
AVLayerVideoGravityResizeAspect;// 保持原始比例,填充整个bounds,多余的会被剪掉,类似于UIView的contentMode属性的UIViewContentModeScaleAspectFill.
AVLayerVideoGravityResizeAspectFill;// 拉伸直到填充整个bounds,类似于UIView的contentMode属性的UIViewContentModeScaleToFill.
AVLayerVideoGravityResize
2.2. 转换关系
其实, 我们主要需要做的就是, 将我们屏幕中的扫描区域, 转换到图像中对应的位置上;
首先需要知道图像的方向, AVFoundation
捕获的图像是横着的, 且方向朝右;
其次, 需要知道我们在屏幕中看到的图像, 和实际的图像是镜像关系;
这里以图像完全填充屏幕(即 videoGravity
值为 AVLayerVideoGravityResize
), 假设, 我们设置的扫描区域为(30, 80, 100, 80)
;
另外, 转换的关系与设备方向也有关系, 下面就是图像完全填充屏幕时的不同设备方向的转换关系
2.2.1. 竖屏, Home
键在下 (UIInterfaceOrientationPortrait
)
此时设备方向为朝上(竖屏, Home
键在下), 扫描区域在设备屏幕中的位置为:
首先, 将图像进行左右镜像, 中间图所示
接着顺时针旋转 90 度, 使设备方向和图像方向一致,右边图所示
此时, 显示的位置, 大致就是其在图像中的位置; 扫描区域的位置有了, 但是其坐标原点呢? 上面说了是在图像的左上角, 是上面图中的哪个呢? o1
? 还是 o2
?
答案是 o2
, 图像的左上角, 当然是当图像是正着看的时候的左上角, 也就是我们看到的图像的右上角; 这时的坐标系如下所示:
坐标系有了, 接下来就是坐标的转化了; 和最初的扫描区域相比, 可以看到 x/y/width/height
都进行的镜像翻转, 即最终的结果为:
CGRect r = CGRectMake(0, 0, 1., 1.);
r.origin.x = (cropRect.origin.y)/size.height;
r.origin.y = (size.width - CGRectGetMaxX(cropRect))/size.width;
r.size.width = cropRect.size.height/size.height;
r.size.height = cropRect.size.width/size.width;
这里的size
为预览图层的Size
, 一般也是屏幕的size
;
上面是竖屏情况下, 图片完全填充时的计算方式; 其他情况转换方式是一样的, 只不过需要计算图片的实际宽高;
2.2.2. 横屏, Home
键在右 (UIInterfaceOrientationLandscapeRight
)
扫描区域在设备屏幕中位置为:
由上面知道, 捕获到的图像是横着的, 方向朝右, 此时设备也是横着的, 但是设备的方向和图像相反, 所以, 将扫描区域进行镜像翻转之后, 再顺时针旋转 180 度:
其坐标原点同上, 也是在我们看到的图像的右上角; 所以, 其转换公式为:
CGRect r = CGRectMake(0, 0, 1., 1.);r.origin.x = (CGRectGetMinX(cropRect))/size.width;r.origin.y = (CGRectGetMinY(cropRect))/size.height;r.size.width = cropRect.size.width/size.width ;r.size.height = cropRect.size.height/size.height;
2.2.3. 横屏, Home
键在左 (UIInterfaceOrientationLandscapeLeft
)
扫描区域在设备屏幕中位置为:
此时设备和图像方向一致, 所以, 只需要进行上下镜像即可:
所以, 其转换公式为:
CGRect r = CGRectMake(0, 0, 1., 1.);r.origin.x = (CGRectGetMinX(cropRect))/size.width;r.origin.y = (CGRectGetMinY(cropRect))/size.height;r.size.width = cropRect.size.width/size.width ;r.size.height = cropRect.size.height/size.height;
2.2.4. 竖屏, Home
键在上(UIInterfaceOrientationPortraitUpsideDown
)
先进行左右镜像, 然后, 逆时针旋转 90 度:
所以, 其转换公式为:
CGRect r = CGRectMake(0, 0, 1., 1.);r.origin.x = (size.height - CGRectGetMaxY(cropRect))/size.height;r.origin.y = (CGRectGetMinX(cropRect))/size.width;r.size.width = cropRect.size.height/size.height;r.size.height = cropRect.size.width/size.width;
2.3. 其他图像填充模式转换
上面讲的是, 图像完全填充满整个屏幕的情况, 有可能会导致图片被挤压变形, 实际使用时很少采用; 而 AVLayerVideoGravityResizeAspect
模式, 屏幕中可能会左右留有黑边或者上下留有黑边, 也很少采用; 一般我们在使用的时候会使用 AVLayerVideoGravityResizeAspectFill
方式进行图片填充, 铺满屏幕, 多余的会被裁掉; 不同的填充模式, 在参考上面的转换方式进行转换时, 需要考虑被裁掉的部分图像, 或者出现的黑边;
以下示意图的说明
左图为扫描区域在屏幕中的位置, 右图为扫描区域转换为图像中的位置;
灰色背景为设备屏幕大小;
红色线框为图像大小
红色区域为扫描框
2.3.1. 竖屏, Home 键在下
- AVLayerVideoGravityResizeAspectFill
- 设备竖屏状态
- 图像填充模式为
AVLayerVideoGravityResizeAspectFill
- 图像上下或左右部分会被裁去
扫描区域在设备中的位置如下图左所示:
可以看到, 实际我们在屏幕中看到的图像只是原图像的中间部分, 上下或左右 padding
大小的区域被裁去;
此时, 坐标的转换应该是:
// p1 为设备屏幕(即previewLayer)的height/width
// p2 为图像的分辨率 height/widthif (p1 < p2) {CGFloat height = size.width * p2;CGFloat padding = (height - size.height)/2.;r.origin.x = (CGRectGetMinY(cropRect) + padding)/height ;r.origin.y = (size.width - CGRectGetMaxX(cropRect))/size.width ;r.size.width = cropRect.size.height/height ;r.size.height = cropRect.size.width/size.width ;} else {CGFloat width = size.height / p2;CGFloat padding = (width - size.width)/2.;r.origin.x = CGRectGetMinY(cropRect)/size.height ;r.origin.y = (size.width - CGRectGetMaxX(cropRect) + padding)/width;r.size.width = cropRect.size.height/size.height ;r.size.height = cropRect.size.width/width ;}
- AVLayerVideoGravityResizeAspect
- 设备竖屏状态
- 图像填充模式为
AVLayerVideoGravityResizeAspect
- 图像上下或左右部分会有黑边
可以看到, 实际我们在屏幕中看到的图像是原图像的等比缩小, 上下 或左右会有 padding 区域的黑边;
此时, 坐标的转换应该是:
// p1 为设备屏幕(即previewLayer)的height/width
// p2 为图像的分辨率 height/widthif (p1 > p2) {CGFloat height = size.width * p2;CGFloat padding = (size.height - height)/2.0;r.origin.x = (CGRectGetMinY(cropRect) - padding)/height ;r.origin.y = (size.width - CGRectGetMaxX(cropRect))/size.width ;r.size.width = cropRect.size.height/height ;r.size.height = cropRect.size.width/size.width ;} else {CGFloat width = size.height * (1./p2);CGFloat padding = (size.width - width)/2;r.origin.x = CGRectGetMinY(cropRect)/size.height ;r.origin.y = (size.width - CGRectGetMaxX(cropRect) - padding)/width ;r.size.width = cropRect.size.height/size.height ;r.size.height = cropRect.size.width/width ;}
2.3.2. 横屏, Home 键在右
- AVLayerVideoGravityResizeAspectFill
图像填充模式为
AVLayerVideoGravityResizeAspectFill
图像上下或左右部分会被裁去
扫描区域在设备中的位置如下图左所示:
可以看到, 实际我们在屏幕中看到的图像只是原图像的中间部分, 上下或左右 padding
大小的区域被裁去;
此时, 坐标的转换应该是:
// p1 为设备屏幕(即previewLayer)的height/width
// p2 为图像的分辨率 height/widthif (p1 > p2) {CGFloat width = size.height / p2;CGFloat padding = (width - size.width) / 2.0;r.origin.x = (CGRectGetMinX(cropRect) + padding)/width ;r.origin.y = (CGRectGetMinY(cropRect))/size.height ;r.size.width = cropRect.size.width/width ;r.size.height = cropRect.size.height/size.height ;} else {CGFloat height = size .width * p2;CGFloat padding = (height - size.height) / 2.0;r.origin.x = CGRectGetMinX(cropRect) / size.width;r.origin.y = (CGRectGetMinY(cropRect) + padding) / height;r.size.width = CGRectGetWidth(cropRect) / size.width;r.size.height = CGRectGetHeight(cropRect) / height;}
- AVLayerVideoGravityResizeAspect
图像填充模式为
AVLayerVideoGravityResizeAspect
图像上下或左右部分会有黑边
可以看到, 实际我们在屏幕中看到的图像是原图像的等比缩小, 上下 或左右会有 padding 区域的黑边;
此时, 坐标的转换应该是:
// p1 为设备屏幕(即previewLayer)的height/width
// p2 为图像的分辨率 height/widthif (p1 > p2) {CGFloat height = size.width * p2;CGFloat padding = (size.height - height)/2.0;r.origin.x = CGRectGetMinX(cropRect)/size.width ;r.origin.y = (CGRectGetMinY(cropRect) - padding)/ height ;r.size.width = cropRect.size.width/size.width ;r.size.height = cropRect.size.height/height ;} else {CGFloat width = size.height / p2;CGFloat padding = (size.width - width)/2.;r.origin.x = (CGRectGetMinX(cropRect) - padding)/width ;r.origin.y = (CGRectGetMinY(cropRect))/size.height ;r.size.width = cropRect.size.width/width ;r.size.height = cropRect.size.height/size.height ;}
2.3.3. 横屏, Home 键在左
- AVLayerVideoGravityResizeAspectFill
图像填充模式为
AVLayerVideoGravityResizeAspectFill
图像上下或左右部分会被裁去
扫描区域在设备中的位置如下图左所示:
可以看到, 实际我们在屏幕中看到的图像只是原图像的中间部分, 上下或左右 padding
大小的区域被裁去;
此时, 坐标的转换应该是:
// p1 为设备屏幕(即previewLayer)的height/width
// p2 为图像的分辨率 height/widthif (p1 > p2) {CGFloat width = size.height / p2;CGFloat padding = (width - size.width) / 2.0;r.origin.x = (CGRectGetMinX(cropRect) + padding)/width ;r.origin.y = (CGRectGetMinY(cropRect))/size.height ;r.size.width = cropRect.size.width/width ;r.size.height = cropRect.size.height/size.height ;} else {CGFloat height = size .width * p2;CGFloat padding = (height - size.height) / 2.0;r.origin.x = CGRectGetMinX(cropRect) / size.width;r.origin.y = (CGRectGetMinY(cropRect) + padding) / height;r.size.width = CGRectGetWidth(cropRect) / size.width;r.size.height = CGRectGetHeight(cropRect) / height;}
- AVLayerVideoGravityResizeAspect
图像填充模式为
AVLayerVideoGravityResizeAspect
图像上下或左右部分会有黑边
可以看到, 实际我们在屏幕中看到的图像是原图像的等比缩小, 上下 或左右会有 padding 区域的黑边;
此时, 坐标的转换应该是:
// p1 为设备屏幕(即previewLayer)的height/width
// p2 为图像的分辨率 height/widthif (p1 > p2) {CGFloat height = size.width * p2;CGFloat padding = (size.height - height)/2.0;r.origin.x = CGRectGetMinX(cropRect)/size.width ;r.origin.y = (CGRectGetMinY(cropRect) - padding)/ height ;r.size.width = cropRect.size.width/size.width ;r.size.height = cropRect.size.height/height ;} else {CGFloat width = size.height / p2;CGFloat padding = (size.width - width)/2.;r.origin.x = (CGRectGetMinX(cropRect) - padding)/width ;r.origin.y = (CGRectGetMinY(cropRect))/size.height ;r.size.width = cropRect.size.width/width ;r.size.height = cropRect.size.height/size.height ;}
2.3.4. 竖屏, Home 键在上
- AVLayerVideoGravityResizeAspectFill
图像填充模式为
AVLayerVideoGravityResizeAspectFill
图像上下或左右部分会被裁去
扫描区域在设备中的位置如下图左所示:
可以看到, 实际我们在屏幕中看到的图像只是原图像的中间部分, 上下或左右 padding
大小的区域被裁去;
此时, 坐标的转换应该是:
// p1 为设备屏幕(即previewLayer)的height/width
// p2 为图像的分辨率 height/widthif (p1 < p2) {CGFloat height = size.width * p2;CGFloat padding = (height - size.height)/2.;r.origin.x = (size.height - CGRectGetMaxY(cropRect) + padding)/height;r.origin.y = (CGRectGetMinX(cropRect))/size.width;r.size.width = cropRect.size.height/height;r.size.height = cropRect.size.width/size.width;} else {CGFloat width = size.height / p2;CGFloat padding = (width - size.width)/2.;r.origin.x = (size.height - CGRectGetMaxY(cropRect))/size.height;r.origin.y = (CGRectGetMinX(cropRect) + padding)/width;r.size.width = cropRect.size.height/size.height;r.size.height = cropRect.size.width/width;}
- AVLayerVideoGravityResizeAspect
图像填充模式为
AVLayerVideoGravityResizeAspect
图像上下或左右部分会有黑边
可以看到, 实际我们在屏幕中看到的图像是原图像的等比缩小, 上下 或左右会有 padding 区域的黑边;
此时, 坐标的转换应该是:
// p1 为设备屏幕(即previewLayer)的height/width
// p2 为图像的分辨率 height/widthif (p1 > p2) {CGFloat height = size.width * p2;CGFloat padding = (size.height - height)/2.0;r.origin.x = (size.height - CGRectGetMaxY(cropRect) - padding)/height;r.origin.y = (CGRectGetMinX(cropRect))/size.width;r.size.width = cropRect.size.height/height;r.size.height = cropRect.size.width/size.width;} else {CGFloat width = size.height * (1./p2);CGFloat padding = (size.width - width)/2;r.origin.x = CGRectGetMinY(cropRect)/size.height ;r.origin.y = (CGRectGetMinX(cropRect) - padding)/width ;r.size.width = cropRect.size.height/size.height ;r.size.height = cropRect.size.width/width ;}
参考文章 iOS 二维码有效区域rectOfInterest详解
[iOS] 图文讲解原生二维码有效扫描区域 rectOfInterest相关推荐
- iOS系统原生二维码条形码扫描
本文讲述如何用系统自带的东东实现二维码扫描的功能:点击当前页面的某个按钮,创建扫描VIEW.细心的小伙伴可以发现 title被改变了,返回按钮被隐藏了.这个代码自己写就行了,与本文关系不大...绿色的 ...
- iOS原生二维码扫描(一)
首先搭建一个最初步的能识别出二维码信息的最基本框架: @interface ScanCodeViewController ()<AVCaptureMetadataOutputObjectsDel ...
- iOS 原生二维码扫描和生成
代码地址如下: http://www.demodashi.com/demo/12551.html 一.效果预览: 功能描述:WSLNativeScanTool是在利用原生API的条件下封装的二维码扫描 ...
- iOS原生二维码扫码实现(含蒙版和扫码动画)
#一.iOS实现原生扫码的意义 二维码扫码功能对于现在的iOS App开发来说是非常重要的. 通常为了节省开发时间,很多开发者会采用ZXing和ZBar等第三方SDK进行开发. 这样的好处是快速便捷, ...
- iOS 7原生二维码扫描中文gbk编码乱码的解决
有的二维码生成的含有中文的数据编码是GBK编码,如百度二维码生成器,使用系统原生二维码扫描就会出现乱码,于是开始网上查阅,该试的方法都尝试过了,终于功夫不负有心人,问题得到了解决,先上代码 NSStr ...
- iOS开发-定制多样式二维码
iOS开发-定制多样式二维码 二维码/条形码是按照某种特定的几何图形按一定规律在平台(一维/二维方向上)分布的黑白相间的图形纪录符号信息.使用若干个与二进制对应的几何形体来表示文字数值信息. 最常 ...
- iOS--AVFoundation原生二维码与一维码扫描
概述 实现二维码和条形码扫描,两大开源组件ZBar与ZXing ZBar: 扫描灵敏性,内存较优,但"圆角二维码"扫描比较困难. ZXing: Google Code上的一个开源的 ...
- 苹果原生二维码生成与扫描及生成的二维码不清楚的解决方案
苹果原生二维码生成与扫描及生成的二维码不清楚的解决方案 参考文章: (1)苹果原生二维码生成与扫描及生成的二维码不清楚的解决方案 (2)https://www.cnblogs.com/CoderEYL ...
- ios使用AVFoundation读取二维码的方法
二维码(Quick Response Code,简称QR Code)是由水平和垂直两个方向上的线条设计而成的一种二维条形码(barcode).可以编码网址.电话号码.文本等内容,能够存储大量的数据信息 ...
最新文章
- animate inater插件_C4D R20插件下载 旧版插件C4D R20桥接插件INSYDIUMS Plug-In Bridge Cinema 4D R20 免费版 下载-脚本之家...
- [YTU]_2914 ( xiaoping学构造函数)
- easyui combo自动高度(下拉框空白问题)
- 设计模式C++实现(5)——原型模式
- Module build failed: Error: Missing binding
- HDOJ 4734 数位DP
- 实现弹出窗口并转到另一个页面
- ubuntu 12.04 3D特效
- 透视形变及其校准的方法
- Maven dependency plugin使用
- 谷歌浏览器弹出Chrome版本太旧解决方式
- 陈强老师公开课笔记2——中介效应的原理与检验
- php二次开发帝国,帝国CMS二次开发注意事项
- 青龙面板安装搭建详细教程
- Debian权威发音
- python表情包多样化聊天室_Python | 信不信我分分钟批量做你大堆的表情包?
- AI辅助检测脑动脉瘤,灵敏度达97.5%,华为云联合成果登上国际顶级期刊
- 【windows】window10打开图片显示黑屏,一直打不开
- linux dd 填充全ff,用shell命令tr dd生成内容为FF指定大小的命令。
- svg-icon的使用(将svg转换为icon来使用)