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缓存bit
int pos; // 当前bit的指针
int count;// bitset中bit的个数 // 构造函数
BitBuffer(); // 从bitset中取出一个bit
bool getBit(); // 从bitset中取出一个byte
uint8_t getByte(); // 向bitset中写入一个bit
void putBit(bool b); // 向bitset中写入一个byte
void 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(); // 取出一个byte
uint8_t getByte(); // 写入一个byte
void 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:
// 写入一个bit
void putBit(bool b); // 写入多个相同的bit
void 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 buffer
bool flush(); uint64_t getTotalCodeLength()
{
return bytes.pos;
} private:
BitBuffer bits;
ByteBuffer bytes;
}; class BitInputStream
{
public:
// 读取一个bit
bool getBit(); // 设置byte buffer
void 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 empty
BUFFER_END_SYMBOL, // end_symbol 编码的中止符,已经没有编码的数据
BUFFER_LACK, // buffer数据不足以完成解码,需要新的buffer
BUFFER_ENGOUGH // 数据足够,继续解码
}; // 检测buffer的状态
// 在每次解码开始前调用
BufferState BitInputStream::check()
{
// buffer中已无数据
if (bits.count <= 0 && bytes.pos >= bytes.length)
return BufferState::BUFFER_EMPTY; // buffer中还有数据,分为两种情况:不足64bits和有64bits
auto count = (bytes.length - bytes.pos) * 8 + bits.count; // buffer中的数据足够64位
if (count >= 64)
{
// bit buffer中数据就有64bits
if (bits.count >= 64)
{
if (bits.data.none()) // 64 bits 0
return BufferState::BUFFER_END_SYMBOL; // 编码中止符
else
return BufferState::BUFFER_ENGOUGH; // 数据足够继续解码
}
// bit buffer中的数据不足64bit
else
{ 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_LACK
else
{
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中的数据并不是全部的数据,需要继续读入数据已完成解码

本文所使用的源代码,

2017年的第一篇博文,完。

Golomb及指数哥伦布编码原理介绍及实现的更多相关文章

  1. 【视频编解码·学习笔记】8. 熵编码算法:基本算法列举 & 指数哥伦布编码

    一.H.264中的熵编码基本方法: 熵编码具有消除数据之间统计冗余的功能,在编码端作为最后一道工序,将语法元素写入输出码流 熵解码作为解码过程的第一步,将码流解析出语法元素供后续步骤重建图像使用 在H ...

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

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

  3. golomb哥伦布编码——本质上就是通过0来区分商和余数

    哥伦布编码是一个针对整数的变长编码方式,详细介绍可以看维基百科.这里简单介绍下: 哥伦布编码使用指定的整数 M 把输入的整数分成两部分:商数 q.余数 r. 商数当做一元编码,而余数放在后面做为可缩短 ...

  4. Base64图片编码原理,base64图片工具介绍,图片在线转换Base64

    Base64图片编码原理,base64图片工具介绍,图片在线转换Base64 DataURI 允许在HTML文档中嵌入小文件,可以使用 img 标签或 CSS 嵌入转换后的 Base64 编码,减少  ...

  5. LZ77压缩算法编码原理详解(结合图片和简单代码)

    前言 LZ77算法是无损压缩算法,由以色列人Abraham Lempel发表于1977年.LZ77是典型的基于字典的压缩算法,现在很多压缩技术都是基于LZ77.鉴于其在数据压缩领域的地位,本文将结合图 ...

  6. 液晶常用接口“LVDS、TTL、RSDS、TMDS”技术原理介绍

    液晶常用接口“LVDS.TTL.RSDS.TMDS”技术原理介绍 1:Lvds Low-Voltage Differential Signaling 低压差分信号 1994年由美国国家半导体公司提出之 ...

  7. Kylin系列之二:原理介绍

    Kylin系列之二:原理介绍 2018年4月15日 15:52 因何而生 Kylin和hive的区别 1. hive主要是离线分析平台,适用于已经有成熟的报表体系,每天只要定时运行即可. 2. Kyl ...

  8. 无线通信中FEC 编码原理及评价

    转自:http://blog.csdn.net/wiznet2012/article/details/7492146 大家好,前面我们给大家介绍了无线通信中FEC编码原理(1)和(2),今天继续献上F ...

  9. 哈夫曼编码(Huffman coding)的那些事,(编码技术介绍和程序实现)

    前言 哈夫曼编码(Huffman coding)是一种可变长的前缀码.哈夫曼编码使用的算法是David A. Huffman还是在MIT的学生时提出的,并且在1952年发表了名为<A Metho ...

随机推荐

  1. javascript 基础 onclick(this)用法介绍

    http://www.5idev.com/p-javascript_events_onclick.shtml --------------------------------------------- ...

  2. Paragraph 对象'代表所选内容、范围或文档中的一个段落。Paragraph 对象是 Paragraphs 集合的一个成员。Paragraphs 集合包含所选内容、范围或文档中的所有段落。

    Paragraph 对象'代表所选内容.范围或文档中的一个段落.Paragraph 对象是 Paragraphs 集合的一个成员.Paragraphs 集合包含所选内容.范围或文档中的所有段落. 方法 ...

  3. DHCP配置部分

    1.DHCP工作过程 dhcp discover \\客户端请求服务器器提供地址 dhcp offer \\服务器提供ip地址 dhcp request \\客户端租用ip地址 dhcp ack \\ ...

  4. MAC OSX 10.10 下安装PHP环境

    Apache和PHP已经在系统里面预装好了,只要你开启即可使用.这篇文章给大家介绍如何开启并设置好PHP开发环境. 1.开启默认Apache服务 打开终端命令行,输入如下命令就会开启Apache了.然 ...

  5. 高效判断奇偶性,利用位运算符&

    这种位运算判断奇偶性,在程序和数据库里面都是可以用的 public static bool isOdd(i) { return (i&1)!=0 } 最小奇数是:1   最小偶数是:0   所 ...

  6. java工程师联通XX面试题目

    什么是“长连接”和“短连接”? 所谓短连接指建立SOCKET连接后发送后接收完数据后马上断开连接,一般银行都使用短连接解释2长连接就是指在基于tcp的通讯中,一直保持连接,不管当前是否发送或者接收数据 ...

  7. (简单) POJ 2251 Dungeon Master,BFS。

    Description You are trapped in a 3D dungeon and need to find the quickest way out! The dungeon is co ...

  8. tableview的reloadData应注意

    http://blog.csdn.net/ouyangtianhan/article/details/7835041 http://stackoverflow.com/questions/160715 ...

  9. EF LINQ根据某个字段去除重复行

    1.ydc.GameScore.OrderByDescending(o => o.Score).GroupBy(ic => ic.UserPhone).Select(g => g.F ...

  10. uses crt;

    1.uses CRT,表示引用CRT.pas单元.CRT.pas单元是Pascal最重要的单元之一,主要用于字符界面的操作,里面内置了清屏.光标定位.删除行.调整字符亮度.前景色.背景色等功能函数.2 ...