視頻圖像處理中的錯幀同步是怎么實(shí)現(xiàn)的?

該原創(chuàng)文章首發(fā)于微信公眾號:字節(jié)流動

為什么會用到錯幀同步?

一般 Android 系統(tǒng)相機(jī)的最高幀率在 30 FPS 左右,當(dāng)幀率低于 20 FPS 時,用戶可以明顯感覺到相機(jī)畫面卡頓和延遲。

我們在做相機(jī)預(yù)覽和視頻流處理時,對每幀圖像處理時間過長(超過 30 ms)就很容易造成畫面卡頓,這個場景就需要用到錯幀同步方法去提升畫面的流暢度。

錯幀同步,簡單來說就是把當(dāng)前的幾幀緩沖到子線程中處理,主線程直接返回子線程之前的處理結(jié)果,屬于典型的以空間換時間策略。

錯幀同步策略也有不足之處,它不能在子線程中緩沖太多的幀,否則造成畫面延遲。另外,每個子線程分配的任務(wù)也要均衡(即每幀在子線程中的處理時間大致相同),不然會因?yàn)?CPU 線程調(diào)度的時間消耗適得其反。

錯幀同步的原理

錯幀同步的原理如上圖所示,我們開啟三個線程:一個主線程,兩個工作線程,每一幀圖像的處理任務(wù)分為 2 步,第一個工作線程完成第一步處理,第二個工作線程完成第二步處理,每一幀都要經(jīng)過這兩步的處理。

當(dāng)主線程輸入第 n + 1 幀到第一個工作線程后,主線程會等待第二個工作線程中第 n 幀的處理結(jié)果然后返回,這種情況下你肯定會問第 0 幀怎么辦?第 0 幀就直接返回就行了。

這些步驟下來,可以看成第 n+1 幀和第 n 幀在 2 個工作線程中同時處理,若忽略 CPU 線程調(diào)度時間,2 線程錯幀可以提升一倍的性能(性能提升情況,下面會給出實(shí)測數(shù)據(jù))。

錯幀同步的簡單實(shí)現(xiàn)

錯幀同步在實(shí)現(xiàn)上類似于“生產(chǎn)者-消費(fèi)者”模式,我們借助于 C 語言信號量 #include <semaphore.h> 可以很方便的實(shí)現(xiàn)錯幀同步模型。

C 的信號量常用的幾個 API :

--------------------------------------------------------------------
int sem_init(sem_t *sem, int pshared, unsigned int value);
    功能:初始化信號量
    參數(shù):
        sem:指定要初始化的信號量
        pshared:0:應(yīng)用于多線程
                非 0:多進(jìn)程
        value:指定了信號量的初始值
    返回值:0 成功
          -1 失敗

----------------------------------------------------------------------
int sem_destroy(sem_t *sem);
    功能:銷毀信號量
    參數(shù):sem:指定要銷毀的信號量
    返回值:0 成功
          -1 錯誤 

----------------------------------------------------------------------
int sem_post(sem_t *sem);

    功能:信號量的值加 1 操作
    參數(shù):
        sem:指定的信號量,就是這個信號量加 1 
    返回值:0 成功
          -1 錯誤 

-----------------------------------------------------------------------

int sem_wait(sem_t *sem);
    功能:信號量的值減 1 , 如果信號量的值為 0 , 阻塞等待
    參數(shù):
        sem:指定的信號量, 如果信號量的值為 0, 阻塞等待, 否則信號量的值減 1
    返回值:0 成功
          -1 錯誤

在這里為了簡化代碼邏輯,我們用字符串來表示視頻幀,每個工作線程對輸入的字符串進(jìn)行標(biāo)記,表示工作線程對視頻幀做了處理,最后的輸出(第 0 幀除外)都是經(jīng)過工作線程標(biāo)記過的字符串。

//初始化
void AsyncFramework::Init() {
    LOGCATE("AsyncFramework::Init");
    memset(work_buffers, 0, sizeof(work_buffers));
    work_thread_running = true;
    main_thread_running = true;

    index = 0;

    // 初始化 3 個信號量
    sem_init(&main_sem, 0, 0);
    sem_init(&first_thread_sem, 0, 0);
    sem_init(&second_thread_sem, 0, 0);

    // WORK_THREAD_NUM = 2 ,為 2 個工作線程申請 2 塊 buffer
    for (int i = 0; i < WORK_THREAD_NUM; ++i) {
        work_buffers[i] = static_cast<char *>(malloc(WORK_BUFFER_SIZE));
    }

    // 啟動三個線程
    main_thread = new thread(MainThreadProcess);
    first_thread = new thread(FirstStepProcess);
    second_thread = new thread(SecondStepProcess);

}
// 反初始化
void AsyncFramework::UnInit() {
    LOGCATE("AsyncFramework::UnInit");
    //等待三個線程結(jié)束
    main_thread_running = false;
    main_thread->join();
    delete main_thread;
    main_thread = nullptr;

    work_thread_running = false;
    sem_post(&first_thread_sem);
    sem_post(&second_thread_sem);
    first_thread->join();
    second_thread->join();

    delete first_thread;
    first_thread = nullptr;
    delete second_thread;
    second_thread = nullptr;

    //銷毀信號量
    sem_destroy(&main_sem);
    sem_destroy(&first_thread_sem);
    sem_destroy(&second_thread_sem);

    //釋放緩沖區(qū)
    for (int i = 0; i < WORK_THREAD_NUM; ++i) {
        if (work_buffers[i]) {
            free(work_buffers[i]);
            work_buffers[i] = nullptr;
        }
    }

}

