iOS中實(shí)時(shí)視頻幀處理的小tips

如何計(jì)算當(dāng)前的幀率

在蘋果的官方demo中, 有一個(gè)很好的計(jì)算方法


...
    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);
    }
}


//在調(diào)用時(shí)候:

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

實(shí)時(shí)處理視頻幀過程中如何丟幀

iOS的AVCaptureSession的delegate回調(diào)方法- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection會(huì)把視頻幀和音頻幀通過該回調(diào)方法返回.

我們將視頻幀CMSampleBufferRef拋到自建線程中進(jìn)行實(shí)時(shí)處理, 該線程每次只處理一幀數(shù)據(jù), 處理過程中, 其他的視頻幀回調(diào)過來就丟棄, 具體的可以用以下兩種方法:

方法一, 使用static變量控制, 該處理線程,每次最多只能處理1幀數(shù)據(jù).如果當(dāng)前正在處理數(shù)據(jù), 則直接返回

    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中獲取 緩存一個(gè)previewPixelBuffer, 然后在外部輸出的_delegateCallbackQueue中獲取之前準(zhǔn)備好的pixelBuffer. 這樣可以丟棄部分內(nèi)容

 主要由于 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處理的幀, 保證預(yù)覽的低延遲, 這里緩存一個(gè)pixelBuffer, 在delegate里面完成獲取要顯示的pixelBuffer

    // 為什么這里注釋 - 可以通過丟幀來保證界面顯示的低預(yù)覽延遲.因?yàn)檫@里雖然使用self.currentPreviewPixelBuffer 獲取緩存了一個(gè)最近使用的pixelBuffer, 但是如果操作過快. 在進(jìn)入delegate queue 以后,有可能這個(gè)方法已經(jīng)調(diào)用了好幾次. 在delegate queue里面拿到的pixelbuffer 一定是最新的那一個(gè), 之前傳遞進(jìn)來的數(shù)據(jù)幀就被丟棄了!!!!!!!
    self.currentPreviewPixelBuffer = previewPixelBuffer;


    // 低速通道
    [self invokeDelegateCallbackAsync:^{
        CVPixelBufferRef currentPreviewPixelBuffer = NULL;
        // 在delegate queue里面處理時(shí)候,對(duì)currentPreviewPixelBuffer上鎖. 在delegate queue中使用一個(gè)局部變量獲取之前緩存的那個(gè)previewPixelBuffer
        // 這里給 self 加鎖,為了 獲取currentPreviewPixelBuffer
        @synchronized(self) {
            // 如果當(dāng)前有最優(yōu)的current previewPixelBuffer, 用一個(gè)local 變量獲取它,并把local變量拋出去
            currentPreviewPixelBuffer = self.currentPreviewPixelBuffer;
            if ( currentPreviewPixelBuffer ) {
                CFRetain( currentPreviewPixelBuffer );
                self.currentPreviewPixelBuffer = NULL;
            }
        }
        
        if (currentPreviewPixelBuffer) {// 如果當(dāng)前存在最近一個(gè)的pixel buffer, 那么拋出去準(zhǔn)備完成界面繪制
            // 通過delegate方法將準(zhǔn)備好的pixelBuffer 通過代理拋出去
            [self.delegate captureService:self processSampleBuffer:currentBuffer];
            CFRelease(currentPreviewPixelBuffer);
        }
    }];
}

CMSampleBufferRef轉(zhuǎn)化UIImage的性能問題

前一段時(shí)間在開發(fā)刷臉的過程中, 由于視頻幀(CMSampleBufferRef -> UIImage)性能瓶頸導(dǎo)致, 進(jìn)行實(shí)時(shí)的人臉檢測(cè)處理幀率太慢, 在性能較弱的機(jī)器比如iPhone4s, iPhone5中, 檢測(cè)幀率太低(不到10fps),導(dǎo)致(動(dòng)作活體, 反光活體)檢測(cè)效果不好.

在檢查了各個(gè)函數(shù)的運(yùn)行時(shí)間以后,確定了問題出現(xiàn)的原因: 視頻幀 -> UIImage 方法耗時(shí)嚴(yán)重, 因此導(dǎo)致幀率下降. 在優(yōu)圖原來使用的將視頻幀 -> 圖片的方法是用的CoreImage的函數(shù), 具體代碼如下:

-(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;
}

在使用該方法每幀數(shù)據(jù)耗時(shí)達(dá)100+ms, 因此單位時(shí)間內(nèi)處理例如轉(zhuǎn)化后的UIImage圖像時(shí), 檢測(cè)處理幀率會(huì)大幅減少, 測(cè)試結(jié)果顯示,iPhone5上動(dòng)作檢測(cè)只有10fps.

