音視頻入門-15-手動(dòng)生成一張JPEG圖片

* 音視頻入門文章目錄 *

JPEG 編碼知識(shí)

JPEG 是 Joint Photographic Exports Group 的英文縮寫琅捏,中文稱之為聯(lián)合圖像專家小組教硫。該小組隸屬于 ISO 國(guó)際標(biāo)準(zhǔn)化組織杈绸,主要負(fù)責(zé)定制靜態(tài)數(shù)字圖像的編碼方法珊燎,即所謂的 JPEG 算法。

JPEG 專家組開發(fā)了兩種基本的壓縮算法姑廉、兩種熵編碼方法缺亮、四種編碼模式。如下所示:

  • 壓縮算法:
  1. 有損的離散余弦變換 DCT(Discrete Cosine Transform)

  2. 無損的預(yù)測(cè)壓縮技術(shù)庄蹋;

  • 熵編碼方法:
  1. Huffman 編碼瞬内;

  2. 算術(shù)編碼;

  • 編碼模式:
  1. 基于 DCT 的順序模式:編碼限书、解碼通過一次掃描完成虫蝶;

  2. 基于 DCT 的漸進(jìn)模式:編碼、解碼需要多次掃描完成倦西,掃描效果由粗到精能真,逐級(jí)遞增;

  3. 無損模式:基于 DPCM扰柠,保證解碼后完全精確恢復(fù)到原圖像采樣值粉铐;

  4. 層次模式:圖像在多個(gè)空間分辨率中進(jìn)行編碼,可以根據(jù)需要只對(duì)低分辨率數(shù)據(jù)做解碼卤档,放棄高分辨率信息蝙泼;

在實(shí)際應(yīng)用中,JPEG 圖像編碼算法使用的大多是 離散余弦變換 DCT劝枣、Huffman 編碼汤踏、順序編碼模式

這被稱為 JPEG 的基本系統(tǒng)舔腾。這里介紹的 JPEG 編碼算法的流程溪胶,也是針對(duì)基本系統(tǒng)而言∥瘸希基本系統(tǒng)的 JPEG 壓縮編碼算法一共分為 11 個(gè)步驟:

  1. 顏色模式轉(zhuǎn)換
  2. 采樣
  3. 分塊
  4. 離散余弦變換(DCT)
  5. 量化
  6. Zigzag 掃描排序
  7. DC 系數(shù)的差分脈沖調(diào)制編碼
  8. DC 系數(shù)的中間格式計(jì)算
  9. AC 系數(shù)的游程長(zhǎng)度編碼
  10. AC 系數(shù)的中間格式計(jì)算
  11. 熵編碼

JPEG 編碼步驟

JPEG 編碼涉及到的知識(shí)還是很復(fù)雜的哗脖,每一個(gè)步驟如果展開的話都是非常大的篇幅。我們這里簡(jiǎn)單了解每一步是干什么的扳还、怎么做才避,達(dá)到每一步的目標(biāo)就可以了。

本文使用了 ffjpeg library a simple jpeg codec氨距,幾乎所有的編碼算法都是使用了這個(gè)庫(kù)里的桑逝。

1. 顏色模式轉(zhuǎn)換

JPEG 采用的是 YCrCb 顏色空間,YCrCb 顏色空間中衔蹲,Y 代表亮度,Cr,Cb 則代表色度和飽和度(也有人將 Cb,Cr 兩者統(tǒng)稱為色度),三者通常以 Y,U,V 來表示舆驶,即用 U 代表 Cb橱健,用 V 代表 Cr。RGB 和 YCrCb 之間的轉(zhuǎn)換關(guān)系如下所示:

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);
}

相關(guān)閱讀:


YUV - RGB Color Format Conversion


2. 采樣

為節(jié)省帶寬起見沙廉,大多數(shù) YUV 格式平均使用的每像素位數(shù)都少于 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 的水平取樣驶兜,垂直完全采樣。


表格中远寸,每一格代表一個(gè)像素

未采樣前

- 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 的水平取樣抄淑,垂直完全采樣
每采樣過一個(gè)像素點(diǎn),都會(huì)采樣其 Y 分量驰后,而 U肆资、V 分量就會(huì)間隔一個(gè)采集一個(gè)。

