JPEG編碼中的采樣過程其實就是一個圖像數據轉換成若干個8*8數據塊的過程,如下圖將原始圖像分成8*8個小塊(block),每個block中有64個像素:
ImageSharp源碼中關於采樣有有兩種選擇,一種叫JpegSubsample.Ratio444,一種叫JpegSubsample.Ratio420。這兩種選擇就是對於JPEG圖像的兩種采樣方法,就是我們常說的YUV采樣。
1. 什么是YUV
與我們熟知的RGB類似,YUV也是一種顏色編碼方法,主要用於電視系統以及模擬視頻領域,它將亮度信息(Y)與色彩信息(UV)分離,沒有UV信息一樣可以顯示完整的圖像,只不過是黑白的,下圖就是我用ImageSharp源碼做的實驗,只保留Y通道,UV通道設成0:
原圖 Ratio420只留Y通道,159KB
Ratio444只留Y通道,181KB
YUV不像RGB那樣要求三個獨立的視頻信號同時傳輸,所以用YUV方式傳送占用極少的頻寬。YUV碼流的存儲格式其實與其采樣的方式密切相關,主流的采樣方式有三種,YUV4:4:4,YUV4:2:2,YUV4:2:0。現在的YUV是通常用於計算機領域用來表示使用YCbCr編碼的文件。可以粗淺地視YUV為YCbCr,我們后面也換成這種叫法。
一般看到這里,會出現一個概念,叫做MCU。不是單片機更不是漫威,它中文意思是最小的編碼單元(Minimum Coded Unit),它是圖像中一個正方矩陣像素的數據,對於不同的采樣,MCU的像素也不一樣:
1.YCbCr 4:4:4(其他資料可能叫1:1:1)
這種是最簡單的采樣方式,這種方式里面,數據流存放數據順序就是YCbCr ,每個MCU代表着8*8個像素塊。
2.YCbCr 4:2:2
這種方式下數據流存放順序如圖所示,每個MCU代表着16x8個像素,Y0和Y1分別指向兩個8*8像素塊,這兩個像素塊共用一個Cb和Cr通道。
3.YCbCr 4:2:0(其他資料可能叫4:1:1)
這種方式下每個MCU代表着16x16個像素,Y0和Y1分別指向4個8*8像素塊,這4個像素塊共用一個Cb和Cr通道。
2. YUV采樣源碼分析
回到ImageSharp源碼中,我們可以看到兩種采樣方法,分別是4:4:4和4:2:0,我們看一下他們的方法,先是4:4:4。

1 /// <summary> 2 /// Encodes the image with no subsampling. 3 /// </summary> 4 /// <typeparam name="TPixel">The pixel format.</typeparam> 5 /// <param name="pixels">The pixel accessor providing access to the image pixels.</param> 6 private void Encode444<TPixel>(Image<TPixel> pixels) 7 where TPixel : struct, IPixel<TPixel> 8 { 9 // TODO: Need a JpegScanEncoder<TPixel> class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.) 10 // (Partially done with YCbCrForwardConverter<TPixel>) 11 Block8x8F temp1 = default; 12 Block8x8F temp2 = default; 13 14 Block8x8F onStackLuminanceQuantTable = this.luminanceQuantTable; 15 Block8x8F onStackChrominanceQuantTable = this.chrominanceQuantTable; 16 17 var unzig = ZigZag.CreateUnzigTable(); 18 19 // ReSharper disable once InconsistentNaming 20 int prevDCY = 0, prevDCCb = 0, prevDCCr = 0; 21 22 var pixelConverter = YCbCrForwardConverter<TPixel>.Create(); 23 24 for (int y = 0; y < pixels.Height; y += 8) 25 { 26 for (int x = 0; x < pixels.Width; x += 8) 27 { 28 pixelConverter.Convert(pixels.Frames.RootFrame, x, y); 29 30 prevDCY = this.WriteBlock( 31 QuantIndex.Luminance, 32 prevDCY, 33 &pixelConverter.Y, 34 &temp1, 35 &temp2, 36 &onStackLuminanceQuantTable, 37 unzig.Data); 38 prevDCCb = this.WriteBlock( 39 QuantIndex.Chrominance, 40 prevDCCb, 41 &pixelConverter.Cb, 42 &temp1, 43 &temp2, 44 &onStackChrominanceQuantTable, 45 unzig.Data); 46 prevDCCr = this.WriteBlock( 47 QuantIndex.Chrominance, 48 prevDCCr, 49 &pixelConverter.Cr, 50 &temp1, 51 &temp2, 52 &onStackChrominanceQuantTable, 53 unzig.Data); 54 } 55 } 56 }
首先對圖像長寬進行兩次for循環,不難看出每次for循環就是一個8*8的行程,然后通過pixelConverter.Convert方法將RGB轉換為YCbCr,最后依次寫入這8*8個像素的Y,Cb,Cr通道數據。
再看4:2:0的源碼:

