在视频压缩技术中,压缩方式通常分为有损压缩 和无损压缩 两大类。有损压缩通过舍弃人眼不敏感的信息来获得更高的压缩比,而无损压缩则要求在解码后完全恢复原始数据,不允许任何信息丢失。无损压缩在医学影像、监控取证、工业检测、视频中间处理流程等场景中具有重要价值。
在视频无损压缩体系中,**熵编码(Entropy Coding)**是核心环节,而 Huffman 编码作为最经典、应用最广泛的熵编码方法之一,被广泛应用于 JPEG、MPEG、H.264、H.265(HEVC)等视频编码标准中。
视频无损压缩中的熵编码位置
在典型的视频编码流程中(以 H.264 为例),即便在"无损模式"下,也仍然会经历如下步骤:
- 帧内/帧间预测
- 残差计算(Prediction Error)
- 变换(无损模式下可跳过或使用整数变换)
- 熵编码(Entropy Coding)
可以看到,在无损压缩中,熵编码是决定最终压缩效率的关键步骤。Huffman 编码正是最早、也是最基础的熵编码方式之一。
Huffman 编码基本原理
3.1 信息熵与编码思想
Huffman 编码基于信息论中的**信息熵(Entropy)**概念:
- 出现概率越高的符号,应使用越短的码字;
- 出现概率越低的符号,使用较长的码字。
其目标是:
在不丢失任何信息的前提下,使平均码长最小化。
Huffman 编码是一种前缀码(Prefix Code),即任意一个码字都不是其他码字的前缀,从而保证了解码的唯一性。
3.2 Huffman 树构建过程
Huffman 编码的核心是构建 Huffman 树,基本步骤如下:
- 统计符号出现频率
对输入数据(如像素值、残差值、变换系数)进行统计,得到每个符号的出现概率。 - 初始化节点集合
每个符号作为一个独立节点,权值为其出现频率。 - 合并最小权值节点
从节点集合中选取权值最小的两个节点,合并为一个新节点,其权值为两者之和。 - 重复合并过程
不断重复上述步骤,直到只剩下一个根节点。 - 生成码字
从根节点出发,向左分支记为 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 编码通过为高频符号分配短码、低频符号分配长码,实现无损熵编码。在视频无损压缩中,常用于对预测残差和变换系数进行编码,能够在不丢失任何信息的前提下显著降低码率。