【实验五】JPEG原理分析及JPEG解码器的调试

一、实验目的

掌握JPEG编解码系统的基本原理。初步掌握复杂的数据压缩算法实现,并能根据理论分析需要实现所对应数据的输出。

二、实验原理

1.编解码原理

  1. 编码过程

    编码原理框图

    (1)亮度信号0偏置电平下移
    对于灰度级是2^n的像素,通过减去2^{n-1},将无符号的整数值变成有符号数,从而降低亮度信号直流的系数,减少数据量。
    (2)做8*8的DCT变换
    对图像进行分块,通过DCT变换去除图像数据的相关性,便于量化过程去除图像数据的空间冗余。
    (3)对8*8的块系数做量化
    使用色度和亮度两种量化表,根据人眼视觉特性对低频分量采取较细的量化,对高频分量采取较粗的量化。
    (4)对DC系数做预测编码+VLC编码
    由于8*8图象块经过DCT变换后得到的DC直流系数具有系数数值较大、数值变化不大这两个特点,因此对DC系数采用差分脉冲调制编码(DPCM)技术,对相邻图像块之间的DC系数的差值进行编码,再进行熵编码。
    (5)对AC系数做之字形扫描+游程编码+VLC编码
    由于DCT变换后系数集中在左上角,因此采用之字形扫描降低数据量,再进行熵编码。

  2. 解码过程
    解码过程是编码的逆过程。

2.文件格式

2.1 Segment的组织形式

JPEG 在文件中以 Segment 的形式组织,它具有以下特点:

  • 均以 0xFF 开始,后跟 1 byte 的 Marker 和 2 byte 的 Segment length(包含表示Length 本身所占用的 2 byte,不含“0xFF” + “Marker” 所占用的 2 byte);
  • 采用 Motorola 序(相对于 Intel 序),即保存时高位在前低位在后
  • Data 部分中,0xFF 后若为 0x00,则跳过此字节不予处理;

2.2 JPEG的 Segment Marker

  • 非分层霍夫曼编码——帧标记的开始
符号 标记代码 说明
SOF_0 0XFFC0 Baseline DCT
SOF_1 0XFFC1 Extended sequential DCT
SOF_2 0XFFC2 Progressive DCT
SOF_3 0XFFC3 Spatial (sequential) lossless
  • 分层霍夫曼编码——帧标记的开始
符号 标记代码 说明
SOF_5 0XFFC5 Differential sequential DCT
SOF_6 0XFFC6 Differential progressive DCT
SOF_7 0XFFC7 Differential spatial lossless
  • 非分层算术编码——帧标记的开始
符号 标记代码 说明
JPG 0XFFC8 Reserved for JPEG extensions
SOF_9 0XFFC9 Extended sequential DCT
SOF_{10} 0XFFCA Progressive DCT
SOF_{11} 0XFFCB Spatial (sequential) Lossless
  • 分层算术编码——帧标记的开始
符号 标记代码 说明
SOF_{13} 0XFFCD Differential sequential DCT
SOF_{14} 0XFFCE Differential progressive DCT
SOF_{15} 0XFFCF Differential spatial lossless

2.3 JPEG 的解码流程

(1)读取文件
  /* Load the Jpeg into memory */
  fp = fopen(infilename, "rb");
  if (fp == NULL)
    exitmessage("Cannot open filename\n");
  length_of_file = filesize(fp);
  buf = (unsigned char *)malloc(length_of_file + 4);
  if (buf == NULL)
    exitmessage("Not enough memory for loading file\n");
  fread(buf, length_of_file, 1, fp);
  fclose(fp);
