JPEG编码学习笔记(1)

学习背景

近期为了完成公司项目中视频合成能力的开发,笔者接触和学习了较多音视频、FFmpeg等相关知识。为了更进一步了解音视频文件、编解码等相关的知识,笔者计划从易到难地学习一些常见的媒体文件格式,通过实现其编/解码器的方式来深化学习成果。JPEG作为图片格式,其使用的编码技术已有不少和视频编码有相通之处,并且入门难度尚可,因此可以作为较好的学习对象。

相关链接

学习过程中以下这些资料和文章是笔者参考较多的

解码篇

拆解文件和读取元信息

通过上图我们可以知道,JPEG作为一种二进制文件,内部是由若干个带有特殊标记开头的片段(marker segment)组成的。每个片段的特殊标记后紧随的是此片段的长度(segment length),由此我们可以先将文件完整读取一遍,并将上述的片段拆解出来,后续再分别处理各个片段。

读取JPEG需要对二进制文件进行操作。而对JPEG文件进行分段,主要涉及到以下这些文件操作:

  • 以字节为单位读取文件内容,并判断是否与某字符串相等(判断是JFIF的哪个片段)
  • 获取文件当前的读取位置,或者改变文件当前的读取位置(一般称为seek操作,用来跳过大段的JPEG文件数据,并定位到下一个片段的开头)
  • 从文件内容中读取一个数字(用于获取片段的长度)

以下是代码示例

c++ 复制代码
void JpegDecoder::get_segments() {
  // JPEG文件必须以SOI头开始,marker为ffd8
  // read_assert_str_equal是一个宏,用来判断接下来读取的文件内容,是否等于某字符串
  read_assert_str_equal(this->file, buf, "\xff\xd8", "start of image header error");

  char marker[2];
  int seg_length;
  // 在到达文件结束位置之前,不断读取JPEG的文件片段
  while (true) {
    // 读取marker
    this->file.read(marker, 2);
    // 读取当前片段的长度,注意JPEG中的数字是大端序存储的,需要读取2个字节
    seg_length = read_u16_be(this->file) - 2;
    // 将此片段的位置和长度保持下来,方便后续快速跳转
    long long cur = this->file.tellg();
    segment_info_t info = { cur, seg_length };
    // 根据marker的不同,将片段的位置和长度信息添加到不同类型的片段数组中。
    // 某些片段(例如DHT, DQT等允许存在多个)
    if (!memcmp(marker, "\xff\xe0", 2)) {
      this->segments[segment_t::APP0].push_back(info);
    } else if (!memcmp(marker, "\xff\xdb", 2)) {
      this->segments[segment_t::DQT].push_back(info);
    } else if (!memcmp(marker, "\xff\xc4", 2)) {
      this->segments[segment_t::DHT].push_back(info);
    } else if (!memcmp(marker, "\xff\xc0", 2)) {
      this->segments[segment_t::SOF0].push_back(info);
    } else if (!memcmp(marker, "\xff\xfe", 2)) {
      this->segments[segment_t::COMMENT].push_back(info);
    } else if (!memcmp(marker, "\xff\xdd", 2)) {
      this->segments[segment_t::DRI].push_back(info);
    } else if (!memcmp(marker, "\xff\xda", 2)) {
      this->segments[segment_t::SOS].push_back(info);
      // SOS片段的最后会紧跟着压缩后的图像数据,此时不会再有其他的marker了。跳出循环
      break;
    }

    // 跳过当前片段,到达下一个片段的marker位置
    this->file.seekg(seg_length, ios::cur); 
  }
}

准备解码上下文

JPEG文件中,除了SOS(Start of scan)片段紧跟着的图像数据,其余的部分都是为解码提供各种参数的上下文。如果需要实现一个最简单的JPEG解码器(不考虑一些JPEG规范中的高级功能的情况下),以下这些上下文是解码过程中所必须的:

量化表x2

