Unity中的图片处理:透明图片灰边问题
最近在Unity中使用带透明色的图片(PNG和GIF),发现会出现灰色的边,如下图。左边是有问题的效果,右边是正常的效果
因为缺乏对计算机图像处理知识的了解,踩了许多坑,恶补了许多知识后才了解这个问题出现的原因和解决办法。下面分享一下相关的知识。
图片的缩放
图片的缩放(Scaling),又叫做图片的重采样(Resampling)。图片缩放问题可以这样描述:
我们有一张宽高的图片,我们知道这个图片上每个像素点的颜色值。我们要得到一张
的新图片,需要确定新图片的每个像素点的颜色,使得新的图片和已有的图片长得”差不多“。
简便起见,假设我们要把一张的图片缩放为
大小。首先,我们把原来的图片的像素点摆放在一张网格上。如下图所示
我们把新图片拉伸,让它的边界和原图对齐。
新图片的每个像素”观察“它的四周,根据附近的原图像素颜色确定自己的颜色。具体的方法,就是不同的缩放算法的区别。
一个简单算法是最近邻居(Nearest Neighbor)。顾名思义,每个像素选择原图中离它最近的像素的颜色。
另外一类算法是对原图像素做加权平均,使用过滤器(Filter)决定不同的像素对目标点影响的权重值。过滤器是一个函数,如下图。左边是一维过滤器,右边是二维。
对每个待确定颜色的目标点,将目标点的位置当做原点,把过滤器的中心”放“到这个位置上。原图中每个像素的权重,就是这个函数在那一点的值。图片缩放需要二维过滤器。为了简便,下图用一维的示例说明这个过程。
一般情况下,Filter函数完全集中于原点附近,只有附近的几个点才能得到非零的权重值。
下面图中,左边是最近邻居法的效果,右边是Bilinear Filter的效果。
- Unity支持这两种缩放算法。在Project面板中选择一个图片资源,在Inspector面板中可以选择”Filter Mode“来改变缩放算法。Bilinear和Trilinear是两种不同的Filter。默认采用Bilinear。
![]()
- 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
- 左右拼接,各加宽度为2的边距:
缩放图片:
convert -resize 300 input.png output.png