问题说明
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