构建一个JPEG解码器:文件结构
Posted weixin_46596227
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了构建一个JPEG解码器:文件结构相关的知识,希望对你有一定的参考价值。
文件结构
在上一部分中,我们简要研究了 JPEG 用于压缩图像的技术。在检查这些技术的详细实现之前,查看 JPEG 文件的整体结构很有用,原因有二:
- 编码器使用的一些过程使用值表,这些值表与图像信息本身一起存储,因此在需要它们之前将它们检索到内存中是明智的。
- 我们还需要在某个地方放置图像解压缩算法的实现,并且有一个框架可以很好地处理这一点。
本系列文章中开发的实现将用 C++ 编写,但这些结构可以移植到您选择的语言中,而不会增加额外的复杂性。
JPEG 图像的类型
此时应该说明的是,此处开发的实现将仅适用于所有可能类型的 JPEG 图像的一个公共子集。首先,标准支持四种类型的压缩:
- 基本:最常见的压缩类型,其中所有图像信息都包含在一系列 8x8 块中。
- 扩展队列:大多数用于医学成像,这种类型允许每个像素有更多的级别。
- 渐进:来自频域的信息以一系列扫描方式写入,每个块的最重要值首先出现在文件中。这允许以低分辨率渲染整个图像,并在图像下载时填充细节。
- 无损:一种罕见的编码,基于目标像素与其周围环境之间的预测差异。
此外,有两种形式的编码应用于图像压缩之上,以进一步压缩文件数据:
- 基于霍夫曼的熵:图像被制成位流,最常见的值编码为短(2 位或 3 位)流条目,不太常见的值记录为较长的位串。
- 算术:一种以前获得专利的编码方法,其中图像数据表示为一系列出现的值的概率,并组合成一个小数。
本系列将实现一个熵编码基线 JPEG 解码器。
文件段
JPEG 文件由不同长度的段组成,每个段都以“标记”开头,以表示它是哪种段。有 254 种可能的片段类型,但在我们将要解码的图像类型中列出以下几种:
名称 | 简称 | 标记 | 描述 | 长度(字节) |
---|---|---|---|---|
图片开头 | SOI | FF D8 | 分隔文件的开头 | 2 |
定义量化表 | DQT | FF DB | 解码器使用的值 | 69 |
定义霍夫曼表 | DHT | FF C4 | 解压缩器使用的值 | 可变的 |
帧开始 | SOF | FF C0 | 熵编码基线帧的信息 | 10 |
扫描开始 | SOS | FF DA | 编码和压缩的图像比特流 | 可变的 |
End of Image | EOI | FF D9 | 分隔文件的结尾 | 2 |
表 1:存在于熵编码基线 JPEG 文件中的段
大多数不同类型的段在标记之后都有一个“长度”值,表示段的长度(以字节为单位)(包括长度值);这可用于跳过解码器不知道的段。这个一般规则有三个例外:
- SOI 和 EOI: 因为这些分隔符多于标记,所以它们只包含标记值。
- SOS: 扫描是一个比特流,并在图像完全编码后自动“结束”。因此,没有为 SOS 段写入文件的长度。有两种策略可以解决这个问题:我们可以假设文件的其余部分是扫描的一部分,或者我们可以通读文件寻找表示新段开始的标记。
对于本文,如果我们遇到 SOS 段,我将假设文件的其余部分是扫描的一部分,并直接跳到 EOI。
实现:列出 JPEG 文件中的段
作为第一步,编写一个程序来打开一个 JPEG 文件,并运行它寻找段标记。这种程序的结构可以扩展为处理不同类型的段的实现,并且跳过给定大小的段的机制稍后可以用于跳过文件中对解码过程不重要的部分。
由于 JPEG 文件中值的大小是以字节数的绝对形式指定的,因此将基本整数类型抽象为引用大小的类型是一个好主意。为此,我们将使用一个简短的头文件。
源码:inttypes.h
与体系结构无关的整数大小定义
#ifndef __INTTYPES_H_
#define __INTTYPES_H_
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;
typedef signed char s8;
typedef signed short s16;
typedef signed int s32;
#endif//__INTTYPES_H_
上述文件是为 32 位编译设置的,但如果需要 64 位或 16 位代码,可以进行调整。这样做的好处是在 JPEG 解码器实现本身中对整数的引用可以与体系结构无关,只需引用 u16 和此处定义的其他类型。
有了这些抽象,段列表的实现就非常简单了。由于我们将解码功能构建到一个类中,因此此时定义类本身是值得的。
源码:jpeg.h
JPEG 解码器类定义
#ifndef __JPEG_H_
#define __JPEG_H_
#include "inttypes.h"
#include <string>
#include <vector>
#include <map>
#include <stdio.h>
// Macro to read a 16-bit word from file
#define READ_WORD() ((fgetc(fp) << 8) | fgetc(fp))
// Segment parsing error codes
#define JPEG_SEG_ERR 0
#define JPEG_SEG_OK 1
#define JPEG_SEG_EOF -1
class JPEG {
private:
// Names of the possible segments
std::string segNames[64];
// The file to be read from, opened by constructor
FILE *fp;
// Segment parsing dispatcher
int parseSeg();
public:
// Construct a JPEG object given a filename
JPEG(std::string);
};
#endif//__JPEG_H_
源码:jpeg.cpp
JPEG 段列表实现
#include "jpeg.h"
#include <stdlib.h>
#include <string.h>
#include <math.h>
//-------------------------------------------------------------------------
// Function: Parse JPEG file segment (parseSeg)
// Purpose: Retrieves 16-bit block ID from file, shows name
int JPEG::parseSeg()
{
if (!fp) {
printf("File failed to open.\\n");
return JPEG_SEG_ERR;
}
u32 fpos = ftell(fp);
u16 id = READ_WORD(), size;
if (id < 0xFFC0)
{
printf("Segment ID expected, not found.\\n");
return JPEG_SEG_ERR;
}
printf(
"Found segment at file position %d: %s\\n",
fpos, segNames[id-0xFFC0].c_str());
switch (id) {
// The SOI and EOI segments are the only ones not to have
// a length, and are always a fixed two bytes long; do
// nothing to advance the file position
case 0xFFD9:
return JPEG_SEG_EOF;
case 0xFFD8:
break;
// An SOS segment has a length determined only by the
// length of the bitstream; for now, assume it's the rest
// of the file less the two-byte EOI segment
case 0xFFDA:
fseek(fp, -2, SEEK_END);
break;
// Any other segment has a length specified at its start,
// so skip over that many bytes of file
default:
size = READ_WORD();
fseek(fp, size-2, SEEK_CUR);
break;
}
return JPEG_SEG_OK;
}
//-------------------------------------------------------------------------
// Function: Array initialisation (constructor)
// Purpose: Fill in arrays used by the decoder, decode a file
// Parameters: filename (string) - File to decode
JPEG::JPEG(std::string filename)
{
// Debug messages used by parseSeg to tell us which segment we're at
segNames[0x00] = std::string("Baseline DCT; Huffman");
segNames[0x01] = std::string("Extended sequential DCT; Huffman");
segNames[0x02] = std::string("Progressive DCT; Huffman");
segNames[0x03] = std::string("Spatial lossless; Huffman");
segNames[0x04] = std::string("Huffman table");
segNames[0x05] = std::string("Differential sequential DCT; Huffman");
segNames[0x06] = std::string("Differential progressive DCT; Huffman");
segNames[0x07] = std::string("Differential spatial; Huffman");
segNames[0x08] = std::string("[Reserved: JPEG extension]");
segNames[0x09] = std::string("Extended sequential DCT; Arithmetic");
segNames[0x0A] = std::string("Progressive DCT; Arithmetic");
segNames[0x0B] = std::string("Spatial lossless; Arithmetic");
segNames[0x0C] = std::string("Arithmetic coding conditioning");
segNames[0x0D] = std::string("Differential sequential DCT; Arithmetic");
segNames[0x0E] = std::string("Differential progressive DCT; Arithmetic");
segNames[0x0F] = std::string("Differential spatial; Arithmetic");
segNames[0x10] = std::string("Restart");
segNames[0x11] = std::string("Restart");
segNames[0x12] = std::string("Restart");
segNames[0x13] = std::string("Restart");
segNames[0x14] = std::string("Restart");
segNames[0x15] = std::string("Restart");
segNames[0x16] = std::string("Restart");
segNames[0x17] = std::string("Restart");
segNames[0x18] = std::string("Start of image");
segNames[0x19] = std::string("End of image");
segNames[0x1A] = std::string("Start of scan");
segNames[0x1B] = std::string("Quantisation table");
segNames[0x1C] = std::string("Number of lines");
segNames[0x1D] = std::string("Restart interval");
segNames[0x1E] = std::string("Hierarchical progression");
segNames[0x1F] = std::string("Expand reference components");
segNames[0x20] = std::string("JFIF header");
segNames[0x21] = std::string("[Reserved: application extension]");
segNames[0x22] = std::string("[Reserved: application extension]");
segNames[0x23] = std::string("[Reserved: application extension]");
segNames[0x24] = std::string("[Reserved: application extension]");
segNames[0x25] = std::string("[Reserved: application extension]");
segNames[0x26] = std::string("[Reserved: application extension]");
segNames[0x27] = std::string("[Reserved: application extension]");
segNames[0x28] = std::string("[Reserved: application extension]");
segNames[0x29] = std::string("[Reserved: application extension]");
segNames[0x2A] = std::string("[Reserved: application extension]");
segNames[0x2B] = std::string("[Reserved: application extension]");
segNames[0x2C] = std::string("[Reserved: application extension]");
segNames[0x2D] = std::string("[Reserved: application extension]");
segNames[0x2E] = std::string("[Reserved: application extension]");
segNames[0x2F] = std::string("[Reserved: application extension]");
segNames[0x30] = std::string("[Reserved: JPEG extension]");
segNames[0x31] = std::string("[Reserved: JPEG extension]");
segNames[0x32] = std::string("[Reserved: JPEG extension]");
segNames[0x33] = std::string("[Reserved: JPEG extension]");
segNames[0x34] = std::string("[Reserved: JPEG extension]");
segNames[0x35] = std::string("[Reserved: JPEG extension]");
segNames[0x36] = std::string("[Reserved: JPEG extension]");
segNames[0x37] = std::string("[Reserved: JPEG extension]");
segNames[0x38] = std::string("[Reserved: JPEG extension]");
segNames[0x39] = std::string("[Reserved: JPEG extension]");
segNames[0x3A] = std::string("[Reserved: JPEG extension]");
segNames[0x3B] = std::string("[Reserved: JPEG extension]");
segNames[0x3C] = std::string("[Reserved: JPEG extension]");
segNames[0x3D] = std::string("[Reserved: JPEG extension]");
segNames[0x3E] = std::string("Comment");
segNames[0x3F] = std::string("[Invalid]");
// Open the requested file, keep parsing blocks until we run
// out of file, then close it.
fp = fopen(filename.c_str(), "rb");
if (fp) {
while(parseSeg() == JPEG_SEG_OK);
fclose(fp);
}
else {
perror("JPEG");
}
}
使用文件构造时,此 JPEG 类的对象将提供类似于以下内容的输出。
源码:main.cpp
/**
* Let's Build a JPEG Decoder: Segment lister
* Entry point [main.cpp]
* Imran Nazar, Jan 2013
*/
#include "jpeg.h"
int main(int argc, char **argv)
{
if (argc != 2) {
printf("Usage: jpegparse <file.jpg>\\n");
return 1;
}
std::string in = std::string(argv[1]);
JPEG j(in);
return 0;
}
源码:Makefile
CC = g++ -c -g
LD = g++
all: jpegparse
jpegparse: jpeg.o main.o
$(LD) -o $@ $^
%.o: %.cpp
$(CC) -o $@ $^
%.cpp: %.h
.PHONY: clean
clean:
rm -rf jpegparse *.o
用于测试的素材图:test.jpg
下面的示例图,请右键保存
JPEG 段列表的输出
Found segment at file position 0: Start of image
Found segment at file position 2: JFIF header
Found segment at file position 20: Quantisation table
Found segment at file position 89: Quantisation table
Found segment at file position 158: Baseline DCT; Huffman
Found segment at file position 177: Huffman table
Found segment at file position 208: Huffman table
Found segment at file position 289: Huffman table
Found segment at file position 318: Huffman table
Found segment at file position 371: Start of scan
Found segment at file position 32675: End of image
最后:检查扫描的编码
从上面可以看出,“扫描”构成了熵编码基线 JPEG 的大部分;由于整个图像数据都在扫描中编码,这就有意思了。熵编码基于霍夫曼压缩算法,因此,在下一篇文章中,我将检查 JPEG 文件的各个部分,这些部分提供将扫描从比特流解码为可用于进一步处理的内容所需的信息。
以上是关于构建一个JPEG解码器:文件结构的主要内容,如果未能解决你的问题,请参考以下文章