AVFoundation框架(五) 媒体捕捉下 -进阶篇

上一篇介绍了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:AVCaptureAudioDataOutputAVCaptureVideoDataOutput来控制视频数据达到我们想要的效果.
AVCaptureVideoDataOutput 通过AVCaptureVideoDataOutputSampleBufferDelegate回调视频数据.这个代理有包含两个方法didOutputSampleBufferdidDropSampleBuffer;前者是每当获取一个新的视频帧就会调用,数据在其中进行解码或重新编码;而后者是每当一个迟到的帧被丢弃时调用 (通常是由于在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定义了视频帧被捕捉时访问该帧的接口,它可以提供对呈现的数据和处理中数据全方面的控制.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容