(2)解析Segment Marker
static int parse_JFIF(struct jdec_private *priv, const unsigned char *stream)
{
  int chuck_len;
  int marker;
  int sos_marker_found = 0;
  int dht_marker_found = 0;
  const unsigned char *next_chunck;

  /* Parse marker */
  while (!sos_marker_found)
   {
     if (*stream++ != 0xff)
       goto bogus_jpeg_format;
     /* Skip any padding ff byte (this is normal) */
     while (*stream == 0xff)
       stream++;

     marker = *stream++;
     chuck_len = be16_to_cpu(stream);
     next_chunck = stream + chuck_len;
     switch (marker)
      {
       case SOF:
     if (parse_SOF(priv, stream) < 0)
       return -1;
     break;
       case DQT:
     if (parse_DQT(priv, stream) < 0)
       return -1;
     break;
       case SOS:
     if (parse_SOS(priv, stream) < 0)
       return -1;
     sos_marker_found = 1;
     break;
       case DHT:
     if (parse_DHT(priv, stream) < 0)
       return -1;
     dht_marker_found = 1;
     break;
       case DRI:
     if (parse_DRI(priv, stream) < 0)
       return -1;
     break;
       default:
     break;
      }
     stream = next_chunck;
   }

  if (!dht_marker_found) {
    build_default_huffman_tables(priv);
  }

#ifdef SANITY_CHECK
  if (   (priv->component_infos[cY].Hfactor < priv->component_infos[cCb].Hfactor)
      || (priv->component_infos[cY].Hfactor < priv->component_infos[cCr].Hfactor))
    snprintf(error_string, sizeof(error_string),"Horizontal sampling factor for Y should be greater than horitontal sampling factor for Cb or Cr\n");
  if (   (priv->component_infos[cY].Vfactor < priv->component_infos[cCb].Vfactor)
      || (priv->component_infos[cY].Vfactor < priv->component_infos[cCr].Vfactor))
    snprintf(error_string, sizeof(error_string),"Vertical sampling factor for Y should be greater than vertical sampling factor for Cb or Cr\n");
  if (   (priv->component_infos[cCb].Hfactor!=1) 
      || (priv->component_infos[cCr].Hfactor!=1)
      || (priv->component_infos[cCb].Vfactor!=1)
      || (priv->component_infos[cCr].Vfactor!=1))
    snprintf(error_string, sizeof(error_string),"Sampling other than 1x1 for Cr and Cb is not supported");
#endif

  return 0;
bogus_jpeg_format:
  return -1;
}

1. 解析SOI

SOI表示图像开始,标记代码为0xFFD8,2字节。

  /* Identify the file */
  if ((buf[0] != 0xFF) || (buf[1] != SOI)) /* 必须以FFD8开始 */
    snprintf(error_string, sizeof(error_string),"Not a JPG file ?\n");

2.解析APP0

APP0表示应用程序保留标记,标记代码为0xFFE0,2字节。
APPN表示应用细节信息,标记代码范围为0xFFE1~0xFFEF。

具体字段 大小 说明
数据长度 2字节 9个字段的总长度
标识符 5字节 固定值0x4A46494600,即字符串JFIF0
版本号 2字节 一般为0x0102,版本号1.2
X和Y的密度单位 1字节 0:无单位 1:点数/英寸 2:点数/厘米
X方向像素密度 2字节 取值范围未知
Y方向像素密度 2字节 取值范围未知
缩略图水平像素数目 1字节 取值范围未知
缩略图垂直像素数目 1字节 取值范围未知
缩略图RGB位图 可能是3的倍数 缩略RGB位图数据
SOI APP0二进制信息
  • FF D8:图像开始
  • FF E0:应用程序保留标记
  • 00 10:数据长度16字节
  • 4A 46 49 00:标识符JFIF0
  • 01 01:版本号1.1
  • 00:密度无单位
  • 00 01:X方向像素密度1
  • 00 01:Y方向像素密度1
  • 00 00:无缩略图

3.解析DQT

DQT表示量化表,标记代码为0xFFDB,2字节。
包含数据长度量化表两个字段。
数据长度2字节,量化表长度是数据长度-2。

量化表字段 长度 说明
精度及量化表ID 1字节 高4位表示精度,0:8位 1:16位;低4位表示量化表ID,取值0~3
表项 (64*(精度+1))字节 ----
static int parse_DQT(struct jdec_private *priv, const unsigned char *stream)
{
  int qi;
  float *table;
  const unsigned char *dqt_block_end;

  dqt_block_end = stream + be16_to_cpu(stream);
  stream += 2;  /* Skip length */

  while (stream < dqt_block_end)
   {
     qi = *stream++;
#if SANITY_CHECK
     if (qi>>4) /* 量化表第一字节高4位只能是0 */
       snprintf(error_string, sizeof(error_string),"16 bits quantization table is not supported\n");
     if (qi>4) /* 量化表第一字节低4位不能超过4 */
       snprintf(error_string, sizeof(error_string),"No more 4 quantization table is supported (got %d)\n", qi);
#endif
     table = priv->Q_tables[qi];
     build_quantization_table(table, stream);/* 建立量化表 */
     stream += 64;/* 精度为8,量化表大小为64字节 */
   }
  return 0;
}
DQT
  • FF DB:DQT开始
  • 00 43:数据长度67字节
  • 00 02:精度8位,量化表ID为2

