上一篇介绍了AVFoundation框架的核心基础功能媒体捕捉,现在深入了解下它更高级方面的应用.
1. 视频缩放
关于视频缩放,iOS7之前通过AVCaptureConnection的VideoScaleAndCropFactor
属性对摄像头进行限制.但是这个属性只能在配置AVCaptureStillImageOutput
类输出的connection时设置.这让在进行视频捕捉时无法使用.
而iOS7版本为缩放功能提供了一个更好的方法,现在可以直接对AVCaptureDevice对象使用缩放参数videoZoomFactor
来控制设备的缩放等级(参数最大值由设备的activeFormat确定,最小值1.0),这适合所有会话输出.并在预览层AVCaptureVideoPreviewLayer
在内都会自动响应这个变化.
- (BOOL)cameraSupportsZoom { // 只有设备的缩放参数大于1.0才表示可以进行缩放
return self.activeCamera.activeFormat.videoMaxZoomFactor > 1.0f;
}
- (void)setZoomValue:(CGFloat)zoomValue {
if (!self.activeCamera.isRampingVideoZoom) { // 是否正在缩放
NSError *error;
if ([self.activeCamera lockForConfiguration:&error]) { // 上面介绍过,修改捕捉设备都需要先锁定设备-修改-再解锁.
self.activeCamera.videoZoomFactor = zoomValue;
[self.activeCamera unlockForConfiguration];
} else {
[self.delegate deviceConfigurationFailedWithError:error];
}
}
}
// 如果你希望缩放是连续平滑的,调用
// [self.activeCamera rampToVideoZoomFactor:zoomFactor withRate:1.0f]; 来改变缩放参数的值.
// 停止这个过程 [self.activeCamera cancelVideoZoomRamp];
2. 人脸检测
iOS的内置相机会自动识别人脸并建立相应的焦点.我们可以用通过一个特定的AVCaptureOutput类型AVCaptureMetadataOutput
来实现相同功能.这个Output输出的是元数据,这来自于一个AVMetadataObject抽象类的形式,该类定义了用来多种处理元数据类型的接口.而人脸识别将要用到它的一个子类AVMetadataFaceObject
.
AVMetadataFaceObject
定义了多个用来描述被检测人脸的属性.
- (BOOL)setupSessionOutputs:(NSError **)error {
self.metadataOutput = [[AVCaptureMetadataOutput alloc] init];
// 添加元数据类型Output到会话上
if ([self.captureSession canAddOutput:self.metadataOutput]) {
[self.captureSession addOutput:self.metadataOutput];
// 配置metadataOutput对象时,指定输出的元数据类型可以优化处理,减少我们不需要的对象数量.
NSArray *metadataObjectTypes = @[AVMetadataObjectTypeFace];
self.metadataOutput.metadataObjectTypes = metadataObjectTypes;
// 当有新的元数据被检测到时,需要一个回调来处理.由于人脸检测用到硬件加速,很多重要任务要在主线程中执行.
dispatch_queue_t mainQueue = dispatch_get_main_queue();
[self.metadataOutput setMetadataObjectsDelegate:self
queue:mainQueue];
return YES;
} else {
if (error) {
NSDictionary *userInfo = @{NSLocalizedDescriptionKey:
@"Failed to still image output."};
*error = [NSError errorWithDomain:THCameraErrorDomain
code:THCameraErrorFailedToAddOutput
userInfo:userInfo];
}
return NO;
}
}
// 捕捉元数据回调
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputMetadataObjects:(NSArray *)metadataObjects
fromConnection:(AVCaptureConnection *)connection {
for (AVMetadataFaceObject *face in metadataObjects) { // 人脸属性
NSLog(@"Face detected with ID: %li", (long)face.faceID);
NSLog(@"Face bounds: %@", NSStringFromCGRect(face.bounds));
}
// 将元数据可视化.
[self.faceDetectionDelegate didDetectFaces:metadataObjects];
}
// THPreviewView.m 中 可视化实现如下.
- (void)didDetectFaces:(NSArray *)faces {
// 由于metadataOutput捕捉到的元数据位于设备空间,使用之前必须转换到视图坐标系空间.
NSMutableArray *transformedFaces = [NSMutableArray array]
for (AVMetadataObject *face in faces) {
AVMetadataObject *transformedFace =
[self.layer transformedMetadataObjectForMetadataObject:face];
[transformedFaces addObject:transformedFace];
}
// 这个数组用于确定哪些人脸移出了视图并将其对应图层移除用户界面.
NSMutableArray *lostFaces = [self.faceLayers.allKeys mutableCopy];
for (AVMetadataFaceObject *face in transformedFaces) {
NSNumber *faceID = @(face.faceID); // 这是检测到人脸的唯一标识
[lostFaces removeObject:faceID];
// 如果对于给定得faceID没有对应图层,则创建新图层.
CALayer *layer = [self.faceLayers objectForKey:faceID];
if (!layer) { // 添加对应的蓝色边框。
layer = [CALayer layer]
layer.borderWidrh = 5.0f;
layer.borderColor = RGBA(0.188,0.517,0.877,1).CGColor;
[self.overlayLayer addSublayer:layer];
self.faceLayers[faceID] = layer;
}
layer.transform = CATransform3DIdentity;
layer.frame = face.bounds;
for (NSNumber *faceID in lostFaces) {
CALayer *layer = [self.faceLayers objectForKey:faceID];
[layer removeFromSuperlayer];
[self.faceLayers removeObjectForKey:faceID];
}
}
3.机器码识别
包括一维条形码和二维码. 下面来创建一个扫描器.其实基本流程都是一样的,首先配置Session的Input和Output.
// 设置近距离缩放限制来提高扫描机器码成功率.
- (BOOL)setupSessionInputs:(NSError *__autoreleasing *)error {
BOOL success = [super setupSessionInputs:error];
if (success) {
if (self.activeCamera.autoFocusRangeRestrictionSupported) {
if ([self.activeCamera lockForConfiguration:error]) {
self.activeCamera.autoFocusRangeRestriction =
AVCaptureAutoFocusRangeRestrictionNear;
[self.activeCamera unlockForConfiguration];
}
}
}
return success;
}
// 设置捕捉元数据类型为QRCode,AztecCode,UPCECode;
- (BOOL)setupSessionOutputs:(NSError **)error {
self.metadataOutput = [[AVCaptureMetadataOutput alloc] init];
if ([self.captureSession canAddOutput:self.metadataOutput]) {
[self.captureSession addOutput:self.metadataOutput];
dispatch_queue_t mainQueue = dispatch_get_main_queue();
[self.metadataOutput setMetadataObjectsDelegate:self
queue:mainQueue];
NSArray *types = @[AVMetadataObjectTypeQRCode,
AVMetadataObjectTypeAztecCode,
AVMetadataObjectTypeUPCECode];
self.metadataOutput.metadataObjectTypes = types;
} else {
NSDictionary *userInfo = @{NSLocalizedDescriptionKey:
@"Failed to still image output."};
*error = [NSError errorWithDomain:THCameraErrorDomain
code:THCameraErrorFailedToAddOutput
userInfo:userInfo];
return NO;
}
return YES;
}
// 最后在回调中处理捕捉到的元数据.
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputMetadataObjects:(NSArray *)metadataObjects
fromConnection:(AVCaptureConnection *)connection {
// 交给自定义view来可视化.
[self.codeDetectionDelegate didDetectCodes:metadataObjects];
}
THPreviewView.m 中
- (void)didDetectCodes:(NSArray *)codes {
NSArray *transformedCodes = [self transformedCodesFromCodes:codes];
NSMutableArray *lostCodes = [self.codeLayers.allKeys mutableCopy];
for (AVMetadataMachineReadableCodeObject *code in transformedCodes) {
// 处理机器码的有效字符串.使用continue跳过无效值.
NSString *stringValue = code.stringValue;
if (stringValue) {
[lostCodes removeObject:stringValue];
} else {
continue;
}
NSArray *layers = self.codeLayers[stringValue];
if (!layers) {
layers = @[[self makeBoundsLayer], [self makeCornersLayer]];
self.codeLayers[stringValue] = layers;
[self.previewLayer addSublayer:layers[0]];
[self.previewLayer addSublayer:layers[1]];
}
// 基于元数据对象的属性构建一个对应CGPath来绘制它的几何图形.
CAShapeLayer *boundsLayer = layers[0];
boundsLayer.path = [self bezierPathForBounds:code.bounds].CGPath;
boundsLayer.hidden = NO;
CAShapeLayer *cornersLayer = layers[1];
cornersLayer.path = [self bezierPathForCorners:code.corners].CGPath;
cornersLayer.hidden = NO;
NSLog(@"String: %@", stringValue);
}
for (NSString *stringValue in lostCodes) {
for (CALayer *layer in self.codeLayers[stringValue]) {
[layer removeFromSuperlayer];
}
[self.codeLayers removeObjectForKey:stringValue];
}
}
// 将空间坐标转换视图坐标
- (NSArray *)transformedCodesFromCodes:(NSArray *)codes {
NSMutableArray *transformedCodes = [NSMutableArray array];
for (AVMetadataObject *code in codes) {
AVMetadataObject *transformedCode =
[self.previewLayer transformedMetadataObjectForMetadataObject:code];
[transformedCodes addObject:transformedCode];
}
return transformedCodes;
}
- (UIBezierPath *)bezierPathForBounds:(CGRect)bounds {
return [UIBezierPath bezierPathWithRect:bounds];
}
- (UIBezierPath *)bezierPathForCorners:(NSArray *)corners {
UIBezierPath *path = [UIBezierPath bezierPath];
for (int i = 0; i < corners.count; i++) {
CGPoint point = [self pointForCorner:corners[i]];
if (i == 0) {
[path moveToPoint:point];
} else {
[path addLineToPoint:point];
}
}
[path closePath];
return path;
}
- (CGPoint)pointForCorner:(NSDictionary *)corner {
NSLog(@"%@", corner);
CGPoint point;
CGPointMakeWithDictionaryRepresentation((CFDictionaryRef)corner, &point);
return point;
}
- (CAShapeLayer *)makeBoundsLayer {
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.strokeColor =
[UIColor colorWithRed:0.95f green:0.75f blue:0.06f alpha:1.0f].CGColor;
shapeLayer.fillColor = nil;
shapeLayer.lineWidth = 4.0f;
return shapeLayer;
}
- (CAShapeLayer *)makeCornersLayer {
CAShapeLayer *cornersLayer = [CAShapeLayer layer];
cornersLayer.lineWidth = 2.0f;
cornersLayer.strokeColor =
[UIColor colorWithRed:0.172 green:0.671 blue:0.428 alpha:1.000].CGColor;
cornersLayer.fillColor =
[UIColor colorWithRed:0.190 green:0.753 blue:0.489 alpha:0.500].CGColor;
return cornersLayer;
}
4. 高帧率捕捉
以高帧率(FPS)捕捉可以使视频更逼真,流畅度更高,适合快速移动物体的拍摄. 此外也可以用于制作慢动作视频效果.
- 捕捉: AVFoundation框架支持60FPS帧率来捕捉720P视频.
- 播放: AVPlayer支持多种播放帧率来播放资源内容.
- 导出: 提供了保存原始帧率的功能, 所以高FPS内容可以被导出或进行帧率转换,从而保证所以内容都可以以标准的30FPS进行输出.
要进行高帧率捕捉, 首先要确定设备是否支持 ,其次就是开启高FPS捕捉. 其他部分跟视频捕捉一样.
创建一个AVCaptureDevice分类
来添加这两个功能,
- (BOOL)supportsHighFrameRateCapture {
if (![self hasMediaType:AVMediaTypeVideo]) { // 是否支持video
return NO;
}
AVCaptureDeviceFormat *maxFormat = nil;
AVFrameRateRange *maxFrameRateRange = nil;
for (AVCaptureDeviceFormat *format in self.formats) {
// 遍历所有设备支持的formats.获取相应的codecType . 筛选出视频格式.
FourCharCode codecType = CMVideoFormatDescriptionGetCodecType(format.formatDescription);
if (codecType == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) {
NSArray *frameRateRanges = format.videoSupportedFrameRateRanges;
// 遍历视频格式找到这个摄像头所提供的最高format和帧率.
for (AVFrameRateRange *range in frameRateRanges) {
if (range.maxFrameRate > maxFrameRateRange.maxFrameRate) {
maxFormat = format;
maxFrameRateRange = range;
}
}
}
}
return maxFrameRateRange.maxFrameRate > 30.0f;
}
- (BOOL)enableMaxFrameRateCapture:(NSError **)error {
if (![self supportsHighFrameRateCapture]) {
if (error) {
NSString *message = @"Device does not support high FPS capture";
NSDictionary *userInfo = @{NSLocalizedDescriptionKey : message};
NSUInteger code = THCameraErrorHighFrameRateCaptureNotSupported;
*error = [NSError errorWithDomain:THCameraErrorDomain
code:code
userInfo:userInfo];
}
return NO;
}
if ([self lockForConfiguration:error]) { // 修改设备前,为配置操作锁住设备.
CMTime minFrameDuration = maxFrameRateRange.minFrameDuration;
// 设置最大帧率.
self.activeFormat = maxFormat;
self.activeVideoMinFrameDuration = minFrameDuration;
self.activeVideoMaxFrameDuration = minFrameDuration;
[self unlockForConfiguration];
return YES;
}
return NO;
}
3. 视频处理与SampleBuffer
上一节介绍了简单的视频捕捉.它使用的是AVCaptureMovieFileOutput
,不支持我们同视频数据的交互;如果有更多需求,我们就要用最底层的SessionOutput:AVCaptureAudioDataOutput
和AVCaptureVideoDataOutput
来控制视频数据达到我们想要的效果.
AVCaptureVideoDataOutput 通过AVCaptureVideoDataOutputSampleBufferDelegate
回调视频数据.这个代理有包含两个方法didOutputSampleBuffer
和didDropSampleBuffer
;前者是每当获取一个新的视频帧就会调用,数据在其中进行解码或重新编码;而后者是每当一个迟到的帧被丢弃时调用 (通常是由于在didOutputSampleBuffer的调用中消耗了过多的处理时间,系统优化所致. 所以开发者要尽量提高处理效率,否则将收不到缓存数据.)
这两个方法都要处理Sample Buffer.它以CMSampleBuffer
对象的形式存在. 这个类型非常重要.
3.1 CMSampleBuffer
CMSampleBuffer是一个由Core Media框架提供的Core Foundation风格对象. 用于在媒体管道中传输数字样本. CMSampleBuffer的角色是将基础的样本数据进行封装并提供格式和时间信息,以及所有在转换和处理数据时要用到的元数据.
-
样本数据 : 使用
AVCaptureVideoDataOutput
时,sample buffer 会包含一个CVPixeBuffer
,它是一个带有单个视频帧原始数据的Core Video中的对象.它在内存中保存像素数据,给我们提供了操作内容的机会.例如给捕捉到的图片应用灰度效果. -
格式信息: 除了原始媒体样本外,CMSampleBuffer还提供了以
CMFormatDescription
对象的形式存在的样本格式信息. 它定义了大量函数用于访问媒体样本的更多细节. 例如: 识别音频和视频数据.
CMFormatDescriptionRef formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer);
CMMediaType *mediaType = CMFormatDescriptionGetMediaType(formatDescription);
if (mediaType == kCMMediaType_Video) {
} else if (mediaType == kCMMediaType_Audio) {
}
- 时间信息: CMSampleBuffer还定义了关于媒体样本的时间信息,可以获取到原始的表示时间戳和解码时间戳.
- 附加的元数据 : Core Media在CMAttachment.h中定义了元数据协议,可以读取和写入底层元数据.比如可交换图片文件格式的标签.
视频处理示例:捕捉视频并将视频帧映射为一个OpenGL 贴图.
在开始之前简单了解了 OpenGL ES, 它是给高性能AVFoundation视频应用提供控制和功能的唯一的选择. Core Video提供了一个CVOpenGLESTextureCache
类型来作为像素buffer和OpenGL 贴图之间的桥梁.
// 设置AVCaptureVideoDataOutput类型的会话输出.
- (BOOL)setupSessionOutputs:(NSError **)error {
self.videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
self.videoDataOutput.alwaysDiscardsLateVideoFrames = YES;
// 配置Outout支持BGRA格式 (方便使用OpenGL)
self.videoDataOutput.videoSettings =
@{(id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA)};
[self.videoDataOutput setSampleBufferDelegate:self
queue:dispatch_get_main_queue()];
if ([self.captureSession canAddOutput:self.videoDataOutput]) {
[self.captureSession addOutput:self.videoDataOutput];
return YES;
}
return NO;
}
// 在数据回调中创建OpenGL贴图
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection {
// 从捕捉到的sampleBuffer中获取基础CVImageBufferRef.
CVReturn err;
CVImageBufferRef pixelBuffer =
CMSampleBufferGetImageBuffer(sampleBuffer);
// 获取sampleBuffer格式信息中的视频帧维度.
CMFormatDescriptionRef formatDescription =
CMSampleBufferGetFormatDescription(sampleBuffer);
CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(formatDescription);
// 从CVPixelBuffer创建一个OpenGL ES贴图. (_textureCache 是 由OpenGL的渲染context 生成的 CVOpenGLESTextureCacheRef类型贴图缓存.要用它来显示pixelBuffer)
err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
_textureCache,
pixelBuffer,
NULL,
GL_TEXTURE_2D,
GL_RGBA,
dimensions.width,
dimensions.height,
GL_BGRA,
GL_UNSIGNED_BYTE,
0,
&_cameraTexture);
// 刷新贴图缓存.
if (_cameraTexture) {
CFRelease(_cameraTexture);
_cameraTexture = NULL;
}
CVOpenGLESTextureCacheFlush(_textureCache, 0);
}
AVCaptureVideoDataOutput定义了视频帧被捕捉时访问该帧的接口,它可以提供对呈现的数据和处理中数据全方面的控制.
网友评论