2017年的第一篇博文。

本文主要有以下三部分内容:

  • 介绍了Golomb编码,及其两个变种:Golomb-Rice和Exp-Golomb的基本原理
  • C++实现了一个简单的BitStream库,能够方便在bit流和byte数字之间进行转换
  • C++实现了Golomb-Rice和Exp-Golomb的编码,并进行了测试。

在文章的最后提供了本文中的源代码下载。

Golomb编码的基本原理

Golomb编码是一种无损的数据压缩方法,由数学家Solomon W.Golomb在1960年代发明。Golomb编码只能对非负整数进行编码,符号表中的符号出现的概率符合几何分布(Geometric Distribution)时,使用Golomb编码可以取得最优效果,也就是说Golomb编码比较适合小的数字比大的数字出现概率比较高的编码。它使用较短的码长编码较小的数字,较长的码长编码较大的数字。

Golomb编码是一种分组编码,需要一个正整数参数m,然后以m为单位对待编码的数字进行分组,如下图:

对于任一待编码的非负正整数N,Golomb编码将其分为两个部分:所在组的编号GroupID以及分组后余下的部分,GroupID实际是待编码数字N和参数m的商,余下的部分则是其商的余数,具体计算如下:
\[ \begin{array}{c} q = N / m \\ \\ r = N \% m \end{array} \]
对于得到的组号q使用一元编码(Unary code),余下部分r则使用固定长度的二进制编码(binary encoding)。

一元编码(Unary coding)是一种简单的只能对非负整数进行编码的方法,对于任意非负整数num,它的一元编码就是num个1后面紧跟着一个0。例如:

num Unary coding
0 0
1 10
2 110
3 1110
4 11110
5 111110

其编解码的伪代码如下:

UnaryEncode(n) {while (n > 0) {WriteBit(1);n--;}WriteBit(0);
}UnaryDecode() {n = 0;while (ReadBit(1) == 1) {n++;}return n;
}

使用一元编码编码组号也就是商q后,对于余下的部分r则有根据编码数字大小的不同有不同的处理方法。

  • 如果参数m是2的次幂(这也是下面将要介绍的Golomb-Rice编码),则使用取r的二进制表示的低\(\log_2(m)\)位,作为r的码字
  • 如果参数m不是2的次幂,如果m不是2的次幂,设\(b = \lceil{\log_2(m)}\rceil\)
    1. 如果\(r < 2^b - m\),则使用b-1位的二进制编码r。
    2. 如果\(r \geqq 2^b -m\),则使用b位二进制对\(r+2^b-m\)进行编码

总结,设待编码的非负整数为N,Golomb编码流程如下:

  • 初始化正整数参数m
  • 取得组号q以及余下部分r,计算公式为:\(q = N / m,r = N \% m\)
  • 使用一元编码的方式编码q
  • 使用二进制的方式编码r,r所使用位数的如下:
    • 如果参数m是2的次幂(这也是下面将要介绍的Golomb-Rice编码),则使用取r的二进制表示的低\(\log_2(m)\)位,作为r的码字。
    • 如果参数m不是2的次幂,如果m不是2的次幂,设\(b = \lceil{\log_2(m)}\rceil\)
      1. 如果\(r < 2^b - m\),则使用b-1位的二进制编码r。
      2. 如果\(r \geqq 2^b -m\),则使用b位二进制对\(r+2^b-m\)进行编码

说明:

  • \(\lceil a \rceil\) 大于a的最小整数 ceil运算
  • \(\lfloor a \rfloor\) 小于a的最大整数 floor运算

Golomb-Rice 编码

Golomb-Rice是Golomb编码的一个变种,它给Golomb编码的参数m添加了个限制条件:m必须是2的次幂。这样有两个好处:

  • 不需要做模运算即可得到余数r,r = N & (m - 1)
  • 对余数r编码更为简单,只需要取r二进制的低\(\log_2(m)\)位即可。

则Golomb-Rice的编码过程更为简洁:

  • 初始化参数m,m必须为2的次幂
  • 计算q和r,q = N / m ; r = N & (m - 1)
  • 使用一元编码编码q
  • 取r的二进制位的低\(\log_2(m)\)位作为r的码字。