4.解析SOF0

SOF0表示帧图像开始,标记代码为0xFFC0,2字节。

具体字段 大小 说明
数据长度 2字节 6个字段的总长度
精度 1字节 每个数据样本的位数
图像高度 2字节 单位为像素
图像宽度 2字节 单位为像素
颜色分量数 1字节 1:灰度图;3:YCrCb或YIQ;4:CMYK
颜色分量信息 颜色分量数×3字节(通常为9字节) 颜色分量ID:1字节;水平/垂直采样因子:1字节;量化表:1字节
static int parse_SOF(struct jdec_private *priv, const unsigned char *stream)
{
  int i, width, height, nr_components, cid, sampling_factor;
  int Q_table;
  struct component *c;

  print_SOF(stream);/* 在trace中输出 */

  height = be16_to_cpu(stream+3);
  width  = be16_to_cpu(stream+5);
  nr_components = stream[7];
#if SANITY_CHECK
  if (stream[2] != 8) /* 不支持8位以外的精度 */
    snprintf(error_string, sizeof(error_string),"Precision other than 8 is not supported\n");
  if (width>JPEG_MAX_WIDTH || height>JPEG_MAX_HEIGHT)/* 宽高超过最大值 */
    snprintf(error_string, sizeof(error_string),"Width and Height (%dx%d) seems suspicious\n", width, height);
  if (nr_components != 3)/* 只支持三种颜色分量的情况 */
    snprintf(error_string, sizeof(error_string),"We only support YUV images\n");
  if (height%16) /* 宽高双字对齐 */
    snprintf(error_string, sizeof(error_string),"Height need to be a multiple of 16 (current height is %d)\n", height);
  if (width%16)
    snprintf(error_string, sizeof(error_string),"Width need to be a multiple of 16 (current Width is %d)\n", width);
#endif
  stream += 8; /* 开始读颜色分量信息 */
  for (i=0; i<nr_components; i++) {
     cid = *stream++;/* 颜色分量ID */
     sampling_factor = *stream++;/* 采样因子 */
     Q_table = *stream++;/* 当前分量使用的量化表ID */
     c = &priv->component_infos[i];
#if SANITY_CHECK
     c->cid = cid;
     if (Q_table >= COMPONENTS)
       snprintf(error_string, sizeof(error_string),"Bad Quantization table index (got %d, max allowed %d)\n", Q_table, COMPONENTS-1);
#endif
     c->Vfactor = sampling_factor&0xf;/* 高4位 水平采样因子 */
     c->Hfactor = sampling_factor>>4;/* 低4位 垂直采样因子 */
     c->Q_table = priv->Q_tables[Q_table];
  }
  priv->width = width;
  priv->height = height;

  return 0;
}
SOF0
  • FF C0:SOF0,帧图像开始
  • 00 11:数据长度17字节
  • 08:精度8位
  • 04 00:高度1024
  • 04 00:宽度1024
  • 03:颜色格式为YUV
  • 01 11 00: 颜色分量Y,420格式取样,量化表ID为0
  • 02 11 01: 颜色分量U,420格式取样,量化表ID为1
  • 03 11 01: 颜色分量V,420格式取样,量化表ID为1

5.解析DHT

DHT表示哈夫曼表,标记代码为0xFFC4,2字节。
包含数据长度哈夫曼表
数据长度2字节,哈夫曼表为数据长度-2字节。

