算术编码的数据压缩
算术编码是无损和有损数据压缩算法中常用的一种算法。
这是一种熵编码技术,其中常见符号比罕见符号用更少的比特进行编码。与诸如霍夫曼编码之类的众所周知的技术相比,它具有一些优势。本文将详细描述CACM87算术编码的实现,让您很好地了解实现它所需的所有细节。
从历史的角度来看,这篇文章是我20多年前写的一篇关于算术编码的数据压缩文章的更新。这篇文章发表在多布博士期刊的印刷版上,这意味着为了避免过多的页数,我们进行了大量的编辑。特别是,多布博士的文章结合了两个主题:算术编码的描述,以及使用PPM(部分匹配预测)进行压缩的讨论。
因为这篇新文章将在网上发布,空间考虑不再是一个重要因素,我希望这能让我公正地对待算术编码的细节。PPM本身就是一个有价值的话题,将在后面的文章中讨论。我希望这项新的努力,虽然冗长得令人恼火,但将是对我在1991年想做的主题的彻底解释。
我认为理解算术编码的最好方法是将其分为两部分,我将在本文中使用这个想法。首先,我将描述算术编码是如何工作的,使用标准C++数据类型实现的常规浮点算术。这允许实现一个完全可以理解但有点不切实际的实现。换句话说,它是有效的,但它只能用于编码非常短的消息。
文章的第二部分将描述一种实现,在该实现中,我们切换到对无界二进制数进行特殊类型的数学运算。这本身就是一个有点令人难以置信的话题,所以如果你已经理解了算术编码,它会有所帮助——你不必为同时学习两件事而烦恼。
最后,我将介绍用现代C++编写的工作示例代码。它不一定是世界上优化程度最高的代码,但它是可移植的,很容易添加到现有项目中。它应该非常适合学习和试验这种编码技术。
基本原理
关于算术编码,首先要了解的是它产生了什么。算术编码采用由符号(几乎总是八位字符)组成的消息(通常是一个文件),并将其转换为大于或等于零且小于1的浮点数。这个浮点数字可能很长——实际上,整个输出文件都是一个长数字——这意味着它不是传统编程语言中习惯使用的普通数据类型。我的算法实现必须从头开始,一点一点地创建这个浮点数,同样地,一点地读入并解码它。
这个编码过程是逐步完成的。当文件中的每个字符都被编码时,一些比特将被添加到编码的消息中,因此随着算法的进行,它会随着时间的推移而建立起来。
关于算术编码,需要理解的第二件事是,它依赖于一个模型来表征它正在处理的符号。该模型的工作是告诉编码器给定消息中字符的概率是多少。如果模型给出了消息中字符的准确概率,那么它们将被编码为非常接近最优。如果模型歪曲了符号的概率,那么编码器实际上可能会扩展消息,而不是压缩它!
using System;
namespace Legalsoft.Truffer
{
/// <summary>
/// compression by arithmetic coding
/// </summary>
public class Arithcode
{
private int NWK { get; } = 20;
private int nch { get; set; }
private int nrad { get; set; }
private int ncum { get; set; }
private int jdif { get; set; }
private int nc { get; set; }
private int minint { get; set; }
private int[] ilob { get; set; }
private int[] iupb { get; set; }
private int[] ncumfq { get; set; }
public Arithcode(int[] nfreq, int nnch, int nnrad)
{
this.nch = nnch;
this.nrad = nnrad;
this.ilob = new int[NWK];
this.iupb = new int[NWK];
this.ncumfq = new int[nch + 2];
if (nrad > 256)
{
throw new Exception("output radix must be <= 256 in Arithcode");
}
minint = (int)(int.MaxValue / nrad);
ncumfq[0] = 0;
for (int j = 1; j <= nch; j++)
{
ncumfq[j] = ncumfq[j - 1] + Math.Max(nfreq[j - 1], 1);
}
ncum = ncumfq[nch + 1] = ncumfq[nch] + 1;
}
public void messageinit()
{
jdif = (int)(nrad - 1);
for (int j = NWK - 1; j >= 0; j--)
{
iupb[j] = nrad - 1;
ilob[j] = 0;
nc = (int)j;
if (jdif > minint)
{
return;
}
jdif = (int)((jdif + 1) * nrad - 1);
}
throw new Exception("NWK too small in arcode.");
}
public void codeone(int ich, byte[] code, ref int lcd)
{
if (ich > nch)
{
throw new Exception("bad ich in Arithcode");
}
advance(ich, code, ref lcd, 1);
}
public int decodeone(byte[] code, ref int lcd)
{
int ja = (byte)code[lcd] - ilob[nc];
for (int j = nc + 1; j < NWK; j++)
{
ja *= (int)nrad;
ja += (byte)code[lcd + j - nc] - ilob[j];
}
int ihi = (int)(nch + 1);
int ich = 0;
while (ihi - ich > 1)
{
int m = (int)((ich + ihi) >> 1);
if (ja >= multdiv(jdif, ncumfq[m], (int)ncum))
{
ich = (int)m;
}
else
{
ihi = m;
}
}
if (ich != nch)
{
advance(ich, code, ref lcd, -1);
}
return ich;
}
public void advance(int ich, byte[] code, ref int lcd, int isign)
{
int jh = multdiv(jdif, ncumfq[ich + 1], (int)ncum);
int jl = multdiv(jdif, ncumfq[ich], (int)ncum);
jdif = jh - jl;
arrsum(ilob, iupb, jh, NWK, (int)nrad, nc);
arrsum(ilob, ilob, jl, NWK, (int)nrad, nc);
int j = nc;
for (; j < NWK; j++)
{
if (ich != nch && iupb[j] != ilob[j])
{
break;
}
if (isign > 0)
{
code[lcd] = (byte)ilob[j];
}
lcd++;
}
if (j + 1 > NWK)
{
return;
}
nc = j;
for (j = 0; jdif < minint; j++)
{
jdif *= (int)nrad;
}
if (j > nc)
{
throw new Exception("NWK too small in arcode.");
}
if (j != 0)
{
for (int k = nc; k < NWK; k++)
{
iupb[k - j] = iupb[k];
ilob[k - j] = ilob[k];
}
}
nc -= j;
for (int k = (int)(NWK - j); k < NWK; k++)
{
iupb[k] = ilob[k] = 0;
}
return;
}
public int multdiv(int j, int k, int m)
{
return (int)((ulong)j * (ulong)k / (ulong)m);
}
public void arrsum(int[] iin, int[] iout, int ja, int nwk, int nrad, int nc)
{
int karry = 0;
for (int j = (int)(nwk - 1); j > nc; j--)
{
int jtmp = ja;
ja /= nrad;
iout[j] = iin[j] + (jtmp - ja * nrad) + karry;
if (iout[j] >= nrad)
{
iout[j] -= nrad;
karry = 1;
}
else
{
karry = 0;
}
}
iout[nc] = iin[nc] + ja + karry;
}
}
}