音视频入门-15-手动生成一张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 个步骤:

颜色模式转换
采样
分块
离散余弦变换(DCT)
量化
Zigzag 扫描排序
DC 系数的差分脉冲调制编码
DC 系数的中间格式计算
AC 系数的游程长度编码
AC 系数的中间格式计算
熵编码

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 的水平取样,垂直完全采样。

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

未采样前

- 1 2 3 4
1 Y0 U0 V0 Y1 U1 V1 Y2 U2 V2 Y3 U3 V3
2 Y4 U4 V4 Y5 U5 V5 Y6 U6 V6 Y7 U7 V7
3 Y8 U8 V8 Y9 U9 V9 Y10 U10 V10 Y11 U11 V11
4 Y12 U12 V12 Y13 U13 V13 Y14 U14 V14 Y15 U15 V15

4:4:4 采样

4:4:4 表示完全取样

- 1 2 3 4
1 Y0 U0 V0 Y1 U1 V1 Y2 U2 V2 Y3 U3 V3
2 Y4 U4 V4 Y5 U5 V5 Y6 U6 V6 Y7 U7 V7
3 Y8 U8 V8 Y9 U9 V9 Y10 U10 V10 Y11 U11 V11
4 Y12 U12 V12 Y13 U13 V13 Y14 U14 V14 Y15 U15 V15

映射的像素:

- 1 2 3 4
1 Y0 U0 V0 Y1 U1 V1 Y2 U2 V2 Y3 U3 V3
2 Y4 U4 V4 Y5 U5 V5 Y6 U6 V6 Y7 U7 V7
3 Y8 U8 V8 Y9 U9 V9 Y10 U10 V10 Y11 U11 V11
4 Y12 U12 V12 Y13 U13 V13 Y14 U14 V14 Y15 U15 V15

4:2:2 采样

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

每采样过一个像素点,都会采样其 Y 分量,而 U、V 分量就会间隔一个采集一个。

- 1 2 3 4
1 Y0 U0 - Y1 - V1 Y2 U2 - Y3 - V3
2 Y4 U4 - Y5 - V5 Y6 U6 - Y7 - V7
3 Y8 U8 - Y9 - V9 Y10 U10 - Y11 - V11
4 Y12 U12 - Y13 - V13 Y14 U14 - Y15 - V15

映射的像素:

- 1 2 3 4
1 Y0 U0 V1 Y1 U0 V1 Y2 U2 V3 Y3 U2 V3
2 Y4 U4 V5 Y5 U4 V5 Y6 U6 V7 Y7 U6 V7
3 Y8 U8 V9 Y9 U8 V9 Y10 U10 V11 Y11 U10 V11
4 Y12 U12 V13 Y13 U12 V13 Y14 U14 V15 Y15 U14 V15

4:2:0 采样

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

每采样过一个像素点,都会采样其 Y 分量,而 U、V 分量就会间隔一行按照 2 : 1 进行采样。

- 1 2 3 4
1 Y0 U0 - Y1 - - Y2 U2 - Y3 - -
2 Y4 - V4 Y5 - - Y6 - V6 Y7 - -
3 Y8 U8 - Y9 - - Y10 U10 - Y11 - -
4 Y12 - V12 Y13 - - Y14 - V14 Y15 - -

映射的像素:

- 1 2 3 4
1 Y0 U0 V4 Y1 U0 V4 Y2 U2 V6 Y3 U2 V6
2 Y4 U0 V4 Y5 U0 V4 Y6 U2 V6 Y7 U2 V6
3 Y8 U8 V12 Y9 U8 V12 Y10 U10 V14 Y11 U10 V14
4 Y12 U8 V12 Y13 U8 V12 Y14 U10 V14 Y15 U10 V14

4:1:1 采样

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

每采样过一个像素点,都会采样其 Y 分量,而 U、V 分量就会间隔一行按照 2 : 1 进行采样。

- 1 2 3 4
1 Y0 U0 - Y1 - - Y2 - V2 Y3 - -
2 Y4 U4 - Y5 - - Y6 - V6 Y7 - -
3 Y8 U8 - Y9 - - Y10 - V10 Y11 - -
4 Y12 U12 - Y13 - - Y14 - V14 Y15 - -

映射的像素:

- 1 2 3 4
1 Y0 U0 V2 Y1 U0 V2 Y2 U0 V2 Y3 U0 V2
2 Y4 U4 V6 Y5 U4 V6 Y6 U4 V6 Y7 U4 V6
3 Y8 U8 V10 Y9 U8 V10 Y10 U8 V10 Y11 U8 V10
4 Y12 U12 V14 Y13 U12 V14 Y14 U12 V14 Y15 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 数据进行分块:

示例代码:

// 计算分块数量
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 变换的公式为:

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

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

8x8 的原始图像:

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

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

矩阵最左上角的是直流系数( 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”值系数的数目。

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

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

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

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

总体上来说,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”字形编排,如图:

对于前面量化的系数所作的 “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 编码表:

如果 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:

标记 标记代码 描述
SOI 0xD8 图像开始
APP0 0xE0 JFIF应用数据块
APPn 0xE1 - 0xEF 其他的应用数据块(n, 1~15)
DQT 0xDB 量化表
SOF0 0xC0 帧开始
DHT 0xC4 霍夫曼(Huffman)表
DRI 0xDD 差分编码累计复位的间隔
SOS 0xDA 扫描线开始
EOI 0xD9 图像结束

通过上面的 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 图片:

代码:

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编解码原理

内容有误?联系作者:

谢谢你请我吃糖果

支付宝

微信

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章