You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ffmpeg_develop_doc/iOS资料/iOS音视频 -- AVFoundation捕捉.md

15 KiB

iOS音视频 -- AVFoundation捕捉

1、视频捕捉

1.1、实现QuickTime视频的录制

在上文中,简述了通过AVCapturePhotoOutput、AVCapturePhotoSettings来实现代理,获取当前摄像头所捕捉到的photo数据,生成一张图片。

视频录制过程大致也是如此,通过AVCaptureMovieFileOutput来获取视频数据,大致流程如下:

\1. 开启录制之前需要判断当前是否处于录制状态,只有在非录制状态才能进入录制状态

/// 是否在录制状态
- (BOOL)isRecording {
 return self.movieOutput.isRecording;
}

\2. 通过AVCaptureConnection来获取当前视频捕捉的连接信息

\1. 调整视频方向

\2. 判断是否支持视频稳定功能(非必要)

\3. 判读是否支持平滑对焦(非必要)

\4. 为视频输出配置输出路径

\5. 开始视频recording

/// 开始录制
- (void)startRecording {
 
 if (![self isRecording]) {
        //获取当前视频捕捉连接信息
        AVCaptureConnection *videoConnection = [self.movieOutput connectionWithMediaType:AVMediaTypeVideo];
 
        //调整方向
 if ([videoConnection isVideoOrientationSupported]) {
            videoConnection.videoOrientation = [self currentVideoOrientation];
        }
 
        //判断是否支持视频稳定功能(保证视频质量)
 if ([videoConnection isVideoStabilizationSupported]) {
            videoConnection.preferredVideoStabilizationMode = YES;
        }
 
        //拿到活跃的摄像头
        AVCaptureDevice *device = [self activeCamera];
        //判断是否支持平滑对焦(当用户移动设备时, 能自动且快速的对焦)
 if (device.isSmoothAutoFocusEnabled) {
            NSError *error;
 if ([device lockForConfiguration:&error]) {
                device.smoothAutoFocusEnabled = YES;
                [device unlockForConfiguration];
            } else {
                //失败回调
 
            }
        }
 
        //获取路径
        self.outputURL = [self uniqueURL];
 
        //摄像头的相关配置完成, 也获取到路径, 开始录制(这里录制QuckTime视频文件, 保存到相册)
        [self.movieOutput startRecordingToOutputFileURL:self.outputURL recordingDelegate:self];
 
    }
}

\3. 停止视频recording

/// 停止录制
- (void)stopRecording {
 if ([self isRecording]) {
        [self.movieOutput stopRecording];
    }
}
///路径转换
- (NSURL *)uniqueURL {
    NSURL *url = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@%@", NSTemporaryDirectory(), @"output.mov"]];
 return url;
}
///获取方向值
- (AVCaptureVideoOrientation)currentVideoOrientation {
    AVCaptureVideoOrientation result;
 
    UIDeviceOrientation deviceOrientation = [UIDevice currentDevice].orientation;
    switch (deviceOrientation) {
 case UIDeviceOrientationPortrait:
 case UIDeviceOrientationFaceUp:
 case UIDeviceOrientationFaceDown:
            result = AVCaptureVideoOrientationPortrait;
 break;
 case UIDeviceOrientationPortraitUpsideDown:
            //如果这里设置成AVCaptureVideoOrientationPortraitUpsideDown,则视频方向和拍摄时的方向是相反的。
            result = AVCaptureVideoOrientationPortrait;
 break;
 case UIDeviceOrientationLandscapeLeft:
            result = AVCaptureVideoOrientationLandscapeRight;
 break;
 case UIDeviceOrientationLandscapeRight:
            result = AVCaptureVideoOrientationLandscapeLeft;
 break;
        default:
            result = AVCaptureVideoOrientationPortrait;
 break;
    }
 return result;
}

\4. 保存影片至相册

///通过代理来获取视频数据#pragma mark - AVCaptureFileOutputRecordingDelegate
- (void)captureOutput:(AVCaptureFileOutput *)captureOutput
didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL
      fromConnections:(NSArray *)connections
                error:(NSError *)error {
 if (error) {
        //错误回调
 
    } else {
        //视频写入到相册
        [self writeVideoToAssetsLibrary:[self.outputURL copy]];
    }
    self.outputURL = nil;
}
//写入捕捉到的视频
- (void)writeVideoToAssetsLibrary:(NSURL *)videoURL {
 
    __block PHObjectPlaceholder *assetPlaceholder = nil;
    [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
        //保存进相册
        PHAssetChangeRequest *changeRequest = [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:videoURL];
        assetPlaceholder = changeRequest.placeholderForCreatedAsset;
 
    } completionHandler:^(BOOL success, NSError * _Nullable error) {
        NSLog(@"OK");
        //保存成功
        dispatch_async(dispatch_get_main_queue(), ^{
 
            //通知外部一个略缩图
            [self generateThumbnailForVideoAtURL:videoURL];
 
        });
 
    }];
}

