您現在的位置是:首頁 > 棋牌

顏色空間變換這事,也有好多門道和故事

  • 由 繼珩的溫故知新小本本 發表于 棋牌
  • 2022-11-04
簡介方法1 - 用C實現一般來講,CPU適合整數計算、位運算這型別的操作的(除了一些特殊架構的CPU設計),因此上面的計算公式,如果能對等的轉換位一們由整數運算和位運算混合的演算法設計,就會加速非常多

bt矩陣什麼意思

Ask how something can be done rather than say it can‘t be done。 ——- Bo Bennett

為什麼說技術人是有差別的

技術人是有天差地別的差距的,第一次有這個感覺是2006年的時候。

那會兒我還在學校讀研,一個偶然的機會拿到了到Thomson Corporate Research實習的機會,就興沖沖地去了。離學校是真的遠啊,一個來回三個小時,一個月收入到手差不多1500吧我記得,比之前兼職少得多。基本都花在車費和吃飯上了,算白乾。

顏色空間變換這事,也有好多門道和故事

但是在那裡的經歷對人生的幫助真的很大,認識了很多傳說中的大牛,對自己的研究工作幫助很大。趕上了標準組織最蓬勃發展的階段(雖然已過最高峰),並參與了一些其中的事情。同時也見識了不同技術人做事情想問題,根本不是一個層次的。

今天我們聊其中的一位,現在回想也覺得在那個階段硬體能力、軟體SDK能力限制條件下,做當時在做的事情,並使其商業化,一些想法和思路太超前了。以後我們有機會陸續再講幾位。對於技術工作人員,見工作如見人,向他們致敬。

一切還得續著上面,從顏色空間轉化開始說起。

顏色空間變化的幾種實現方式

我們今天只說從BT。601 YUV轉RGB這一個場景,其他的變換方式找到相應的轉換矩陣就可以,並不複雜。而且也可以套用今天我們講到的所有方法去做,並沒有太特別的。

關注YUV到RGB的變化,最大的場景就是從影片解碼之後到影片顯示這個過程。因為編解碼資料的結果就是YUV資料,一般為YUV420,廣電級別的有YUV422, YUV444的,原理也相似。

顏色空間變換這事,也有好多門道和故事

對於一個輸入畫素(Y, U, V),BT。601的轉換至(R, G, B)的演算法如下

R = (Y - 16) * 1。164 - V * -1。596G = (Y - 16) * 1。164 - U * 0。391 - V * 0。813B = (Y - 16) * 1。164 - U * -2。018

這個演算法用任意一種語言都很容易寫出來,但是因為如此多的浮點運算,每個畫素都 要計算,CPU本來就是一個整數向的運算單元,效能可以預見,都會非常差。因此針對計算環境最佳化好這部分運算,變成一個必須且基礎的事情。

方法1 - 用C實現

一般來講,CPU適合整數計算、位運算這型別的操作的(除了一些特殊架構的CPU設計),因此上面的計算公式,如果能對等的轉換位一們由整數運算和位運算混合的演算法設計,就會加速非常多。

思路很簡單,就是把小數透過左移位和乘法轉換為儘可能大的32位以內大數,然後透過右移位除回去。

以計算R的過程為例:

R = (Y - 16) * 1。164 - V * -1。596

展開後也就是:

R = Y * 1。164 - 16 * 1。164 - V * -1。596

其中需要解決效能問題的是1。164和1。596的兩個乘法處理。如上一篇裡講到,Y對於顏色的貢獻更大,UV只是顏色差分分量,相對對於計算精度要求沒那麼高。所以很多演算法也是加大了對Y部分計算精度的考慮。

我們以libyuv裡的實現為例為參考:

第一步,每一分項都左移6位,讓小數部分整數化,另外結果+0。5,確保小數運算結果四捨五入:

R = (Y * 1。164 * 64 + 16 * 1。164 * 64 - V * -1。596 * 64 + 64 / 2) / 64

