以下内容主要参考了《VP8 Data Format and Decoding Guide》第七章Boolean Entropy Decoder
7.1 Underlying Theory of Coding
在编码时,假设:a <= x < b,p是出现0的概率。那么,编0的时候上式变为:a <= x < a + ( p * (b - a) );编1的时候,上式变为:a + ( p * (b - a) ) <= x < b。
在解码时,假设:a <= x < b,p是出现0的概率。那么,如果a <= x < a + ( p * (b - a) ),说明应该输出0,且在输出后要更新上式;如果a + ( p * (b - a) ) <= x < b,说明应该输出1,且在输出后要更新上式。
理解用于编0的更新公式a <= x < a + ( p * (b - a) )和用于编1的更新公式a + ( p * (b - a) ) <= x < b可以这样记忆:
- 0 <= x < 1,且0出现的概率为p
- 当出现0时,更新为0 <= x < p。更新了上边界。
- 当出现1时,更新为p <= x < 1。更新了下边界。
7.2 Practical Algorithm Implementation
VP8使用0 <= p <= 255来表示概率,此时真实的概率就是p / 256(等价于右移8位)。在编码时并没有像上一节记录边界a和b。而是记录bottom(其实就是a)和range(就是b - a)。那么,p * (b - a)就可以表示为split = 1 + ( ( (range - 1) * prob ) >> 8 )。此时,1 <= split <= range - 1。
如果编0,那么bottom不动,range = split。参考:a <= x < a + ( p * (b - a) ),range = a + ( p * (b - a) ) - a = p * (b - a) = split。
如果编1,那么bottom = bottom + split,且range = range - split。参考:a + ( p * (b - a) ) <= x < b,range = b - a - ( p * (b - a) ) = range - split。
下面代码展示了二进制熵编码器的具体编码过程。代码和英文注释依然来自《VP8 Data Format and Decoding Guide》。中文注释是我加入的。
/* Encoding very rarely produces a carry that must be propagated
to the already-written output. The arithmetic guarantees that
the propagation will never go beyond the beginning of the
output. Put another way, the encoded value x is always less
than one. */
// 对前面已经写过的数据进行加一
// 考虑这样一个例子:0x 01 FF FF 08
// 当前在写最后的08。此时发现bottom已经大于等于 1 << 31,此时要向0x 01 FF FF加一。
// 最后的结果就是0x 02 FF FF
void add_one_to_output(uint8_t *q)
{
// *(--q),这里注意有指针移动操作
// 255就是FF。当前值是255的时候,需要向前进位,同时把FF置为0。
while (*--q == 255)
*q = 0;
// ++(*q)
++*q;
}
/* Main function writes a bool_value whose probability of being
zero is (expected to be) prob/256. */
void write_bool(bool_encoder *e, Prob prob, int bool_value)
{
/* split is approximately (range * prob) / 256 and,
crucially, is strictly bigger than zero and strictly
smaller than range */
uint32_t split = 1 + (((e->range - 1) * prob) >> 8);
if (bool_value)
{
e->bottom += split; /* move up bottom of interval */
e->range -= split; /* with corresponding decrease in range */
}
else
e->range = split; /* decrease range, leaving bottom alone */
while (e->range < 128)
{
e->range <<= 1;
// 等价于e->bottom >= (1 << 31)
if (e->bottom & (1 << 31)) /* detect carry */
// 因为下面要对e->bottom左移,而此时e->bottom >= (1 << 31),再左移一位会溢出。
// 所以,此时要向前面已经写过的bytes“进位”。add_one_to_output就是对前面已经写过的bytes进行加一操作的。
add_one_to_output(e->output);
// 编码器要输出的值不会每次输出一位,而是不断左移到bottom的高位。积累到一定时候一起处理。
e->bottom <<= 1; /* before shifting bottom */
// !( --(e->bit_count) )
// 当e->bit_count == 1时,下面的判断为真。也即,本次是e->bottom左移的第24次,或者是第32次、40次、48次......
if (!--e->bit_count)
{ /* write out high byte of bottom ... */
// *e->output = (uint8_t)(e->bottom >> 24);
// e->output++;
// 把当前output指向的地址赋值为bottom的高8位
// 再将output地址加一
*e->output++ = (uint8_t)(e->bottom >> 24);
e->bottom &= (1 << 24) - 1; /* ... keeping low 3 bytes */
e->bit_count = 8; /* 8 shifts until next output */
}
}
}