手把手教你写一个 JPEG 解码器(二):解码器 C/C++ 实现

系列文章目录



前言

前一章我们介绍 jpeg 编码原理的相关知识,本章则进入实战环节,手写 jpeg 解码器。

所有代码我已经放到了 github-simple_jpeg_decoder,你可以运行 tests 下单测,或者运行 main.cpp 来解码一个 jpeg 文件。

接下来是对整体解码流程的介绍,主要包含两个部分:JFIF文件解析和JEPG数据解码。

一、JFIF 文件解析

1.1 JFIF 文件介绍

JPEG本身只规定了图像的压缩算法和数据格式,并没有规定文件的封装结构,经过 JPEG 压缩后得到的比特流,需要存放在一个文件中,并且这个文件中还需要有其他额外信息,帮助我们进行解码,例如需要有图片的宽高、文件像素格式等等。

JFIF 全称是 JPEG File Interchange Format,即“JPEG文件交换格式”。JFIF 是在 JPEG 压缩的图像流之上,增加了简单的文件结构和元数据定义的一种文件格式。他定义了包括像素密度、图像宽高、颜色空间等信息的存储方式。

JFIF是一种JPEG文件的实现封装格式,在实际用途中,绝大多数以.jpg或.jpeg为后缀的图片其实就是“JPEG编码+JFIF封装”。
也就是说,常见的JPEG图片文件其实是“JFIF格式的JPEG图片”。

因此,我们解码 jpeg 文件时,首先要做的是解析 JFIF 文件,获取该文件中各个"段",这些段中有很多重要的信息,用这些信息可以进行 jpeg 比特流的解码。

2.1 JFIF 格式

JFIF 文件格式指定了对应于不同片段(segment)的标记(marker)。每个片段包含了解码图像所需信息的一部分。标记实际上就是两个字节(word)大小的数据,总是以字节 ff 开头,结尾则是一个取值不是 ff 或 00 的字节。下面表格中使我们需要关心的列表:

MarkerSegment (English)中文含义
ff d8Start of Image segment (SOI)图像开始标记段
ff e0JPEG/JFIF Image segment (APP-0)JPEG/JFIF 图像信息段
ff feComment segment (COM)注释段
ff dbDefine Quantization Table (DQT)定义量化表段
ff c0Start of Frame-0 (SOF-0)帧开始标记0段
ff c4Define Huffman Table (DHT)定义哈夫曼表段
ff daStart of Scan segment (SOS)扫描开始段
ff d9End of Image segment (EOI)图像结束标记段

下面几个字段的顺序是固定要求的

  1. SOI
  2. APP-0
  3. SOS
  4. EOI
    其他的字段,例如 COM、DQT 没有严格的顺序,但必须在 APP-0 和 SOS 之间。

此外,一些标记如APP-0、COM等在其后面紧跟着额外的数据,而像SOI和EOI这样的标记则是自包含的。

ff xx l1 l2 <payload>

在这里,前两个字节ff和xx描述标记,接着是两个字节l1和l2,描述标记后面载荷数据的长度。然后是实际的载荷数据,其长度正好等于字节l1和l2所指定的长度。

我们的解码器将只处理定义了SOF-0段的图像,因为SOF-0对应的是基线DCT JPEG图像。

接下来对每个段进行说明,并配上解析的代码。详细代码参考 sjpg_jfif_parser.h

2.1.1 SOI

Marker: ff d8
每个 JFIF 文件都以这个 marker 开头

