音视频学习(七十七):无损压缩: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 编码通过为高频符号分配短码、低频符号分配长码,实现无损熵编码。在视频无损压缩中,常用于对预测残差和变换系数进行编码,能够在不丢失任何信息的前提下显著降低码率。

相关推荐
却道天凉_好个秋11 小时前
音视频学习(八十四):视频压缩:MPEG 1、MPEG 2和MPEG 4
学习·音视频
却道天凉_好个秋12 小时前
音视频学习(八十三):视频压缩:MJPEG技术
学习·音视频·mjpeg·视频压缩
qianbo_insist12 小时前
基于图像尺寸的相机内参拼接视频
数码相机·音视频·拼接
水中加点糖13 小时前
RagFlow实现多模态搜索(文、图、视频)与(关键字/相似度)搜索原理(二)
python·ai·音视频·knn·ragflow·多模态搜索·相似度搜索
却道天凉_好个秋13 小时前
音视频学习(八十二):mp4v
学习·音视频·mp4v
winfredzhang13 小时前
从零构建:基于 Node.js 的全栈视频资料管理系统开发实录
css·node.js·html·音视频·js·收藏,搜索,缩略图
行业探路者1 天前
二维码标签是什么?主要有线上生成二维码和文件生成二维码功能吗?
学习·音视频·语音识别·二维码·设备巡检
Android系统攻城狮1 天前
Android16音频之获取Record状态AudioRecord.getState:用法实例(一百七十七)
音视频·android16·音频进阶
liefyuan1 天前
【RV1106】rkipc:分析(一)
音视频
aqi001 天前
FFmpeg开发笔记(九十八)基于FFmpeg的跨平台图形用户界面LosslessCut
android·ffmpeg·kotlin·音视频·直播·流媒体