Digital Imaging · 2021-12-30

如何获得一张30档动态范围的“RAW”

该文章提供了一种新的HDRi合成思路,用于保留完整的场景光(Scene Linear Reflection)信息,包括现场相对亮度、光比,并存储为一种新的“RAW”格式。

相比于传统的包围曝光HDR技术,我们的优势在于完美保留光比信息,并且允许用户完全浏览并修改这些信息;而不是像传统包围HDR那样平衡光比,或者说是破坏原有光比!

对不起 ,我最高只获得了20档,因为场景只有20档

30档本身是个伪命题,因为实在是难以找到30档动态范围的场景。这句话更大的意义是说明,该容器拥有几乎无限的动态范围,足以容纳所有的光照信息!

风光摄影师们,请想一下这个场景:美丽而刺眼的夕阳,壮阔风光,光与影交织在一块……

这是一句非常美好的描述语,足够漂亮了吧!这时你架好相机开始拍摄,却发现要么是中间调细节丰富,而高光过曝,要么就是高光保留了,但暗部却太黑了。

我们用一组图像生动的模拟这个心碎的过程

首先是地面正常曝光,你在Camera RAW中打开这张图像

并试图把高光拉回来

曝光-5

很遗憾,我们还是没能找回太阳的细节

所以你长教训了,你可能会采用保守策略拍摄——曝光时优先保留高光,后期再把暗部拉回来

然后你在PS中拉回曝光

真是糟糕透顶了,拉回来都是一堆噪点,不堪入目。

机智的摄影师们肯定想到了,使用包围曝光+后期HDRi(注意不是HDR)合成,从而用蒙版拿回暗部和高光的细节。

风光摄影师们的文件夹可能经常长这样

事实上,这是目前最主流的风光摄影后期手段,简单来说,就是一种补丁思想,以某一种中间曝光的图像为基点,过曝的用减挡位的图像去替换,欠曝的图像用加挡位的图像去替换。HDRi合成有效的解决了相机动态范围有限的问题,并且能让画面中细节更加丰富,暗部的不死黑了,高光也不死白了!

一个基础的HDRi包围合成演示

传统HDRi合成带来的问题是,由于画面中原有的光比信息已经被破坏,这张图像有时候会看上去有点“假”,比如,原来阴影区跟太阳的亮光差了8档,但现在只有3档,所以就看起来会像画一样,并不像现场看到的效果。另一方面,这种图片也不适用于存档,因为画面中原有的光信息已经被破坏。

另外一点,这种修图方法是非常费时的,因为需要精确的控制每个补丁之间的衔接部分,如果控制不好,就容易出现边缘问题(比如一颗周围发着光的树)

由于蒙版操作手法拙劣导致的边缘发光问题

因此,一个能够完整保留现场所有光照信息的容器就很有必要了,但在此之前,我们先要知道,什么叫做现场光照信息。

Scene Linear(场景线性)Gamma(伽马)

在这里我们提出了一个很重要的概念 - Linear,线性光就是真正物理意义上的光照了。

而人眼看到的东西,并不是线性的

(这里偷点懒,引用一点他人的文章,我就不用再写一遍了)

以光为例,若在一小黑屋中,点亮了一支蜡烛A,这支蜡烛对屋内的贡献是显著的,在视觉上也感受到极大的明度提升。但是若是屋内已经点亮了1000支蜡烛,此时再点亮一支蜡烛B的话,从物理能量贡献上,这支新蜡烛B与蜡烛A的物理贡献是一样大的,但是在人的视觉中,B引起的“明度”变化,远远不如A。


为什么?很好理解啊:对于某事物,同样的变化量△a,总量少的时候,变化显著,容易被人感知,事物总量大了,再变化同样的△a,就不那么容易被察觉了。

我们如果去检测一下中灰油漆的反射率,再把它和纯白油漆的反射率相比较。若我们定义白油漆的反射率是100%,黑油漆的反射率是0%,你会发现,中灰油漆的反射率不是直觉中的50%,而是一个在20%上下徘徊的数值。

