音视频入门-15-手动生成一张JPEG图片

binglingziyu

音视频入门文章目录

JPEG 编码知识

JPEG 是 Joint Photographic Exports Group 的英文缩写,中文称之为联合图像专家小组。该小组隶属于 ISO 国际标准化组织,主要负责定制静态数字图像的编码方法,即所谓的 JPEG 算法。

JPEG 专家组开发了两种基本的压缩算法、两种熵编码方法、四种编码模式。如下所示:
  • 压缩算法:
  1. 有损的离散余弦变换 DCT(Discrete Cosine Transform)
  2. 无损的预测压缩技术;
  • 熵编码方法:
  1. Huffman 编码;
  2. 算术编码;
  • 编码模式:
  1. 基于 DCT 的顺序模式:编码、解码通过一次扫描完成;
  2. 基于 DCT 的渐进模式:编码、解码需要多次扫描完成,扫描效果由粗到精,逐级递增;
  3. 无损模式:基于 DPCM,保证解码后完全精确恢复到原图像采样值;
  4. 层次模式:图像在多个空间分辨率中进行编码,可以根据需要只对低分辨率数据做解码,放弃高分辨率信息;
在实际应用中,JPEG 图像编码算法使用的大多是 离散余弦变换 DCTHuffman 编码顺序编码模式

这被称为 JPEG 的基本系统。这里介绍的 JPEG 编码算法的流程,也是针对基本系统而言。基本系统的 JPEG 压缩编码算法一共分为 11 个步骤:

  1. 颜色模式转换
  2. 采样
  3. 分块
  4. 离散余弦变换(DCT)
  5. 量化
  6. Zigzag 扫描排序
  7. DC 系数的差分脉冲调制编码
  8. DC 系数的中间格式计算
  9. AC 系数的游程长度编码
  10. AC 系数的中间格式计算
  11. 熵编码

JPEG 编码步骤

JPEG 编码涉及到的知识还是很复杂的,每一个步骤如果展开的话都是非常大的篇幅。我们这里简单了解每一步是干什么的、怎么做,达到每一步的目标就可以了。

本文使用了 ffjpeg library a simple jpeg codec,几乎所有的编码算法都是使用了这个库里的。

1. 颜色模式转换

JPEG 采用的是 YCrCb 颜色空间,YCrCb 颜色空间中,Y 代表亮度,Cr,Cb 则代表色度和饱和度(也有人将 Cb,Cr 两者统称为色度),三者通常以 Y,U,V 来表示,即用 U 代表 Cb,用 V 代表 Cr。RGB 和 YCrCb 之间的转换关系如下所示:

Y = 0.299*R + 0.587*G + 0.114*B

Cb = -0.169*R - 0.331*G + 0.500*B + 128

Cr = 0.500*R - 0.419*G - 0.081*B + 128

取值范围:
R/G/B  [0 ~ 255]
Y/Cb/Cr[0 ~ 255]

示例代码:

// Y = 0.299*R + 0.587*G + 0.114*B
// U = Cb = -0.169*R - 0.331*G + 0.500*B + 128
// V = Cr = 0.500*R - 0.419*G - 0.081*B + 128
// R/G/B  [0 ~ 255]
// Y/Cb/Cr[0 ~ 255]
void rgb_to_yuv(uint8_t R, uint8_t G, uint8_t B, uint8_t *Y, uint8_t *U, uint8_t *V) {
    int y_val = (int)round(0.299*R + 0.587*G + 0.114*B);
    int u_val = (int)round(-0.169*R - 0.331*G + 0.500*B + 128);
    int v_val = (int)round(0.500*R - 0.419*G - 0.081*B + 128);
    *Y = bound(0, y_val, 255);
    *U = bound(0, u_val, 255);
    *V = bound(0, v_val, 255);
}

相关阅读:


YUV - RGB Color Format Conversion


2. 采样

为节省带宽起见,大多数 YUV 格式平均使用的每像素位数都少于 24 位。主要的采样(subsample)格式有 YCbCr4:2:0、YCbCr4:2:2、YCbCr4:1:1 和 YCbCr4:4:4。YUV 的表示法称为 A:B:C 表示法:

  • 4:4:4 表示完全取样。
  • 4:2:2 表示 2:1 的水平取样,垂直完全采样。
  • 4:2:0 表示 2:1 的水平取样,垂直 2:1 采样。
  • 4:1:1 表示 4:1 的水平取样,垂直完全采样。

<br/>

表格中,每一格代表一个像素

未采样前

