构建一个JPEG解码器(4):帧和比特流

之前,我讨论了由 JPEG 实现的霍夫曼压缩算法,以及 JPEG 编码器选择用于压缩的替换值的机制。图像本身,在基线熵编码的 JPEG 文件中,作为霍夫曼代码流存储在一次“扫描”中;代码是可变长度的,并且不一定在偶数字节边界处。

出于这个原因,需要在文件的字节和 JPEG 解码器看到的值之间引入某种队列:这允许将比特从文件的字节推入队列,并由解码器以不同的数量拉出。因此,可以将单个例程作为任何对可变宽度符号的请求的中央路由点。

事物队列,当以这种方式使用时,通常有两种操作模式:队列中有足够的事物供请求处理,或者在队列中有足够的事物处理之前必须进行额外的读取请求。在从文件读取的位级队列的情况下,正如我们在这里需要的那样,请求的位数是决定因素。在下面的第一个例子中,一个包含五位的队列被要求返回前三位,并且能够毫无问题地处理请求。

队列中有足够内容的请求处理
图 1:队列中有足够内容的请求处理

如果随后收到四位请求,则队列中没有足够的位来处理该请求,并且必须首先从 JPEG 文件中获取另一个完整字节(此处以红色显示)。

队列为空时的请求处理
图 2:队列为空时的请求处理

实现位队列

尝试编写如上所示的位队列的实现时会出现一个复杂情况:队列的输出端在左侧,如果使用一个连续值来存储队列,则左侧是最重要的一端最高值位。如果我们将队列划分为 32 位长,并定义一个 32 位值来保存其全部内容,则上述情况分解如下:

拉三个位

  • 请求比队列长度 5 短,不会从文件中读取。
  • 输出值是队列的前三位,向下移动 (32 - 3) 位成为输出值的后三位;
  • 队列左移三位,长度为 2。

再拉四位

  • 请求长于队列长度2:读入一个字节,上移(24-2)位加入队列;
  • 队列现在有 10 位长,不再需要读取字节;
  • 输出值是队列的前四位,向下移动 (32 - 4);
  • 队列左移四位,长度为 6。

在队列操作期间,下一个可用位始终位于连续队列变量的高值端。其实现可能如下所示。

jpeg.h:位分辨率文件阅读器的定义

class JPEG {
    private:
        // Read a number of bits from file
        u16 readBits(int);
};

jpeg.cpp:位分辨率文件阅读器

u16 JPEG::readBits(int len)
{
    // The number of bits left in the queue, and the value represented
    static int queueLen = 0;
    static u32 queueVal = 0;

    u8 readByte;
    u16 output;

    if (len > queueLen) {
        do {
            // Read a byte in, shift it up to join the queue
            readByte = fgetc(fp);
            queueVal = queueVal | (readByte << (24 - queueLen));
            queueLen += 8;
        } while (len > queueLen);
    }

    // Shift the requested number of bytes down to the other end
    output = ((queueVal >> (32 - len)) & ((1 << len) - 1));

    queueLen -= len;
    queueVal <<= len;

    return output;
}

JPEG SOF 段

如本系列的前一部分所述,一个 JPEG 文件最多可以定义 32 个霍夫曼代码表,每个表都在自己的DHT段中。JPEG 文件将对应于图像本身的数据保存在“帧”中,由“帧开始”段标头表示。SOF 头包含解码帧所需的部分信息,其结构如下表所示。

FieldValueSize (bytes)
Precision (the number of pixels in a JPEG block)81
Image heightUp to 655352
Image widthUp to 655352
ComponentsNumber of colour components1
For each component (in a YUV-colour file, three)
IDIdentifier for later use1
Sampling resolutionFor later examination1
Quantisation tableFor later examination1
表 1:SOF 报头的结构

从上表中可以看出,一些字段涉及我们尚未检查的操作(每个组件的采样分辨率和量化表)。为了完成 SOF 段处理程序,我们可以保留此信息以供以后使用。

