音视频学习(七十七):无损压缩:Huffman编码

在视频压缩技术中,压缩方式通常分为有损压缩无损压缩 两大类。有损压缩通过舍弃人眼不敏感的信息来获得更高的压缩比,而无损压缩则要求在解码后完全恢复原始数据,不允许任何信息丢失。无损压缩在医学影像、监控取证、工业检测、视频中间处理流程等场景中具有重要价值。

在视频无损压缩体系中,**熵编码(Entropy Coding)**是核心环节,而 Huffman 编码作为最经典、应用最广泛的熵编码方法之一,被广泛应用于 JPEG、MPEG、H.264、H.265(HEVC)等视频编码标准中。

视频无损压缩中的熵编码位置

在典型的视频编码流程中(以 H.264 为例),即便在"无损模式"下,也仍然会经历如下步骤:

  1. 帧内/帧间预测
  2. 残差计算(Prediction Error)
  3. 变换(无损模式下可跳过或使用整数变换)
  4. 熵编码(Entropy Coding)

可以看到,在无损压缩中,熵编码是决定最终压缩效率的关键步骤。Huffman 编码正是最早、也是最基础的熵编码方式之一。

Huffman 编码基本原理

3.1 信息熵与编码思想

Huffman 编码基于信息论中的**信息熵(Entropy)**概念:

  • 出现概率越高的符号,应使用越短的码字;
  • 出现概率越低的符号,使用较长的码字。

其目标是:

在不丢失任何信息的前提下,使平均码长最小化。

Huffman 编码是一种前缀码(Prefix Code),即任意一个码字都不是其他码字的前缀,从而保证了解码的唯一性。

3.2 Huffman 树构建过程

Huffman 编码的核心是构建 Huffman 树,基本步骤如下:

  1. 统计符号出现频率
    对输入数据(如像素值、残差值、变换系数)进行统计,得到每个符号的出现概率。
  2. 初始化节点集合
    每个符号作为一个独立节点,权值为其出现频率。
  3. 合并最小权值节点
    从节点集合中选取权值最小的两个节点,合并为一个新节点,其权值为两者之和。
  4. 重复合并过程
    不断重复上述步骤,直到只剩下一个根节点。
  5. 生成码字
    从根节点出发,向左分支记为 0,向右分支记为 1,即可得到每个符号的 Huffman 码字。

3.3 编码与解码过程

  • 编码过程
    将原始数据中的每个符号替换为对应的 Huffman 码字,形成比特流。
  • 解码过程
    按比特流从 Huffman 树根节点开始遍历,遇到叶子节点即输出对应符号。

由于 Huffman 编码是无损的,解码后可以100% 恢复原始数据

流程示例

背景(贴近视频场景)

假设在视频无损压缩中,我们对预测残差进行熵编码,得到如下残差符号序列(已离散化):

复制代码
符号序列:
0  0  0  1  0  -1  0  0  2  0  1  0  0  0  -1 -2

这是视频残差中非常典型的分布:
0 出现最多,小幅正负残差次之,大残差最少

步骤 1:统计符号频率

统计每个符号出现的次数:

符号 出现次数 概率
0 10 10/16
1 2 2/16
-1 2 2/16
2 1 1/16
-2 1 1/16

步骤 2:构建 Huffman 树

3.1 初始化节点(按频率排序)

复制代码
-2(1), 2(1), -1(2), 1(2), 0(10)

3.2 合并最小频率节点

第一次合并:

复制代码
-2(1) + 2(1) → A(2)

第二次合并:

复制代码
A(2) + -1(2) → B(4)

第三次合并:

复制代码
1(2) + B(4) → C(6)

第四次合并:

复制代码
C(6) + 0(10) → Root(16)

3.3 Huffman 树结构(示意)

复制代码
                (16)
               /    \
           0(10)     (6)
                    /   \
                 1(2)   (4)
                        /   \
                     -1(2)  (2)
                             / \
                          -2(1) 2(1)

步骤 3:生成 Huffman 编码表

约定:

  • 左分支 = 0
  • 右分支 = 1

得到编码表:

符号 Huffman 编码
0 0
1 10
-1 110
-2 1110
2 1111

出现频率越高,编码越短

步骤 4:编码示例

原始符号序列:

复制代码
0  0  0  1  0  -1  0  0  2  0  1  0  0  0  -1 0

替换为 Huffman 编码:

复制代码
0    → 0
0    → 0
0    → 0
1    → 10
0    → 0
-1   → 110
0    → 0
0    → 0
2    → 1111
0    → 0
1    → 10
0    → 0
0    → 0
0    → 0
-1   → 110
0    → 0

拼接后的比特流:

复制代码
0001001100011110100001100

压缩效果对比

原始表示(定长编码)

假设每个残差用 3 bit 表示:

复制代码
16 × 3 = 48 bit

Huffman 编码后

统计 Huffman 编码比特数:

符号 次数 码长 总 bit
0 10 1 10
1 2 2 4
-1 2 3 6
2 1 4 4
-2 1 4 4
合计 28 bit

压缩率:

复制代码
48 → 28 bit
节省约 41.7%

解码端实现(c++)

c++ 复制代码
#include <iostream>
#include <vector>
#include <unordered_map>
#include <string>
#include <stdexcept>

