# JM reference software 學習筆記 2025/01/03 從軍營回來後花了兩天學習一下C語言,接著重新開始查看JM這一份code。 先補充JM中視訊編碼器lencod的 **函式調用關係圖**方便理解 source:[CSDN](https://blog.csdn.net/leixiaohua1020/article/details/49822701?spm=1001.2101.3001.6650.3&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-3-49822701-blog-105369954.235%5Ev43%5Epc_blog_bottom_relevance_base6&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-3-49822701-blog-105369954.235%5Ev43%5Epc_blog_bottom_relevance_base6&utm_relevant_index=6) ![image](https://hackmd.io/_uploads/S1LQclB8Jl.png) ## 資料夾 在JM reference software中有inc以及src兩個資料夾。 1. inc功能是存放header files,定義函數界面(interfaces)也就是.h檔,包含 全域變數,巨集定義,結構體以及型別定義。 2. src功能是存放實作檔案(source files)也就是.c檔,包含具體的函數實作,負責完成演算法,資料處理和功能執行。 3. # 1. 從 main()主程式切入(lencod.c第254行) ![image](https://hackmd.io/_uploads/S1SKKlr81x.png) ```C= /*! *********************************************************************** * \brief * Main function for encoder. * \param argc * number of command line arguments * \param argv * command line arguments * \return * exit code *********************************************************************** */ ``` *補充:Doxygen標註格式,主要描述函式參數的用途,常出現在C/C++程式碼中,便於自動生成程式碼說明書。* 解釋: \param * 表示該段描述是針對函數的參數。 ## argc * 參數名稱,表示命令列參數的數量。 * 這通常出現在 main(int argc, char **argv) 函數中。 ## argv * 參數名稱,表示一個指向字元陣列的指標,每個字元陣列對應於一個命令列參數。 * 例如,在命令列輸入 ./program arg1 arg2 時,argv[0] 是 ./program,argv[1] 是 arg1,argv[2] 是 arg2。 描述文字 * 為每個參數提供說明。 * number of command line arguments 解釋了 argc 的用途。 * command line arguments 解釋了 argv 的用途。 ### 例子 Command Line如下: ```bash ./program hello 42 ``` 內存結構如下 ```sql argc = 3 argv: +--------+----------------+ | argv[0] | "./program" | | argv[1] | "hello" | | argv[2] | "42" | | argv[3] | NULL | // 結尾是 NULL +--------+----------------+ ``` argv 是 指向字串的指標陣列,所以 argv[i] 是 char*,指向每個參數的字串。 --- # int main()主程式中的主要函式 ### Configure() * 讀取argc,argv*,完成VideoParameters以及InputParameters。 ### alloc_encoder() * 分配編碼器資源 ### init_encoder() * 初始化編碼器 ### encode_sequence() * 進行序列編碼 ### free_encoder_memory() * 釋放資源 # lencod.c 詳解 ## //check the scaling factor to avoid overflow; 以下來自defines.h | define | 中文解釋 | | | ------------------ | ---------------------------------------------------------------------------------------------------- | --- | | LAMBDA_ACCURACY_BITS | | 5 | | JCOST_CALC_SCALEUP | !< 1: J = (D<<LAMBDA_ACCURACY_BITS)+Lambda*R; 0: J = D + ((Lambda*R+Rounding)>>LAMBDA_ACCURACY_BITS) | 1 | | IMGTYPE | 定義 imgpel 大小類型。0 表示字節(無法處理大於 8 位的深度),1 表示無符號短整數。 | 1 | --- ```C static const int mb_width_cr[4] = {0, 8, 8,16}; ``` * static:變數生命週期會延續到程式結束(即為靜態存儲區)。 同事,也表示此變數僅限於該文件,其他文件無法訪問。 * const: 表示該變數值**不可更改**,(唯讀)是一種防止code意外修改變數的值的保護機制。 --- ```C EncoderParams *p_Enc = NULL; ``` * NULL: 將指標初始化為NULL,表示指標目前沒有指向任何有效的記憶體位置。 * 好處: * 1. 可以防止指向位置的記憶體地址,可以減少潛在的錯誤。 * 2. 在後續使用該指標前,可以檢查它是否為NULL,以確保指標已經正確分配記憶體位置。 ```C= if (p_Enc != NULL) { // 安全使用 p_Enc } ``` # rd_intra_jm_low.c 解析 以下是對檔案 `rd_intra_jm_low.c` 的詳細解釋,這個檔案是 JM H.264 參考軟體的一部分,主要負責內部預測(intra prediction)的模式決策。這個檔案的目標是為 4x4 和 8x8 塊選擇最佳的內部預測模式,通過率失真優化(Rate-Distortion Optimization, RDO)來平衡編碼效率和視頻質量。讓我們一步步分析其流程和功能。 --- ## **檔案概述** - **檔案名稱**:`rd_intra_jm_low.c` - **主要功能**:為 H.264 編碼中的內部預測(Intra Prediction)塊(4x4 和 8x8 大小)進行模式決策,選擇最佳的預測模式。 - **作者**:Heiko Schwarz, Valeri George, Lowell Winger, Alexis Michael Tourapis - **日期**:2001 年 4 月 12 日 - **背景**:這是 H.264 編碼器的一部分,專注於低複雜度的內部預測模式選擇。 檔案中包含兩個核心函數: 1. **`mode_decision_for_I4x4_blocks_JM_Low`**:處理 4x4 塊的模式決策。 2. **`mode_decision_for_I8x8_blocks_JM_Low`**:處理 8x8 塊的模式決策。 這兩個函數的共同目標是計算並選擇一個最佳的內部預測模式,使得率失真成本(RD cost)最小化。 --- ## **主要流程解釋** ### **1. `mode_decision_for_I4x4_blocks_JM_Low` - 4x4 塊的模式決策** 這個函數負責為一個 4x4 的內部塊選擇最佳的預測模式。以下是其詳細流程: #### **步驟:** - **初始化** - 獲取當前宏塊(macroblock)和切片(slice)的相關信息,例如像素坐標 (`pic_pix_x`, `pic_pix_y`) 和塊的相對位置 (`block_x`, `block_y`)。 - 準備輸入參數,如 `lambda`(拉格朗日乘數,用於率失真計算)和當前圖像數據。 - **檢查鄰近塊可用性** - 檢查左邊和上邊的鄰近塊是否可用,這取決於它們是否位於圖像邊界內,或者是否為內部塊(受 `UseConstrainedIntraPred` 限制)。 - 使用 `get4x4Neighbour` 函數獲取鄰近塊的信息。 - **確定最可能模式(Most Probable Mode)** - 根據左邊塊 (`leftMode`) 和上邊塊 (`upMode`) 的預測模式,計算最可能模式。 - 如果任一鄰近塊不可用,則默認為 `DC_PRED`(直流預測模式);否則,取左邊和上邊模式中較小的值。 - **內部預測準備** - 調用 `set_intrapred_4x4` 函數,根據鄰近塊的可用性設置 4x4 塊的內部預測值。 - **遍歷所有預測模式** - 對 9 種可能的內部預測模式(`ipmode` 從 0 到 8)進行遍歷: - **模式可用性檢查**:根據鄰近塊的可用性,判斷當前模式是否可用(例如,`VERT_PRED` 需要上邊塊可用)。 - **生成預測塊**:調用 `get_intrapred_4x4` 生成該模式的預測數據。 - **計算 RD cost**: - 如果是「最可能模式」,使用較低的編碼成本 `onecost`(`lambda * 1`)。 - 其他模式使用固定成本 `fixedcost`(`lambda * 4`)。 - 加上失真成本(通過 `compute_cost4x4` 計算預測塊與原始塊的差異)。 - **更新最佳模式**:如果當前成本低於當前最小成本 (`min_cost`),則更新最佳模式 (`best_ipmode`) 和最小成本。 - **設置最佳模式** - 將最佳模式記錄在 `ipredmode` 數組中,表示該塊的預測模式。 - 更新宏塊的 `intra_pred_modes`,用於後續編碼。 - **生成預測誤差** - 使用最佳模式生成預測塊,並計算與原始塊的殘差(`mb_ores`),通過 `generate_pred_error_4x4` 實現。 - **殘差變換與量化** - 對殘差進行變換和量化(`residual_transform_quant_luma_4x4`),檢查是否有非零係數(`nonzero`),這會影響編碼決策。 #### **關鍵點** - 使用率失真優化(RDO)來選擇模式,平衡編碼成本(碼率)和失真(視頻質量)。 - 最可能模式因其更高的出現概率,分配較低的編碼成本。 --- ### **2. `mode_decision_for_I8x8_blocks_JM_Low` - 8x8 塊的模式決策** 這個函數與 4x4 塊的模式決策類似,但針對 8x8 塊。流程如下: #### **步驟:** - **初始化** - 設置 8x8 塊的坐標 (`block_x`, `block_y`) 和像素位置 (`pic_pix_x`, `pic_pix_y`)。 - 初始化相關數據結構,如殘差 (`mb_ores`) 和預測值 (`mb_pred`)。 - **檢查鄰近塊可用性** - 同樣檢查左邊和上邊塊的可用性,使用 `get4x4Neighbour`。 - **確定最可能模式** - 根據鄰近塊的模式(`upMode` 和 `leftMode`)計算最可能模式,邏輯與 4x4 塊相同。 - **內部預測準備** - 調用 `set_intrapred_8x8` 設置 8x8 塊的內部預測值。 - **首先檢查最可能模式** - 先計算最可能模式的 RD cost: - 生成預測塊(`get_intrapred_8x8`)。 - 計算成本(使用 `mprobcost`,即 `lambda * 1`)並加上失真(`compute_cost8x8`)。 - 如果可用,設置為當前最佳模式和最小成本。 - **遍歷其他模式** - 對其餘模式進行遍歷: - **模式可用性檢查**:類似 4x4 塊,根據鄰近塊可用性判斷。 - **生成預測塊**:生成當前模式的預測數據。 - **計算 RD cost**:使用 `fixedcost`(`lambda * 4`)加上失真成本。 - **更新最佳模式**:如果成本更低,更新 `best_ipmode` 和 `min_cost`。 - **設置最佳模式** - 將最佳模式記錄在 `ipredmode8x8` 數組中,並更新宏塊的 `intra_pred_modes8x8`。 - **生成預測誤差** - 使用最佳模式生成預測塊和殘差(`generate_pred_error_8x8`)。 - **殘差變換與量化** - 對殘差進行變換和量化(`residual_transform_quant_luma_8x8`),返回非零係數的標誌。 #### **關鍵點** - 與 4x4 塊相比,8x8 塊的處理單位更大,但邏輯相似。 - 先檢查最可能模式以節省計算資源,這是低複雜度設計的一部分。 --- ## **總結** `rd_intra_jm_low.c` 的主要功能是為 H.264 編碼中的 4x4 和 8x8 內部預測塊選擇最佳模式。其流程可以概括為: 1. **初始化與準備**:獲取塊信息並檢查鄰近塊可用性。 2. **模式選擇**:通過率失真優化遍歷所有可能模式,選擇 RD cost 最小的模式。 3. **後處理**:設置預測模式,生成殘差,並進行變換與量化。 這個檔案是 H.264 編碼器中內部預測模組的重要組成部分,通過低複雜度的實現確保了高效的模式決策,從而在壓縮效率和視頻質量之間找到最佳平衡點。 # 結構 在JM中使用眾多(巢狀結構),有利於多層數據管理。 * 巢狀結構:結構中包有其他結構存在。 ``` EncoderParams (p_Enc) | ├── VideoParameters (p_Vid) | | | └──SeqStructure(p_pred) | | | └──FrameUnitStructure(p_frm) | ├── InputParameters (p_Vid) --> typedef (inp_par_enc) InputParameters ``` * FrameUnitStructure: 是指「顯示幀」(displayed frame)指的是 最終播放出來的畫面,對應螢幕上的影像。 * VideoParameters: 代表當前影片的全域變數(resolution,frame rate...)。 ## 💡 小延伸:Intra Refresh 是什麼? 在實際應用中,不能每幾秒就強制插入 I-frame,因為會耗很多 bitrate。 所以 encoder 會「每幾行或幾個 macroblock 漸進式地 Intra encode」,這就是所謂的 intra refresh 機制。 ----------------------------------------------------------- 這行條件就是在判斷要不要做這種漸進式的 intra。 | 應用場景 🌎 | 傳輸策略 💡 | 備註 📌 | |------------------------------|--------------------------------------------------|------------------------------------------------------------------------| | 🎞️ **離線影片(如 MP4 / MKV)** | 影片開頭傳一次 SPS + PPS | 通常嵌入 container header(像 MP4 `avcC` box 裡),播放器開檔時就能抓到參數 | | 🖥️ **本地解碼實驗 / 測試流程** | 第一幀(通常是 IDR)前送一次 SPS + PPS | JM / x264 會這樣做,對省頻寬最極致,但對容錯性差 | | 📺 **網路直播(RTMP、HLS、DASH)** | 每個 IDR 幀都插入 SPS + PPS | 增加容錯、支援 random access,中途加入的觀眾也能立刻播放 | | 📶 **低延遲串流(WebRTC, Zoom)** | 定期送 SPS(或透過訊號通道傳參數) | 頻寬極限下可能不每幀送,但一定會設機制保護「第一幀能正確解碼」 | | 📼 **DVR/監控錄影** | 每段片段開頭送一次 SPS + PPS(或每次I幀) | 檔案切割後仍能單獨播放,類似 segment-based 對應 | | 📱 **OTT影片平台(YouTube、Netflix)** | 初始化送一次,segment 切換(或 Profile 變動)才重送 | 若切換清晰度/畫質時 Profile/Level 改變,會重新送一個新的 SPS,PPS 視情況跟著更新 | # 名詞解釋 * Frame Structure:在 H.264/AVC(Advanced Video Coding) 或其他視頻編碼標準中,Frame Structure(幀結構) 指的是 一幀影像(frame)在編碼或解碼時的組織方式。 它決定了: * 這一幀是以逐行掃描(Progressive) 還是 隔行掃描(Interlaced) 來處理? * 這一幀的數據如何組織?是單個幀(Frame Picture)還是場(Field Picture)? * 這一幀如何與其他幀建立參考關係(GOP 結構)? | 描述符 | 中文釋義 | 解析方式 | |-------------|---------------------------------------------|--------------------------------------------------------| | **ae(v)** | 上下文自適應算術熵編碼語法元素 | 按照第 9.3 節進行算術編碼解析 | | **b(8)** | 8 位任意位元組(Byte) | `read_bits(8)`;常用於字元或多位元組碼,不當作無符號數 | | **ce(v)** | 左位先行的上下文自適應可變長熵編碼語法元素 | 按照第 9.2 節進行可變長碼解析 | | **f(n)** | 固定位圖模式串:n 位固定比特,從左到右寫入 | `read_bits(n)` | | **i(n)** | n 位有符號整數(Two’s complement) | `read_bits(n)` 讀取後按二補數形式解讀 | | **me(v)** | 映射後的 Exp-Golomb 可變長碼語法元素 | 按照第 9.1 節,以左位先行方式解析 | | **se(v)** | 帶符號 Exp-Golomb 可變長碼語法元素 | 按照第 9.1 節,以左位先行方式解析 | | **te(v)** | 截斷式 Exp-Golomb 可變長碼語法元素 | 按照第 9.1 節,以左位先行方式解析 | | **u(n)** | n 位無符號整數 | `read_bits(n)` 讀取後按二進制當無符號整數解讀 | | **ue(v)** | 無符號 Exp-Golomb 可變長碼語法元素 | 按照第 9.1 節,以左位先行方式解析 | intra link [intra](https://people.xiph.org/~xiphmont/demo/daala/demo2.shtml) 在 JM Reference Software 中,你不會看到一個叫做 “IME.c” 或 “FME.c” 的單一檔案,因為整數格點 (Integer Motion Estimation, IME) 和次畫素格點 (Fractional Motion Estimation, FME) 的功能是分散在多個模組裡,由不同的檔案共同協作完成的。下面分別說明這兩部分程式碼通常出現在哪些檔案中,以及為什麼 JM 沒有獨立的 IME/FME 檔案。 # 📁 為什麼 JM Software 中沒有 IME.c / FME.c? ## 1. 沒有獨立的 IME.c / FME.c 的原因 1. **整合式設計** JM 的架構將 Motion Estimation(ME)與 Motion Compensation(MC)流程分別為: - 演算法邏輯(在不同檔案如 macroblock.c、me_fullsearch.c) - 畫素插值(統一放在 MC 模組,如 mc_prediction.c) 2. **多種搜尋演算法共存** JM 支援多種搜尋方式(Full Search、Diamond Search、EPZS),每種對應不同的程式檔案,不會整合成單一 `IME.c`。 3. **次畫素插值集中在 MC 模組** 所有 sub-pixel interpolation 都放在 motion compensation 相關檔案,例如 `mc_prediction.c` 或 `img_int.c`,而非在 Motion Estimation 專屬檔案。 --- ## 2. Integer Motion Estimation(IME)在哪些檔案實作? - `me_fullsearch.c` 整數像素級的全域搜尋實作(Full Search),依序計算 SAD/SSE/SATD。 - `me_epzs.c` Enhanced Predictive Zonal Search(EPZS)快速搜尋演算法的實作,是 JM 預設啟用的演算法。 - `macroblock.c` 高階函式 `PartitionMotionSearch()`、`MotionEstimation()` 呼叫上述演算法並統一管理。 --- ## 3. Fractional Motion Estimation(FME)在哪些檔案實作? - `mc_prediction.c`(或 `img_int.c`) 實作 sub-pixel interpolation filter(6-tap FIR、bilinear),產生 1/2、1/4 畫素位置。 - `macroblock.c` `sub_pel_search()` 函式處理次畫素 MV 搜尋與誤差計算。 - `encoder.cfg` FME 的精度與插值設定可透過 encoder.cfg 控制(如 quarter-pel 精度開關)。 --- ## 4. 如何快速找到這些程式碼? 1. 移動至 `JM/src/encoder/` 目錄 2. 搜尋關鍵字: - `PartitionMotionSearch` → 整數 ME - `sub_pel_search` → 次畫素 ME - `get_block_luma()`、`InterpolationFilter` → 插值函式 3. 查看 `Makefile` 或 Visual Studio 專案設定,確定哪些檔案會被編譯進 `lencod.exe` --- ## ✅ 小結 - **沒有單一的 `IME.c` / `FME.c` 檔案**,ME 的功能是分散實作的。 - **整數 ME**:實作於 `me_fullsearch.c`、`me_epzs.c`、`macroblock.c` 中。 - **次畫素 ME**:插值實作於 `mc_prediction.c`,搜尋與管理實作於 `macroblock.c` 的 `sub_pel_search()`。 - 需搭配 `encoder.cfg` 調整參數與精度。 > 只要掌握這些主檔案與函式名稱,就可以快速定位 JM reference 中 ME 的整個流程。