1 /// <summary> 2 /// Encodes the image with subsampling. The Cb and Cr components are each subsampled 3 /// at a factor of 2 both horizontally and vertically. 4 /// </summary> 5 /// <typeparam name="TPixel">The pixel format.</typeparam> 6 /// <param name="pixels">The pixel accessor providing access to the image pixels.</param> 7 private void Encode420<TPixel>(Image<TPixel> pixels) 8 where TPixel : struct, IPixel<TPixel> 9 { 10 // TODO: Need a JpegScanEncoder<TPixel> class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.) 11 Block8x8F b = default; 12 13 BlockQuad cb = default; 14 BlockQuad cr = default; 15 var cbPtr = (Block8x8F*)cb.Data; 16 var crPtr = (Block8x8F*)cr.Data; 17 18 Block8x8F temp1 = default; 19 Block8x8F temp2 = default; 20 21 Block8x8F onStackLuminanceQuantTable = this.luminanceQuantTable; 22 Block8x8F onStackChrominanceQuantTable = this.chrominanceQuantTable; 23 24 var unzig = ZigZag.CreateUnzigTable(); 25 26 var pixelConverter = YCbCrForwardConverter<TPixel>.Create(); 27 28 // ReSharper disable once InconsistentNaming 29 int prevDCY = 0, prevDCCb = 0, prevDCCr = 0; 30 31 for (int y = 0; y < pixels.Height; y += 16) 32 { 33 for (int x = 0; x < pixels.Width; x += 16) 34 { 35 for (int i = 0; i < 4; i++) 36 { 37 int xOff = (i & 1) * 8; 38 int yOff = (i & 2) * 4; 39 40 pixelConverter.Convert(pixels.Frames.RootFrame, x + xOff, y + yOff); 41 42 cbPtr[i] = pixelConverter.Cb; 43 crPtr[i] = pixelConverter.Cr; 44 45 prevDCY = this.WriteBlock( 46 QuantIndex.Luminance, 47 prevDCY, 48 &pixelConverter.Y, 49 &temp1, 50 &temp2, 51 &onStackLuminanceQuantTable, 52 unzig.Data); 53 } 54 55 Block8x8F.Scale16X16To8X8(&b, cbPtr); 56 prevDCCb = this.WriteBlock( 57 QuantIndex.Chrominance, 58 prevDCCb, 59 &b, 60 &temp1, 61 &temp2, 62 &onStackChrominanceQuantTable, 63 unzig.Data); 64 65 Block8x8F.Scale16X16To8X8(&b, crPtr); 66 prevDCCr = this.WriteBlock( 67 QuantIndex.Chrominance, 68 prevDCCr, 69 &b, 70 &temp1, 71 &temp2, 72 &onStackChrominanceQuantTable, 73 unzig.Data); 74 } 75 } 76 }
這里先對圖像進行16*16行程的遍歷,然后在for循環內部,再單獨對Y通道進行4*4的遍歷,每次遍歷都寫入依次Y通道的編碼,最后再寫入Cb和Cr的編碼,這樣一來,就使得每4個Y通道共用一個Cb和Cr通道,我們根據這個采樣規則也不難得出結論:4:2:0采樣的圖像存儲大小肯定比4:4:4的少。我們從上面那個實驗也可以證明這個結論。
注意這里的WriteBlock這個方法,這個方法就包含后面要說的DCT變換、量化和熵編碼。