\5. 生成一个略缩图通知外部

///通过视频获取视频的第一帧图片当做略缩图
- (void)generateThumbnailForVideoAtURL:(NSURL *)videoURL {
 
    dispatch_async(self.videoQueue, ^{
 
        //拿到视频信息
        AVAsset *asset = [AVAsset assetWithURL:videoURL];
        AVAssetImageGenerator *imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset];
        imageGenerator.maximumSize = CGSizeMake(100, 0);
        imageGenerator.appliesPreferredTrackTransform = YES;
 
        //通过视频将第一帧图片数据转化为CGImage
        CGImageRef imageRef = [imageGenerator copyCGImageAtTime:kCMTimeZero actualTime:NULL error:nil];
        UIImage *image = [UIImage imageWithCGImage:imageRef];
 
        //通知外部
        NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
        [nc postNotificationName:ThumbnailCreatedNotification object:image];
 
    });
 
 
}

1.2、关于QuickTime

上文主要是通过AVCaptureMovieFileOutput将QuickTime影片捕捉到磁盘,这个类大多数核心功能继承与超类AVCaptureFileOutput。它有很多实用的功能,例如:录制到最长时限或录制到特定文件大小为止。

通常当QuickTime影片准备发布时,影片头的元数据处于文件的开始位置。这样可以让视频播放器快速读取头包含信息,来确定文件的内容、结构和其包含的多个样本的位置。当录制一个QuickTime影片时,直到所有的样片都完成捕捉后才能创建信息头。当录制结束时,创建头数据并将它附在文件结尾。

img

将创建头的过程放在所有影片样本完成捕捉之后存在一个问题。 在移动设备中,比如录制的时候接到电话或者程序崩溃等问题,影片头就不能被正确写入。会在磁盘生成一个不可读的影片文件。AVCaptureMovieFileOutput提供一个核心功能就是分段捕捉QuickTime影片。

img

2、AVFoundation的人脸识别

人脸识别实际上是非常复杂的一个功能,要想自己完全实现人脸识别是非常困难的。苹果为我们做了很多人脸识别的功能,例如CoreImage、AVFoundation,都是有人脸识别的功能的。还有Vision face++ 等。这里就简单介绍一下AVFoundation中的人脸识别。

在拍摄视频中,我们通过AVFoundation的人脸识别,在屏幕界面上用一个红色矩形来标识识别到的人脸。

2.1、人脸识别流程

\1. 使用AVCaptureMetadataOutput来建立输出

\1. 添加进session

\2. 设置获取数据类型

\3. 在主线程中执行任务

- (BOOL)setupSessionOutputs:(NSError **)error {
    //配置输入信息
    self.metadataOutput = [[AVCaptureMetadataOutput alloc] init];
    //对session添加输出
 if ([self.captureSession canAddOutput:self.metadataOutput]) {
        [self.captureSession addOutput:self.metadataOutput];
        //从输出数据中设置只获取人脸数据(可以是人脸、二维码、一维码....)
        NSArray *metadataObjectType = @[AVMetadataObjectTypeFace];
        self.metadataOutput.metadataObjectTypes = metadataObjectType;
 
        //因为人脸检测使用了硬件加速器GPU, 所以它的任务需要在主线程中执行
        dispatch_queue_t mainQueue = dispatch_get_main_queue();
        //设置metadataOutput代理方法, 检测视频中一帧一帧数据里是否包含人脸数据. 如果包含则调用回调方法
        [self.metadataOutput setMetadataObjectsDelegate:self queue:mainQueue];
 return YES;
    } else {
        //错误回调
    }
 return NO;
}

\2. 实现相关代理方法,将捕捉到的人脸数据传递给layer层

- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection {
 
    //metadataObjects包含了捕获到的人脸数据(人脸数据会重复, 会一直捕获人脸数据)
 for (AVMetadataFaceObject *face in metadataObjects) {
        NSLog(@"Face ID:%li",(long)face.faceID);
    }
    //将人脸数据通过代理发送给外部的layer层
    [self.faceDetectionDelegate didDetectFaces:metadataObjects];
}

\3. 配置相关显示的图层。在layer层的视图中配置图层,我们在人脸四周添加一个矩形是在这个AVCaptureVideoPreviewLayer上进行一个个添加矩形。(因为人脸在识别过程中会出现旋转抖动等,需要进行一些3D转换等操作,后续也会出现此类操作,不在此篇作过多讲解)

