如何计算当前的帧率
在苹果的官方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中绘制当前进度等等.