PNG sRGB cutout/decal AA = 问题重重

发表于2017-10-09
评论1 2.9k浏览

翻译:袁笠凯(°Sinmere)    审校:王成林(麦克斯韦的麦斯威尔)

最近很多人,也包括我自己,在我的博客GPUs preferringpremultiplication 一文中发现了几个问题。首先让我们把这些问题一个个梳理清,然后将它们一点点组织起来,来解释为什么PNG在储存基于物理渲染生成抗锯齿cutoutdecal图片(alpha分量图像)不是很好。事实证明,这不是PNG图片的问题,而是PNG查看器的实现问题。我提供两个可下载的PNG图片测定你自己的浏览器或渲染器对sRGB和合成是否工作正常。

如果你已经确信你应该在线性空间中做filtering(和大多数其他的计算),请跳过第一章节。如果你已经知道,你应该想起像素的线性值在内部经过预乘处理,因为它们代表了像素的亮度,请跳过两个章节。如果你知道查看器和浏览器不能将png图片和alpha通道正确混合,请直接跳到文章结尾结论处,看看你是否同意我的观点。我依旧还在学习过程,因此可以想象我在做了一件蠢事(更新:实际上我的确做了),没错,正是我,虽然我已经很努力不去这样做。我的确很惊讶有多少查看器和浏览器(也许所有?)都不能将这种图片类型显示,过滤,合成运作正确。


别对sRGB进行过滤

这是现在每个人应该都知道的事情,但为了以防万一...

你现在在你的PNG中存储了三个纹理元素以及红色和绿色两种颜色:

