构建一个JPEG解码器(2):文件结构
文件结构
- JPEG 图像的类型
- 文件段
- 实现:列出 JPEG 文件中的段
- 源码:inttypes.h
- 源码:jpeg.h
- 源码:jpeg.cpp
- 源码:main.cpp
- 源码:Makefile
- 用于测试的素材图:test.jpg
- 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 -1class JPEG {private:// Names of the possible segmentsstd::string segNames[64];// The file to be read from, opened by constructorFILE *fp;// Segment parsing dispatcherint parseSeg();public:// Construct a JPEG object given a filenameJPEG(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 nameint 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 positioncase 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 segmentcase 0xFFDA:fseek(fp, -2, SEEK_END);break;// Any other segment has a length specified at its start,// so skip over that many bytes of filedefault: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 decodeJPEG::JPEG(std::string filename)
{ // Debug messages used by parseSeg to tell us which segment we're atsegNames[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: jpegparsejpegparse: 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解码器(2):文件结构相关推荐
- 构建一个JPEG解码器(3):霍夫曼表
霍夫曼表 简述 重构 即时 霍夫曼算法 JPEG DHT 段 源代码 jpeg.h jpeg.cpp inttypes.h main.cpp Makefile 接下来:读取比特流 简述 在上一部分中, ...
- 在NVIDIA A100 GPU上利用硬件JPEG解码器和NVIDIA nvJPEG库
在NVIDIA A100 GPU上利用硬件JPEG解码器和NVIDIA nvJPEG库 根据调查,普通人产生的1.2万亿张图像可以通过电话或数码相机捕获.这样的图像的存储,尤其是以高分辨率的原始格式, ...
- JPEG原理分析及JPEG解码器的解析
文章目录 JPEG原理分析及JPEG解码器的调试 原理分析 JPEG编解码流程图 DC系数编码 AC系数编码 JPEG文件格式 Segment组织形式 JPEG 的 Segment Marker no ...
- NVIDIA A100 GPUs上硬件JPEG解码器和NVIDIA nvJPEG库
NVIDIA A100 GPUs上硬件JPEG解码器和NVIDIA nvJPEG库 Leveraging the Hardware JPEG Decoder and NVIDIA nvJPEG Lib ...
- 如何用socket构建一个简单的Web Server
2019独角兽企业重金招聘Python工程师标准>>> 背景 现代社会网络应用随处可见,不管我们是在浏览网页.发送电子邮件还是在线游戏都离不开网络应用程序,网络编程正在变得越来越重要 ...
- 几十行python代码构建一个前后端分离的目标检测演示网站,代码开源
在深度学习更讲究实用和落地的今天,构建一个简单的,可以利用浏览器和后端交互的演示性 Demo 可以说非常重要且实用了.本文我们将简单的介绍如何用几十行核心代码构建一个好用的.前后端分离的Demo. 2 ...
- 【数据压缩-实验5】JPEG原理分析及JPEG解码器的调试
目录 JEPG原理 简述 优点 缺点 JPEG文件格式 常用标记码 编解码原理 编码原理 Level offset-零偏置 DCT变换 量化 DC系数差分编码 AC系数的之字形扫描+游程编码 解码原理 ...
- 快速构建一个简单的对话+问答AI (上)
文章目录 前言 part0 资源准备 基本功能 语料 停用词 问答 闲聊语料 获取 part01句的表达 表达 one-hot编码 词嵌入 大致原理 实现 简单版 复杂版 如何训练 转换后的形状 pa ...
- AI:2023年6月9日北京智源大会演讲分享之基础模型前沿技术论坛—《工程化打造AI中的CPU》、《构建一个AI系统:在LLM上应用带有RLHF来推进定制》、《多模态预训练的进展回顾与展望》、《扩展大
AI:2023年6月9日北京智源大会演讲分享之基础模型前沿技术论坛-<工程化打造AI中的CPU>.<构建一个AI系统:在LLM上应用带有RLHF来推进定制>.<多模态预训 ...
最新文章
- C02-程序设计基础提高班(C++)第9周上机任务-类和对象
- 钉钉接入access_无需开发,IT事件接入钉钉的方法详解
- Python的win32serviceutil之疑似BUG
- webpack那些事儿
- 如何快速把音乐转成MP3格式
- 数据挖掘--挖掘建模-时序模式-ARIMA模型
- 大屏监控系统实战(16)-项目拾遗
- Improved Semantic Representations From Tree-Structured Long Short-Term Memory Networks
- Linux 网络基本配置
- python连接access2007_使用Python / pyodbc插入Access DB
- CodeSmith模板
- win10下微软office2010卸载
- Codeforces 741D dsu on tree
- Adnroid Studio kotlin 报错AssertionError: Could not delete caches xxx kotlin\compileKotlin文件名太长
- 微信lbs开发java_微信LBS获取
- python多进程协同_简单谈谈python中的多进程
- FPGA复位电路设计学习分析
- 微商铺php,帮助中心-微商铺的功能详解
- SpringBoot数据库密码动态配置
- 由一道简单的图片隐写题总结思路
热门文章
- 简化开发流程--UEditor富文本编辑器
- 大咖来了!今年的 COSCon 主论坛你可以见到这些大咖
- 勒索软体大转型,防御难度提升:黒产供应链经营成本高,锁定有能力付赎金的知名企业
- win10预装软件卸载工具
- duilib 分屏显示bug
- 【C++】动态内存分配详解(new/new[]和delete/delete[])
- ms dtc 启动失败的解决办法。
- 重新审视Visio的本质
- Python线上培训机构推荐|如何选靠谱Python培训机构?
- cf: Ehab and Path-etic MEXs