二、算术编码的基本概念
算术编码属于熵编码的一种重要的类型,其作用同变长编码等熵编码方法类似,用于压缩输入数据中的统计冗余,并且使用算术编码的压缩同样是无损压缩。
在本系列第1篇中讨论了典型的变长编码方法——哈夫曼编码。包括哈夫曼编码在内的变长编码具有一个共同特点,就是针对每一个码元不同的概率,分配每个码元对应的码字。通常针对概率更高的码元,分配长度更短的码字;针对概率较低的码元,分配长度较长的码字。通过这种不同长度码字的分配使得整体输入信息的平均码字长度小于定长编码,达到数据压缩的效果。
另一方面,由于采用这种变长度的编码方法,变长编码存在一项难以突破的性能瓶颈:即使是某一个输入信源的概率再高,也至少需要1个bit的码字。这种特性限制了编码性能进一步向信源熵逼近,也导致了无法进一步提升整体的压缩性能。
算术编码的引入可以有效解决这个问题。算术编码的思想同变长编码完全不同,算术编码无法针对每一个输入码元准确细分出对应的码字。另外,变长编码可以针对短输入信息进行编码,而算术编码对类似一两个码元的输入信息通常没有任何意义,因为生成的码流长度通常更长。
在算术编码执行的过程中,始终需要两个区间来计算,这两个区间即信源的概率区间和码流的编码区间。
三、概率区间与编码区间
信源的概率区间用于表示输入信源的码元之间的概率关系。假设输入的信源为二进制信源,只存在0和1两个元素,那么元素0和1的概率之和为100%。如果0和1的概率比为7:3,那么概率区间可以用下图表示:
与概率区间按照码元的概率分割不同,编码区间为了标记输出码流,将自身区间递归二等分,分割点的左右分别表示一个码元0和1。每一次分割都增加一个bit输出。编码区间可以用下图表示:
四、一个简单的算术编码执行过程
在一次算术编码的执行前,为简便起见,首先假设输入的信源为0/1的二进制信源,0和1的概率比为7:3。即二者的概率为:
p(0) = 0.7;
p(1) = 0.3;
假设输入的待编码信息为[0, 0, 1],在编码每一个符号时,都需要对概率区间进行分割,并通过与编码区间进行比较,判断是否输出码流的bit位,以及更新编码下一个符号的上下文。
在第一次进行分割之后,概率区间和编码区间的关系如下图所示:
第一个字符的概率区间分割之后,不满足输出码流的条件,因此结束这个字符的编码,准备开始编码下一个字符。
第二个字符依然为0,此时概率区间和编码区间的关系为:
此时概率区间已经完全处于编码区间的下半区,因此应输出一个bit-0。而后,编码区间的下半区间扩展2倍到原有的完整编码区间继续进行下一个编码。该过程由下图所示:
我们设定的最后一个待编码符号为1,因此最后一次分割概率区间,选取上30%作为结果。此时的概率区间分割结果如下图所示:
由图中可看出,概率区间已经完全处于编码区间的上半区,因此需要输出一个bit-1,并循环进行如下操作,直到概率区间长度大于编码区间总长的一半:
1.检测概率区间的长度和位置;
2.根据概率区间特性,输出0或1,或记录待输出位;
3.概率区间随编码区间归一化。
当循环结束后,对每一个码元编码的区间分割过程结束。
对码元的区间分割结束后,整个编码过程并未完全结束,还需要一个重要的收尾过程,即处理最终的概率区间。最终的概率区间的处理方法为:
1.检查最终概率区间下限的位置;
2.若该下限位置小于整体编码区间的1/4分割点,输出bit-0,否则输出bit-1。
原文链接:https://blog.csdn.net/shaqoneal/article/details/79167491
#ifndef _ARITHMETIC_CODER_H_
#define _ARITHMETIC_CODER_H_
//定义编码区间的节点(4个)
#define FIRST_QUATER 2500
#define HALF_VALUE 5000
#define THIRD_QUATER 7500
#define FULL_VALUE 10000
//定义一个算术编码器
typedef struct ARITHMETIC_ENCODER
{
//定义区间
long low;
long high;
//若概率区间小于半区长度但跨过编码区间的中点,则记录下一个待输出的比特
long bits_to_follow;
} ARITHMETIC_ENCODER;
int Encode_one_symbol(unsigned int symbol);
int Encode_stop();
#endif
#include "ArithmeticCoder.h"
#include "stdio.h"
/*
|0---------------------7000|----------10000|
输入0,在0-7000,输入1,在7000-10000,概率是7:3
*/
static unsigned int probability_model[3] = { 0, 7000, 10000 };
static unsigned int upper_bound = 10000;
//上下节点+待输出比特
ARITHMETIC_ENCODER encoder = { 0, FULL_VALUE, 0 };
void output_bits(int bit)
{
printf("%d", bit);
while (encoder.bits_to_follow > 0)
{
//持续输出待输出位的相反值,直到没有带输出位
printf("%d", !bit);
encoder.bits_to_follow--;
}
}
int Encode_one_symbol(unsigned int symbol)
{
//当前区间长度
long range = encoder.high - encoder.low + 1;
//
encoder.high = encoder.low + (range * probability_model[symbol + 1] / upper_bound) - 1;
encoder.low = encoder.low + (range * probability_model[symbol] / upper_bound);
while (1)
{
//当前的高点小于中点,输出0
if (encoder.high < HALF_VALUE)
{
// Output 0;
output_bits(0);
}
//当前低点大于中点,输出1,并重新归一化区间
else if (encoder.low >= HALF_VALUE)
{
// Output 1;
output_bits(1);
//输出1后,值在上半区,不能直接高低点直接*2,会导致范围超过10000,
//所以先减去半区间的值。在最后再做*2操作进行归一化
encoder.high -= HALF_VALUE;
encoder.low -= HALF_VALUE;
}
//低点大于1/4分界点,高点小于3/4分界点,不确定输出0还是1
else if (encoder.low >= FIRST_QUATER && encoder.high < THIRD_QUATER)
{
//记录待输出比特,并归一化,需要先减去1/4,再乘2
//[2500-7500]-->[0-10000]
encoder.bits_to_follow++;
encoder.high -= FIRST_QUATER;
encoder.low -= FIRST_QUATER;
}
//区间长度大于半区长度,直接退出
else
{
break;
}
//最后再归一化
encoder.low = 2 * encoder.low;
encoder.high = 2 * encoder.high + 1;
}
return 0;
}
int Encode_stop()
{
//待输出自增1
encoder.bits_to_follow++;
//如果低点小于前1/4值,输出0
if (encoder.low < FIRST_QUATER)
{
output_bits(0);
}
//否则输出1
else
{
output_bits(1);
}
printf("\n");
return 0;
}
#include "stdafx.h"
#include "ArithmeticCoder.h"
int main() {
unsigned int input_data[] = { 0, 1, 1 };
//计算输入数组中有多少个元素
unsigned int input_length = sizeof(input_data) / sizeof(unsigned int);
for (int idx = 0; idx < input_length; idx++)
{
//编码每一个元素
Encode_one_symbol(input_data[idx]);
}
Encode_stop();
return 0;
}