在这两种颜色进行平均插值,中间的纹理像素会是什么颜色(存储在你的PNG)呢?答案并不是两端的纹理像素平均值(1281280。你通过下面的例子就能稍微明白是怎么一回事:

而正确的答案是:

你不应该在sRGB(经过伽马校正过的)空间进行插值或其他过滤操作,这就是为什么它看起来会很糟糕的原因。你不应该这样做这些操作,因为sRGB是非线性的,像加法和乘法这类线性运算是无法正常运作。更新:你可以看看这个链接,链接例子中的公交车车牌就是一个很好的例子用来证明这点。

反过来,你若想从sRGB转换到线性空间,在线性空间中插值,然后再变换回sRGB(方程式:这里)。你要想获得一个正确的mipmap或者任何你从使用多个样品中来得一个新值,这些都同样适用。说到这个,我其中最中意的文章是Larry GritzGPU精粹3。最近还有一篇在Renderman社区网站上的关于此工作流的文章也很不错,说明如何将纹理转换为线性空间,再做光照处理,然后转换回去再显示。如果这些文章还没有说服你——线性化是必要的,那么我也不知道该怎么做了。

下面是另一个例子,条纹横穿4个纹理像素的sRGB插值与其相对的正确线性插值:

sRGB插值会出现一个黑色条纹,而正确的线性插值会有一个平滑的过渡(我个人看到一个更偏黄的过渡,这是因为它跨越了多个像素,但一般亮度在这里才是最需要注意的东西如果你将黄色抹去一点,第一个图像中的黑色条纹依然会出现。在手机上你可能必须得去放大才能看清)。


转换为sRGB前进行预乘

假设你计算在线性空间渲染的三角形覆盖面。它覆盖了一些像素的一半区域,阿尔法= 0.5。你计算三角形覆盖一半像素的颜色,并且它颜色是(1.00.00.0)。我将使用3个浮点数表示这里线性空间的颜色这些sRGB的值对应到我们存储在一个PNG图像文件中可以显示的值。

通常情况下你把你的颜色用截取处理(clamp)或其他方式,限定每个RGB值映射在[0.0,1.0]范围中(可能使用色调映射),然后转换为sRGB用以显示和存储。现在的问题是:你是先将颜色值预乘alpha值,然后将其转换为sRGB,还是顺序反过来来呢?

很显然,你不会在sRGB修改alpha自身覆盖面。覆盖面,其在任何色彩空间都是相同的。覆盖面表示的是一个表面在像素中有多大部分是可见的。如果你仔细想想,三角表面颜色为(1.00.00.0)的半覆盖像素的辐射度应该等于表面颜色为(0.50.00.0)的全覆盖像素的辐射度。让这些等价的唯一方法是先乘以Alpha值,然后再将得到的颜色再转换为sRGB。正如Larry Gritz简练的总结,“辐射度是有参与其中的”,即,在像素方面发射器的面积有其重要性。该辐射度是通过包括区域覆盖方面的计算算出来的。

因此,其顺序是按 线性空间 - >预乘获得辐射度的数值 - >这个辐射度转换为sRGB空间。使用颜色为(1.00.00.0)和alpha0.5的三角形,我们得到(0.50.00.00.5)的RGBA。因此,alpha值参与了辐射值的计算。

若要我们在屏幕上显示这个抗锯齿效果我们需要转换到sRGB空间(或γ空间,如果你喜欢更简单的称呼)。当然,我们的屏幕本身不存储一个alpha值,我们无法通过屏幕看到,所以我们通常认为这样的结果是与黑色背景相融合。使用sRGB的转换,我们得到(0.73660.00.0)。乘以255得到8位显示,显示的值为(187,0,0)。


PNG不能存储所有经过截取处理的线性值

我或许是一个糟糕的悬疑作家,因为我的章节中标题就把接下来中会发生什么都给泄露了。可是,我是根据字数来获得稿费的(哈,开个玩笑),我将会慢慢地仔细地讲解每一步,去营造悬念(让你烦得要死)。

这有点奇怪的是:当涉及到alpha值为小数时,你不能在PNG中存储一些看似有效的RGBA值。

更新:下面的逻辑是错误的,但你的浏览器需要它才能正常工作。如果你想省略这个错误部分,可以跳到下一个“更新:处,但是其实还是有着一些有趣的信息挖掘。

在一个PNG中存储一个sRGB值,我们需要“未关联的”或“未进行预乘”的RGBA值。换句话说就是:

未关联的RGB = 关联过的RGB /阿尔法

然后,我们用255乘以所产生的RGBA浮点值以便将结果存储在一个PNG中。

你只要弄明白,阿尔法本身就是不会因未关联和相关的颜色而改变,仅仅是籍由RGB来改变。如果alpha1.0时,未关联的RGB值与关联的RGB值是等价的。如果Alpha0.0,我们则不能进行除法我们假设RGB是(0.00.00.0),因为结果没有区域之说,因此,没有辐射度。只有alpha值为小数时非关联值和关联值才会不同。

从上面我们获取RGBA值(0.50.00.00.5)。

我们将颜色转换为sRGB,四个值变为(0.73530.00.00.5)。

现在,通过unmultiplying(又名除法)将RGB值除以alpha值,得到PNG所需的未关联的RGB值。通过除以0.5 alpha值得到的值也就是,将值乘以2.0。我们最后得到(1.47070.00.00.5)。

将四个值都全部乘以255得到能让我们可存储的8位值。只是为了显示我们还没有转换成PNG的未关联格式,让我们保留这些精确的浮点值:(375.00.00.0127.5)。经过四舍五入,得到(37500128)。

如果我们可以存储预乘(关联的)值,我们可以简单地存储(0.73530.00.00.5)的255倍数,也就是(18700128),要知道当我们转换到线性空间,总会将这些值转换回(0.50.00.00.5)。


总结一下:

0.50.00.00.5)线性空间的预乘结果

0.73530.00.00.5)转换到sRGB

1.47070.00.00.5RGB除以0.5alpha得到未关联的alpha

37500128)乘以255并四舍五入


可笑的是:这个值不能很好地存储在一个PNG中,因为在PNG能存储的最大值为255PNG通常是非关联的。我们存储最好的结果是存储(25500128)。但是,如果我们再将s​​RGB转换回线性空间,我们没有得到任何原来的(0.50.00.00.5)近似结果:

25500128)中存储的PNG

12800128)相关联(乘以alpha值 / 255

0.2160.00.00.5)从sRGB转换到线性空间