主線程的邏輯就是不斷地生成“視頻幀”,將“視頻幀”傳給第一個工作線程進(jìn)行第一步處理,然后等待第二個工作線程的處理結(jié)果。

void AsyncFramework::MainThreadProcess() {
    LOGCATE("AsyncFramework::MainThreadProcess start");
    while (main_thread_running) {
        memset(work_buffers[index % WORK_THREAD_NUM], 0, WORK_BUFFER_SIZE);
        sprintf(work_buffers[index % WORK_THREAD_NUM], "FrameIndex=%d ", index);
        //通知第一個工作線程處理
        sem_post(&first_thread_sem);
        if (index == 0) {
            //第 0 幀直接返回,不交給工作線程處理
            LOGCATE("AsyncFramework::MainThreadProcess %s", work_buffers[index % WORK_THREAD_NUM]);
            index++;
            continue;
        } else {
            //等待第二個工作線程的處理結(jié)果 
            sem_wait(&main_sem);
        }
        LOGCATE("AsyncFramework::MainThreadProcess %s", work_buffers[(index - 1) % WORK_THREAD_NUM]);
        index++;
        if (index == 100) break;//生成100幀
    }
    LOGCATE("AsyncFramework::MainThreadProcess end");

}

2個工作線程的處理邏輯類似,第一個工作線程收到主線程發(fā)來的信號,然后進(jìn)行第一步處理,處理完成后通知第二個工作線程進(jìn)行第二步處理,等到第二步處理完成后再通知主線程結(jié)束等待,取出處理結(jié)果。

void AsyncFramework::FirstStepProcess() {
    LOGCATE("AsyncFramework::FirstStepProcess start");
    int index = 0;
    while (true) {
        //等待主線程發(fā)來的信號
        sem_wait(&first_thread_sem);
        if(!work_thread_running) break;
        LOGCATE("AsyncFramework::FirstStepProcess index=%d", index);
        strcat(work_buffers[index % WORK_THREAD_NUM], "FirstStep ");
        //休眠模擬處理耗時
        this_thread::sleep_for(chrono::milliseconds(200));
        //處理完成后通知第二個工作線程進(jìn)行第二步處理
        sem_post(&second_thread_sem);
        index++;
    }
    LOGCATE("AsyncFramework::FirstStepProcess end");

}

void AsyncFramework::SecondStepProcess() {
    LOGCATE("AsyncFramework::SecondStepProcess start");
    int index = 0;
    while (true) {
        //等待第一個工作線程發(fā)來的信號
        sem_wait(&second_thread_sem);
        if(!work_thread_running) break;
        LOGCATE("AsyncFramework::SecondStepProces index=%d", index);
        strcat(work_buffers[index % WORK_THREAD_NUM], "SecondStep");
        //休眠模擬處理耗時
        this_thread::sleep_for(chrono::milliseconds(200));
        //第二步處理完成后通知主線程結(jié)束等待
        sem_post(&main_sem);
        index++;
    }
    LOGCATE("AsyncFramework::SecondStepProcess end");
}

主線程打印的處理結(jié)果(第 0 幀直接返回,沒被處理):


主線程打印的處理結(jié)果

我們設(shè)定視頻幀的 2 步處理一共耗時 400 ms (各休眠 200 ms),由于采用錯幀同步方式,主線程耗時只有 200 ms 左右,性能提升一倍。


main_thread_cost_time

聯(lián)系與交流

技術(shù)交流可以添加我的微信:Byte-Flow

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 教程一:視頻截圖(Tutorial 01: Making Screencaps) 首先我們需要了解視頻文件的一些基...
    90后的思維閱讀 4,987評論 0 3
  • feisky云計算、虛擬化與Linux技術(shù)筆記posts - 1014, comments - 298, trac...
    不排版閱讀 4,310評論 0 5
  • 彭銀華醫(yī)生的婚宴是他籌劃人生新篇章的起點(diǎn),他將與支持他工作的妻子,還有未出生的孩子迎接人生的各種幸福時刻。在他毅然...
    慕崩予我孤獨(dú)閱讀 148評論 0 0
  • 因?yàn)橹霸谧鰅OS開發(fā),對安卓不熟悉??,難免會被一些問題困擾,這里也是對網(wǎng)上資源的整合。 Flutter vers...
    FlowYourHeart閱讀 553評論 0 0
  • 喜歡說話的的在人群里聊天 喜歡安靜的沉浸在自己的世界 喜歡吃肉的吃肉 喜歡吃蔬菜的吃蔬菜 我們都快樂。
    沈清月閱讀 273評論 0 1

友情鏈接更多精彩內(nèi)容