几种gamma标准(transfer characteristic)的曲线,横轴为Y/RGB的信号大小,纵轴为亮度。
gamma=2.2的近似公式(纯幂函数关系)
srgb标准的公式(部分线性,部分幂函数关系)
signal就是指gamma compressed的Y/RGB的量化值对应的信号值(0-1),power则是指linear scale的亮度值(0-1)
sRGB、SMPTE 170M(BT.709、BT.601)、SMPTE 240M、BT.2020的gamma compress与linear scale转换公式:
代码: 全选
# sRGB SMPTE 170M SMPTE 240M BT-2020
k0 = 0.04045 0.081 0.0912 0.08145
phi = 12.92 4.5 4.0 4.5
alpha = 0.055 0.099 0.1115 0.0993
gamma = 2.4 2.22222 2.22222 2.22222
# gamma to linear
power = (signal <= k0) ? (signal / phi) : ((signal + alpha) / (1 + alpha)) ^ gamma
# linear to gamma
signal = (power <= k0 / phi) ? (power * phi) : (power ^ (1 / gamma)) * (1 + alpha) - alpha
2.gamma这个东西产生的源头是在摄像端/CG渲染端,这些源端一般就已经将得到的图像量化值进行gamma compress了。最终输出端是显示设备,而一般的显示设备也有它的gamma特性。如果要获得正确的色彩还原,那么就必须让显示端和源端的gamma特性一致(实际上中间处理过程也是一样,下面就详细说明)。
3.由于量化值本身非线性的属性(gamma compressed),而一般的处理都是以量化值为linear scale的基础考虑的。如果做的处理没有考虑画面的gamma,而这个处理从原则上是针对亮度而非纯像素值的,这个处理就会导致画面对比度、亮度的失真。要实现gamma-aware的处理,需要将其转为线性的量化值(linear scale)然后再应用一般的处理。
4.对于gamma-aware处理与linear处理结果的区别,下面给一个直观的例子。
在srgb的gamma、PC Range下,像素值0(亮度0)和像素值255(亮度1)如果在线性下取平均,得到的像素值是128(亮度0.216),前后的亮度是有明显失真的,gamma-aware的平均后得到的像素值是188(亮度0.503),这样得到的前后亮度是一致的。188/255对应的是0.5亮度的中灰——这个例子也让我们对gamma compressed的概念有了更直观的理解。
这种失真的大小在进行计算的两个Y/RGB值的差距越大时也越明显,对应于resize/convolution这类spatial处理中,在局部高对比度区域影响会更明显(edge部分),下面的参考文章中的例子基本都是基于这个原理来设计的。
也就是说,我们通常所认为的0.25+0.75=1的概念是在线性空间下成立的,但是在像gamma compressed这种非线性空间下,线性的运算规律都会失效。用2作为gamma值的近似的话就和几何中用到的勾股定理的运算法则相同:0.25^2+0.75^2=0.79^2。
当然在现在图像领域使用的transfer characteristic标准中,也就是上方的公式里,是对亮度低于k0的部分使用线性压缩,高于k0的部分使用幂函数进行压缩。
而我们正确处理非线性空间(称之为gamma-aware的处理)的最简单办法就是先将其转成线性空间(linear scale)进行线性处理(linear处理),然后再将其转回非线性空间(gamma compressed),而不考虑gamma的影响直接对非线性空间进行线性处理我们称之为gamma-ignorant的处理。
5.但我们需要知道一点,之所以有gamma compression这种技术,在最早的模拟信号时代,是因为录制设备/回放设备本身的物理特性所致,输入输出的亮度和电压之间呈现的就是这种非线性关系(学过中学物理中的电学就应该知道P=U^2/R,而这里所说的设备的光电转换部分虽然不是简单固定的电阻R,但也是类似的道理),而其实人眼对光的亮暗呈现的也是非线性关系。这些物理特性导致了gamma这种概念的产生,而不同设备的物理特性之间的差别也导致了有多种gamma标准。
而到了数字信号时代,gamma仍然存在,它的重要意义之一就是在8bit-12bit这些低精度的图像色深下,给较暗的部分分配更大的量化区间(每2个相邻值之间的亮度差距小),相应地给较亮的部分分配较少的量化区间(每2个相邻值之间的亮度差距大),而这是符合人眼的视觉特性的(对暗部的亮度变化比亮部敏感)。我们经常说视频里的暗场有什么什么问题,实际上gamma compression在数字信号时代就是为了更好地保留暗场信息而存在的,现在人们有能力做出“电压——亮度”呈线性关系的显示器,但是大部分显示器仍然在沿用gamma这一概念。
所以如果我们要将8bit gamma compress的源转换为linear scale的图像,至少需要16bit来保留,实际上最好是有32bit float的精度来保留信息,根据我在AVS里的测试,8bit gamma->16bit linear->8bit gamma的转换都不是无损的,即使是16bit的中间精度还是会有rounding error。
6.大部分滤镜的处理过程都是gamma-ignorant的,只有少数如VariableBlur、ResampleHQ可以进行gamma-aware的处理(内部精度32bit float)。
dither工具中有下面的方法进行16bit下的gamma-aware处理。
先将输入的gamma compressed的图像转为16bit YUV/RGB48Y(应该根据源端是在YUV还是RGB下进行gamma compress来选择,但是通常来说gamma compress都是在RGB上进行的所以最好是用RGB48Y进行gamma-aware处理)。
通过Dither_y_gamma_to_linear将其按源端的transfer characteristic标准转为16bit linear scale。
做全程精度不低于16bit的处理,这个中间不能有降到8bit的过程(如nnedi3_resize16的upscale、GSMC、SMDegrain),根据上面第3条原因已经很明确了。
通过Dither_y_linear_to_gamma将其按显示端的transfer characteristic标准转为16bit gamma compressed。
例如对BT.709 transfer characteristic标准的输入源做gamma-aware的resize,输出给sRGB transfer characteristic标准的显示设备——这里同时也就做了一个transfer characteristic标准的转换:
CODEBOX_PLUS_CODE: [全选]
[CODEBOX_PLUS_EXPAND/CODEBOX_PLUS_COLLAPSE]
# stacked-16bit input
Dither_y_gamma_to_linear(curve="709")
Dither_resize16(1280, 720)
Dither_y_linear_to_gamma(curve="srgb")
# stacked-16bit output
Dither_y_gamma_to_linear(curve="709")
Dither_resize16(1280, 720)
Dither_y_linear_to_gamma(curve="srgb")
# stacked-16bit output
按照设置的方式做chroma upscaling成为YUV444,根据源的matrix转换为gamma-compressed RGB,根据源的transfer characteristic转换为linear RGB,根据设置的方式做image upscaling/downscaling,根据显示器的transfer characteristic转换为gamma-compressed RGB,dither为8bit的RGB32输出给系统/DirectX接口,最后一步加入的gamma compress是根据你自己显示设备的设置进行的。
如果要让madVR做gamma-aware的resize,那么就要勾上image upscaling/downscaling里的scale in linear light,默认的不勾则是为gamma-ignorant的resize。
从实际效果来看,推荐对image downscaling使用gamma-aware的resize,而image upscaling时用gamma-aware resize会有比较明显的aliasing和ringing,所以不建议使用。
7.这里也能看出几个问题,例如压片时我们往往不知道视频原始的制作流程是怎样的——源端是在RGB还是YUV下采用何种transfer characteristic标准进行gamma compress的?
(考虑到实际摄制/渲染中一般都是生成RGB而gamma compression在这时就引入了,所以从正确性角度考虑gamma-aware的处理一般也应该在RGB下做)
而显示端的transfer characteristic标准又是一个不确定的东西,如果压片只是压给自己的可以按照自己用的显示设备来决定使用的transfer characteristic标准,如果压片是发出去的,那么又要怎么办?
所以如果光就压片中resize这一环节而言,最好的做法就是不要resize——as is——源是什么分辨率就保留什么分辨率。而且就从信息保留的角度而言,1080p源压制的高码720p往往是不如同码率的1080p(一个是又resizer滤除所有高频信息后再给Video Encoder决定保留什么信息,另一个是又Video Encoder决定保留什么信息舍去什么信息),当然这不是这篇文章的主题所以就不继续扯了。。。
当然由于几种常见的transfer characteristic标准的曲线都很接近(srgb相差稍微大一点),在源端和显示端都不确定的情况下,可以像下面那样做transfer characteristic标准近似的gamma-aware处理,前后的transfer curve相同所以不会发生transfer characteristic标准的转换。可以这样近似也是因为,相比于gamma-ignorant处理和gamma-aware处理之间的区别,不同transfer characteristic标准之间的区别可以说是小得多了。
CODEBOX_PLUS_CODE: [全选]
[CODEBOX_PLUS_EXPAND/CODEBOX_PLUS_COLLAPSE]
# stacked-16bit input
Dither_y_gamma_to_linear(curve="709")
Dither_resize16(1280, 720)
Dither_y_linear_to_gamma(curve="709")
# stacked-16bit output
Dither_y_gamma_to_linear(curve="709")
Dither_resize16(1280, 720)
Dither_y_linear_to_gamma(curve="709")
# stacked-16bit output
1920x1080 YUV420P8的输入源,按BT.709的Matrix和Transfer Curve将其转为Linear RGB48Y,downscale到1280x720,再按BT.709的Transfer Curve输出interleaved-YUV444P10。
CODEBOX_PLUS_CODE: [全选]
[CODEBOX_PLUS_EXPAND/CODEBOX_PLUS_COLLAPSE]
# 1920x1080 TV-Range YUV420P8 input
Dither_convert_yuv_to_rgb(matrix="709", tv_range=True, lsb_in=False, output="rgb48y")
# Convert to RGB48Y with BT.709 matrix
Dither_y_gamma_to_linear(False, False, "709")
# Convert to linear RGB48Y with BT.709 transfer
Dither_resize16(1280, 720, Y=3, U=1, V=1)
# Linear resize to 1280x720
Dither_y_linear_to_gamma(False, False, "709")
# Convert to gamma compressed RGB48Y with BT.709 transfer
Dither_convert_rgb_to_yuv(SelectEvery(3, 0), SelectEvery(3, 1), SelectEvery(3, 2), matrix="709", tv_range=False, lsb=True, output="yv24")
# Convert to PC-Range stacked-YUV444P16
Down10(10, TVrange=False, stack=False)
# PC-Range interleaved-YUV444P10 output
Dither_convert_yuv_to_rgb(matrix="709", tv_range=True, lsb_in=False, output="rgb48y")
# Convert to RGB48Y with BT.709 matrix
Dither_y_gamma_to_linear(False, False, "709")
# Convert to linear RGB48Y with BT.709 transfer
Dither_resize16(1280, 720, Y=3, U=1, V=1)
# Linear resize to 1280x720
Dither_y_linear_to_gamma(False, False, "709")
# Convert to gamma compressed RGB48Y with BT.709 transfer
Dither_convert_rgb_to_yuv(SelectEvery(3, 0), SelectEvery(3, 1), SelectEvery(3, 2), matrix="709", tv_range=False, lsb=True, output="yv24")
# Convert to PC-Range stacked-YUV444P16
Down10(10, TVrange=False, stack=False)
# PC-Range interleaved-YUV444P10 output
CODEBOX_PLUS_CODE: [全选]
[CODEBOX_PLUS_EXPAND/CODEBOX_PLUS_COLLAPSE]
# 1920x1080 TV-Range YUV420P8 input
Dither_convert_yuv_to_rgb(matrix="709", tv_range=True, lsb_in=False, output="rgb48y")
# Convert to RGB48Y with BT.709 matrix
Dither_y_gamma_to_linear(False, False, "709")
# Convert to linear RGB48Y with BT.709 transfer
Dither_resize16(1280, 720, Y=3, U=1, V=1)
# Linear resize to 1280x720
Dither_y_linear_to_gamma(False, False, "srgb")
# Convert to gamma compressed RGB48Y with sRGB transfer
Down10(8, TVrange=False)
# Dither down to RGB24Y
MergeRGB(SelectEvery(3, 0), SelectEvery(3, 1), SelectEvery(3, 2))
# PC-Range RGB24 output
Dither_convert_yuv_to_rgb(matrix="709", tv_range=True, lsb_in=False, output="rgb48y")
# Convert to RGB48Y with BT.709 matrix
Dither_y_gamma_to_linear(False, False, "709")
# Convert to linear RGB48Y with BT.709 transfer
Dither_resize16(1280, 720, Y=3, U=1, V=1)
# Linear resize to 1280x720
Dither_y_linear_to_gamma(False, False, "srgb")
# Convert to gamma compressed RGB48Y with sRGB transfer
Down10(8, TVrange=False)
# Dither down to RGB24Y
MergeRGB(SelectEvery(3, 0), SelectEvery(3, 1), SelectEvery(3, 2))
# PC-Range RGB24 output
CODEBOX_PLUS_CODE: [全选]
[CODEBOX_PLUS_EXPAND/CODEBOX_PLUS_COLLAPSE]
# 1920x1080 PC-Range RGB24 input
Interleave(ShowRed("Y8"), ShowGreen("Y8"), ShowBlue("Y8")).U16(TVrange=False)
# Convert to RGB48Y
Dither_y_gamma_to_linear(False, False, "srgb")
# Convert to linear RGB48Y with sRGB transfer
Dither_resize16(1280, 720, Y=3, U=1, V=1)
# Linear resize to 1280x720
Dither_y_linear_to_gamma(False, False, "srgb")
# Convert to gamma compressed RGB48Y with sRGB transfer
Down10(8, TVrange=False)
# Dither down to RGB24Y
MergeRGB(SelectEvery(3, 0), SelectEvery(3, 1), SelectEvery(3, 2))
# PC-Range RGB24 output
Interleave(ShowRed("Y8"), ShowGreen("Y8"), ShowBlue("Y8")).U16(TVrange=False)
# Convert to RGB48Y
Dither_y_gamma_to_linear(False, False, "srgb")
# Convert to linear RGB48Y with sRGB transfer
Dither_resize16(1280, 720, Y=3, U=1, V=1)
# Linear resize to 1280x720
Dither_y_linear_to_gamma(False, False, "srgb")
# Convert to gamma compressed RGB48Y with sRGB transfer
Down10(8, TVrange=False)
# Dither down to RGB24Y
MergeRGB(SelectEvery(3, 0), SelectEvery(3, 1), SelectEvery(3, 2))
# PC-Range RGB24 output
例如我在压The Amazing Spider Man时,这片原盘里的Y范围只有16-184左右,我将其转为RGB48Y(量化范围0-50273);利用一个Dither_lut16的表达式,将其先按照BT.709的标准(这个我只能靠猜,我并不知道原始制作流程是如何的)转为linear scale(亮度范围0-0.589),再将此时获得的亮度信号除以0.589将亮度范围标准化到0-1,然后按照BT.709的标准转回gamma compressed,最终输出的是量化范围为0-65535的RGB48Y;最后转成PC Range的YUV444P10进行压制。
像这样gamma-aware的亮度标准化,相比于直接对量化范围做线性的标准化,在实际画面对比中区别也非常明显,后者导致很多纹理细节看起来都变得不明显(对比度失真),而前者在处理前后整个画面的对比度看起来都没有什么变化。
CODEBOX_PLUS_CODE: [全选]
[CODEBOX_PLUS_EXPAND/CODEBOX_PLUS_COLLAPSE]
LWLibavVideoSource("D:\The Amazing Spider-Man 2012 4K Remastered Blu-Ray 1080p AVC DTS-HD MA 5.1-HDWinG\BDMV\STREAM\00001.m2ts", threads=1)
Crop(0, 140, 0, -140)
nnedi3_resize16(matrix="709", tv_range=True, lsb_in=False, output="RGB48Y")
c_num = 1 # SMPTE 170M (BT.709/BT.601)
# sRGB SMPTE 170M SMPTE 240M BT-2020
k0 = Select (c_num, " 0.04045", " 0.081 ", " 0.0912 ", " 0.08145")
phi = Select (c_num, "12.92 ", " 4.5 ", " 4.0 ", " 4.5 ")
alpha = Select (c_num, " 0.055 ", " 0.099 ", " 0.1115 ", " 0.0993 ")
gamma = Select (c_num, " 2.4 ", " 2.22222", " 2.22222", " 2.22222")
expr = "x 65535 /"
g2l = expr + " " + k0 +" <= " + expr + " " + phi +" / " + expr + " " + alpha + " + 1 " + alpha + " + / " + gamma + " ^ ?"
expr = g2l + " 0.58909115751250729778767280605716 /"
l2g = expr + " " + k0 + " " + phi + " / <= " + expr + " " + phi + " * " + expr + " 1 " + gamma + " / ^ " + alpha + " 1 + * " + alpha + " - ?"
expr = l2g + " 65535 *"
Dither_lut16(expr, y=3, u=1, v=1)
Dither_convert_rgb_to_yuv(SelectEvery(3, 0), SelectEvery(3, 1), SelectEvery(3, 2), matrix="709", tv_range=False, lsb=True, output="YV24")
Down10(dither=-3, TVrange=False, stack=False)
Crop(0, 140, 0, -140)
nnedi3_resize16(matrix="709", tv_range=True, lsb_in=False, output="RGB48Y")
c_num = 1 # SMPTE 170M (BT.709/BT.601)
# sRGB SMPTE 170M SMPTE 240M BT-2020
k0 = Select (c_num, " 0.04045", " 0.081 ", " 0.0912 ", " 0.08145")
phi = Select (c_num, "12.92 ", " 4.5 ", " 4.0 ", " 4.5 ")
alpha = Select (c_num, " 0.055 ", " 0.099 ", " 0.1115 ", " 0.0993 ")
gamma = Select (c_num, " 2.4 ", " 2.22222", " 2.22222", " 2.22222")
expr = "x 65535 /"
g2l = expr + " " + k0 +" <= " + expr + " " + phi +" / " + expr + " " + alpha + " + 1 " + alpha + " + / " + gamma + " ^ ?"
expr = g2l + " 0.58909115751250729778767280605716 /"
l2g = expr + " " + k0 + " " + phi + " / <= " + expr + " " + phi + " * " + expr + " 1 " + gamma + " / ^ " + alpha + " 1 + * " + alpha + " - ?"
expr = l2g + " 65535 *"
Dither_lut16(expr, y=3, u=1, v=1)
Dither_convert_rgb_to_yuv(SelectEvery(3, 0), SelectEvery(3, 1), SelectEvery(3, 2), matrix="709", tv_range=False, lsb=True, output="YV24")
Down10(dither=-3, TVrange=False, stack=False)
ResampleHQ文档里的例子:
原图
Avisynth resizers(gamma-ignorant)
ResampleHQ(gamma-aware)
可以看出gamma-ignorant的resize很明显导致了对比度、亮度上的失真