iOS中实时视频帧处理的小tips

如何计算当前的帧率

在苹果的官方demo中, 有一个很好的计算方法


...
    NSMutableArray *_previousSecondTimestamps = [[NSMutableArray alloc] init];
...

- (void)calculateFramerateAtTimestamp:(CMTime)timestamp
{
    [_previousSecondTimestamps addObject:[NSValue valueWithCMTime:timestamp]];
    
    CMTime oneSecond = CMTimeMake( 1, 1 );
    CMTime oneSecondAgo = CMTimeSubtract( timestamp, oneSecond );
    
    while( CMTIME_COMPARE_INLINE( [_previousSecondTimestamps[0] CMTimeValue], <, oneSecondAgo ) ) {
        [_previousSecondTimestamps removeObjectAtIndex:0];
    }
    
    if ( [_previousSecondTimestamps count] > 1 ) {
        const Float64 duration = CMTimeGetSeconds( CMTimeSubtract( [[_previousSecondTimestamps lastObject] CMTimeValue], [_previousSecondTimestamps[0] CMTimeValue] ) );
        const float newRate = (float)( [_previousSecondTimestamps count] - 1 ) / duration;
        self.previewFrameRate = newRate;
        NSLog(@"FrameRate - %f", newRate);
    }
}


//在调用时候:

    CMTime timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
    [self calculateFramerateAtTimestamp:timestamp];

实时处理视频帧过程中如何丢帧

iOS的AVCaptureSession的delegate回调方法- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection会把视频帧和音频帧通过该回调方法返回.

我们将视频帧CMSampleBufferRef抛到自建线程中进行实时处理, 该线程每次只处理一帧数据, 处理过程中, 其他的视频帧回调过来就丢弃, 具体的可以用以下两种方法:

方法一, 使用static变量控制, 该处理线程,每次最多只能处理1帧数据.如果当前正在处理数据, 则直接返回

    static NSInteger nProcessFrame = 0;
    if (nProcessFrame > 0) {
        return;    //last frame not finished
    }
    nProcessFrame ++;
    __block CMSampleBufferRef currentBuffer;
    CMSampleBufferCreateCopy(kCFAllocatorDefault, sampleBuffer, ¤tBuffer);
    CMTime timestamp = CMSampleBufferGetPresentationTimeStamp( currentBuffer );
    [self calculateFramerateAtTimestamp:timestamp];
    dispatch_async(_bufferQueue, ^{
        [self.delegate captureService:self processSampleBuffer:currentBuffer];
        CFRelease(currentBuffer);
        nProcessFrame1--;
        return;
    });

方法二, apple 的官方demo RosyWriter中的方法:

/**
 // call under @synchronized( self ) 

 注意这里使用的丢弃帧的方法.
 
 在videoDataOutput 的 delegate queue中获取 缓存一个previewPixelBuffer, 然后在外部输出的_delegateCallbackQueue中获取之前准备好的pixelBuffer. 这样可以丢弃部分内容

 主要由于 videoDataOutput queue 的速度 和delegatecallback queue 的速度决定的
 */
- (void)outputPreviewPixelBuffer:(CVPixelBufferRef)previewPixelBuffer {
    // Keep preview latency low by dropping stale frames that have not been picked up by the delegate yet
    // Note that access to currentPreviewPixelBuffer is protected by the @synchronized lock

    // 通过丢弃那些还没有被delegate处理的帧, 保证预览的低延迟, 这里缓存一个pixelBuffer, 在delegate里面完成获取要显示的pixelBuffer

    // 为什么这里注释 - 可以通过丢帧来保证界面显示的低预览延迟.因为这里虽然使用self.currentPreviewPixelBuffer 获取缓存了一个最近使用的pixelBuffer, 但是如果操作过快. 在进入delegate queue 以后,有可能这个方法已经调用了好几次. 在delegate queue里面拿到的pixelbuffer 一定是最新的那一个, 之前传递进来的数据帧就被丢弃了!!!!!!!
    self.currentPreviewPixelBuffer = previewPixelBuffer;


    // 低速通道
    [self invokeDelegateCallbackAsync:^{
        CVPixelBufferRef currentPreviewPixelBuffer = NULL;
        // 在delegate queue里面处理时候,对currentPreviewPixelBuffer上锁. 在delegate queue中使用一个局部变量获取之前缓存的那个previewPixelBuffer
        // 这里给 self 加锁,为了 获取currentPreviewPixelBuffer
        @synchronized(self) {
            // 如果当前有最优的current previewPixelBuffer, 用一个local 变量获取它,并把local变量抛出去
            currentPreviewPixelBuffer = self.currentPreviewPixelBuffer;
            if ( currentPreviewPixelBuffer ) {
                CFRetain( currentPreviewPixelBuffer );
                self.currentPreviewPixelBuffer = NULL;
            }
        }
        
        if (currentPreviewPixelBuffer) {// 如果当前存在最近一个的pixel buffer, 那么抛出去准备完成界面绘制
            // 通过delegate方法将准备好的pixelBuffer 通过代理抛出去
            [self.delegate captureService:self processSampleBuffer:currentBuffer];
            CFRelease(currentPreviewPixelBuffer);
        }
    }];
}

CMSampleBufferRef转化UIImage的性能问题

前一段时间在开发刷脸的过程中, 由于视频帧(CMSampleBufferRef -> UIImage)性能瓶颈导致, 进行实时的人脸检测处理帧率太慢, 在性能较弱的机器比如iPhone4s, iPhone5中, 检测帧率太低(不到10fps),导致(动作活体, 反光活体)检测效果不好.

在检查了各个函数的运行时间以后,确定了问题出现的原因: 视频帧 -> UIImage 方法耗时严重, 因此导致帧率下降. 在优图原来使用的将视频帧 -> 图片的方法是用的CoreImage的函数, 具体代码如下:

-(UIImage *) imageFromImageBuffer:(CVImageBufferRef)imageBuffer{
    CIImage *ciImage = [CIImage imageWithCVPixelBuffer:imageBuffer];

    CGImageRef videoImage = [get_temp_context()
                             createCGImage:ciImage
                             fromRect:CGRectMake(0, 0,
                                                 CVPixelBufferGetWidth(imageBuffer),
                                                 CVPixelBufferGetHeight(imageBuffer))];
    
    UIImage *image = [[UIImage alloc] initWithCGImage:videoImage scale:1.0f orientation:UIImageOrientationUpMirrored];
    CGImageRelease(videoImage);
    return image;
}

在使用该方法每帧数据耗时达100+ms, 因此单位时间内处理例如转化后的UIImage图像时, 检测处理帧率会大幅减少, 测试结果显示,iPhone5上动作检测只有10fps.

camera360的一个同行给出建议,切换成CoreGraphic相关函数能够大幅降低处理时间, 具体代码如下:

- (UIImage *) imageFromSampleBuffer:(CMSampleBufferRef) sampleBuffer {
    // 为媒体数据设置一个CMSampleBuffer的Core Video图像缓存对象
    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    // 锁定pixel buffer的基地址
    CVPixelBufferLockBaseAddress(imageBuffer, 0);

    // 得到pixel buffer的基地址
    void *baseAddress = CVPixelBufferGetBaseAddress(imageBuffer);

    // 得到pixel buffer的行字节数
    size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
    // 得到pixel buffer的宽和高
    size_t width = CVPixelBufferGetWidth(imageBuffer);
    size_t height = CVPixelBufferGetHeight(imageBuffer);

    // 创建一个依赖于设备的RGB颜色空间
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();

    // 用抽样缓存的数据创建一个位图格式的图形上下文(graphics context)对象
    CGContextRef context = CGBitmapContextCreate(baseAddress, width, height, 8,
                                                 bytesPerRow, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
    // 根据这个位图context中的像素数据创建一个Quartz image对象
    CGImageRef quartzImage = CGBitmapContextCreateImage(context);
    // 解锁pixel buffer
    CVPixelBufferUnlockBaseAddress(imageBuffer,0);

    // 释放context和颜色空间
    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);

    // 用Quartz image创建一个UIImage对象image
    UIImage *image = [UIImage imageWithCGImage:quartzImage];

    // 释放Quartz image对象
    CGImageRelease(quartzImage);

    return (image);
}

使用新方法以后, 每帧由视频帧->图片的处理时间降低一个数量级, 在10ms左右, 大大提升了单位时间检测效率.

CoreGraphic的应用很广泛, 基本是iOS中图像绘制最棒的框架, 通常我们调整UIImage,例如改变大小宽度,旋转重绘等等都会使用它.同时我们自定义控件, 例如创建一个圆形进度条 - progressView,时候我们往往在drawRect中绘制当前进度等等.

你可能感兴趣的:(iOS中实时视频帧处理的小tips)