-1234
1Y0 U0 V0Y1 U1 V1Y2 U2 V2Y3 U3 V3
2Y4 U4 V4Y5 U5 V5Y6 U6 V6Y7 U7 V7
3Y8 U8 V8Y9 U9 V9Y10 U10 V10Y11 U11 V11
4Y12 U12 V12Y13 U13 V13Y14 U14 V14Y15 U15 V15

4:4:4 采样

4:4:4 表示完全取样
-1234
1Y0 U0 V0Y1 U1 V1Y2 U2 V2Y3 U3 V3
2Y4 U4 V4Y5 U5 V5Y6 U6 V6Y7 U7 V7
3Y8 U8 V8Y9 U9 V9Y10 U10 V10Y11 U11 V11
4Y12 U12 V12Y13 U13 V13Y14 U14 V14Y15 U15 V15

映射的像素:

-1234
1Y0 U0 V0Y1 U1 V1Y2 U2 V2Y3 U3 V3
2Y4 U4 V4Y5 U5 V5Y6 U6 V6Y7 U7 V7
3Y8 U8 V8Y9 U9 V9Y10 U10 V10Y11 U11 V11
4Y12 U12 V12Y13 U13 V13Y14 U14 V14Y15 U15 V15

4:2:2 采样

4:2:2 表示 2:1 的水平取样,垂直完全采样
每采样过一个像素点,都会采样其 Y 分量,而 U、V 分量就会间隔一个采集一个。
-1234
1Y0 U0 -Y1 - V1Y2 U2 -Y3 - V3
2Y4 U4 -Y5 - V5Y6 U6 -Y7 - V7
3Y8 U8 -Y9 - V9Y10 U10 -Y11 - V11
4Y12 U12 -Y13 - V13Y14 U14 -Y15 - V15

映射的像素:

-1234
1Y0 U0 V1Y1 U0 V1Y2 U2 V3Y3 U2 V3
2Y4 U4 V5Y5 U4 V5Y6 U6 V7Y7 U6 V7
3Y8 U8 V9Y9 U8 V9Y10 U10 V11Y11 U10 V11
4Y12 U12 V13Y13 U12 V13Y14 U14 V15Y15 U14 V15

4:2:0 采样

4:2:0 表示 2:1 的水平取样,垂直 2:1 采样
每采样过一个像素点,都会采样其 Y 分量,而 U、V 分量就会间隔一行按照 2 : 1 进行采样。
-1234
1Y0 U0 -Y1 - -Y2 U2 -Y3 - -
2Y4 - V4Y5 - -Y6 - V6Y7 - -
3Y8 U8 -Y9 - -Y10 U10 -Y11 - -
4Y12 - V12Y13 - -Y14 - V14Y15 - -

映射的像素:

-1234
1Y0 U0 V4Y1 U0 V4Y2 U2 V6Y3 U2 V6
2Y4 U0 V4Y5 U0 V4Y6 U2 V6Y7 U2 V6
3Y8 U8 V12Y9 U8 V12Y10 U10 V14Y11 U10 V14
4Y12 U8 V12Y13 U8 V12Y14 U10 V14Y15 U10 V14

4:1:1 采样

4:1:1 表示 4:1 的水平取样,垂直完全采样
每采样过一个像素点,都会采样其 Y 分量,而 U、V 分量就会间隔一行按照 2 : 1 进行采样。
-1234
1Y0 U0 -Y1 - -Y2 - V2Y3 - -
2Y4 U4 -Y5 - -Y6 - V6Y7 - -
3Y8 U8 -Y9 - -Y10 - V10Y11 - -
4Y12 U12 -Y13 - -Y14 - V14Y15 - -

映射的像素:

-1234
1Y0 U0 V2Y1 U0 V2Y2 U0 V2Y3 U0 V2
2Y4 U4 V6Y5 U4 V6Y6 U4 V6Y7 U4 V6
3Y8 U8 V10Y9 U8 V10Y10 U8 V10Y11 U8 V10
4Y12 U12 V14Y13 U12 V14Y14 U12 V14Y15 U12 V14

示例代码:

void yuv444_to_yuv420(uint8_t *inbuf, uint8_t *outbuf, int w, int h) {
    uint8_t *srcY = NULL, *srcU = NULL, *srcV = NULL;
    uint8_t *desY = NULL, *desU = NULL, *desV = NULL;
    srcY = inbuf;//Y
    srcU = srcY + w * h;//U
    srcV = srcU + w * h;//V

    desY = outbuf;
    desU = desY + w * h;
    desV = desU + w * h / 4;

    int half_width = w / 2;
    int half_height = h / 2;
    memcpy(desY, srcY, w * h * sizeof(uint8_t));//Y分量直接拷贝即可
    //UV
    for (int i = 0; i < half_height; i++) {
        for (int j = 0; j < half_width; j++) {
            *desU = *srcU;
            *desV = *srcV;
            desU++;
            desV++;
            srcU += 2;
            srcV += 2;
        }
        srcU = srcU + w;
        srcV = srcV + w;
    }
}

