数字视频编码中的熵编码是一种无损压缩技术,它根据数据中各符号出现的概率进行编码,使得平均码长最短。在视频编码中,熵编码通常用于对量化后的变换系数进行编码,以进一步减少数据的大小。常见的熵编码方法包括哈夫曼编码、算术编码和游程编码。
一、哈夫曼编码
(一)哈夫曼编码步骤
哈夫曼编码是一种基于概率的熵编码方法,它为每个符号分配一个唯一的码字,使得整个编码的平均长度最小。哈夫曼编码的步骤如下:
- 初始化:统计每个字符在数据或文本中出现的频率。
- 构建优先队列:根据字符的频率,将它们放入一个优先队列中。这个队列会按照频率从小到大排序。
- 构建哈夫曼树:从优先队列中取出两个频率最低的节点,创建一个新的内部节点作为它们的父节点,这个新节点的频率是这两个子节点频率的和。将新创建的节点放回优先队列中。重复上述过程,直到优先队列中只剩下一个节点,这个节点就是哈夫曼树的根节点。
- 生成哈夫曼编码:从根节点开始,向左的路径代表0,向右的路径代表1。为每个字符生成一个唯一的编码,高频率的字符会有更短的编码。
- 压缩数据:使用生成的哈夫曼编码替换原始数据中的每个字符,得到压缩后的数据。
(二)哈夫曼编码步骤示例
为了更直观地展示哈夫曼编码的过程,我们可以通过一个图例来说明。以下是一个简化的哈夫曼编码过程图例:
初始数据出现次数频率: A: 1, B: 2, C: 4, D: 8, E: 16, F: 32
- 创建优先队列(按频率排序):
[A(1), B(2), C(4), D(8), E(16), F(32)] - 构建哈夫曼树:
- 取出A(1)和B(2),合并为节点AB(3)
- 队列更新:[C(4), D(8), E(16), F(32), AB(3)]
- 取出C(4)和D(8),合并为节点CD(12)
- 队列更新:[E(16), F(32), AB(3), CD(12)]
- 取出AB(3)和CD(12),合并为节点ABCD(15)
- 队列更新:[E(16), F(32), ABCD(15)]
- 取出E(16)和F(32),合并为节点EF(48)
- 队列更新:[ABCD(15), EF(48)]
- 取出ABCD(15)和EF(48),合并为根节点ABCDEF(63)
哈夫曼树:
ABCDEF(63)
/ \
ABCD(15) EF(48)
/ \ / \
AB(3) CD(12) E(16) F(32)
/ \ / \
A(1) B(2) C(4) D(8)
-
生成哈夫曼编码:
A: 1111, B: 1110, C: 110, D: 10, E: 0, F: 00 -
压缩数据:
原始数据:AAFBDCE
压缩后数据:1111 1111 00 1110 10 110 0
压缩后的数据序列包含了原始数据的全部信息,但是使用的位数更少,因为频率较高的字符(如 E 和 F)有更短的编码。这就是哈夫曼编码的压缩原理:频繁出现的字符使用较短的编码,不频繁出现的字符使用较长的编码,从而减少整个数据集的编码长度。
(三)利用MATLAB进行哈夫曼编码和解码
在 MATLAB 中,你可以使用内置的函数来执行哈夫曼编码和解码。MATLAB 提供了一个名为 huffman 的函数,它可以用于构建哈夫曼树,并为给定的字符串生成哈夫曼编码。此外,还有一个名为 decodestring 的函数,用于解码哈夫曼编码的字符串。
% 创建一个包含字符和它们频率的字典
chars = {'A', 'B', 'C', 'D', 'E', 'F'};
freq = [1, 2, 4, 8, 16, 32];
% 创建哈夫曼树并获取编码
[code, root] = huffman(chars, freq);
% 显示编码
disp('Huffman Coding:');
disp(code);
% 创建一个字符串用于编码和解码
input_string = 'ABACD';
% 对字符串进行哈夫曼编码
encoded_string = encode(input_string, root);
% 显示编码后的字符串
disp('Encoded String:');
disp(encoded_string);
% 对编码后的字符串进行哈夫曼解码
decoded_string = decodestring(encoded_string, root);
% 显示解码后的字符串
disp('Decoded String:');
disp(decoded_string);
按照离散,无记忆信源的无失真编码定理,在理想的情况下,哈夫曼编码的平均码长可以达到其理纶下限,也就是信源的熵,但这只有在每个信源符号的信息量都为整数时才成,即信源每个符号的概率分布均为2^-n(n为整数)。例如,当信源中的某个符号出现的概率为0.9时,其包含的自信息量为0.152 bit,但编码时却至少要分配1个码元的码字;又如,编码二值图像时,因为信源只有两种符号“0”和“1”,因此无论两种符号出现的概率如何分配,都将指定 1bit。所以,哈夫曼编码对于这种只包含两种符号的信源输出的数据一点也不能压缩。
二、算术编码
(一)算术编码步骤
算术编码是一种熵编码方法,与哈夫曼编码不同,它不需要为每个符号分配一个唯一的比特序列。算术编码通过在编码范围内为每个符号分配一个子范围来工作,这个范围通常是0到1之间的实数。编码过程涉及不断缩小这个范围,并且这个范围的上下界可以用来生成编码输出。
算术编码跳出了分组编码的范畴,它在编码时不是按符号编码,即不是用一个特定的码字与输入符号之间建立一一对应的关系,而是从整个符号序列出发,采用递推形式进行连续编码,用一个单独的算术码字来表示整个信源符号序列。它将整个符号序列映射为实数轴上[0,1)区间内的一个小区间,其长度等于该序列的概率。从小区间内选择一个具有代表性的二进制小数,作为实际的编码输出,从而达到高效编码的目的。不论是否为二元信源,也不论数据的概率分布如何,其平均码长均能逼近信源的熵。
算术编码过程是在[0,1)区间上划分子区间的过程,给定符号序列的算术编码步骤如下:
1)初始化:编码器将“当前区间”[low,high)设置为[0,1)。
2) 对每一个信源符号,分配一个初始编码子区[symbol_low,symbol_high),其长度与信源符号出现的概率成正比。当输入符号序列时,编码器在“当前区间”内按照每个信源符号的初始编码子区间的划分,以一定的比例再细分,选择对应于当前输入符号的子区间,并使它成为新的“当前区间”[low,high)。
3)重复步骤2),最后输出的“当前区间”[low,high)的左端点值 low 就是该给定符号序列的算术编码。
(二)算术编码步骤示例
让我们通过一个简单的例子来展示算术编码的过程。假设我们有以下符号和它们的概率:
符号 | 概率 |
---|---|
A | 0.4 |
B | 0.3 |
C | 0.2 |
D | 0.1 |
使用算术编码对符号序列进行编码时,我们需要根据每个符号的概率来不断缩小编码范围。以下是对符号序列 “ABACD” 进行算术编码的详细步骤,其中符号 A、B、C 和 D 的概率分别为 0.4、0.3、0.2 和 0.1。
步骤 1: 初始化
设置初始的编码范围为 [0, 1);设置初始的低位和高位边界为 0 和 1。
步骤 2: 编码第一个符号 ‘A’
‘A’ 的概率是 0.4,所以 ‘A’ 的编码范围是 [0, 0.4),更新低位和高位边界为 0 和 0.4。
步骤 3: 编码第二个符号 ‘B’
‘B’ 的概率是 0.3,在当前的编码范围内,‘B’ 的编码范围是 [0, 0.4) 的前 0.3 * 0.4 = 0.12 长度,即 [0, 0.12),更新低位和高位边界为 0 和 0.12。
步骤 4: 编码第三个符号 ‘A’
‘A’ 的概率是 0.4,在当前的编码范围内,‘A’ 的编码范围是 [0, 0.12) 的后 0.4 * 0.12 = 0.048 长度,即 [0.092, 0.12),更新低位和高位边界为 0.092 和 0.12。
步骤 5: 编码第四个符号 ‘C’
‘C’ 的概率是 0.2,在当前的编码范围内,‘C’ 的编码范围是 [0.092, 0.12) 的后 0.2 * 0.028 = 0.0056 长度,即 [0.1176, 0.12),更新低位和高位边界为 0.1176 和 0.12。
步骤 6: 编码第五个符号 ‘D’
‘D’ 的概率是 0.1在当前的编码范围内,‘D’ 的编码范围是 [0.1176, 0.12) 的,= 0.00024 长度,即 [0.11776, 0.118),更新低位和高位边界为 0.11776 和 0.118。
现在,我们可以选择任何在 [0.11776, 0.118) 范围内的数作为编码输出。例如,我们可以选择 0.1178 作为编码结果。在实际应用中,为了传输和存储,这个数通常会转换为二进制表示,并且可能需要额外的操作来确保解码器能够准确地重建原始的浮点数。
(三)算术解码步骤示例
算术解码是算术编码的逆过程,它根据编码值和符号的概率表来重建原始的符号序列。以下是一个详细的算术解码步骤,假设我们已经有了编码值和符号的概率表。
符号: A B C D
概率: 0.4 0.3 0.2 0.1
并且我们收到了一个编码值,例如 0.1178。
步骤 1: 初始化
设置初始的解码范围为 [0, 1),设置初始的低位和高位边界为 0 和 1。
步骤 2: 解码第一个符号
根据符号的概率,我们可以确定每个符号的解码范围。
‘A’ 的解码范围是 [0, 0.4)。
‘B’ 的解码范围是 [0.4, 0.7)。
‘C’ 的解码范围是 [0.7, 0.9)。
‘D’ 的解码范围是 [0.9, 1)。
由于 0.1178 在 [0.7, 0.9) 范围内,所以第一个符号是 ‘C’。更新解码范围为 [0.7, 0.9)。
步骤 3: 解码第二个符号
现在我们需要根据新的解码范围和符号的概率来解码第二个符号。为了做到这一点,我们需要计算新的解码范围与原始解码范围的比率,并应用符号的概率来找到新的解码范围。假设我们计算出的比率是 0.1178 / 0.7 = 0.167。根据这个比率,我们可以确定第二个符号的解码范围。由于 0.167 在 [0.4, 0.7) 范围内,所以第二个符号是 ‘B’。更新解码范围为 [0.4, 0.7)。
步骤 4: 重复步骤 3,直到所有符号都被解码。
(四)利用MATLAB进行算术编码和解码
以下是一个简单的 MATLAB 函数示例,它展示了如何对一个简单的符号序列进行算术编码和解码。
function arithmetic_encoding_decoding
% 符号及其概率
symbols = {'A', 'B', 'C', 'D'};
probabilities = [0.4, 0.3, 0.2, 0.1];
% 要编码的符号序列
input_message = 'ABACD';
% 编码
encoded_value = arithmetic_encode(input_message, symbols, probabilities);
% 解码
decoded_message = arithmetic_decode(encoded_value, symbols, probabilities);
disp('Encoded Value:');
disp(encoded_value);
disp('Decoded Message:');
disp(decoded_message);
end
function encoded_value = arithmetic_encode(message, symbols, probabilities)
low = 0;
high = 1;
for i = 1:length(message)
% 找到当前符号在 symbols 中的索引
index = find(symbols == message(i), 1);
% 更新 low 和 high
range = high - low;
high = low + range * probabilities(index);
low = low + range * sum(probabilities(1:index-1));
end
% 选择编码范围内的任意值作为编码结果
encoded_value = (low + high) / 2;
end
function decoded_message = arithmetic_decode(value, symbols, probabilities)
decoded_message = '';
low = 0;
high = 1;
while true
% 计算当前值在哪个符号的范围内
for i = 1:length(symbols)
% 更新 low 和 high
range = high - low;
next_high = low + range * probabilities(i);
next_low = low + range * sum(probabilities(1:i-1));
% 如果值在当前符号的范围内,添加到解码消息中
if value >= next_low && value < next_high
decoded_message = [decoded_message symbols{i}];
% 更新 low 和 high 为当前符号的范围内
low = next_low;
high = next_high;
break;
end
end
% 如果解码消息的长度等于原始消息的长度,停止解码
if length(decoded_message) == length(value)
break;
end
end
end
三、游程编码
游程编码(Run-Length Encoding,RLE)是一种简单的无损数据压缩方法,它通过记录连续重复的字符或像素的数量来压缩数据。以下是一个详细的游程编码步骤:
- 初始化计数器和数据指针
①初始化一个计数器(通常是整数类型),用于记录连续重复的字符或像素的数量。
②初始化一个数据指针,用于指向数据序列的当前位置。 - 遍历数据序列
从数据序列的开始位置开始,使用数据指针遍历整个序列。 - 检查重复字符或像素
在当前数据指针的位置,检查接下来的连续字符或像素是否与前一个字符或像素相同。如果相同,则将计数器加一。如果不同,则记录当前的重复次数(即计数器的值),并将计数器重置为1。 - 输出编码结果
对于每个重复字符或像素,将重复次数和字符或像素本身输出为编码结果。如果数据序列结束,则输出剩余的重复次数(如果有的话)。 - 压缩数据:
将编码结果转换为更适合存储或传输的格式(例如,转换为二进制或十六进制)。 - 解码(如果需要):
在解码过程中,使用相反的步骤来恢复原始数据。从编码结果开始,遍历每个重复次数和字符或像素,并重复该字符或像素的次数,直到到达数据的末尾。
(二)游程编码步骤示例
举个例子,假设我们有一个字符串 “AAABBBCCDD”,我们可以使用游程编码来压缩它。
初始数据: A A A A B B B C C D D
编码过程: 4A 3B 2C 2D
在这个例子中,我们首先遇到了四个连续的 ‘A’,然后是三个连续的 ‘B’,接着是两个连续的 ‘C’,最后是两个连续的 ‘D’。将这些信息组合起来,我们得到了编码结果 “4A3B2C2D”。
解码过程是编码的逆过程。从编码结果开始,我们首先遇到 “4A”,这意味着我们需要重复 ‘A’ 四次,得到 “AAAA”。然后我们遇到 “3B”,这意味着我们需要重复 ‘B’ 三次,得到 “AAAABBB”。接着我们遇到 “2C”,这意味着我们需要重复 ‘C’ 两次,得到 “AAAABBBCC”。最后我们遇到 “2D”,这意味着我们需要重复 ‘D’ 两次,得到最终的解码结果 “AAAABBBCCDD”。
(三)利用MATLAB进行游程编码和解码
在 MATLAB 中,你可以使用内置的函数来执行游程编码和解码。MATLAB 提供了一个名为 rle 的函数,它可以用于对一维数组进行游程编码。此外,还有一个名为 rledecode 的函数,用于解码游程编码的数组。
% 创建一个包含字符和它们频率的字典
chars = {'A', 'B', 'C', 'D', 'E', 'F'};
freq = [1, 2, 4, 8, 16, 32];
% 对字符串进行游程编码
encoded_string = rle(input_string);
% 显示编码后的字符串
disp('Encoded String:');
disp(encoded_string);
% 对编码后的字符串进行游程解码
decoded_string = rledecode(encoded_string);
% 显示解码后的字符串
disp('Decoded String:');
disp(decoded_string);