熵编码(算术+霍夫曼)编解码基础知识总结

在MPEG的TMC13模型中,对于surface point cloud compression,对block和vertices进行熵编码;对于lidar point cloud compression,需要对量化残差进行算术编码。这里对熵编码相关的知识进行了总结。


熵编码:

(1)https://blog.csdn.net/m0_37579288/article/details/79552526  (2)https://zhuanlan.zhihu.com/p/26454072

信息论中,Shannon发明了信息熵,信息熵是信息量的度量单位,信息学里的熵表示数据的无序度,熵越高,则包含的信息越多。

熵编码是一种无损编码方式。最常见熵编码有霍夫曼(Huffman)编码,算术编码,还有行程编码 (RLE)、基于上下文的自适应变长编码(CAVLC)、基于上下文的自适应二进制算术编码(CABAC)。

在图像(视频)压缩和点云压缩中经常用到熵编码,譬如,JPEG用的是Huffman和算术编码,而CAVLC、CABAC是HEVC中使用的两种编码方式。


算术编码:

原理解析:https://blog.csdn.net/u010029439/article/details/104147157

模拟算数编码步骤:https://blog.csdn.net/lkxiaolou/article/details/8781024

算数编码实验:https://blog.csdn.net/CSDNJay/article/details/46628603 MATLAB代码

算术编码和其他熵编码方式的不同在于,其他的熵编码通常是把输入的消息分割为符号,对每个符号进行编码,而算术编码是直接吧整个输入的消息编码为一个数,小数介于 [ 0.0 , 1.0 ) 区间。其大体过程为:

  1. 假设有一段数据需要编码,统计里面所有的字符和出现的次数。
  2. 将区间 [0,1) 连续划分成多个子区间,每个子区间代表一个上述字符, 区间的大小正比于这个字符在文中出现的概率 p。概率越大,则区间越大。所有的子区间加起来正好是 [0,1)。
  3. 编码从一个初始区间 [0,1) 开始,设置:,
  4. 不断读入原始数据的字符,找到这个字符所在的区间,比如 [ LH ),更新:
  5. 最后将得到的区间 [low, high)中任意一个小数以二进制形式输出即得到编码的数据。

举例说明:

(1)有一段原始数据“ARBER”,他们出现的概率和次数如下:

Symbol Times P
A 1 0.2
B 1 0.2
E 1 0.2
R 2 0.4

(2)将这几个字符的区间在 [0,1) 上按照概率大小连续一字排开,我们得到一个划分好的 [0,1)区间:
图片描述

(3)开始编码,初始区间是 [0,1)。注意这里又用了区间这个词,不过这个区间不同于上面代表各个字符的概率区间 [0,1)。这里我们可以称之为编码区间,这个区间是会变化的,确切来说是不断变小。我们将编码过程用下图完整地表示出来:
图片描述

可以看出编码区间逐渐变小。每次编码一个字符,就在现有的编码区间上,按照概率比例取出这个字符对应的子区间。例如一开始A落在0到0.2上,因此编码区间缩小为 [0,0.2),第二个字符是R,则在 [0,0.2)上按比例取出R对应的子区间 [0.12,0.2),以此类推。每次得到的新的区间都能精确无误地确定当前字符,并且保留了之前所有字符的信息,因为新的编码区间永远是在之前的子区间。最后我们会得到一个长长的小数,这个小数神奇地包含了所有的原始数据,不得不说这真是一种非常精彩的思想。

(4)解码:如果你理解了编码的原理,则解码的方法显而易见,就是编码过程的逆推。从编码得到的小数开始,不断地寻找小数落在了哪个概率区间,就能将原来的字符一个个地找出来。例如得到的小数是0.14432,则第一个字符显然是A,因为它落在了 [0,0.2)上,接下来再看0.14432落在了 [0,0.2)区间的哪一个相对子区间,发现是 [0.6,1), 就能找到第二个字符是R,依此类推。在这里就不赘述解码的具体步骤了。

代码:MATLAB

https://www.zhihu.com/question/32301828  介绍function cumsum()

涉及的MATLAB函数:cumsum()    find()    vpa()

%%算术编码
clc; 
a=['_','a','e','r','s','t']; 
s=input('字符串'); 
space=0;a1=0;e1=0;r1=0;s1=0;t1=0; 
for i=1:length(s)
    c=find(a==s(i));
    switch c 
        case 1 
            space=space+1; 
        case 2 
            a1=a1+1; 
        case 3 
            e1=e1+1; 
        case 4
            r1=r1+1; 
        case 5
            s1=s1+1;
        case 6
            t1=t1+1;
    end 
