# 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)

## 資料夾
在JM reference software中有inc以及src兩個資料夾。
1. inc功能是存放header files,定義函數界面(interfaces)也就是.h檔,包含 全域變數,巨集定義,結構體以及型別定義。
2. src功能是存放實作檔案(source files)也就是.c檔,包含具體的函數實作,負責完成演算法,資料處理和功能執行。
3.
# 1. 從 main()主程式切入(lencod.c第254行)

```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 的整個流程。