级别:★★☆☆☆
标签:「iOS 原生扫描」「AVCaptureSession」「AVCaptureDevice」「rectOfInterest」
作者: Xs·H
审校: QiShare团队


最近做IoT项目,在智能设备配网过程中有一个扫描设备或说明书上的二维码/条形码来读取设备信息的需求,要达到的效果大体如下:

想到几年前在帐号卫士中开发过扫码功能,就扒出来封装了一下(可以从QiQRCode中获取),以方便在项目中复用。
封装共包括QiCodeManager和QiCodePreviewView两个类。QiCodeManager负责扫描功能(二维码/条形码的识别和读取等),QiCodePreviewView负责扫描界面(扫码框、扫描线、提示语等)。可按照如下方式在项目中使用两个类。

// 初始化扫码界面
_previewView = [[QiCodePreviewView alloc] initWithFrame:self.view.bounds];
_previewView.autoresizingMask = UIViewAutoresizingFlexibleHeight;
[self.view addSubview:_previewView];// 初始化扫码管理类
__weak typeof(self) weakSelf = self;
_codeManager = [[QiCodeManager alloc] initWithPreviewView:_previewView completion:^{// 开始扫描[weakSelf.codeManager startScanningWithCallback:^(NSString * _Nonnull code) {} autoStop:YES];
}];
复制代码

QiCodePreviewView内部使用CAShapeLayer绘制了遮罩maskLayer、扫描框rectLayer、框角标cornerLayer和扫描线lineLayer。因为此部分涉及代码较多,本文不做详解,可从QiQRCode中查看源码。关于CAShapeLayer的使用,QiShare在iOS 绘制圆角文章中有介绍到。

接下来重点介绍一下QiCodeManager中扫码功能的实现过程。

一、识别(捕捉)二维码/条形码

QiCodeManager是基于iOS 7+,对AVFoundation框架中的AVCaptureSession及相关类进行的封装。AVCaptureSessionAVFoundation框架中捕捉音视频等数据的核心类。要实现扫码功能,除了用到AVCaptureSession之外,还要用到AVCaptureDeviceAVCaptureDeviceInputAVCaptureMetadataOutputAVCaptureVideoPreviewLayer。核心代码如下:

// input
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:nil];// output
AVCaptureMetadataOutput *output = [[AVCaptureMetadataOutput alloc] init];
[output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];// session
_session = [[AVCaptureSession alloc] init];
_session.sessionPreset = AVCaptureSessionPresetHigh;
if ([_session canAddInput:input]) {[_session addInput:input];
}
if ([_session canAddOutput:output]) {[_session addOutput:output];// output在被add到session后才可设置metadataObjectTypes属性output.metadataObjectTypes = @[AVMetadataObjectTypeQRCode, AVMetadataObjectTypeCode128Code, AVMetadataObjectTypeEAN13Code];
}// previewLayer
AVCaptureVideoPreviewLayer *previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:_session];
previewLayer.frame = previewView.layer.bounds;
previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
[previewView.layer insertSublayer:previewLayer atIndex:0];
复制代码
// AVCaptureMetadataOutputObjectsDelegate
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection {AVMetadataMachineReadableCodeObject *code = metadataObjects.firstObject;if (code.stringValue) { }
}
复制代码

以“面向人脑”的编程思想对上述代码进行解释:
1、我们需要使用AVCaptureVideoPreviewLayer的实例previewLayer显示扫描二维码/条形码时看到的影像;
2、但是previewLayer的初始化需要AVCaptureSession的实例session对数据的输入输出进行控制;
3、那我们就初始化一个session,并将输出流的质量设置为高质量AVCaptureSessionPresetHigh;
4、因为session是依靠AVCaptureDeviceInput和AVCaptureMetadataOutput来控制数据输入输出的;
5、那就用AVCaptureDevice的实例device初始化一个input,指明device为AVMediaTypeVideo类型;
6、再初始化一个output,设置好delegate和queue以及所支持的元数据类型(二维码和不同格式的条形码);
7、然后将inputoutput添加到session中就OK了,调用[session startRunning];就可以扫描二维码了;
8、最终从-captureOutput:didOutputMetadataObjects:fromConnection:方法中得到捕捉到的二维码/条形码数据。

至此,在previewLayer范围内就可以识别二维码/条形码了。

