ImageSharp源碼詳解之JPEG編碼原理(2)采樣


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         }    
View Code

 首先對圖像長寬進行兩次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         }    
View Code

這里先對圖像進行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         }
View Code

這個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編碼原理(2)采樣

ImageSharp源碼詳解之JPEG壓縮原理(3)DCT變換

ImageSharp源碼詳解之JPEG壓縮原理(4)量化

ImageSharp源碼詳解之JPEG壓縮原理(5)熵編碼

ImageSharp源碼詳解之JPEG壓縮原理(6)C#源碼解析及調試技巧


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM