音视频编解码之路:JPEG编码
音視頻編解碼之路:JPEG編碼
本文首發于音視頻編解碼之路:JPEG編碼
前言
本篇是新開坑的 音視頻編解碼之路 的第一篇,這個系列主要通過書籍、網上的博文/代碼等渠道,整理與各編碼協議相關的資料和自己的理解,同時手擼下對應格式的“編解碼器”,形成對真正編解碼器的原理的基礎認識,從而后續可以進一步研究真正意義上的編解碼器如libx264的邏輯與優化。
之前在查找編解碼的學習資料時,看到了韋神的經驗之談,因此就以JPEG的編碼來開篇吧。
本篇整體脈絡來自于手動生成一張JPEG圖片,不過針對文章中的諸多細節做了補充和資料匯總,另外把代碼也用C++和OOP的方式修改了下。范例工程位于avcodec_tutorial。
編碼步驟
基本系統的 JPEG 壓縮編碼算法一共分為 10 個步驟:
接下去我們將逐一介紹上述的各個步驟,并在其中穿插涉及的一些概念與實際代碼。
顏色模式轉換
JPEG 采用的是 YCbCr 顏色空間,這里不再贅述為啥選擇YUV等等重復較多的內容,之前沒有接觸過的可以看下一文讀懂 YUV 的采樣與格式和其他資料來補補課。
顏色模式從RGB轉為YUV的過程中可以把采樣也一起做了,這里Demo采樣按照YUV444也就是全采樣不做額外處理的方式簡單實現,代碼如下:
uint8_t bound(uint8_t min, int value, uint8_t max) {if(value <= min) {return min;}if(value >= max) {return max;}return value; }bool JpegEncoder::rgbToYUV444(const uint8_t *r, const uint8_t *g, const uint8_t *b,const unsigned int &w, const unsigned int &h,uint8_t *const y, uint8_t *const u, uint8_t *const v) {for (int row = 0; row < h; row++) {for (int column = 0; column < w; column++) {int index = row * w + column;// rgb -> yuv 公式// 這里在實現的時候踩了個坑// 之前直接將cast后的值賦值給了y/u/v數組,y/u/v數組類型是uint8,計算出來比如v是256直接越界數值會被轉成其他如0之類的值// 導致最后顏色效果錯誤int yValue = static_cast<int>(round(0.299 * r[index] + 0.587 * g[index] + 0.114 * b[index]));int uValue = static_cast<int>(round(-0.169 * r[index] - 0.331 * g[index] + 0.500 * b[index] + 128));int vValue = static_cast<int>(round(0.500 * r[index] - 0.419 * g[index] - 0.081 * b[index] + 128));// 做下邊界容錯y[index] = bound(0, yValue, 255);u[index] = bound(0, uValue, 255);v[index] = bound(0, vValue, 255);}}return true; }分塊
由于后面的 DCT 變換需要對 8x8 的子塊進行處理,因此在進行 DCT 變換之前必須把源圖像數據進行分塊。源圖象經過上面的顏色模式轉換、采樣后變成了 YUV 數據,所以需要分別對 Y U V 三個分量進行分塊。具體分塊方式為由左及右,由上到下依次讀取 8x8 的子塊,存放在長度為 64 的數組中,之后再進行 DCT 變換。
因為這個分塊機制的原因,有些素材的寬高如果不是8的倍數的話,都需要在處理的時候進行補齊。
bool JpegEncoder::yuvToBlocks(const uint8_t *y, const uint8_t *u, const uint8_t *v,const unsigned int &w, const unsigned int &h,uint8_t yBlocks[][64], uint8_t uBlocks[][64], uint8_t vBlocks[][64]) {int wBlockSize = w / 8 + (w % 8 == 0 ? 0 : 1);int hBlockSize = h / 8 + (h % 8 == 0 ? 0 : 1);for (int blockRow = 0; blockRow < hBlockSize; blockRow++) {for (int blockColumn = 0; blockColumn < wBlockSize; blockColumn++) {int blockIndex = blockRow * wBlockSize + blockColumn; // 當前子塊下標uint8_t *yBlock = yBlocks[blockIndex];uint8_t *uBlock = uBlocks[blockIndex];uint8_t *vBlock = vBlocks[blockIndex];for (int row = 0; row < 8; row++) {for (int column = 0; column < 8; column++) {int indexInSubBlock = row * 8 + column; // 塊中數據位置int realPosX = blockColumn * 8 + column; // 在完整YUV數據中的X位置int realPosY = blockRow * 8 + row; // 在完整YUV數據中的Y位置int indexInOriginData = realPosY * w + realPosX; // 在原始數據中的位置if (realPosX >= w || realPosY >= h) {// 補齊數據yBlock[indexInSubBlock] = 0;uBlock[indexInSubBlock] = 0;vBlock[indexInSubBlock] = 0;} else {yBlock[indexInSubBlock] = y[indexInOriginData];uBlock[indexInSubBlock] = u[indexInOriginData];vBlock[indexInSubBlock] = v[indexInOriginData];}}}}}return true; }分塊后的結果類似下面這樣,假設源圖像像素寬高為64x64,顏色轉換并分塊后將變成YUV三個通道,且每通道按8x8進行拆分:
DCT
JPEG 里要對數據壓縮,就需要先要做一次 DCT 變換。數學方面的細節不是目前的重點,只需要知道這個變換是將數據域從時(空)域變換到頻域,把圖片里點和點間的規律呈現出來,是為了更方便后續的壓縮的。
先貼一下公式,對數學原理感興趣的話可以擴展看看JPEG編碼&算術編碼、LZW編碼等資料:
DCT變換與圖像壓縮、去燥里面還講到了為什么JPEG選擇DCT而不選擇DFT的原因。
再貼一下代碼:
/********* 外部邏輯 *********/ bool JpegEncoder::blocksFDCT(const uint8_t (*yBlocks)[64], const uint8_t (*uBlocks)[64], const uint8_t (*vBlocks)[64],const unsigned int &w, const unsigned int &h,int yDCT[][64], int uDCT[][64], int vDCT[][64]) {int wBlockSize = w / 8 + (w % 8 == 0 ? 0 : 1);int hBlockSize = h / 8 + (h % 8 == 0 ? 0 : 1);int blockSize = wBlockSize * hBlockSize;std::shared_ptr<JpegFDCT> fdct = std::make_shared<JpegFDCT>();for (int blockIndex = 0; blockIndex < blockSize; blockIndex++) {uint8_t *yBlock = yBlocks[blockIndex];uint8_t *uBlock = uBlocks[blockIndex];uint8_t *vBlock = vBlocks[blockIndex];for (int i = 0; i < 64; i++) {yDCT[blockIndex][i] = yBlock[i] - 128;uDCT[blockIndex][i] = uBlock[i] - 128;vDCT[blockIndex][i] = vBlock[i] - 128;yDCT[blockIndex][i] = yDCT[blockIndex][i] << 2;uDCT[blockIndex][i] = uDCT[blockIndex][i] << 2;vDCT[blockIndex][i] = vDCT[blockIndex][i] << 2;}fdct->fdct2d8x8(yDCT[blockIndex]);fdct->fdct2d8x8(uDCT[blockIndex]);fdct->fdct2d8x8(vDCT[blockIndex]);}return true; }/********* DCT 邏輯 *********/ JpegFDCT::JpegFDCT() {int i, j;float factor[64];// 初始化fdct factorconst float AAN_DCT_FACTOR[DCT_SIZE] = {1.0f, 1.387039845f, 1.306562965f, 1.175875602f,1.0f, 0.785694958f, 0.541196100f, 0.275899379f,};for (i = 0; i < DCT_SIZE; i++) {for (j = 0; j < DCT_SIZE; j++) {factor[i * 8 + j] = 1.0f / (AAN_DCT_FACTOR[i] * AAN_DCT_FACTOR[j] * 8);}}for (i = 0; i < 64; i++) {mFDCTFactor[i] = float2Fix(factor[i]);} } int JpegFDCT::float2Fix(float value) {return static_cast<int>(value * (1 << FIX_Q)); } void JpegFDCT::fdct2d8x8(int *data8x8, int *ftab) {int tmp0, tmp1, tmp2, tmp3;int tmp4, tmp5, tmp6, tmp7;int tmp10, tmp11, tmp12, tmp13;int z1, z2, z3, z4, z5, z11, z13;int *dataptr;int ctr;/* Pass 1: process rows. */dataptr = data8x8;for (ctr = 0; ctr < DCT_SIZE; ctr++) {tmp0 = dataptr[0] + dataptr[7];tmp7 = dataptr[0] - dataptr[7];tmp1 = dataptr[1] + dataptr[6];tmp6 = dataptr[1] - dataptr[6];tmp2 = dataptr[2] + dataptr[5];tmp5 = dataptr[2] - dataptr[5];tmp3 = dataptr[3] + dataptr[4];tmp4 = dataptr[3] - dataptr[4];/* Even part */tmp10 = tmp0 + tmp3; /* phase 2 */tmp13 = tmp0 - tmp3;tmp11 = tmp1 + tmp2;tmp12 = tmp1 - tmp2;dataptr[0] = tmp10 + tmp11; /* phase 3 */dataptr[4] = tmp10 - tmp11;z1 = (tmp12 + tmp13) * float2Fix(0.707106781f) >> FIX_Q; /* c4 */dataptr[2] = tmp13 + z1; /* phase 5 */dataptr[6] = tmp13 - z1;/* Odd part */tmp10 = tmp4 + tmp5; /* phase 2 */tmp11 = tmp5 + tmp6;tmp12 = tmp6 + tmp7;/* The rotator is modified from fig 4-8 to avoid extra negations. */z5 = (tmp10 - tmp12) * float2Fix(0.382683433f) >> FIX_Q; /* c6 */z2 = (float2Fix(0.541196100f) * tmp10 >> FIX_Q) + z5; /* c2-c6 */z4 = (float2Fix(1.306562965f) * tmp12 >> FIX_Q) + z5; /* c2+c6 */z3 = tmp11 * float2Fix(0.707106781f) >> FIX_Q; /* c4 */z11 = tmp7 + z3; /* phase 5 */z13 = tmp7 - z3;dataptr[5] = z13 + z2; /* phase 6 */dataptr[3] = z13 - z2;dataptr[1] = z11 + z4;dataptr[7] = z11 - z4;dataptr += DCT_SIZE; /* advance pointer to next row */}/* Pass 2: process columns. */dataptr = data8x8;for (ctr = 0; ctr < DCT_SIZE; ctr++) {tmp0 = dataptr[DCT_SIZE * 0] + dataptr[DCT_SIZE * 7];tmp7 = dataptr[DCT_SIZE * 0] - dataptr[DCT_SIZE * 7];tmp1 = dataptr[DCT_SIZE * 1] + dataptr[DCT_SIZE * 6];tmp6 = dataptr[DCT_SIZE * 1] - dataptr[DCT_SIZE * 6];tmp2 = dataptr[DCT_SIZE * 2] + dataptr[DCT_SIZE * 5];tmp5 = dataptr[DCT_SIZE * 2] - dataptr[DCT_SIZE * 5];tmp3 = dataptr[DCT_SIZE * 3] + dataptr[DCT_SIZE * 4];tmp4 = dataptr[DCT_SIZE * 3] - dataptr[DCT_SIZE * 4];/* Even part */tmp10 = tmp0 + tmp3; /* phase 2 */tmp13 = tmp0 - tmp3;tmp11 = tmp1 + tmp2;tmp12 = tmp1 - tmp2;dataptr[DCT_SIZE * 0] = tmp10 + tmp11; /* phase 3 */dataptr[DCT_SIZE * 4] = tmp10 - tmp11;z1 = (tmp12 + tmp13) * float2Fix(0.707106781f) >> FIX_Q; /* c4 */dataptr[DCT_SIZE * 2] = tmp13 + z1; /* phase 5 */dataptr[DCT_SIZE * 6] = tmp13 - z1;/* Odd part */tmp10 = tmp4 + tmp5; /* phase 2 */tmp11 = tmp5 + tmp6;tmp12 = tmp6 + tmp7;/* The rotator is modified from fig 4-8 to avoid extra negations. */z5 = (tmp10 - tmp12) * float2Fix(0.382683433f) >> FIX_Q; /* c6 */z2 = (float2Fix(0.541196100f) * tmp10 >> FIX_Q) + z5; /* c2-c6 */z4 = (float2Fix(1.306562965f) * tmp12 >> FIX_Q) + z5; /* c2+c6 */z3 = tmp11 * float2Fix(0.707106781f) >> FIX_Q; /* c4 */z11 = tmp7 + z3; /* phase 5 */z13 = tmp7 - z3;dataptr[DCT_SIZE * 5] = z13 + z2; /* phase 6 */dataptr[DCT_SIZE * 3] = z13 - z2;dataptr[DCT_SIZE * 1] = z11 + z4;dataptr[DCT_SIZE * 7] = z11 - z4;dataptr++; /* advance pointer to next column */}ftab = ftab ? ftab : mFDCTFactor;for (ctr = 0; ctr < 64; ctr++) {data8x8[ctr] *= ftab[ctr];data8x8[ctr] >>= FIX_Q + 2;} }JPEG 里是對每 8x8 個點作為一個單位處理的,上述代碼就是對我們剛才所劃分好的各個 8x8 的子塊進行DCT變換。首先我們看到在進行變換前需要將矩陣中的每個數值減去 128,然后再一一帶入 DCT 變換公式,這是因為 DCT 公式所接受的數字范圍是 -128 到 127 之間,其目的在于使像素的絕對值出現3位10進制的概率大大減少。其他部分的處理邏輯暫時沒有去深究。
假定有一個8x8的圖像數據如下圖所示:
那么在減去128之后,將變成:
再經過DCT變換,最終將變成DCT系數矩陣:
對應于u=0,v=0的系數,稱做直流分量,即DC系數,即位于矩陣的最左上角,上圖是-415的位置 對于除DC系數意外的矩陣中的其余 63 個則稱為是交流系數(AC)
DCT 輸出的頻率系數矩陣中最左上角的直流(DC)系數幅度最大,以 DC 系數為出發點向下、向右的其它 DCT 系數,離 DC 分量越遠,頻率越高,幅度值越小,即圖像信息的大部分集中于直流系數及其附近的低頻頻譜上,離 DC 系數越來越遠的高頻頻譜幾乎不含圖像信息,甚至于只含雜波。這個點從數據上的理解可以看下JPEG壓縮原理與DCT離散余弦變換中量化與反量化部分。
關于圖像頻率可以擴展看下圖像上的頻率指的是什么。
量化
量化是對經過 FDCT(解碼為IDCT) 變換后的頻率系數進行量化,是一個將信號的幅度離散化的過程,量化過程實際上就是對 DCT 系數的一個優化過程,它是利用了人眼對高頻部分不敏感的特性來實現數據的大幅簡化。在這個過程實際上是簡單地把頻率領域上每個成份,除以一個對于該成份的常數,且接著四舍五入取最接近的整數,這就是整個過程中的主要有損運算。
從結果來看,這個過程經常會把很多高頻率的成份四舍五入而接近0,且剩下很多會變成小的正或負數。而整個量化的目的正是減小非“0”系數的幅度以及增加“0”值系數的數目,量化是圖像質量下降的最主要原因,量化表也是控制 JPEG 壓縮比的關鍵。
因為人眼對亮度信號比對色差信號更敏感,因此使用了兩種量化表:亮度量化值和色差量化值。
將前面所得到的 DCT 系數矩陣與上圖中的亮度/色度量化矩陣進行相除并四舍五入后可得到:
總體來說這個過程就類似于是一個空間域的低通濾波器,對 Y 分量采用細量化,對 UV 采用粗量化,對低頻細量化,對高頻粗量化。對于濾波器感興趣的話可以擴展看看這篇文章:常見低通、高通、帶通三種濾波器的工作原理
JPEG壓縮比例,就是通過控制量化的多少來控制。比如,上面的量化矩陣,如果我們把矩陣的每個數都double一下,那是不是會出現更多的0了?說不定都只有DC系數非0,其他都是0,如果是這樣那編碼時就可以更省空間了,N個0只要一個游程編碼搞定,數據量超小。但也意味著,恢復時,會帶來更多的誤差,圖像質量也會變差了。
雖然量化步驟除掉了一些高頻量,也就是損失了高頻細節,但事實上人眼對高空間頻率遠沒有低頻敏感,所以處理后的視覺損失很小。另一個重要原因是所有圖片的點與點之間會有一個色彩過渡的過程,大量的圖像信息被包含在低頻率中,經過量化處理后,在高頻率段,將出現大量連續的零。對于這部分可以擴展閱讀下為什么說圖像的低頻是輪廓,高頻是噪聲和細節以及圖像壓縮中,為什么要將圖像從空間域轉換到頻率域
/********* 外部邏輯 *********/ bool JpegEncoder::fdctToQuant(int yDCT[][64], int uDCT[][64], int vDCT[][64],const unsigned int &w, const unsigned int &h) {int wBlockSize = w / 8 + (w % 8 == 0 ? 0 : 1);int hBlockSize = h / 8 + (h % 8 == 0 ? 0 : 1);int blockSize = wBlockSize * hBlockSize;std::shared_ptr<JpegQuant> quant = std::make_shared<JpegQuant>();for (int blockIndex = 0; blockIndex < blockSize; blockIndex++) {int *yBlock = yDCT[blockIndex];int *uBlock = uDCT[blockIndex];int *vBlock = vDCT[blockIndex];quant->quantEncode8x8(yBlock, true);quant->quantEncode8x8(uBlock, false);quant->quantEncode8x8(vBlock, false);}return true; }/********* 量化邏輯 *********/ void JpegQuant::quantEncode8x8(int *data8x8, bool luminance) {for (int i = 0; i < 64; i++) {if (luminance) {data8x8[i] /= STD_QUANT_TAB_LUMIN[i];} else {data8x8[i] /= STD_QUANT_TAB_CHROM[i];}} }Zigzag 掃描排序
量化后的數據,有一個很大的特點,就是直流分量相對于交流分量來說要大,而且交流分量中含有大量的 0。這樣,對這個量化后的數據如何來進行簡化,從而再更大程度地進行壓縮呢?
這就出現了“Z”字形編排的想法,主要思路就是從左上角第一個像素開始以Z字形進行編排:
至于為什么使用 Zigzag 進行掃描排序,我個人認為主要是因為圖像信息的大部分集中于直流系數及其附近的低頻頻譜上,離 DC 系數越來越遠的高頻頻譜幾乎不含圖像信息,因此通過該方式可以將更多的高頻數據排序到一起,以便于后續的游程編碼(RLE:Run Length Coding)對它們進行編碼。大家可以對照上面量化后的矩陣看下使用ZigZag排序與不使用的話0數據的連續性上的差異。
/********* 外部邏輯 *********/ bool JpegEncoder::quantToZigzag(int yQuant[][64], int uQuant[][64], int vQuant[][64],const unsigned int &w, const unsigned int &h) {int wBlockSize = w / 8 + (w % 8 == 0 ? 0 : 1);int hBlockSize = h / 8 + (h % 8 == 0 ? 0 : 1);int blockSize = wBlockSize * hBlockSize;std::shared_ptr<JpegZigzag> zigzag = std::make_shared<JpegZigzag>();for (int blockIndex = 0; blockIndex < blockSize; blockIndex++) {int *yBlock = yQuant[blockIndex];int *uBlock = uQuant[blockIndex];int *vBlock = vQuant[blockIndex];zigzag->zigzag(yBlock);zigzag->zigzag(uBlock);zigzag->zigzag(vBlock);}return true; }/********* 排序邏輯 *********/ void JpegZigzag::zigzag(int *const data8x8) {int temp[64];for (int i = 0; i < 64; i++) {temp[i] = data8x8[ZIGZAG_INDEX[i]];}for (int i = 0; i < 64; i++) {data8x8[i] = temp[i];} }DC 系數的 DPCM
8x8 圖像塊經過 DCT 變換之后得到的 DC 直流系數有兩個特點,一是系數的數值比較大,二是相鄰 8x8 圖像塊的 DC 系數值變化不大。根據這個特點,JPEG 算法使用了差分脈沖調制編碼 (DPCM) 技術,對相鄰圖像塊之間量化DC系數的差值 (Delta) 進行編碼。
DC(0)=0 Delta = DC(i) - DC(i-1)此部分代碼混雜在最后熵編碼的整個流程中,截取部分代碼:
... // DC 系數的差分脈沖調制編碼(DPCM) diff = block[0] - dc; dc = block[0]; code = diff; // 對code做中間格式計算 ...DC 系數的中間格式計算
JPEG 中為了更進一步節約空間,并不直接保存數據的具體數值,而是將數據按照位數分為 16 組,保存在表里面。這也就是所謂的變長整數編碼 VLI。即,第 0 組中保存的編碼位數為 0,其編碼所代表的數字為 0;第 1 組中保存的編碼位數為 1,編碼所代表的數字為 -1 或者 1 ......,如下面的表格所示,這里,暫且稱其為 VLI 編碼表:
如果 DC 系數差值為 3,通過查找 VLI 可以發現,整數 3 位于 VLI 表格的第 2 組,因此,可以寫成(2)(3)的形式,這里的2代表后面的數字(3)的編碼長度為2位,該形式稱之為 DC 系數的中間格式。
對于VLI可以擴展閱讀下可變長度整數的編碼,這類思想核心就是用較小的空間存儲小數字,而用較大的空間存儲大數字,采用這種算法來對整數進行編碼是有意義的,它可以節省存儲數據需要的空間或者傳輸數據時所需的帶寬。
// 接上一章節最后傳入其Code可進行DC系數的中間格式計算 // 這里category的命名如果覺得不好理解可以進一步去看下 // https://sce.umkc.edu/faculty-sites/lizhu/teaching/2018.fall.video-com/notes/lec04.pdf 第16頁 void HuffmanCodec::categoryEncode(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;} }AC 系數的 RLE
游程編碼 RLC(Run Length Coding)是一種比較簡單的壓縮算法,其基本思想是將重復且連續出現多次的字符使用(連續出現次數,字符)來描述,從而來更進一步降低數據的傳輸量,舉例來說,一組數據"AAAABBBCCDEEEE",由4個A、3個B、2個C、1個D、4個E組成,經過RLC可將數據壓縮為4A3B2C1D4E(由14個單位轉成10個單位)。簡而言之,其優點在于將重復性高的數據量壓縮成小單位,然而,其缺點在于─若該數據出現頻率不高,可能導致壓縮結果數據量比原始數據量大,例如:原始數據"ABCDE",壓縮結果為"1A1B1C1D1E"(由5個單位轉成10個單位)。
但是,在JPEG編碼中,RLC的含義就同其原有的意義略有不同。在JPEG編碼中,假設RLC編碼之后得到了一個(M,N)的數據對,其中M是兩個非零AC系數之間連續的0的個數(即,行程長度),N是下一個非零的AC系數的值。采用這樣的方式進行表示,是因為AC系數當中有大量的0,而采用Zigzag掃描也會使得AC系數中有很多連續的0的存在,如此一來,便非常適合于用RLC進行編碼。
舉個例子來解釋一下,假設有以下數據:
- 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。這是因為后面 huffman 編碼的要求,每組數字前一個表示 0 的數量的必須是 4 bit,因此只能是 0~15,所以,如果有這么一組數字:
- 57, 十八個0, 3, 0, 0, 0, 0, 2, 三十三個0, 895, EOB
我們實際這樣編碼:
- (0,57) ; (15,0) (2,3) ; (4,2) ; (15,0) (15,0) (1,895) , (0,0) 注意 (15,0) 表示了 16 個連續的 0。
EOB:EOB 是一個結束標記, 表示后面都是 0 了。我們用 (0,0) 表示 EOB,但是,如果這組數字不以 0 結束, 那么就不需要 EOB。
/********* RLE的數據 *********/typedef struct { unsigned runlen : 4; unsigned codesize : 4; unsigned codedata : 16;} RLEITEM;// AC 系數的游程長度編碼(RLE) // AC 系數的中間格式計算 // rle encode for acfor (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; // AC 系數的中間格式計算 categoryEncode(code, size); rlelist[j].runlen = n; rlelist[j].codesize = size; rlelist[j].codedata = code; n = 0; j++; if (size != 0) { eob = j; } }}// 設置 eobif (du[63] == 0) { rlelist[eob].runlen = 0; rlelist[eob].codesize = 0; rlelist[eob].codedata = 0; j = eob + 1;}AC 系數的中間格式計算
以DC 系數的中間格式計算中的編碼表以及AC 系數的 RLE中所舉例的RLC后的數據為例:
(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 。
該部分在上面章節中已有涉及,就不貼代碼了。
熵編碼
在得到 DC 系數的中間格式和 AC 系數的中間格式之后,為進一步壓縮圖像數據,有必要對兩者進行熵編碼,通過對出現概率較高的字符采用較小的 bit 數編碼達到壓縮的目的。JPEG 標準具體規定了兩種熵編碼方式:Huffman 編碼和算術編碼。JPEG 基本系統規定采用 Huffman 編碼。
熵編碼的介紹可以擴展閱讀下三分鐘學習 | 熵編碼,簡單說熵編碼就是在信息熵的極限范圍內進行編碼,即無損壓縮。
Huffman 編碼:對出現概率大的字符分配字符長度較短的二進制編碼,對出現概率小的字符分配字符長度較長的二進制編碼,從而使得字符的平均編碼長度最短。Huffman 編碼的原理可以擴展閱讀下算法科普:有趣的霍夫曼編碼。
Huffman 編碼時 DC 系數與 AC 系數分別采用不同的 Huffman 編碼表,對于亮度和色度也采用不同的 Huffman 編碼表。因此,需要 4 張 Huffman 編碼表才能完成熵編碼的工作,等到具體 Huffman 編碼時再采用查表的方式來高效地完成。然而,在 JPEG 標準中沒有定義缺省的 Huffman 表,用戶可以根據實際應用自由選擇,也可以使用 JPEG 標準推薦的 Huffman 表,或者預先定義一個通用的 Huffman 表,也可以針對一副特定的圖像,在壓縮編碼前通過搜集其統計特征來計算 Huffman 表的值。
結合 Huffman 編碼以及上述的DPCM、RLE以及對應的中間格式,參照VLI編碼表,我們再來整體地通過一個例子解釋下數據最終壓縮后的樣子:
假定經過RLE之后有如下AC數據:
(0,57) ; (0,45) ; (4,23) ; (1,-30) ; (0,-8) ; (2,1) ; (0,0)
只處理每對數右邊的那個:
- 57 是第 6 組的, 實際保存值為 111001 , 所以被編碼為 (6,111001)
- 45 , 同樣的操作, 編碼為 (6,101101)
- 23 -> (5,10111)
- -30 -> (5,00001)
- -8 -> (4,0111)
- 1 -> (1,1)
最后,最開始的那串數字就變成了:
- (0,6), 111001 ; (0,6), 101101 ; (4,5), 10111; (1,5), 00001; (0,4) , 0111 ; (2,1), 1 ; (0,0)
括號里的數值正好合成一個字節,后面被編碼的數字表示范圍是 -32767..32767。合成的字節里,高 4 位是前續 0 的個數,低 4 位描述了后面數字的位數。
再進一步通過 Huffman 查找得到如果 (0,6) 的 huffman 編碼為 111000 ,那么最終編碼的數據便是:
- 111000 111001
最后看下DC的編碼,假設DC的diff值是-511,編碼為 (9, 000000000) ,如果 9 的 Huffman 編碼是 1111110 ,那么在 JPG 文件中, DC 的 2 進制表示為 1111110 000000000,最終加上上面AC的第一個數據,編碼為:
- 1111110 000000000 111000 111001 ...
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 | 圖像結束 |
更具體的細節可擴展閱讀下JPEG文件格式詳解。
bool JpegEncoder::writeToFile(char* buffer, long dataLength, const unsigned int &w, const unsigned int &h) { FILE *fp = fopen(mOutputPath.data(), "wb"); // SOI fputc(0xff, fp); fputc(0xd8, fp); // DQT const int *pqtab[2] = {JpegQuant::STD_QUANT_TAB_LUMIN, JpegQuant::STD_QUANT_TAB_CHROM}; 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][JpegZigzag::ZIGZAG_INDEX[j]], fp); } } // 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(h >> 8, fp); // height fputc(h >> 0, fp); // height fputc(w >> 8, fp); // width fputc(w >> 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); // DHT AC const uint8_t *huftabAC[] = { HuffmanCodec::STD_HUFTAB_LUMIN_AC, HuffmanCodec::STD_HUFTAB_CHROM_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 += huftabAC[i][j]; } fputc(len >> 8, fp); fputc(len >> 0, fp); fputc(i + 0x10, fp); fwrite(huftabAC[i], len - 3, 1, fp); } // DHT DC const uint8_t *huftabDC[] = { HuffmanCodec::STD_HUFTAB_LUMIN_DC, HuffmanCodec::STD_HUFTAB_CHROM_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 += huftabDC[i][j]; } fputc(len >> 8, fp); fputc(len >> 0, fp); fputc(i + 0x00, fp); fwrite(huftabDC[i], len - 3, 1, fp); } // 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); // data fwrite(buffer, dataLength, 1, fp); // EOI fputc(0xff, fp); fputc(0xd9, fp); fflush(fp); fclose(fp); return true;}完工
到這里我們編寫的JpegEncoder就可以將傳入的RGB24格式的數據壓縮編碼成YUV444的JPEG文件了,可以運行 avcodec_tutorial 項目,運行后將在你的桌面看到如下內容隨機生成的圖片:
參考文章
手動生成一張JPEG圖片
JPEG 簡易文檔
JPEG 中的范式哈夫曼編碼
JPEG圖像壓縮算法流程詳解
JPEG編解碼原理及代碼調試
JPEG standard
Variable Length Coding (VLC) in JPEG
JPEG編碼&算術編碼、LZW編碼
圖像的時頻變換——離散余弦變換
圖像上的頻率指的是什么
為什么說圖像的低頻是輪廓,高頻是噪聲和細節
DCT變換與圖像壓縮、去燥
JPEG壓縮原理與DCT離散余弦變換
JPEG 標準推薦的亮度、色度DC、AC Huffman 編碼表
一文讀懂 YUV 的采樣與格式
JPEG文件格式詳解
[數據壓縮之游程編碼](
本文由博客一文多發平臺 OpenWrite 發布!
總結
以上是生活随笔為你收集整理的音视频编解码之路:JPEG编码的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: iphone11返回上一级手势怎么设置_
- 下一篇: 怎么在计算机上面掉出CMD,电脑没有cm