Unity中的图片处理:透明图片灰边问题

最近在Unity中使用带透明色的图片(PNG和GIF),发现会出现灰色的边,如下图。左边是有问题的效果,右边是正常的效果

因为缺乏对计算机图像处理知识的了解,踩了许多坑,恶补了许多知识后才了解这个问题出现的原因和解决办法。下面分享一下相关的知识。

图片的缩放

图片的缩放(Scaling),又叫做图片的重采样(Resampling)。图片缩放问题可以这样描述:

我们有一张宽高的图片,我们知道这个图片上每个像素点的颜色值。我们要得到一张的新图片,需要确定新图片的每个像素点的颜色,使得新的图片和已有的图片长得”差不多“。

简便起见,假设我们要把一张的图片缩放为大小。首先,我们把原来的图片的像素点摆放在一张网格上。如下图所示

我们把新图片拉伸,让它的边界和原图对齐。

新图片的每个像素”观察“它的四周,根据附近的原图像素颜色确定自己的颜色。具体的方法,就是不同的缩放算法的区别。

一个简单算法是最近邻居(Nearest Neighbor)。顾名思义,每个像素选择原图中离它最近的像素的颜色。

另外一类算法是对原图像素做加权平均,使用过滤器(Filter)决定不同的像素对目标点影响的权重值。过滤器是一个函数,如下图。左边是一维过滤器,右边是二维。

对每个待确定颜色的目标点,将目标点的位置当做原点,把过滤器的中心”放“到这个位置上。原图中每个像素的权重,就是这个函数在那一点的值。图片缩放需要二维过滤器。为了简便,下图用一维的示例说明这个过程。

一般情况下,Filter函数完全集中于原点附近,只有附近的几个点才能得到非零的权重值。

下面图中,左边是最近邻居法的效果,右边是Bilinear Filter的效果。

  1. Unity支持这两种缩放算法。在Project面板中选择一个图片资源,在Inspector面板中可以选择”Filter Mode“来改变缩放算法。Bilinear和Trilinear是两种不同的Filter。默认采用Bilinear。

  1. UIWidgets中的图片Image组件也有一个filterMode参数,默认为FilterMode.Bilinear

灰边问题

上文讲的图片缩放算法,在实际应用中,是在图片的每个颜色通道上分别计算的。对于只有RGB通道的图片,一切都正常。但Alpha通道引入了新的问题。

Alpha通道,简单理解为透明通道,但对它的理解却不止一种。最直观的理解是,它就是这个颜色自身的”权重“。用这种理解方式,当这个颜色叠加在一个背景色之上的时候,得到的结果应该是

最终颜色 = 新颜色.RGB * 新颜色.A + 背景色.RGB * (1 - 新颜色.A)

新颜色的Alpha为1时,结果为新颜色,背景色被完全盖住;新颜色的Alpha为0时,结果为背景色,新颜色完全不起作用。

当Alpha为0时,颜色是纯透明的。此时,这个颜色的RGB通道无论取什么值,都不影响这个颜色。许多图像处理软件,输出Alpha为0的颜色时,就默认把RGB也都设为0。也就是说,只看RGB的话,那些透明的地方都是黑色。下面左图是带透明通道的原图,右图为只取RGB的结果。

经过缩放算法之后,因为过滤器的缘故,边界的地方会出现一点点灰色,那是前景色和背景的黑色加权平均的结果。下图为RGB部分经过缩放之后的结果。

至少,在黑色背景上,这看起来没有太大问题。边界上有些模糊,是缩放带来的正常效果。

但是,这只是RGB通道上的样子。在模糊的边界上,Alpha值介于0和1之间。如果加上Alpha通道,放在白色背景上,那些模糊的边界就成了灰边。

解决方法

Color Bleeding

如果项目很急,没有时间改代码,只有时间改图片,你会采取什么措施?嗯,问题不就是出在,表面上干净透明的背景,其实是黑色的吗?把所有Alpha为0的地方的RGB部分改成白色,不就可以了吗?

确实可以,但只解决了把这个图片放在白色背景上时的问题。如果把透明白色背景的图片放到黑色背景上,会出现类似的白边。类似地,如果图片要放在红色背景上,就需要把透明部分改为红色。

好,你不嫌麻烦,为每个可能用到的背景色都准备一个版本。但如果背景色是运行时可以改变的呢,比如用户可以自选背景颜色?这时候你才去想,能不能把透明的地方设为一种”万能“的颜色,让这个图片放到任何背景上都没有问题呢?