static bool hasSOIMark(std::istream &stream) {
  // 记录当前位置
  unsigned char soi[2];
  stream.read(reinterpret_cast<char *>(soi), 2);
  bool result =
      (stream.gcount() == 2 && soi[0] == JFIF_BYTE_FF && soi[1] == JFIF_SOI);
  return result;

2.2.2 APP-0

Bytes大小(字节)描述
ff e02APP-0 段标记
<byte> <byte>2载荷长度
4a 46 49 46 005字符串 “JFIF\0”
<byte> <byte>2JFIF 版本(高字节为主版本号,低字节为次版本号)
<byte>1像素密度单位:00 无单位,01 像素/英寸,02 像素/厘米
<byte> <byte>2水平像素密度(>0)
<byte> <byte>2垂直像素密度(>0)
<byte>1缩略图水平像素数(0 表示无缩略图)
<byte>1缩略图垂直像素数(0 表示无缩略图)
<thumbnail-data>3 × 水平像素数 × 垂直像素数未压缩 24 位 RGB 缩略图数据
void parseAPP0Segment(std::istream &stream) {
  app0_ = std::make_unique<segments::APP0Segment>();
  app0_->file_pos = stream.tellg();
  app0_->length = StreamReader::read2BytesBigEndian(stream);
  StreamReader::readTo(stream, app0_->identifier, 5);
  app0_->major_version = StreamReader::readByte(stream);
  app0_->minor_version = StreamReader::readByte(stream);
  app0_->pixel_units = StreamReader::readByte(stream);
  app0_->x_density = StreamReader::read2BytesBigEndian(stream);
  app0_->y_density = StreamReader::read2BytesBigEndian(stream);
  app0_->thumbnail_width = StreamReader::readByte(stream);
  app0_->thumbnail_height = StreamReader::readByte(stream);
  static constexpr int kRGBChannels = 3;
  static constexpr int kRGBBits = 8; // 每个RGB通道8位
  static constexpr int kRGBBytesPerPixel = kRGBChannels * (kRGBBits / 8);
  const auto thumbnail_image_pixel_count =
      app0_->thumbnail_width * app0_->thumbnail_height;
  const auto thumbnail_data_length =
      kRGBBytesPerPixel * thumbnail_image_pixel_count;
  app0_->thumbnail_data.resize(thumbnail_data_length);
  if (thumbnail_data_length > 0) {
    StreamReader::readTo(
        stream, reinterpret_cast<char *>(app0_->thumbnail_data.data()),
        thumbnail_data_length);
  }
  app0_->print();
}

2.2.3 COM

Bytes大小(字节)描述
ff fe2注释段标记
<byte> <byte>2载荷长度
<comment-data>n实际注释数据,n = 上一步指定的长度
void parseCOMSegment(std::istream &stream) {
  com_ = std::make_unique<segments::COMSegment>();
  com_->file_pos = stream.tellg();
  com_->length = StreamReader::read2BytesBigEndian(stream);
  auto commentSize = com_->length - 2; // 减去长度字段的2个字节
  com_->comment.resize(commentSize);   // 减去长度字段的2个字节
  StreamReader::readTo(stream, reinterpret_cast<char *>(com_->comment.data()),
                       commentSize);
  com_->print();

2.2.4 DQT

Bytes大小(字节)描述
ff db2DQT 段标记
<byte> <byte>2段长度 𝑛(含 DQT 段标记)
<byte>1高 4 位:精度 𝑃𝑞(0 = 8 位,1 = 16 位)
低 4 位:量化表编号 𝑇𝑞
<quantization-table-data>64量化表数据,按 Zig-zag 顺序排列的 64 个元素
void parseDQTSegment(std::istream &stream) {
  // TODO: multiple table in a dqt segment
  segments::DQTSegment dqt;
  dqt.file_pos = static_cast<unsigned long>(stream.tellg());
  dqt.length = StreamReader::read2BytesBigEndian(stream);
  auto tmp = StreamReader::readByte(stream);
  segments::QuantizationTable table;
  table.precision = tmp >> 4;
  table.id = tmp & 0x0F;
  constexpr static int kQuantizationTableSize = 64; // 8x8 matrix
  table.data.resize(kQuantizationTableSize);
  StreamReader::readTo(stream, reinterpret_cast<char *>(table.data.data()),
                       kQuantizationTableSize);
  dqt.tables.emplace_back(table);
  dqt.print();
  dqt_segments_.emplace_back(dqt);
  auto &last_segment = dqt_segments_.back();
  for (auto i = 0; i < last_segment.tables.size(); i++) {
    auto table_id = last_segment.tables[i].id;
    q_table_refs[table_id] = &last_segment.tables[i];
  }
}

2.2.5 SOF-0

Bytes大小(字节)描述
ff c02SOF-0 段标记
<byte> <byte>2段长度
<byte>1帧数据精度
<byte> <byte>2图像高度(像素)
<byte> <byte>2图像宽度(像素)
<byte>1帧内分量数 n(RGB 图像通常为 3)
<component-data>3 × n每分量 3 字节:
– 第 1 字节:分量标识符
– 第 2 字节:采样因子(高 4 位=水平,低 4 位=垂直)
– 第 3 字节:使用的量化表号
void parseSOF0Segment(std::istream &stream) {
  sof0_ = std::make_unique<segments::SOF0Segment>();
  sof0_->file_pos = stream.tellg();
  sof0_->length = StreamReader::read2BytesBigEndian(stream);
  sof0_->bitPerSample = StreamReader::readByte(stream);
  sof0_->height = StreamReader::read2BytesBigEndian(stream);
  sof0_->width = StreamReader::read2BytesBigEndian(stream);
  sof0_->num_components = StreamReader::readByte(stream);
  for (int i = 0; i < sof0_->num_components; i++) {
    auto b0 = StreamReader::readByte(stream);
    auto b1 = StreamReader::readByte(stream);
    auto b2 = StreamReader::readByte(stream);
    sof0_->component_id.push_back(b0);
    sof0_->sampling_factor.push_back(b1);
    sof0_->quantization_table_id.push_back(b2);
  }
  sof0_->print();
}

2.2.6 DHT

Bytes大小(字节)描述
ff c42DHT 段标记
<byte> <byte>2段长度(含标记)
<byte>1霍夫曼表信息:高 4 位 = 类型(0 = DC,1 = AC),低 4 位 = 表号
<symbol-count>16各码长(1–16 位)的符号数量(共 16 字节)
<symbols>n符号数据,按码长顺序排列,共 n 个字节
void parseDHTSegment(std::istream &stream) {
  auto dht = segments::DHTSegment();
  dht.file_pos = static_cast<unsigned long>(stream.tellg());
  dht.length = StreamReader::read2BytesBigEndian(stream);
  auto b = StreamReader::readByte(stream);
  dht.dc_or_ac = b >> 4;
  dht.table_id = b & 0x0F;
  static constexpr int kHuffmanTableSize = 16;
  dht.symbol_counts.resize(kHuffmanTableSize);
  StreamReader::readTo(stream,
                       reinterpret_cast<char *>(dht.symbol_counts.data()),
                       kHuffmanTableSize);
  auto num_total_symbols =
      std::accumulate(dht.symbol_counts.begin(), dht.symbol_counts.end(), 0);
  dht.symbols.resize(num_total_symbols);
  StreamReader::readTo(stream, reinterpret_cast<char *>(dht.symbols.data()),
                       num_total_symbols);
  dht.print();
  dht_segments_.emplace_back(dht);
}

2.2.7 SOS

Bytes大小(字节)描述
ff da2SOS 段标记
<byte> <byte>2段长度
<byte>1分量数 n
<component-data>2 × n每分量 2 字节:第 1 字节 = 分量 ID;第 2 字节 = 高 4 位 DC 霍夫曼表号,低 4 位 AC 霍夫曼表号
<skip-bytes>3固定跳过字节
void parseSOSSegment(std::istream &stream) {
  sos_ = std::make_unique<segments::SOSSegment>();
  sos_->file_pos = static_cast<unsigned long>(stream.tellg());
  sos_->length = StreamReader::read2BytesBigEndian(stream);
  sos_->num_components = StreamReader::readByte(stream);
  for (int i = 0; i < sos_->num_components; i++) {
    auto b0 = StreamReader::readByte(stream);
    auto b1 = StreamReader::readByte(stream);
    sos_->component_id.push_back(b0);
    auto huffman_table_id_dc = b1 >> 4;
    auto huffman_table_id_ac = b1 & 0x0F;
    sos_->huffman_table_id_ac.push_back(huffman_table_id_ac);
    sos_->huffman_table_id_dc.push_back(huffman_table_id_dc);
  }
  // skip 3 bytes
  for (int j = 0; j < 3; j++) {
    StreamReader::readByte(stream);
  }
  sos_->print();
}

2.2.8 图像数据 和 EOI

在 SOS 段后,立马就是图像压缩后的比特流数据,接着是 EOI 端。因此在解析完 SOS 段后,里面开始读压缩数据:

parseSOSSegment(stream);
scanImageDataAndEOI(stream);

void scanImageDataAndEOI(std::istream &stream) {
  for (;;) {
    auto b = StreamReader::readByte(stream);
    if (b == JFIF_BYTE_FF) {
      auto next_b = StreamReader::readByte(stream);
      if (next_b == JFIF_EOI) {
        // build eoi segment
        eoi_ = std::make_unique<segments::EOISegment>();
        eoi_->file_pos = static_cast<unsigned long>(stream.tellg());
        return;
      }else {
        // it is encoded data
        encoded_data_.push_back(b);
      }
    }else {
      encoded_data_.push_back(b);
    }
  }

注意代码中关于填充字节的处理,0xFF 字节被用作各种标记(marker)的起始标志。为了防止压缩数据中意外产生的 0xFF 被误认为是标记的起始,JPEG 标准规定:

  • 如果在扫描数据中出现了 0xFF,就会在其后插入一个 0x00 字节(即 0xFF 0x00)。
  • 这个 0x00 就是“填充字节”或者“填塞字节”。

二、JPEG 数据解码

2.1 比特流

解码是编码的逆过程,让我们先回忆下编码的过程,但这次逆着方向来,从比特流开始。

解析 JFIF 后,我们得到了一些段,另外最重要的就是一堆 01 组成的比特流数据,例如:

010001010101010101101......

JPEG 编码时最小单位是 MCU,因此这些比特流是按 MCU 编号来排列的,编码顺序是从左到右,然后从上到下,就像阅读文本一样。

假设一个图像被分割成多个8×8的块(MCU):

┌─────┬─────┬─────┬─────┐
│ MCU │ MCU │ MCU │ MCU │
│  1  │  2  │  3  │  4  │  ← 第一行
├─────┼─────┼─────┼─────┤
│ MCU │ MCU │ MCU │ MCU │
│  5  │  6  │  7  │  8  │  ← 第二行
├─────┼─────┼─────┼─────┤
│ MCU │ MCU │ MCU │ MCU │
│  9  │ 10  │ 11  │ 12  │  ← 第三行
├─────┼─────┼─────┼─────┤
│ MCU │ MCU │ MCU │ MCU │
│ 13  │ 14  │ 15  │ 16  │  ← 第四行
└─────┴─────┴─────┴─────┘

编码顺序:1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9 → 10 → 11 → 12 → 13 → 14 → 15 → 16

因此这些比特流,也是按照 MCU 编码来排序的

100110 | 0110 | 10101011 | 110 | 00100101 | 1110
   |       |        |      |        |        |
  MCU1   MCU2    MCU3     MCU4    MCU5     MCU6

每个 MCU 的比特流由两部分依次组成:

  1. DC 系数编码(使用差分编码)

    • Category 字段(哈夫曼编码,表示该差值的位宽)
    • Bit representation 字段(该差值按 Category 指定的位宽进行的实际二进制表示)
  2. AC 系数游程编码
    逐个游程-幅值(run-length/value)进行编码,每个非零 AC 系数的编码都包含:

    • (Run/Category) 字段(哈夫曼编码,其中 Run 为前导 0 的数量,Category 为该非零 AC 的位宽,两者合成一个字节如 RRRRSSSS)
    • Bit representation 字段(该 AC 系数经过 Category 位宽的实际二进制代码)
      全部 AC 系数以 EOB(End-of-Block)标记终结。

这部分很绕,很复杂,也是解码过程中最麻烦的地方。首先我们要了解 Category 和 Bit representation 的概念。

2.2 Category 概念

在 JPEG 编解码中,Category(类别) 是指对 DCT 系数的幅值(绝对值)进行分组分类 的一个概念,主要用于 哈夫曼编码的设计。它是 JPEG 熵编码部分(尤其是 DC/AC 系数的编码)里的一个重要术语。

1. Category 的定义

  • Category 表示一个系数幅值的范围(具体来说,是幅度的二进制位数)。
  • 对于一个待编码的数值(比如 DPCM 后的 DC 差值或某个 AC 系数),Category = 该数字的最小二进制编码所需的位数。

2. 具体划分方式

  • Category 0: 0
  • Category 1: ±1
  • Category 2: ±2, ±3
  • Category 3: ±4 ~ ±7
  • Category 4: ±8 ~ ±15
  • Category 5: ±16 ~ ±31
  • … 以此类推

3. 作用
JPEG 熵编码把 DCT 系数(DC/AC)拆成两部分编码:
Category(类别):通过哈夫曼编码对“类别号”进行编码。
Amplitude(幅值):用定长二进制补全实际值。
这样做可以利用系数幅值分布的特性,大多数系数绝对值较小,从而提高编码效率。

4. 与 JPEG 码流的关系
在 JPEG 码流里,DC 系数和 AC 系数的哈夫曼表其实对应的就是 category。
例如,(RUNLENGTH, CATEGORY) 作为 AC 系数编码的前缀(哈夫曼编码),category 也用于 DC。

简单来说,Category 就是指明“接下来你要读取几位比特(bit)” 来获得系数的实际数值。

2.3 Bit representation

Bit representation(JPEG 标准里常叫 additional bits、raw bits、或 appended bits)就是 在 Category 指明长度之后,紧跟的那一串原始比特,用来唯一地确定量化后系数的实际正负值。

长度由 Category 决定(1-10 位)。
内容是一个“带符号的变长整数”——最左位是符号位,需要按 JPEG 的约定再转成真正的有符号数。

所以:

  • Category = “读几位”;
  • Bit representation = “那几位到底长什么样”。

接下去我们需要将 Bit representation 转换为真正的数字,逻辑只有两步,记住“先还原无符号数,再转成有符号数”即可。

设额外读到的 bit 串为 B,长度为 L = Category

  1. B 当无符号整数:V = uint(B)
  2. 计算有符号值:
    • 如果 V < 2^L - 1,则系数 = V
    • 否则 系数 = V - (2^{L+1} - 1)

一个更常用的等价写法(代码最直观):

if (B >= (1 << (L-1)))      // 最高位为 1 → 正数
    coeff = B;
else                        // 最高位为 0 → 负数
    coeff = B - (1 << L) + 1;

举例
Category = 4,读到的 4 bit = 1011 (11)

  • 11 < 15 → 系数 = 11 - (2^4 - 1) = -4

这就是 JPEG 把 bit representation 变成真实 DCT 系数的全部逻辑。下面是具体代码逻辑

static int16_t decodeNumber(uint16_t code_length, const std::string &bits)
{
  if (code_length == 0) return 0;          // Category 0 时无附加位
  uint16_t v = static_cast<uint16_t>(std::stoi(bits, nullptr, 2));
  int16_t  half = 1 << (code_length - 1);  // 2^(L-1)
  // JPEG 的“附加位”编码规则
  return (v < half) ? v - (2 * half - 1) : v;
}

2.4 举例说明

让我用例子来进一步说明。假设某个 MCU 有这样一个比特流

11000001 0110 1101101

解码过程如下图
在这里插入图片描述

DC 解码过程(左图):

  1. 匹配到Huffman码(110),查表获Category(5)。
  2. 根据类别长度,读取后续5位(00001),得到差分的比特流。
  3. 解析 bit representation,得到 -30
  4. 用 DC_pred 之前的直流值加这个Δ,得到当前 mcu 的DC系数。

AC 系数解码过程(中图):

  1. 匹配到Huffman码(01),查表得 rrrr=0, ssss=2,得到 zero_count = 0, Category=2
  2. 根据 Category 读取2bit(10),以JPEG规则转化为系数值:2
  3. 得到游程编码是(0, 2),实际表示 [2]

AC 系数解码过程(中图):

  1. 匹配到Huffman码(11011),查表得 rrrr=1, ssss=2,得到 zero_count = 1, Category=2
  2. 根据 Category 读取2bit(10),以JPEG规则转化为系数值:2
  3. 得到游程编码是(1, 2),实际表示 [0, 2]

经过上面三次解码,我们得到 4 个值,以此类推解码整个 mcu

-30  2   0   2
DC   AC0 AC1 AC2

2.5 理解哈夫曼表

注意,DC 和 AC 编解码过程中分别使用到了不同的哈夫曼表,此外 Y 分量和 UV 分量使用的哈夫曼表也是不一样的,因此通常一共有 4 个哈夫曼表。

分量/类型DC(直流)AC(交流)
Y(亮度分量)哈夫曼表 #0 (DHT0)哈夫曼表 #1 (DHT1)
UV(色度分量)哈夫曼表 #2 (DHT2)哈夫曼表 #3 (DHT3)

DHT 段中定义了哈夫曼表的数据,例如下面有 4 个 DHT 段,“DC(0)/AC(1)” 和 “Table ID” 可以让我们标识这个表格。

DHT segment
	File position: 245
	Length: 31
	DC(0)/AC(1): 0
	Table ID: 0
	Symbol counts(16): 
0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0,
	Symbols(12): 
0,1,2,3,4,5,6,7,8,9,10,11,
DHT segment
	File position: 278
	Length: 181
	DC(0)/AC(1): 1
	Table ID: 0
	Symbol counts(16): 
0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,125,
	Symbols(162): 
1,2,3,0,4,17,5,18,33,49,65,6,19,81,97,7,34,113,20,50,129,145,161,8,35,66,177,193,21,82,209,240,36,51,98,114,130,9,10,22,23,24,25,26,37,38,39,40,41,42,52,53,54,55,56,57,58,67,68,69,70,71,72,73,74,83,84,85,86,87,88,89,90,99,100,101,102,103,104,105,106,115,116,117,118,119,120,121,122,131,132,133,134,135,136,137,138,146,147,148,149,150,151,152,153,154,162,163,164,165,166,167,168,169,170,178,179,180,181,182,183,184,185,186,194,195,196,197,198,199,200,201,202,210,211,212,213,214,215,216,217,218,225,226,227,228,229,230,231,232,233,234,241,242,243,244,245,246,247,248,249,250,
DHT segment
	File position: 461
	Length: 31
	DC(0)/AC(1): 0
	Table ID: 1
	Symbol counts(16): 
0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0,
	Symbols(12): 
0,1,2,3,4,5,6,7,8,9,10,11,
DHT segment
	File position: 494
	Length: 181
	DC(0)/AC(1): 1
	Table ID: 1
	Symbol counts(16): 
0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,119,
	Symbols(162): 
0,1,2,3,17,4,5,33,49,6,18,65,81,7,97,113,19,34,50,129,8,20,66,145,161,177,193,9,35,51,82,240,21,98,114,209,10,22,36,52,225,37,241,23,24,25,26,38,39,40,41,42,53,54,55,56,57,58,67,68,69,70,71,72,73,74,83,84,85,86,87,88,89,90,99,100,101,102,103,104,105,106,115,116,117,118,119,120,121,122,130,131,132,133,134,135,136,137,138,146,147,148,149,150,151,152,153,154,162,163,164,165,166,167,168,169,170,178,179,180,181,182,183,184,185,186,194,195,196,197,198,199,200,201,202,210,211,212,213,214,215,216,217,218,226,227,228,229,230,231,232,233,234,242,243,244,245,246,247,248,249,250,

DHT 端中最重要的数据是 Symbol counts 和 Symbols,其中

  • Symbols,如你所见,就是一些真正的数,例如 0, 10, 100 等,由于 YUV 数据范围是 [0, 256],所以这些数都在这个范围内。
  • Symbol counts,是一个数组,固定长度为 16;第 i 个字节(i = 1 … 16)存放的是 码长 = i 的所有符号的个数;告诉解码器“码长 1 有多少个码字,码长 2 有多少个码字……码长 16 有多少个码字”。
    通过 Symbol counts 和 Symbols 我们可以重建哈夫曼表,逻辑如下:
static std::map<std::string, uint16_t> buildHuffmanTable(
  const std::vector<uint8_t>& counts,
  const std::vector<uint8_t>& symbols) {
  constexpr static auto kMaxHuffmanCodeLength = 16;
  assert(counts.size() == kMaxHuffmanCodeLength);
  std::map<std::string, uint16_t> table;
  int code = 0;
  int symInd = 0;
  // length: 1 ~ 16 (JPEG哈夫曼表的码长范围)
  for (int length = 1; length <= kMaxHuffmanCodeLength; ++length) {
    int numCodes = counts[length - 1];
    for (int i = 0; i < numCodes; ++i) {
      // 生成指定位长的二进制字符串
      std::string codeStr = std::bitset<kMaxHuffmanCodeLength>(code).to_string().substr(kMaxHuffmanCodeLength - length, length);
      table[codeStr] = symbols[symInd++];
      code++;
    }
    code <<= 1;
  }
  return table;
}

把生成的码字与 symbols 列表中的第 k 个符号一一配对,得到 {码字 → 符号} 的查找表。这样我们可以输入一个 bit 流,得到一个 symbol。

此外,SOS 段告诉我们该使用哪个哈夫曼表,例如:

SOS segment
	File position: 677
	Length: 12
	Number of components: 3
	Component ID: 1
	Huffman table ID DC: 0
	Huffman table ID AC: 0
	Component ID: 2
	Huffman table ID DC: 1
	Huffman table ID AC: 1
	Component ID: 3
	Huffman table ID DC: 1
	Huffman table ID AC: 1
  • Y分量(亮度):使用第0号DC哈夫曼表、第0号AC哈夫曼表。
  • Cb分量(色度1):使用第1号DC哈夫曼表、第1号AC哈夫曼表。
  • Cr分量(色度2):使用第1号DC哈夫曼表、第1号AC哈夫曼表。

因此我们根据输入的分量信息以及是否是 DC,可以获得相应的哈夫曼表

2.6 哈夫曼解码

这部分立即直接看代码吧,理解了上面的内容后,就很简单啦。代码链接:deHuffman

2.7 剩下的步骤

哈夫曼解码后,我们得到了一个 mcu 的 dct 系数,首先对它做反量化,这个步骤我也写带了 deHuffman 函数中

// dequant
for (int i = 0; i < kMCUPixelSize; ++i) {
  decoded_data[i] *= qtable->data[i];
}
return decoded_data;

接着,反 zig-zag、DCT逆变换、逆电平位移,最后将 mcu 解码出来的数据填充到一个大数组里。整体逻辑如下代码,细节还请参考源码。

auto num_block_in_x_dir = sof0->width / h_block_size;
    auto num_block_in_y_dir = sof0->height / v_block_size;

    int16_t pre_dc_value_y = 0;
    int16_t pre_dc_value_u = 0;
    int16_t pre_dc_value_v = 0;

    auto mcu_count = 0;

    for (auto y = 0; y < num_block_in_y_dir; ++y) {
      for (auto x = 0; x < num_block_in_x_dir; ++x) {
        mcu_count++;

        auto y_data = deHuffman(parser, 0, pre_dc_value_y);
        auto u_data = deHuffman(parser, 1, pre_dc_value_u);
        auto v_data = deHuffman(parser, 2, pre_dc_value_v);

        auto zig_zag_y = deZigZag(y_data);
        auto zig_zag_u = deZigZag(u_data);
        auto zig_zag_v = deZigZag(v_data);

        auto idct_y = idct(zig_zag_y);
        auto idct_u = idct(zig_zag_u);
        auto idct_v = idct(zig_zag_v);

        levelShift(idct_y);
        levelShift(idct_u);
        levelShift(idct_v);

        // fill mcu to decoded data
        auto left_top_x = x * 8;
        auto left_top_y = y * 8;
        for (int j = 0; j < kMCUPixelSize; j++) {
          auto img_pos_x = left_top_x + j % 8;
          auto img_pos_y = left_top_y + j / 8;
          auto img_index = img_pos_y * sof0->width + img_pos_x;
          y_decoded_data_[img_index] = static_cast<uint8_t>(idct_y[j]);
          u_decoded_data_[img_index] = static_cast<uint8_t>(idct_u[j]);
          v_decoded_data_[img_index] = static_cast<uint8_t>(idct_v[j]);
        }
      }

2.8 保存为 RGB 图片

JPEG 解码后是 YUV 数据,在 main.cpp 中我们演示了如何将 yuv 数据转换为 rgb,并保存到本地。

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值