相关阅读:


一文读懂 YUV 的采样与格式

音视频入门-07-认识YUV


3. 分块

后面的 DCT 变换是是对 8x8 的子块进行处理的,因此,在进行 DCT 变换之前必须把源图象数据进行分块。源图象经过上面的颜色模式转换采样变成了 YUV 数据,所以需要分别对 Y U V 三个分量进行分块。由左及右,由上到下依次读取 8x8 的子块,存放在长度为 64 的数组中,即可以进行 DCT 变换。

图像 YUV444 数据进行分块:

JPEG分块

示例代码:

// 计算分块数量
int calc_block_size(int width, int height) {
    int h_block_size = width/8 + (width%8==0?0:1);
    int v_block_size = height/8 + (height%8==0?0:1);
    return h_block_size*v_block_size;
}

// 获取一个子块
void get_8x8_block(const uint8_t *data, uint8_t *block_data, int h_block_pos, int v_block_pos, int width, int height) {
    for(int i = 0; i < 8; i++) {
        for(int j = 0; j < 8; j++) {
            int h_pos = h_block_pos*8 + i;
            int v_pos = v_block_pos*8 + j;
            if(h_pos >= width || v_pos >= height) {
                block_data[8*i+j] = 0;
            } else {
                block_data[8*i+j] = data[h_pos*width + v_pos];
            }
        }
    }
}

// 对数据进行分块
void block_data_8x8(uint8_t *data, uint8_t blocks[][64], int width, int height) {
    int h_block_size = width/8 + (width%8==0?0:1);
    int v_block_size = height/8 + (height%8==0?0:1);
    for(int i = 0; i < h_block_size; i++) {
        for(int j = 0; j < v_block_size; j++) {
            uint8_t *tmp = blocks[h_block_size*i+j];
            get_8x8_block(data, tmp, i, j, width, height);
        }
    }
}

相关阅读:


JPEG压缩原理与DCT离散余弦变换-以8x8的图象块为基本单位进行编码

JPEG图像压缩算法流程详解-分块

JPG的工作原理-图像分成8x8像素块


4. 离散余弦变换(DCT)

DCT 变换的公式为:

DCT 变换的公式

将原始图像数据分为 8x8 的数据单元矩阵之后,还必须将每个数值减去 128,然后一一带入 DCT 变换公式,即可达到 DCT 变换的目的。图像的数据值必须减去 128,是因为 DCT 公式所接受的数字范围是 -128 到 127 之间。

举例来说明一下,例子来自【JPEG 原理详细实例分析及其在嵌入式 Linux 中的应用】

8x8 的原始图像:

JPEG-DCT-1

减去 128 后,使其范围变为 -128~127:

JPEG-DCT-2

使用离散余弦变换,并四舍五入取最接近的整数:

JPEG-DCT-3