- (void)setupView {
 
    //用来记录人脸图层
    self.faceLayers = [NSMutableDictionary dictionary];
    //图层的填充方式: 设置videoGravity 使用AVLayerVideoGravityResizeAspectFill 铺满整个预览层的边界范围
    self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
    //在previewLayer上添加一个透明的图层
    self.overlayLayer = [CALayer layer];
    self.overlayLayer.frame = self.bounds;
    //假设你的图层上的图形会发生3D变换, 设置投影方式
    self.overlayLayer.sublayerTransform = CATransform3DMakePerspective(1000);
    [self.previewLayer addSublayer:self.overlayLayer];
}
static CATransform3D CATransform3DMakePerspective(CGFloat eyePosition) {
    //CATransform3D 图层的旋转,缩放,偏移,歪斜和应用的透
    //CATransform3DIdentity是单位矩阵,该矩阵没有缩放,旋转,歪斜,透视。该矩阵应用到图层上,就是设置默认值。
    CATransform3D  transform = CATransform3DIdentity;
    //透视效果(就是近大远小),是通过设置m34 m34 = -1.0/D 默认是0.D越小透视效果越明显
    //D:eyePosition 观察者到投射面的距离
    transform.m34 = -1.0/eyePosition;
 
 return transform;
}

\4. 处理通过代理传递过来的人脸数据

\1. 将人脸在摄像头中的坐标转化为屏幕坐标

\2. 定义一个数组,保存所有的人脸数据,用于存放待从屏幕上删除的人脸数据

\3. 遍历人脸数据

\1. 通过对比屏幕上的layer(框框)数量来与传递过来的人脸进行对比,判断是否需要移除layer(框框)

\2. 根据人脸数据的ID来从屏幕上的layer(框框)中查找是否已经存在,不存在则需要生成一个layer(框框),并更新屏幕的layer(框框)数组。

\3. 根据传递过来的人脸数据来设置layer(框框)的位置,注意:当最后一个人脸离开屏幕,此时代理方法不会调用,会导致最后一个layer(框框)仍停留在屏幕上,所以需要处理一下人脸将要离开屏幕就对其进行移除处理。

\4. 在捕捉过程中,人脸会左右前后摆动(即z、y轴变化),来做不同的处理

\4. 遍历一下待删除数组,将之与传递过来的人脸数据进行对比,删除多余的人脸数据

注意:此处省略了一些3D转换的方法

- (void)didDetectFaces:(NSArray *)faces {
    //人脸数据位置信息(摄像头坐标系)转换为屏幕坐标系
    NSArray *transfromedFaces = [self transformedFacesFromFaces:faces];
 
    //人脸消失, 删除图层
 
    //需要删除的人脸数据列表
    NSMutableArray *lostFaces = [self.faceLayers.allValues mutableCopy];
 
    //遍历每个人脸数据
 for (AVMetadataFaceObject *face in transfromedFaces) {
 
        //face ID
        NSNumber *faceID = @(face.faceID);
        //face ID存在即不需要删除(从删除列表中移除)
        [lostFaces removeObject:faceID];
 
        //假如有新的人脸加入
        CALayer *layer = self.faceLayers[faceID];
 if (!layer) {
            NSLog(@"新增人脸");
            layer = [self makeFaceLayer];
            [self.overlayLayer addSublayer:layer];
 
            //更新字典
            self.faceLayers[faceID] = layer;
        }
 
        //根据人脸的bounds设置layer的frame
        layer.frame = face.bounds;
        CGSize size = self.bounds.size;
        //当人脸特别靠近屏幕边缘, 直接当作无法识别此人脸(因为人脸离开屏幕不会走此代理方法, 需要提前做移除)
 if (face.bounds.origin.x < 3 ||
            face.bounds.origin.x > size.width - layer.frame.size.width - 3 ||
            face.bounds.origin.y < 3 ||
            face.bounds.origin.y > size.height - layer.frame.size.height - 3 ) {
            [layer removeFromSuperlayer];
            [self.faceLayers removeObjectForKey:faceID];
        }
 
        //设置3D属性(人脸是3D的, 需要根据人脸的3D变化做不同的变化处理)
        layer.transform = CATransform3DIdentity;
        //人脸z轴变化
 if (face.hasRollAngle) {
            CATransform3D t = [self transformForRollAngle:face.rollAngle];
            //矩阵相乘
            layer.transform = CATransform3DConcat(layer.transform, t);
        }
        //人脸y轴变化
 if (face.hasYawAngle) {
            CATransform3D t = [self transformForYawAngle:face.hasYawAngle];
            //矩阵相乘
            layer.transform = CATransform3DConcat(layer.transform, t);
        }
    }
    //处理已经从镜头消失的人脸(人脸消失,图层并没有消失)
 for (NSNumber *faceID in lostFaces) {
        CALayer *layer = self.faceLayers[faceID];
        [self.faceLayers removeObjectForKey:faceID];
        [layer removeFromSuperlayer];
    }
}

2.1、其他类型数据的识别

有的同学在设置AVMetadataObjectTypeFace的可能会发现,还有会有一些其他的类型,例如AVMetadataObjectTypeQRCode等,就是从摄像头中捕获二维码数据,它的流程与人脸识别极度相似,甚至要更为简单一些,因为二维码并不像人脸一样需要做一些3D的转换等操作,所以此处不再示例捕捉二维码。

原文https://zhuanlan.zhihu.com/p/222418988