二、指定识别二维码/条形码的区域

如果要控制在previewLayer的指定区域内识别二维码/条形码,可以通过修改output的rectOfInterest属性来达到目的。代码如下:

// 计算rect坐标
CGFloat y = rectFrame.origin.y;
CGFloat x = previewView.bounds.size.width - rectFrame.origin.x - rectFrame.size.width;
CGFloat h = rectFrame.size.height;
CGFloat w = rectFrame.size.width;
CGFloat rectY = y / previewView.bounds.size.height;
CGFloat rectX = x / previewView.bounds.size.width;
CGFloat rectH = h / previewView.bounds.size.height;
CGFloat rectW = w / previewView.bounds.size.width;// 坐标赋值
output.rectOfInterest = CGRectMake(rectY, rectX, rectH, rectW);
复制代码

1、上述的CGRectMake(rectY, rectX, rectH, rectW)与CGRectMake(x, y, w, h)的传统定义不同,可以将rectOfInterest理解成被翻转过的CGRect;
2、而rectY, rectX, rectH, rectW也不是控件或区域的值,而是所对应的比例,如上述代码中的计算公式,y, x, h, w的值可参考下图;
3、rectOfInterest的默认值为CGRectMake(.0, .0, 1.0, 1.0),表示识别二维码/条形码的区域为全屏(previewLayer区域)。

PS: 其实iOS提供了官方API来将标准rect转换成rectOfInterest,但只有在[session startRunning]之后调用才有效果,而且还会时不时地出现卡顿式地闪一下。代码如下:

// 可以在[session startRunning]之后用此语句设置扫码区域
metadataOutput.rectOfInterest = [previewLayer metadataOutputRectOfInterestForRect:rectFrame];
复制代码

三、拉近二维码/条形码(放大视频内容)

当二维码/条形码离我们较远时,拉近二维码/条形码会是一个不错的功能,效果如下:

上述效果是使用双指缩放的方式来实现的,具体代码如下:

// 添加缩放手势
UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinch:)];
[previewView addGestureRecognizer:pinchGesture];
复制代码
- (void)pinch:(UIPinchGestureRecognizer *)gesture {AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];// 设定有效缩放范围,防止超出范围而崩溃CGFloat minZoomFactor = 1.0;CGFloat maxZoomFactor = device.activeFormat.videoMaxZoomFactor;if (@available(iOS 11.0, *)) {minZoomFactor = device.minAvailableVideoZoomFactor;maxZoomFactor = device.maxAvailableVideoZoomFactor;}static CGFloat lastZoomFactor = 1.0;if (gesture.state == UIGestureRecognizerStateBegan) {// 记录上次缩放的比例,本次缩放在上次的基础上叠加lastZoomFactor = device.videoZoomFactor;// lastZoomFactor为外部变量}else if (gesture.state == UIGestureRecognizerStateChanged) {CGFloat zoomFactor = lastZoomFactor * gesture.scale;zoomFactor = fmaxf(fminf(zoomFactor, maxZoomFactor), minZoomFactor);[device lockForConfiguration:nil];// 修改device属性之前须lockdevice.videoZoomFactor = zoomFactor;// 修改device的视频缩放比例[device unlockForConfiguration];// 修改device属性之后unlock}
}
复制代码

上述代码的核心逻辑比较简单:
1、在previewView上添加一个双指捏合的手势 pinchGesture,并设定target和selector
2、在selector方法中根据gesture.scale调整device.videoZoomFactor;
3、注意在修改device属性之前要lock一下,修改完后unlock一下。

四、弱光环境下开启手电筒

弱光环境对扫码功能有较大的影响,通过监测光线亮度给用户提供打开手电筒的选择会提升不少的体验,如下图:

弱光监测的代码如下:

- (void)observeLightStatus:(void (^)(BOOL, BOOL))lightObserver {_lightObserver = lightObserver;AVCaptureVideoDataOutput *lightOutput = [[AVCaptureVideoDataOutput alloc] init];[lightOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()];if ([_session canAddOutput:lightOutput]) {[_session addOutput:lightOutput];}
}// AVCaptureVideoDataOutputSampleBufferDelegate
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {// 通过sampleBuffer获取到光线亮度值brightnessCFDictionaryRef metadataDicRef = CMCopyDictionaryOfAttachments(NULL, sampleBuffer, kCMAttachmentMode_ShouldPropagate);NSDictionary *metadataDic = (__bridge NSDictionary *)metadataDicRef;CFRelease(metadataDicRef);NSDictionary *exifDic = metadataDic[(__bridge NSString *)kCGImagePropertyExifDictionary];CGFloat brightness = [exifDic[(__bridge NSString *)kCGImagePropertyExifBrightnessValue] floatValue];// 初始化一些变量,作为是否透传brightness的因数AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];BOOL torchOn = device.torchMode == AVCaptureTorchModeOn;BOOL dimmed = brightness < 1.0;static BOOL lastDimmed = NO;// 控制透传逻辑:第一次监测到光线或者光线明暗变化(dimmed变化)时透传if (_lightObserver) {if (!_lightObserverHasCalled) {_lightObserver(dimmed, torchOn);_lightObserverHasCalled = YES;lastDimmed = dimmed;}else if (dimmed != lastDimmed) {_lightObserver(dimmed, torchOn);lastDimmed = dimmed;}}
}
复制代码

弱光监测是依赖AVCaptureVideoDataOutput和AVCaptureVideoDataOutputSampleBufferDelegate来实现的。
1、初始化AVCaptureVideoDataOutput的实例lightOutput后,设定delegate并将lightOutput添加到session中;
2、实现AVCaptureVideoDataOutputSampleBufferDelegate的回调方法-captureOutput:didOutputSampleBuffer:fromConnection:
3、对回调方法中的sampleBuffer进行各种操作(具体参考上述代码细节),并最终获取到光线亮度brightness
4、根据brightness的值设定弱光的标准以及是否透传给业务逻辑(这里认为brightness < 1.0为弱光)。

调用- observeLightStatus:方法并实现blck即可接收透传过来的光线状态和手电筒状态,并根据状态对UI做相应的调整,代码如下:

__weak typeof(self) weakSelf = self;
[self observeLightStatus:^(BOOL dimmed, BOOL torchOn) {if (dimmed || torchOn) {// 变为弱光或者手电筒处于开启状态[weakSelf.previewView stopScanning];// 停止扫描动画[weakSelf.previewView showTorchSwitch];// 显示手电筒开关} else {// 变为亮光并且手电筒处于关闭状态[weakSelf.previewView startScanning];// 开始扫描动画[weakSelf.previewView hideTorchSwitch];// 隐藏手电筒开关}
}];
复制代码

当出现手电筒开关时,我们可以通过点击开关改变手电筒的状态。开关手电筒的代码如下:

+ (void)switchTorch:(BOOL)on {AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];AVCaptureTorchMode torchMode = on? AVCaptureTorchModeOn: AVCaptureTorchModeOff;if (device.hasFlash && device.hasTorch && torchMode != device.torchMode) {[device lockForConfiguration:nil];// 修改device属性之前须lock[device setTorchMode:torchMode];// 修改device的手电筒状态[device unlockForConfiguration];// 修改device属性之后unlock}
}
复制代码

手电筒开关(按钮)封装在QiCodePreviewView中,QiCodeManager中通过QiCodePreviewViewDelegate的- codeScanningView:didClickedTorchSwitch:方法获取手电筒开关的点击事件,并做相应的逻辑处理。代码如下:

// QiCodePreviewViewDelegate
- (void)codeScanningView:(QiCodePreviewView *)scanningView didClickedTorchSwitch:(UIButton *)switchButton {switchButton.selected = !switchButton.selected;[QiCodeManager switchTorch:switchButton.selected];_lightObserverHasCalled = switchButton.selected;
}
复制代码

综上,扫描二维码/条形码的功能就实现完了。此外,QiCodeManager中还封装了生成二维码/条形码的方法,下篇文章介绍。


示例源码:QiQRCode可从GitHub的QiShare开源库中获取。


关注我们的途径有:
QiShare(简书)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公众号)

推荐文章:
iOS 了解Xcode Bitcode
iOS 重绘之drawRect
iOS 编写高质量Objective-C代码(八)
iOS KVC与KVO简介
奇舞周刊