具体字段 大小 说明
表ID和表类型 1字节 高4位:类型,0代表直流,1代表交流
低4位:哈夫曼表ID,直流和交流分开编码
不同位数的码字数量 16字节 ----
static int parse_DHT(struct jdec_private *priv, const unsigned char *stream)
{
  unsigned int count, i;
  unsigned char huff_bits[17];
  int length, index;

  length = be16_to_cpu(stream) - 2;/* 哈夫曼表长度 */
  stream += 2;  /* Skip length */

  while (length>0) {
     index = *stream++;/* 表ID和表类型 */

     /* We need to calculate the number of bytes 'vals' will takes */
     huff_bits[0] = 0;
     count = 0;
     for (i=1; i<17; i++) {
    huff_bits[i] = *stream++;
    count += huff_bits[i];/* 计算码字数量 */
     }
#if SANITY_CHECK
     if (count >= HUFFMAN_BITS_SIZE)/* 码字数量超过最大值 */
       snprintf(error_string, sizeof(error_string),"No more than %d bytes is allowed to describe a huffman table", HUFFMAN_BITS_SIZE);
     if ( (index &0xf) >= HUFFMAN_TABLES)/* 索引超过个数 */
       snprintf(error_string, sizeof(error_string),"No more than %d Huffman tables is supported (got %d)\n", HUFFMAN_TABLES, index&0xf);
#endif

     if (index & 0xf0 )/* 形成交流DHT表 */
       build_huffman_table(huff_bits, stream, &priv->HTAC[index&0xf]);
     else/* 形成直流DHT表 */
       build_huffman_table(huff_bits, stream, &priv->HTDC[index&0xf]);

     length -= 1;
     length -= 16;
     length -= count;
     stream += count;
  }

  return 0;
}
DHT
  • 黑色部分:数据长度为35。
  • 红色部分:表类型为DC直流,ID为0。
  • 绿色部分:没有1位的码字;2位的码字有3个;3位-9位的码字各一个;没有9位及以上的码字。
  • 蓝色部分:由绿色部分数据知道,此哈夫曼树有0+3+1+1+1+1+1+1+1=10个叶子结点,即本字段应该有10个字节。这段数据表示10个叶子结点按从小到大排列,其权值依次为04、 05、 06、 03、 02、 01、 00、09、 07、 08 (16进制)。

6.解析SOS

SOS表示扫描开始,标记代码为0xFFDA,2字节。

具体字段 大小 说明
数据长度 2字节 6个字段的总长度
颜色分量数 1字节 1:灰度图;3:YCrCb或YIQ;4:CMYK
颜色分量信息
颜色分量ID 1字节
直流/交流系数表号 1字节 高4位:直流分量使用的哈夫曼树编号;
低4位:交流分量使用的哈夫曼树编号
压缩图像数据 3字节 谱选择开始 1字节 固定值0x00;
谱选择结束 1字节 固定值0x3F;
谱选择 1字节 在基本JPEG中总为00
static int parse_SOS(struct jdec_private *priv, const unsigned char *stream)
{
  unsigned int i, cid, table;
  unsigned int nr_components = stream[2];/* 获取颜色分量数 */

#if SANITY_CHECK
  if (nr_components != 3) /* 颜色分量数只能为3 */
    snprintf(error_string, sizeof(error_string),"We only support YCbCr image\n");
#endif

  stream += 3;/* 获取颜色分量信息:颜色分量和哈夫曼树编号*/
  for (i=0;i<nr_components;i++) {
     cid = *stream++;
     table = *stream++;
#if SANITY_CHECK
     if ((table&0xf)>=4)/* 直流和交流的哈夫曼树不能超过2个 */
    snprintf(error_string, sizeof(error_string),"We do not support more than 2 AC Huffman table\n");
     if ((table>>4)>=4)
    snprintf(error_string, sizeof(error_string),"We do not support more than 2 DC Huffman table\n");
     if (cid != priv->component_infos[i].cid)/* 颜色分量应该与SOF部分对应 */
        snprintf(error_string, sizeof(error_string),"SOS cid order (%d:%d) isn't compatible with the SOF marker (%d:%d)\n",
          i, cid, i, priv->component_infos[i].cid);
#endif
     priv->component_infos[i].AC_table = &priv->HTAC[table&0xf];
     priv->component_infos[i].DC_table = &priv->HTDC[table>>4];
  }
  priv->stream = stream+3;
  return 0;
}
SOS
  • 红色部分:数据长度为12。
  • 绿色部分:颜色分量为3。
  • 蓝色部分:颜色分量Y,直流和交流的哈夫曼树编号都为0;颜色分量UV,直流和交流的哈夫曼树编号都为1。
  • 紫色部分:压缩图像数据。
  • 剩余数据为压缩数据。
(3)依据每个分量的水平、垂直采样因子计算 MCU 的大小,并得到每个MCU 中 8*8宏块的个数
xstride_by_mcu = ystride_by_mcu = 8;/* 初始化为4:4:4采样情况 */
  if ((priv->component_infos[cY].Hfactor | priv->component_infos[cY].Vfactor) == 1) {
      /* 4:4:4采样 */
     decode_MCU = decode_mcu_table[0];
     convert_to_pixfmt = colorspace_array_conv[0];
  } else if (priv->component_infos[cY].Hfactor == 1) {
      /* 4:2:2采样 */
     decode_MCU = decode_mcu_table[1];
     convert_to_pixfmt = colorspace_array_conv[1];
     ystride_by_mcu = 16;/* MCU 8*16*/
  } else if (priv->component_infos[cY].Vfactor == 2) {
      /* 4:2:0采样 */
     decode_MCU = decode_mcu_table[3];
     convert_to_pixfmt = colorspace_array_conv[3];
     xstride_by_mcu = 16;
     ystride_by_mcu = 16;/* MCU 16*16*/
  } else {
     decode_MCU = decode_mcu_table[2];
     convert_to_pixfmt = colorspace_array_conv[2];
     xstride_by_mcu = 16;
  }
