音视频学习(六十):H264中的PPS

发布于:2025-08-31 ⋅ 阅读:(23) ⋅ 点赞:(0)

什么是PPS?

PPS 是 H.264 码流中的一种特定 NAL 单元(NAL Unit),其主要任务是提供与一幅或多幅图像相关的参数信息。与 SPS 负责整个视频序列的全局配置不同,PPS 的作用范围相对较小,通常用于一个或多个 GOP(Group of Pictures)。这种层次化的参数管理机制,使得 H.264 具有极高的灵活性和鲁棒性。

  • 层级管理: SPS 位于最高层级,定义了整个视频序列的宏观参数,如 profile、level、图像尺寸等。PPS 位于其下,定义了帧级的具体参数,如熵编码模式、量化参数、环路滤波等。
  • 共享参数: PPS 的核心思想是参数共享。对于一个 GOP 中的所有图像,如果它们共享相同的解码参数,那么这些参数只需在 PPS 中定义一次,而不需要在每一帧的切片头(slice header)中重复携带。这大大减少了码流的开销,提高了编码效率。
  • 动态更新: 虽然一个 PPS 可能适用于多个图像,但如果某些参数需要在编码过程中发生变化(例如,量化参数 Qp 的调整),编码器可以插入一个新的 PPS,从而实现参数的动态更新。这为编码器提供了精细控制码流的能力,以适应不同场景的需求。

PPS结构体

// H.264 Picture Parameter Set (PPS)
typedef struct H264PPS {
    int pic_parameter_set_id;           // PPS 的 ID,范围通常 0~255,对应到具体的一组 PPS 参数
    int seq_parameter_set_id;           // 引用的 SPS ID,表示此 PPS 属于哪个 SPS

    int entropy_coding_mode_flag;       // 0 = CAVLC (Context-Adaptive VLC),1 = CABAC (Context-Adaptive Binary Arithmetic Coding)
    int bottom_field_pic_order_in_frame_present_flag; // 帧内是否包含底场的 POC(Picture Order Count)

    int num_slice_groups_minus1;        // 切片组数量 - 1,=0 表示只有一个 slice group
    int slice_group_map_type;           // 切片组映射类型(决定分片方式)

    int num_ref_idx_l0_default_active_minus1; // 默认参考帧列表 L0 的参考帧个数 - 1
    int num_ref_idx_l1_default_active_minus1; // 默认参考帧列表 L1 的参考帧个数 - 1

    int weighted_pred_flag;             // P 帧是否启用加权预测
    int weighted_bipred_idc;            // B 帧加权预测模式:0=禁用,1=启用,2=隐式

    int pic_init_qp_minus26;            // 初始 QP(相对值),实际 QP = 26 + 该值
    int pic_init_qs_minus26;            // 初始 QS(用于 SP/SI slice)
    int chroma_qp_index_offset;         // 色度分量 QP 偏移量
    int second_chroma_qp_index_offset;  // 第二个色度分量 QP 偏移量(通常等于 chroma_qp_index_offset)

    int deblocking_filter_control_present_flag; // 是否支持去块滤波控制
    int constrained_intra_pred_flag;    // 是否强制约束帧内预测(防止跨块预测)
    int redundant_pic_cnt_present_flag; // 是否允许冗余片

    // 扩展字段(可选)
    int transform_8x8_mode_flag;        // 是否启用 8x8 DCT 变换
    int pic_scaling_matrix_present_flag;// 是否有缩放矩阵
    int scaling_list_4x4[6][16];        // 4x4 变换缩放矩阵
    int scaling_list_8x8[2][64];        // 8x8 变换缩放矩阵
} H264PPS;

