JPEG格式研究——(3)霍夫曼解码

因为霍夫曼编码以bit为单位,长度又不确定,读取时无法区分,JPEG采用了范式霍夫曼编码

读取并生成霍夫曼表

JPEG中DC系数和AC系数是分别进行编码将霍夫曼表保存在DQT中。

直接上代码解释可能更直接:

let mut code = 0usize;
let mut length = [0; 16];
for i in 0..16 {
    length[i] = data[offset + i + 1];
    for j in 0..length[i] as usize {
        if code >= (1 << (i + 2)) {
            return Err(HuffmanErrorType::InvalidCode(data[off + j], code));
        }
        map.insert(Binary::new(code, i + 1), data[off + j]);
        code += 1;
    }
    off += length[i] as usize;
    code <<= 1;
}

首先定义了一个变量code,用于计算对应的编码,根据范式霍夫曼编码的计算方法从0开始。因为霍夫曼编码的长度最大为16bit,所以用了长16字节的数组,保存了不同长度的编码个数

let mut code = 0usize;
let mut length = [0; 16];

然后依次读取不同长度的编码个数

for i in 0..16 {
    length[i] = data[offset + i + 1];
    ...
}

然后根据编码长度进行循环计算,相同长度的编码每计算出一个就将code+1。这里为了后面解码方便直接用map保存键值对。

Binary是一个保存了值和二进制长度的结构体,用于区分不同长度的二进制串

for j in 0..length[i] as usize {
    // 错误处理部分省略
    map.insert(Binary::new(code, i + 1), data[off + j]);
    code += 1;
}

然后在编码长度+1时将code左移一位

code <<= 1;

霍夫曼解码

选择霍夫曼表

霍夫曼表数量=颜色分量数*2,比如RGB和YCbCr都是3个颜色分量,而灰度则是1个颜色分量,颜色分量数以及每一个颜色分量的DC、AC系数解码所需的霍夫曼表编号都保存在SOF中。

选择出需要的DC、AC系数的霍夫曼表后,就可以开始解码了

解码

JPEG中将图像按8x8大小进行分块,所以都是以64个数为一组进行解码的,其中第一个数是DC系数,其余的63个则是AC系数

DC系数

DC系数需要首先读取一个经过霍夫曼编码的数据,这个数表示需要读取的bit长度。再以这一长度读取一个二进制串(未经过霍夫曼编码),如果长度为0则表示这里的数据就是0。

这一个二进制串的最高位是符号位,为1表示正数,为0表示负数,如果只有1位那就是只有符号位。然后要对符号位之外表示的数按位取反,按符号位得出正负得到DPCM编码

DPCM编码实际上也很简单,就是加上上一个DC系数就好了(如果是第一个DC系数则不用加或者加0)

代码如下:

let codeval = dc.huff.decode(bs)?;
let len = codeval as usize;
if len == 0 {
    code[0] = last_dc;
} else if len == 1 {
    code[0] = last_dc + bs.read(len)? as isize * 2 - 1; // 0 -> -1, 1 -> 1
} else {
    let sign = bs.read(1)?;
    let num = sign << (len - 1) | bs.read(len - 1)?;
    let result;
    if sign == 0 {
        result = -(((!num) & ((1 << len) - 1)) as isize); // Rust中按位取反是!有点不适应
    } else {
        result = num as isize;
    }
    code[0] = result + last_dc;
}

AC系数

先用霍夫曼解码出一个数,这个数的高4位表示0的个数,而低4位后面的数据的bit长度。其中有两种特殊情况:全为0则是EOB(End Of Block),直接结束AC系数解码,剩余的部分用0填充;高4位为1,低4为0则表示有连续16个0.

后面读取出的数据和DC系数解码的方式一样,先是1位符号位,后面跟着剩余位的数据。

代码如下:

let mut i = 1;
while i < 64 {
    let codeval = ac.huff.decode(bs)?;
    let zero = codeval >> 4;
    let len = (codeval & 0x0f) as usize;

    if len == 0 {
        if codeval == 0xf0 { // 连续16个0
            i += 16;
            continue;
        } else { // End Of Block,直接结束
            break;
        }
    } else if len == 1 {
        i += zero as usize;
        code[i] = bs.read(len)? as isize * 2 - 1; // 0 -> -1, 1 -> 1
    } else {
        let sign = bs.read(1)?;
        let num = sign << (len - 1) | bs.read(len - 1)?;
        let result;
        if sign == 0 {
            result = -(((!num) & ((1 << len) - 1)) as isize);
        } else {
            result = num as isize;
        }
        i += zero as usize;
        code[i] = result;
    }
    i += 1;
}

参考资料

博客园博客:JPEG解码——(4)霍夫曼解码 - OnlyTime_唯有时光 - 博客园 (cnblogs.com)

JPEG标准:Microsoft Word - T081E.DOC (w3.org)

一个Rust写的JPEG解码器:MROS/jpeg_tutorial: 跟我寫 JPEG 解碼器 (Write a JPEG decoder with me) (github.com)

友情链接

我学习过程中写的JPEG图片查看器:Ryan1202/my-tiny-jpeg-viewer: A Tiny Jpeg Viewer (github.com)

posted @ 2024-10-09 21:51  迷路的鹿1202  阅读(61)  评论(0编辑  收藏  举报