camera360的一個(gè)同行給出建議,切換成CoreGraphic相關(guān)函數(shù)能夠大幅降低處理時(shí)間, 具體代碼如下:

- (UIImage *) imageFromSampleBuffer:(CMSampleBufferRef) sampleBuffer {
    // 為媒體數(shù)據(jù)設(shè)置一個(gè)CMSampleBuffer的Core Video圖像緩存對(duì)象
    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    // 鎖定pixel buffer的基地址
    CVPixelBufferLockBaseAddress(imageBuffer, 0);

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

    // 得到pixel buffer的行字節(jié)數(shù)
    size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
    // 得到pixel buffer的寬和高
    size_t width = CVPixelBufferGetWidth(imageBuffer);
    size_t height = CVPixelBufferGetHeight(imageBuffer);

    // 創(chuàng)建一個(gè)依賴于設(shè)備的RGB顏色空間
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();

    // 用抽樣緩存的數(shù)據(jù)創(chuàng)建一個(gè)位圖格式的圖形上下文(graphics context)對(duì)象
    CGContextRef context = CGBitmapContextCreate(baseAddress, width, height, 8,
                                                 bytesPerRow, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
    // 根據(jù)這個(gè)位圖context中的像素?cái)?shù)據(jù)創(chuàng)建一個(gè)Quartz image對(duì)象
    CGImageRef quartzImage = CGBitmapContextCreateImage(context);
    // 解鎖pixel buffer
    CVPixelBufferUnlockBaseAddress(imageBuffer,0);

    // 釋放context和顏色空間
    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);

    // 用Quartz image創(chuàng)建一個(gè)UIImage對(duì)象image
    UIImage *image = [UIImage imageWithCGImage:quartzImage];

    // 釋放Quartz image對(duì)象
    CGImageRelease(quartzImage);

    return (image);
}

使用新方法以后, 每幀由視頻幀->圖片的處理時(shí)間降低一個(gè)數(shù)量級(jí), 在10ms左右, 大大提升了單位時(shí)間檢測(cè)效率.

CoreGraphic的應(yīng)用很廣泛, 基本是iOS中圖像繪制最棒的框架, 通常我們調(diào)整UIImage,例如改變大小寬度,旋轉(zhuǎn)重繪等等都會(huì)使用它.同時(shí)我們自定義控件, 例如創(chuàng)建一個(gè)圓形進(jìn)度條 - progressView,時(shí)候我們往往在drawRect中繪制當(dāng)前進(jìn)度等等.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市晰房,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,820評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赡译,死亡現(xiàn)場(chǎng)離奇詭異浑槽,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)庶香,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來简识,“玉大人赶掖,你說我怎么就攤上這事∑呷牛” “怎么了奢赂?”我有些...
    開封第一講書人閱讀 168,324評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)颈走。 經(jīng)常有香客問我膳灶,道長(zhǎng),這世上最難降的妖魔是什么立由? 我笑而不...
    開封第一講書人閱讀 59,714評(píng)論 1 297
  • 正文 為了忘掉前任轧钓,我火速辦了婚禮,結(jié)果婚禮上锐膜,老公的妹妹穿的比我還像新娘毕箍。我一直安慰自己,他們只是感情好道盏,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評(píng)論 6 397
  • 文/花漫 我一把揭開白布而柑。 她就那樣靜靜地躺著,像睡著了一般荷逞。 火紅的嫁衣襯著肌膚如雪媒咳。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,328評(píng)論 1 310
  • 那天种远,我揣著相機(jī)與錄音涩澡,去河邊找鬼。 笑死院促,一個(gè)胖子當(dāng)著我的面吹牛筏养,可吹牛的內(nèi)容都是我干的斧抱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,897評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼渐溶,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼辉浦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起茎辐,我...
    開封第一講書人閱讀 39,804評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤宪郊,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后拖陆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體弛槐,經(jīng)...
    沈念sama閱讀 46,345評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評(píng)論 3 340
  • 正文 我和宋清朗相戀三年依啰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了乎串。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,561評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡速警,死狀恐怖叹誉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情闷旧,我是刑警寧澤长豁,帶...
    沈念sama閱讀 36,238評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站忙灼,受9級(jí)特大地震影響匠襟,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜该园,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評(píng)論 3 334
  • 文/蒙蒙 一酸舍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧爬范,春花似錦父腕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽萧诫。三九已至斥难,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間帘饶,已是汗流浹背哑诊。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留及刻,地道東北人镀裤。 一個(gè)月前我還...
    沈念sama閱讀 48,983評(píng)論 3 376
  • 正文 我出身青樓竞阐,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親暑劝。 傳聞我的和親對(duì)象是個(gè)殘疾皇子骆莹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容