解码过程如下:

bool b;
uint64_t unary = 0;
b = bitStream.getBit();
while (b)
{unary++;b = bitStream.getBit();
}std::bitset<64> bits;
bits.reset();
for (int i = 0; i < k; i++)
{b = bitStream.getBit();bits.set(i, b);
}N  = unary * m + bits.to_ulong();

Exponential Golomb 指数哥伦布编码

Rice的编码方式和Golomb的方法是大同小异的,只是选择m必须为2的次幂。而Exp-Golomb则有了一个很大的改进,不再使用固定大小的分组,而使组的大小呈指数增长。如下图:

Exp-Golomb的码元结构是:** [M zeros prefix] [1] [Offset] **,其中M是分组的编号GroupID,1可以看着是分隔符,Offset是组内的偏移量。

Exp-Golomb需要一个非负整数K作为参数,称之为K阶Exp-Golomb。其中当K = 0时,称为0阶Exp-Golomb,目前比较流行的H.264视频编码标准中使用的就是0阶的Exp-Golomb,并且可以将任意的阶数K转为0阶Exp-Golomb编码。

首先来看下0阶Exp-Golomb编码,如下图:

上图是0阶Exp-Golomb编码的前几个组的分组情况,可以看出编号为m的组,其组内的最小元素的值是\(2 ^ m - 1\),也就是说对于非负整数N,其在编号为m的组内的充要条件是:\(2^m-1 \leq N \leq 2^{m+1}-1\)。所以可以由如下公式计算得到组号m以及组内的偏移量Offset
\[ \begin{array}{r} m = \lfloor{log_2{(num+1)}}\rfloor \\ \\ Offset = num + 1 - 2^m \end{array} \]
有了组号以及组内的偏移量后,其编码就比较简单了,具体过程如下:

  • 首先使用公式计算组号m,\(m = \lfloor{log_2{(num+1)}}\rfloor\)
  • 对组号m进行编码,连续写入m个0,最后写入一个1作为结束。
  • 计算组内偏移量offset,\(Offset = num + 1 - 2^m\)
  • 取offset二进制形式的低m位作为offset码元

0阶Exp-Golomb的编码后的长度是:\(2*m + 1\),其解码过程和上面的Rice码类似,读入bit流,是0则继续,1则停止,然后统计0的个数m;接着读入m位的bit,就是offset,最后解码后的数值是:\(N = 2^m - 1 + offset\)。

k阶Exp-Golomb

前面提到任意的k阶Exp-Golomb可以转换为0阶Exp-Golomb进行求解,这是为何呢。Exp-Golomb的组的大小实际上是呈2的指数增长,不同的参数k,实际控制的是起始分组的大小,具体是什么意思呢。

  • k = 0,其组的大小为1,2,4,8,16,32,...
  • k = 1,其组的大小为2,4,8,16,32,64,...
  • k = 2,其组的大小为4,8,16,32,64,...
  • ...
  • k = n,其组的大小为\(2^n,2^{n+1},\cdots\)

不同的k造成了其起始分组的大小不同,所以对于任意的k阶Exp-Golomb编码都可以转化为0阶,具体如下:
设待编码数字为N,参数为k

  • 使用0阶Exp-Golomb编码 \(N + 2^k - 1\)
  • 从第一步的结果中删除掉高位的k个0

以上的算法描述来自: https://en.wikipedia.org/wiki/Exponential-Golomb_coding

在搜索得到中文资料中,对于K阶Exp-Golomb的算法描述大多如下:

  • 将num以二进制的形式表示(若不足k位,则在高位补0),去掉其低k位(若刚好是k位,则为0)得到数字n
  • 计算n + 1的最低有效位数lsb,则M = lsb - 1。就是prefix中0的个数
  • 将第1步中去掉的k位二进制串放到(n + 1)的低位,得到[1][INFO]