字段说明(核心)

  • pic_parameter_set_id
    唯一标识 PPS,在解码时通过该 ID 找到对应的 PPS 参数。
  • seq_parameter_set_id
    表示该 PPS 依赖的 SPS(序列参数集),解码器需要先解析 SPS。
  • entropy_coding_mode_flag
    指定熵编码方式:
    • 0 → CAVLC(简单一些,解码器必须支持),CAVLC 是一种基于变长码表的熵编码方法,计算量小,适用于 Baseline 和 Main Profile;
    • 1 → CABAC(压缩率更高,但解码复杂),CABAC 是一种基于上下文的二进制算术编码,其压缩效率更高,但计算复杂度也更高,适用于 High Profile。
  • num_slice_groups_minus1 / slice_group_map_type
    用于 FMO(Flexible Macroblock Ordering),决定宏块分组方式。大部分码流设置为 0,即不使用 FMO。
  • num_ref_idx_l0_default_active_minus1 / num_ref_idx_l1_default_active_minus1
    定义 P/B 帧的默认参考帧数,影响解码器参考帧列表。
  • weighted_pred_flag / weighted_bipred_idc
    加权预测相关,主要用于提高 B/P 帧的压缩效率。
  • pic_init_qp_minus26 / pic_init_qs_minus26 / chroma_qp_index_offset
    量化参数设置:
    • QP(亮度量化参数);
    • QS(SI/SP slice 使用的量化参数);
    • Chroma 偏移(色度分量的量化偏移)。
  • deblocking_filter_control_present_flag
    是否开启去块滤波(Deblocking filter)。
  • constrained_intra_pred_flag
    是否限制帧内预测,通常用于误码鲁棒性。
  • redundant_pic_cnt_present_flag
    是否允许冗余切片,用于容错或错误恢复。
  • transform_8x8_mode_flag / scaling_matrix
    H.264 High Profile 扩展字段,是否启用 8x8 变换,以及自定义缩放矩阵。

PPS的编解码

编码端:

  1. 选择参数: 编码器根据用户配置和视频内容,选择合适的编码参数,如熵编码模式、量化参数等。
  2. 生成 SPS: 首先生成并发送 SPS NAL 单元,定义整个视频序列的全局参数。
  3. 生成 PPS: 随后生成并发送 PPS NAL 单元。PPS 内部包含了与帧级参数相关的字段,并引用了之前生成的 SPS。
  4. 编码视频数据: 编码器将视频帧编码成切片,并在每个切片头中插入 pic_parameter_set_id,指向之前生成的 PPS。
  5. 动态更新: 如果编码参数发生变化,编码器会生成并发送一个新的 PPS,然后后续的切片将引用新的 PPS。

解码端:

  1. 接收码流: 解码器接收 H.264 码流。
  2. 解析 SPS 和 PPS: 解码器首先解析并缓存 SPS 和 PPS NAL 单元。SPS 和 PPS 通常会在码流的起始位置发送,或者在 seek(跳转)操作后重新发送。
  3. 解析切片: 当解码器接收到一个切片 NAL 单元时,它会首先解析切片头。
  4. 查找参数: 从切片头中提取 pic_parameter_set_id,然后到缓存中查找对应的 PPS。
  5. 获取完整参数集: 通过 PPS 内部的 seq_parameter_set_id,解码器找到对应的 SPS。此时,解码器就拥有了完整的、用于解码当前切片的参数集。
  6. 解码图像: 结合这些参数,解码器对切片数据进行熵解码、反量化、反变换、运动补偿和环路滤波等操作,最终重建出视频帧。

使用示例(c++)

#include <cstdint>
#include <vector>
#include <stdexcept>
#include <cstring>
#include <iostream>
#include <array>

// ------------------------- BitReader (with emulation prevention removal) -------------------------
class BitReader {
public:
    // 构造:传入 NAL RBSP(含 emulation prevention bytes 0x03)
    BitReader(const std::vector<uint8_t>& nal_rbsp) {
        buffer = removeEmulationPrevention(nal_rbsp);
        bit_pos = 0;
    }

    // 读单个比特
    uint32_t readBit() {
        if (bit_pos >= buffer.size() * 8) throw std::out_of_range("readBit out of range");
        size_t byte_idx = bit_pos / 8;
        int offset = 7 - (bit_pos % 8);
        uint8_t bit = (buffer[byte_idx] >> offset) & 0x01u;
        ++bit_pos;
        return bit;
    }

    // 读 n bits(n <= 32)
    uint32_t readBits(int n) {
        if (n == 0) return 0;
        if (n < 0 || n > 32) throw std::invalid_argument("readBits n invalid");
        uint32_t v = 0;
        for (int i = 0; i < n; ++i) {
            v = (v << 1) | readBit();
        }
        return v;
    }