指的是分别用于亮度(Luma)和色度(Chroma)的两张量化表。量化表的形式上是一个长度为64的正整数的数组,用于8x8的块中每个系数(coefficient)的量化。我们可以在JPEG的DQT段(Define Quantization Table)中解析出来量化表,由于量化表是在文件中连续储存的,并且使用时也是连续的,我们直接将这段文件数据储存到数组中即可。

Huffman表x4

JPEG编码的最后一步,是使用Huffman编码,将一组系数(coefficient)转化为连续的Huffman编码形成的0和1的bitstream。因此在解码的场景中,第一步就需要我们基于Huffman表进行解码。

Huffman编码是著名的信息无损压缩算法,其特点是

  • 生成的编码为可变长度的,较为节省空间。实践中可以把一段信息中高频出现的符号分配给短的Huffman编码,达到更好的总体压缩率
  • 一个Huffman表中的某一编码,不会是其他任意编码的前缀。这使得Huffman表也可以被转化成一个二叉树,对应编码的符号均在树的叶子节点上。在解码的场景中,我们可以借由Huffman表转化成的二叉树,一边读取由0和1组成的压缩数据流,一边从树的根节点出发,选择向左或向右走,到达叶子节点时,则表示解出了一个有效的符号。相比使用符号和编码的映射表来说,使用二叉树会更加适合JPEG解码的场景,更为高效。

在JPEG标准中,每张Huffman表使用了一种非常紧凑的方式来存储,被称为 Huffman table specification。大致格式如下(注意下面仅为示意,实际文件中,每一个字符对应着一个字节,该字节不一定是可见的ascii字符)

css 复制代码
0 1 5 1 1 1 1 1 1 0 0 0 0 0 0 0 a b c d e f g h i j k l

上面这段 Huffman table specification 中,前16个字节表示从1-16每个长度分别有多少个编码 。从左至右,我们可以这样来解读:0表示这张Huffman表中,没有长度为1的编码;1表示有1个长度为2的编码;5表示有5个长度为3的编码;1表示有1个长度为4的编码。依此类推。剩余的所有字节,其总长度为前16个字节的数字之和(这里是1+5+1+1+1+1+1+1 = 12),表示Huffman表中所有的需要被编码的符号

仅仅是了解了 Huffman table specification的结构,仍然不足以理解实际的Huffman表或者二叉树是如何生成的。我们可以进一步地举例来看:

rust 复制代码
- 没有长度为1的编码
无需处理
- 有1个长度为2的编码
我们将长度为2的最小编码 00 赋给符号中的第一个a,即 a -> 00
- 有5个长度为3的编码
由于编码不能前缀重合的原则,则 00x 都不能使用了。我们将此时长度为3的最小编码 010 赋给下一个符号b,即 b -> 010;并依次确定其余的长度为5的编码 c -> 011, d -> 100, e -> 101, f -> 110
- 有1个长度为4的编码
仍然是由于编码不能前缀重合的原则,110x 不能使用了。我们将此时长度为4的最小编码 1110 赋给下一个符号g,即 g -> 1110
- 依此类推,剩余的编码为 h -> 11110, i -> 111110, j -> 1111110, k -> 11111110, l -> 111111110