其实现以及描述都不如wikipedia,故在下面的实现部分使用的是Wikipedia的方法。
在资料搜集的过程中,对于Exp-Golomb算法描述不止上述的两种,还有其他的形式,但都是殊途同归,也许得到的编码是不一样的,但是其编码的长度却是一样的,也就没有过多的计较。
最后附上k = 0,1,2,3时前29个数字的编码:

注意1之前的0的个数就是该数字所在的组的编号,同一组内的编码长度是相同的。

实现

通过上面的描述可以发现,Golomb编码的实现是很简单的,唯一的难点在于bit的操作。编码过程是将对bit进行操作,然后拼凑为byte,写入buffer;解码则是相反的过程,读取byte转化为bit stream,操作一个个的bit。具体来说就是以下两个功能:

  • 将bit流转换为byte数组
  • 将byte数组转换为bit流

而在C/C++中最小的数据类型也是8位的byte,这就造成了对bit的进行操作有一定的难度,好在C++中std::bitset结构能够在一定成都上简化对bit的操作。

BitBuffer / ByteBuffer

首先实现一个底层的库,实现bit流和byte之间的转换。在Golomb编码中,对bit和byte的操作只需要简单的get/put操作,因此封装了两个结构体BitBufferByteBuffer,具体的声明如下:

//
//
// Bits buffer
//  将bytes转化为bit stream时,在该buffer中缓存待处理的bit
//
/
struct BitBuffer
{std::bitset<bit_length> data; // 使用bitset缓存bitint                     pos;  // 当前bit的指针int                     count;// bitset中bit的个数// 构造函数BitBuffer();// 从bitset中取出一个bitbool getBit();// 从bitset中取出一个byteuint8_t getByte();// 向bitset中写入一个bitvoid putBit(bool b);// 向bitset中写入一个bytevoid putByte(uint8_t b);
};//
// Bytes buffer
//
///
struct ByteBuffer
{uint8_t *data;   // Byte数据指针uint64_t pos;    // 当期byte的指针uint64_t length; // 数据长度uint64_t totalLength; // 总的放入到 byte buffer中的字节数// 构造函数ByteBuffer();// 取出一个byteuint8_t getByte();// 写入一个bytevoid putByte(uint8_t b);// 设置byte数组void setData(uint8_t *buffer, int len);
};
  • BitBuffer是一个bit的缓存,无论是将bit流转换为byte还是将byte转换为bit流,都将bit放在此结构体中进行缓存。
  • ByteBuffer用来管理byte数组的缓存

这两个结构体中只向上层提供简单的get/put方法,不做任何的逻辑判断。也就是说只要调用了get方法就一定会有数据返回,调用了put方法就一定有空间存放数据。

BitStream

在编码时,需要将得到的bit流以byte的形式写出;解码则是将byte数组以bit流的形式读入。这就需要两种类型的bitstream:BitOutputStreamBitInputStream,其声明如下:


//
// Bit Output Stream
//  将bit stream转化为byte数组
//  这里也只提供功能,至于byte缓存满的处理放到编码器中处理
//class BitOutputStream
{;
public:// 写入一个bitvoid putBit(bool b);// 写入多个相同的bitvoid putBit(bool b, int num);// 设置数据数组void setBuffer(uint8_t *buffer, int len);void resetBuffer();/*判断byte buffer中是可用的bit长度*/uint64_t freeLength();// Flush bit buffer to byte bufferbool flush();uint64_t getTotalCodeLength(){return bytes.pos;}private:BitBuffer bits;ByteBuffer bytes;
};class BitInputStream
{
public:// 读取一个bitbool getBit();// 设置byte buffervoid setBuffer(uint8_t *buffer, int len);BufferState check();private:BitBuffer bits;ByteBuffer bytes;
};

编码时需要BitOutputStream将bit流转换为byte数组,也就是个putBit的过程,需要注意的一点是在编码结束的时候需要调用方法flush,该函数有两个功能:

  • 将BitBuffer中缓存的bit刷新到byte数组中
  • 写入编码的编码终止符。编码终止符在解码过程中是一个很重要的判断标志,这里假定Golomb编码后码元的最大长度为64位,所以可设编码终止符为:连续64bits的0。在解码时,要判断接下来的是不是编码终止符。
  • 将编码后输出的字节数填充为8(8 bytes,64 bits)的倍数,在解码时以8 bytes为单位进行解码,并且每次判断是不是编码终止符时也需要至少8 bytes。