- 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 采樣
每采樣過一個(gè)像素點(diǎn)郑原,都會(huì)采樣其 Y 分量,而 U监署、V 分量就會(huì)間隔一行按照 2 : 1 進(jìn)行采樣颤专。

- 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 的水平取樣,垂直完全采樣
每采樣過一個(gè)像素點(diǎn)钠乏,都會(huì)采樣其 Y 分量栖秕,而 U、V 分量就會(huì)間隔一行按照 2 : 1 進(jìn)行采樣晓避。

- 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;
    }
}

相關(guān)閱讀:


一文讀懂 YUV 的采樣與格式

音視頻入門-07-認(rèn)識(shí)YUV


3. 分塊

后面的 DCT 變換是是對(duì) 8x8 的子塊進(jìn)行處理的簇捍,因此,在進(jìn)行 DCT 變換之前必須把源圖象數(shù)據(jù)進(jìn)行分塊俏拱。源圖象經(jīng)過上面的顏色模式轉(zhuǎn)換暑塑、采樣變成了 YUV 數(shù)據(jù),所以需要分別對(duì) Y U V 三個(gè)分量進(jìn)行分塊锅必。由左及右事格,由上到下依次讀取 8x8 的子塊惕艳,存放在長(zhǎng)度為 64 的數(shù)組中,即可以進(jìn)行 DCT 變換驹愚。

圖像 YUV444 數(shù)據(jù)進(jìn)行分塊:

JPEG分塊

示例代碼:

// 計(jì)算分塊數(shù)量
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;
}

// 獲取一個(gè)子塊
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];
            }
        }
    }
}

// 對(duì)數(shù)據(jù)進(jìn)行分塊
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);
        }
    }
}

相關(guān)閱讀:


JPEG壓縮原理與DCT離散余弦變換-以8x8的圖象塊為基本單位進(jìn)行編碼

JPEG圖像壓縮算法流程詳解-分塊

JPG的工作原理-圖像分成8x8像素塊


4. 離散余弦變換(DCT)

DCT 變換的公式為:

DCT 變換的公式

將原始圖像數(shù)據(jù)分為 8x8 的數(shù)據(jù)單元矩陣之后远搪,還必須將每個(gè)數(shù)值減去 128,然后一一帶入 DCT 變換公式逢捺,即可達(dá)到 DCT 變換的目的谁鳍。圖像的數(shù)據(jù)值必須減去 128,是因?yàn)?DCT 公式所接受的數(shù)字范圍是 -128 到 127 之間劫瞳。

舉例來說明一下倘潜,例子來自【JPEG 原理詳細(xì)實(shí)例分析及其在嵌入式 Linux 中的應(yīng)用】

8x8 的原始圖像:

JPEG-DCT-1

減去 128 后,使其范圍變?yōu)?-128~127:

JPEG-DCT-2

使用離散余弦變換志于,并四舍五入取最接近的整數(shù):

JPEG-DCT-3

矩陣最左上角的是直流系數(shù)(DC
矩陣其余 63 個(gè)是交流系數(shù)(AC

DCT 輸出的頻率系數(shù)矩陣最左上角的直流(DC)系數(shù)幅度最大涮因,圖中為-415;以 DC 系數(shù)為出發(fā)點(diǎn)向下恨憎、向右的其它 DCT 系數(shù)蕊退,離 DC 分量越遠(yuǎn),頻率越高憔恳,幅度值越小瓤荔,圖中最右下角為2,即圖像信息的大部分集中于直流系數(shù)及其附近的低頻頻譜上钥组,離 DC 系數(shù)越來越遠(yuǎn)的高頻頻譜幾乎不含圖像信息输硝,甚至于只含雜波。

示例代碼:

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]);
}

相關(guān)閱讀:


JPEG 原理詳細(xì)實(shí)例分析及其在嵌入式 Linux 中的應(yīng)用-3程梦、離散余弦變換 DCT

JPEG 簡(jiǎn)易文檔 - 2. DCT (離散余弦變換)

