本期作者
1. 背景
色彩空间(Color Space)是一种数学模型,用于描述和表示颜色的方式。不同的色彩空间有不同的用途和特点,可以用于不同的应用,如图像处理、计算机图形、印刷、摄影等领域。它一般用于描述设备的色彩能力,或者视频、图片的色彩范围。比如现在大部分人用的手机的屏幕,色彩空间大多为sRGB或者DCI-P3;网页上常见的图片或者视频内容,大多属于sRGB色彩空间;B站客户端播放的HDR视频,则处于BT.2020色彩空间。
目前B站UP主投稿视频是千变万化的,它们包含各种不同的色彩参数, 比如不同的色深,不同的色度采样坐标,不同的色彩空间,不同的亮度传递函数等等(如下图),而且每个用户观看这些视频的终端也不一样,这样就产生了各种各样的兼容性问题——很多时候用户的设备并不能完全正确显示UP主投稿的“原片”,而且老旧的设备或者浏览器也并不支持广色域或者HDR视频。为了考虑终端设备的兼容性和B站服务的完整性,我们会对用户投稿的不同色彩空间的视频进行统一处理。但是如何将各种不同色彩参数的视频进行准确地统一处理,同时又能保证视频的处理速度,这对于我们来说是一个巨大的挑战。
色彩空间转换中需要处理的参数
2. 引擎架构
为了处理各种不同色彩参数的视频,我们研发了一套色彩空间的转换引擎。该引擎会对各种不同色深、色彩空间、亮度传递函数的普通视频统一转换为标准的SDR视频。除了SDR视频, B站也会接收很多HDR视频投稿,对于HDR投稿,B站会统一转出一路标准的HDR视频和一路标准的SDR视频。
该图像转换引擎分为三层,包括滤镜层,引擎层,和设备层。其中滤镜层包含各种滤镜,用于处理不同的色彩参数。引擎层的主要目的是做一些预计算与计算路径优化,比如有一些矩阵乘法只需要计算一次,那么就可以预先计算出来,而不是在像素处理的时候再去做。有一些GPU需要用到的矩阵参数,则也可以进行预拷贝,防止出现CPU与GPU之间出现频繁拷贝的情况,计算路径优化,则是用来去除一些不必要的步骤,来减少计算量。最后,设备层主要包含每种滤镜在不同平台的实现,该引擎包含一个指令集优化的X86版本和一个CUDA版本。接下来我介绍一下该滤镜层的各种色彩转换能力,包括坐标变换、量化、色度采样,色调映射等等。
色彩空间处理引擎的基本架构图
2.1 坐标变换
很多时候,用户投稿的视频会有显示矩阵(Display matrix),它表示视频被旋转、被翻转、或者被转置。如果我们在服务端检测到这种视频,那么就需要对输入视频按照显示矩阵进行坐标变换。无论是软解的CPU数据帧还是硬解的CUDA帧,该引擎都能将其准确地渲染出来。
2.2 量化与反量化
信号的量化主要用于连续的YUV信号转为离散的Y’CbCr,而反量化是将输入视频不同的色深转换为连续信号的过程。该引擎支持8bit、10bit、12bit等不同量化精度的Y’CbCr信号与YUV信号的互相转换,同时也支持pc range、tv range的转换。
2.3 色度采样
色度采样(Chroma subsampling)是一种用于减少图像或视频中色度信息采样率的技术。在数字图像和视频编码中,颜色信息通常由亮度分量(Y)和两个色度分量(U和V)组成。由于人眼对亮度信息更加敏感,而对色度信息的感知相对较弱,因此可以通过减少色度分量的采样率来降低数据量,同时尽量保持图像或视频的质量。下图为常见的像素格式YUV420的采样坐标。
为了色度采样尽可能平滑,我们采用了双线性(Bilinear)插值策略进行上采样和下采样。例如对YUV420像素格式的视频进行色度的上采样, 我们并不会简单地将一个色度像素共享给4个亮度像素, 而是对其进行插值,否则最后输出的视频会有色度过渡的不均匀。同理,对于色度的下采样,我们也会采用同样的插值方法。
常见的色度采样坐标(4:2:0 chroma format)
2.4 色彩矩阵转换
视频处理中的一个关键参数是色彩矩阵(Color matrix),有时也称为色彩空间(Color space)。该参数主要用于确定信号如何转换为RGB,色彩矩阵转换是将输入信号通过色彩矩阵映射到RGB色彩空间的过程。
举例来说,常见的BT.709标准就属于一种色彩矩阵,它指导了如何将YUV信号转换为RGB,其转换方式如下:
为什么选择了上述的矩阵而不是其他的呢?这是因为BT.709协议所指定的色彩空间与sRGB相对应,通过sRGB的色域坐标可以推导出上述的转换矩阵。不同的色彩空间对应不同的转换矩阵,具体的矩阵根据输入的色彩矩阵而定。如果需要将R'G'B'转换为Y'U'V',则需要对上述矩阵进行逆运算。
而对于特殊信号,例如ICtCp信号,我们会首先将其转换为L'M'S'信号,然后再进行后续的转换操作。
2.5 亮度传递函数转换
亮度传递函数是一种用于调整图像或视频信号亮度级别的函数,分为两种OETF(Opto-Electronic Transfer Function)和EOTF(Electro-Optical Transfer Function)。由于人眼对不同光线区域的敏感度不同,人眼对暗部细微亮度变化的敏锐程度较高,而对于高亮度的细微变化则较难察觉。为了以一种均匀的方式呈现信号,以符合人眼的感知,并且为了能够编码更多的信息,我们需要对信号进行OETF压缩。相反地,如果我们需要解压信号,则需要进行相反的处理,即EOTF。常见的EOTF曲线有Gamma2.4曲线和感知量化曲线(Perceptual Quantization,PQ),如下图。感知量化曲线是一种最接近人眼感知的量化方法,因此它也被广泛用作HDR视频的主要亮度传递函数。
Gamma2.4和PQ的EOTF曲线
在该引擎中,我们实现了十几种亮度传递函数的转换,包括Gamma2.2,Gamma2.4,PQ,HLG等等。
2.6色域映射
当涉及到色彩转换或图像处理时,色域映射是一个重要的概念。它指的是将一个色彩空间中的颜色值映射到另一个色彩空间中的过程。
假设有一个图像,它使用BT.2020(一种广色域)色彩空间进行编码,但你希望在一个只支持sRGB色彩空间的设备上显示这个图像。由于这两个色彩空间具有不同的色域范围,直接将图像的颜色值转换为sRGB可能会导致颜色失真。在这种情况下,色域映射就派上用场了。色域映射算法会根据源色彩空间和目标色彩空间的特性,将图像的颜色值进行适当的转换,以确保在目标设备上显示的图像颜色尽可能接近原始图像的颜色。
色域映射的方法一般有两种,一种是基于Clip的方法,一种是基于压缩的方法。首先介绍一下基于Clip方法的色域映射。想要做色域映射,首先需要将RGB线性信号转换到XYZ色彩空间,具体转换矩阵可以由r177协议得到。
比如BT.2020色域的RGB转XYZ的公式如下:
要将XYZ信号转换为BT.709 RGB信号,使用BT.709的XYZ到RGB的转换矩阵就可以了,如下:
上述两步过程就完成了最简单的色域映射,它将广色域的BT.2020 RGB转换为了窄色域的BT.709 RGB。但是该方法有一个缺点,当广色域转换到窄色域会对信号进行截断(Clip),从而丢失一些色彩信息,导致视频的有些区域会过于饱和。一般来说,更好的色域映射方法是通过色域压缩的方式进行。
色域压缩可以选择一个人眼看起来相对均匀的色彩空间进行压缩,比如CIE Lab,CIE Luv,在饱和度为某个阈值内的色彩不进行压缩,过于饱和的色彩才进行压缩。在色域压缩的时候,还需要考虑到色相映射(Hue mapping),如果不考虑色相,信号处理的过程可能会有色偏。有些色域压缩的算法的复杂度会非常高,甚至需要用GPU来加速。
下图为BT.2407协议中一种色域映射后的效果图,左图为基于Clip的色域映射,右图为基于压缩方法的色域映射,可以看到优秀的的色域映射算法可以呈现更丰富的细节,它对最后的效果呈现影响非常大。
Clip方法和色域压缩方法效果对比图
我们的转换引擎也实现了一套自研的色域压缩算法,可以将广色域的视频压缩至sRGB而不会产生色彩溢出,同时也会保留了高饱和区域的细节。
2.7 色调映射(Tone mapping)
HDR图像包含比传统SDR图像更大的亮度范围,可以捕捉到更多的细节和光照信息。然而,SDR显示设备(如计算机显示器或手机屏幕)的亮度范围有限,无法直接显示HDR图像中的所有细节和对比度。这就需要通过色调映射(Tone mapping)技术将HDR图像转换为SDR图像,使其在常规显示设备上呈现出较好的视觉效果。
下图展示了不同的Tone mapping算法对图像的影响,b为原始HDR图片的色阶图,含有低亮度区域和高光区域,c为归一化(Normalization)方法的进行Tone mapping,虽然c的高光被压缩了,但是它的暗部细节也被等比例压缩,造成暗部细节丢失。d则是简单地将高光进行截断(Clipping),虽然暗部细节保留了,但是高光细节丢失了。e表征了一种更高级的方法,既可以保证暗部细节丢失很少,又可以保留一定的高光细节。
不同算法对HDR 高光区域渲染的效果
图片来源:ITU-R BT.2390-3
基于Clipping和基于压缩算法的Tone mapping效果
图片来源:https://www.bilibili.com/video/BV1WA411E7i7
我们对比了几种常用的Tone mapping算法,并最终选择了BT.2390协议中的一种曲线作为我们Tone mapping的指导算法。BT.2390算法的主要思想是将输入的线性信号转换为PQ空间,以使输入信号更符合人眼的感知特性。然后设定了一个阈值,保证小于该阈值的信号尽可能接近原始信号,大于该阈值的信号通过Hermite样条曲线将其压缩,从而保证了暗部和高光都丢失比较少的细节。
通过BT.2390将线性信号压缩至不同范围的曲线
图片来源:ITU-R BT.2390-3
通过色域映射和色调映射,B站最终实现的HDR转SDR效果如下,可以看到,对比于VLC的效果,B站的效果做到了比较好的色相还原,并且保证了饱和度和亮度不溢出,暗部的细节也还原得较好。
B站自研HDR转SDR效果(上)与VLC的效果(下)对比
2.8 HDR动态元数据渲染
动态元数据是相对于静态元数据的一个定义,首先介绍一下静态元数据。静态元数据主要包含了整个播放序列的一些参数,包括最高亮度,最大平均亮度,色域信息等。
HDR视频中静态元数据不随着视频场景的变化而变化,整个播放序列维持着同一个值,终端可以根据静态元数据来判断自身的显示能力能否显示视频内容,一般地,优秀的显示软件会根据自身的显示能力对视频信号进行Tone mapping处理,来保证不丢失高光细节。
静态元数据的缺点是显而易见的,通过静态元数据进行Tone mapping的方法在整个播放序列中是不变的,以至于它不能适配复杂的场景。有时候它显示高光细节是优秀的,但却无法还原暗部细节;有时候它显示暗部细节不错,但显示高光场景会过曝。总而言之,通过静态元数据进行Tone mapping,它无法识别视频中每个场景的亮度,以至于无法有效地去针对每一个场景进行Tone mapping。
对比静态元数据,动态元数据则灵活很多。在拥有动态元数据的视频中,视频中的每个场景甚至每帧都会包含一组元数据,来标识视频的亮度信息,它还会对每帧内容生成几组调色曲线来适配不同亮度的显示终端。终端在显示该场景的时候,会根据自身的显示能力来渲染调色曲线,从而让每个场景,无论是暗部还是亮部,都能得到较完美的显示,从而避免了暗部细节和高光细节的丢失。在服务端的色彩空间转换模块中,我们也会根据HDR的动态元数据来生成下变换后的SDR视频,这样生成的SDR视频也会比全局的Tone mapping算法好很多。
包含HDR动态元数据的视频主要包括杜比视界和国产高动态范围视频HDR Vivid,目前我们正在推动HDR Vivid在B站的上线。对于HDR Vivid在服务端的处理,我们除了正常编码一路HDR Vivid,还会借助引擎中动态元数据渲染的能力,来生成一路SDR视频,以适配不支持HDR 的手机。
HDR Vivid 视频在服务端的转换
3. 工程化加速
对于该引擎的工程化加速,我们尝试了很多办法,具体大致有如下图几种:
工程化加速用到的方法
色彩空间转换中,有很多参数是需要预先计算的,比如各种矩阵的参数,图像插值的数值,亮度传递函数中的一些常数等等。如果这些参数在像素处理的时候再去计算会极大地浪费计算资源,所以在引擎初始化的时候就将其计算出来是一个不错的选择。除此,在CUDA计算中,矩阵参数和插值参数也是需要进行预拷贝处理,这样可以避免程序在像素处理中进行频繁拷贝而影响性能。
计算路径优化的主要目的是为了消除重复计算,以找到一个色彩空间到另外一个色彩空间的最优转换路径。举例来说,如果一个视频只需要进行range转换,那么我们无需整个转换路径走一遍,从而避免了额外的色彩转换和电光转换,如下图。
pc range信号转tv range计算路径优化示意图
对于CPU优化来说,色彩空间转换中的幂运算和矩阵乘法对CPU的消耗很大。为了提高程序的性能,在CPU处理中,我们对频繁使用的矩阵乘法和幂运算进行了指令集优化。这样的优化显著提升了程序的性能。
在对幂运算进行指令集优化的过程中,存在一个难点。常见的编译器并没有提供针对幂运算的向量化函数,因此我们需要手动编写一套幂运算函数。为了实现幂运算的向量化优化,首先将pow函数写成形如pow(x, m) = exp(m * log(x))的形式。然后,通过泰勒级数展开逐步实现了经过AVX指令集加速的log(x)和exp(x)函数。
这种优化策略充分利用了指令集的特性,以提高幂运算和矩阵乘法的计算效率。通过手动编写幂运算函数并进行向量化优化,我们能够更好地利用CPU的计算能力,从而加速色彩空间转换的处理过程。
除了使用指令集加速,我们还采用了查找表进行优化,通过将复杂的计算提前存储在查找表中,可以显著减少计算量。
通过使用指令集优化和使用查找表优化策略,CPU端的性能提升达到如下
CPU加速策略对性能的提升
将CPU端的代码进行一系列优化后,通过4K视频在AMD Ryzen 9 5950X上的测试,我们的色彩空间转换引擎的性能达到了如下指标:
CPU端色彩空间转换性能实验数据
对于CUDA端优化来说,加速手段主要有C++模板和Kernel融合技术。由于色彩空间的属性和种类非常多,需要写很多if else分支,但是CUDA是天然不支持分支预测功能的,所以我们没有将很多if else放在核函数内部,而是使用C++模板来编译kernel函数,这样可以防止kernel中出现很多分支,从而大大加快了GPU的处理速度。
此外,由于CUDA在执行全局内存(Gobal memory)的I/O操作时非常耗时,因此我们应避免为每个细小的操作编写单独的核函数(Kernel)。相反,我们可以将一些操作合并到一个核函数中以减少I/O开销。例如,我们可以将range转换和量化处理合并为一个核函数,将色彩矩阵转换和亮度传递函数的转换也可以合并为一个核函数(如下图所示)。通过合并核函数,我们在一定程度上牺牲了一些通用性,但却能大大节省GPU的I/O开销。这样做是一个明智的选择,特别是在涉及I/O操作频繁的情况下。
色彩空间转换引擎中kernel融合示意图
通过使用C++模板和使用Kernel融合策略,CUDA端的性能提升达到如下
CUDA 加速策略对性能的提升
将CUDA端的代码进行一系列优化后,通过4K视频在RTX2060上的测试,我们的色彩空间引擎转换性能达到了225FPS+(未编码),对比于CPU端,处理速度得到了很大的提升,详细数据如下表
CUDA端色彩空间转换性能实验数据(未包含编解码耗时)
4. 总结与展望
目前,我们的色彩空间转换引擎已经稳定地在线上为成千上万的用户提供服务。在用户的观看体验中,视频的色彩质量至关重要。因此,将视频的色彩准确呈现在用户眼前一直是我们追求的目标。我们将继续致力于优化色彩转换效果,以提供更好的用户体验。
我们实现了一套HDR转SDR的算法,但是当下并没有一个好的算法可以准确评估HDR转SDR的效果,像HDR-VDP(一种客观的图像质量评估方法),它只能评估亮度、对比度、细节上的质量,并无法评估两张图像在色度上的差异。色差公式CIEDE2000可以比较两个像素在亮度、色度、饱和度上的差异,但是由于HDR和SDR之间存在巨大的亮度范围差异,仅仅使用CIEDE2000公式计算的色差值无法准确反映HDR转SDR后的差异。在实际应用中,评估HDR转SDR的效果通常需要结合其他指标和方法,如亮部细节、暗部细节、色相差异等。结合这些指标可以更全面地衡量HDR与SDR之间的差异,以便更好地评估转换后的图像质量和观看体验。
随着高动态范围的视频内容越来越普及,HDR协议也在扩充新的标准,最新的高动态范围标准主要有杜比视界以及HDR Vivid。其中,HDR Vivid是中国超高清视频产业联盟发布的国产高动态范围的视频技术标准,目前最新的开源框架FFmpeg已经集成了HDR Vivid元数据的解码,我们正在将HDR Vivid元数据的渲染集成到我们自己的色彩转换引擎中,来支持国产的动态范围标准,相信未来的不久,大家就可以在B站客户端上欣赏到来自国产高动态范围标准的视频了。