编码/解码

有了BitStream的支持后,编解码过程是很简单的。

编码

每次编码前,首先计算编码后码元的长度,如果byte缓存空间不足以存放整个码元,则将byte buffer填充满后,剩余的部分,在bitset中缓存。返回false,指出缓存已满,需要处理缓存中的数据后才能继续编码或者更换一个新的Byte buffer存放编码后的数据.

bool GolombEncoder::encode(uint64_t num)
{uint64_t q = num >> k;uint64_t r = num & (m - 1);auto len = q + 1 + k; // 编码后码元的长度/*不会判断缓存是否为满,直接向里面放,不足的话缓存到bit buffer中*/bitStream.putBit(1, q);bitStream.putBit(0);for (int i = 0; i < k; i++){bitStream.putBit(static_cast<bool>(r & 0x01));r >>= 1;}return bitStream.freeLength() >= len; // 空间足够,存放编码后的码元则返回true;否则返回false
}

上述代码以Golomb-Rice编码为例。在putBit时候的不会判断缓存是否够用,直接存放,如果Byte Buffer不足以存放本次编码的bits,则将Byte Buferr填充满后,余下的bits在BitBuffer中缓存,然后返回false,告诉调用者byte buffer已经填满,可以处理当前buffer的数据后调用resetBuffer后继续编码;也可以直接更换一个新的byte buffer。

解码

在每次解码前,先要调用check方法来判断byte buffer的状态,byte buffer中有以下几种状态

  • 空,数据已读取完
  • 编码终止符,buffer中的数据是编码终止符,解码结束
  • 数据不足,buffer中的数据不足以完成本次解码,需要读取新的buffer
  • 数据足够,继续解码

check的实现如下:

enum BufferState
{BUFFER_EMPTY, // buffer emptyBUFFER_END_SYMBOL, // end_symbol 编码的中止符,已经没有编码的数据BUFFER_LACK, // buffer数据不足以完成解码,需要新的bufferBUFFER_ENGOUGH // 数据足够,继续解码
};// 检测buffer的状态
// 在每次解码开始前调用
BufferState BitInputStream::check()
{// buffer中已无数据if (bits.count <= 0 && bytes.pos >= bytes.length)return BufferState::BUFFER_EMPTY;// buffer中还有数据,分为两种情况:不足64bits和有64bitsauto count = (bytes.length - bytes.pos) * 8 + bits.count;// buffer中的数据足够64位if (count >= 64){// bit buffer中数据就有64bitsif (bits.count >= 64){if (bits.data.none()) // 64 bits 0return BufferState::BUFFER_END_SYMBOL; // 编码中止符elsereturn BufferState::BUFFER_ENGOUGH; // 数据足够继续解码}// bit buffer中的数据不足64bitelse{if (!bits.data.none())return BufferState::BUFFER_ENGOUGH;int count = ((64 - bits.count) / 8 + 1);int index = 0;while (index < count){auto b = bytes.data[bytes.pos + index];index++;if (b != 0)return BufferState::BUFFER_ENGOUGH;}return BUFFER_END_SYMBOL;}}// buffer中数据不足64位,不进行解码,// 将byte buffer中的数据取出放在bit buffer后,返回BUFFER_LACKelse{while (bytes.pos < bytes.length){auto b = bytes.getByte();bits.putByte(b);}return BufferState::BUFFER_LACK;}
}

check的过程有些复杂,但代码中的注释已足够清晰,这里就不再详述了。

Golomb-Rice的解码过程如下:

/
//
// 解码
// 在每次解码前需要check buffer的状态,根据不同的状态决定解码是否继续
//
///
BufferState GolombDecoder::decode(uint64_t& num)
{auto state = bitStream.check();// buffer中数据足够,进行解码if (state == BufferState::BUFFER_ENGOUGH){bool b;uint64_t unary = 0;b = bitStream.getBit();while (b){unary++;b = bitStream.getBit();}std::bitset<64> bits;bits.reset();for (int i = 0; i < k; i++){b = bitStream.getBit();bits.set(i, b);}num = unary * m + bits.to_ulong();}return state;
}