JPEG壓縮原理與DCT離散余弦變換

圖像的時(shí)頻變換——離散余弦變換

DCT變換與圖像壓縮点把、去燥

github.com/VersBinarii/dct


5. 量化

量化過程實(shí)際上就是對(duì) DCT 系數(shù)的一個(gè)優(yōu)化過程。它是利用了人眼對(duì)高頻部分不敏感的特性來實(shí)現(xiàn)數(shù)據(jù)的大幅簡(jiǎn)化屿附。

量化過程實(shí)際上是簡(jiǎn)單地把頻率領(lǐng)域上每個(gè)成份郎逃,除以一個(gè)對(duì)于該成份的常數(shù),且接著四舍五入取最接近的整數(shù)挺份。

這是整個(gè)過程中的主要有損運(yùn)算褒翰。

以這個(gè)結(jié)果來說,經(jīng)常會(huì)把很多高頻率的成份四舍五入而接近0匀泊,且剩下很多會(huì)變成小的正或負(fù)數(shù)优训。

整個(gè)量化的目的是減小非“0”系數(shù)的幅度以及增加“0”值系數(shù)的數(shù)目。

量化是圖像質(zhì)量下降的最主要原因各聘。

因?yàn)槿搜蹖?duì)亮度信號(hào)比對(duì)色差信號(hào)更敏感揣非,因此使用了兩種量化表:亮度量化值和色差量化值。

jpeg-量化-1

使用這個(gè)量化矩陣與前面所得到的 DCT 系數(shù)矩陣:

jpeg-量化-2

如躲因,使用?415(DC系數(shù))且四舍五入得到最接近的整數(shù)

jpeg-量化-3

總體上來說早敬,DCT 變換實(shí)際是空間域的低通濾波器忌傻。對(duì) Y 分量采用細(xì)量化,對(duì) UV 采用粗量化搞监。

量化表是控制 JPEG 壓縮比的關(guān)鍵芯勘,這個(gè)步驟除掉了一些高頻量;另一個(gè)重要原因是所有圖片的點(diǎn)與點(diǎn)之間會(huì)有一個(gè)色彩過渡的過程腺逛,大量的圖像信息被包含在低頻率中,經(jīng)過量化處理后衡怀,在高頻率段棍矛,將出現(xiàn)大量連續(xù)的零。

示例代碼:

/* 全局變量定義 */
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,
};

/* 函數(shù)實(shí)現(xiàn) */
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]);
}

相關(guān)閱讀:


JPEG 簡(jiǎn)易文檔-4.量化

JPEG 原理詳細(xì)實(shí)例分析及其在嵌入式 Linux 中的應(yīng)用-4. 量化

JPEG壓縮編碼-2. 量化

JPEG壓縮原理與DCT離散余弦變換-2.4 量化與反量化


6. Zigzag 掃描排序

量化后的數(shù)據(jù)抛杨,有一個(gè)很大的特點(diǎn)够委,就是直流分量相對(duì)于交流分量來說要大,而且交流分量中含有大量的 0怖现。這樣茁帽,對(duì)這個(gè)量化后的數(shù)據(jù)如何來進(jìn)行簡(jiǎn)化,從而再更大程度地進(jìn)行壓縮呢屈嗤。

這就出現(xiàn)了“Z”字形編排潘拨,如圖:

jpeg-zigzag

對(duì)于前面量化的系數(shù)所作的 “Z”字形編排結(jié)果就是:

底部 ?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 頂部

這樣做的特點(diǎn)就是會(huì)連續(xù)出現(xiàn)多個(gè)0虫腋,這樣很有利于使用簡(jiǎn)單而直觀的游程編碼(RLE:Run Length Coding)對(duì)它們進(jìn)行編碼。

示例代碼:

/* 內(nèi)部全局變量定義 */
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,
        };

/* 函數(shù)實(shí)現(xiàn) */
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]);
}

相關(guān)閱讀:


5稀余、“Z”字形編排


7. DC 系數(shù)的差分脈沖調(diào)制編碼