(4)对每个 MCU 解码(依照各分量水平、垂直采样因子对 MCU 中每个分量宏块解码)
/* 4:4:4 decode */
static void decode_MCU_1x1_3planes(struct jdec_private *priv)
{
  // Y
  process_Huffman_data_unit(priv, cY);/* 以 8*8 宏块为单位进行 Huffman 解码 */
  IDCT(&priv->component_infos[cY], priv->Y, 8);/* 反DCT变换 */
  
  // Cb
  process_Huffman_data_unit(priv, cCb);
  IDCT(&priv->component_infos[cCb], priv->Cb, 8);

  // Cr
  process_Huffman_data_unit(priv, cCr);
  IDCT(&priv->component_infos[cCr], priv->Cr, 8);
}
/*
 * Decode a 2x1
 *  .-------.
 *  | 1 | 2 |
 *  `-------'
 */
static void decode_MCU_2x1_3planes(struct jdec_private *priv)//4:2:2
{
  // Y
  process_Huffman_data_unit(priv, cY);
  IDCT(&priv->component_infos[cY], priv->Y, 16);
  process_Huffman_data_unit(priv, cY);
  IDCT(&priv->component_infos[cY], priv->Y+8, 16);

  // Cb
  process_Huffman_data_unit(priv, cCb);
  IDCT(&priv->component_infos[cCb], priv->Cb, 8);

  // Cr
  process_Huffman_data_unit(priv, cCr);
  IDCT(&priv->component_infos[cCr], priv->Cr, 8);
}
/*
* Decode a 2x2
* .-------.
* | 1 | 2 |
* |---+---|
* | 3 | 4 |
* `-------'
*/
static void decode_MCU_2x2_3planes(struct jdec_private *priv) // 4:2:0
{
 // Y
 process_Huffman_data_unit(priv, cY);
 IDCT(&priv->component_infos[cY], priv->Y, 16);
 process_Huffman_data_unit(priv, cY);
 IDCT(&priv->component_infos[cY], priv->Y+8, 16);
 process_Huffman_data_unit(priv, cY);
 IDCT(&priv->component_infos[cY], priv->Y+64*2, 16);
 process_Huffman_data_unit(priv, cY);
 IDCT(&priv->component_infos[cY], priv->Y+64*2+8, 16);
 // Cb
 process_Huffman_data_unit(priv, cCb);
 IDCT(&priv->component_infos[cCb], priv->Cb, 8);
 // Cr
 process_Huffman_data_unit(priv, cCr);
 IDCT(&priv->component_infos[cCr], priv->Cr, 8);
}
/* 以 8*8 宏块为单位进行 Huffman 解码 */
static void process_Huffman_data_unit(struct jdec_private *priv, int component)
{ 
   unsigned char j;
   unsigned int huff_code;
   unsigned char size_val, count_0;
   struct component *c = &priv->component_infos[component];
   struct component *c = &priv->component_infos[component];
   short int DCT[64];

  /* Initialize the DCT coef table */
  memset(DCT, 0, sizeof(DCT));

  /* DC coefficient decoding */
  huff_code = get_next_huffman_code(priv, c->DC_table);

  if (huff_code) {
      /* 查表得直流的DCT系数 (残差值)*/
     get_nbits(priv->reservoir, priv->nbits_in_reservoir, priv->stream, huff_code, DCT[0]);
     DCT[0] += c->previous_DC;/* 差分编码恢复原值,保存上一个值 */
     c->previous_DC = DCT[0];
  } else {
     DCT[0] = c->previous_DC;
  }

  /* AC coefficient decoding */
  j = 1;
  while (j<64)
   {
     huff_code = get_next_huffman_code(priv, c->AC_table);

     size_val = huff_code & 0xF;/* 幅度 */
     count_0 = huff_code >> 4;/* 游程长度 */

     if (size_val == 0)/* 0 不是一个有效的 Amplitude 值,这里做零游程标志 */
      { /* RLE */
    if (count_0 == 0)
      break;    /* EOB found, go out */
    else if (count_0 == 0xF)
      j += 16;  /* skip 16 zeros */
      }
     else
      {
    j += count_0;   /* skip count_0 zeroes */
    if (__unlikely(j >= 64))
     {
       snprintf(error_string, sizeof(error_string), "Bad huffman data (buffer overflow)");
       break;
     }
     /* 查表得交流的DCT系数 */
    get_nbits(priv->reservoir, priv->nbits_in_reservoir, priv->stream, size_val, DCT[j]);
    j++;
      }
   }

  for (j = 0; j < 64; j++)
    c->DCT[j] = DCT[zigzag[j]];/* ZigZag序保存 */
}
(5)解析到 EOI,解码结束
for (y=0; y < priv->height/ystride_by_mcu; y++)//行循环
   {
     priv->plane[0] = priv->components[0] + (y * bytes_per_blocklines[0]);
     priv->plane[1] = priv->components[1] + (y * bytes_per_blocklines[1]);
     priv->plane[2] = priv->components[2] + (y * bytes_per_blocklines[2]);
     for (x=0; x < priv->width; x+=xstride_by_mcu)//列循环
      {
    decode_MCU(priv);//Huffman解码+IDCT
    convert_to_pixfmt(priv);//转换为指定格式
    priv->plane[0] += bytes_per_mcu[0];
    priv->plane[1] += bytes_per_mcu[1];
    priv->plane[2] += bytes_per_mcu[2];
    if (priv->restarts_to_go>0)
     {
       priv->restarts_to_go--;
       if (priv->restarts_to_go == 0)
        {
          priv->stream -= (priv->nbits_in_reservoir/8);
          resync(priv);//清空所有颜色分量
          if (find_next_rst_marker(priv) < 0)//查找RST标记
        return -1;
        }
     }
      }
   }