第二步,提升Y分量的計算精度,將繼續將Y部分的運算左移:

Y * 1。164 * 64 = ((Y * 257) * 1。164 * 65 * 65536 / 257) / 65536

第三步,整合方程,合併裡面計算中的常數,形成簡化演算法:

R = ((Y * 257) * round(1。164 * 64 * 65536 / 257) / 65536 + round(-16 * 1。164 * 64 + 64 / 2) - V * round(-1。596 * 64)) / 64 round(1。164 * 64 * 65536 / 257) = 18897 round(-16 * 1。164 * 64 + 64 / 2) = -1160 round(-1。596 * 64) = 102 257 = 0x0101 // 選擇257的原因:8位數變為16位數,前8位後8位相同,彙編計算代價低。

因此:

R = Clamp((((Y * 0x0101 * 18897) >> 16) - 1160 - V * 102) >> 6)

因此計算速度大幅度提高。

因此擷取libyuv中的C核心程式碼實現如下:

#define LOAD_YUV_CONSTANTS \ int ub = yuvconstants->kUVToB[0]; \ int ug = yuvconstants->kUVToG[0]; \ int vg = yuvconstants->kUVToG[1]; \ int vr = yuvconstants->kUVToR[1]; \ int yg = yuvconstants->kYToRgb[0]; \ int yb = yuvconstants->kYBiasToRgb[0]#define CALC_RGB16 \ int32_t y1 = ((uint32_t)(y32 * yg) >> 16) + yb; \ int8_t ui = u; \ int8_t vi = v; \ ui -= 0x80; \ vi -= 0x80; \ int b16 = y1 + (ui * ub); \ int g16 = y1 - (ui * ug + vi * vg); \ int r16 = y1 + (vi * vr)// C reference code that mimics the YUV assembly。// Reads 8 bit YUV and leaves result as 16 bit。static __inline void YuvPixel(uint8_t y, uint8_t u, uint8_t v, uint8_t* b, uint8_t* g, uint8_t* r, const struct YuvConstants* yuvconstants) { LOAD_YUV_CONSTANTS; uint32_t y32 = y * 0x0101; CALC_RGB16; *b = Clamp((int32_t)(b16) >> 6); *g = Clamp((int32_t)(g16) >> 6); *r = Clamp((int32_t)(r16) >> 6);}

方法2 - 用加速指令集實現

理解了上面的演算法,指令集操作就比較簡單了。使用CPU提供的指令集,可以最大限度地利用CPU SIMD能力,在一個CPU cycle裡,同時並行處理多個運算,這樣的話,可以N倍地提升計算效率。

顏色空間變換這事,也有好多門道和故事

我們舉例使用SSE2指令集時,CPU是支援128位資料運算的,這樣的話,就可以同時計算8個畫素(U,V每個畫素2 bytes,這樣一個128 bit計算裡,最多放8個畫素的平行計算)。

計算邏輯與上面相同,我們直接上程式碼:

// Read 8 UV from 444#define READYUV444 \ xmm3 = _mm_loadl_epi64((__m128i*)u_buf); \ xmm1 = _mm_loadl_epi64((__m128i*)(u_buf + offset)); \ xmm3 = _mm_unpacklo_epi8(xmm3, xmm1); \ u_buf += 8; \ xmm4 = _mm_loadl_epi64((__m128i*)y_buf); \ xmm4 = _mm_unpacklo_epi8(xmm4, xmm4); \ y_buf += 8;// Convert 8 pixels: 8 UV and 8 Y。#define YUVTORGB(yuvconstants) \ xmm3 = _mm_sub_epi8(xmm3, _mm_set1_epi8(0x80)); \ xmm4 = _mm_mulhi_epu16(xmm4, *(__m128i*)yuvconstants->kYToRgb); \ xmm4 = _mm_add_epi16(xmm4, *(__m128i*)yuvconstants->kYBiasToRgb); \ xmm0 = _mm_maddubs_epi16(*(__m128i*)yuvconstants->kUVToB, xmm3); \ xmm1 = _mm_maddubs_epi16(*(__m128i*)yuvconstants->kUVToG, xmm3); \ xmm2 = _mm_maddubs_epi16(*(__m128i*)yuvconstants->kUVToR, xmm3); \ xmm0 = _mm_adds_epi16(xmm4, xmm0); \ xmm1 = _mm_subs_epi16(xmm4, xmm1); \ xmm2 = _mm_adds_epi16(xmm4, xmm2); \ xmm0 = _mm_srai_epi16(xmm0, 6); \ xmm1 = _mm_srai_epi16(xmm1, 6); \ xmm2 = _mm_srai_epi16(xmm2, 6); \ xmm0 = _mm_packus_epi16(xmm0, xmm0); \ xmm1 = _mm_packus_epi16(xmm1, xmm1); \ xmm2 = _mm_packus_epi16(xmm2, xmm2);