    // 读 ue(v) 无符号 Exp-Golomb
    uint32_t readUE() {
        int leadingZeroBits = -1;
        for (uint32_t b = 0; b == 0; ++leadingZeroBits) {
            b = readBit();
            if (bit_pos > buffer.size() * 8 + 32) throw std::out_of_range("readUE overflow");
            if (b == 1) break;
        }
        if (leadingZeroBits < 0) return 0;
        uint32_t codeNum = (1u << leadingZeroBits) - 1 + readBits(leadingZeroBits);
        return codeNum;
    }

    // 读 se(v) 有符号 Exp-Golomb
    int32_t readSE() {
        uint32_t codeNum = readUE();
        int32_t val = (codeNum & 1u) ? (int32_t)((codeNum + 1) / 2) : -(int32_t)(codeNum / 2);
        return val;
    }

    // 读 rbsp_byte_aligned 的剩余零位(用于结束对齐)
    void rbspTrailingBitsAlign() {
        // 读停止位 1,然后跳过剩余 0
        readBit(); // should be 1
        while (bit_pos % 8) {
            uint32_t v = readBit();
            if (v != 0 && bit_pos % 8 != 0) {
                // 非 0 的填充,可能是 malformed
            }
        }
    }

    size_t bitsLeft() const {
        return buffer.size() * 8 - bit_pos;
    }

private:
    std::vector<uint8_t> buffer;
    size_t bit_pos;

    // 删除 emulation prevention bytes (0x00 0x00 0x03)
    static std::vector<uint8_t> removeEmulationPrevention(const std::vector<uint8_t>& src) {
        std::vector<uint8_t> dst;
        dst.reserve(src.size());
        for (size_t i = 0; i < src.size(); ++i) {
            // if pattern 0x00 0x00 0x03 -> skip 0x03
            if (i + 2 < src.size() && src[i] == 0x00 && src[i + 1] == 0x00 && src[i + 2] == 0x03) {
                dst.push_back(0x00);
                dst.push_back(0x00);
                i += 2; // 下一轮 i++ 会跳到 0x03 的后面
                continue;
            }
            // also accomodate when 0x00 0x00 0x03 at the end partial matches - safe to just copy
            dst.push_back(src[i]);
        }
        return dst;
    }
};

// ------------------------- PPS 数据结构(覆盖常用字段) -------------------------
struct H264PPS {
    uint32_t pic_parameter_set_id = 0;
    uint32_t seq_parameter_set_id = 0;

    bool entropy_coding_mode_flag = false;
    bool bottom_field_pic_order_in_frame_present_flag = false;

    uint32_t num_slice_groups_minus1 = 0;
    uint32_t slice_group_map_type = 0;
    // 针对 slice group map type 的可选字段我们会按需解析,但不在结构体展开太多

    uint32_t num_ref_idx_l0_default_active_minus1 = 0;
    uint32_t num_ref_idx_l1_default_active_minus1 = 0;

    bool weighted_pred_flag = false;
    uint8_t weighted_bipred_idc = 0;

    int32_t pic_init_qp_minus26 = 0;
    int32_t pic_init_qs_minus26 = 0;
    int32_t chroma_qp_index_offset = 0;
    int32_t second_chroma_qp_index_offset = 0;

    bool deblocking_filter_control_present_flag = false;
    bool constrained_intra_pred_flag = false;
    bool redundant_pic_cnt_present_flag = false;

    // 高级/可选
    bool transform_8x8_mode_flag = false;
    bool pic_scaling_matrix_present_flag = false;
    // scaling lists: 6 * 4x4, 2 * 8x8 (按规范)
    std::array<std::array<uint8_t, 16>, 6> scaling_list_4x4{};
    std::array<std::array<uint8_t, 64>, 2> scaling_list_8x8{};
    bool scaling_list_4x4_present[6] = {false,false,false,false,false,false};
    bool scaling_list_8x8_present[2] = {false,false};

    // 保留原始字段便于调试
    std::vector<uint8_t> raw_rbsp;
};