基于以上理解,我们可以按下面的算法,将JPEG文件中的 Huffman table specification 转化为一个二叉树(这里主要参考了Let's Write a Simple JPEG Library, Part-II: The Decoder中 Huffman Tree 一节所提供的算法,并作了一定的改进)

构建HuffmanTree的要点在于:

  • 树的深度代表了编码的长度,长度为N的编码,所对应的符号的叶子节点就会出现在这个深度上。
  • 由于symbols(待编码的符号数组)是从短编码的符号长编码的符号顺序排列的,我们应从上到下逐层构建出树的节点
  • 又由于同长度的编码,其递进关系是二进制的递增(如上面的b,c,d,e符号,编码依次是010,011,100,101),这相当于在二叉树中,同一层的节点上进行向右遍历。

据此,我们可以采用这样的构建树算法

  • 创建一个队列,用来保存"当前树深度下,需要处理的节点列表"。
  • 对于某一编码长度(在树的同一深度上),有N个待编码的符号。则我们在队列中从左至右遍历这些节点,将这些空白节点填上符号,变为叶子节点,直至N个符号被用完为止。
  • 此深度上的剩余节点,将会作为下一个编码长度的编码前缀。因此我们从这些剩余节点上继续向下新增左右节点。并把这些新节点加入到队列中,并从队头将上一轮已遍历的节点出列。

下图中展示了先前的例子所对应的HuffmanTree的构建过程,可以看到节点是逐个深度加入到树中的。当前深度正在遍历的节点为粉色,从空白节点新增的,下一深度待遍历的节点为虚线白色。

c++ 复制代码
// HuffmanTree的构造函数,入参为Huffman table specification的两部分
HuffmanTree::HuffmanTree(char nb_sym[16], const char* symbols) {
    // 初始化树的根节点,同时也将根节点的左右子节点初始化
    this->root = std::make_shared<Node>();
    auto root_left = std::make_shared<Node>();
    auto root_right = std::make_shared<Node>();
    this->root->left = root_left;
    this->root->right = root_right;

    // 我们可以维护一个遍历某一深度的节点所使用的队列。队列的头部是当前深度需要遍历的节点,尾部是新增的下一深度需要遍历的节点。在下一深度的节点遍历开始之前,将当前深度的节点出列即可
    std::vector<std::shared_ptr<Node>> depth_queue { root_left, root_right };
    
    size_t j = 0;
    for (size_t depth = 0; depth < 16; depth++) {
        // 对于1-16每一个编码长度
        char nb_sym_in_depth = nb_sym[depth];

        size_t queue_prev_length = depth_queue.size();
        for (size_t i = 0; i < queue_prev_length; i++) {
            // 遍历每一个当前深度的节点
            Node* node = depth_queue[i].get();
            // 从左向右,依次将待编码的符号分配给各个节点
            if (nb_sym_in_depth > 0) {
                node->letter = symbols[j];
                j++;
                nb_sym_in_depth--;
            // 直到这个编码长度的符号没有了,则剩余的各个节点,需要继续向下新增左右节点
            } else {
                auto left = std::make_shared<Node>();
                auto right = std::make_shared<Node>();
                node->left = left;
                node->right = right;
                // 并作为下一次待遍历的节点,入列到队尾
                depth_queue.push_back(left);
                depth_queue.push_back(right);
            }
        }
        // 当前深度节点已全部遍历,从队头出列
        for (size_t i = 0; i < queue_prev_length; i++) {
            depth_queue.erase(depth_queue.begin());
        }
    }
}

在JPEG中共有4张Huffman表,2张用于Luma,2张用于Chroma。每2张Huffman表中,又有1张用于DC系数(即8x8的块中最左上角的值,用以代表二维图片中最主要的信息,例如整体的亮度),另一张用于AC系数(即8x8的块中其余63个值,代表了一个二维信号中的次要信息,例如一些细节的亮度变化)。由通过上面的算法,我们可以得到4个HuffmanTree,并在解码的过程中按需选择。

SOF, SOS 片段

SOF全称为Start of frame,SOS全称为SOS,全称为Start of scan。从JPEG文件的结构来看,Frame表示的是一整个图像,一个JPEG文件中只能包含一个Frame;Scan表示的是一段图像数据,一个JPEG文件中可以包括若干个Scan。 SOF和SOS均提供了对于JPEG解码的重要信息,特别是对一个JPEG文件进行了性质上的分类。JPEG标准中,规定了很多JPEG的变体,例如

  • Baseline or Progressive ,前者意味着JPEG文件中只包含一个清晰度的文件,图像会逐行解码加载;后者顾名思义,JPEG文件中包含了多张分辨率从低到高的图像,图像加载时会逐渐由模糊到清晰
  • Huffman or Arithmetic,这是两种熵编码的算法。使用Huffman方式较为主流
  • 8-bit or 12-bit,色彩的位深,后者使得JPEG的支持的色彩更为精细。8-bit为主流

如果只需要支持最主流JPEG解码的情况下(Baseline + Huffman + 8-bit),我们可以判断SOF的头是否是SOF0类型(marker为ffc0)即可。

同时,我们还需要从SOF,SOS中提取以下这些重要信息:

图像的宽高

存储于SOF片段的第2-5字节中,是两个16位大端数(注意顺序为先高后宽),分别读取到程序的变量中即可。

通道与Huffman table的对应关系

需要从SOS片段的第2个字节开始读取,一共有6个字节(对于3通道的图像来说)。所包含的信息可以通过下面的例子来说明

arduino 复制代码
// 这两个字节表示 第一个通道 Y,使用第0张DC Huffman表和第0张AC Huffman表
0x01 0x00
// 这两个字节表示 第二个通道Cb,使用第1张DC Huffman表和第1张AC Huffman表
0x02 0x11
// 这两个字节表示 第三个通道Cr,使用第1张DC Huffman表和第1张AC Huffman表
0x03 0x11

图像的通道信息、采样信息

在理解这部分信息之前,需要回顾一些基础概念

关于 YUV(YCbCr), Luma和Chroma, 色度下采样:

RGB是一种大家比较熟悉也容易理解的色彩模型,即将某种颜色通过Red, Green, Blue三种通道进行表示。一张图片,或者视频中的一帧,可以看成是三个通道上各自图像的合成结果。

而在图像、视频编码领域,YUV(YCbCr)则是更为常用的色彩模型。这种色彩模型使用了1个亮度通道(即Y,英文称为Luma或Luminance),和2个色度通道(即Cb和Cr,英文称为Chroma或Chrominance)来表示一个颜色。 YUV(YCbCr)的优势在于:可以通过对2个色度通道的图像进行缩小(更准确的说法是 下采样 subsampling),来实现图像的压缩。由于人眼对于色度变化的敏感度弱于对于亮度变化的敏感度。对色度的下采样 (Chroma subsampling)可以带来较好的压缩率。
关于数据单元和MCU:

数据单元(data unit),在JPEG的语境下指的是一个8x8大小的,对图像单个通道进行采样所形成的块。在JPEG编码规范中,诸如DCT/IDCT,Z字扫描,量化,游程编码等各类算法,也都是在8x8大小的最小单元下进行的

最小编码单元(Minimum coded unit,简称MCU),则是一个包括了所有通道的最小数据单元。由于一个MCU对应着图像上的某个特定位置,并且需要包括所有通道的数据,再加上有的通道可能出现下采样(导致各通道之间大小不统一),因此MCU的大小可能是8x8的倍数,不一定为8x8

图像的通道信息、采样信息,需要从SOF片段第7个字节开始读取,一共有9个字节(对于3通道的图像来说)。这些信息包括:每个通道与量化表的对应关系,有怎样的采样因子(sampling factor)等。 各个通道的采样因子是一个重要的解码参数,可以说很大程度上决定了解码逻辑的大致框架。我们可以举下面两个例子: 假设我们从图像中读取到的通道信息、采样信息的9个字节如下

arduino 复制代码
// 这三个字节分别表示 第一个通道 Y,水平/垂直采样因子为1:1,使用第0个量化表(Luma量化表)
0x01 0x11 0x00
// 这三个字节分别表示 第二个通道Cb,水平/垂直采样因子为1:1,使用第1个量化表(Chroma量化表)
0x02 0x11 0x01 
// 这三个字节分别表示 第三个通道Cr,水平/垂直采样因子为1:1,使用第1个量化表(Chroma量化表)
0x03 0x11 0x01

则从第2、5、8字节可知,三个通道的图像在编码时的采样因子相同,没有使用色度下采样(Chroma subsampling)。这就是俗称的YUV444图像格式。由此,我们可以确定:MCU的大小为8x8;并且从压缩后的数据流中解码时,每解出一个MCU所得到的数据中,只会包含Y, Cb, Cr通道的数据各一个,并按YCbCr的顺序排列。

另一个例子,假设9个字节如下

arduino 复制代码
// 这三个字节分别表示 第一个通道 Y,水平/垂直采样因子为2:2,使用第0个量化表(Luma量化表)
0x01 0x22 0x00
// 这三个字节分别表示 第二个通道Cb,水平/垂直采样因子为1:1,使用第1个量化表(Chroma量化表)
0x02 0x11 0x01 
// 这三个字节分别表示 第三个通道Cr,水平/垂直采样因子为1:1,使用第1个量化表(Chroma量化表)
0x03 0x11 0x01

仅第2个字节换成了0x22,这意味着Cb, Cr两个通道都使用了色度下采样,使得每4个Y通道的8x8采样,才能对应一个Cb和Cr通道的8x8采样。这就是俗称的YUV420图像格式。我们可以确定:MCU的大小为16x16;每解出一个MCU所得到的数据中,包含的通道数据的顺序为 Y1Y2Y3Y4CbCr。

以上两个例子(YUV444, YUV420)在JPEG解码时的采样方式以及bitstream中数据顺序的示意图如下

解析出了JPEG图像的通道信息和采样信息,我们才能为接下来的解码过程进行一些参数初始化和buffer的预分配操作,以下面的代码为例来说明:

c++ 复制代码
// 用于读取SOF0片段中的信息,并进行相应初始化的函数
void JpegDecoder::handle_sof0() {
  segment_info_t info = this->segments[segment_t::SOF0][0]; // only one sof0 segment
  // 之前已经为JPEG文件分段完毕,这里可以直接跳转到SOF0
  this->file.seekg(info.offset, ios::beg);
  // 只支持8-bit的JPEG文件
  read_assert_str_equal(this->file, buf, "\x08", "data precision not 8");
  // 按照先高后宽的顺序读取两个16位大端数,得到图片的宽高
  this->h = read_u16_be(this->file);
  this->w = read_u16_be(this->file);
  // 只支持3通道的JPEG文件
  read_assert_str_equal(this->file, buf, "\x03", "image component not 3");
  // 读取图像的通道和采样信息(一共9个字节)
  // frame_components是一个长度为3的struct数组,可以自动将字节中的信息拆解到相应的field中
  this->file.read((char*)&this->frame_components[0], 9);

  int* buf_ptr;
  // 读取3个通道各自的水平/垂直采样因子
  auto [yh, yv] = this->frame_components[0].sampling_factor_packed;
  auto [cbh, cbv] = this->frame_components[1].sampling_factor_packed;
  auto [crh, crv] = this->frame_components[2].sampling_factor_packed;

  // YUV444格式的情况下
  if (
    yh == 1 && yv == 1 &&
    cbh == 1 && cbv == 1 &&
    crh == 1 && crv == 1
  ) {
    // 确定MCU的宽高以及图像格式
    this->mcu_w = 8; this->mcu_h = 8;
    this->sampl = sampling_t::YUV444;

    // 为解码阶段预分配MCU所需的内存,Y,Cb,Cr通道各自一个8x8的buffer即可
    buf_ptr = new int[64];
    this->y_bufs.push_back(buf_ptr);
    buf_ptr = new int[64];
    this->cb_bufs.push_back(buf_ptr);
    buf_ptr = new int[64];
    this->cr_bufs.push_back(buf_ptr);
  // YUV420格式的情况下
  } else if (
    yh == 2 && yv == 2 &&
    cbh == 1 && cbv == 1 &&
    crh == 1 && crv == 1
  )  {
    // 确定MCU的宽高以及图像格式
    this->mcu_w = 16; this->mcu_h = 16;
    this->sampl = sampling_t::YUV420;

    // 为解码阶段预分配MCU所需的内存,Y通道4个,Cb,Cr通道各自一个8x8的buffer
    for (int i = 0; i < 4; i++) {
      buf_ptr = new int[64];
      this->y_bufs.push_back(buf_ptr);
    } 
    buf_ptr = new int[64];
    this->cb_bufs.push_back(buf_ptr);
    buf_ptr = new int[64];
    this->cr_bufs.push_back(buf_ptr);
  // 这里还可以支持更多的格式,不再赘述
  } else {
    throw std::runtime_error("not supported");
  }
}

解码开始前的最后一步

在准备好所有的解码参数、表等上下文之后,在实际开始JPEG图像数据的解码之前,我们还需要执行一个被称为de-stuffing的数据流操作。这个概念难以翻译,大意是解码时,遇到数据流中(看成byte stream的情况下),遇到两个字节为ff00的情况,需要当作一个字节ff来处理。这是由于JPEG在编码时,如果编码器输出了一个字节ff,则容易和分段marker混淆,需要在编码后的数据流中,使用ff00来代表ff。

c++ 复制代码
void JpegDecoder::init_bitstream() {
  char byte, cur, next;
  // 跳转到SOS块的最后,也就是JPEG压缩后的数据开始的位置
  this->file.seekg(this->segments[segment_t::SOS][0].offset + this->segments[segment_t::SOS][0].length, ios::beg);

  while(true) {
    cur = (char)this->file.peek();
    if (cur == '\xff') {
      this->file.seekg(1, ios::cur);
      next = (char)this->file.peek();
      // 前后两个字节分别是ff, 00的情况下,只保留ff
      if (next == '\x00') {
        this->bitstream += '\xff';
        this->file.seekg(1, ios::cur);
      // 前后两个字节分别是ff, d9的情况下,表示JPEG文件结束
      } else if (next == '\xd9') {
        break;
      // 第一个字节是ff,其余情况下,数据流中正常保留这两个字节
      } else {
        this->bitstream += '\xff';
        this->file.seekg(-1, ios::cur);
        this->file.read(&byte, 1);
        this->bitstream += byte;
      }
    // 其余情况下,数据流中正常保留这个字节
    } else {
      this->file.read(&byte, 1);
      this->bitstream += byte;
    }
  }
}

解码过程

以下代码,将所有JPEG中的图片数据解码出来,并以ppm文件格式(一种无压缩的图片格式)输出到stdout中。

c++ 复制代码
void JpegDecoder::decode() {
  // 三个通道,上一次解出的DC值(由于JPEG中所编码的DC信息实际上是相邻的数据单元之间的DC差量)
  int dc_y = 0;
  int dc_cr = 0;
  int dc_cb = 0;

  // 最终输出的未压缩的RGB图像buffer
  auto* output = new uint8_t[this->w*this->h*3];

  if (this->sampl == sampling_t::YUV420) {
    for (int y_mcu = 0; y_mcu < this->h / this->mcu_h; y_mcu++) {
      for (int x_mcu = 0; x_mcu < this->w / this->mcu_w; x_mcu++) {
        // 逐个MCU进行解码,在单个MCU中,按照特定的图像格式规定的通道数据顺序进行解码
        // YUV420的情况下,通道数据顺序为Y1Y2Y3Y4CbCr

        // 这里的decode_8x8_per_component方法封装了在单个数据单元中
        // 对单个通道的数据进行Huffman, 游程,反量化,Z字扫描,IDCT等解码过程
        // 第一个参数用于写入解出的8x8数据,第三个参数用于指定是哪一个通道
        dc_y = this->decode_8x8_per_component(this->y_bufs[0], dc_y, 0);
        dc_y = this->decode_8x8_per_component(this->y_bufs[1], dc_y, 0);
        dc_y = this->decode_8x8_per_component(this->y_bufs[2], dc_y, 0);
        dc_y = this->decode_8x8_per_component(this->y_bufs[3], dc_y, 0);
        dc_cb = this->decode_8x8_per_component(this->cb_bufs[0], dc_cb, 1);
        dc_cr = this->decode_8x8_per_component(this->cr_bufs[0], dc_cr, 2);
        // - 解出三个通道的buffer后,在下面这个方法中进行色彩空间转换以及输出
        // - 由于YUV420的MCU大小为16x16,所以总共可以输出4个8x8块
        // - identical, upsample_xx_xx等参数是c++中的lambda函数
        //   用于表达采样时采样点的变换过程
        // - 最后的三个参数用于定位MCU
        //   确保当前MCU解码后输出结果是在output buffer中正确的位置
        output_rgb_8x8_to_buffer(
          output, this->y_bufs[0], this->cb_bufs[0], this->cr_bufs[0],
          identical, upsample_top_left, upsample_top_left,
          y_mcu*this->mcu_h, x_mcu*this->mcu_w, this->w
        );
        output_rgb_8x8_to_buffer(
          output, this->y_bufs[1], this->cb_bufs[0], this->cr_bufs[0],
          identical, upsample_bottom_left, upsample_bottom_left,
          y_mcu*this->mcu_h, x_mcu*this->mcu_w + 8, this->w
        );
        output_rgb_8x8_to_buffer(
          output, this->y_bufs[2], this->cb_bufs[0], this->cr_bufs[0],
          identical, upsample_top_right, upsample_top_right,
          y_mcu*this->mcu_h + 8, x_mcu*this->mcu_w, this->w
        );
        output_rgb_8x8_to_buffer(
          output, this->y_bufs[3], this->cb_bufs[0], this->cr_bufs[0], 
          identical, upsample_bottom_right, upsample_bottom_right,
          y_mcu*this->mcu_h + 8, x_mcu*this->mcu_w + 8, this->w
        );
      }
    }
  } else if (this->sampl == sampling_t::YUV444) {
    for (int y_mcu = 0; y_mcu < this->h / this->mcu_h; y_mcu++) {
      for (int x_mcu = 0; x_mcu < this->w / this->mcu_w; x_mcu++) {
        // YUV444的情况则更简单,通道数据顺序为YCbCr
        dc_y = this->decode_8x8_per_component(this->y_bufs[0], dc_y, 0);
        dc_cb = this->decode_8x8_per_component(this->cb_bufs[0], dc_cb, 1);
        dc_cr = this->decode_8x8_per_component(this->cr_bufs[0], dc_cr, 2);

        // 由于MCU大小也为8x8,只需要输出一次到output buffer即可,并且不需要进行下采样
        output_rgb_8x8_to_buffer(
          output, this->y_bufs[0], this->cb_bufs[0], this->cr_bufs[0],
          identical, identical, identical,
          y_mcu*this->mcu_h, x_mcu*this->mcu_w, this->w
        );
      }
    }
  } else {
    throw "not supported";
  }
  // 将output整体以ppm的格式输出到stdout
  printf("P6\n%d %d\n255\n", this->w, this->h);
  fwrite(output, 1, this->w*this->h*3, stdout);

  delete[] output;
}

其中的decode_8x8_per_component方法主要涉及Huffman,游程,反量化,Z字扫描,IDCT等算法的解码过程,将在后续的文章中进行剖析。

相关推荐
TANGLONG22233 分钟前
【初阶数据结构与算法】八大排序之非递归系列( 快排(使用栈或队列实现)、归并排序)
java·c语言·数据结构·c++·算法·蓝桥杯·排序算法
不想当程序猿_36 分钟前
【蓝桥杯每日一题】与或异或——DFS
c++·算法·蓝桥杯·深度优先
cccccc语言我来了1 小时前
c++-----------------多态
java·开发语言·c++
sunny-ll1 小时前
【C++】explicit关键字详解(explicit关键字是什么? 为什么需要explicit关键字? 如何使用explicit 关键字)
c语言·开发语言·c++·算法·面试
轩源源1 小时前
C++草原三剑客之一:继承
开发语言·数据结构·c++·算法·青少年编程·继承·组合
未知陨落2 小时前
leetcode题目(1)
c++·leetcode
半盏茶香4 小时前
C语言勘破之路-最终篇 —— 预处理(下)
c语言·开发语言·c++·算法
_君莫笑7 小时前
【视频】将yuv420p的一帧数据写入文件
c++·音视频·yuv420p
测试盐8 小时前
c++编译过程初识
开发语言·c++
single59410 小时前
【c++笔试强训】(第四十五篇)
java·开发语言·数据结构·c++·算法