Tone Mapping Correction
Posted 嵌入式Max
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Tone Mapping Correction相关的知识,希望对你有一定的参考价值。
上一篇说了 Gamma 矫正的概念,而色调映射的本质理解和 Gamma 有相似之处,所以就顺着思路说到色调映射的概念。色调映射(Tone mapping)用一句话来总结就是用于在显示设备上面更好的呈现出 HDR 的图像效果,包括亮度以及色彩的主观感觉上的增强,最直观的影响就是会造成色调和对比度的变化,从目的上来讲,Tone mapping 和 Gamma 的之所以诞生最开始的作用都是用来在 LDR 设备上面显示 HDR 图像的。
基础概念
下面先用两幅图说明一下是否进行 Tone mapping 的图像在设备上面显示出来的效果差异的例子,需要说明的是这个图的来源本身并不是用来说明本文所指的 Tone mapping 的,但是它可以比较直观的用来大致表述做不做 Tone mapping 的显示差异,所以我就直接拿过来用了。
第一幅图是不进行 Tone mapping 情况下的显示效果(再次注释:这个仅用来进行一个可视化的形象说明,不代表下面这个图原本的用意),可以看到画面会呈现出一种过暗或者过亮的状态,整体看起来不均衡,这是因为存储设备或者显示设备无法显示如此宽的动态范围,因而导致整个画面的亮暗范围被截断导致不能正常的显示所有的细节。
下面这个是进行 Tone mapping 处理之后的图像,可以看到整体画面变得更加平淡了,但是亮度分布上会更加的均匀,看起来对比度没有那么大,整体呈现出一种灰色的感觉。这里就可以看出来 Tone mapping 的一个大的作用就是用来处理并使得 HDR 的图像可以比较好的还原到 LDR 的存储、显示设备上面去。
目前很多的显示器设备都已经支持了 HDR(10bit) 显示,但是普遍来讲 8bit 还是比较广泛的显示格式,因为视频大部分还是 8bit 的。而对于 8bit 来说,他的显示范围就是 [0-255],也即是 LDR。但是我们实际现实世界的亮度值范围远远超过了 256 个级别可以表示的维度,自然界的亮度范围可以理解为 [ 0 − ∞ ] [0-\\infty] [0−∞],也即是 HDR,而这部分 HDR 就需要重新映射到可显示的区域,所谓 Tone Mapping。从上面的图片示例大概可以看出 Tone mapping 的思路就是把 HDR 的数据整体压缩到可以可以显示的程度,也就是显示出来的暗部更亮,亮部更暗,所以说看起来和 Gamma 是有很多类似的。
TMO(Tone mapping operator) 也就是色调映射算子,就是用于描述从输入 HDR 图像到输出 LDR 图像的映射方程,
C
o
u
t
=
T
M
O
(
C
i
n
)
C_\\mathrmout = \\mathrmTMO(C_\\mathrmin)
Cout=TMO(Cin)
对于 Tone mapping 来讲,有很多的方法可以完成这个动作,一个比较简单的例子就是在 OpenGL 里面有一些 API 会限制整个的光照到 [0.0-1.0] 这个区间,0.0 就不用解释,1.0 是指显示器可以显示的最大亮度,简单理解为最终显示亮度值是 0.x*255(8bit),0.0-1.0 就是一种归一化的表述方式,下面是一个简单的 OpenGL 的截断方式的 HDR to LDR tone mapping,也就是暴力限制显示像素到显示器的显示范围内,当然这就可以认为是一个简单的 Tone mapping,当然也可以想象得到这种暴力的方法可能会丢失掉高光以及暗部区域的细节,因为它是直接截断的形式,没有一个比较平滑的处理:
T
M
O
c
l
a
m
p
2
(
C
)
=
c
l
a
m
p
(
C
,
0.0
,
1.0
)
\\mathrmTMO_clamp2(C) = \\mathrmclamp\\left(C, 0.0, 1.0\\right)
TMOclamp2(C)=clamp(C,0.0,1.0)
因为本身原始 HDR 图像已经大部分落在了 [0.0-1.0] 的范围内了,所以看起来结果还可以,虽然亮部损失了,我们也可以使用下面的公式来优化下,在做 Clamp 之前先除以一个值,这个值可以使固定的随便想的,也可以是先统计完图像的平均亮度,然后按照一定比例得出,但是终究这些都是线性的变换,而人眼之前的 Gamma 部分说过,他是非线性的感光模式,所以非线性的公式显然更适合于 Tone mapping 的算子。
T
M
O
c
l
a
m
p
2
(
C
)
=
c
l
a
m
p
(
C
k
,
0.0
,
1.0
)
\\mathrmTMO_clamp2(C) = \\mathrmclamp\\left(\\fracCk, 0.0, 1.0\\right)
TMOclamp2(C)=clamp(kC,0.0,1.0)
Reinhard Tone mapping 方法
本方法是最早前的经典方法,属于是比较经验的做法,它的主要步骤包含下面几点:
- 计算场景的平均亮度,这里用 log-average 来表示场景的平均亮度,我们需要这个基准值来重新为整个场景定调到新的色调值上面去。
- 给定一个自定义的缩放系数计算出新的 Luminace,这一步会需要用到一个 key value。
- 限制较亮区域到 1.0 范围内。
L
‾
w
=
1
N
exp
(
∑
x
,
y
log
(
δ
+
L
w
(
x
,
y
)
)
)
\\beginequation \\overlineLw=\\frac1N \\exp \\left ( \\sum_x,y \\log(\\delta + Lw(x,y)) \\right ) \\endequation
Lw=N1exp(x,y∑log(δ+Lw(x,y)))
这个对应的就是第一步的计算图像整体平均亮度,
L
w
(
x
,
y
)
Lw(x,y)
Lw(x,y) 就是每个像素的 Luminance 值,这也是一个公式,如果是 RGB 的话就可以使用:
L
=
0.2126
R
+
0.7152
G
+
0.0722
B
L=0.2126R+0.7152G+0.0722B
L=0.2126R+0.7152G+0.0722B 来做一个转化,
δ
\\delta
δ 就是一个非常小的值,用于避免 log 对 0 取对数的情况,
N
N
N 就是像素个数,最后 $\\overlineLw $ 就是所取得的 log-average 场景平均色度值了。
L
(
x
,
y
)
=
a
L
‾
w
L
w
(
x
,
y
)
\\beginequation L(x,y)=\\fraca\\overlineLw Lw(x,y) \\endequation
L(x,y)=LwaLw(x,y)
这个对应的是第二步的计算缩放系数,其中
a
a
a 就是一个自定的数值,Reinhard 方法里面给出的参考是 0.18~0.36 之间,再小的值会导致画面看起来偏暗,再大的值会导致画面看起来偏亮。这一步最重要的就是给定
a
a
a 的值,我们称为 key value,这里的
L
(
x
,
y
)
L(x,y)
L(x,y) 代表的是 Scaled Luminance。
L
d
(
x
,
y
)
=
L
(
x
,
y
)
1
+
L
(
x
,
y
)
\\beginequation Ld(x,y)=\\fracL(x,y)1+L(x,y) \\endequation
Ld(x,y)=1+L(x,y)L(x,y)
最后我们需要保证太亮的区域也可以被限制到 1.0 的范围内,所以有上面的公式,这里如果是比较暗的像素,那就被映射到比较接近 1 的位置,如果是很亮的像素就会被映射到近似
1
L
\\frac1L
L1,这里可以保证所有的像素都被映射到可以显示的范围内,最终出来的 L 值就是出于 0,1 之间的缩放系数。但是这里也有一个问题,就是如果场景像素的亮度原本就是 (1.0, 1.0, 1.0),也就是达到显示亮度最大值了,套用上面的公式就会得到 (0.5, 0.5, 0.5),还是无法很好的按比例还原图片,所以有下面的拓展公式。
L
d
(
x
,
y
)
=
L
(
x
,
y
)
(
1
+
L
(
x
,
y
)
L
w
h
i
t
e
2
)
1
+
L
(
x
,
y
)
\\beginequation \\mathrmLd(x,y) = \\fracL(x,y)\\left(1 + \\fracL(x,y)L_\\mathrmwhite^2\\right)1 + L(x,y) \\endequation
Ld(x,y)=1+L(x,y)L(x,y)(1+Lwhite2L(x,y))
这里新加了个
C
w
h
i
t
e
C_white
Cwhite 的变量,指的是场景里面我们想映射到 (1.0, 1.0, 1.0) 的最小亮度值,任何超过这个亮度值的像素都会被映射到 (1.0, 1.0, 1.0),可以大概想象到,如果这个值越小,画面整体就会偏亮,因为这个相当于选定了映射到最大值的亮度范围,我们取场景中最亮的那个点就可以,这样的话就只有画面中最亮的那个会被映射到 (1.0, 1.0, 1.0),其它的就不会了,当然也可以根据实际需要进行调节。
最终输出的 L d ( x , y ) Ld(x,y) Ld(x,y) 是一个 [0,1] 之间的缩放系数,最后还是要和我们显示器的最大显示范围进行乘法操作,最终得到想要的数据,并且我们是在亮度域上面进行操作,也就是在 YUV 颜色体系下面对 Y 进行操作是比较合适的,因为如果是直接对 RGB 颜色进行操作,会导致整个的对比度和饱和度发生偏移,因为 RGB 并没有对亮度进行分离表示,我们操作的每一个颜色通道分量实际上都包含了亮度、饱和度、色相等等的整体运算。
其它的还有电影系统的 Tone Mapping,比如 Uncharted 2, ACES 等等,但是实际操作中实时系统的 Tone Mapping 更接近于 Reinhard 方法一点,所以这里就只详细了解下 Reinhard 方法,其它的不做详细了解了,他们的区别就是在于映射算子不同,公式的产生途径不同,别的我个人没有计划做过多的了解。
Local Tone Mapping
从上面的步骤可以看出,所有的像素在经过 mapping 之后都会使用一个全局的 Mapping 方法,它不随着像素的位置而改变,整幅图的计算公式是相同的。而 Local Tone Mapping 则是需要考虑像素的在整幅图里面的像素位置,因为不同的像素位置周边的像素亮度域可能是分区域的,就比如下面这幅图,上半部分是比较正常的亮度域,但是下半部分是比较黑的,这个时候我们可能比较期望对下半部分施加一个独立的 Tone Mapping 算子,而上半部分保持不变:
而 Reinhard 方法论文里面则是提供了一个基于 Automatic dodging-and-burning 的 Local Tone Mapping 的方法,其基本的思路就是对不同亮度区域进行不同的曝光处理以得到更多的 Details 细节,简单化的实现就比如说把图像分为亮暗两个区域范围,像素的范围就是 [DarkMin, DarkMax], [LightMin, LightMax],然后对处于前者区间范围的像素进行一个单独的统计生成 Tone Mapping 算子,后者区间的生成另外一个算子,最后对不同的区域进行不同的映射得到输出,当然这个实际操作不会这么粗暴,只是对于理解这个 Tone Mapping 的操作思路进行描述。
大概的思路就是以下的几个步骤(Local Tone mapping):
- 输入一个 RGB 的待调整图像。
- 计算整体的图像强度 I,这个也就是整个图像的亮度表征值,可以使用 RGB 的均值 (R + G + B) / 3,也就是每一个像素的归一化表征值,当然由于整个的 RGB 实际上对人类主观视觉影响程度是不同的,也有很多其他的表述方式,比如: ( R ∗ 20 + G ∗ 40 + B ) 61 \\frac(R*20 + G*40 + B)61 61(R∗20+G∗40+B) 等等。
- 计算每一个像素的色度,用归一化的描述: R I , G I , B I \\fracRI, \\fracGI, \\fracBI IR,IG,IB
- 计算图像强度的 log, L = l o g 2 ( I ) L = log2(I) L=log2(I)。
- 应用各种 filter,比如说双边滤波,双边滤波的主要作用就是得到一副过滤掉低频信息的图像保留下高频的地方: B = b f ( L ) B = bf(L) B=bf(L)
- 计算细节层: D = L − B D = L-B D=L−B (B 就是第三步算出来的色度图)
- 计算一个特殊处理的参考图: B ′ = ( B − o ) ∗ s B' = (B-o)*s B′=(B−o)Tone Mapping Correction