音视频编解码之路:JPEG编码
音視頻編解碼之路:JPEG編碼
本文首發(fā)于音視頻編解碼之路:JPEG編碼
前言
本篇是新開(kāi)坑的 音視頻編解碼之路 的第一篇,這個(gè)系列主要通過(guò)書(shū)籍、網(wǎng)上的博文/代碼等渠道,整理與各編碼協(xié)議相關(guān)的資料和自己的理解,同時(shí)手?jǐn)]下對(duì)應(yīng)格式的“編解碼器”,形成對(duì)真正編解碼器的原理的基礎(chǔ)認(rèn)識(shí),從而后續(xù)可以進(jìn)一步研究真正意義上的編解碼器如libx264的邏輯與優(yōu)化。
之前在查找編解碼的學(xué)習(xí)資料時(shí),看到了韋神的經(jīng)驗(yàn)之談,因此就以JPEG的編碼來(lái)開(kāi)篇吧。
本篇整體脈絡(luò)來(lái)自于手動(dòng)生成一張JPEG圖片,不過(guò)針對(duì)文章中的諸多細(xì)節(jié)做了補(bǔ)充和資料匯總,另外把代碼也用C++和OOP的方式修改了下。范例工程位于avcodec_tutorial。
編碼步驟
基本系統(tǒng)的 JPEG 壓縮編碼算法一共分為 10 個(gè)步驟:
接下去我們將逐一介紹上述的各個(gè)步驟,并在其中穿插涉及的一些概念與實(shí)際代碼。
顏色模式轉(zhuǎn)換
JPEG 采用的是 YCbCr 顏色空間,這里不再贅述為啥選擇YUV等等重復(fù)較多的內(nèi)容,之前沒(méi)有接觸過(guò)的可以看下一文讀懂 YUV 的采樣與格式和其他資料來(lái)補(bǔ)補(bǔ)課。
顏色模式從RGB轉(zhuǎn)為YUV的過(guò)程中可以把采樣也一起做了,這里Demo采樣按照YUV444也就是全采樣不做額外處理的方式簡(jiǎn)單實(shí)現(xiàn),代碼如下:
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 公式// 這里在實(shí)現(xiàn)的時(shí)候踩了個(gè)坑// 之前直接將cast后的值賦值給了y/u/v數(shù)組,y/u/v數(shù)組類(lèi)型是uint8,計(jì)算出來(lái)比如v是256直接越界數(shù)值會(huì)被轉(zhuǎn)成其他如0之類(lèi)的值// 導(dǎo)致最后顏色效果錯(cuò)誤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));// 做下邊界容錯(cuò)y[index] = bound(0, yValue, 255);u[index] = bound(0, uValue, 255);v[index] = bound(0, vValue, 255);}}return true; }分塊
由于后面的 DCT 變換需要對(duì) 8x8 的子塊進(jìn)行處理,因此在進(jìn)行 DCT 變換之前必須把源圖像數(shù)據(jù)進(jìn)行分塊。源圖象經(jīng)過(guò)上面的顏色模式轉(zhuǎn)換、采樣后變成了 YUV 數(shù)據(jù),所以需要分別對(duì) Y U V 三個(gè)分量進(jìn)行分塊。具體分塊方式為由左及右,由上到下依次讀取 8x8 的子塊,存放在長(zhǎng)度為 64 的數(shù)組中,之后再進(jìn)行 DCT 變換。
因?yàn)檫@個(gè)分塊機(jī)制的原因,有些素材的寬高如果不是8的倍數(shù)的話,都需要在處理的時(shí)候進(jìn)行補(bǔ)齊。
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; // 當(dāng)前子塊下標(biāo)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; // 塊中數(shù)據(jù)位置int realPosX = blockColumn * 8 + column; // 在完整YUV數(shù)據(jù)中的X位置int realPosY = blockRow * 8 + row; // 在完整YUV數(shù)據(jù)中的Y位置int indexInOriginData = realPosY * w + realPosX; // 在原始數(shù)據(jù)中的位置if (realPosX >= w || realPosY >= h) {// 補(bǔ)齊數(shù)據(jù)yBlock[indexInSubBlock] = 0;uBlock[indexInSubBlock] = 0;vBlock[indexInSubBlock] = 0;} else {yBlock[indexInSubBlock] = y[indexInOriginData];uBlock[indexInSubBlock] = u[indexInOriginData];vBlock[indexInSubBlock] = v[indexInOriginData];}}}}}return true; }分塊后的結(jié)果類(lèi)似下面這樣,假設(shè)源圖像像素寬高為64x64,顏色轉(zhuǎn)換并分塊后將變成YUV三個(gè)通道,且每通道按8x8進(jìn)行拆分:
DCT
JPEG 里要對(duì)數(shù)據(jù)壓縮,就需要先要做一次 DCT 變換。數(shù)學(xué)方面的細(xì)節(jié)不是目前的重點(diǎn),只需要知道這個(gè)變換是將數(shù)據(jù)域從時(shí)(空)域變換到頻域,把圖片里點(diǎn)和點(diǎn)間的規(guī)律呈現(xiàn)出來(lái),是為了更方便后續(xù)的壓縮的。
先貼一下公式,對(duì)數(shù)學(xué)原理感興趣的話可以擴(kuò)展看看JPEG編碼&算術(shù)編碼、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 里是對(duì)每 8x8 個(gè)點(diǎn)作為一個(gè)單位處理的,上述代碼就是對(duì)我們剛才所劃分好的各個(gè) 8x8 的子塊進(jìn)行DCT變換。首先我們看到在進(jìn)行變換前需要將矩陣中的每個(gè)數(shù)值減去 128,然后再一一帶入 DCT 變換公式,這是因?yàn)?DCT 公式所接受的數(shù)字范圍是 -128 到 127 之間,其目的在于使像素的絕對(duì)值出現(xiàn)3位10進(jìn)制的概率大大減少。其他部分的處理邏輯暫時(shí)沒(méi)有去深究。
假定有一個(gè)8x8的圖像數(shù)據(jù)如下圖所示:
那么在減去128之后,將變成:
再經(jīng)過(guò)DCT變換,最終將變成DCT系數(shù)矩陣:
對(duì)應(yīng)于u=0,v=0的系數(shù),稱(chēng)做直流分量,即DC系數(shù),即位于矩陣的最左上角,上圖是-415的位置 對(duì)于除DC系數(shù)意外的矩陣中的其余 63 個(gè)則稱(chēng)為是交流系數(shù)(AC)
DCT 輸出的頻率系數(shù)矩陣中最左上角的直流(DC)系數(shù)幅度最大,以 DC 系數(shù)為出發(fā)點(diǎn)向下、向右的其它 DCT 系數(shù),離 DC 分量越遠(yuǎn),頻率越高,幅度值越小,即圖像信息的大部分集中于直流系數(shù)及其附近的低頻頻譜上,離 DC 系數(shù)越來(lái)越遠(yuǎn)的高頻頻譜幾乎不含圖像信息,甚至于只含雜波。這個(gè)點(diǎn)從數(shù)據(jù)上的理解可以看下JPEG壓縮原理與DCT離散余弦變換中量化與反量化部分。
關(guān)于圖像頻率可以擴(kuò)展看下圖像上的頻率指的是什么。
量化
量化是對(duì)經(jīng)過(guò) FDCT(解碼為IDCT) 變換后的頻率系數(shù)進(jìn)行量化,是一個(gè)將信號(hào)的幅度離散化的過(guò)程,量化過(guò)程實(shí)際上就是對(duì) DCT 系數(shù)的一個(gè)優(yōu)化過(guò)程,它是利用了人眼對(duì)高頻部分不敏感的特性來(lái)實(shí)現(xiàn)數(shù)據(jù)的大幅簡(jiǎn)化。在這個(gè)過(guò)程實(shí)際上是簡(jiǎn)單地把頻率領(lǐng)域上每個(gè)成份,除以一個(gè)對(duì)于該成份的常數(shù),且接著四舍五入取最接近的整數(shù),這就是整個(gè)過(guò)程中的主要有損運(yùn)算。
從結(jié)果來(lái)看,這個(gè)過(guò)程經(jīng)常會(huì)把很多高頻率的成份四舍五入而接近0,且剩下很多會(huì)變成小的正或負(fù)數(shù)。而整個(gè)量化的目的正是減小非“0”系數(shù)的幅度以及增加“0”值系數(shù)的數(shù)目,量化是圖像質(zhì)量下降的最主要原因,量化表也是控制 JPEG 壓縮比的關(guān)鍵。
因?yàn)槿搜蹖?duì)亮度信號(hào)比對(duì)色差信號(hào)更敏感,因此使用了兩種量化表:亮度量化值和色差量化值。
將前面所得到的 DCT 系數(shù)矩陣與上圖中的亮度/色度量化矩陣進(jìn)行相除并四舍五入后可得到:
總體來(lái)說(shuō)這個(gè)過(guò)程就類(lèi)似于是一個(gè)空間域的低通濾波器,對(duì) Y 分量采用細(xì)量化,對(duì) UV 采用粗量化,對(duì)低頻細(xì)量化,對(duì)高頻粗量化。對(duì)于濾波器感興趣的話可以擴(kuò)展看看這篇文章:常見(jiàn)低通、高通、帶通三種濾波器的工作原理
JPEG壓縮比例,就是通過(guò)控制量化的多少來(lái)控制。比如,上面的量化矩陣,如果我們把矩陣的每個(gè)數(shù)都double一下,那是不是會(huì)出現(xiàn)更多的0了?說(shuō)不定都只有DC系數(shù)非0,其他都是0,如果是這樣那編碼時(shí)就可以更省空間了,N個(gè)0只要一個(gè)游程編碼搞定,數(shù)據(jù)量超小。但也意味著,恢復(fù)時(shí),會(huì)帶來(lái)更多的誤差,圖像質(zhì)量也會(huì)變差了。
雖然量化步驟除掉了一些高頻量,也就是損失了高頻細(xì)節(jié),但事實(shí)上人眼對(duì)高空間頻率遠(yuǎn)沒(méi)有低頻敏感,所以處理后的視覺(jué)損失很小。另一個(gè)重要原因是所有圖片的點(diǎn)與點(diǎn)之間會(huì)有一個(gè)色彩過(guò)渡的過(guò)程,大量的圖像信息被包含在低頻率中,經(jīng)過(guò)量化處理后,在高頻率段,將出現(xiàn)大量連續(xù)的零。對(duì)于這部分可以擴(kuò)展閱讀下為什么說(shuō)圖像的低頻是輪廓,高頻是噪聲和細(xì)節(jié)以及圖像壓縮中,為什么要將圖像從空間域轉(zhuǎn)換到頻率域
/********* 外部邏輯 *********/ 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 掃描排序
量化后的數(shù)據(jù),有一個(gè)很大的特點(diǎn),就是直流分量相對(duì)于交流分量來(lái)說(shuō)要大,而且交流分量中含有大量的 0。這樣,對(duì)這個(gè)量化后的數(shù)據(jù)如何來(lái)進(jìn)行簡(jiǎn)化,從而再更大程度地進(jìn)行壓縮呢?
這就出現(xiàn)了“Z”字形編排的想法,主要思路就是從左上角第一個(gè)像素開(kāi)始以Z字形進(jìn)行編排:
至于為什么使用 Zigzag 進(jìn)行掃描排序,我個(gè)人認(rèn)為主要是因?yàn)閳D像信息的大部分集中于直流系數(shù)及其附近的低頻頻譜上,離 DC 系數(shù)越來(lái)越遠(yuǎn)的高頻頻譜幾乎不含圖像信息,因此通過(guò)該方式可以將更多的高頻數(shù)據(jù)排序到一起,以便于后續(xù)的游程編碼(RLE:Run Length Coding)對(duì)它們進(jìn)行編碼。大家可以對(duì)照上面量化后的矩陣看下使用ZigZag排序與不使用的話0數(shù)據(jù)的連續(xù)性上的差異。
/********* 外部邏輯 *********/ 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 系數(shù)的 DPCM
8x8 圖像塊經(jīng)過(guò) 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)此部分代碼混雜在最后熵編碼的整個(gè)流程中,截取部分代碼:
... // DC 系數(shù)的差分脈沖調(diào)制編碼(DPCM) diff = block[0] - dc; dc = block[0]; code = diff; // 對(duì)code做中間格式計(jì)算 ...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 ......,如下面的表格所示,這里,暫且稱(chēng)其為 VLI 編碼表:
如果 DC 系數(shù)差值為 3,通過(guò)查找 VLI 可以發(fā)現(xiàn),整數(shù) 3 位于 VLI 表格的第 2 組,因此,可以寫(xiě)成(2)(3)的形式,這里的2代表后面的數(shù)字(3)的編碼長(zhǎng)度為2位,該形式稱(chēng)之為 DC 系數(shù)的中間格式。
對(duì)于VLI可以擴(kuò)展閱讀下可變長(zhǎng)度整數(shù)的編碼,這類(lèi)思想核心就是用較小的空間存儲(chǔ)小數(shù)字,而用較大的空間存儲(chǔ)大數(shù)字,采用這種算法來(lái)對(duì)整數(shù)進(jìn)行編碼是有意義的,它可以節(jié)省存儲(chǔ)數(shù)據(jù)需要的空間或者傳輸數(shù)據(jù)時(shí)所需的帶寬。
// 接上一章節(jié)最后傳入其Code可進(jìn)行DC系數(shù)的中間格式計(jì)算 // 這里category的命名如果覺(jué)得不好理解可以進(jìn)一步去看下 // https://sce.umkc.edu/faculty-sites/lizhu/teaching/2018.fall.video-com/notes/lec04.pdf 第16頁(yè) 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 系數(shù)的 RLE
游程編碼 RLC(Run Length Coding)是一種比較簡(jiǎn)單的壓縮算法,其基本思想是將重復(fù)且連續(xù)出現(xiàn)多次的字符使用(連續(xù)出現(xiàn)次數(shù),字符)來(lái)描述,從而來(lái)更進(jìn)一步降低數(shù)據(jù)的傳輸量,舉例來(lái)說(shuō),一組數(shù)據(jù)"AAAABBBCCDEEEE",由4個(gè)A、3個(gè)B、2個(gè)C、1個(gè)D、4個(gè)E組成,經(jīng)過(guò)RLC可將數(shù)據(jù)壓縮為4A3B2C1D4E(由14個(gè)單位轉(zhuǎn)成10個(gè)單位)。簡(jiǎn)而言之,其優(yōu)點(diǎn)在于將重復(fù)性高的數(shù)據(jù)量壓縮成小單位,然而,其缺點(diǎn)在于─若該數(shù)據(jù)出現(xiàn)頻率不高,可能導(dǎo)致壓縮結(jié)果數(shù)據(jù)量比原始數(shù)據(jù)量大,例如:原始數(shù)據(jù)"ABCDE",壓縮結(jié)果為"1A1B1C1D1E"(由5個(gè)單位轉(zhuǎn)成10個(gè)單位)。
但是,在JPEG編碼中,RLC的含義就同其原有的意義略有不同。在JPEG編碼中,假設(shè)RLC編碼之后得到了一個(gè)(M,N)的數(shù)據(jù)對(duì),其中M是兩個(gè)非零AC系數(shù)之間連續(xù)的0的個(gè)數(shù)(即,行程長(zhǎng)度),N是下一個(gè)非零的AC系數(shù)的值。采用這樣的方式進(jìn)行表示,是因?yàn)锳C系數(shù)當(dāng)中有大量的0,而采用Zigzag掃描也會(huì)使得AC系數(shù)中有很多連續(xù)的0的存在,如此一來(lái),便非常適合于用RLC進(jìn)行編碼。
舉個(gè)例子來(lái)解釋一下,假設(shè)有以下數(shù)據(jù):
- 57, 45, 0, 0, 0, 0, 23, 0, -30, -8, 0, 0, 1, 000…
經(jīng)過(guò) 0 RLC 之后:
- (0,57) ; (0,45) ; (4,23) ; (1,-30) ; (0,-8) ; (2,1) ; (0,0)
注意,如果 AC 系數(shù)之間連續(xù) 0 的個(gè)數(shù)超過(guò) 16,則需要用一個(gè)擴(kuò)展字節(jié) (15,0) 來(lái)表示 16 連續(xù)的 0。這是因?yàn)楹竺?huffman 編碼的要求,每組數(shù)字前一個(gè)表示 0 的數(shù)量的必須是 4 bit,因此只能是 0~15,所以,如果有這么一組數(shù)字:
- 57, 十八個(gè)0, 3, 0, 0, 0, 0, 2, 三十三個(gè)0, 895, EOB
我們實(shí)際這樣編碼:
- (0,57) ; (15,0) (2,3) ; (4,2) ; (15,0) (15,0) (1,895) , (0,0) 注意 (15,0) 表示了 16 個(gè)連續(xù)的 0。
EOB:EOB 是一個(gè)結(jié)束標(biāo)記, 表示后面都是 0 了。我們用 (0,0) 表示 EOB,但是,如果這組數(shù)字不以 0 結(jié)束, 那么就不需要 EOB。
/********* RLE的數(shù)據(jù) *********/typedef struct { unsigned runlen : 4; unsigned codesize : 4; unsigned codedata : 16;} RLEITEM;// AC 系數(shù)的游程長(zhǎng)度編碼(RLE) // AC 系數(shù)的中間格式計(jì)算 // 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 系數(shù)的中間格式計(jì)算 categoryEncode(code, size); rlelist[j].runlen = n; rlelist[j].codesize = size; rlelist[j].codedata = code; n = 0; j++; if (size != 0) { eob = j; } }}// 設(shè)置 eobif (du[63] == 0) { rlelist[eob].runlen = 0; rlelist[eob].codesize = 0; rlelist[eob].codedata = 0; j = eob + 1;}AC 系數(shù)的中間格式計(jì)算
以DC 系數(shù)的中間格式計(jì)算中的編碼表以及AC 系數(shù)的 RLE中所舉例的RLC后的數(shù)據(jù)為例:
(0,57) ; (0,45) ; (4,23) ; (1,-30) ; (0,-8) ; (2,1) ; (0,0)
我們只處理每對(duì)數(shù)據(jù)中右邊的那個(gè)數(shù),對(duì)其進(jìn)行 VLI 編碼 :查找上面的 VLI 編碼表,可以發(fā)現(xiàn),57 在第 6 組當(dāng)中,因此可以將其寫(xiě)成 (0,6),57 的形式,該形式便稱(chēng)之為 AC 系數(shù)的中間格式。
同樣的 (0,45) 的中間格式為 (0,6),45 ;(1,-30) 的中間格式為 (1,5),-30 。
該部分在上面章節(jié)中已有涉及,就不貼代碼了。
熵編碼
在得到 DC 系數(shù)的中間格式和 AC 系數(shù)的中間格式之后,為進(jìn)一步壓縮圖像數(shù)據(jù),有必要對(duì)兩者進(jìn)行熵編碼,通過(guò)對(duì)出現(xiàn)概率較高的字符采用較小的 bit 數(shù)編碼達(dá)到壓縮的目的。JPEG 標(biāo)準(zhǔn)具體規(guī)定了兩種熵編碼方式:Huffman 編碼和算術(shù)編碼。JPEG 基本系統(tǒng)規(guī)定采用 Huffman 編碼。
熵編碼的介紹可以擴(kuò)展閱讀下三分鐘學(xué)習(xí) | 熵編碼,簡(jiǎn)單說(shuō)熵編碼就是在信息熵的極限范圍內(nèi)進(jìn)行編碼,即無(wú)損壓縮。
Huffman 編碼:對(duì)出現(xiàn)概率大的字符分配字符長(zhǎng)度較短的二進(jìn)制編碼,對(duì)出現(xiàn)概率小的字符分配字符長(zhǎng)度較長(zhǎng)的二進(jìn)制編碼,從而使得字符的平均編碼長(zhǎng)度最短。Huffman 編碼的原理可以擴(kuò)展閱讀下算法科普:有趣的霍夫曼編碼。
Huffman 編碼時(shí) DC 系數(shù)與 AC 系數(shù)分別采用不同的 Huffman 編碼表,對(duì)于亮度和色度也采用不同的 Huffman 編碼表。因此,需要 4 張 Huffman 編碼表才能完成熵編碼的工作,等到具體 Huffman 編碼時(shí)再采用查表的方式來(lái)高效地完成。然而,在 JPEG 標(biāo)準(zhǔn)中沒(méi)有定義缺省的 Huffman 表,用戶(hù)可以根據(jù)實(shí)際應(yīng)用自由選擇,也可以使用 JPEG 標(biāo)準(zhǔn)推薦的 Huffman 表,或者預(yù)先定義一個(gè)通用的 Huffman 表,也可以針對(duì)一副特定的圖像,在壓縮編碼前通過(guò)搜集其統(tǒng)計(jì)特征來(lái)計(jì)算 Huffman 表的值。
結(jié)合 Huffman 編碼以及上述的DPCM、RLE以及對(duì)應(yīng)的中間格式,參照VLI編碼表,我們?cè)賮?lái)整體地通過(guò)一個(gè)例子解釋下數(shù)據(jù)最終壓縮后的樣子:
假定經(jīng)過(guò)RLE之后有如下AC數(shù)據(jù):
(0,57) ; (0,45) ; (4,23) ; (1,-30) ; (0,-8) ; (2,1) ; (0,0)
只處理每對(duì)數(shù)右邊的那個(gè):
- 57 是第 6 組的, 實(shí)際保存值為 111001 , 所以被編碼為 (6,111001)
- 45 , 同樣的操作, 編碼為 (6,101101)
- 23 -> (5,10111)
- -30 -> (5,00001)
- -8 -> (4,0111)
- 1 -> (1,1)
最后,最開(kāi)始的那串?dāng)?shù)字就變成了:
- (0,6), 111001 ; (0,6), 101101 ; (4,5), 10111; (1,5), 00001; (0,4) , 0111 ; (2,1), 1 ; (0,0)
括號(hào)里的數(shù)值正好合成一個(gè)字節(jié),后面被編碼的數(shù)字表示范圍是 -32767..32767。合成的字節(jié)里,高 4 位是前續(xù) 0 的個(gè)數(shù),低 4 位描述了后面數(shù)字的位數(shù)。
再進(jìn)一步通過(guò) Huffman 查找得到如果 (0,6) 的 huffman 編碼為 111000 ,那么最終編碼的數(shù)據(jù)便是:
- 111000 111001
最后看下DC的編碼,假設(shè)DC的diff值是-511,編碼為 (9, 000000000) ,如果 9 的 Huffman 編碼是 1111110 ,那么在 JPG 文件中, DC 的 2 進(jìn)制表示為 1111110 000000000,最終加上上面AC的第一個(gè)數(shù)據(jù),編碼為:
- 1111110 000000000 111000 111001 ...
JPEG 文件寫(xiě)入
JPEG 文件大體上可以分成兩個(gè)部分:標(biāo)記碼(Tag)和壓縮數(shù)據(jù)。
常用的標(biāo)記有 SOI、APP0、APPn、DQT、SOF0、DHT、DRI、SOS、EOI:
| SOI | 0xD8 | 圖像開(kāi)始 |
| APP0 | 0xE0 | JFIF應(yīng)用數(shù)據(jù)塊 |
| APPn | 0xE1 - 0xEF | 其他的應(yīng)用數(shù)據(jù)塊(n, 1~15) |
| DQT | 0xDB | 量化表 |
| SOF0 | 0xC0 | 幀開(kāi)始 |
| DHT | 0xC4 | 霍夫曼(Huffman)表 |
| DRI | 0xDD | 差分編碼累計(jì)復(fù)位的間隔 |
| SOS | 0xDA | 掃描線開(kāi)始 |
| EOI | 0xD9 | 圖像結(jié)束 |
更具體的細(xì)節(jié)可擴(kuò)展閱讀下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;}完工
到這里我們編寫(xiě)的JpegEncoder就可以將傳入的RGB24格式的數(shù)據(jù)壓縮編碼成YUV444的JPEG文件了,可以運(yùn)行 avcodec_tutorial 項(xiàng)目,運(yùn)行后將在你的桌面看到如下內(nèi)容隨機(jī)生成的圖片:
參考文章
手動(dòng)生成一張JPEG圖片
JPEG 簡(jiǎn)易文檔
JPEG 中的范式哈夫曼編碼
JPEG圖像壓縮算法流程詳解
JPEG編解碼原理及代碼調(diào)試
JPEG standard
Variable Length Coding (VLC) in JPEG
JPEG編碼&算術(shù)編碼、LZW編碼
圖像的時(shí)頻變換——離散余弦變換
圖像上的頻率指的是什么
為什么說(shuō)圖像的低頻是輪廓,高頻是噪聲和細(xì)節(jié)
DCT變換與圖像壓縮、去燥
JPEG壓縮原理與DCT離散余弦變換
JPEG 標(biāo)準(zhǔn)推薦的亮度、色度DC、AC Huffman 編碼表
一文讀懂 YUV 的采樣與格式
JPEG文件格式詳解
[數(shù)據(jù)壓縮之游程編碼](
本文由博客一文多發(fā)平臺(tái) OpenWrite 發(fā)布!
總結(jié)
以上是生活随笔為你收集整理的音视频编解码之路:JPEG编码的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: iphone11返回上一级手势怎么设置_
- 下一篇: 怎么在计算机上面掉出CMD,电脑没有cm