之前,我讨论了由 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 头包含解码帧所需的部分信息,其结构如下表所示。
Field | Value | Size (bytes) |
---|---|---|
Precision (the number of pixels in a JPEG block) | 8 | 1 |
Image height | Up to 65535 | 2 |
Image width | Up to 65535 | 2 |
Components | Number of colour components | 1 |
For each component (in a YUV-colour file, three) | ||
ID | Identifier for later use | 1 |
Sampling resolution | For later examination | 1 |
Quantisation table | For later examination | 1 |
从上表中可以看出,一些字段涉及我们尚未检查的操作(每个组件的采样分辨率和量化表)。为了完成 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 像素基本块大小。在本系列的下一部分中,我将研究这些较大的单位与图像颜色分量之间的关系。