而Gamma是什么,Gamma可以理解为一个输入输出对应关系

上文这个0-1区间的曲线,就是所谓的Gamma曲线。我们若定义黑是0,白是1,那么在0-1区间,我们是可以用一个幂函数来描述客观自然数值和主观心理感知的对应关系的
不同的幂对应的gamma图像

事实上,为了提高编码效率,以及适应显示设备本身的光电响应特性,我们的图像大多都被编码为带幂函数的Gamma,这大约为2.22 而不是1.0-线性(根据不同标准有所不同,有兴趣可以自行查阅),也就是说,我们看到的图像,其RGB值并不与现场的相对光照强度成「线性对应」。

那能不能用RGB值反解现场光数据呢

理论上,如果编码使用的函数,即OETF已知,则可以套用OETF的逆运算,反解出现场的线性光信息。可事实是,在打开一张RAW图像之后,解码RAW的软件(例如PS,LR)大多都会给图像套一层「渲染变换曲线」,而这条曲线具体长什么样我们也不知道,所以就很难在这个现有的RAW处理平台上求出现场的光照信息了。

注意,即使你的调整参数都是0,这张图像也是经过了软件的渲染变换的!所以就不用想着跟原始光照对应了。

因此,我们需要绕过现在已有软件的这些渲染,严谨的将图像解码为真正原始的状态,没错,我们要从Debayer这一步开始就介入。

不卖关子了,来解决问题吧

根据上述知识,我们便知道了,只要编码体系符合线性Gamma,我们就能更精确的记录现场的光照信息。

这里我们提出一种根据包围曝光RAW生成高动态范围图像的思路

  • STEP1 将RAW解码为线性Gamma,并以32bit精度进行后续运算,色彩空间为ProPhotoRGB - Linear,而非PS中带Gamma的ProPhotoRGB
  • STEP2 对每张RAW在空域内寻找「最佳动态范围」,并生成「分段数据」
  • STEP3 根据曝光设定,补偿每段数据的相对光照关系
  • STEP4 Mask优化 拼接「分段数据」
  • STEP5 存储32bit高动态范围图像

实现案例

受篇幅影响,以下只展示算法思想代码,隐藏非关键代码

STEP1

将RAW解码为 y=1.0 ,WCS为ProPhotoRGB

def getLinearArray(f,hightlight=0):
    print(f)
    if f.endswith('NEF') or f.endswith('ARW')  or f.endswith('CR2') or f.endswith('RAF'):
        img = raw.imread(f)

        img = img.postprocess(
                            output_bps=16,
                            use_camera_wb=True,
                            no_auto_bright=True,
                            half_size=False,
                            gamma=(1,1),
                            highlight_mode=hightlight,
                            output_color=ColorSpace.ProPhotoRGB
                              )

STEP2

计算每个通道的分离alpha

def getMaskimg(img, pattern=1,exp=0):
    # pattern=0 Dark 1 normal 2 Bright
    rgb=np.copy(img)
    midpoint = 0.1 #default 0.1

    low = midpoint * pow(2, -abs(USERDATA.step) / 6)
    high=midpoint*9
    if not pattern == 2:
        img[img = low] = 0
    if not pattern == 0:
        img[img >=high ] = 0 
    alpha=img
    alpha[alpha !=0] = 1#二值化
    alpha=cv2.GaussianBlur(alpha,(13,13),0)
    # rgb=rgb*alpha

STEP3

补偿相对光强度关系,其实就是在Linear下做指数运算

img = img * pow(2, abs(USERDATA.step) / 6 * exp) 

STEP4

循环把每份RAW的有效信息都追加到新图像中

exp=0
for i in raws:
#.......自行脑补之前操作
img=img*alpha#预乘
if exp == 0:
   canvas = img
else:
   canvas = (1-alpha)*canvas + img #使用over混合模式堆叠图像数据
exp = exp + 1

STEP5

后处理