(6)将 Y、Cb、Cr 转化为需要的色彩空间并保存
/**
 *  YCrCb -> YUV420P (1x1)
 *  .---.
 *  | 1 |
 *  `---'
 */
static void YCrCB_to_YUV420P_1x1(struct jdec_private *priv)
{
  const unsigned char *s, *y;
  unsigned char *p;
  int i,j;

  p = priv->plane[0];
  y = priv->Y;
  for (i=0; i<8; i++)//Y
   {
     memcpy(p, y, 8);
     p+=priv->width;
     y+=8;
   }

  p = priv->plane[1];
  s = priv->Cb;
  for (i=0; i<8; i+=2)//Cb
   {
     for (j=0; j<8; j+=2, s+=2)
       *p++ = *s;
     s += 8; /* Skip one line */
     p += priv->width/2 - 4;
   }

  p = priv->plane[2];
  s = priv->Cr;
  for (i=0; i<8; i+=2)//Cr
   {
     for (j=0; j<8; j+=2, s+=2)
       *p++ = *s;
     s += 8; /* Skip one line */
     p += priv->width/2 - 4;
   }
}
/**
 * Save a buffer in three files (.Y, .U, .V) useable by yuvsplittoppm
 */
static void write_yuv(const char *filename, int width, int height, unsigned char **components)
{
  FILE *F;
  char temp[1024];

  snprintf(temp, 1024, "%s.Y", filename);
  F = fopen(temp, "wb");
  fwrite(components[0], width, height, F);
  fclose(F);
  snprintf(temp, 1024, "%s.U", filename);
  F = fopen(temp, "wb");
  fwrite(components[1], width*height/4, 1, F);
  fclose(F);
  snprintf(temp, 1024, "%s.V", filename);
  F = fopen(temp, "wb");
  fwrite(components[2], width*height/4, 1, F);
  fclose(F);
}

三、实验步骤

  1. 逐步调试JPEG解码器程序。将输入的JPG文件进行解码,将输出文件保存为可供YUVViewer观看的YUV文件。
  • 修改如下:
static void write_yuv(const char *filename, int width, int height, unsigned char **components)
{
  FILE *F;
  char temp[1024];

  snprintf(temp, 1024, "%s.yuv", filename);
  F = fopen(temp, "wb");
  fwrite(components[0], width, height, F);
  fwrite(components[1], width * height / 4, 1, F);
  fwrite(components[2], width * height / 4, 1, F);
  fclose(F);
}
  • 图片分辨率为1024*1024,用YUVViewer打开的文件如下:


    test.yuv
  1. 程序调试过程中,应做到:
  • 理解程序设计的整体框架
    本次实验的程序主要分为三部分:
    • 文件的读写
    • 解码过程(如算法框图所示流程)
    • 异常处理和trace记录
  • 理解三个结构体的设计目的
    1. struct jdec_private