矩阵最左上角的是直流系数(DC
矩阵其余 63 个是交流系数(AC

DCT 输出的频率系数矩阵最左上角的直流(DC)系数幅度最大,图中为-415;以 DC 系数为出发点向下、向右的其它 DCT 系数,离 DC 分量越远,频率越高,幅度值越小,图中最右下角为2,即图像信息的大部分集中于直流系数及其附近的低频频谱上,离 DC 系数越来越远的高频频谱几乎不含图像信息,甚至于只含杂波。

示例代码:

void fdct2d8x8(int *data) {
    int u, v;
    int x, y, i;
    float buf[64];
    float temp;
    for (u=0; u<8; u++) {
        for (v=0; v<8; v++) {
            temp = 0;
            for (x=0; x<8; x++) {
                for (y=0; y<8; y++) {
                    temp += data[y * 8 + x]
                          * (float)cos((2.0f * x + 1.0f) / 16.0f * u * M_PI)
                          * (float)cos((2.0f * y + 1.0f) / 16.0f * v * M_PI);
                }
            }
            buf[v * 8 + u] = alpha(u) * alpha(v) * temp;
        }
    }
    for (i=0; i<64; i++) data[i] = buf[i];
}

for(int y_index = 0; y_index < block_size; y_index++) {
    uint8_t *y_block = y_blocks[y_index];
    // DCT 之前减去 128
    for(int i = 0; i < 64; i++) {y_blocks_dct[y_index][i] = y_block[i]-128;}
    fdct2d8x8(y_blocks_dct[y_index]);
}
for(int u_index = 0; u_index < block_size; u_index++) {
    uint8_t *u_block = u_blocks[u_index];
    // DCT 之前减去 128
    for(int i = 0; i < 64; i++) {u_blocks_dct[u_index][i] = u_block[i] - 128;}
    fdct2d8x8(u_blocks_dct[u_index]);
}
for(int v_index = 0; v_index < block_size; v_index++) {
    uint8_t *v_block = v_blocks[v_index];
    // DCT 之前减去 128
    for(int i = 0; i < 64; i++) {v_blocks_dct[v_index][i] = v_block[i] - 128;}
    fdct2d8x8(v_blocks_dct[v_index]);
}

相关阅读:


JPEG 原理详细实例分析及其在嵌入式 Linux 中的应用-3、离散余弦变换 DCT

JPEG 简易文档 - 2. DCT (离散余弦变换)

JPEG压缩原理与DCT离散余弦变换

图像的时频变换——离散余弦变换

DCT变换与图像压缩、去燥

github.com/VersBinarii/dct


5. 量化

量化过程实际上就是对 DCT 系数的一个优化过程。它是利用了人眼对高频部分不敏感的特性来实现数据的大幅简化。

量化过程实际上是简单地把频率领域上每个成份,除以一个对于该成份的常数,且接着四舍五入取最接近的整数。

这是整个过程中的主要有损运算。

以这个结果来说,经常会把很多高频率的成份四舍五入而接近0,且剩下很多会变成小的正或负数。

整个量化的目的是减小非“0”系数的幅度以及增加“0”值系数的数目。

量化是图像质量下降的最主要原因。

因为人眼对亮度信号比对色差信号更敏感,因此使用了两种量化表:亮度量化值和色差量化值。

jpeg-量化-1

使用这个量化矩阵与前面所得到的 DCT 系数矩阵:

jpeg-量化-2

如,使用−415(DC系数)且四舍五入得到最接近的整数

jpeg-量化-3

总体上来说,DCT 变换实际是空间域的低通滤波器。对 Y 分量采用细量化,对 UV 采用粗量化。

量化表是控制 JPEG 压缩比的关键,这个步骤除掉了一些高频量;另一个重要原因是所有图片的点与点之间会有一个色彩过渡的过程,大量的图像信息被包含在低频率中,经过量化处理后,在高频率段,将出现大量连续的零。

示例代码:

/* 全局变量定义 */
const int STD_QUANT_TAB_LUMIN[64] = {
    16, 11, 10, 16, 24, 40, 51, 61,
    12, 12, 14, 19, 26, 58, 60, 55,
    14, 13, 16, 24, 40, 57, 69, 56,
    14, 17, 22, 29, 51, 87, 80, 62,
    18, 22, 37, 56, 68, 109,103,77,
    24, 35, 55, 64, 81, 104,113,92,
    49, 64, 78, 87, 103,121,120,101,
    72, 92, 95, 98, 112,100,103,99,
};

const int STD_QUANT_TAB_CHROM[64] = {
    17, 18, 24, 47, 99, 99, 99, 99,
    18, 21, 26, 66, 99, 99, 99, 99,
    24, 26, 56, 99, 99, 99, 99, 99,
    47, 66, 99, 99, 99, 99, 99, 99,
    99, 99, 99, 99, 99, 99, 99, 99,
    99, 99, 99, 99, 99, 99, 99, 99,
    99, 99, 99, 99, 99, 99, 99, 99,
    99, 99, 99, 99, 99, 99, 99, 99,
};

/* 函数实现 */
void quant_encode(int du[64], int qtab[64]) {
    int i; for (i=0; i<64; i++) du[i] /= qtab[i];
}

int *pqtab[2];
pqtab[0] = malloc(64*sizeof(int));
pqtab[1] = malloc(64*sizeof(int));
memcpy(pqtab[0], STD_QUANT_TAB_LUMIN, 64*sizeof(int));
memcpy(pqtab[1], STD_QUANT_TAB_CHROM, 64*sizeof(int));
for(int y_index = 0; y_index < block_size; y_index++) {
    quant_encode(y_blocks_dct[y_index], pqtab[0]);
}
for(int u_index = 0; u_index < block_size; u_index++) {
    quant_encode(u_blocks_dct[u_index], pqtab[1]);
}
for(int v_index = 0; v_index < block_size; v_index++) {
    quant_encode(v_blocks_dct[v_index], pqtab[1]);
}

相关阅读:


JPEG 简易文档-4.量化

JPEG 原理详细实例分析及其在嵌入式 Linux 中的应用-4. 量化

JPEG压缩编码-2. 量化

JPEG压缩原理与DCT离散余弦变换-2.4 量化与反量化


6. Zigzag 扫描排序

量化后的数据,有一个很大的特点,就是直流分量相对于交流分量来说要大,而且交流分量中含有大量的 0。这样,对这个量化后的数据如何来进行简化,从而再更大程度地进行压缩呢。

这就出现了“Z”字形编排,如图:

jpeg-zigzag

对于前面量化的系数所作的 “Z”字形编排结果就是:

底部 −26,−3,0,−3,−3,−6,2,−4,1 −4,1,1,5,1,2,−1,1,−1,2,0,0,0,0,0,−1,−1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 顶部

这样做的特点就是会连续出现多个0,这样很有利于使用简单而直观的游程编码(RLE:Run Length Coding)对它们进行编码。

示例代码:

/* 内部全局变量定义 */
const int ZIGZAG[64] =
        {
                0,  1,  8, 16,  9,  2,  3, 10,
                17, 24, 32, 25, 18, 11,  4,  5,
                12, 19, 26, 33, 40, 48, 41, 34,
                27, 20, 13,  6,  7, 14, 21, 28,
                35, 42, 49, 56, 57, 50, 43, 36,
                29, 22, 15, 23, 30, 37, 44, 51,
                58, 59, 52, 45, 38, 31, 39, 46,
                53, 60, 61, 54, 47, 55, 62, 63,
        };

/* 函数实现 */
void zigzag_encode(int data[64]) {
    int buf[64], i;
    for (i=0; i<64; i++) buf [i] = data[ZIGZAG[i]];
    for (i=0; i<64; i++) data[i] = buf[i];
}

for(int y_index = 0; y_index < block_size; y_index++) {
    zigzag_encode(y_blocks_dct[y_index]);
}
for(int u_index = 0; u_index < block_size; u_index++) {
    zigzag_encode(u_blocks_dct[u_index]);
}
for(int v_index = 0; v_index < block_size; v_index++) {
    zigzag_encode(v_blocks_dct[v_index]);
}

相关阅读:


5、“Z”字形编排


7. DC 系数的差分脉冲调制编码

8x8 图像块经过 DCT 变换之后得到的 DC 直流系数有两个特点,一是系数的数值比较大,二是相邻 8x8 图像块的 DC 系数值变化不大。根据这个特点,JPEG 算法使用了差分脉冲调制编码 (DPCM) 技术,对相邻图像块之间量化DC系数的差值 (Delta) 进行编码。

DC(0)=0

Delta = DC(i)  - DC(i-1)

示例代码:

void encode_du(HUFCODEC *hfcac, HUFCODEC *hfcdc, int du[64], int *dc) {
    ...
    // 7.  DC 系数的差分脉冲调制编码
    // dc
    diff = du[0] - *dc;
    *dc  = du[0];
    ...
}

int   dc[3]= {0};
for(int index = 0; index < block_size; index++) {
    encode_du(phcac[0], phcdc[0], y_blocks_dct[index], &(dc[0]));
    encode_du(phcac[1], phcdc[1], u_blocks_dct[index], &(dc[1]));
    encode_du(phcac[1], phcdc[1], v_blocks_dct[index], &(dc[2]));
}

相关阅读:


JPEG图像压缩算法流程详解-(7)DC系数的差分脉冲调制编码

JPEG格式与压缩流程分析-7.DC系数的差分脉冲调制编码


8. DC 系数的中间格式计算

JPEG 中为了更进一步节约空间,并不直接保存数据的具体数值,而是将数据按照位数分为 16 组,保存在表里面。这也就是所谓的变长整数编码 VLI。即,第 0 组中保存的编码位数为 0,其编码所代表的数字为 0;第 1 组中保存的编码位数为 1,编码所代表的数字为 -1 或者 1 ......,如下面的表格所示,这里,暂且称其为 VLI 编码表:

VLI

如果 DC 系数差值为 3,通过查找 VLI 可以发现,整数 3 位于 VLI 表格的第 2 组,因此,可以写成(2)(3)的形式,该形式,称之为 DC 系数的中间格式。

示例代码:

void category_encode(int *code, int *size) {
    unsigned absc = abs(*code);
    unsigned mask = (1 << 15);
    int i    = 15;
    if (absc == 0) { *size = 0; return; }
    while (i && !(absc & mask)) { mask >>= 1; i--; }
    *size = i + 1;
    if (*code < 0) *code = (1 << *size) - absc - 1;
}

// 7.  DC 系数的差分脉冲调制编码
diff = du[0] - *dc;

// 8.  DC 系数的中间格式计算
code = diff;
category_encode(&code, &size);

相关阅读:


JPEG图像压缩算法流程详解-(8)DC系数的中间格式计算

JPEG格式与压缩流程分析-8.DC系数的中间格式计算


9. AC 系数的游程长度编码

游程编码 RLC(Run Length Coding)来更进一步降低数据的传输量。利用该编码方式,可以将一个字符串中重复出现的连续字符用两个字节来代替,其中,第一个字节代表重复的次数,第二个字节代表被重复的字符串。

57, 45, 0, 0, 0, 0, 23, 0, -30, -8, 0, 0, 1, 000…

经过 0 RLC 之后,将呈现出以下的形式:

(0,57) ; (0,45) ; (4,23) ; (1,-30) ; (0,-8) ; (2,1) ; (0,0)

注意,如果 AC 系数之间连续 0 的个数超过 16,则用一个扩展字节 (15,0) 来表示 16 连续的 0。

示例代码:

typedef struct {
    unsigned runlen   : 4;
    unsigned codesize : 4;
    unsigned codedata : 16;
} RLEITEM;

// 9.  AC 系数的游程长度编码
// 10. AC 系数的中间格式计算
// rle encode for ac
for (i=1, j=0, n=0, eob=0; i<64 && j<63; i++) {
    if (du[i] == 0 && n < 15) {
        n++;
    } else {
        code = du[i]; size = 0;
        // 10. AC 系数的中间格式计算
        category_encode(&code, &size);
        rlelist[j].runlen   = n;
        rlelist[j].codesize = size;
        rlelist[j].codedata = code;
        n = 0;
        j++;
        if (size != 0) eob = j;
    }
}

// set eob
if (du[63] == 0) {
    rlelist[eob].runlen   = 0;
    rlelist[eob].codesize = 0;
    rlelist[eob].codedata = 0;
    j = eob + 1;
}

相关阅读:


JPEG图像压缩算法流程详解-(9)AC系数的行程长度编码(RLC)

JPEG格式与压缩流程分析-9. AC系数的行程长度编码(RLC)


10. AC 系数的中间格式计算

根据前面提到的 VLI 表格,对于前面的字符串:

(0,57) ; (0,45) ; (4,23) ; (1,-30) ; (0,-8) ; (2,1) ; (0,0)

只处理每对数右边的那个数据,对其进行 VLI 编码: 查找上面的 VLI 编码表格,可以发现,57 在第 6 组当中,因此,可以将其写成 (0,6),57 的形式,该形式,称之为 AC 系数的中间格式。

同样的 (0,45) 的中间格式为:(0,6),45;

(1,-30) 的中间格式为:(1,5),-30;

示例代码:

void category_encode(int *code, int *size) {
    unsigned absc = abs(*code);
    unsigned mask = (1 << 15);
    int i    = 15;
    if (absc == 0) { *size = 0; return; }
    while (i && !(absc & mask)) { mask >>= 1; i--; }
    *size = i + 1;
    if (*code < 0) *code = (1 << *size) - absc - 1;
}

// 9.  AC 系数的游程长度编码
// 10. AC 系数的中间格式计算
// rle encode for ac
for (i=1, j=0, n=0, eob=0; i<64 && j<63; i++) {
    if (du[i] == 0 && n < 15) {
        n++;
    } else {
        code = du[i]; size = 0;
        // 10. AC 系数的中间格式计算
        category_encode(&code, &size);
        rlelist[j].runlen   = n;
        rlelist[j].codesize = size;
        rlelist[j].codedata = code;
        n = 0;
        j++;
        if (size != 0) eob = j;
    }
}

// set eob
if (du[63] == 0) {
    rlelist[eob].runlen   = 0;
    rlelist[eob].codesize = 0;
    rlelist[eob].codedata = 0;
    j = eob + 1;
}

相关阅读:


JPEG图像压缩算法流程详解-(10)AC系数的中间格式

JPEG格式与压缩流程分析-10.AC系数的中间格式算


11. 熵编码

在得到 DC 系数的中间格式和 AC 系数的中间格式之后,为进一步压缩图像数据,有必要对两者进行熵编码,通过对出现概率较高的字符采用较小的 bit 数编码达到压缩的目的。JPEG 标准具体规定了两种熵编码方式:Huffman 编码和算术编码。JPEG 基本系统规定采用 Huffman 编码。

Huffman 编码:对出现概率大的字符分配字符长度较短的二进制编码,对出现概率小的字符分配字符长度较长的二进制编码,从而使得字符的平均编码长度最短。Huffman 编码的原理请参考数据结构中的 Huffman 树或者最优二叉树。

Huffman 编码时 DC 系数与 AC 系数分别采用不同的 Huffman 编码表,对于亮度和色度也采用不同的 Huffman 编码表。因此,需要 4 张 Huffman 编码表才能完成熵编码的工作。具体的 Huffman 编码采用查表的方式来高效地完成。然而,在 JPEG 标准中没有定义缺省的 Huffman 表,用户可以根据实际应用自由选择,也可以使用J PEG 标准推荐的 Huffman 表。或者预先定义一个通用的 Huffman 表,也可以针对一副特定的图像,在压缩编码前通过搜集其统计特征来计算 Huffman 表的值。

示例代码:

// huffman codec ac
HUFCODEC *phcac[2];
phcac[0]= calloc(1, sizeof(HUFCODEC));
phcac[1]= calloc(1, sizeof(HUFCODEC));
// huffman codec dc
HUFCODEC *phcdc[2];
phcdc[0] = calloc(1, sizeof(HUFCODEC));
phcdc[1] = calloc(1, sizeof(HUFCODEC));
// init huffman codec
memcpy(phcac[0]->huftab, STD_HUFTAB_LUMIN_AC, MAX_HUFFMAN_CODE_LEN + 256);
memcpy(phcac[1]->huftab, STD_HUFTAB_CHROM_AC, MAX_HUFFMAN_CODE_LEN + 256);
memcpy(phcdc[0]->huftab, STD_HUFTAB_LUMIN_DC, MAX_HUFFMAN_CODE_LEN + 256);
memcpy(phcdc[1]->huftab, STD_HUFTAB_CHROM_DC, MAX_HUFFMAN_CODE_LEN + 256);
phcac[0]->output = bs; huffman_encode_init(phcac[0], 1);
phcac[1]->output = bs; huffman_encode_init(phcac[1], 1);
phcdc[0]->output = bs; huffman_encode_init(phcdc[0], 1);
phcdc[1]->output = bs; huffman_encode_init(phcdc[1], 1);

// 7.  DC 系数的差分脉冲调制编码
// 8.  DC 系数的中间格式计算
// 9.  AC 系数的游程长度编码
// 10. AC 系数的中间格式计算
// 11. 熵编码
// 7、8、9、10、11 在 encode_du 里完成
for(int index = 0; index < block_size; index++) {
    encode_du(phcac[0], phcdc[0], y_blocks_dct[index], &(dc[0]));
    encode_du(phcac[1], phcdc[1], u_blocks_dct[index], &(dc[1]));
    encode_du(phcac[1], phcdc[1], v_blocks_dct[index], &(dc[2]));
}

void encode_du(HUFCODEC *hfcac, HUFCODEC *hfcdc, int du[64], int *dc) {
    ...

    // 7.  DC 系数的差分脉冲调制编码
    diff = du[0] - *dc;
    *dc  = du[0];

    // 8.  DC 系数的中间格式计算
    code = diff;
    category_encode(&code, &size);

    // 11. 熵编码 DC
    huffman_encode_step(hfcdc, size);
    bitstr_put_bits(bs, code, size);

    // 9.  AC 系数的游程长度编码
    // 10. AC 系数的中间格式计算
    // rle encode for ac
    for (i=1, j=0, n=0, eob=0; i<64 && j<63; i++) {
    ...
    }
    ...
    // 11. 熵编码 AC
    for (i=0; i<j; i++) {
        huffman_encode_step(hfcac, (rlelist[i].runlen << 4) | (rlelist[i].codesize << 0));
        bitstr_put_bits(bs, rlelist[i].codedata, rlelist[i].codesize);
    }
}

相关阅读:


JPEG 简易文档 V2.15

JPEG 原理详细分析

JPEG格式图像编解码

JPEG/itu-t81.pdf

JPEG文件编/解码详解

简述JPEG编码实现的基本步骤

JPG的工作原理

How JPG Works

JPEG图像压缩算法流程详解

JPEG编解码原理


JPEG 文件写入

根据 音视频入门-14-JPEG文件格式详解, JPEG 文件大体上可以分成两个部分:标记码(Tag)和压缩数据。

常用的标记有 SOI、APP0、APPn、DQT、SOF0、DHT、DRI、SOS、EOI:

标记标记代码描述
SOI0xD8图像开始
APP00xE0JFIF应用数据块
APPn0xE1 - 0xEF其他的应用数据块(n, 1~15)
DQT0xDB量化表
SOF00xC0帧开始
DHT0xC4霍夫曼(Huffman)表
DRI0xDD差分编码累计复位的间隔
SOS0xDA扫描线开始
EOI0xD9图像结束

通过上面的 11 个编码步骤,我们也得到了需要的数据,下一步只需将数据写入文件即可。

    // 下面开始将数据写入文件
    //FILE *fp = fopen("C:\\Users\\Administrator\\Desktop\\rainbow-rgb-to-jpeg.jpg", "wb+");
    FILE *fp = fopen("/Users/staff/Desktop/rainbow-rgb-to-jpeg.jpg", "wb");

    // output SOI
    fputc(0xff, fp);
    fputc(0xd8, fp);

    // output DQT
    for (int i = 0; i < 2; i++) {
        int len = 2 + 1 + 64;
        fputc(0xff, fp);
        fputc(0xdb, fp);
        fputc(len >> 8, fp);
        fputc(len >> 0, fp);
        fputc(i   , fp);
        for (int j = 0; j < 64; j++) {
            fputc(pqtab[i][ZIGZAG[j]], fp);
        }
    }

    // output SOF0
    int SOF0Len = 2 + 1 + 2 + 2 + 1 + 3 * 3;
    fputc(0xff, fp);
    fputc(0xc0, fp);
    fputc(SOF0Len >> 8, fp);
    fputc(SOF0Len >> 0, fp);
    fputc(8   , fp); // precision 8bit
    fputc(height >> 8, fp); // height
    fputc(height >> 0, fp); // height
    fputc(width  >> 8, fp); // width
    fputc(width  >> 0, fp); // width
    fputc(3, fp);
    fputc(0x01, fp); fputc(0x11, fp); fputc(0x00, fp);
    fputc(0x02, fp); fputc(0x11, fp); fputc(0x01, fp);
    fputc(0x03, fp); fputc(0x11, fp); fputc(0x01, fp);

    // output DHT AC
    for (int i = 0; i < 2; i++) {
        fputc(0xff, fp);
        fputc(0xc4, fp);
        int len = 2 + 1 + 16;
        for (int j = 0; j < 16; j++) len += phcac[i]->huftab[j];
        fputc(len >> 8, fp);
        fputc(len >> 0, fp);
        fputc(i + 0x10, fp);
        fwrite(phcac[i]->huftab, len - 3, 1, fp);
    }

    // output DHT DC
    for (int i = 0; i < 2; i++) {
        fputc(0xff, fp);
        fputc(0xc4, fp);
        int len = 2 + 1 + 16;
        for (int j = 0; j < 16; j++) len += phcdc[i]->huftab[j];
        fputc(len >> 8, fp);
        fputc(len >> 0, fp);
        fputc(i + 0x00, fp);
        fwrite(phcdc[i]->huftab, len - 3, 1, fp);
    }

    // output SOS
    int SOSLen = 2 + 1 + 2 * 3 + 3;
    fputc(0xff, fp);
    fputc(0xda, fp);
    fputc(SOSLen >> 8, fp);
    fputc(SOSLen >> 0, fp);
    fputc(3, fp);

    fputc(0x01, fp); fputc(0x00, fp);
    fputc(0x02, fp); fputc(0x11, fp);
    fputc(0x03, fp); fputc(0x11, fp);

    fputc(0x00, fp);
    fputc(0x3F, fp);
    fputc(0x00, fp);

    // output data
    if (buffer) {
        fwrite(buffer, dataLength, 1, fp);
    }

    // output EOI
    fputc(0xff, fp);
    fputc(0xd9, fp);

    // 释放各种资源
    ......

JPEG 结果验收

以上完整代码在 binglingziyu/audio-video-blog-demos 可以获取。

运行代码,生成 JPEG 图片:

生成 JPEG 图片


代码:
15-rgb-to-jpeg

参考资料:

JPEG 简易文档 V2.15

5.7 JPEG压缩编码

JPEG 原理详细分析

JPEG格式压缩算法

JPEG压缩编码笔记

JPEG格式图像编解码

JPEG/itu-t81.pdf

某科学的JPEG编码函数 svjpeg()

JPEG文件编/解码详解

影像算法解析——JPEG 压缩算法

简述JPEG编码实现的基本步骤

JPG的工作原理

How JPG Works

JPEG图像压缩算法流程详解

音视频入门-07-认识YUV

一文读懂 YUV 的采样与格式

YUV - RGB Color Format Conversion

JPEG压缩原理与DCT离散余弦变换

图像的时频变换——离散余弦变换

DCT变换与图像压缩、去燥

github.com/VersBinarii/dct

JPEG编解码原理

内容有误?联系作者:

联系作者


本文由博客一文多发平台 OpenWrite 发布!
阅读 1.6k

Coding!

3 声望
0 粉丝
0 条评论
你知道吗?

Coding!

3 声望
0 粉丝
宣传栏