視頻圖像處理中的錯幀同步是怎么實現的?

該原創文章首發于微信公眾號:字節流動

為什么會用到錯幀同步?

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

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

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

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

錯幀同步的原理

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

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

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

錯幀同步的簡單實現

錯幀同步在實現上類似于“生產者-消費者”模式,我們借助于 C 語言信號量 #include <semaphore.h> 可以很方便的實現錯幀同步模型。

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

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

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

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

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

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

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

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

//初始化
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");
    //等待三個線程結束
    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);

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

}

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

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 {
            //等待第二個工作線程的處理結果 
            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個工作線程的處理邏輯類似,第一個工作線程收到主線程發來的信號,然后進行第一步處理,處理完成后通知第二個工作線程進行第二步處理,等到第二步處理完成后再通知主線程結束等待,取出處理結果。

void AsyncFramework::FirstStepProcess() {
    LOGCATE("AsyncFramework::FirstStepProcess start");
    int index = 0;
    while (true) {
        //等待主線程發來的信號
        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));
        //處理完成后通知第二個工作線程進行第二步處理
        sem_post(&second_thread_sem);
        index++;
    }
    LOGCATE("AsyncFramework::FirstStepProcess end");

}

void AsyncFramework::SecondStepProcess() {
    LOGCATE("AsyncFramework::SecondStepProcess start");
    int index = 0;
    while (true) {
        //等待第一個工作線程發來的信號
        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));
        //第二步處理完成后通知主線程結束等待
        sem_post(&main_sem);
        index++;
    }
    LOGCATE("AsyncFramework::SecondStepProcess end");
}

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


主線程打印的處理結果

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


main_thread_cost_time

聯系與交流

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容

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