試簡介視頻編碼技術?

我需要宏觀的關於視頻編碼的指導,最好是基於HEVC的編碼器原理。如幀間預測編碼過程,幀內預測編碼過程,熵編碼過程。細節可不詳細,但求有宏觀的流程概念,不勝感激!


要徹底理解視頻編碼原理,看書都是虛的,需要實際動手,實現一個簡單的視頻編碼器:

知識準備:基本圖像處理知識,信號的時域和頻域問題,熟練掌握傅立葉正反變換,一維、二維傅立葉變換,以及其變種,dct變換,快速dct變換。

第一步:實現有損圖像壓縮和解壓

參考 JPEG原理,將RGB-&>YUV,然後Y/U/V看成三張不同的圖片,將其中一張圖片分為 8x8的block進行 dct變換(可以直接進行二維dct變換,或者按一定順序將8x8的二維數組整理成一個64位元組的一維數組),還是得到一個8x8的整數頻率數據。於是表示圖像大輪廓的低頻信號(人眼敏感的信號)集中在 8x8的左上角;表示圖像細節的高頻信號集中在右下角。

接著將其量化,所謂量化,就是信號採樣的步長,8x8的整數頻率數據塊,每個數據都要除以對應位置的步長,左上角相對重要的低頻信號步長是1,也就是說0-255,是多少就是多少。而右下角是不太重要的高頻信號,比如步長取10,那麼這些位置的數據都要/10,實際解碼的時候再將他們*10恢復出來,這樣經過編碼的時候/10和解碼的時候*10,那麼步長為10的信號1, 13, 25, 37就會變成規矩的:0, 10, 20, 30, 對小於步長10的部分我們直接丟棄了,因為高頻不太重要。

經過量化以後,8x8的數據塊左上角的數據由於步長小,都是比較離散的,而靠近右下角的高頻數據,都比較統一,或者是一串0,因此圖像大量的細節被我們丟棄了,這時候,我們用無損壓縮方式,比如lzma2演算法(jpeg是rle + huffman)將這64個byte壓縮起來,由於後面高頻數據步長大,做了除法以後,這些值都比較小,而且比較靠近,甚至右下部分都是一串0,十分便於壓縮。

JPEG圖像有個問題就是低碼率時 block邊界比較嚴重,現代圖片壓縮技術往往要配合一些de-block演算法,比如最簡單的就是邊界部分幾個像素點和周圍插值模糊一下。

做到這裡我們實現了一個同 jpeg類似的靜態圖片有損壓縮演算法。在視頻裡面用來保存I幀數據。

第二步:實現宏塊誤差計算

視頻由連續的若干圖像幀組成,分為 I幀,P幀,所謂I幀,就是不依賴就可以獨立解碼的視頻圖像幀,而P幀則需要依賴前面已解碼的視頻幀,配合一定數據才能生成出來。所以視頻中I幀往往都比較大,而P幀比較小,如果播放器一開始收到了P幀那麼是無法播放的,只有收到下一個I幀才能開始播放。I幀多了視頻就變大,I幀少了,數據量是小了,但視頻受到丟包或者數據錯誤的影響卻又會更嚴重。

那麼所謂運動預測編碼,其實就是P幀的生成過程:繼續將圖片分成 16x16的block(為了簡單只討論yuv的y分量壓縮)。I幀內部單幀圖片壓縮我們採用了8x8的block,而這裡用16x16的block來提高幀間編碼壓縮率(當然也會有更多細節損失),我們用 x, y表示像素點坐標,而s,t表示block坐標,那麼坐標為(x,y)的像素點所屬的block坐標為:

s = x / 16 = x &>&> 4
t = y / 16 = y &>&> 4

接著要計算兩個block的相似度,即矢量的距離,可以表示為一個256維矢量(16x16)像素點色彩距離的平方,我們先定義兩個顏色的誤差為:

PixelDiff(c1, c2) = (c1- c2) ^ 2

那麼256個點的誤差可以表示為所有對應點的像素誤差和:

BlockDiff(b1, b2) = sum( PixelDiff(c1, c2) for c1 in b1 for c2 in b2)

代碼化為:

int block_diff(const unsigned char b1[16][16], const unsigned char b2[16][16]) {
int sum = 0;
for (int i = 0; i &< 16; i++) { for (int j = 0; j &< 16; j++) { int c1 = b1[i][j]; int c2 = b2[i][j]; sum += (c1 - c2) * (c1 - c2); } } return sum; }

有了這個block求差的函數,我們就可以針對特定block,搜索另外若干個block中哪個和它最相似了(誤差最小)。

第三步:實現運動預測編碼