嗯,之所以出现这个边,是因为这个边的颜色和前景色背景色都不一样。与其费尽心机去预测背景色,为什么不能让这个边的颜色和前景色一样呢?前景色是黄的,边也是黄的,不就看不出来了吗?

对这张图片,问题确实解决了。但毕竟不是所有图片都是纯色的(这张图其实也不是纯色,有一点渐变色,不过影响不大)。如果左半边是红色,右半边是绿色呢?当然,你可以把左半边背景改成红色,右半边背景改成绿色。可以解决问题,只不过对PS的技术要求高了那么一些。如果是下面这些图标之一呢……

根本的解决方案还是要靠算法。如果你要在Unity中用这个图片,那么不需要对图片做任何处理,只需要在Project面板中选中这个图片,在Inspector面板中选中Alpha Is Transparency

于是灰边就神奇地消失了。实际上,Unity就是对图片做了类似于上面说的那种处理,如下所示

不过,不是很清楚Unity具体用了什么算法,为什么会有这么多奇奇怪怪的形状。关于这类方法,我在网上没有找到权威的说法,姑且将这类方法称为Color Bleeding。

另外,也不清楚为什么Unity把这个参数叫做Alpha Is Transparency。这个名字很误导人,让我以为Unity用的是下一节讲的Premultiplied Alpha。

Premultiplied Alpha

Premultiplied Alpha,简称PMA,是另一种理解Alpha值的方法:Alpha值是对背景颜色的”遮挡“程度。当一个颜色置于背景色上时,结果如下

最终颜色 = 新颜色.RGB + 背景色.RGB * (1 - 新颜色.A)

和之前的公式相比,这个公式中,新颜色不再乘以Alpha值。

按照PMA,Alpha值不再代表图片本身贡献的颜色。所以,Alpha值的增加或降低不再代表这个图片是变亮了还是变暗了。Alpha值只会调节背景的亮暗。如果你想让这个图片本身变暗,你要把RGB降低。

所以,拿到一张图片,如果要把它转为PMA的图片,就需要调整其RGB值,将其乘以Alpha,这样使用PMA方式渲染时,得到的效果才和正常图片一样。实际上,如果一个正常的图片先做了PMA处理,再用PMA的方式渲染,结果为

最终颜色 = PMA.RGB + 背景色.RGB * (1 - PMA.A)
       = 新颜色.RGB * 新颜色.A + 背景色.RGB * (1 - 新颜色.A)

可以看到,和之前的公式一模一样。

看起来PMA图片和非PMA图片只不过是对Alpha的理解方式不同而已,效果并没有不同。然而,当面临图片缩放时,它们的区别就显现了。PMA图片的渲染不会出现上文的灰边问题。

可以这么理解:在使用Filter计算权重缩放时,如果图片经过了PMA的处理,相当于把原图的对应像素的Alpha值乘到了权重上。Alpha为0的地方的RGB值就不会再对结果产生影响。

和Color Bleeding相比,PMA简单直接,而且造成其他奇奇怪怪问题的可能性更小。不过,渲染的时候要区分这个图片是不是PMA的,采用对应的渲染方式。

不过,我没有在Unity中找到对应的参数。所以,对于直接通过Unity加载的图片资源,暂时使用Alpha Is Transparency参数。而对于网络上下载并用Texture2D.LoadImage()方法获取的图片,则需要手动进行PMA处理,如下:

int width = texture.width;
int height = texture.height;
for(int i = 0; i < width; i++) {
  for(int j = 0; j < height; j++) {
    Color color = texture.GetPixel(i, j);
    texture.SetPixel(i, j, new Color(
      color.r * color.a,
      color.g * color.a,
      color.b * color.a,
      color.a)
    );
  }
}

结论

本文介绍了图片缩放算法的原理,以及因此产生的灰边问题。然后,本文介绍了解决灰边问题的方法,即Color Bleeding和Premultiplied Alpha。

附录

参考文章

用到的工具

Imagemagick,强大的命令行图片处理工具

  • 把一张图片的RGB部分取出来:convert -alpha Off input.png output.png

  • 把两张图片拼在一起:

    • 左右拼接,各加宽度为2的边距:montage -geometry +2+2 1.png 2.png output.png
    • 上下拼接:convert -append 1.png 2.png output.png
  • 缩放图片:convert -resize 300 input.png output.png