8x8 圖像塊經(jīng)過 DCT 變換之后得到的 DC 直流系數(shù)有兩個(gè)特點(diǎn)悦冀,一是系數(shù)的數(shù)值比較大,二是相鄰 8x8 圖像塊的 DC 系數(shù)值變化不大睛琳。根據(jù)這個(gè)特點(diǎn)盒蟆,JPEG 算法使用了差分脈沖調(diào)制編碼 (DPCM) 技術(shù),對(duì)相鄰圖像塊之間量化DC系數(shù)的差值 (Delta) 進(jìn)行編碼师骗。

DC(0)=0

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

示例代碼:

void encode_du(HUFCODEC *hfcac, HUFCODEC *hfcdc, int du[64], int *dc) {
    ...
    // 7.  DC 系數(shù)的差分脈沖調(diào)制編碼
    // 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]));
}

相關(guān)閱讀:


JPEG圖像壓縮算法流程詳解-(7)DC系數(shù)的差分脈沖調(diào)制編碼

JPEG格式與壓縮流程分析-7.DC系數(shù)的差分脈沖調(diào)制編碼


8. DC 系數(shù)的中間格式計(jì)算

JPEG 中為了更進(jìn)一步節(jié)約空間茁影,并不直接保存數(shù)據(jù)的具體數(shù)值,而是將數(shù)據(jù)按照位數(shù)分為 16 組丧凤,保存在表里面。這也就是所謂的變長(zhǎng)整數(shù)編碼 VLI步脓。即愿待,第 0 組中保存的編碼位數(shù)為 0,其編碼所代表的數(shù)字為 0靴患;第 1 組中保存的編碼位數(shù)為 1仍侥,編碼所代表的數(shù)字為 -1 或者 1 ......,如下面的表格所示鸳君,這里农渊,暫且稱其為 VLI 編碼表:

VLI

如果 DC 系數(shù)差值為 3,通過查找 VLI 可以發(fā)現(xiàn)或颊,整數(shù) 3 位于 VLI 表格的第 2 組砸紊,因此,可以寫成(2)(3)的形式囱挑,該形式醉顽,稱之為 DC 系數(shù)的中間格式。

示例代碼:

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 系數(shù)的差分脈沖調(diào)制編碼
diff = du[0] - *dc;

// 8.  DC 系數(shù)的中間格式計(jì)算
code = diff;
category_encode(&code, &size);

相關(guān)閱讀:


JPEG圖像壓縮算法流程詳解-(8)DC系數(shù)的中間格式計(jì)算

JPEG格式與壓縮流程分析-8.DC系數(shù)的中間格式計(jì)算


9. AC 系數(shù)的游程長(zhǎng)度編碼

游程編碼 RLC(Run Length Coding)來更進(jìn)一步降低數(shù)據(jù)的傳輸量平挑。利用該編碼方式游添,可以將一個(gè)字符串中重復(fù)出現(xiàn)的連續(xù)字符用兩個(gè)字節(jié)來代替,其中通熄,第一個(gè)字節(jié)代表重復(fù)的次數(shù)唆涝,第二個(gè)字節(jié)代表被重復(fù)的字符串。

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

經(jīng)過 0 RLC 之后唇辨,將呈現(xiàn)出以下的形式:

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

注意廊酣,如果 AC 系數(shù)之間連續(xù) 0 的個(gè)數(shù)超過 16,則用一個(gè)擴(kuò)展字節(jié) (15,0) 來表示 16 連續(xù)的 0赏枚。

示例代碼:

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

// 9.  AC 系數(shù)的游程長(zhǎng)度編碼
// 10. AC 系數(shù)的中間格式計(jì)算
// 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 系數(shù)的中間格式計(jì)算
        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;
}

相關(guān)閱讀:


JPEG圖像壓縮算法流程詳解-(9)AC系數(shù)的行程長(zhǎng)度編碼(RLC)

JPEG格式與壓縮流程分析-9. AC系數(shù)的行程長(zhǎng)度編碼(RLC)


10. AC 系數(shù)的中間格式計(jì)算

根據(jù)前面提到的 VLI 表格啰扛,對(duì)于前面的字符串:

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