一个“帧”由 SOF 报头和许多“扫描”组成;顾名思义,每次扫描都是对图像整个矩形的一次扫描。例如,在一个隔行扫描的 JPEG 文件中,单个帧会有多个扫描,每个扫描的分辨率都比上一个更高;在渐进式 JPEG 文件中,只有一次扫描包含图像的所有信息。由于本系列涉及为渐进式 JPEG 文件构建解码器,因此我们将专注于处理帧中的单次扫描。

事实证明,霍夫曼解码所需的信息,特别是使用哪个DHT表,是由帧中的每次扫描定义的,而不是由帧本身定义的。我们将在本系列的下一部分中更详细地查看扫描级别信息;现在,将我们对图像中颜色分量的表示与SOF数据分离就足够了,然后为一个分量定义确切的元数据。

结构定义

有了详细说明SOF标头结构的信息,构建段解析器以插入我们现有的代码就变得相对简单了。唯一的复杂之处在于 JPEG 中的多字节值以大端格式存储,这可能不一定是大整数的主机格式。有一组用于透明处理大端值的定义很有用,如下所示。

byteswap.h:大端值处理宏

/**
* Let's Build a JPEG Decoder
* Big-endian value handling macros
* Imran Nazar, May 2013
*/

#ifndef __BYTESWAP_H_
#define __BYTESWAP_H_

#if __SYS_BIG_ENDIAN == 1
#   define htoms(x) (x)
#   define htoml(x) (x)
#   define mtohs(x) (x)
#   define mtohl(x) (x)
#else
#   define htoms(x) (((x)>>8)|((x)<<8))
#   define htoml(x) (((x)<<24)|(((x)&0xFF00)<<8)|(((x)&0xFF0000)>>8)|((x)>>24))
#   define mtohs(x) (((x)>>8)|((x)<<8))
#   define mtohl(x) (((x)<<24)|(((x)&0xFF00)<<8)|(((x)&0xFF0000)>>8)|((x)>>24))
#endif

#endif//__BYTESWAP_H_

jpeg.h:SOF 头结构

// Prevent padding bytes from creeping into structures
#define PACKED __attribute__((packed))
 
class JPEG {
    private:
        // Information in the SOF header
        struct PACKED {
            u8  precision;
            u16 height;
            u16 width;
            u8  component_count;
        } sofHead;

        typedef struct PACKED {
            u8 id;
            u8 sampling;
            u8 q_table;
        } sofComponent;

        // Internal information about a colour component
        typedef struct PACKED {
            u8 id;
            // There is likely to be more data here...
        } Component;

        // The set of colour components in the image
        std::vector components;

        // The SOF segment handler
        int SOF();
};

jpeg.cpp:将控制权传递给段处理程序

int JPEG::parseSeg()
{
    ...

    switch (id) {
        // The SOF segment defines the components and resolution
        // of the JPEG frame for a baseline Huffman-coded image
        case 0xFFC0:
            size = READ_WORD() - 2;
            if (SOF() != size) {
                printf("Unexpected end of SOF segment\n");
                return JPEG_SEG_ERR;
            }
            break;

        ...
    }

    return JPEG_SEG_OK;
}

jpeg.cpp:SOF 段处理

int JPEG::SOF()
{
    int ctr = 0, i;

    fread(&sofHead, sizeof(sofHead), 1, fp);
    ctr += sizeof(sofHead);

    sofHead.width   = mtohs(sofHead.width);
    sofHead.height  = mtohs(sofHead.height);

    printf("Image resolution: %dx%d\n", sofHead.width, sofHead.height);

    for (i = 0; i < sofHead.component_count; i++) {
        sofComponent s;
        fread(&s, sizeof(sofComponent), 1, fp);
        ctr += sizeof(sofComponent);

        Component c;
        c.id = s.id;

        components.push_back(c);
    }

    return ctr;
}

下一次:最小编码单位

如上所述,渐进式JPEG文件中的图像帧被编码为一个扫描,由一系列块组成;根据图像中组件的采样分辨率,这些块可能大于 JPEG 算法的 8x8 像素基本块大小。在本系列的下一部分中,我将研究这些较大的单位与图像颜色分量之间的关系。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值