解码完成后会返回当前byte buffer的状态,

  • 状态是BUFFER_END_SYMBOL,则解码过程已经完成
  • 状态是BUFFER_EMPTY,byte buffer没有设置
  • 状态是BUFFER_LACK,byte buffer中的数据不足以完成一次解码,需要读入新的数据
  • 状态是BUFFER_ENGOUGH,byte buffer中的数据足够,继续下一次的解码

测试

仍然以Golomb-Rice编码为例,测试代码如下

    GolombEncoder encoder(m);encoder.setBuffer(buffer, 1024);ofstream ofs;ofs.open("golomb.gl", ios::binary);for (int i = 0; i < length; i++){auto b = encoder.encode(nums[i]);if (!b){cout << "Lack of buffer space,write the data to file" << endl;cout << "reset buffer" << endl;ofs.write((const char*)buffer, encoder.getToalCodeLength());encoder.resetBuffer();break;}}encoder.close();ofs.write((const char*)buffer, encoder.getToalCodeLength());ofs.close();cout << "Golomb finished coding" << endl;
  • 实例编码器时,需要设定编码的参数m和以及存放编码后数据的buffer;
  • 编码时,判断编码的的返回值,如果为true则继续编码,为false则buffer已满,将buffer写入文件后,resetBuffer继续编码。
  • 编码结束后,调用close方法,写入编码终止符,并将整个编码后的数据填充为8的倍数。

下面代码Golomb-Rice的解码调用过程

    ifstream ifs;ifs.open("golomb.gl", ios::binary);memset(buffer, 0, 1024);ifs.read((char*)buffer, 664);ofstream encodeOfs;encodeOfs.open("encode.txt");GolombDecoder decoder(m);decoder.setBuffer(buffer, 1024);uint64_t num;auto state = decoder.decode(num);int index = 0;while (state != BufferState::BUFFER_END_SYMBOL){encodeOfs << num << endl;state = decoder.decode(num);index++;}ifs.close();encodeOfs.close();cout << "decode finished" << endl;

编码是也需要根据返回的状态,来处理byte buffer,在上面已详述。

总结

终于完成了这篇博文,本文主要对Golomb编码进行了一个比较详尽的描述,包括Golomb编码的两个变种:Golomb-Rice和Exp-Golomb。在编码实现部分,难点有三个:

  • byte数组和bit流之间的转换
  • 需要一个唯一的编码终止符
  • 解码时,byte buffer中剩余数据不足以完成一次解码

针对上述问题,做了如下工作:

  • 实现了一个简单的BitStream库,能够方便在bit流和byte数组之间进行转换
  • 对编码后的码元长度做了一个假设,其最长长度不会超过64位,这样就使用64比特的0作为编码的终止符
  • 在编码的时,会将编码后的总字节数填充为8的倍数,解码的过程中就以8字节为单位进行,当byte buffer中的数据不足8字节时,可以判定当前buffer中的数据并不是全部的数据,需要继续读入数据已完成解码

本文所使用的源代码,

  • Github https://github.com/brookicv/GolombCode
  • CSDN http://download.csdn.net/detail/brookicv/9740838

2017年的第一篇博文,完。