// ------------------------- 解析 scaling list 的辅助函数 -------------------------
static void parseScalingList(BitReader& br, uint8_t *dst, int size, bool &presentFlag) {
    // size 是元素数量(16 或 64)
    // scaling_list() 参考标准:初始化 lastScale = 8, nextScale = 8;读取 deltaScale(se(v)),计算 nextScale = (lastScale + deltaScale + 256) % 256
    // 如果 nextScale != 0, scale[i] = nextScale; else scale[i] = lastScale
    presentFlag = true;
    int lastScale = 8;
    int nextScale = 8;
    for (int j = 0; j < size; ++j) {
        if (nextScale != 0) {
            int32_t deltaScale = br.readSE();
            nextScale = (lastScale + deltaScale + 256) % 256;
        }
        if (nextScale == 0) {
            dst[j] = (uint8_t)lastScale;
        } else {
            dst[j] = (uint8_t)nextScale;
            lastScale = nextScale;
        }
    }
}

// ------------------------- parsePPS 主函数 -------------------------
H264PPS parsePPS(const std::vector<uint8_t>& nal_rbsp_payload) {
    BitReader br(nal_rbsp_payload);
    H264PPS pps;
    pps.raw_rbsp = nal_rbsp_payload;

    pps.pic_parameter_set_id = br.readUE();
    pps.seq_parameter_set_id = br.readUE();

    pps.entropy_coding_mode_flag = br.readBit();
    pps.bottom_field_pic_order_in_frame_present_flag = br.readBit();

    pps.num_slice_groups_minus1 = br.readUE();
    if (pps.num_slice_groups_minus1 > 0) {
        pps.slice_group_map_type = br.readUE();
        if (pps.slice_group_map_type == 0) {
            for (uint32_t iGroup = 0; iGroup <= pps.num_slice_groups_minus1; ++iGroup) {
                // run_length_minus1[iGroup]
                (void)br.readUE();
            }
        } else if (pps.slice_group_map_type == 2) {
            for (uint32_t iGroup = 0; iGroup <= pps.num_slice_groups_minus1; ++iGroup) {
                // top_left, bottom_right
                (void)br.readUE();
                (void)br.readUE();
            }
        } else if (pps.slice_group_map_type == 3 ||
                   pps.slice_group_map_type == 4 ||
                   pps.slice_group_map_type == 5) {
            // slice_group_change_direction_flag + slice_group_change_rate_minus1
            (void)br.readBit();
            (void)br.readUE();
        } else if (pps.slice_group_map_type == 6) {
            uint32_t pic_size_in_map_units_minus1 = br.readUE();
            for (uint32_t i = 0; i <= pic_size_in_map_units_minus1; ++i) {
                // slice_group_id[i]
                // 读的比特数由 num_slice_groups_minus1 决定
                int bits = 0;
                uint32_t n = pps.num_slice_groups_minus1 + 1;
                while ((1u << bits) < n) bits++;
                if (bits > 0) (void)br.readBits(bits);
            }
        }
    }

    pps.num_ref_idx_l0_default_active_minus1 = br.readUE();
    pps.num_ref_idx_l1_default_active_minus1 = br.readUE();

    pps.weighted_pred_flag = br.readBit();
    pps.weighted_bipred_idc = (uint8_t)br.readBits(2);

    pps.pic_init_qp_minus26 = br.readSE();
    pps.pic_init_qs_minus26 = br.readSE();
    pps.chroma_qp_index_offset = br.readSE();
    pps.second_chroma_qp_index_offset = br.readSE();

    pps.deblocking_filter_control_present_flag = br.readBit();
    pps.constrained_intra_pred_flag = br.readBit();
    pps.redundant_pic_cnt_present_flag = br.readBit();

    // high profile / future params: transform_8x8_mode_flag, pic_scaling_matrix_present_flag ...
    // 这些字段在 PPS(rbsp) 的后续位置,当 slice_group... 等都解析完成后出现(按规范)
    // 解析可选的扩展字段(存在于大部分编码器生成的 PPS)
    if (br.bitsLeft() > 0) {
        // Check if next bits correspond to more data (guarded)
        // transform_8x8_mode_flag 可能出现在后面,仅当存在相应的位时读取(依据规范是紧跟一些 profile/sps 信息)
        try {
            // 尝试读取 transform_8x8_mode_flag(如果有)
            pps.transform_8x8_mode_flag = br.readBit();
            pps.pic_scaling_matrix_present_flag = br.readBit();
            if (pps.pic_scaling_matrix_present_flag) {
                // parse pic_scaling_list_present_flag for 6 + (2 if transform8x8) lists
                for (int i = 0; i < 6; ++i) {
                    bool present = br.readBit();
                    if (present) {
                        parseScalingList(br, pps.scaling_list_4x4[i].data(), 16, pps.scaling_list_4x4_present[i]);
                    }
                }
                if (pps.transform_8x8_mode_flag) {
                    for (int i = 0; i < 2; ++i) {
                        bool present = br.readBit();
                        if (present) {
                            parseScalingList(br, pps.scaling_list_8x8[i].data(), 64, pps.scaling_list_8x8_present[i]);
                        }
                    }
                }
            }
        } catch (...) {
            // 如果读取失败(比特不够),忽略扩展字段(很多流不会包含)
        }
    }

    // 最后 rbsp_trailing_bits (对齐)
    //br.rbspTrailingBitsAlign();

    return pps;
}