end
disp('_,a,e,r,s,t概率分别是');
p=[space,a1,e1,r1,s1,t1]/length(s) 
sp=cumsum(p);  %cumsum累计求和,sp=a1,a1+a2,a1+a2+a3,...
low=0;high=1;
range_length=high-low; 
for i=1:length(s) 
    c=find(a==s(i)); %find函数用来定位
    High=sp(c);
    Low=High-p(c);
    next_low=low+range_length*Low; 
    next_high=low+range_length*High;
    low=next_low;high=next_high;
    range_length=high-low; 
    disp([vpa(low,11),vpa(high,11)]) %vpa用于精度控制
end

在命令行窗口输入‘state_tree’,(注意加单引号),结果如下:

 

clc;
format long
p=[0.1,0.1,0.3,0.1,0.1,0.3];
s='_aerst';
high=cumsum(p);
low=high-p;
number=input('输入数值');
while(number)
        c=find((number>=low)&(number<high));
        disp(s(c));
        range_low=low(c);
        range=p(c);
        number_next=vpa(((number-range_low)/range),11);
        number=double(number_next);
end

 输出是state_treatttt.....由于取了精度,最后的结果会有误差。


霍夫曼编码:

(1)https://www.cnblogs.com/Jezze/archive/2011/12/23/2299884.html 哈夫曼编码与哈夫曼树

(2)https://blog.csdn.net/u014106644/article/details/90050977 霍夫曼编码原理和Java代码

(3)https://blog.csdn.net/xgf415/article/details/52628073 霍夫曼编码很详细的步骤,好评鸭

霍夫曼编码的具体步骤如下:

  1. 将信源符号的概率按减小的顺序排队。
  2. 把两个最小的概率相加,并继续这一步骤,始终将较高的概率分支放在右边,直到最后变成概率1。
  3. 画出由概率1处到每个信源符号的路径,顺序记下沿路径的0和1,所得就是该符号的霍夫曼码字。   
  4. 将每对组合的左边一个指定为0,右边一个指定为1(或相反)。

例如给定一段数据,统计里面字符的出现次数,生成哈夫曼树,我们可以得到字符编码集:

Symbol Times Encoding
a 3 00
b 3 01
c 2 10
d 1 110
e 2 111

图片描述

仔细观察编码所表示的小数,从0.0到0.111,其实就是构成了算数编码中的各个概率区间,并且概率越大,所用的bit数越少,区间则反而越大。概率越小的字符,用更多的bit去表示,这反映到概率区间上就是,概率小的字符所对应的区间也小,因此这个区间的上下边际值的差值越小,为了唯一确定当前这个区间,则需要更多的数字去表示它。我们仍以十进制来说明,例如大区间0.2到0.3,我们需要0.2来确定,一位足以表示;但如果是小的区间0.11112到0.11113,则需要0.11112才能确定这个区间,编码时就需要5位才能将这个字符确定。

哈夫曼编码的不同之处就在于,它所划分出来的子区间并不是严格按照概率的大小等比例划分的(节点的左右子树按照0.5/0.5的概率划分)。例如上面的d和e,概率其实是不同的,但却得到了相同的子区间大小0.125。可以将哈夫曼编码和算术编码在这个例子里的概率区间做个对比:

图片描述

代码:C语言

#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <iostream>

#define MAXBIT      100
#define MAXVALUE  10000
#define MAXLEAF     30
#define MAXNODE    MAXLEAF*2 -1

typedef struct
{
	int bit[MAXBIT];
	int start;
} HCodeType;        /* 编码结构体 */

typedef struct
{
	int weight;
	int parent;
	int lchild;
	int rchild;
	char value;
} HNodeType;        /* 结点结构体 */