综合结构体,储存了JPEG图片的尺寸信息、量化表、huffman表和每个通道的信息。
该结构体包含了其余两个结构体,解码过程频繁用到该结构体。

struct jdec_private
{
  /* Public variables */
  uint8_t *components[COMPONENTS];
  unsigned int width, height;   /* Size of the image */
  unsigned int flags;

  /* Private variables */
  const unsigned char *stream_begin, *stream_end;
  unsigned int stream_length;

  const unsigned char *stream;  /* Pointer to the current stream */
  unsigned int reservoir, nbits_in_reservoir;

  struct component component_infos[COMPONENTS];
  float Q_tables[COMPONENTS][64];       /* quantization tables */
  struct huffman_table HTDC[HUFFMAN_TABLES];    /* DC huffman tables   */
  struct huffman_table HTAC[HUFFMAN_TABLES];    /* AC huffman tables   */
  int default_huffman_table_initialized;
  int restart_interval;
  int restarts_to_go;               /* MCUs left in this restart interval */
  int last_rst_marker_seen;         /* Rst marker is incremented each time */

  /* Temp space used after the IDCT to store each components */
  uint8_t Y[64*4], Cr[64], Cb[64];

  jmp_buf jump_state;
  /* Internal Pointer use for colorspace conversion, do not modify it !!! */
  uint8_t *plane[COMPONENTS];

};
  1. struct component

用于保存一个MCU(最小编码单元)的信息。
包含了水平、垂直因子,量化表和huffman表的指针,上一个DC系数和DCT系数矩阵。

struct component 
{
  unsigned int Hfactor;//水平因子
  unsigned int Vfactor;//垂直因子
  float *Q_table;       /* Pointer to the quantisation table to use */
  struct huffman_table *AC_table;
  struct huffman_table *DC_table;
  short int previous_DC;    /* Previous DC coefficient */
  short int DCT[64];        /* DCT coef */
#if SANITY_CHECK
  unsigned int cid;
#endif
};
  1. struct huffman_table

创建一个快速查找表用于解码,查找失败启用慢速查找表。
用于加速解码过程。

struct huffman_table
{
  /* Fast look up table, using HUFFMAN_HASH_NBITS bits we can have directly the symbol,
   * if the symbol is <0, then we need to look into the tree table */
  short int lookup[HUFFMAN_HASH_SIZE];
  /* code size: give the number of bits of a symbol is encoded */
  unsigned char code_size[HUFFMAN_HASH_SIZE];
  /* some place to store value that is not encoded in the lookup table 
   * FIXME: Calculate if 256 value is enough to store all values
   */
  uint16_t slowtable[16-HUFFMAN_HASH_NBITS][256];
};

  • 理解在视音频编解码调试中TRACE的目的和含义
    • TRACE用于输出解码的信息和错误信息,便于理解程序运行的流程和查找错误。
    • 打开和关闭trace只需在定义trace的头文件中改变条件值,0表示关闭,1表示打开。
    #define  snprintf _snprintf//add by nxn
    #define TRACE 1//add by nxn
    #define  TRACEFILE "trace_jpeg.txt"//add by nxn
    
  1. 以txt文件输出所有的量化矩阵和所有的HUFFMAN码表。
  • Huffman码表的输出

在解码时将码表输出即可。

#if TRACE
     fprintf(p_trace,"Huffman table %s[%d] length=%d\n", (index&0xf0)?"AC":"DC", index&0xf, count);
     fflush(p_trace);
#endif
输出的Huffman码表
  • 量化矩阵的输出

由于编码时采用了之字形扫描,在输出量化矩阵时需要对存储顺序进行一定的处理。

  static void build_quantization_table(float *qtable, const unsigned char *ref_table)
  {

  int i, j;
  static const double aanscalefactor[8] = {
   1.0, 1.387039845, 1.306562965, 1.175875602,
   1.0, 0.785694958, 0.541196100, 0.275899379
  };
  const unsigned char *zz = zigzag;

  for (i=0; i<8; i++) {
     for (j=0; j<8; j++) {
       *qtable++ = ref_table[*zz++] * aanscalefactor[i] * aanscalefactor[j];
       /* 将量化表写入trace文件 */
  #if TRACE
     * zz--;
     fprintf(p_trace, "%d\t", ref_table[*zz++]);
     if (j == 7) {
         fprintf(p_trace, "\n");
     }
  #endif
   }
 }
}

