多媒体编码和压缩算法

314 阅读8分钟

无损压缩

信息论基础

  • 决策量
    在有限数目的互斥事件集合中,决策量是事件数的对数。
    H0=log(n)H_0=log(n),n为事件数。
  • 信息量
    假如事件A出现的概率为P(A), A事件的信息量为log2(1/P(A))=log2P(A)log_2(1/P(A))=-log_2P(A)

概率越小,不确定性越大,信息量越大


  • 信息论中,熵是信息量的度量。
    数据压缩技术中,熵指非冗余且不压缩的数据量的度量,单位是bit。表示为H(X)=pilog(pi),X=x1,x2,...,xn,p(xi)=1H(X)=-\sum p_ilog(p_i), 且 X={x_1,x_2,...,x_n},\sum p(x_i)=1
  • 数据冗余量
    R=H0HR=H_0-H
  • 平均码长
    编码数据需要的位数和字符长度的比值
  • 压缩比
    压缩前后编码数据的平均码长的比值
  • 理论压缩比
    编码前的平均码长和熵的比值
  • 编码效率
    熵和编码后的平均码长的比值

统计编码

统计编码是给已知统计信息的符号分配代码的数据无损压缩方法。

香农-范诺编码

  • 原理 image.png
  • 举例

image.png

image.png

image.png

image.png

霍夫曼编码

“从下到上”的熵编码方法。
根据给定数据集中各元素所出现的频率来压缩数据的一种统计压缩编码方法。这些元素(如字母)出现的次数越多,其编码的位数就越少。广泛用在JPEG, MPEG, H.26X等各种信息编码标准中。

  • 原理 image.png

词典编码

动态词典编码

LZ77

  • 概念
    • 前向缓冲存储器:存放从编码位置到输入数据流结束的字符序列的存储器
    • 滑动窗口:包含W个字符的窗口,起始状态为W个空字符。
    • 输出格式:三元组(指针位置,匹配长度,未匹配的第一个字符)
  • 原理

执行步骤:

  1. 编码位置设置到输入数据流的初始位置
  2. 查找窗口中的最长匹配串
  3. 以(指针位置,匹配长度,未匹配的第一个字符)的格式输出
  4. 如果前向缓冲存储器不为空,也就是说还有没有编码的字符,就把编码位置和窗口向后移动匹配长度加一个字符,然后返回到步骤2。
  • 示例 W=5,完成下列字符串的LZ77编码
位置12345678910
字符AABCBBABCA
  • 编码
    起初窗口中没有字符,前向缓冲区中 A 进入窗口;再次匹配时从位置2开始,A 与窗口中的 A 匹配,此时窗口中A在第五位,故输出(5,1,B),接着窗口后移1+1=2位;前向缓冲区中字符为C,未匹配,故输出(0,0,C),窗口后移0+1=1位;前向缓冲区中在窗口中可以找到匹配项,且窗口中:_ AABC匹配位置为4,故输出(4,1,B),窗口后移两位;前向缓冲区中此时可以匹配的字符串为ABC,故输出(1,3,A)。 70D94420ED863DE757551BB148A6BACF.png
步骤输出
1(0,0,A)
2(5,1,B)
3(0,0,C)
4(4,1,B)
5(1,3,A)
  • 解码
    根据编码结果,按顺序进行解码。例如(5,1,B),就是与滑动窗口的第五位匹配一位,第一个不匹配的字符为B,故把AB放入窗口中。 3A7559CCA5A1B6A0611B8079AE996421.png

LZSS

  • 提出 LZ77算法输出有冗余信息,如当窗口中没有匹配的字符串时输出为(0, 0, C)
  • 原理

核心思想:如果匹配串的长度比指针本身的长度长,就输出指针,否则输出真实的字符。
执行步骤:

  1. 把编码位置置于输入数据流的开始位置
  2. 在前向缓冲存储器中查找与窗口中最长的匹配串,并设置Pointer=匹配串位置,Length=匹配串长度。
  3. Length>=给定的阈值:输出指针,编码位置移动Length个字符;否则,输出前向缓冲存储器中的第一个字符,然后把编码位置向前移动一个字符。
  4. 前向缓冲存储器不为空时,继续执行步骤2。
  • 示例 74A9F9B09A6E72161ED84B1BD6693D03.png
  • 解码
    与LZ77类似。

静态词典编码

LZ78

思想:维护一个全局的词典,不断地从字符流中提取新的字符串,通俗的理解为新词条,然后用词条的码字表示这个词条。压缩数据体现在对字符流的编码变成对码字的编码,生成码字流。

流程概述:开始编码时词典为空,此时编码器输出一个表示空字符串的特殊码字0,并把当前字符C添加到词典中作为一个由一个字符组成的字符串(未找到匹配字符串的的情况操作与之类似);在词典中已经存入一些字符串之后,我们令当前正在匹配的字符串结构为:前缀P(已处理字符串)+当前字符C,假如已经在词典中,那么我们用C来扩展这个前缀直到获得一个在词典中没有的字符串为止;此时输出表示当前前缀P的码字和字符C,并把P+C加入字典中,开始处理字符流中的下一个字符。