演算法邏輯與上面C的邏輯是完全一致的。這樣可以實現至少8倍的相對C程式碼的效能提升(事實上會更多)。

其他加速指令集,如NEON、MMI、MSA等原理基本一致,都是在各自框架下,進行SIMD運算。

方法3 - 使用GPGPU的方式實現

使用GPU進行顏色空間轉化是個非常自然的想法,nVidia等顯示卡廠商對這部分的實現也都是作為很經典的示例提供給開發者的。好處不言而喻,在影片播放的場景下,CPU適合對前面的Entropy Coding, IDCT, MC等模組進行前處理,解碼為YUV資料後,上傳到兩張GPU texture上,一張放Y(一般是一張Luma texture),另一張放UV(一般是一張Luma-Alpha texture),剩下的事情就交給取樣器了。

顏色空間變換這事,也有好多門道和故事

顏色空間變換這事,也有好多門道和故事

對不熟悉GPU shader運算的同學們介紹兩句背景,GPU是一個有大量處理單元的運算架構。它有兩個最大的特點,一個是運算單元特別多,可以並行處理很多工,特別擅長大量同樣邏輯的資料運算邏輯;另一個特點是相比CPU架構來講,浮點計算效率更高,而整形計算的效率則更低。那這樣就太適合進行顏色空間變換這種事情了。

顏色空間變換這事,也有好多門道和故事

演算法很簡單,向目標texture畫兩個三角形,拼為一個長方形(因為有一條公共邊,所以一共四個頂點就可以完成了),柵格化後,每個畫素取樣對應位置的YUV值,使用轉換矩陣進行處理,輸出到對應texture即可。一般在這步之後,可以直接顯示在顯示器上或是進入到下一步效果處理流程裡。

我們從GPUImage裡,把相應Fragment Shader貼出來,很容易理解。