iOS 扫描二维码/条形码相关推荐

  1. iOS 生成二维码/条形码

    级别:★★☆☆☆ 标签:「iOS CIFilter」「CIQRCodeGenerator」「CICode128BarcodeGenerator」「二维码加logo」 作者: Xs·H 审校: QiSh ...

  2. Android PAD扫描枪扫描二维码条形码

    Android PAD扫描枪扫描二维码条形码 1,目前扫描条码只有通过按键触发,按下按键会发送F12的键值,可以通过监听F12键判断是否触发扫描 2,扫到的条码我们会在当前光标处显示出来,同时也发了一 ...

  3. Android使用ZBar扫描二维码/条形码(实例)+常见问题汇总

    写在前面:因项目需求,需要实现二维码扫码功能,笔者测试过多种开源扫码工具,但因不跨平台.扫描速度慢等问题逐个放弃,最后选用ZBar实现功能,笔者发现ZBar扫码在跨主流手机平台.扫码速度等方面有较明显 ...

  4. 微信小程序扫描二维码条形码 (wx.scanCode)

    前言 在业务中遇到需要获取商品的二维码的信息返回商品的二维码信息,在调用后台的接口,首先想到用小程序的Api,wx.scanCode(Object object)获取二维码信息. 支持相机扫描或者相册 ...

  5. 微信小程序扫描二维码条形码

    wxml代码 <button class='deaBtn' bindtap='scancode'>扫描二维码</button> js代码 scancode: function( ...

  6. iOS 扫描二维码自动打开灯 检测环境光线强度

    扫描二维码自动打开灯 检测环境光线强度,做的不太好,打开灯了没有关闭,后期优化可以打开关闭的代码,并且加一个定时器时间间隔(例如10秒左右)来控制是否改变灯的状态,否则灯会随着光线闪来闪去的.最简单 ...

  7. python扫描二维码输出内容_通过python扫描二维码/条形码并打印数据

    需提前安装好pyzbar和opencv-python库(博主的电脑安装opencv-python库比较麻烦,但大部分都不会出现该问题) 安装方法:打开命令框输入 pip install pyzbar/ ...

  8. python发票二维码条码识别_通过python扫描二维码/条形码并打印数据

    需提前安装好pyzbar和opencv-python库(博主的电脑安装opencv-python库比较麻烦,但大部分都不会出现该问题) 安装方法:打开命令框输入 pip install pyzbar/ ...

  9. ionic 扫描二维码/条形码功能

    一.安装插件@ionic-native/qr-scanne 二.page scan.ts import { Component } from '@angular/core'; import { Ion ...

最新文章

  1. 创新工场提出中文分词和词性标注模型,性能分别刷新五大数据集| ACL 2020​
  2. 实战c++中的vector系列--vectorlt;unique_ptrlt;gt;gt;初始化(全部权转移)
  3. 11 为了进一步_小米11正式官宣!12月28号整装待发,这几点或成关键
  4. vi 编辑器基本使用
  5. 通信 / 各种协议默认端口汇总
  6. jQuery获取时间,一位数则补零
  7. 2.cocos2d-x坐标体系(UI坐标系,GL坐标系,本地坐标,世界坐标,节点坐标)
  8. php imap配置,怎么为PHP编译imap扩展?
  9. ListString^^ 引用空间
  10. maven编译项目时提示:cached in the local repository
  11. linux arm current_thread_info定义,linux 内核 current全局变量
  12. 2020顶会指南:征稿截止时间、举办地、举办时间一览
  13. Java里面as_与Java中的C#关键字“ as”等效
  14. C和指针 第五章 位数组
  15. mysql 减去_MySql进阶面试题
  16. vba手机号码归属_手机号码归属地查询代码
  17. Qt 语言切换 QTranslator cmake qmake
  18. [源码]VB6.0操作注册表
  19. php 音频上传之ogg格式,如何快速将MP3格式转化成ogg格式
  20. python-can库基于PCAN-USB使用方法

热门文章

  1. Google 新系统 Fuchsia 概览和浅析
  2. sql2012试用版本过期处理
  3. windows下Python安装pymysql
  4. FFmpeg 命令详解
  5. Windows下如何硬盘安装Ubuntu
  6. 【OpenCV 例程300篇】208. Photoshop 对比度自动调整算法
  7. 序列化之Serialize
  8. 人体姿态识别-pose estimation
  9. 浅谈Vue渐进式的理解
  10. 二分图的判定最大匹配