def encode(text):
    '''
    输入:待编码字符串
    输出:码字列表,字符列表,词典 (W,C)分开存储
    '''
    mydict = dict()
    index = 1
    lstNumbers = []
    lstLetters = []
    p=""
    for c in text:
        p_c=p+c
        if p_c in mydict.values():
            p=p_c#P+C在词典里就用C扩展P
        else:
            mydict[index]=p_c#不在词典里就把p+c添加到词典
            if p=="":lstNumbers.append(0)
            else: 
                L_V=list(mydict.values())
                key_index=L_V.index(p)
                L=list(mydict.keys())
                lstNumbers.append(L[key_index])#前缀P在词典中的位置
            lstLetters.append(c)
            index+=1
            p=""#前缀赋值为空
    return lstNumbers, lstLetters, mydict#码字,字符,词典
    
def decode(lstNumbers, lstLetters, mydict):
    '''
    输入:码字列表,字符列表,词典 (W,C)分开存储
    输出:原字符串
    '''
    i = 0
    while i < len(lstNumbers):
        if (lstNumbers[i] != 0):    
            #print(list(mydict.keys())[list(mydict.values()).index(lstNumbers[i])], end="")
            print(mydict[lstNumbers[i]],end='')
        print(lstLetters[i], end="")
        i = i+1
    print('\n')

LZW算法

思路:LZW采用贪心策略,每一次分析都要串行的检查来自字符流的字符串,从中分解出已经识别的最长的字符串,也就是已经在词典中出现的最长的前缀。用已知的前缀加上下一个输入的字符C也就是当前字符作为判断该前缀的扩展字符,形成新的扩展字符串,判断新的串是否在词典中,是就继续读入C,不是就加入词典,分配码字。

流程:
1.初始化:所有不同的单字符加入词典,当前前缀P为空;
2.前字符C=字符流中的下一个字符;
3.判断前缀=P+C是否在词典中: 在词典中P=P+C;不在词典中则,前缀P的码字输出到码字流;把前缀也就是P+C加入词典;P=C,更新前缀
4.判断字符流中是否还有字符需要编码
注:每个输出的码字都对应词典中的一个词条,因为只有出现新的字符串的时候才输出码字。感觉思路可能不太规范,导致需要考虑编码解码最后一个字符的特殊情况

def encode(text):
    mydict = dict()
    # prepare the initial mydict 
    output = []
    # write your encoding code here... 
    i=1
    count=1#计数器,防止最后一个字符串在词典,不能输出
    #初始化词典
    for c in text:
        if c not in mydict.values():
            mydict[i]=c
            i+=1
    p=""#前缀
    for c in text:
        p_c=p+c
        if p_c in mydict.values():
            p=p_c
            if count==len(text):#是不是最后一个
                output.append(list(mydict.keys())[list(mydict.values()).index(p_c)])#防止最后一个在字典中匹配了但是没有输出码字
        else:
            mydict[i]=p_c
            output.append(list(mydict.keys())[list(mydict.values()).index(p)])#此时存入(输出)前缀p在词典中的key值,也就是p的编码位置
            i+=1
            p=c
            if count==len(text):#是不是最后一个
                output.append(list(mydict.keys())[list(mydict.values()).index(c)])#防止最后一个在字典中不匹配了但是没有输出码字
        count+=1
    print(mydict)
def decode(output, mydict):
    i = 0
    while i < len(output):
        #print(list(mydict.keys())[list(mydict.values()).index(output[i])], end="")
        print(mydict[output[i]],end='')
        i = i+1
    print('\n')

有损压缩

常用于压缩多媒体数据(音频、视频、图片),尤其常用于流媒体以及互联网电话领域。

  • 基本概念
    • 量化

    指将信号的连续取值(或者大量可能的离散取值 )近似为有限多个(或较少的)离散值的过程。是用有限的离散量代替无限的连续模拟量的多对一的映射操作。

    • 量化器

image.png

  • 预测编码

预测编码(Predictive coding)利用前面已经出现了的符号来预测目前的符号,然后将实际上的符号与预测符号比较得到预测误差,将此误差编码并送出。

    • 基本思想
  1. 建立一个数学模型
  2. 利用以往的样本数据对新样本值进行预测
  3. 将预测值与实际值相减
  4. 对其差值进行编码
    注:预测编码不一定是有损压缩,关键点在于差值是否进行量化!
    • 示例 image.png
  • 变换编码

  • 变换编码是进行一种函数变换,经过数学转换后映射至另一值域后再进行编码处理;
  • 不直接对空域相关信号编码,而是首先将空域图像信号映射变换到另一个正交矢量空间(变换域或频域),产生一系列变换系数,然后对这些变换系数进行处理编码。常用于音频信号编码和图像/视频信号编码;
  • 变换编码经常与量化一起使用,进行有损数据压缩。
  • 在视频和音频信号数字化后,变换编码更常用从最常见的JPEG静止图像压缩标准MPEG等 运动图像压缩标准,都使用了变换编码。
  • 最常用的变换是离散余弦变换,其次还有小波变换、Hadamard变换等等。离散余弦变换在性能上接近K-L变换(KarhunenLoève变换),能够很好的实现能量集中,广泛的应用于几乎所有的视频压缩标准中。

-原理

image.png

  • JPEG压缩编码