关于色相Hue调整的问题

问题说明

http://bbs.chinaffmpeg.com/a.html
遇到一个用canvas对图片进行hue-rotate 90和用ffmpeg fiter处理hue颜色对不上的问题。中间看到很多东西,分析过程简单记录一下。
原始视频第一帧:

origin-video.png

js canvas ctx.filter = 'hue-rotate(90deg)'
canvas.png

ffmpeg: ffmpeg -y -i http://bbs.chinaffmpeg.com/b.mp4 -filter_complex "hue=h=90" -vframes 1 ffmpeg.jpg
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度可以看出来大概的色彩。

394px-HSL_color_solid_cylinder_alpha_lowgamma.png

394px-HSV_color_solid_cylinder_alpha_lowgamma.png

photoshop-color-wheel.png

600px-HSV-RGB-comparison.svg.png

HSL&HSV&RGB.png

由于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度以后得到的图为:
photoshop-90.jpg

还有一个很常用的图像处理开源项目IM(ImageMagick后面简称IM)有个 modulate接口默认使用HSL颜色空间,很庆幸文档中恰好给了一个红花的示例图片,虽然并不是一样的红色。IM支持自己选择颜色空间,比如HSL、HSV等。上面的modulate命令默认在HSL空间中hue调整90度,得到的结果是

convert ffmpeg-origin.png -modulate 100,100,150 ffmpeg-hsl-150.png

ffmpeg-hsl-150.png

使用HSB空间调整90度Hue:

convert ffmpeg-origin.png -define modulate:colorspace=HSB -modulate 100,100,150 ffmpeg-hsv-150.png

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好了,推荐用其他的球坐标系LabLuv好了。

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分量。

yuv-uv.png

Luv极坐标表示就是LCHuv,这里L不变,将uv看作向量,两个向量所表示的颜色的模为Chroma,夹角为Hue,用sRGB表示出来的色域图如下:
SRGB_gamut_within_CIELCHuv.png

对应的还有LCHab,基本原理是一样的。ImageMagick支持很多种colorspaces,恰好其中包括LCHuv和LCHab。使用LCHuv得到的结果:
IM-LCHuv-150.png

IM-LCHab-150.png

这里我们看到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。不过这里的转换公式着实让人懵逼:

chrome-hue.jpg

看一下计算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源码写的还算亲民一些,至少知道了那一堆数字都是怎么来的!

firefox-hue.jpg

最终还是stack over flow的讨论找到了答案。另一个stackOverFlow问题中Michael Mullany的回答,css中的hue-rotate实现只是为了效率的线性近似,原始的HSL或HSV的计算非线性很复杂,css做了一个线性近似,对于不是很纯色的结果还算比较接近HSL:
less-pure-color-hue-HSLvsCSS.jpg

但是对于纯色,CSS filter hue-rotate得到的结果在0-180度可以说是很烂,在180-360还算可以。
Pure-color-hue-HSLvsCSS.jpg

如果想自己对比一下css结果和HSL),Mullany给了一个对比css

After all,css最终使用的近似方程是这样子,想看证明的可以看一下MultiplyByZer0的回答

css-hue-rotate-equation.jpg

References

  1. HSL && HSV wiki
  2. Image magick document
  3. HSL && HSV color space disadvantages
  4. 知乎关于ps为什么选择HSB作为拾色器
  5. LUV color space
  6. Lab color space
  7. jsfiddle net: A css online test
  8. color selector online tool
  9. Photoshop HSL HSP understanding
  10. A wikipedia pdf doc: HSL && HSV color space, and photoshop principle
  11. stack over flow, photoshop hue adjust
  12. 知乎讨论 为何jpg反复压缩质量奇差且发绿
  13. chromium source code: render_surface_filter.cc
  14. 62.0.3178.1 chromium source code: render_surface_filter.cc
  15. HCL color space
  16. StackOverFlow: Why doesn't hue rotation by +180deg and -180deg yield the original color?
  17. StackOverFlow: How to transform black into any given color using only CSS filters
  18. Comparison of Hue Rotations: Red (S 50%, L 75%)
  19. w3.org hue rotate
  20. An interesting messages below 17 question
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容