Golomb及指数哥伦布编码原理介绍及实现相关推荐

  1. 八、H.264中的熵编码基本方法、指数哥伦布编码

    GitHub代码地址:点击这里 本节视频免费 1. H.264中的熵编码基本方法 在成功从NAL Unit中获取到语法元素的码流之后,接下来就是对语法元素的码流进行解析.根据我们在前面的博文中所讲述的 ...

  2. 【H.264/AVC视频编解码技术详解】八、 熵编码算法(2):H.264中的熵编码基本方法、指数哥伦布编码

    <H.264/AVC视频编解码技术详解>视频教程已经在"CSDN学院"上线,视频中详述了H.264的背景.标准协议和实现,并通过一个实战工程的形式对H.264的标准进行 ...

  3. 【编解码】从零开始写H264解码器(4) 熵编码之指数哥伦布编码

    1. 引言 经过上一章的学习,我们学会看描述子.这时候我们就会发现,在语法中,除了简单的 u(n),i(n) 这种读取固定长度的二进制解析方法之外,用的更多的还有ue(v),se(v)这些. 只有学会 ...

  4. 【H264/AVC 句法和语义详解】(五):Exp-Golomb指数哥伦布编码(理论篇)

    版权声明:本文为博主原创文章,未经博主允许不得转载.    https://blog.csdn.net/u011399342/article/details/80472399 本篇隶属于文集:< ...

  5. H.264中的熵编码算法(主讲指数哥伦布编码)

    以下文章参考于殷文杰的博客. https://yinwenjie.blog.csdn.net/article/details/52301584 1 熵编码基本概念 1)"熵"这一概 ...

  6. 指数哥伦布编码 java_H.264学习笔记6——指数哥伦布编码

    一.哥伦布码 哥伦布码就是将编码对象分能成等间隔的若干区间(Group),每个Group有一个索引值:Group Id. >对于Group Id采用二元码编码: >对于Group内的编码对 ...

  7. 【H.264/AVC视频编解码技术】第六章【指数哥伦布编码】

    H264中语法元素描述符 指数哥伦布 (Exponential-Golomb) 熵编码 指数哥伦布编码同哈夫曼编码一样,都是变长编码. 二者的显著区别: 信源相关性:哈夫曼编码依赖于信源的概率分布:指 ...

  8. [熵编码] 指数哥伦布编码

    规定语法元素的编解码模式的描述符如下: 比特串: b(8):任意形式的8比特字节(就是为了说明语法元素是为8个比特,没有语法上的含义) f(n):n位固定模式比特串(其值固定,如forbidden_z ...

  9. 熵编码之指数哥伦布编码(六)

    1.指数哥伦布编码(Exponential-Golomb coding) 指数哥伦布编码概述 指数哥伦布编码是一种无损数据压缩方法.Exp-Golomb编码是一种可变长前缀码, 其硬件实现简单, 无需 ...

最新文章

  1. III USP Freshmen ContestH. MaratonIME gets candies
  2. POJ 1936 All in All
  3. iOS 自定义Cell按钮的点击代理事件
  4. HTMLCSSJavaScript个人入门自学笔记
  5. 有关于腾讯地图服务端IP定位接口的获取当前城市的处理
  6. 线程中sleep导致崩溃是一种错觉
  7. 如何在51单片机实现日程提醒(生日闹钟)
  8. php红包退回通知,php红包
  9. 一切钻空子的行为最后都会惨淡收场
  10. Scrum 计划、站会、评审、回顾会议介绍
  11. OpenCV3入门(五)图像的阈值
  12. 面试记录-苏宁(客服研发中心)
  13. java long 随机 正数_java中如何产生随机正负数
  14. Android 开发中的SSL pinning
  15. Lab12_Environment_Variable
  16. 【OpenCV】在没有安装OpenCV的电脑运行OpenCV程序
  17. 沈师 数据库原理 PTA 选择题答案
  18. 这家日本餐厅第一!2022年度亚洲50最佳餐厅榜单出炉,上海、深圳、香港均有上榜...
  19. pcb焊盘尺寸设计尺寸参考
  20. 三步建立自己的电影网站 2 (使用Cpolar发布电影网站)

热门文章

  1. Java基础学习总结(11)——重载与重写
  2. 使用rancher 搭建docker集群
  3. C#编程(五十三)----------字典DictionaryTKey,TValue
  4. MySQL之check、optimize和analyze
  5. FastDFS分布式文件系统的安装及配置
  6. android ORM 框架 search
  7. php 安全基础 第一章简介 原则
  8. 江西理工大学计算机管理技术期末复习(wangzhendong)网络管理与维护
  9. 一个popup弹窗实现思路--(基于mintui分析)
  10. svn命令行 批量添加(add)所有新增文件