只處理每對(duì)數(shù)右邊的那個(gè)數(shù)據(jù)嚎京,對(duì)其進(jìn)行 VLI 編碼: 查找上面的 VLI 編碼表格,可以發(fā)現(xiàn)隐解,57 在第 6 組當(dāng)中鞍帝,因此,可以將其寫成 (0,6),57 的形式煞茫,該形式帕涌,稱之為 AC 系數(shù)的中間格式。

同樣的 (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 系數(shù)的游程長(zhǎng)度編碼
// 10. AC 系數(shù)的中間格式計(jì)算
// 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 系數(shù)的中間格式計(jì)算
        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;
}

相關(guān)閱讀:


JPEG圖像壓縮算法流程詳解-(10)AC系數(shù)的中間格式

JPEG格式與壓縮流程分析-10.AC系數(shù)的中間格式算


11. 熵編碼

在得到 DC 系數(shù)的中間格式和 AC 系數(shù)的中間格式之后,為進(jìn)一步壓縮圖像數(shù)據(jù)钦扭,有必要對(duì)兩者進(jìn)行熵編碼纫版,通過對(duì)出現(xiàn)概率較高的字符采用較小的 bit 數(shù)編碼達(dá)到壓縮的目的。JPEG 標(biāo)準(zhǔn)具體規(guī)定了兩種熵編碼方式:Huffman 編碼和算術(shù)編碼客情。JPEG 基本系統(tǒng)規(guī)定采用 Huffman 編碼其弊。

Huffman 編碼:對(duì)出現(xiàn)概率大的字符分配字符長(zhǎng)度較短的二進(jìn)制編碼,對(duì)出現(xiàn)概率小的字符分配字符長(zhǎng)度較長(zhǎng)的二進(jìn)制編碼膀斋,從而使得字符的平均編碼長(zhǎng)度最短梭伐。Huffman 編碼的原理請(qǐng)參考數(shù)據(jù)結(jié)構(gòu)中的 Huffman 樹或者最優(yōu)二叉樹。

Huffman 編碼時(shí) DC 系數(shù)與 AC 系數(shù)分別采用不同的 Huffman 編碼表仰担,對(duì)于亮度和色度也采用不同的 Huffman 編碼表糊识。因此,需要 4 張 Huffman 編碼表才能完成熵編碼的工作摔蓝。具體的 Huffman 編碼采用查表的方式來高效地完成赂苗。然而,在 JPEG 標(biāo)準(zhǔn)中沒有定義缺省的 Huffman 表贮尉,用戶可以根據(jù)實(shí)際應(yīng)用自由選擇哑梳,也可以使用J PEG 標(biāo)準(zhǔn)推薦的 Huffman 表』婷耍或者預(yù)先定義一個(gè)通用的 Huffman 表鸠真,也可以針對(duì)一副特定的圖像,在壓縮編碼前通過搜集其統(tǒng)計(jì)特征來計(jì)算 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 系數(shù)的差分脈沖調(diào)制編碼
// 8.  DC 系數(shù)的中間格式計(jì)算
// 9.  AC 系數(shù)的游程長(zhǎng)度編碼
// 10. AC 系數(shù)的中間格式計(jì)算
// 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 系數(shù)的差分脈沖調(diào)制編碼
    diff = du[0] - *dc;
    *dc  = du[0];

    // 8.  DC 系數(shù)的中間格式計(jì)算
    code = diff;
    category_encode(&code, &size);

    // 11. 熵編碼 DC
    huffman_encode_step(hfcdc, size);
    bitstr_put_bits(bs, code, size);

    // 9.  AC 系數(shù)的游程長(zhǎng)度編碼
    // 10. AC 系數(shù)的中間格式計(jì)算
    // 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);
    }
}

相關(guān)閱讀:


JPEG 簡(jiǎn)易文檔 V2.15

JPEG 原理詳細(xì)分析

JPEG格式圖像編解碼

JPEG/itu-t81.pdf

JPEG文件編/解碼詳解

簡(jiǎn)述JPEG編碼實(shí)現(xiàn)的基本步驟

JPG的工作原理

How JPG Works

JPEG圖像壓縮算法流程詳解

JPEG編解碼原理


JPEG 文件寫入

