Antialiasing
在 Triangles 关于绘制三角形的讲解中我们成功地把三角形绘制在了屏幕像素上,但是出现了很严重的锯齿(走样)现象,在这里我们需要去处理这个问题。
锯齿(Jaggies)的学名叫走样(Aliasing),针对走样问题,我们需要做反走样(Antialiasing)。在讨论反走样(或反锯齿)之前,我们继续探讨下采样。
采样理论
Sampling Artifacts
采样在图形学中的应用非常广泛:
- 三角形光栅化判断离散的点(屏幕空间中的像素)是否在三角形中的过程就是采样。
- 相机拍照时,所有到达感光元件的光学信息被离散成图像上的像素。
- 视频的本质是针对时间的采样。
图二:Rasterization = Sample 2D Positions
图三:Photograph = Sample Image Sensor Plane
像这种在视觉上看起来不太对的东西(错误、误差等),我们管它叫瑕疵(或失真) (Sampling Artifacts)。采样会产生一些 Artifacts,锯齿就属于其中之一;而采样产生的另一个 Artifact 就是摩尔纹(Moire)。
闫老师对摩尔纹的解释是:如图五,去除左边图像信息的奇数行和奇数列,就能看到摩尔纹(图五的右图)。也可以参考网上的一些理解,比如 摩尔纹的原理与产生条件。
看下面一个反直觉的例子(原视频来自 Youtube):
视频中顺时针旋转带条纹的纸片,在视觉上却造成了有些条纹是逆时针旋转。生活中也存在类似的例子,比如高速行驶的汽车的轮子看起来是倒着转的。这些问题都可以通过采样来表示,我们之所以看到这样现象,是因为人眼在时间中的采样出了问题,具体来说是人眼在时间上的采样跟不上运动的速度。
简单归纳一下,采样产生的 Artifacts 包括但不限于以下几项:
- 锯齿(走样):Jaggies - sampling in space
- 摩尔纹:Moire - undersampling images
- 车轮错觉:Wagon wheel effect - sampling in time
采样问题的本质是:采样的速度跟不上信号变化的速度。
Antialiasing Idea
反走样的一个实行方向是:采样前对原始的函数或者信号先做模糊(或者说是滤波)。
我们知道,如果直接对原始信号进行采样,会看到非常明显的锯齿:
而如果在采样前对原始的函数或者信号先做模糊(滤波),则能有效改善锯齿现象:
我们来看一些实际的例子:
这时候可能有人会问:能否先采样后模糊?
答案是不行,这种操作也有名字:Blurred Aliasing。
如上图十一左图就是先采样后模糊的结果,效果明显不如先模糊后采样。但是为什么会这样呢?我们需要先去了解频域相关的知识。
频域(Frequency Domain)
我们可以从简单的函数去理解频率与频域,先从余弦和正弦函数开始:
余弦和正弦函数的区别就在于相位不一样,左右平移了一下。那么什么是频率呢?直觉上我们知道,频率可以表示一个东西变化得有多块。这在函数上如何体现?我们以余弦函数举例,稍微修改下上面的函数定义,增加变量 表示频率,则我们可以得到:
我们看到 的余弦函数的变化要比 的余弦函数变化要密集,接着我们定义一个叫周期的变量 :
即周期 等于频率 f 的倒数。我们用周期来表示函数经过多少个 后会重复出现。以上面的余弦函数举例:
- 时,
- 时,
接着我们引入傅里叶级数展开的概念:
任何一个周期函数,都可以写成一系列正弦函数和余弦函数的线性组合再加一个常数项。
假如我们要表示一个周期函数 ,它长这样:
那么我们可以先简单近似表示成:
更近一步可以表示成:
通过不断地增加适当的余弦函数,它会越来越接近目标周期函数:
而傅里叶变换是一种线性积分变换,用于函数(应用上称作“信号”)在时域和频域之间的变换。它跟傅里叶级数展开是两个不同的东西,但却有所关联。在我大学的时候,曾经为了 A 一道题目,还专门花了好长时间去理解快速傅里叶变换,并记录了整个理解的过程:大数乘法-快速傅里叶变换。而在这里,傅里叶变换可以简单理解为:
一个函数 可以经过一个非常复杂的变换后变成 ,而 也能经过一个复杂的逆变换变回 :
由前面的傅里叶级数展开我们注意到, 代表频率,因此,任何一个周期函数都可以分解成不同的频率。而傅里叶变换就是把函数变成不同频率的段,并且把这些不同频率的段显示出来。
这是什么意思呢?我们以周期函数 举例,假设 分别由 、、、、 这 5 个不同频率的周期函数组成。然后按照固定的间隔分别对这 5 个函数进行采样:
从图十九我们看出,我们可以根据 的采样点大致还原(把采样点依次连接起来)出 ,而 的话也勉强可以还原出大致形状。但再看 ,根据它的采样点还原出来的形状跟它原本函数表示的形状误差已经很大了;而 和 根据采样点还原出来的形状基本是错误的。
为什么会出现这样的现象呢?我们注意到这里存在两个方面的频率:
- 周期函数本身的频率
- 采样频率
当采样频率跟函数本身的频率接近(或者说匹配)时(比如 ),还原的结果是比较准确的;而如果函数本身的频率高而采样频率过低(比如 ),也就是采样的频率跟不上信号的频率,那还原的结果误差就会很大。
接着我们思考这样一个问题:用同样的采样间隔去采样两种不同频率的函数,有可能得到完全一样的结果吗? 回答:有可能的
图二十:在给定采样率下无法区分的两个不同频率的函数
也就是说,如果用频率去分析走样现象的话,可以描述为:用同样的采样间隔去采样两种不同频率的函数,得出相同的结果。
滤波
滤波的意思就是去除原始信号中某些特定频率的内容。
我们先来看看一个从时域转换(通过傅里叶变换)为频域的真实例子(闫老师视频里说时域只是一个称呼,它包含时间和空间两个域。我自行搜寻资料发现专门用空间域来表示图像空间,这里暂且先 follow 闫老师的说法):
上图中左图为图像的时域图,右图为其频域图。频域图中,中心代表最低频的区域,周围代表最高频的区域,从中心到周围,频率越来越高,通过亮度来区分不同频率的信息量。从这张频域图我们看出,信息基本集中在低频,其实生活中大多数自然图片的信息也都集中在低频。
我们发现在频域图里,中心有一条水平和竖直的”线“,那是因为通常分析一个频域图的时候,我们会认为它是一个周期性重复的信号转换过来的,像上图的频域图,可以看作是其时域图的多个周期的内容转换而来的。而单独时域图几乎没有重复的信息,在它往左右或者上下扩展时,在边界上会发生剧烈的信号变化,在频域图上体现就是会产生一个极其高的高频。
根据上面的内容,也说明了傅里叶变换能让我们看到图像在各个不同的频率长什么样,更通用来说是傅里叶变换可以让我们看到任何信号在不同频率的样子,也就是我们说的频谱。
所谓高通滤波,就是通过应用一种滤波器,使得只有高频的信号可以通过。如下图二十三:
我们发现,经过高通滤波后,图像中一些跟”边界“相关的信息被保留了下来。为什么说这些边界的信息就是高频信息呢?因为我们前面说了,边界往往会引起剧烈的信号变化,而边界也不仅仅是指图像的边界,图像内也存在可明确区分的边界。这些边界所代表的强烈的信号变化在频域图上就表现为高频。
对应的,低通滤波就是把高频信号去除,只让低频信号通过:
经过低通滤波后我们发现,图像变模糊了,我们马上可以联想到,这是因为代表边界的高频信息被去除了导致的。
如果我们把最高频和最低频的信息都去除掉的话会剩下什么呢?
图二十五:去除最高频和最低频信息
类似地,我们可以根据需求不断地尝试各种频率的滤波:
如果需要学习更多滤波相关的知识,需要学习的一门课是《数字图像处理》。
从另一个角度讲,滤波等同于卷积(Convolution),或者说平均(Averaging)。
我们可以把一个滤波器当作一个有固定数量的滑动窗口。比如下图二十七的 Filter,该滑动窗口的三个值代表各自的比例。窗口在信号列表中进行滑动时,我们把窗口中心对应的信号的值更改为该信号相邻的信号与滑动窗口中对应的比例做点乘后的和:
窗口继续往右滑动:
最终,我们可以通过这种加权平均的方法更新整个信号列表,而这一过程,可以简单理解为图形学中的卷积。
卷积理论:
时域(空间域)中的卷积等于频域中的乘积,反之亦然。
Convolution in the spatial domain is equal to multiplication in the frequency domain, and vice versa。
空间域中的卷积可以直接通过滑动窗口(比如下图二十九中是 的窗口)做加权平均去完成:
也可以把空间域和滑动窗口通过傅里叶变换分别转换成各自的频域,然后把频域相乘,最后通过逆傅里叶变换转换成空间域:
在这个例子中,我们使用的是 大小的滤波器,如下图三十一:
它的意思是滑动到某个信号(或者说像素)时,取它周围 的信号(或者说像素)相加做平均后的值取更新。在这里我们思考一个问题:是窗口越大滤波后越模糊,还是窗口越小滤波后越模糊?我们用极端的例子代入下就知道了,假设窗口大小是 ,结果其实就相当于没有做滤波;而如果窗口大小远大于图像本身,那所有信号滤波后的值将会是一样的。所以,窗口越大,图像滤波后越模糊;也即窗口越大,频域图中高频信息被去除的越多,相应地,低频信息保留地就越多:
再论采样
接下来我们探讨下采样与频率的关系,假设我们要对下图三十四 (a) 中的函数 进行固定间隔的采样,使其变成 (e) 中的函数 。那我们可以用 去乘以 (c) 中的冲激函数 ,其中 只有在 t 等于采样点时才有值,且值为 1,当 t 不是采样点时,。也就是可以简单写成:
图三十四:采样与频率的关系(https://www.researchgate.net/figure/The-evolution-of-sampling-theorem-a-The-time-domain-of-the-band-limited-signal-and-b_fig5_301556095)
对应的频域图是 (b) 中的函数 。前面我们说过时域中的乘积等于频域中的卷积。当我们把 (b) 和 (d) 做卷积后,我们可以得到 (f)(这里暂时没有理解是如何运算的)。根据 (f) 我们可以知道,采样其实就是在重复原始信号的频谱。
而采样的间隔是会影响频谱复制的间隔的。它们之间的关系是:如果采样不够快,采样点之间的距离增大,对应频谱上就是频谱复制的间隔变小。而当复制的频谱之间出现混叠的时候,这种情况下就可以说是走样。
反走样(Antialiasing)
反走样的方案:
- 提高采样率(局限性:受制于物理限制)
- 先模糊后采样(低通滤波:把高频信息去除后再采样)
提高采样率在很多情况下并不适用,比如屏幕最大分辨率是固定的;所以我们通常会采用先模糊后采样的方案。那为什么这样可行呢?
由前面的知识我们知道,模糊就是用低通滤波器把高频信息去除。具体在频谱上的表现就是它可以把频谱混叠的部分去除:
这样,就相当于频谱没有发生混叠,也就相当于没有走样。
那么现在重新看前面的图七,回到我们需要解决的问题上:
三角形光栅化的过程中,采样的方法我们前面已经说过了,那要怎样做模糊呢?其实上面也有提到,就是给三角形的包围盒(像素-信号)做卷积操作。
在这个场景中,一个像素有可能全部在三角形内,有可能部分在三角形内。对一个像素做卷积操作,实际上就是计算该像素有多大面积在三角形内,然后根据比例来进行颜色填充:
Multiple Sample Antialiasing
如果直接去计算覆盖面积的话很难,而且运算量大。所以通常我们会通过一个叫 Multiple Sample Antialiasing(MSAA)的方法去近似地进行反走样。
举个例子,如果直接对下面的三角形进行采样的话会得到一个不太好的效果:
而如果我们对于每一个像素,再划分出 的区域,每个像素内的 4 个点都判断是否在三角形内,那我们可以得到:
但其实我们只是在内部计算时扩充了像素,实际上像素的真实数量还是没有变,不过我们可以通过每个像素内的 4 个像素在三角形中的比例,来更新该像素的颜色,最终我们可以得到:
这样,就相当于做了模糊操作,它取得的效果也还不错。
MSAA 的代价
我们对每个像素都进行扩充,以此来实现模糊,很显然这会引入额外的计算量。比如每个像素扩充成 的格子,我们会引入 4 倍计算量,以此类推。但在真实应用的抗锯齿的实现中,消耗其实没有那么大,因为真实应用的抗锯齿的实现,并不是采用这种均匀的格子划分,它们可能会根据实际的场景采用一些不规则划分,甚至复用一些资源。所以如果在游戏中开启抗锯齿的功能并不会导致帧率成倍地掉。
其他的抗锯齿方案
另外两个比较重要的抗锯齿方案:
- FXAA (Fast Approximate AA)
先直接采样,然后通过一些图像处理的算法找到锯齿,之后用没有锯齿的图案替换掉有锯齿的图案。 - TAA (Temporal AA)
跟时间相关,通过合理利用前一帧图像的信息来去除锯齿。
超分辨率
超分辨率跟抗锯齿不太一样,但本质是相似的。简单来说,超分辨率可以理解成需要把一张小的图拉大,比如从 变成 。如果直接拉大,必然会出现锯齿,本质上也是样本不足的问题。而超分辨率通常是通过深度学习(DLSS:Deep Learning Super Sampling)来填充拉大时缺失的信息。