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


    * 音视频入门文章目录 *

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

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

    未采样前

    - 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 数据进行分块:

    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:

    标记 标记代码 描述
    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 图片:

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

    内容有误?联系作者:

    联系作者


  • 相关阅读:
    CSPS_108
    TortoiseGit配置密钥的方法
    SLF4J和Logback和Log4j和Logging的区别与联系
    slf4j log4j logback关系详解和相关用法
    dubbo服务telnet命令的使用
    基于IDEA的JavaWeb开发环境搭建
    jdk8--十大新特性
    jdk8--collect总结
    字符串的排列
    调整数组顺序使奇数位于偶数前面
  • 原文地址:https://www.cnblogs.com/binglingziyu/p/audio-video-basic-15-generate-jpeg-by-hand.html
Copyright © 2020-2023  润新知