内容简介:级别:★★☆☆☆标签:「iOS 原生扫描」「AVCaptureSession」「AVCaptureDevice」「rectOfInterest」作者: Xs·H
级别:★★☆☆☆
标签:「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 及相关类进行的封装。 AVCaptureSession 是 AVFoundation 框架中捕捉音视频等数据的核心类。要实现扫码功能,除了用到 AVCaptureSession 之外,还要用到 AVCaptureDevice 、 AVCaptureDeviceInput 、 AVCaptureMetadataOutput 和 AVCaptureVideoPreviewLayer 。核心代码如下:
// 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、然后将 input 和 output 添加到 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属性之前须lock
device.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获取到光线亮度值brightness
CFDictionaryRef 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(微信公众号)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- iOS 生成二维码/条形码
- zbar条形码识别库vs配置
- 小程序中生成二维码和条形码
- Seppiko Chart 1.5 已经发布,Java 条形码库
- 用OpenCV和Python识别二维码和条形码
- 条形码处理类库 ZXing 3.3.3 发布,支持 Java 9
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Data Structures and Algorithm Analysis in Java
Mark A. Weiss / Pearson / 2011-11-18 / GBP 129.99
Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course wa......一起来看看 《Data Structures and Algorithm Analysis in Java》 这本书的介绍吧!