// ------------------------- 示例 DecoderContext 与 applyPPSToDecoder(伪代码风格) -------------------------
struct DecoderContext {
    bool useCABAC = false;
    bool deblockingEnabled = true;
    int chromaQPOffset[2] = {0,0};
    int initQP = 26;
    bool transform8x8 = false;
    // ... 其他上下文(参考帧列表、slice config 等)
};

void applyPPSToDecoder(const H264PPS& pps, DecoderContext& ctx) {
    // entropy coding
    ctx.useCABAC = pps.entropy_coding_mode_flag;

    // initial QP
    ctx.initQP = 26 + pps.pic_init_qp_minus26;

    // chroma QP offsets
    ctx.chromaQPOffset[0] = pps.chroma_qp_index_offset;
    ctx.chromaQPOffset[1] = pps.second_chroma_qp_index_offset;

    // deblocking
    ctx.deblockingEnabled = pps.deblocking_filter_control_present_flag;

    // transform 8x8
    ctx.transform8x8 = pps.transform_8x8_mode_flag;

    // 加权预测,参考数等对参考管理和运动补偿模块有影响(示例性 log)
    if (pps.weighted_pred_flag) {
        // 在 P slice 处理里要启用加权预测代码路径
        std::clog << "[PPS] weighted_pred_flag = 1 (enable weighted pred for P)\n";
    }
    if (pps.weighted_bipred_idc) {
        std::clog << "[PPS] weighted_bipred_idc = " << (int)pps.weighted_bipred_idc << "\n";
    }

    // scaling lists:如果提供,则用于重建逆量化矩阵(这里仅标记存在)
    for (int i = 0; i < 6; ++i) {
        if (pps.scaling_list_4x4_present[i]) {
            std::clog << "[PPS] scaling_list_4x4 present for idx " << i << "\n";
            // 你需把 pps.scaling_list_4x4[i] 转为解码器内部的 IQ matrix
        }
    }
    for (int i = 0; i < 2; ++i) {
        if (pps.scaling_list_8x8_present[i]) {
            std::clog << "[PPS] scaling_list_8x8 present for idx " << i << "\n";
            // 同上
        }
    }

    // TODO: 根据实际解码器把这些参数映射到相应模块(CABAC tables, deblocking filter params,
    //       quantization matrices, ref list lengths 等)
}

// ------------------------- 使用示例 -------------------------
int main() {
    // 假设这是 PPS NAL 的 RBSP payload(仅举例,非真实 PPS)
    // real use: fill from NAL unit payload (exclude nal header)
    std::vector<uint8_t> examplePPS = {
        // 下面字节仅为示例,不表示有效 PPS
        0x68, 0xCE, 0x06, 0xE2
    };

    try {
        H264PPS pps = parsePPS(examplePPS);
        DecoderContext ctx;
        applyPPSToDecoder(pps, ctx);

        std::cout << "pps.pic_parameter_set_id = " << pps.pic_parameter_set_id << "\n";
        std::cout << "useCABAC = " << ctx.useCABAC << ", initQP = " << ctx.initQP << "\n";
    } catch (const std::exception& e) {
        std::cerr << "parsePPS error: " << e.what() << "\n";
    }
    return 0;
}