問題說明
http://bbs.chinaffmpeg.com/a.html
遇到一個用canvas對圖片進行hue-rotate 90和用ffmpeg fiter處理hue顏色對不上的問題。中間看到很多東西,分析過程簡單記錄一下。
原始視頻第一幀:
js canvas ctx.filter = 'hue-rotate(90deg)'
ffmpeg: ffmpeg -y -i http://bbs.chinaffmpeg.com/b.mp4 -filter_complex "hue=h=90" -vframes 1 ffmpeg.jpg
分析過程和中間遇到的問題
初步懷疑是canvas使用HSL,ffmpeg使用HSV,簡單的colorspace差異導致的問題,對比了一下HSV和HSL的顏色表,研究了一下覺得兩個都不是直接簡單的使用了HSL或HSV色彩空間,而是HCL。
HSL and HSV wiki中說明了HSL和HSV的區別,hue都是指色相,相對于RGB2HSL和RGB2HSV有相同的轉換公示,s是飽和度,L和V是定義上和含義上區別的,之前在color space的學習中說過這個問題。兩個顏色空間的S和LV由RGB計算的表達公式不一樣。photoshop的拾色器使用HSV,查看花的色相Hue大概在320-350之間。至于為什么ps picker選擇HSB空間,大致是因為HSB的表達能力更強、更符合人對于拾色器的習慣,請看知乎的一個討論。對照wiki,正向旋轉90度可以看出來大概的色彩。
由于canvas不知道怎么實現的,從上面的wiki中發現問題圖片中間的花紅色(320-340 degree之間),調整90度hue(50-70),應該大致是黃色,跟canvas和ffmpeg的結果都對不上,相差有些大,到這里就覺得有些奇怪了。
先試了一下使用PS調整Hue,ps調整hue的功能跟picker不一致,我猜測使用的是HSL,因為L調整的時候是從黑到白的,而不是由最淺到最深,有的人說是ps這里的調整L是帶著S一起調節的,還有一個stack overflow問題,也有直接說是用的HSL空間,我個人更傾向于HSL。調整90度以后得到的圖為:
還有一個很常用的圖像處理開源項目IM(ImageMagick后面簡稱IM)有個 modulate接口默認使用HSL顏色空間,很慶幸文檔中恰好給了一個紅花的示例圖片,雖然并不是一樣的紅色。IM支持自己選擇顏色空間,比如HSL、HSV等。上面的modulate命令默認在HSL空間中hue調整90度,得到的結果是
convert ffmpeg-origin.png -modulate 100,100,150 ffmpeg-hsl-150.png
使用HSB空間調整90度Hue:
convert ffmpeg-origin.png -define modulate:colorspace=HSB -modulate 100,100,150 ffmpeg-hsv-150.png
HSB和HSL對于hue的定義是一樣的,看wiki中RGB->Hue的計算公式也一致,紅色的花朵變成了黃色,跟顏色空間模型和PS基本都能對應上了。不過還是對不上ffmpeg和canvas。
HCL顏色空間
在wiki中有一章Disadvantages,這里大致是說最早HSL或者HSV的提出是基于RGB轉換過來的,計算起來方便高效,也就是說從RGB cube的color model中硬生生算出來一個色相、飽和度、亮度(明度),計算快速,符合當時硬件的能力,但是實際上HSL和HSV都不太符合真正的人眼對于色相飽和度亮度的看法。由于基于RGB變形,給了一個HSL的值還需要知道對應的RGB空間,比如sRGB、bt601等,甚至gamma值,這就很不方便了。而且在HSL和HSV中的亮度都不太符合人眼所認為的亮度。
另外,這倆空間都有些毛病,尤其是HSV的V和HSL的S,比如在HSV中,純藍色和純白色有相同的value,在人眼看來純藍色明顯有著更高的亮度,HSL中接近白色和純綠色有相同的S,而人眼看來純綠色明顯飽和度要高。另外,在做色相調整的時候,還會影響到人眼中認為的SL/V。既然這么多問題,有些專家就說那就拋棄HSB HSL好了,推薦用其他的球坐標系Lab和Luv好了。
Luv和Lab都是后來在1976年提出的,都是直接基于XYZ的,不基于RGB spaces,這樣就提供了視覺感知的一致性,而且兩個都有理論基礎,就是人眼的拮抗原理。像之前在color space的講解中說的,Luv和Lab都是球坐標系,L都是希望是能表示人眼認為的不變的亮度,uv和ab都是指顏色兩個方向上的“差異”,uv或ab應該都不是代表什么單詞的縮寫。更加類似于視頻處理的YUV中uv,這里借用知乎一篇回答jpg反復壓縮變綠的圖片,按照XYZ計算U的公式得到的結果,u更偏向于藍色的程度,v表示紅色的程度,所以也可以認為u是Cb分量,v是Cr分量。
Luv極坐標表示就是LCHuv,這里L不變,將uv看作向量,兩個向量所表示的顏色的模為Chroma,夾角為Hue,用sRGB表示出來的色域圖如下:
對應的還有LCHab,基本原理是一樣的。ImageMagick支持很多種colorspaces,恰好其中包括LCHuv和LCHab。使用LCHuv得到的結果:
這里我們看到LCHuv得到的結果和ffmpeg基本一致,但是還是不同。這里后面看源碼ffmpeg使用的就是LCHuv。LCHab的結果不同,更接近canvas得到的結果。
FFMPEG\IM\Canvas 實現
看看源碼實現吧。ffmpeg的源碼可以直接下到,我看的3.24;canvas的firrefox和chrome都是開源的,這里我看的是chrome源碼版本64.0.3253.1;ImageMagick源碼我看的是7.0.7-8
FFMPEG
FFmpeg中Hue調整代碼在libavfilter/vf_hue.c中,基本算法過程是:
1, compute_sin_and_cos (line:101)
根據需要調整的HueContext計算Hue的sin cos,對于飽和度的調整根Hue一起,乘在sin和cos上
2,create_chrominance_lut (line:122)
根據HueContext和計算出來sin cos計算出來一個顏色查找表hue_lut,這里ffmpeg為了速度并不是對每個pixel做Hue調整,而是對uv所有可能出現的值u[0-255]v[0-255]計算出來目標值。這里consider U and V as the components of a 2D vector then its angle is the hue and the norm is the saturation,這樣就是一個初中幾何問題了。
這里對照一下上面那個知乎上摳出來的uv圖就容易理解了,從原點隨便一個vector,Saturation逐漸增大,Hue保持不變;確定半徑下旋轉一個vector,是Saturation保持不變,Hue在逐漸調整。
uv旋轉以后的新坐標是:
new_u = cos * u - sin * v;
new_v = sin * u + cos * v;
3, 對AVFrame的成對的uv直接apply_lut(line:378)
4,對于亮度直接是y[0-255]計算出一個lut,然后對y pixels apply_lut
很高效的算法,但ffmpeg的做法實際是有些問題的,只是強把yuv的uv作為Hue調整的對象,沒有考慮color space和transfer,不過其實在Hue調整處理中,這些影響因素可能沒那么敏感了吧,對比IM的結果,ffmpeg得到的結果還有些跑偏。
ImageMagick
IM中調整Hue的代碼在enhance.c中,line:3092
static inline void ModulateLCHuv(const double percent_luma,
const double percent_chroma,const double percent_hue,double *red,
double *green,double *blue)
{
double
hue,
luma,
chroma;
/*
Increase or decrease color luma, chroma, or hue.
*/
ConvertRGBToLCHuv(*red,*green,*blue,&luma,&chroma,&hue);
luma*=0.01*percent_luma;
chroma*=0.01*percent_chroma;
hue+=fmod((percent_hue-100.0),200.0)/200.0;
ConvertLCHuvToRGB(luma,chroma,hue,red,green,blue);
}
IM的算法還是比較標準的,對每個RGB pixel進行處理(效率不高,但這不是重點),ConvertRGBToLCHuv在MagickCore/gem.c line:1375,先將RGB->XYZ,然后ConvertXYZToLuv,跟wiki公式一致,另外可以參考另外一篇關于HSV RGB等相互轉換的公式blog,這里使用圖片常見的sRGB,得到LCHuv以后Hue執行:
hue+=fmod((percent_hue-100.0),200.0)/200.0;
IM的Hue調整是百分比的方式
hue_angle = ( modulate_arg - 100 ) * 180/100
modulate_arg = ( hue_angle * 100/180 ) + 100
最后轉換會RGB,perfect result!
Canvas hue-rotate
Canvas的執行算法如上參考Chromium源碼render_surface_filters.cc line: 176,或者另外一個地方FEColorMatrix.cpp。不過這里的轉換公式著實讓人懵逼:
看一下計算S和Gray的GetSaturateMatrix和GetGrayscaleMatrix好像明白點什么,matrix的第一列就是RGB2XYZ的Y:
Y=0.2126729*r+0.7151522*g+0.0721750*b;
另外參考一本書InkScape中對于Saturation 的說明能跟canvas的matrix對應上,另外一本書Colour Reproduction in Electronic Imaging Systems中14.8.1小節好像也有些關系,而且Canvas變換Hue和Saturation的矩陣根SVG源碼中是一樣的,還有一個什么OpenPalace也都能對上,還有一個人統計了一堆css應該使用的color轉換js……
雖然找到了很多一致的地方,但是大家好像都是抄的css源碼,并沒有什么“理論”的根據。firefox line: 423源碼寫的還算親民一些,至少知道了那一堆數字都是怎么來的!
最終還是stack over flow的討論找到了答案。另一個stackOverFlow問題中Michael Mullany的回答,css中的hue-rotate實現只是為了效率的線性近似,原始的HSL或HSV的計算非線性很復雜,css做了一個線性近似,對于不是很純色的結果還算比較接近HSL:
但是對于純色,CSS filter hue-rotate得到的結果在0-180度可以說是很爛,在180-360還算可以。
如果想自己對比一下css結果和HSL),Mullany給了一個對比css。
After all,css最終使用的近似方程是這樣子,想看證明的可以看一下MultiplyByZer0的回答:
References
- HSL && HSV wiki
- Image magick document
- HSL && HSV color space disadvantages
- 知乎關于ps為什么選擇HSB作為拾色器
- LUV color space
- Lab color space
- jsfiddle net: A css online test
- color selector online tool
- Photoshop HSL HSP understanding
- A wikipedia pdf doc: HSL && HSV color space, and photoshop principle
- stack over flow, photoshop hue adjust
- 知乎討論 為何jpg反復壓縮質量奇差且發綠
- chromium source code: render_surface_filter.cc
- 62.0.3178.1 chromium source code: render_surface_filter.cc
- HCL color space
- StackOverFlow: Why doesn't hue rotation by +180deg and -180deg yield the original color?
- StackOverFlow: How to transform black into any given color using only CSS filters
- Comparison of Hue Rotations: Red (S 50%, L 75%)
- w3.org hue rotate
- An interesting messages below 17 question