输出的量化矩阵
  1. 输出DC图像并统计其概率分布。

输出直流和交流图像需要在解码时输出对应文件。

由于在编码时进行了0电平偏置下移,所以输出时需要上抬128。原始图像大小为1024*1024,分成了8*8的小块,输出图像大小为128*128

  • 将DC和AC图像写入文件
/* 头文件 tinyjpeg.h中定义 */
#define OUT 1
#define ac_index 1
#define DC_FILE "test_dc.yuv"
#define AC_FILE "test_ac.yuv"
int tinyjpeg_decode(struct jdec_private *priv, int pixfmt)
{
/* 变量定义 省略*/
#if OUT
  int count = 0;
  unsigned char* dbuf, * abuf;
  dbuf = (unsigned char*)malloc(sizeof(unsigned char));
  abuf = (unsigned char*)malloc(sizeof(unsigned char));
    FILE* F = fopen(DC_FILE, "wb");
    if (F == NULL)
        printf("open DC_FILE error!");
    FILE* F2 = fopen(AC_FILE, "wb");
    if (F2 == NULL)
        printf("open AC_FILE error!");
#endif
/* 解码过程 省略*/
    decode_MCU(priv);

    #if OUT
        dbuf = (unsigned char)(priv->component_infos->DCT[0]/4 + 128);
        abuf = (unsigned char)(priv->component_infos->DCT[ac_index] + 128);
        fwrite(&dbuf, 1, 1, F);
        fwrite(&abuf, 1, 1, F2);
        count++;
    #endif
/* 转换格式操作 省略*/
#if OUT
  unsigned char* uv = 128;
  for (int i = 0; i < count/2; i++) {
      fwrite(&uv, 1, 1, F);
      fwrite(&uv, 1, 1, F2);
  }
  fclose(F);
  fclose(F2);
#endif

  return 0;
}
  • 统计概率分布
#include <stdio.h>
#include <stdlib.h>

#define height 128
#define width 128

int main(int argc,char** argv) {
    /* 输入图像  输出概率分布 */
    
    int size = height * width;
    FILE* file;
    FILE* out_file;
    if (fopen_s(&file, argv[1], "rb") != 0) {
        printf("open file error!\n");
        exit(0);
    }
    if (fopen_s(&out_file, argv[2], "w") != 0) {
        printf("open file error!\n");
        exit(0);
    }

    unsigned char* ybuf, *ubuf, *vbuf;
    ybuf = (unsigned char*)malloc(size);
    ubuf = (unsigned char*)malloc(size / 4);
    vbuf = (unsigned char*)malloc(size / 4);

    fread(ybuf, 1, size, file);
    fread(ubuf, 1, size/4, file);
    fread(vbuf, 1, size/4, file);

    double p[256] = { 0 };
    for (int i = 0; i < size; i++) {
        int k = (int)*(ybuf + i);
        p[k]++;
    }
    for (int i = 0; i < 256; i++) {
        p[i] /= (double)size;
        fprintf(out_file, "%f\n",  p[i]);
    }
    return 0;
}
  • DC图像和概率分布图
图像 概率分布
原图
直流
  1. 输出某一个AC值图像并统计其概率分布。
AC图像 概率分布
DCT[1]
DCT[9]
DCT[17]

由于对亮度信号进行了零偏置电平下移,本例中0~255的值域转换为了值域-128~127。从直流信号的概率分布中可以看到,偏移后像素绝对值出现3位十进制的概率大大减少。
对于交流系数,它们的分布都近似为拉普拉斯分布,有利于熵编码。此外,交流系数的能量值小于直流系数的能量值,从图片中可以看出,交流图像更多地保留了图像的细节信息。

四、总结

  • 采样因子
    1.YCbCr 4:4:4
    数据流存放顺序YCbCrYCbCr...,采样因子为1*1,每个MCU代表着8*8个像素块。
    4:4:4格式采样

2.YCbCr 4:2:2
数据流存放顺序Y0Y1CbCr...,采样因子为1*2,每个MCU代表着16*8个像素块。Y0和Y1分别指向两个8*8像素块,这两个像素块共用一个Cb和Cr通道。

4:2:2格式采样

3.YCbCr 4:2:0
数据流存放顺序如下图,采样因子为2*2,每个MCU代表着16*16个像素块。Y0和Y1分别指向4个8*8像素块,这4个像素块共用一个Cb和Cr通道。

4:2:0格式采样

参考文章及图源:ImageSharp源码详解之JPEG编码原理(2)采样

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容