根據上面的宏塊比較函數,你已經可以知道兩個block到底像不像了,越象的block,block_diff返回值越低。那麼我們有兩幀相鄰的圖片,P1,P2,假設 P1已經完成編碼了,現在要對 P2進行P幀編碼,其實就是輪詢 P2裡面的每一個 block,為P2中每一個block找出上一幀中相似度最高的block坐標,並記錄下來,具體偽代碼可以表示為:

unsigned char block[16][16];
for (int t = 0; t &<= maxt; t++) { for (int s = 0; s &<= maxs; s++) { picture_get_block(P2, s * 16, t * 16, block); // 取得圖片 P2 的 block int x, y; block_search_nearest(P1, x, y, block); // 在P1中搜索最相似的block output(x, y); // 將P1中最相似的block的左上角像素坐標 (x, y) 輸出 } }

其中在P1中搜索最相似 block的 block_search_nearest 函數原理是比較簡單的,我們可以暴力點用兩個for循環輪詢 P1中每個像素點開始的16x16的block(速度較慢),當然實際中不可能這麼暴力搜索,而是圍繞P2中該block對應坐標在P1中位置作為中心,慢慢四周擴散,搜索一定步長,並得到一個:按照一定順序進行搜索,並且在一定範圍內最相似的宏塊坐標

於是P2進行運動預測編碼的結果就是一大堆(x,y)的坐標,代表P2上每個block在上一幀P1裡面最相似的 block的位置。反過來說可能更容易理解,我們可以把第三步整個過程定義為:

怎麼用若干 P1里不同起始位置的block拼湊出圖片P2來,使得拼湊以後的結果和P2最像。

拼湊的結果就是一系列(x,y)的坐標數據,我們繼續用lzma2將它們先壓縮起來,按照 vcd的解析度352 x 240,我們橫向需要 352 / 16 = 22個block,縱向需要 240 / 16 = 15 個block,可以用 P1中 22 x 15 = 330 個 block的坐標信息生成一張和P2很類似的圖片 P2" :

for (int t = 0; t &< 15; t++) { for (int s = 0; s &< 22; s++, next++) { int x = block_positions[next].x; // 取得對應 P1上的 block像素位置 x int y = block_positions[next].y; // 取得對應 P1上的 block像素位置 y // 將 P1位置(x,y)開始的 16 x 16 的圖塊拷貝到 P2"的 (s * 16, t * 16)處 CopyRect(P2", s * 16, t * 16, P1, x, y, 16, 16); } }

我們把用來生成P2的P1稱為 P2的 「參考幀」,再把剛才那一堆P1內用來拼成P2的 block坐標稱為 「運動矢量」,這是P幀裡面最主要的數據內容。但是此時由P1和這些坐標數據拼湊出來的P2,你會發現粗看和P2很象,但細看會發現有些支離破碎,並且邊緣比較明顯,怎麼辦呢?我們需要第四步。

第四步:實現P幀編碼

有了剛才的運動預測矢量(一堆block的坐標),我們先用P1按照這些數據拼湊出一張類似 P2的新圖片叫做P2",然後同P2上每個像素做減法,得到一張保存 differ的圖片:

D2 = (P2 - P2") / 2

誤差圖片 D2上每一個點等於 P2上對應位置的點的顏色減去 P2"上對應位置的點的顏色再除以2,用8位表示差值,值是循環的,比如-2就是255,這裡一般可以在結果上 + 0x80,即 128代表0,129代表2,127代表-2。繼續用一個 8位的整數可以表示 [-254, 254] 之間的誤差範圍,步長精度是2。

按照第三步實現的邏輯,P2"其實已經很像P2了,只是有些誤差,我們將這些誤差保存成了圖片D2,所以圖片D2中,信息量其實已經很小了,都是些細節修善,比起直接保存一張完整圖片熵要低很多的。所以我們將 D2用類似第一步提到的有損圖片壓縮方法進行編碼,得到最終的P幀數據:

Encode(P2) = Lzma2(block_positions) + 有損圖像編碼(D2)

具體在操作的時候,D2的圖像塊可以用16x16進行有損編碼,因為前面的運動預測數據是按16x16的宏塊搜索的,而不用象I幀那樣精確的用8x8表示,同時保存誤差圖時,量化的精度可以更粗一些用不著象I幀那麼精確,可以理解成用質量更低的JPEG編碼,按照16x16的塊進行編碼,加上誤差圖D2本來信息量就不高,這樣的保存方式能夠節省不少空間。

第五步:實現GOP生成

通過前面的代碼,我們實現了I幀編碼和P幀編碼,P幀是參考P1對P2進行編碼,而所謂B幀,就是參考 P1和 P3對P2進行編碼,當然間隔不一定是1,比如可以是參考P1和P5對P2進行編碼,前提條件是P5可以依賴P1及以前的數據進行解碼。

不過對於一個完整的簡版視頻編碼器,I幀和P幀編碼已經夠了,市面上任然有很多面向低延遲的商用編碼器是直接幹掉B幀的,因為做實時傳輸時收到B幀沒法播放,之後再往後好幾幀收到下一個I或者P幀時,先前收到的B幀才能被解碼出來,造成不少的延遲。

而所謂的 GOP (Group of picture) 就是由一系列類似 I, P, B, B, P, B, B, P, B, B P 組成的一個可以完整被解碼出來的圖像組,而所謂視頻文件,就是一個接一個的GOP,每個GOP由一個I幀開頭,然後接下來一組連續的P 或者 B構成,播放時只有完整收到下一個GOP的I幀才能開始播放。

最後是關於參考幀選擇,前面提到的 P2生成過程是參考了 P1,假設一個GOP中十張圖片,是 I1, P1, P2, P3, P4, ... P9 保存的,如果P1參考I1,P2參考P1, P3參考P2 .... P9參考P8這樣每一個P幀都是參考上一幀進行編碼的話,誤差容易越來越大,因為P1已經引入一定誤差了,P2在P1的基礎上誤差更大,到了P9的話,圖片質量可能已經沒法看了。

因此正確的參考幀選擇往往不需要這樣死板,比如可以P1-P9全部參考I1來生成,或者,P1-P4參考I1來生成,而P5-P9則參考P5來生成,這樣步子小點,誤差也不算太離譜。

第六步:容器組裝

我們生成了一組組編碼過的GOP了,這時候需要一定的文件格式將他們恰當的保存下來,記錄視頻信息,比如解析度,幀率,時間索引等,就是一個類似MP4(h.264的容器)文件的東西。至此一個簡單的小型編碼器我們已經完成了,可以用 SDL / DirectX / OpenGL 配合實現一個播放器,愉快的將自己編碼器編碼的視頻播放出來。

第七步:優化改進

這時候你已經大概學習並掌握了視頻編碼的基礎原理了,接下來大量的優化改進的坑等著你去填呢。優化有兩大方向,編碼效率優化和編碼性能優化:前者追求同質量(同信噪比)下更低的碼率,後者追求同樣質量和碼率的情況下,更快的編碼速度。

有這個基礎後接下來可以回過頭去看JPEG標準,MPEG1-2標準,並閱讀相關實現代碼,你會發現簡單很多了,接著肯H.264代碼,不用全部看可以針對性的了解以下H.264的I幀編碼和各種搜索預測方法,有H.264的底子,你了解 HEVC和 vpx就比較容易了。

參考這些編碼器一些有意思的實現來改進自己的編碼器,試驗性質,可以側重原理,各種優化技巧了解下即可,本來就是hack性質的。

有卯用呢?首先肯定很好玩,其次,當你有需要使用並修改這些編碼器為他們增加新特性的時候,你會發現前面的知識很管用了。

------
有朋友說光有代碼沒有圖片演示看不大明白,好我們補充一下圖片演示:

這是第一幀畫面:P1(我們的參考幀)

這是第二幀畫面:P2(需要編碼的幀)

從視頻中截取的兩張間隔1-2秒的畫面,和實際情況類似,下面我們進行幾次運動搜索:

搜索演示1:搜索P2中車輛的車牌在P1中最接近的位置(上圖P1,下圖P2)

這是一個演示程序,滑鼠選中P2上任意16x16的Block,即可搜索出P1上的 BestMatch 宏塊。雖然車輛在運動,從遠到近,但是依然找到了最接近的宏塊坐標。

搜索演示2:空中電線交叉位置(上圖P1,下圖P2)

搜索演示3:報刊停的廣告海報

同樣順利在P1中找到最接近P2裏海報的宏塊位置。

圖片全搜索:根據P1和運動矢量數據(在P2中搜索到每一個宏塊在P1中最相似的位置集合)還原出來的P2",即完全用P1各個位置的宏塊拼湊出來最像P2的圖片P2",效果如下:

仔細觀察,有些支離破碎對吧?肯定啊,拼湊出來的東西就是這樣,現在我們用P2`和P2像素相減,得到差分圖 D2 = (P2" - P2) / 2 + 0x80:

嗯,這就是P2`和P2兩幅圖片的不同處,看到沒?基本只有低頻了!高頻數據少到我們可以忽略,這時用有損壓縮方式比較差的效果來保存誤差圖D2,只要5KB的大小。

接著我們根據運動矢量還原的 P2"及差分圖D2來還原新的 P2,NewP2 = P2" + (D2 - 0x80) * 2:

這就是之前支離破碎的 P2` 加上誤差 D2之後變成了清晰可見的樣子,基本還原了原圖P2。

由於D2僅僅佔5KB,加上壓縮過後的運動矢量不過7KB,所以參考P1我們只需要額外 7KB的數據量就可以完整表示P2了,而如果獨立將P2用質量尚可的有損壓縮方式獨立壓縮,則至少要去到50-60KB,這一下節省了差不多8倍的空間,正就是所謂運動編碼的基本原理。

實際在使用中,參考幀並不一定是前面一幀,也不一定是同一個GOP的I幀,因為GOP間隔較長時,後面的圖片離I幀變化可能已經很大了,因此常見做法是最近15幀中選擇一幀誤差最小的作為參考幀,雖然彩色畫面有YUV三個分量,但是大量的預測工作和最有選擇通常是根據Y分量的灰度幀進行判斷的。

再者誤差我們保存的是(P2-P2』)/2 + 0x80,實際使用時我們會用更有效率的方式,比如讓[-64,64]之間的色差精度為1,[-255,-64], [64, 255] 之間的色差精度為2-3,這樣會更加真實一些。

同時上文很多地方用的是直接lzma2進行簡單存儲,實際使用時一般會引入熵編碼,對數據進行一定層次的整理然後再壓縮,性能會好不少。

現代視頻編碼中,除了幀間預測,I幀還使用了大量幀內預測,而不是完全dct量化後編碼,前面幀間預測我們使用了參考幀的宏塊移動拼湊新幀的方式進行,而所謂幀內預測就是同一幅畫面中,未編碼部分使用已編碼部分拼湊而成。。。。。。。

這些說來話就長了,不過此時相信各位理解起 MPEG2 來會發現並不是什麼太深奧的東西,MPEG2的各項規範熟悉了,H264也就好說了,讀資料的同時自己做一下試驗參照理論,應該能輕鬆很多。

---


視音頻編碼的意義就是用儘可能小的帶寬傳送高質量的視音頻數據。
從mpeg-1到mpeg-2,H.263到H.264到你提到的HEVC即H.265,標準的新提出也是為了讓碼流更優化、壓縮效率更高、穩定性更強。
視頻編碼的大致過程就是將基本碼流通過幀內預測編碼、幀間預測編碼、整數變換、量化、熵編碼等步驟獲取更加優化、剛乾擾能力強的碼流。
另外,各類編碼標準不僅負責進行視頻編碼,還會對音頻進行編碼,最後將視頻碼流和音頻碼流進行時分復用。

你所問的三種編碼的方式,大體來說如下。
幀內預測編碼不直接對圖像塊進行處理,而是根據鄰近塊的值來預測當前宏塊的值,再對預測值與原始值的差值進行變換、量化、編碼,從而減少傳輸相同信息所需帶寬。
幀間預測編碼即為基於塊的運動估計和運動補償,用相鄰幀的塊值預測當前幀的塊值,再對預測值與原始值的差值進行變換、量化、編碼,從而減少傳輸相同信息所需帶寬。
熵編碼在視音頻編碼中的作用與其他領域當中相同,將經過前面步驟得到的碼流進行進一步的編碼,如霍夫曼編碼,將平均碼長降低,獲得更高的傳輸效率。

不同的編碼標準在以上三步編碼當中具體的標準都不同,隨著技術的進步、演算法複雜度的增加,都在使視音頻碼流一步一步更加優化,如幀內和幀間編碼中使用的宏塊的大小和形狀類型越來越多,針對不同的視頻信號能夠進行更有效率的處理。


如果要了解宏觀的概念,就去看下HEVC的overview,順帶看下H.264的overview。論文的解釋會比較全面,清楚。如果樓主不願意看英文的話,網上搜下,應該會有很多blog的介紹


宏觀的流程的話,其實還是比較好理解的,網上資料也多,可以下載一些論文看看,還有網上的一些博客也不錯,這些對於宏觀的了解基本夠了


可以從JPEG編碼,H261編碼的源代碼讀起,這兩個編碼簡單,通過它們基本可以理解編解碼過程。H264來說,開始看標準,會覺得複雜,現在看看,主要是編碼數據加上編碼的邏輯判斷。


推薦閱讀:

使用電腦進行圖像處理(視頻、照片、繪圖、設計)工作,顯示器色彩空間為何不能統一?怎麼解決?
色度圖的問題
如何用ps把常見圖片設置為只有自己想要的顏色呢?如下圖
這種像是點陣圖的是如何做到放大後是另一張圖片的?
有什麼軟體可以實現移動拍攝有視差的圖像拼接?

TAG:H264 | 圖像處理 | 編碼 | 編程技巧 | 視頻編碼 |