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

作者: brownfeng | 来源:发表于2017-07-25 16:57 被阅读510次

    如何计算当前的帧率

    在苹果的官方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, &currentBuffer);
        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

        本文链接:https://www.haomeiwen.com/subject/ahzwkxtx.html