/* ===============================
 * Huffman Tree Node
 * =============================== */
struct HuffmanNode
{
    int symbol;                 // 叶子节点的符号
    HuffmanNode* left;
    HuffmanNode* right;

    HuffmanNode(int s = -1)
        : symbol(s), left(nullptr), right(nullptr) {}
};

/* ===============================
 * BitReader(模拟视频比特流)
 * =============================== */
class BitReader
{
public:
    explicit BitReader(const std::string& bits)
        : m_bits(bits), m_pos(0) {}

    bool readBit(int& bit)
    {
        if (m_pos >= m_bits.size())
            return false;

        bit = (m_bits[m_pos++] == '1') ? 1 : 0;
        return true;
    }

private:
    std::string m_bits;
    size_t m_pos;
};

/* ===============================
 * 构建 Huffman Tree
 * =============================== */
HuffmanNode* buildHuffmanTree(
    const std::unordered_map<int, std::string>& table)
{
    HuffmanNode* root = new HuffmanNode();

    for (const auto& kv : table)
    {
        int symbol = kv.first;
        const std::string& code = kv.second;

        HuffmanNode* cur = root;
        for (char c : code)
        {
            if (c == '0')
            {
                if (!cur->left)
                    cur->left = new HuffmanNode();
                cur = cur->left;
            }
            else if (c == '1')
            {
                if (!cur->right)
                    cur->right = new HuffmanNode();
                cur = cur->right;
            }
            else
            {
                throw std::runtime_error("Invalid Huffman code");
            }
        }
        cur->symbol = symbol; // 叶子节点
    }
    return root;
}

/* ===============================
 * Huffman 解码器
 * =============================== */
class HuffmanDecoder
{
public:
    explicit HuffmanDecoder(HuffmanNode* root)
        : m_root(root) {}

    bool decodeOne(BitReader& br, int& symbol)
    {
        HuffmanNode* cur = m_root;
        int bit;

        while (true)
        {
            if (!br.readBit(bit))
                return false; // bitstream 结束

            cur = (bit == 0) ? cur->left : cur->right;

            if (!cur)
                throw std::runtime_error("Invalid bitstream");

            // 到达叶子节点
            if (!cur->left && !cur->right)
            {
                symbol = cur->symbol;
                return true;
            }
        }
    }

private:
    HuffmanNode* m_root;
};

/* ===============================
 * 测试入口
 * =============================== */
int main()
{
    try
    {
        /* Huffman 编码表(解码端已知)
         *
         * 0  -> 0
         * 1  -> 10
         * -1 -> 110
         * -2 -> 1110
         * 2  -> 1111
         */
        std::unordered_map<int, std::string> huffmanTable = {
            {0,  "0"},
            {1,  "10"},
            {-1, "110"},
            {-2, "1110"},
            {2,  "1111"}
        };

        // 编码后的 bitstream(来自编码端)
        std::string bitstream = "0001001100011110100001100";

        // 构建 Huffman 树
        HuffmanNode* root = buildHuffmanTree(huffmanTable);

        // 初始化 BitReader
        BitReader br(bitstream);

        // 初始化解码器
        HuffmanDecoder decoder(root);

        // 解码
        std::vector<int> decoded;
        int symbol;

        while (decoder.decodeOne(br, symbol))
        {
            decoded.push_back(symbol);
        }

        // 输出解码结果
        std::cout << "Decoded symbols:\n";
        for (int v : decoded)
            std::cout << v << " ";
        std::cout << std::endl;
    }
    catch (const std::exception& e)
    {
        std::cerr << "Error: " << e.what() << std::endl;
    }

    return 0;
}

总结

Huffman 编码通过为高频符号分配短码、低频符号分配长码,实现无损熵编码。在视频无损压缩中,常用于对预测残差和变换系数进行编码,能够在不丢失任何信息的前提下显著降低码率。

相关推荐
卢锡荣16 小时前
Type-c OTG数据与充电如何进行交互使用应用讲解
c语言·开发语言·计算机外设·电脑·音视频
沛沛老爹20 小时前
Web开发者转型AI:多模态Agent视频分析技能开发实战
前端·人工智能·音视频
等风来不如迎风去21 小时前
【UniVA】1:统一的视频agent:智能体系统,专门用于处理复杂的视频生成、编辑和理解任务
音视频
知秋一叶1231 天前
Miloco v0.1.6 :米家摄像头清晰度配置 + RTSP 音频传输
人工智能·音视频·智能家居
xmRao1 天前
Qt+FFmpeg 实现音频重采样
qt·ffmpeg·音视频
发哥来了1 天前
主流AI视频生成模型商用化能力评测:三大核心维度对比分析
大数据·人工智能·音视频
发哥来了1 天前
《AI图生视频技术深度剖析:原理、应用与发展趋势》
人工智能·音视频
EasyCVR2 天前
国标GB28181视频监控平台EasyCVR智慧农场监管可视化方案设计
音视频
雾江流2 天前
HDx播放器1.0.184 | 支持多种格式和4K/8K高清视频播放,内置推特~脸书下载器
音视频·软件工程
tongyue2 天前
智慧家居——Flask网页视频服务器
服务器·flask·音视频