canvas = canvas * pow(2, -abs(USERDATA.step) / 6 * len(filelist)/2) #曝光中间化
    return canvas.astype('Float32')

如何存储这一张“RAW”

常规的8bit jpg png当然Hold不住这么庞大的一张图像了,那当然是请出我们大名鼎鼎的TIF啦!不过这次还有点不一样,我们把最后的结果存储为32Bit TIF或OpenEXR,这样便能记录超过1.0的数据,从而保留所有的高动态范围信息。

同时,由于采用Linear编码,因此这张图像具有RAW属性,想要什么细节就能拿回来。要修改曝光,只需要用简单的乘法,例如对所有RGB值x2就是加一档,x8就是加三档曝光,与相机内调整ISO得到的结果是一致的。

但由于我们的显示设备无法显示如此大的动态范围,所以高光部分都会爆掉,这时就需要做取舍了,我们可以通过改变灰度系数,牺牲反差来拿回一些暗部的信息。

开始测试

我们用Sony A7R IV 拍摄了8张连续的曝光,每次步长2档,我们假定每张RAW图像最大拥有9档的有效动态范围,则最后我们至少能获得20档的动态范围。

经过大约一分钟的处理,堆栈程序最终输出了一张拥有超高动态范围的图像

在PS中打开这张图像之后,我们发现它看上去跟我们平时打开一张RAW图片的样子不太一样。看上去反差非常大,为什么会这样呢,这里提出一个思考题……

显然就是因为这张图像是Linear了啦,由于没有Gamma校准,因此图像看上去对比度就是过大的,我们平时打开一张RAW的时候,PS都会帮我们做Gamma校正和进行一些其他的对比度预控制,让它第一眼看上去更好,但这张图像不会——因此我们可以说他保留了所有原始场景中的光数据。

像RAW一样修改曝光

由于这张图像采用线性编码,因此要修改曝光,只需要用曝光度工具,对画面做指数乘法运算就可以了。在曝光度工具中输入的值n,最终会转化为2^n 并与原始图像相乘。

我们首先看看高光处——输入-7,即减7档曝光,此时暗部已经看不见了,但我们看见了太阳的细节

要知道传统图像体系下,即使你拍的是RAW,也不可能在减7档后,还能拉回如此亮物体的细节

我们再来把曝光改为+7,即加7档,此时暗部的细节全部拿回,高光细节由于过曝而丢失。但我们能够惊讶的发现,暗部没有任何的噪声,而且是-“一点也没有”

这是因为我们的算法在处理图像的时候采用了“寻优”策略,在画面中的每一处都使用了所有输入图像的「最佳曝光区间」。

你一定注意到了,这就像光场相机一样,颠覆了我们原有的拍摄思路,因为现在我们可以先拍摄,后曝光!只要拍摄时记录到了,后期都能找回。

小孩子才做选择,我全都要!

显然,目前还没有显示器能够显示如此大的动态范围。所以,虽然从数据上,这张图像拥有足够大的动态范围,但在显示器上,却不能看到所有的信息。

这里有一个妥协的方案,通过灰度系数工具,更改画面的中灰Gamma,从而把所有的动态范围“压”到一块,而代价就是画面变灰了。

不过至少,我们拿到所有细节了。

聪明的你一定想到了,为什么不能去Camera RAW再做一下反差呢?所以我们现在就把这张32bit的图像先下转到16bit,然后再在Camera RAW中打开。

只需简单暴力的把几个参数拉高,就能让画面的反差基本恢复正常。是不是比那些动辄一大堆遮罩蒙版的风光摄影修图简单多了呢。

再拿开场案例

还记得本文开头的图像吗?

将5张图像输入程序中运行,输出并在PS中打开得到以下的结果

按上述方式调整灰度系数

在Camera RAW中简单的把所有的参数全部拉满,即可获得初步看上去不错的图像

高光亮而不溢,暗部黑而不死,无需降噪,即可获得细节丰富的图像!

万能的摄影师们,充分发挥你们的创造力吧,用更多的信息创造无限的可能性!