NSString *const kGPUImageYUVVideoRangeConversionForLAFragmentShaderString = SHADER_STRING( varying highp vec2 textureCoordinate; uniform sampler2D luminanceTexture; uniform sampler2D chrominanceTexture; uniform mediump mat3 colorConversionMatrix; void main() { mediump vec3 yuv; lowp vec3 rgb; yuv。x = texture2D(luminanceTexture, textureCoordinate)。r - (16。0/255。0); yuv。yz = texture2D(chrominanceTexture, textureCoordinate)。ra - vec2(0。5, 0。5); rgb = colorConversionMatrix * yuv; gl_FragColor = vec4(rgb, 1); }

其中colorConversionMatrix是轉換矩陣,對BT。601來講,

// BT。601, which is the standard for SDTV。GLfloat kColorConversion601Default[] = { 1。164, 1。164, 1。164, 0。0, -0。392, 2。017, 1。596, -0。813, 0。0,};

與上面CPU的演算法是一致的。

Let’s meet 韓博

回到2006年,剛進入Thomson實習,那時候我的mentor朱立華先生,第一個研究專案就是GPGPU的影片應用,當時的專案代號叫GAMA,全稱是GPU Accelerated Multimedia Applications。那會兒實在有點早,還只有上面大家看到的OpenGL shader或是DirectX shader框架可以使用。CUDA這樣的更加通用化的應用都是在2007年以後才被nVidia推出,然後逐漸開始一些實驗性的應用。

比我早一些進團隊的一位北大的博士,名叫韓博,我開始還以為是大家對他尊稱韓博,是韓博士的意思。但後來才知道,這是他的真名。就是下面這位:

顏色空間變換這事,也有好多門道和故事

當時得知他的工作就是GPGPU運算下的影片解碼,我當時想那應該還好,沒啥,應該就是把上面的顏色空間變換最佳化的比較好吧,然後可以在上面做一些顏色特效(變色啊,加強銳度啊什麼的,GPGPU比較擅長的畫素級別運算),如下面這個流程。

顏色空間變換這事,也有好多門道和故事

然後大家告訴我不是的,他的主要工作是在對原始碼流Entropy解碼後,獲得預測模式、執行向量和殘差這些資訊之後,就直接丟到GPU上去運算了,MC,IDCT等流程全部放在GPU上做完,顏色空間變化只是最後極小極小的一個操作。

顏色空間變換這事,也有好多門道和故事

說實話,當時第一時間腦子是嗡嗡的。在大多人說要think outside the box的時候,還是太簡單了,有些人壓根不關心box在哪,這種東西有可能就叫天份吧。

這個工作的結果是在一臺2006年極其普通的工作站上,可以並行解碼播放10路1080p的10Mbps@24fps的MPEG-2影片,並且同時支援在每一路顯示上繼續進行很多影片特效處理,CPU利用率還不足30%。在當時那個年代的演算法框架下,真的是太漂亮的結果了。

後來跟著這個結果,又陸續開發出來MPEG-4 ASP的解碼器,DV標準系列的解碼器,H。264的低CPU消耗解碼器,和一部分標準的編碼器,無一不大幅度地得到了效能的提升,並且被商用到很多場景中,很多周邊的技術還變向解決了一些極速非對稱解碼的問題。

當時這份工作被ACM Graphics Hardware 2006上,是SIGGRAPH的Workshop,行業內的頂會,論文名叫Efficient Video Decoding on GPUs by Point Based Rendering[1],有興趣的朋友可以自取。

寫在最後

再後面幾年,隨著nVidia CUDA計算框架問世後,GPGPU應用程式設計越來越容易了,逐漸被商用在如影片特效、如大資料探勘與計算、機器學習、量化交易等等場景。再往後,Deep Learning橫空出世,TensorFlow, PyTorch等AI技術進入到超速發展階段,顯示卡價格也被搶著挖礦、或訓練模型,奇貨可居。如果沒有基於CUDA的GPGPU運算的加持,根本沒有想象有那麼多新的AI模型被訓練出來,有那麼多新的AI演算法一下子都冒出來。

顏色空間變換這事,也有好多門道和故事

DL應用隨著各框架把GPGPU運算能力作為一種基礎能力封裝在其中,變成一種唾手可得的工具,相當大一部分的AI研發人員的核心技術,變成了引數選擇或引數調教。如何寫出高質量的加速程式,如果繼續最佳化底層演算法變成了少部分關心的事情,技術能力自然分出伯仲。逐漸從底子上更加理解和挖掘潛能才會形成新的突破,也與大家共勉。

另外從創新力的層面,永遠都是有大量空間可以想的,只是是否投入了足夠的精力去做,哪怕只是像顏色空間變換這種極成熟、無聊的事情也會有新東西不斷出現。也許在堆疊程式碼、刷題的時間,將工作中遇到的問題花一些深入的想一下,就會有非常多的新視角和收穫。類似這樣的案例還真的不少,後面有機會隨著繼續往下梳理的過程,儘可能逐漸寫出來,分享給大家。

References

[1]

Efficient Video Decoding on GPUs by Point Based Rendering:

https://www。icst。pku。edu。cn/Graphic

Top