答案应该是(0.50.00.00.5),但截取处理极大地将颜色值降低。我们能存储最好的结果是alpha0.216的值的线性化的颜色值,并不是我们能想存储一个alpha0.5的线性化的颜色值。换而言之,我们的三角形不能变得比超过这一数值(0.4320.00.0)的两倍更亮,而不是(1.00.00.0 - 对于线性空间物体来说相当大地降低了明暗度。

我不知道你对这个结果是否感到惊讶,但我惊奇地发现PNG实际上是无法存储在线性空间进行常规渲染并经过抗锯齿抠出的图像。

主要的问题往往是在提升存储8位预乘颜色和alpha值会使你丢失精度:255128灰度级在alpha1时都将用1来表示。另一方面,对于在预乘并转换为sRGB时具有完全有效的颜色值和alpha值的颜色,用于普通PNG中的无关联的存储值无法正确保存这些RGBA值。可悲的是PNG没有用于存储的预乘模式,所以才会有这些问题如果有这样一个模式,它可以妥善保存(18700128)值然后在屏幕上正常显示(18700)。

如果你不相信这个结果,认为它有些不足,尝试下解决这个谜题                               

更新:其实,有一个问题!事实证明,对于PNG你需要在转换为sRGB之前进行除法。这与理论背道而驰,你通常需要一个预乘的结果,将其转换为sRGB显示(和黑色背景混合)。但事实证明,对于PNG转换正确的顺序是未预乘,然后再转换到sRGB。所以正确的答案是存储(25500128)。你将它转换为线性空间,(1.00.00.0),乘以(0.50.00.0alpha值,转换回sRGB空间(187,0,0),并显示结果。这只是这么简单。这就是为什么预乘更佳:如果PNG可以存储预乘值,这些转换都是不必要的,你只是忽略了alpha并将RGB存储值显示出来。

查看谜题这篇文章来获得更多信息,感谢friedlinguini寻找在规范正确的通道。我很高兴地看到PNG本身不是非常糟糕!在新的信息基础上,让我们来看看查看器和浏览器如何看待这样的带alphaPNG图像。


让我们在家用查看器来一探究竟

图像操作程序,查看器和浏览器能否将PNGalpha实现正确?让我们使用灰度来找出答案...(提示:答案是一个非常响亮的“不” - 如果你发现一个正确的方案,请务必让我知道)。

一个问题是png图片是否默认是sRGB,还是默认是线性的也就是说,如果伽马或sRGB部分缺失,会出现什么情况呢?我翻遍了资料,但没有看到一个明确的答案,老实说,凭我的经验来看,我见到所有没有标签的PNG99.98%​​都在sRGB中的 他们本来就是用来直接显示的。

但是,让我们来测试。下面是两个PNG示例图像:

它们(可能)在你的显示屏上看起来相同:两个浅灰色正方型在左边,一个深灰色正方形在右上,一个白色的正方形在右下。我查了一下:在iPhone 6或三星Galaxy S3上不会这样,因为你无法在其原生分辨率显示此图像。这些设备中的图像执行简易的和不正确的过滤操作(它们在sRGB空间进行过滤;下文会有更多的介绍)。

两个图像具有相同的数据:

两种情况中的左上方方块都是由全白和全黑的直线交错组成。你的视线会被混淆,得到一个半灰色。当你的视线被混淆时,左下图和右上图(在sRGB 显示屏上)的相称显示了该灰色的sRGB特征,这是一个基本的伽马测试。这表明,两张PNG都被视为储存非线性sRGB值,因为187灰度值是线性空间中半灰色的sRGB等价值。PNG中有一个gamma,但它很少被用到。

两个图像之间的唯一区别是,在左侧的那图不具有伽马校正或sRGBPNG块(使用LodePNG生成),右侧的那图都有(它是通过读左边的图到paint.net然后输出出来的,你可以在详细模式下使用pngcheck查看块)。它们显示相同,因此,在浏览器显然假设如果这两个块被丢失,PNG应默认为存储的sRGB值。这实际上是条准则:PNG图像通常用于图像的无损显示,所以颜色值自然是直接复制sRGB值到显示的。然而,这意味着PNG中“你可以在伽玛设为1.0”的选项是极不可能被大多数工具所兑现。此外,即使有可能,在线性空间存储8位值可以换算为sRGB得到条纹状。PNG不支持16位的存储,这将从使用的1.0伽马值解决条纹问题。

IrfanView软件中显示此图像和黑色背景的混合,你会得到这样的图:

注意,右下角是128-半灰色。

如果你想依次看到测试图片在黑色、白色和灰色背景下的图像,请查看这个页面

大多数(全部?)浏览器和查看器都有点问题

现在我们知道,如果他们PNG图片默认在sRGB空间进行处理。然而,事实证明,大多数浏览器和查看器在带有alpha值的情况下不能正确地解析或混合PNG的颜色值时,或者即使他们没有alpha值!以下是证明过程。

在右边的两个正方形都有0.5alpha值。上面的正方形为黑色,下面的正方形为白色。浏览器将图像和背景颜色进行混合后。如果背景色为白色(当它在这个页面上),然后右上方正方形混合后显示的是用的(0,0,0,128)的值的半黑半白。也就是说,表面覆盖有一个黑色的颜色,是半透明的,从而使白色背景应提供仅一半的辐射度。如果计算得当的话-  sRGB到线性空间,进行混合后,再从线性空间转换为sRGB - 所得的颜色应该是在(187,187,187)左右跟左侧的结果一样。这样显然不行,浏览器简单地将两种颜色直接混合在sRGB空间,没有任何线性化,会得到比应该显示的灰色要更暗些。

反过来,如果你显示这些图像与黑色混合,就像热门的IrfanView软件一样,你会得到一个右下方的深灰色效果,你会再次应该得到一个187级灰度,如上图所示。因此,IrfanView(其他我测试过的查看器)在混合是都没有进行线性化。

你也能知道即使没有alpha值存在,混合也会显示不正确。通过使用“resize”功能。调整测试图像尺寸到原来的大小,即50%,使大小调至128×128。使用市面上最好的过滤器(例如,Lanczos)。

下面是XnView的得到结果,例如(我不得不让IrfanView的问题,妥善保存alpha通道):

这是错误的,它不是在线性空间进行融合。可以说,因为在左上方的交替行现在是一个128级灰度,而不是正确的187级灰度。在左上角的灰色明显比原始图像的缩小版本显得更暗。如果你有一个图像处理程序能获得正确的答案,请告诉我。想象以下,在mip-map金字塔下一阶段中,你可以看到为什么在交互式3D图形的标准是过滤之前进行线性化,以及为什么有GPU支持它。可惜我们无法获得2D图形正确的算法。

这边还是原始那张图像,但是在你的浏览器中把它调整至HTML图像显示更小的宽度和高度(128×128)。

我敢用甜甜圈跟你打赌,你会看到错误的结果,跟XnView的(以及我试过的其他免费所有图像处理程序)一样。图像被缩小到一半大小等的白色和黑色的交替线是不能正确获得128灰色模糊效果。

顺便一提,图片样式使用白色和黑色相间的交替线,而不是使用白色和黑色棋盘样式,其原因是为了避免显示器可能具有响应问题。这曾经是CRT显示器存在的问题,我不知道这是否对液晶显示器也有这样的问题,但是让我们把他放在讨论范围之外。

如果你想尝试做实验,右键单击这两张图像并保存它们作为表面纹理贴上去看看,看看是否合成正确。如果在右边的正方形灰色比较跟左侧的灰色还不是很相近,那么软件执行alpha混合并不正确。它应该预乘操作(每一个查看器和浏览器做PNG转换都是正确的),对每个颜色值线性化,与线性化背景颜色值混合,然后再转换回sRGB空间进行显示。相反,大多数软件直接sRGB空间进行简单的混合,这是不对的。

如果左边的两个正方形没有或多或少地让你感到适应(模糊你的眼睛),那么可能你在一台古老的MacNexTSGI,或者其他非sRGB的操作。更有可能,你在智能手机或其他设备没有将测试图片每个纹理像素每个像素显示出。当黑白交替的线条交替的应该是187灰色级的时候,过滤缺失使它被限制为128个灰度级。

我怀疑,我尝试的所有大多数查看器和浏览器都打破了这种方式只是权宜之计(将每个像素转换是很消耗性能,并且在在PNG格式alpha小数是比较少见的)和不易理解,再加上可能遗留的用户期望按原先的方式。当然,当我开始这篇博文的时候,,我肯定没有完全理解如何解读PNG数据,所以不得不去改正它!

现在我明白为什么OpenEXR,具有alpha 浮点数格式来存储预乘颜色,由电影公司等行业,其中正确的合成是至关重要的首选。简单地去显示和预乘使得显示和合成消耗性能要小得多。


总结

1. 执行插值,混合,纹理映射,或其他过滤操作在线性空间,而不是在sRGB中。

2. 在线性空间中,如果你的计算过程产生了一个小数alpha值,确保颜色由颜色值,在转换至sRGB之前预乘。更新:除非你转换到PNG时,想在到转换类sRGB空间之前,对RGBA进行unmultiply。。                  

更新:错了。 如果您有小数alpha值并你想合成时,存储这些颜色值供以后合成时使用,你可能会在存储在PNG中的进行alpha值去非联化后的颜色值,得到过一个大的值。抠图缺失部分alpha信息,或较暗颜色值,才能进行存储。

4. 不要指望PNGalpha能在大多数查看器或Web浏览器能正确显示。这本身并不是PNG的错,而是浏览器/查看器在合成时不进行线性化。

5. 测试并找出答案。这张PNG测试图像可以帮助你理解一个应用程序如何处理数据的。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引