1 /// <summary> 2 /// Writes a block of pixel data using the given quantization table, 3 /// returning the post-quantized DC value of the DCT-transformed block. 4 /// The block is in natural (not zig-zag) order. 5 /// </summary> 6 /// <param name="index">The quantization table index.</param> 7 /// <param name="prevDC">The previous DC value.</param> 8 /// <param name="src">Source block</param> 9 /// <param name="tempDest1">Temporal block to be used as FDCT Destination</param> 10 /// <param name="tempDest2">Temporal block 2</param> 11 /// <param name="quant">Quantization table</param> 12 /// <param name="unzigPtr">The 8x8 Unzig block pointer</param> 13 /// <returns> 14 /// The <see cref="int"/> 15 /// </returns> 16 private int WriteBlock( 17 QuantIndex index, 18 int prevDC, 19 Block8x8F* src, 20 Block8x8F* tempDest1, 21 Block8x8F* tempDest2, 22 Block8x8F* quant, 23 byte* unzigPtr) 24 { 25 //1.DCT變換 26 FastFloatingPointDCT.TransformFDCT(ref *src, ref *tempDest1, ref *tempDest2); 27 //2.量化 28 Block8x8F.Quantize(tempDest1, tempDest2, quant, unzigPtr); 29 float* unziggedDestPtr = (float*)tempDest2; 30 //JTEST:如果是色度的話,全部舍去 31 if (index== QuantIndex.Chrominance) 32 { 33 for (int i = 0; i < 64; i++) 34 { 35 unziggedDestPtr[i] = 0; 36 } 37 } 38 int dc = (int)unziggedDestPtr[0]; 39 //3.熵編碼寫入DC分量 40 // Emit the DC delta. 41 this.EmitHuffRLE((HuffIndex)((2 * (int)index) + 0), 0, dc - prevDC); 42 //4.熵編碼寫入AC分量 43 // Emit the AC components. 44 var h = (HuffIndex)((2 * (int)index) + 1); 45 int runLength = 0; 46 47 for (int zig = 1; zig < Block8x8F.Size; zig++) 48 { 49 int ac = (int)unziggedDestPtr[zig]; 50 //JTEST:這里將ac分量變成0 zig>0 zig>1 zig>2... zig>64 51 //if (zig>0) 52 //{ 53 // ac = 0; 54 //} 55 if (ac == 0) 56 { 57 runLength++; 58 } 59 else 60 { 61 while (runLength > 15) 62 { 63 this.EmitHuff(h, 0xf0); 64 runLength -= 16; 65 } 66 67 this.EmitHuffRLE(h, runLength, ac); 68 runLength = 0; 69 } 70 } 71 72 if (runLength > 0) 73 { 74 this.EmitHuff(h, 0x00); 75 } 76 77 return dc; 78 }
這個WriteBlock方法將是后面文章分析重點。
3. 采樣因子
我們怎么確定一張JPEG圖像是用那種采樣方式呢,這個信息可以在標記為SOF0中獲取到,這個標記中有一個顏色分類信息通常為9個字節,里面就包含了 水平/垂直采樣因子 ,我們根據采樣因子就能知道是用4:4:4、4:2:2還是4:2:0。
4. 總結
這一章算是JPEG編碼\解碼過程中的頭一場戲,請大家記住我們在這個過程中輸入的是一張圖像數據,輸出的是若干個8*8數據塊,8*8數據塊這個概念將會一直伴隨我們JPEG整個編碼。后面章節我們將重點分析上文出現的WriteBlock這個方法,這個方法里包含DCT變換、量化和熵編碼。
系列目錄:
ImageSharp源碼詳解之JPEG編碼原理(1)JPEG介紹
ImageSharp源碼詳解之JPEG壓縮原理(3)DCT變換
ImageSharp源碼詳解之JPEG壓縮原理(4)量化
ImageSharp源碼詳解之JPEG壓縮原理(6)C#源碼解析及調試技巧