根據(jù) 音視頻入門-14-JPEG文件格式詳解, JPEG 文件大體上可以分成兩個(gè)部分:標(biāo)記碼(Tag)和壓縮數(shù)據(jù)。

常用的標(biāo)記有 SOI疾渴、APP0千贯、APPn、DQT搞坝、SOF0搔谴、DHT、DRI桩撮、SOS敦第、EOI:

標(biāo)記 標(biāo)記代碼 描述
SOI 0xD8 圖像開始
APP0 0xE0 JFIF應(yīng)用數(shù)據(jù)塊
APPn 0xE1 - 0xEF 其他的應(yīng)用數(shù)據(jù)塊(n, 1~15)
DQT 0xDB 量化表
SOF0 0xC0 幀開始
DHT 0xC4 霍夫曼(Huffman)表
DRI 0xDD 差分編碼累計(jì)復(fù)位的間隔
SOS 0xDA 掃描線開始
EOI 0xD9 圖像結(jié)束

通過上面的 11 個(gè)編碼步驟,我們也得到了需要的數(shù)據(jù)店量,下一步只需將數(shù)據(jù)寫入文件即可芜果。

    // 下面開始將數(shù)據(jù)寫入文件
    //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 結(jié)果驗(yàn)收

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

運(yùn)行代碼融师,生成 JPEG 圖片:

生成 JPEG 圖片

代碼:
15-rgb-to-jpeg

參考資料:

JPEG 簡(jiǎn)易文檔 V2.15

5.7 JPEG壓縮編碼

JPEG 原理詳細(xì)分析

JPEG格式壓縮算法

JPEG壓縮編碼筆記

JPEG格式圖像編解碼

JPEG/itu-t81.pdf

某科學(xué)的JPEG編碼函數(shù) svjpeg()

JPEG文件編/解碼詳解

影像算法解析——JPEG 壓縮算法

簡(jiǎn)述JPEG編碼實(shí)現(xiàn)的基本步驟

JPG的工作原理

How JPG Works

JPEG圖像壓縮算法流程詳解

音視頻入門-07-認(rèn)識(shí)YUV

一文讀懂 YUV 的采樣與格式

YUV - RGB Color Format Conversion

JPEG壓縮原理與DCT離散余弦變換

圖像的時(shí)頻變換——離散余弦變換

DCT變換與圖像壓縮右钾、去燥

github.com/VersBinarii/dct

JPEG編解碼原理


本文由博客一文多發(fā)平臺(tái) OpenWrite 發(fā)布!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末旱爆,一起剝皮案震驚了整個(gè)濱河市舀射,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌疼鸟,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件庙曙,死亡現(xiàn)場(chǎng)離奇詭異空镜,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)捌朴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門吴攒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人砂蔽,你說我怎么就攤上這事洼怔。” “怎么了左驾?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵镣隶,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我诡右,道長(zhǎng)安岂,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任帆吻,我火速辦了婚禮域那,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘猜煮。我一直安慰自己次员,他們只是感情好败许,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著淑蔚,像睡著了一般市殷。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上束倍,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天被丧,我揣著相機(jī)與錄音,去河邊找鬼绪妹。 笑死甥桂,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的邮旷。 我是一名探鬼主播黄选,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼婶肩!你這毒婦竟也來了办陷?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤律歼,失蹤者是張志新(化名)和其女友劉穎民镜,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體险毁,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡制圈,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了畔况。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鲸鹦。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖跷跪,靈堂內(nèi)的尸體忽然破棺而出馋嗜,到底是詐尸還是另有隱情,我是刑警寧澤吵瞻,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布葛菇,位于F島的核電站,受9級(jí)特大地震影響橡羞,放射性物質(zhì)發(fā)生泄漏熟呛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一尉姨、第九天 我趴在偏房一處隱蔽的房頂上張望庵朝。 院中可真熱鬧,春花似錦、人聲如沸九府。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)侄旬。三九已至肺蔚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間儡羔,已是汗流浹背宣羊。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留汰蜘,地道東北人仇冯。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像族操,于是被迫代替她去往敵國(guó)和親苛坚。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容