寫在前面
我相信幾乎所有做圖像處理方面的人都聽過伽馬校正(Gamma Correction)這一個名詞,但真正明白它是什麼、為什麼要有它、以及怎麼用它的人其實不多。我也不例外。
最初我查過一些資料,但很多文章的說法都不一樣,有些很晦澀難懂。直到我最近在看《Real Time Rendering,3rd Edition》這本書的時候,才開始慢慢對它有所理解。
本人才疏學淺,寫的這篇文章很可能成為網上另一篇誤導你的「伽馬傳說」,但我儘可能把目前了解的資料和可能存在的寫在前面
我相信幾乎所有做圖像處理方面的人都聽過伽馬校正(Gamma Correction)這一個名詞,但真正明白它是什麼、為什麼要有它、以及怎麼用它的人其實不多。我也不例外。
最初我查過一些資料,但很多文章的說法都不一樣,有些很晦澀難懂。直到我最近在看《Real Time Rendering,3rd Edition》這本書的時候,才開始慢慢對它有所理解。
本人才疏學淺,寫的這篇文章很可能成為網上另一篇誤導你的「伽馬傳說」,但我儘可能把目前了解的資料和可能存在的疏漏寫在這裡。如有錯誤,還望指出。
伽馬的傳說
關於這個方面,龔大寫過一篇文章,但我認為其中的說法有不準確的地方。
從我找到的資料來看,人們使用伽馬曲線來進行顯示最開始是源於一個巧合:在早期,CRT幾乎是唯一的顯示設備。但CRR有個特性,它的輸入電壓和顯示出來的亮度關係不是線性的,而是一個類似冪律(pow-law)曲線的關係,而這個關係又恰好跟人眼對光的敏感度是相反的。這個巧合意味著,雖然CRT顯示關係是非線性的,但對人類來說感知上很可能是一致的。
我來詳細地解釋一下這個事件:在很久很久以前(其實沒多久),全世界都在使用一種叫CRT的顯示設備。這類設備的顯示機制是,使用一個電壓轟擊它屏幕上的一種圖層,這個圖層就可以發亮,我們就可以看到圖像了。但是,人們發現,咦,如果把電壓調高兩倍,屏幕亮度並沒有提高兩倍啊!典型的CRT顯示器的伽馬曲線大致是一個伽馬值為2.5的冪律曲線。顯示器的這類伽馬也稱為display gamma。由於這個問題的存在,那麼圖像捕捉設備就需要進行一個伽馬校正,它們使用的伽馬叫做encoding gamma。所以,一個完整的圖像系統需要2個伽馬值:
– encoding gamma:它描述了encoding transfer function,即圖像設備捕捉到的場景亮度值(scene radiance values)和編碼的像素值(encoded pixel values)之間的關係。
– display gamma:它描述了display transfer function,即編碼的像素值和顯示的亮度(displayed radiance)之間的關係。
如下圖所示:
而encoding gamma和display gamma的乘積就是真箇圖像系統的end-to-end gamma。如果這個乘積是1,那麼顯示出來的亮度就是和捕捉到的真實場景的亮度是成比例的。
上面的情景是對於捕捉的相片。那麼對於我們渲染的圖像來說,我們需要的是一個encoding gamma。如果我們沒有用一個encoding gamma對shader的輸出進行校正,而是直接顯示在屏幕上,那麼由於display gamma的存在就會使畫面失真。
至此為止,就是龔大所說的伽馬傳說。由此,龔大認為全部的問題都出在CRT問題上,跟人眼沒有任何關係。
但是,在《Real-time Rendering》一書中,指出了這種乘積為1的end-to-end gamma的問題。看起來,乘積為1的話,可以讓顯示器精確重現原始場景的視覺條件。但是,由於原始場景的觀察條件和顯示的版本之間存在兩個差異:1)首先是,我們能夠顯示的亮度值其實和真實場景的亮度值差了好幾個數量級,說通俗點,就是顯示器的精度根本達不到真實場景的顏色精度(大自然的顏色種類幾乎是無窮多的,而如果使用8-bit的編碼,我們只能顯示256^3種顏色);2)這是一種稱為surround effect的現象。在真實的場景中,原始的場景填充了填充了觀察者的所有視野,而顯示的亮度往往只局限在一個被周圍環境包圍的屏幕上。這兩個差別使得感知對比度相較於原始場景明顯下降了。也就是我們一開始說的,對光的靈敏度對不同亮度是不一樣的。如下圖所示(來源:Youtube: Color is Broken):
為了中和這種現象,所以我們需要乘積不是1的end-to-end gamma,來保證顯示的亮度結果在感知上和原始場景是一致的。根據《Real-time Rendering》一書中,推薦的值在電影院這種漆黑的環境中為1.5,在明亮的室內這個值為1.125。
個人電腦使用的一個標準叫sRGB,它使用的encoding gamma大約是0.45(也就是1/2.2)。這個值就是為了配合display gamma為2.5的設備工作的。這樣,end-to-end gamma就是0.45 * 2.5 = 1.125了。
這意味著,雖然CRT的display gamma是2.5,但我們使用的encoding gamma應該是1.125/2.5 = 1/2.2,而不是1/2.5。這樣才能保證end-to-end gamma為1.125,從而在視覺上進行了補償。
雖然現在CRT設備很少見了,但為了保證這種感知一致性(這是它一直沿用至今的很重要的一點),同時也為了對已有圖像的兼容性(之前很多圖像使用了encoding gamma對圖像進行了編碼),所以仍在使用這種伽馬編碼。而且,現在的LCD雖然有不同的響應曲線(即display gamma不是2.5),但是在硬體上做了調整來提供兼容性。
重要:上面的說法主要來源於Real-time Rendering》一書。
來自其他領域的伽馬傳說
今天很幸運聽了知乎上韓世麟童鞋的講解。在聽了他的講座後,我聽到了另一個版本的伽馬傳說。和上面的討論不同,他認為伽馬的來源完全是由於人眼的特性造成的。對伽馬的理解和職業很有關係,長期從事攝影、視覺領域相關的工作的人可能更有發言權。我覺得這個版本更加可信。感興趣的同學可以直接去知乎上領略一下。
我在這裡來大致講一下他的理解。
事情的起因可以從在真實環境中拍攝一張圖片說起。攝像機的原理可以簡化為,把進入到鏡頭內的光線亮度編碼成圖像(例如一張JEPG)中的像素。這樣很簡單啦,如果採集到的亮度是0,像素就是0,亮度是1,像素就是1,亮度是0.5,像素就是0.5。這裡,就是這裡,出現了一點問題!如果我們假設只用8位空間來存儲像素的話,以為著0-1可以表示256種顏色,沒錯吧?但是,人眼有的特性,就是對光的靈敏度在不同亮度是不一樣的。還是這張圖Youtube: Color is Broken:
這張圖說明一件事情,即亮度上的線性變化在人眼看來是非均勻的,再通俗點,從0亮度變到0.01亮度,人眼是可以察覺到的,但從0.99變到1.0,人眼可能就根本差別不出來,覺得它們是一個顏色。也就是說,人眼對暗部的變化更加敏感,而對亮部變化其實不是很敏感。也就是說,人眼認為的中灰其實不在亮度為0.5的地方,而是在大約亮度為0.18的地方(18度灰)。強烈建議去看一下Youtube上的視頻,Color is Broken。
那麼,這和拍照有什麼關係呢?如果在8位圖中,我們仍然用0.5亮度編碼成0.5的像素,那麼暗部和亮部區域我們都使用了128種顏色來表示,但實際上,亮部區域使用這麼多種其實相對於暗部來說是種存儲浪費。不浪費的做法是,我們應該把人眼認為的中灰亮度放在像素值為0.5的地方,也就是說,0.18亮度應該編碼成0.5像素值。這樣存儲空間就可以充分利用起來了。所以,攝影設備如果使用了8位空間存儲照片的話,會用大約為0.45的encoding gamma來對輸入的亮度編碼,得到一張圖像。0.45這個值完全是由於人眼的特性測量得到的。
那麼顯示的時候到了。有了一張圖片,顯示的時候我們還是要把它還原成原來的亮度值進行顯示。畢竟,0.454隻是為了充分利用存儲空間而已。我們假設一下,當年CRT設備的輸入電壓和產生亮度之間完全是線性關係,我們還是要進行伽馬校正的。這是為了把用0.45伽馬編碼後的圖像正確重現在屏幕上。巧合的是,當年人們發現CRT顯示器竟然符合冪律曲線!人們想,「天哪,太棒了,我們不需要做任何調整就可以讓拍攝的圖像在電腦上看起來和原來的一樣了」。這就是我們一直說的「那個巧合」。當年,CRT的display gamma是2.5,這樣導致最後的end-to-end gamma大約是0.45 * 2.5 = 1.125,其實是非1的。
直到後來,微軟聯合愛普生、惠普提供了sRGB標準,推薦顯示器中display gamma值為2.2。這樣,配合0.45的encoding gamma就可以保證end-to-end gamma為1了。當然,上一節提到的兩個觀察差異,有些時候我們其實更希望end-to-end gamma非1的結果,例如,在電影院這種暗沉沉的環境中,end-to-end gamma為1.5我們人看起來更爽、更舒服,而在明亮的辦公室這種環境中1.125的end-to-end gamma值更舒服、更漂亮。所以,我們可以根據環境的不同,去選擇使用什麼樣的display gamma。
總之,伽馬校正一直沿用至今說到底是人眼特性決定的。你會說,伽馬這麼麻煩,什麼時候可以捨棄它呢?按韓世麟童鞋的說法,如果有一天我們對圖像的存儲空間能夠大大提升,通用的格式不再是8位的時候,例如是32位的時候,伽馬就沒有用了。因為,我們不需要為了提高精度而把18度灰編碼成0.5像素,因為我們有足夠多的顏色空間可以利用,不需要考慮人眼的特性。
好啦,上面就是來自攝影、建築領域的看法和理解。希望這兩種看法可以讓大家更深地理解伽馬校正的存在意義。
這和渲染有什麼關係
其實,對伽馬傳說的理解就算有偏差,也不會影響我們對伽馬校正的使用。我們只要知道,根據sRGB標準,大部分顯示器使用了2.2的display gamma來顯示圖像。
前面提到了,和渲染相關的是encoding gamma。我們知道了,顯示器在顯示的時候,會用display gamma把顯示的像素進行display transfer之後再轉換成顯示的亮度值。所以,我們要在這之前,像圖像捕捉設備那樣,對圖像先進行一個encoding transfer,與此相關的就是encoding gamma了。
而不幸的是,在遊戲界長期以來都忽視了伽馬校正的問題,也造成了為什麼我們渲染出來的遊戲總是暗沉沉的,總是和真實世界不像。
回到渲染的時候。我們來看看沒有正確進行伽馬校正到底會有什麼問題。
以下實驗均在Unity中進行。
光照
我們來看一個最簡單的場景:在場景中放置一個球,使用默認的Diffuse材質,打一個平行光:
看起來很對是嗎?但實際上,這和我們在真實場景中看到的是不一樣的。在真實的場景中,如果我們把一個球放在平行光下,它是長這個樣子的:
假設球上有一點B,它的法線和光線方向成60°,還有一點A,它的法線和光線方向成90°。那麼,在shader中計算diffuse的時候,我們會得出B的輸出是(0.5, 0.5, 0.5),A的輸出的(1.0, 1.0, 1.0)。
在第一張圖中,我們沒有進行伽馬校正。因此,在把像素值轉換到屏幕亮度時並不是線性關係,也就是說B點的亮度其實並不是A亮度的一半,在Mac顯示器上,這個亮度只有A亮度的1/1.8唄,約為四分之一。在第二章圖中,我們進行了伽馬校正,此時的亮度才是真正跟像素值成正比的。
混合
混合其實是非常容易受伽馬的影響。我們還是在Unity裡創建一個場景,使用下面的shader渲染三個Quad:
上面的shader其實很簡單,就是在Quad上畫了個邊緣模糊的圓,然後使用了混合模式來會屏幕進行混合。我們在場景中畫三個這樣不同顏色的圓,三種顏色分別是(0.78, 0, 1),(1, 0.78, 0),(0, 1, 0.78):
看出問題了嗎?在不同顏色的交接處出現了不正常的漸變。例如,從綠色(0, 1, 0.78)到紅色(0.78, 0, 1)的漸變中,竟然出現了藍色。
正確的顯示結果應該是:
第一張圖的問題出在,在混合後進行輸出時,顯示器進行了display transfer,導致接縫處顏色變暗。
非線性輸入
shader中非線性的輸入最有可能的來源就是紋理了。
為了直接顯示時可以正確顯示,大多數圖像文件都進行了提前的校正,即已經使用了一個encoding gamma對像素值編碼。但這意味著它們是非線性的,如果在shader中直接使用會造成在非線性空間的計算,使得結果和真實世界的結果不一致。
Mipmaps
在計算紋理的Mipmap時也需要注意。如果紋理存儲在非線性空間中,那麼在計算mipmap時就會在非線性空間裡計算。由於mipmap的計算是種線性計算——即降採樣的過程,需要對某個方形區域內的像素去平均值,這樣就會得到錯誤的結果。正確的做法是,把非線性的紋理轉換到線性空間後再計算Mipmap。
擴展
由於未進行伽馬校正而造成的混合問題其實非常常見,不僅僅是在渲染中才遇到的。
Youtube上有一個很有意思的視頻,非常建議大家看一下。裡面講的就是,由於在混合前未對非線性紋理進行轉換,造成了混合純色時,在純色邊界處出現了黑邊。用數學公式來闡述這一現象就是:
我們可以把xˆ(1/gamma)和yˆ(1/gamma)看成是兩個非線性空間的紋理,如果直接對它們進行混合(如取平均值),得到的結果實際要暗於在線性空間下取平均值再伽馬校正的結果。
所以,在處理非線性紋理時一定要格外小心。
進行伽馬校正
我們的目標是:保證所有的輸入都轉換到線性空間,並在線性空間下做各種光照計算,最後的輸出在通過一個encoding gamma進行伽馬校正後進行顯示。
在Unity中,有一個專門的設置是為伽馬校正服務的,具體可以參見官方文檔(Linear Lighting)。
簡單來說就是靠Edit -> Project Settings -> Player -> Other Settings中的設置:
它有兩個選項:一個是Gamma Space,一個Linear Space。
– 當選擇Gamma Space時,實際上就是「放任模式」,不會對shader的輸入進行任何處理,即使輸入可能是非線性的;也不會對輸出像素進行任何處理,這意味著輸出的像素會經過顯示器的display gamma轉換後得到非預期的亮度,通常表現為整個場景會比較昏暗。
● 當選擇Linear Space時,Unity會背地裡把輸入紋理設置為sRGB模式,這種模式下硬體在對紋理進行採樣時會自動將其轉換到線性空間中;並且,也會設置一個sRGB格式的buffer,此時GPU會在shader寫入color buffer前自動進行伽馬校正。如果此時開啟了混合(像我們之前的那樣),在每次混合是,之前buffer中存儲的顏色值會先重新轉換回線性空間中,然後再進行混合,完成後再進行伽馬校正,最後把校正後的混合結果寫入color buffer中。這裡需要注意,Alpha通道是不會參與伽馬校正的。
sRGB模式是在近代的GPU上才有的東西。如果不支持sRGB,我們就需要自己在shader中進行伽馬校正。對非線性輸入紋理的校正通常代碼如下:
float3 diffuseCol = pow(tex2D( diffTex, texCoord ), 2.2 );
在最後輸出前,對輸出像素值的校正代碼通常長下面這樣:
fragColor.rgb = pow(fragColor.rgb, 1.0/2.2);
return fragColor;
但是,手工對輸出像素進行伽馬校正在使用混合的時候會出現問題。這是因為,校正後導致寫入color buffer的顏色是非線性的,這樣混合就發生在非線性空間中。一種解決方法時,在中間計算時不要對輸出進行伽馬校正,在最後進行一個屏幕後處理操作對最後的輸出進行伽馬校正,但很顯然這會造成性能問題。
還有一些細節問題,例如在進行屏幕後處理的時候,要小心我們目前正在處理的圖像到底是不是已經伽馬校正後的。
總之,一切工作都是為了「保證所有的輸入都轉換到線性空間,並在線性空間下做各種光照計算,最後的輸出(最最最最後的輸出)進行伽馬校正後再顯示」。
雖然Unity的這個設置非常方便,但是其支持的平臺有限,目前還不支持移動平臺。也就是說,在安卓、iOS上我們無法使用這個設置。因此,對於移動平臺,我們需要像上面給的代碼那樣,手動對非線性紋理進行轉換,並在最後輸出時再進行一次轉換。但這又會導致混合錯誤的問題。
在 Unity 中使用 Linear Space
如果我們在Edit -> Project Settings -> Player -> Other Settings中使用了Linear Space,那麼之前的光照、混合問題都可以解決(這裡的解決是說和真實場景更接近)。但在處理紋理時需要注意,所有Unity會把所有輸入紋理都設置成sRGB格式,也就說,所有紋理都會被硬體當成一個非線性紋理,使用一個display gamma(通常是2.2)進行處理後,再傳遞給shader。但有時,輸入紋理並不是非線性紋理就會發生問題。
例如,我們繪製一個亮度為127/255的紋理,傳給shader後乘以2後進行顯示:
可以看出,Gamma Space的反而更加正確。這是因為,我們的輸入紋理已經是線性了,而Unity錯誤地又進行了sRGB的轉換處理。這樣一來,右邊顯示的亮度實際是,(pow(0.5, 2.2) * 2, 1/2.2)。
為了告訴Unity,「嘿,這張紋理就是線性的,不用你再處理啦」,可以在Texture的面板中設置:
上面的「Bypass sRGB Sample」就是告訴Untiy要繞過sRGB處理,「它是啥就是啥!」。
這樣設置後,就可以得到正確採樣結果了。
寫在最後
伽馬校正一直是個眾說紛紜的故事,當然我寫的這篇也很可能會有一些錯誤,如果您能指出不勝感激。
即便關於一些細節問題說法很多,但本質是不變的。GPU Gems上的一段話可以說明伽馬校正的重要性:
● This is one reason why most (but not all) CG for film looks much better than games—a reason that has nothing to do with the polygon counts, shading, or artistic skills of game creators. (It’s also sometimes a reason why otherwise well-made film CG looks poor—because the color palettes and gammas have been mismatched by a careless compositor.)
最後,給出GPU Gems中的一段總結,以下步驟應該在遊戲開發中應用:
1. 假設大部分遊戲使用沒有校正過的顯示器,這些顯示器的display gamma可以粗略地認為是2.2。(對於更高質量要求的遊戲,可以讓你的遊戲提供一個伽馬校正表格,來讓用戶選擇合適的伽馬值。)
2. 在對非線性紋理(也就是那些在沒有校正的顯示器上看起來是正確的紋理)進行採樣時,而這些紋理又提供了光照或者顏色信息,我們需要把採樣結果使用一個伽馬值轉換到線性空間中。不要對已經在線性顏色空間中的紋理,例如一些HDR光照紋理、法線紋理、凹凸紋理(bump heights)、或者其他包含非顏色信息的紋理,進行這樣的處理。對於非線性紋理,儘量使用sRGB紋理格式。
3. 在顯示前,對最後的像素值應用一個伽馬校正(即使用1/gamma對其進行處理)。儘量使用sRGB frame-buffer extensions來進行有效自動的伽馬校正,這樣可以保證正確的混合。
所幸的是,在Unity中,上面的過程可以通過設置Edit -> Project Settings -> Player -> Other Settings->Color Space輕鬆地完成,需要注意的是對紋理的處理。但不幸的是,不支持移動平臺。
最後,一句忠告,在遊戲渲染的時候一定要考慮伽馬校正的問題,否則就很難得到非常真實的效果。
來源:伯克在線
【推薦閱讀】
Vulkan 教程--Overview
超酷炫!基於MIPS的君正T10芯360智能攝像機懸浮版雙11熱賣!
Microchip最新推出兩款PIC32 Curiosity開發板,成本低廉且功能豐富
PowerVR 工具鏈和SDK 2016 R2開發包發布了!
嘿!程式設計師們,不要光顧著寫碼。」——編程大牛們對年輕程式設計師的職業建議
Imagination中文社區
權威發布有關Imagination公司CPU,GPU以及連接IP、無線IP最新資訊,提供有關物聯網、可穿戴、通信、汽車電子、醫療電子等應用信息,每日更新大量信息,讓你緊跟技術發展,歡迎免費註冊。網址:imgtec.eetrend.com
想了解更多信息,關注後反饋給我吧!