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

Posted 妇男主任

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了构建一个JPEG解码器:帧和比特流相关的知识,希望对你有一定的参考价值。

之前,我讨论了由 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 像素基本块大小。在本系列的下一部分中,我将研究这些较大的单位与图像颜色分量之间的关系。

以上是关于构建一个JPEG解码器:帧和比特流的主要内容,如果未能解决你的问题,请参考以下文章

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

构建一个JPEG解码器:霍夫曼表

Easy Tech:什么是I帧P帧和B帧?

构建一个JPEG解码器:文件结构

I帧P帧和B帧的特点

I帧P帧和B帧的特点