/* 构造一颗哈夫曼树 */
void HuffmanTree(HNodeType HuffNode[MAXNODE], int n)
{
	/* i、j: 循环变量,m1、m2:构造哈夫曼树不同过程中两个最小权值结点的权值,
	x1、x2:构造哈夫曼树不同过程中两个最小权值结点在数组中的序号。*/
	int i, j, m1, m2, x1, x2;
	/* 初始化存放哈夫曼树数组 HuffNode[] 中的结点 */
	for (i = 0; i < 2 * n - 1; i++)
	{
		HuffNode[i].weight = 0;//权值 
		HuffNode[i].parent = -1;
		HuffNode[i].lchild = -1;
		HuffNode[i].rchild = -1;
		HuffNode[i].value = ' '; //实际值,可根据情况替换为字母  
	} /* end for */



	/* 输入 n 个叶子结点的权值 */
	for (i = 0; i < n; i++)
	{
		printf("Please input char of leaf node: ", i);
		scanf("%c", &HuffNode[i].value);
		getchar();
	} /* end for */

	for (i = 0; i < n; i++)
	{
		printf("Please input  weight of leaf node: ", i);
		scanf("%d", &HuffNode[i].weight);
		getchar();
	} /* end for */

	/* 构循环造 Huffman 树 */
	for (i = 0; i < n - 1; i++)
	{
		m1 = m2 = MAXVALUE;     /* m1、m2中存放两个无父结点且结点权值最小的两个结点 */
		x1 = x2 = 0;
		/* 找出所有结点中权值最小、无父结点的两个结点,并合并之为一颗二叉树 */
		for (j = 0; j < n + i; j++)
		{
			if (HuffNode[j].weight < m1 && HuffNode[j].parent == -1)
			{
				m2 = m1;
				x2 = x1;
				m1 = HuffNode[j].weight;
				x1 = j;
			}
			else if (HuffNode[j].weight < m2 && HuffNode[j].parent == -1)
			{
				m2 = HuffNode[j].weight;
				x2 = j;
			}
		} /* end for */

		/* 设置找到的两个子结点 x1、x2 的父结点信息 */
		HuffNode[x1].parent = n + i;
		HuffNode[x2].parent = n + i;
		HuffNode[n + i].weight = HuffNode[x1].weight + HuffNode[x2].weight;
		HuffNode[n + i].lchild = x1;
		HuffNode[n + i].rchild = x2;

		printf("x1.weight and x2.weight in round %d: %d, %d\n", i + 1, HuffNode[x1].weight, HuffNode[x2].weight);  /* 用于测试 */
		printf("\n");
	} /* end for */
} /* end HuffmanTree */


//解码 
void decodeing(char string[], HNodeType Buf[], int Num)
{
	int i, tmp = 0, code[1024];
	int m = 2 * Num - 1;
	char *nump;
	char num[1024];
	for (i = 0; i < strlen(string); i++)
	{
		if (string[i] == '0')
			num[i] = 0;
		else
			num[i] = 1;
	}
	i = 0;
	nump = &num[0];

	while (nump < (&num[strlen(string)]))
	{
		tmp = m - 1;
		while ((Buf[tmp].lchild != -1) && (Buf[tmp].rchild != -1))
		{
			if (*nump == 0)
			{
				tmp = Buf[tmp].lchild;
			}
			else tmp = Buf[tmp].rchild;
			nump++;
		}
	//	printf("123");
		printf("字符编号是:%d", tmp);
		printf("%c", Buf[tmp].value);//这里一直显示不出来
	}
}


int main(void)
{
	HNodeType HuffNode[MAXNODE];            /* 定义一个结点结构体数组 */
	HCodeType HuffCode[MAXLEAF], cd;       /* 定义一个编码结构体数组, 同时定义一个临时变量来存放求解编码时的信息 */
	int i, j, c, p, n;
	char pp[100];
	printf("Please input n:\n");
	scanf("%d", &n);
	HuffmanTree(HuffNode, n);

	for (i = 0; i < n; i++)
	{
		cd.start = n - 1;
		c = i;
		p = HuffNode[c].parent;
		while (p != -1)   /* 父结点存在 */
		{
			if (HuffNode[p].lchild == c)
				cd.bit[cd.start] = 0;
			else
				cd.bit[cd.start] = 1;
			cd.start--;        /* 求编码的低一位 */
			c = p;
			p = HuffNode[c].parent;    /* 设置下一循环条件 */
		} /* end while */

		/* 保存求出的每个叶结点的哈夫曼编码和编码的起始位 */
		for (j = cd.start + 1; j < n; j++)
		{
			HuffCode[i].bit[j] = cd.bit[j];
		}
		HuffCode[i].start = cd.start;
	} /* end for */


	/* 输出已保存好的所有存在编码的哈夫曼编码 */
	for (i = 0; i < n; i++)
	{
		printf("%d 's Huffman code is: ", i);
		for (j = HuffCode[i].start + 1; j < n; j++)
		{
			printf("%d", HuffCode[i].bit[j]);
		}
		printf(" start:%d", HuffCode[i].start);
		printf("\n");
	}

	printf("Decoding,Please Enter 0/1 code:\n");
	scanf("%s", &pp);
	decodeing(pp, HuffNode, n);
	getchar();
	return 0;
}

实验结果如下:【疑问】本来是输入0/1编码后,直接输出解码的结果,但解码部分最后一步printf("%c", Buf[tmp].value);不知为何一直显示不了,我看了好久,也不知道哪里有问题,枯了,请各位路过的道友知道怎么改的烦请在评论区纠正一下,谢谢啦~


ε=(´ο`*)))唉,做菜鸡好难,每天只会ctrl+c、ctrl+v,啥也不是。

猜你喜欢

转载自blog.csdn.net/yamgyutou/article/details/107654926