高动态范围(High-Dynamic Range,简称HDR),相应的,LDR(Low-Dynamic Range),也就是我们常见的8bit存储方式的图片.之前都是在介绍视频方面.但是在高动态图片(HDRI)方面也有一些少量应用, 目前常用的存储格式有三种:OpenEXR、RadianceRGBE、FloatTIFF.
OpenEXR
总之, Exr格式的储存方式是使用4*16Bit RGBA来储存数据的,即是说,它使用四个通道分别为红绿蓝和alpha,每个通道占16位储存空间,每个像素占48位储存空间来储存数据。而这16位信息exr图片是这样分配的,拿出一位来表示这个数值的正负;拿出10位来储存RGBA的基本信息;拿出5位来表示曝光度。跟下面要说的.hdr图片的记录方式有些类似。只不过,我们基本的RGBA值的精度更高,有2的10次方,1024个色阶;而能表达的亮度范围却比较小一些,只有2的5次方,32个档。
RadianceRGBE与FloatTIFF
RGBE文件的扩展名为.hdr,具体信息同样参考这里。
类型 输出格式
char programtype[16]; //#?Radiance/n#Generated by still/n
float gamma; //1.0
float exposure; //1.0
字符串常量 //FORMAT=32-bit_rle_rgbe/n/n
int nWidth, int nHeight //-Y nHeight +X nWidth/n
RGBE数据与HDR FP32(RGB)相互转换公式如下:
1、rgbe->FP32(RGB)
如果e为0, R = G = B = 0.0,否则:
R = r * 2^(e – 128 - 8);
G = g * 2^(e – 128 - 8);
B = b * 2^(e – 128 - 8);
2、FP32(RGB) -> rgbe
v = max(R, G, B);
如果v < 1e-32, r = g = b = e = 0, 否则:
将v用科学计算法表示成 v = m * 2 ^ n ( 0 < m < 1):
r = R * m * 256.0/v;
g = G * m * 256.0/v;
b = B * m * 256.0/v;
e = n + 128;
我们一般说HDR采用FP32,指的是HDR图象运算时候的内存数据类型,而Radiance RGBE文件采用8bit BYTE类型存储HDR数据。也就是说打开Radiance RGBE文件,要使用上面的公式1将Radiance RGBE文件的8bit BYTE文件数据转换为FP 32的HDR内存数据进行运算;保存为Radiance RGBE文件时,要使用上面的公式2将HDR的FP32内存数据转换为Radiance RGBE的8bit BYTE文件数据进行保存。同理,OpenEXR文件的读写也存在将其FP16的文件数据到HDR的 FP32图象数据的转换;而下面将要讲的Float Tiff是不需要进行数据转换,直接将HDR的FP32图象数据保存到TIFF文件中即可.Tiff文件的扩展名为.tif(.tiff),FloatTiff每个通道为FP32(32bit Float Point)类型,一共3个通道96bpp。用Tiff文件存储HDR数据,直接将HDR的FP32保存到TIFF文件中.引用自
用文字表达的可能不大明确,可以用该公式表示:
上面所述的e就是曝光通道,你可以把前面的RGB理解成某个像素的颜色,而这个E通道,可以理解成这个像素的亮度.因为E有8位,所以取值范围也是0到255;当E为128时,2的0次方为1,RGB值保持初始。当E大于128时,没大个1,RGB的亮度就会比之前亮一倍,指数上升
所以这两种图片格式的区别就很明确了。openEXR图片的精度要高于HDR图片(RGBA有1024色阶);而HDR图片的亮度范围要高于openEXR图片;但是最关键的点在这里,HDR图片不能带alpha非常不方便合成,所以摄影里用的相对多一些;而openEXR格式除了能带alpha通道以外,还可以人工置入一些我们三维软件里渲染出来的别的通道,非常适合CG工作者,来自
色调映射
HDRI文件是一种文件,扩展名是hdr或tif格式,有足够的能力保存光照信息,但不一定是全景图。Dynamic Range(动态范围)是指一个场景的最亮和最暗部分之间的相对比值。一张HDR图片,它记录了远远超出256个级别的实际场景的亮度值,超出的部分在屏幕上是显示不出来的。它比其它格式的图像有着更大亮度的数据贮存,而且它记录亮度的方式与传统的图片不同,不是用非线性的方式将亮度信息压缩到8bit或16bit的颜色空间内,而是用直接对应的方式记录亮度信息,它可以说记录了图片环境中的照明信息,因此我们可以使用这种图象来“照亮”场景.
由于大部分显示设备仍然使用最高100nit的显示亮度,由此上面提到的将超出256个亮度级别的实际场景使用普通显示设备进行显示前,需要进行了一些亮度色度等方面信息的转换,将高动态图片映射成地动态图片.这个过程就是色调映射.但需要明确的是, 并不一定是HDR-LDR转换.由于当前HDR定义太过广泛,比如说现在大部分能够显示HDR的设备大部分最高支持1000nit,但是拍摄时的光的信息有可能超过1000, 比如说是2000,但这仍属于HDR标准范围. 因此, 不同所谓的HDR标准之间的映射也是色调映射.
现在的色调映射算法通常都遵循着一种算法设计流水线。现在的大多数算法的不同,也只是体现在流水线中不同步骤的实现方式存在差异,或者额外添加了一些新的限制,但是算法的主要部分,依然遵循着这样一种流水线.
这个流水线接收 HDR 图像,输出 LDR 图像。首先,先对输入的 HDR 图像进行预处理,主要包括测算图像的亮度信息,并将亮度信息转换到对数域。接着,处理后的图像通过边缘保留滤波器(例如双边,三边,金字塔型滤波器),提取出图像的亮度信息,也就是图中的 base layer。然后从处理后的图像中减去 base layer,就可以得到 detail layer,也就是图像的边缘信息,或者称之为图像细节。也就是说,第二步操作,将图像的亮度信息与图像细节分开,以供后续步骤进行不同的操作。再之后,第三步,提取出的亮度信息通过不同的 tone curve(色调曲线)进行压缩,或称之为映射,进而映射到 LDR 图像的亮度域之中。最后,将新的,映射后的亮度与之前提取出的 detail layer 叠加,并通过前一节提到的方式,将图像的颜色还原添加,就得到了最终的 LDR 图像.
有一种称为快速双边滤波算法的色调映射,其过程大致为:
第一步是将 HDR 图像分解为亮度分量和色度分量,此时用双边滤波器对对数域内的亮度进行滤波,并通过亮度与滤波后的商来计算细节层,滤波后的亮度随后使用一个全局算法进行色调映射。最后,将色调映射的基本层、细节层和色度分量重组,形成色调映射的最终结果.通过这个步骤发现其色度分量没有进行色调映射,主要是在亮度分量上运用了色调映射公式.
基于快速双边滤波的色调映射算法
HDR-SDR这种向下的映射一般可以分为全局映射和局部映射.全局映射的结果就是在同一幅内相同像素点的值肯定都是相同的,而局部映射不一定.一般全局映射过程比较简单,有根据某个公式,然后将像素点带入即可,其余的还有直方图理论的映射或根据某种视觉适应模型的映射.局部色调映射相对比较麻烦一些,有的为保持局部的对比度的方法,这是空间不均匀缩放方法的一种.其他的局部色调映射还可能用到写摄影学色调重现的原理来调整局部色调因子.除了上述两种方法外,还可以从频域的方法进行入手来进行色调映射
动态范围
一直再说高动态范围,那么到底是什么动态范围.平时生活中,我们使用手机相机进行照相,当手指点击某个部分意味着在这里聚焦.如果这里比较亮,那么点击这里进行对焦后,其他部分或者图像整体部分就会变暗. 相应的,如果点击比较暗的地方,那么明亮的地方就会显得过曝,这种现象在背景里面有太阳这种比较亮的发光体特别容易出现. 这就是由于相机的动态范围有限造成的.
动态范围,简单理解就是从最亮到最暗所能识别的范围,为什么人眼能清楚识别出背景和人脸,而相机要么看清楚背景,要么看清楚人脸,就是因为人眼的动态范围比相机更加广。这样说好像有点抽象,那我们就把它量化,我们假设,人眼的动态范围为20级,而相机的动态范围为10级,以这张图为例
人的眼睛,从最亮的白到最暗的黑,全部都能识别出来共20级,而相机的识别范围只有10级,这样会导致什么后果。是的,假如相机想要识别到最亮的地方(数值1),由于相机的动态范围只有10级,从亮到暗只能识别10格,所以他最暗的地方只能识别到数值10。低于10的地方,拥有更多高动态范围的人眼让能分辨出亮和黑的层次,但是相机只能统一处理为黑色. 同理,假如相机要识别到最暗的地方(数值20),由于相机的动态范围只有10级,所以他最亮的地方只能识别到数值11.小于11的地方为全白.假如一块石头的亮度都是大于10的,那么照出来的效果这里就是全白.过曝的情况下无法分辨物体.
由此,我们想出,如何拍出更高动态范围的照片的,目前常使用的两种办法就是,第一,使用能够拍摄HDR相机的设备,他拥有足够的高动态范围能力. 第二就是多张照片合成,多次曝光不同范围的聚焦,最终用某些方法进行合成.
如何打开
由于HDR屏幕占有量确实很少,很多软件主要功能并不是查看的功能,而是一种色调映射的功能,然后保存成普通的png图片.更确切的应当称他们为工具. 所以当前市面上能够打开这一类图片的除了主流的ps以外,搜集了一些其他能够打开.hdr文件的软件.
LuminanceHDR,也可以进入官网下载AppImage的形式, .AppImage的后缀文件不需要在进行相应的依赖配置. 双击即可运行软件. 如果双击无反应,点击该文件的属性,权限位置的"执行"位置点对号. 如果需要将该程序添加到程序列表.可以制作一个.desktop文件.
这个软件就应当是一个色调映射的软件,提供了不同的映射曲线
XnView MP, 是一个图像查看软件,可以打开多种图像格式.外国的软件以商业性质使用是收费的,但是个人的是免费.并且提供了主流的windows, Mac以及Linux三个版本
HDR Shop只提供了window版本.不过据了解,现在支持HDR硬件配置的也只有window ,而且必须是win10,
OpenHDR Viewer 提供了一个在线观看HDR文件,并且快速调整色调映射.可以看到曝光度不同的效果.
各个平台能够打开.hdr图片的列表.可谓是想当广泛
HDR File Format: How to Open .HDR Files |File Opener
上面提到的几个平台软件,对各种hdr图片格式的支持存在不一样,有的只支持exr后者.hdr,这个看选择的那款软件.
格式转换
这里主要是记录一下YUV<->RGB之间的互转.首先来说一下经常看到的YUV与YCbCr的关系, 在某种程度上他们表达的意思是一样,但是另一方面YCbCr主要是YUV经过缩放和偏移的翻版(或者成为量化后版本),如何区分呢?一般, 在完全黑画面的时候打印出图像的Y数据, 如果Y=16左右 说明Y经过量化 ,如果Y=0左右 说明Y未经过量化.他是在BT.601色域标准上YUV建议使用的表达方式.引用这里的一句话为:YCbCr is a scaled and offset version of the YUV color space. Y has an 8-bit range from 16 to 235. Cb and Cr have a range of 16-240. YCbCr is used for JPEG compression.
If the RGB data has a range of 0-255 (black-white), as is commonly found in PCs, the following equations should be used to maintain the correct black and white levels,BT.601下的YCbCr与RGB相互转换。
换一个说法,可以把YUV称为full range,而YCbCr称为legal range。其他几种叫法是等价的:legal range=video range=limited range=safe range,full range=data range=computer range。那么为什么会产生这种状况,可以参考这里,传送门一,以及对full range的详解.另外,对于常用的yuv420格式,I420表示着Y是analog(YCbCr),J420表示着Y是digital(YUV)。
在不同色域空间上YUV<->RGB的空间转换也存在系数的不同,可以总结为如下,注意这里用的是YUV,是full range:
Y = a * R + b * G + c * B
Cb = (B - Y) / d
Cr = (R - Y) / e
BT.601 | BT.709 | BT.2020 | |
---|---|---|---|
a | 0.299 | 0.2126 | 0.2627 |
b | 0.587 | 0.7152 | 0.6780 |
c | 0.114 | 0.0722 | 0.0593 |
d | 1.772 | 1.8556 | 1.8814 |
e | 1.402 | 1.5748 | 1.4747 |
R = Y + e * Cr
G = Y - (a * e / b) * Cr - (c * d / b) * Cb
B = Y + d * Cb
如果这种方程式看的不太舒服也可以有矩阵形式(参考文献不同,系数有所不同):
BT.601标准:
BT.709
需要注意的是,一般在使用YUV(full range)中,其范围为Y:[0-1],U:[-0.5,0.5],V:[-0.5,0.5].那么使用上面的RGB-YUV转换时需要注意值的取值范围(rgb的输出0-1)。而YCbCr则是Y:[16,235],CbCr:[16-240],RGB的输出为[16-235].
而上面的集中标准下给出的都是YUV。如果只是知道YCbCr则是不适用上表给的公式的。但是如果有YUV-YCbCr之间的转换公式,则通过其中一种就可以推出另外一种。通过ITU-R BT.2020-2,可以看出:
上图中的公式在BT.709依然适用的,只是n的取值换成8即可。INT取整内的R'G'B'可以看成full range内的取值。,
,
看成是YUV即可。
假如我有一个YCbCr下的值,在BT.709的标准下,实现YUV-RGB的Python代码:
def YUV2RGB_bt709(Y, U, V):
Y = Y.clip(16, 235)
U = U.clip(16, 240)
V = V.clip(16, 240)
Cb = U
Cr = V
Y = (Y - 16) / 219
Cb = (Cb - 128) / 224
Cr = (Cr - 128) / 224
R = Y + e_709 * Cr
G = Y - (a_709 * e_709 / b_709) * Cr - (c_709 * d_709 / b_709) * Cb
B = Y + d_709 * Cb
R = R.clip(0, 1.0)
G = G.clip(0, 1.0)
B = B.clip(0, 1.0)
return R, G, B
def RGB2YUV_bt709(R, G, B, w=960, h=540):
Y = a_709 * R + b_709 * G + c_709 * B
Cb = (B - Y) / d_709
Cr = (R - Y) / e_709
Y = (219 * Y + 16)
Cb = (224 * Cb + 128)
Cr = (224 * Cr + 128)
Y = np.around(Y).astype(np.uint8)
Cb = np.around(Cb).astype(np.uint8)
Cr = np.around(Cr).astype(np.uint8)
Cb = cv.resize(Cb, (w, h), interpolation=cv.INTER_LINEAR)
Cr = cv.resize(Cr, (w, h), interpolation=cv.INTER_LINEAR)
return Y, Cb, Cr
里面的参数在上述表中的abcde对应,只是加了_709.
在其他参考的代码中,特别是C语言,会经常看到stride和plane。这两个的意思分别为:
stride:指在内存中每行像素所占的空间。如下图所示,为了实现内存对齐(或者其它的什么原因),每行像素在内存中所占的空间并不是图像的宽度。
plane:一般是以luma plane、chroma plane的形式出现,其实就是luma层和chroma层,就像RGB,要用三个plane来存
HDR视频的色调映射
这里主要是记录一下ffmpeg的几种转换方法来实现HDR-SDR的向下转换.
ffmpeg中提供的例子:
ffmpeg -i INPUT -vf zscale=transfer=linear,tonemap=clip,zscale=transfer=bt709,format=yuv420p OUTPUT
这里用到了四个参数.并且多次用到了zscale.在早期版本的ffmpeg中,默认是没有zscale的.如果需要将其编译进去需要下载ffmpeg的源码,自己进行编译.
常用的或者说比较全的命令可以参考这个:
zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=2,zscale=t=bt709:m=bt709:r=tv,format=yuv420p
一般在进行编译的时候继续如下步骤:
./configure; make; makeinstall
这种是默认configure文件的编译选项.编出来的ffmpeg缺少很多功能. 但是在linux中,我们可以使用apt-get install ffmpeg方式来安装.通过这种安装方法是对configure进行配置的.通过命令ffmpeg -version查看版本信息时可以看到:
configuration: --extra-libs=-ldl --prefix=/opt/ffmpeg --disable-debug --enable-nonfree --enable-gpl --enable-version3 --enable-libopencore-amrnb --enable-libopencore-amrwb --disable-decoder=amrnb --disable-decoder=amrwb --enable-libpulse --enable-libfreetype --enable-gnutls --enable-libdav1d --enable-libx264 --enable-libx265 --enable-libfdk-aac --enable-libvorbis --enable-libmp3lame --enable-libopus --enable-libvpx --enable-libspeex --enable-libass --enable-avisynth --enable-libsoxr --enable-libxvid --enable-libvidstab --enable-libtheora --enable-libwavpack --enable-libopenjpeg --enable-libgsm --enable-nvenc --enable-libzimg --enable-libaom
--enable-libzimg 该选项即使zscale的配置开关.
一般最新的apt install 的安装方式已经等到目的.但是也可以自定义configure的方式安装.比如 你只需要打开--enable-libzimg,即可按照一下步骤:./configure --enable-libzimg ; make; makeinstall. 这种编出来的ffmpeg连x264这样的编码器都没有的.因为这配置文件里面该编码器是关闭的.
通过ffmpeg的介绍,我们都能够大致了解这四个参数的意义.这里需要提醒的就是这四个参数表示这四个步骤:
首先,
zscale=transfer=linear
这个是将颜色空间通过线性空间进行转换.其实就是一个电光转换(EOTF).将电信号转换成光信号,这里的光信号可以看成是自然界中光线信息.因此称之为线性的.在进行接下来的tonemapping中,必须转换到线性空间.
zscale=p=bt709
该部分是色域映射,对于HDR2SDR,一般都是bt2020->bt709的过程。应当注意的是,这里经过上面的transfer:电光转换函数后是线性光了。可以从这里得到色域映射的矩阵参数。
tonemap=clip
这个即是色调映射的过程.这里是列举clip的方式,还有linear,gamma,hable等几种方式,都可以通过里面的信息找到.
tonemap参数除了有几种方法选择,也还有结果参数可以进行设置。例如,tonemap=hable:desat=3.0
其他的参数还有param,peak。这里param对个别tonemap方法有效,比如说gamma,即tonemap=gamma时配置param的数值才有效。并且不同的色调映射方法,其默认值也不同。
desat的参数是对超过某一亮度的高光进行去饱和度。由于是超过的部分才做,同一张图,desat的值越大,意味着做去饱和度的部分越少,因此,值越大,转出来的图像的饱和度越大。此参数的设置导致超出SDR颜色范围的图像信息减少,但是,也使得图像显得更加自然。
peak参数,直接翻译ffmpeg的说明:用该值覆盖信号/标称/参考峰值。当显示元数据中嵌入的峰值信息不可靠或色调从较低范围映射到较高范围时非常有用。一般的转换,其值为10.0,该值是使用hable时,不设置peak参数和设置peak=10.0时的结果一样得出。
zscale=transfer=bt709
这里是一个光电转换函数的运用.即OETF. 由于SDR下的标准比较统一,因此在bt.709标准下可以找到.
format=yuv420p
这个是制定输出文件的格式,如果不对其进行添加的话,转出来的会使源一样的格式.比如说,hdr是10bit编码,但是在709空间下,用8bit完全够用.但是这里不对其进行format指明,转出来的会使10bit.
hable
经过测试,发现hable的色调映射算法还是最好的。通过源码也可以发现其函数是:
static float hable(float in)
{
float a = 0.15f, b = 0.50f, c = 0.10f, d = 0.20f, e = 0.02f, f = 0.30f;
return (in * (in * a + b * c) + d * e) / (in * (in * a + b) + d * f) - e / f;
}
在ffmpeg里面,都是按照线性光rgb中最亮的那个通道进行habel映射。然后进行归一话,使值都在0-1的范围。归一话的方式就是除以hable(peak)。peak是最高亮度。
这里主要想介绍的是,hable的曲线有点类似与脚的形状(S曲线),有脚趾ton(s曲线的左下角),脚背等,hable中的参数控制着脚趾脚背的形状。在博客中引出了其意义,并且peak换成了white point的概念。
A = Shoulder Strength
B = Linear Strength
C = Linear Angle
D = Toe Strength
E = Toe Numerator
F = Toe Denominator
Note: E/F = Toe Angle
LinearWhite = Linear White Point Value
F(x) = ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F)) - E/F;
FinalColor = F(LinearColor)/F(LinearWhite)
看其实例发现,这里是float3类型,意味着分别对rgb三通道进行hable映射,而不是ffmpeg内部的rgb最大值进行hable映射,然后进行原始rgb的缩放。