在图形学中,我们经常会遇到变量连续变化的函数:图像就是其中之一。但你会随着图形学的研究深入遇到越来越多的这类函数。这类函数和流式数据类似,不能被计算机直接表示。但我们不管咋样,都得在计算机中用一些比特来表示它。有一个很好用的方法是对这些函数进行 采样 :也就是存储这个函数许多不同点的值,然后在需要函数的时候,通过这些值来还原这个函数。
你现在肯定已经熟悉使用二维网格表示像素的过程了——这其实就是一种采样!想象一下数码相机拍的照片:实际图像是由相机镜头构成的一个连续函数,但相机将它采样为二位网格状的数据。从数学的角度来说,这就是将一个
另外一个采样表示的例子就是二位输入平板,可以在上面写写画画。在这里,原始函数是一个描述运动的函数,也就是
让我们增加一个维度,看看 CT 扫描仪:它能无创的扫描人体,然后得到人身体断层的密度值。扫描仪的输出是三维网格的密度数据:它将身体的密度数据(
看上去这些例子不太一样,但实际上他们都是用的同一种数学方法。所有的例子都将一个函数采样成多维网格的形式,同时,所有例子我们都需要将采集后的采样点重新组合成一个函数。
回到二维图像的例子上来,看上去像素已经够表示一幅图片了,我们也没想过怎么将采样点转化回函数。但,如果你想要缩放图像呢?尤其是非整数倍的缩放,那该怎么办?简单的算法难以完成这些要求,因为它在缩放时很容易产生 走样 。想要了解为什么会产生走样,我们需要走进采样理论的世界。
重建一个连续函数,并不是图形学的专利。它在很多地方得以使用,如数字音频、计算物理等,图形学只不过是其中的一个受益人而已。采样重建理论的基础形成于 1920 年左右,并在 1940 年时产生了我们现在用得到的那些理论。
这一张从一维音频开始,介绍了采样重建理论。然后,我们会讲述采样理论基础的数学和算法,最后,我们会研究频域视角的细节,它能让我们更直观的看到算法的行为。
虽然采样很早就在通讯领域使用了,但直到 1982 年光盘的产生之后,数字音频才开始成为采样的第一“大客户”。
在录音时,麦克风将声音(声音是空气中的压力波)转化为随时间不断变化的电压。这种电信号会以某种方式存储起来,以便以后发送给扬声器重新播放,这时候电压就会在扬声器内,通过与电信号同步,让振膜发出声音,信号重新变成压力波。
录制音频,并将它数字化的方法用到了采样:使用 模数转换器 测量电压信号,一秒测量几千次,这样能够形成一个数据流,可以存储在计算机的磁盘上或其他地方;在播放的时候,数据流以合适的速度进行读取,通过 数模转换器 ,数据信号会转换成电信号,放入扬声器的输入区。
事实证明,能否录制出效果好的音频取决于每秒的采样率。较低的采样率对于录制鼓点可能效果还行,但录制笛子的时候就会听上去有问题,升高采样率就能很好的录制笛声。为了去除 欠采样失真 ,数字录音机会将模数转换器的输入端做 滤波 ,去除输入信号的高频区域,正是它们导致了失真问题。
输出端会出现另外一个问题:数模转换器会在接受采样输入的同时产生电压输出,但直到下一个采样输入前,电压是保持不变的,这就造成了一个类似楼梯的电压图像,这样的电压图像转化成声音就会产生高频噪声,听上去“滋滋滋”。为了去除 重建性失真 ,音频播放器也会进行滤波,平滑波形。
我们上面说的录音机的采样流程和图形学需要的采样很类似。同样地,采样率不够会导致欠采样走样和重建性走样;同样地,解决方案都是在采样之前进行滤波,然后在重建后再进行一次滤波。
过低采样率引发的问题,有一个很直观的解释,请看 [图 9.2] 。这里我们有两个
一旦采样完毕后,只有点留下,我们就不知道这些点到底是高频正弦还是低频正弦的采样结果了,也没法正确的还原原始的函数。这样的情形就叫 “走样” 。
只要采样和重建的频率比较凑巧,那么就会出现走样。在音频中,比如原音频是 10KHz 的铃声,使用 8KHz 采样,就会变成 6KHz 的音调,听起来非常奇怪。在图像中,走样一般以 摩尔纹 的形式呈现。如 [图 9.34] 的百叶窗。
同样的, [图 9.34] 提供了图像走样的另外一种样子:也就是斜线产生锯齿。
理解上面的问题比较容易,但还有一些定量的问题需要探究:
- 多大的采样率能保证较好的成品?
- 采样和重建阶段使用的滤波器用哪种合适?
- 抗锯齿需要用哪种程度的平滑?
等到我们在 [9.5 节] 讲完之后,我们就能回答这些问题了。
讨论采样和重建算法之前,我们先要看看数学基础: 卷积 。卷积是一个简单的数学概念,它是我们采样、滤波、重建操作的基础,也是后面我们分析算法的基础。
卷积是一种函数操作:它接受两个函数,将他们合成一个新的函数。本书中,卷积的符号表示采用
卷积可以应用于连续函数(关于实数
为了方便定义,我们假定了函数定义域是无穷,尽管实际应用中它会在某些地方停止。
为了对卷积有一个简单的印象,请思考一下对一个函数使用滑动平均( [图 9.3] )。我们使用距离
这种思想可以用于离散,也可以用于连续函数。如果用于连续函数,如果我们在对一个连续函数
从另外一个角度来说,如果是离散的函数
你会注意到在例子中,我们使用的分母是经过计算的,这样你在处理常数函数的时候结果肯定也是原来那个常数。
滑动平均的思想是卷积的精髓。他们之间唯一的区别是,在卷积中,滑动平均是以一种加权平均。
为什么我们要在这里讨论卷积? 你可以想想滑动平均的功能:它能平滑曲线,滤除超高或超低的值。
我们从最离散的卷积情况开始:也就是将一个离散序列
[图 9.4] 描述了通过一个滤波序列
如果我们忽略上面的
在图形学中,我们用于计算的函数很可能是 有限支持 的,换句话说,函数只在一块区间内是非零。如果我们假设
然后我们也可以写出伪代码:
function convolve(sequence a, filter b, int i)
s = 0
r = b.radius
for j = i − r to i + r do
s = s + a[j] * b[i − j]
return s
由于我们需要卷积来滤波,所以卷积很重要。回过头看看我们之前讲的滑动平均,你会发现滑动平均也是一种特殊的卷积。当我们对某范围内进行滑动平均,也就相当于通过某个半径内权重相同,半径外权重为
就像这个例子一样,卷积滤波一般会被定义为所有的权重和为
写了这么多卷积的东西,我们发现卷积似乎是 不对称操作 :
这不就是原来的式子吗?所以,我们说有
更广泛地说,卷积是一种 “类似于乘法” 的运算。就像数或函数的加法或乘法,卷积也一样,卷积的结果不会因为改变了项或者括号的顺序而改变。也就是具有 结合律、交换律和分配律 。
这样的性质非常自然而然,而且我们还可以在计算的时候利用这些性质来化简。(想想分配律的组合化简)。
最简单的滤波可以是单位滤波,也就是半径为
尽管在计算机中进行运算的实际上是离散的序列,但那些采样的序列总是被用来表示某些连续函数的。所以我们需要能够计算出这些连续函数。所以,研究离散和连续/连续和连续函数的卷积的很有必要的。
连续函数的卷积公式来自离散的推广:
怎么解释这个公式呢?从图像上看,最后的值代表了
定义
那么
[图 9.11] 展示了这个情况。在
这个函数叫 帐篷函数 ,也是一个常用的滤波器函数。见 [9.3.1 节]
在离散卷积中,我们发现了脉冲序列
看上去,狄拉克函数是类似于在
但狄拉克函数在
由于狄拉克函数的性质,我们得以找到对于卷积运算的“单位函数”。
我们已经发现了一种联系离散和连续的方式: 采样 。通过简单的忽略其他的非整数值,我们可以得到一个序列:
倒着讲,我们将离散序列变成连续函数的方式称为 重建 。我们通过使用另外一种形式的 卷积 来完成重建的过程。也就是我们这一节讲的: 离散-连续函数的卷积 :
书上的内容似乎不太好理解。如果你没看懂,我们来看一个例子:
在物理课上, 我们知道机械波是可以叠加的。我们假设有一个机械波
现在我们假设有
现在情况稍微复杂一点:每个振源的振动强度不一样,由
既然振源和振幅一一对应了,我们可以考虑把它写成一个函数
写到这里,你应该已经发现了,对每个时刻
这样我们就得到了单离散单连续情形的卷积
我们之前一直在讲一维的卷积:单个变量
从离散卷积的定义开始,类似的,我们拓展到二维:
如果
具体图像如 [图 9.16] 所示。
用代码表示如下:
function convolve2d(sequence2d a, filter2d b, int i, int j)
s = 0
r = b.radius
for i_1 = i-r to i+r do
for j_1 = j-r to j+r do
s = s+a[i_1][j_1]*b[i-i_1][j-j_1]
return s
它的几何意义和一维是一样的:每个输出的值是输入值周围一圈的带权平均。这里的 滤波器 则是类似于遮罩一样、罩住需要取带权平均的区域。
所以,类似的,我们也能推广出二维连续函数的卷积公式:
上面的情况都一样,都是某个点的值经过周围值平均后的结果。对于双连续函数的卷积,则是按照周围一圈区域的积分来确定平均的权值。
同样的,我们已经可以轻易推出更高维的情况了。
了解了卷积,我们来看看在图形学中常用的一些滤波器。
下面要介绍的滤波器都有一个固定的半径,它们的半径有一个默认值,便于我们使用。例如,盒式滤波的默认半径是
我们在 [9.4.3 节] 即将说到,一些应用需要不同尺寸的滤波器,这可以通过缩放滤波器得到。我们可以通过定义一个缩放系数
这样,这个滤波函数会在水平方向上被拉伸
盒式滤波是分段的常数函数。离散形式是:
为了对称性,这个方程将端点包含在内。
该滤波器的连续形式是:
那么为什么这个方程的区间不是对称的呢?因为在连续的情形,我们想要对函数进行 重建 ,那么就需要连续的区间,这时边界值需要好好考虑。最后,对于默认半径
同理,帐篷滤波是一种连续的、部分线性的函数:
默认半径是
高斯函数,也被称为正态分布函数,是非常重要的一种函数。之后我们会讲到更多这方面的东西。该函数如下:
参数
但注意,高斯滤波没有默认半径,因为我们可以通过控制标准差
很多滤波器被定义为一些 多项式 。而具有四段(默认半径为
在分段立方体中,B 样条非常的特殊,因为它的一阶导和二阶导 连续 (可以用
这个滤波也是样条滤波,它在
三次 Mitchell-Netravali 滤波器用于解决重采样图像问题,在上述两种三次滤波器中折中改进,这种滤波器是前面两种滤波器的加权组合。数学表达式:
滤波器有一些常用的术语,用来描述或比较。
冲激响应 描述了一个滤波函数对一个只包含单个脉冲的信号的相应(回想一下,对单个脉冲进行卷积,得到的是本身)。
如果一个连续滤波函数被用在一个离散序列上进行 重建 操作,重建出来的函数能经过所有的 采样点 ,我们就称之为 插值 。插值滤波就是那些
如果一个滤波器存在负值,那么在滤波时,会产生 振铃效应 / 过冲 :它会让原函数变化的区域产生额外的信号抖动。比如, Catmull-Rom 滤波 在图像两侧都有负值区域,所以当它被用于一个阶跃函数时,会在阶跃函数的 0 点产生 下冲 ,在 1 点产生 过冲 。
如果一个滤波器用于常数函数,输出的仍然是常数函数,那么我们说它是 无纹波 的。这个概念的定义和全域积分为
之前讲的所有滤波函数,在他们的 默认半径下 都是无纹波的,除了 高斯滤波 。但是他们在非整数的应用场景下并不需要无纹波。如果我们很需要无纹波这一特性,可以这样做:计算结果前全部同除以权重和。
这个表达式也可以写成
连续的滤波函数有一个 连续程度 ,通过高阶导数来描述。一个盒式滤波函数,由于有 间断点 ,所以它不是连续的。一个帐篷滤波函数,是连续的,但它在某些地方斜率突然改变,也就是一阶导不连续,我们称为
对于图像这类的 二维 数据,我们也需要一个滤波器。但我们至今只说过一维滤波器。理论上来说,任意的二维函数都可以作为二维滤波器,有些时候这样定义反而比较简单,但是,大多数情况下,我们构造二维滤波器的方式是通过 一维滤波的组合 实现的。
最好用的方法是使用 可分离滤波器 。对于一个可分离滤波器
离散情况也一样:
我们选的是帐篷函数当作
轴方向 的坐标形成的是帐篷函数,但 对角线方向 是 二次函数 。比如说,我们在
同样的,选取高斯函数作为
其实你会发现,将一维高斯滤波绕着
对于其他的二维滤波,可分离的滤波有一个明显的优势:容易实现,效率较高。下面说说为什么效率高:
我们先把可分离滤波
我们发现
将后面的求和缩写为
为啥要这样化呢? 这样化出来,我们能够将
举个例子:如果我们要计算在
对于大型的滤波器,这样能省非常多的性能, 相当于脱掉了一层循环 。
算法伪代码如下:
function filterImage(image I, filter b)
r = b.radius
n_x = I.width
n_y = I.height
allocate storage array S[0 ... nx−1]
allocate image I_out[r ... n_x−r−1,r ... ny−r−1]
initialize S and I_out to all zero
for j=r to ny−r−1 do
for i_1=0 to nx−1 do
S[i_1]=0
for j_1=j−r to j+r do
S[i_1] = S[i_1] + I[i_1, j_1]*b[j − j_1]
for i=r to nx−r−1 do
for i_1 = i−r to i+r do
I_out[i, j] = I_out[i, j] + S[i_1]*b[i − i_1]
return Iout
为了简洁,算法省略了边界问题。实际上边界问题有多种解决方法,见 [9.4.3 节] 。
我们讨论了采样、滤波、和重建,但都是空谈,而且大多数是针对一维信号。这节我们终于可以开始将图像这样的二维数据处理了。
最简单的卷积应用可能就是使用离散卷积处理图像了。而且大多数广泛使用的滤波器都是简单的卷积滤波器。我们可以通过 低通滤波器 实现图像的模糊操作,比如盒式滤波,
模糊的反义词是 锐化 。那么我们怎么做呢?我们可以做原先模糊的“反操作”。鉴于模糊是将周围的值加到中间,锐化滤波就可以将中间的值减去周围的值。我们将相减的系数设为
其中
另外一个使用两个滤波器组合的应用是给图像落下阴影。在落下阴影的过程中,我们需要拷贝原图像的信息,然后做一个模糊的外围环绕阴影。对于“外围”的实现,我们可以做一个 偏离的脉冲函数 :
然后,将两者组合(我们使用高斯滤波):
这样我们就能使用一个函数(接受三个变量)处理三个滤波做的事情。
在图像合成中,我们经常需要对具有连续数学性质的图像进行采样,然后输出各个点的颜色,最经典的例子就是光线追踪。 [第 4 章] 讲述了光线追踪的例子以及对光线追踪实例进行抗锯齿的具体步骤。用我们这节课的术语来说,我们有一个二维 信号 (也就是图像),然后通过 采样 生成一个二维网格。如果我们就这样下去,不采用特殊操作,那么就会遇到各种各样的 混叠 ,在图像的比较尖锐的边缘就会出现 锯齿 。而在重复出现的纹路上,会出现 摩尔纹 。见 [图 9.34] 。
为什么会出现这样的问题?原因是在这里有太多的 小尺寸细节 。如果不进行平滑,那么采样的频率就会跟不上细节的频率,造成混叠。所以为了防止这种视觉问题,我们需要在采样前进行 平滑处理 。见上图 [图 9.34] ,使用简单的盒式滤波仍然会产生一些摩尔纹,但高斯滤波非常平滑,可以有效防止摩尔纹的产生,但要付出更加模糊的代价。上述方法有效说明了,人们一直在做 锐度 和 混叠 之间的斗争。
在对图像做滤波时,我们需要仔细留意 重采样 操作,也就是改变采样率 ,或者说,改变图像尺寸。
假设我们已经用相机拍了一张
一种办法是将这种缩小尺寸的过程看作是扔掉一些不需要的像素。我们发现缩小比例是两到三倍,所以一个像素旁边的一个或两个像素应该被丢弃。这样是可行的,但是你会发现图像质量很差。 [图 9.34] 其实用的就是丢弃像素的方法。虽然效果不会,但丢弃像素运行的非常快,适合生成缩略图。
其实,我们可以将缩放图像的过程看成 重采样 的过程。我们将这个图像的“函数”进行连续化,然后再以我们想要的采样率进行重采样。见 [图 9.36] 。为了防止混叠,我们会在各个步骤时使用合适的滤波器。
[图 9.37] 展示了一个例子:原始的图像是
为了能在后续阶段正确采样出样本,我们需要能够计算样本之间的像素值应该取多少。 丢弃像素法 给了我们一个很好的思路:也就是寻找离得最近的一个原图像的像素,新的采样点的像素值就是它了。其实,这样操作和 使用一个半径为 1 的盒式滤波器 是一样的效果。
如果我们这一节说的是依据性能选择滤波器,那么这个方法肯定能稳坐宝座了。可惜不是。实际上,在整个图像中使用盒式滤波存在一些困难。但对于更高质量的重采样,重建/采样的过程框架提供了宝贵的灵活性。
我们先在一维讨论算法问题,然后将其推广,这样比较简单。我们在 [9.2.5 节] 说过 重建函数 ,这里我们直接利用它:
function resample(sequence a, float x0, float dx, int n, filter f)
create sequence b of length n
for i=0 to n−1 do
b[i] = reconstruct(a, f, x0 + i*dx)
return b
这里的变量
这个过程相当于通过连续的滤波器重建了一个连续的图像,然后用点来对这个连续图像进行采样。其实连续的图像并不存在,只是存在于我们定的规则之中,但实际上来说,这个函数计算了
好像不太对...我们忘记了在采样之前进行模糊化处理! 正确的过程应该是:我们将重建函数与滤波器
我们进行重采样时,会在旧图像中指定一个 源矩形 ,用来确定新图像中需要保留旧图像的哪部分内容。例如,利用我们第三章讲过的 像素位置 ,假如我们想要重采样的图像大小为
我们使用伪代码给这个方法套上两个循环,可以写出:
function resample(sequence a, float xl, float xh, int n, filter f)
create sequence b of length n
r = f.radius
x_0 = x_l + dx/2
for i = 0 to n − 1 do
s = 0
x = x_0 + i*dx
for j = ceil(x−r) to floor(x+r) do
s = s + a[j]*f(x − j)
b[i] = s
return b
这个例程包含了重采样一个图像的所有基础操作。但还有最后一个问题,那就是在图像的边界我们应该做点什么。有一些办法,如下:
- 如果序列到头了,就停止循环——这等价于将图像周围以全
$0$ 像素填补。 - 如果序列超出界限,使用序列的最后一个值作为替代——这等价于在图像的周围做了一圈与最边缘颜色相同的填补。
- 定义滤波器在图像边缘的行为,彻底解决这一问题。
第一种方法会让图像边缘部分变暗,所以不太好用。第二种方案比较容易实现。第三种应该是效果最好的。自定义边缘滤波器可以有一个简单的定义法
:那就是 重新归一化 。简单的将包进来的有效像素取平均,忽略无效像素。这样,滤波器最后的均值依旧为
选择一个好的重采样的滤波器是很重要的。什么是好的滤波器?我们需要回答下面两个问题:我们需要的滤波器形状和半径是什么?由于滤波器在采样和重建阶段都会用到,所以这两个变量的取值很重要。对于重建的任务,我们想要的是一个尽可能平滑的模糊滤波器,能起到反走样的效果;对于采样的任务,我们想要滤波器足够大,以防止采样不足的问题,同时也需要平滑,这样可以防止摩尔纹。 图 9.39 显示了两种不同的需求。
总之,我们会选择一种滤波器形式,然后将它按照输入和输出的图像大小进行各自的缩放。下面两种分辨率决定了滤波器的尺寸:当输出比输入更 稀疏采样 ,也就是我们将缩小图像( 降采样 )时,那么我们对于采样所需的模糊度就比重建滤波器的模糊度要求 更高 。所以我们按照输出图像的间隔(在 [图 9.39] 中是
选择滤波器本身就是一种速度和质量之间的权衡。一般来说,需要速度就用 盒式滤波 ,两者都需要就用 帐篷滤波 ,需要质量就使用 分段三次滤波 。在分段三次滤波中,平滑程度可以通过
正如图像滤波,分离式滤波器可以显著提高速度。分离式滤波器的基本思想是先将所有 行 进行采样,然后得到一个宽度改变但高不变的图像;然后再将图像的 列 进行重采样,得到最终结果,见 [图 9.40] 。我们可以很容易的修改前面的伪代码,然后将这样做带来的好处收入囊中。
如果你只想看看实现的过程,那么就别往下看了!你已经可以使用之前讲过的东西写出很好的代码了。然而,对于通信方面的知识,有好多更深的内容,通过数学理论来阐述。采样理论就是对通信方式的一种严格证明和推导。
最主要的是,采样理论给了我们在图形学的采样和重建领域非常牛逼的一些观点。它能让学生理解,如何使用最优的代码达成最好的效果。
采样理论最基础的两个理论基础,就是 傅里叶变换 和 卷积 。你可以在很多地方读到傅里叶变换的介绍。
傅里叶变换的基本原理就是:任何函数都可以表示为一系列正弦波(可以是不同频率)的和。使用合适的正弦波做叠加,我们就能得到任意一个函数。
举个例子, [图 9.41] 中的方形波可以通过下面的序列来表示:
这个 傅里叶序列 从频率为
但很神奇的是,信号 不一定 需要有周期,也可以由一系列正弦波组成。只是它需要更多的正弦波来表示:要对无周期性的函数进行傅里叶变换,我们就不能使用一系列离散正弦波相加的方式了,而是需要对一簇正弦波进行 积分 。例如,盒式滤波函数可以由一簇余弦波积分而成:
上述公式是相加了无穷个余弦波的结果,见 [图 9.42] ,最后合成了一个盒式滤波函数。当一个函数
上式称作 逆傅里叶变换 ,因为它是之前那个式子的反过程。
对了,你肯定很奇怪为什么会有
通过
我们终于能给它一个名字了,那就是 傅里叶变换 。这俩的区别就是指数上的符号改变了一下。我们可以将这傅里叶变换和逆傅里叶变换都看成一种操作。
有些时候
傅里叶变换有很多有用的性质。我们接下来会讨论的有:
- 一个函数和其傅里叶变换后的函数 的平方 的积分相同 。
物理解释是:他们具有相同的能量。 见 [图 9.43]
-
特殊地,傅里叶变换对数乘可交换,也就是
$\mathcal{F}{ af} = a \mathcal{F}{ f}$ 。 -
将一个函数沿
$x$ 轴方向进行伸长,相当于对其傅里叶变换后的函数在$u$ 轴上压缩,也就是 [图 9-44] :
上面的数乘
$b$ 是为了能量在转换后不变。
这个性质告诉我们,如果我们对一簇函数感兴趣(例如以原点为中心的一系列盒式滤波函数),那么我们只需要知道其中一个函数的 标准化 傅里叶变换(也就是这个盒式滤波函数的宽和高都是
- 原函数的平均值
$\rm avg(f)$ 与变换后函数零处的值$\hat{f}(0)$ 相等。
所以在频域,0 频率也被称为直流分量
- 如果
$f$ 是 实函数 ,那么$\hat{f}$ 就是一个 奇函数 。反之亦然。
我们用不太到这个性质,因为图形学处理的几乎都是实函数。我们关心的是
$\hat{f}$ 的大小。
傅里叶变换最后一个值得一说的性质是它与卷积的关系。简单地说:
也就是说,对两个卷积起来的函数进行傅里叶变换会得到两个傅里叶变换后的函数的 乘积 。类似的,也有:
对两个傅里叶函数的卷积会得到两个 函数乘积的傅里叶变换 。
这些性质都能从定义中简单推导出。而它们之间的关系正是傅里叶变换在采样原理中如此有用的原因。我们已经知道采样、滤波、重建可以看成是不同形式的 卷积 ,但现在傅里叶变换又给了我们另外一个 域 —— 频域 。在频域中,卷积就是简单的乘积。
看完了介绍,我们来看一些例子。尤其是我们在 [9.3.1 节] 讲过的滤波器例子,它们的傅里叶变换在 [图 9.46] 中呈现。
我们已经见过了 盒式滤波函数 的样子:
函数
$\sin x / x$ 非常重要,我们将它称为${\rm sinc} \ x$ 。
同时,请注意上面的函数
$\frac{\sin \pi u}{\pi u}$ 在$u=0$ 的时候是 不存在 的,但这个函数在 零点连续 。我们可以将这个点的值视为 极限值,1 。
帐篷函数就是盒式滤波函数自身的一次卷积。所以,傅里叶变换后就是上面的变换后盒式滤波的 乘方:
我们继续!B-样条滤波函数因此就是四次卷积:
高斯滤波进行傅里叶变换后的样子很神奇:
这是另外一个高斯函数!只不过 标准差 从原来的
我们可以在连续函数和它的傅里叶变换中,使用脉冲来表示样本们。比如一个在
这样,对一系列均匀分布的点坐标所对应的函数值位置 进行采样 的这个操作,就可以表示为对这个函数乘以一系列均匀分布的脉冲,这个脉冲因此被称为 冲激抽样 。见 [图 9.47] 。一个周期为
对于
由于傅里叶变换的 尺度变换特性 ,我们可以推测出一个周期为
我们现在已经有了一些数学基础,我们来试着理解一下 频域 内的采样和重建。傅里叶变换最牛逼的地方就在于它让信号的卷积过程变得更加清晰明了,并且更加清晰的解释了为什么我们在采样和重建需要 滤波 。
先从原始的、连续的信号开始研究。一般来说,对连续函数进行傅里叶变换后,会包含所有不同频率的部分,但对于某些信号(尤其是图像)来说,我们希望频率越高的地方,包含的信息越少,这样能尽量避免混叠。同时,图像也有很大一部分在
我们看看如果我们在采样和重建的过程中啥也不做,会发生什么。见 [图 9.48] ,我们先对信号采样,在操作上来说,也就是将一个冲激抽样乘以该函数。这样我们能得到
回想一下狄拉克函数,它在卷积中起到 “单位函数” 的作用 。这意味着:
也就是说,进行傅里叶变换后的函数
为什么会产生这一问题?其实是因为信号的频谱在 一半的采样频率 之外还有一些值(高频分量)存在。当存在这些高频分量时,每一次的采样,叠加,都会将这些高频分量叠加一遍,而且这样的操作 不可逆 。这就是 第一类混叠 。如果因为这个原因发生了混叠,聪明的你肯定想到是什么原因了:因为 欠采样 。也就是,对信号进行采样的频率太低了。
想想为啥是“一半的采样频率”?
假如我们使用 最近邻方法 重建信号。也就是使用一个
要做高质量的采样和重建,我们已经明白为啥一定需要选取合适的采样和重建滤波器了。从频域的角度来说,使用 低通滤波器 的意义在于限制 频域范围 ,这样那块 混叠频域 就不会和 基础频域 重叠。 [图 9.49] 描述了采样率高低对(经过傅里叶变换后的)频谱造成的影响。更高的采样率能让混叠频域相隔更远,这样再怎么样混叠产生的噪声都不会很明显了。
回忆一下为什么采样率高了反而更远? 因为对于冲激抽样,它的傅里叶变换周期是原周期的倒数。
最主要的就是,频谱必须在一份份采样中 “不相互交叉” 。那就是著名的 奈奎斯特采样定理 :采样频率必须两倍于信号中的最高频率,或更高。 奈奎斯特-香农采样理论指出,一个满足上述准则的信号,可以正确的从样本之中重建出来。
如果我们的采样率实在是特别高,也就是两倍以上了,那么我们就完全不需要使用采样滤波器了。但如果我们处理的信号有很宽的频域(有明显边缘的图像等),那么我们就必须在采样前先对其进行滤波。 [图 9.50] 展示了三种低通滤波器( 平滑滤波 ) , [图 9.51] 则展示了使用不同滤波后的频谱图像。就算频谱会产生混叠,使用低通滤波器已经足以收窄频域,形成一个令人满意的采样结果了。当然,这样我们会 丢失图像的高频信息 。但在混叠的问题前相比,这点信息丢掉更好。
从频域的角度来看,重建滤波器的工作应该是去除掉混叠频域的内容。在 [图 9.48] 中,我们看到了最简单粗暴的重建滤波器——盒式滤波,他能一定程度上减弱混叠频域的强度。更重要的是,它完全阻挡了混叠频域中的 直流尖峰 。这正是我们所需要的重建滤波器:该滤波器的频域中,与采样频率倍数对应的值都是
所以一个好的重建滤波器应该是一个好的 低通滤波器 ,同时能阻隔所有是采样频率倍数的信息。盒式滤波不是那么令人满意,因为它不能做到将混叠频域完全消灭。我们需要一个:消除混叠频域、减少高频泄露、尽量对基础频域干扰小一点的滤波器。 [图 9.52] 展示了重建时不同滤波器的作用。我们可以看到,效果应该是 盒式滤波 < 帐篷滤波 < B-样条滤波 。但 B-样条滤波也将基础频域做了一定的平滑。这就是我们之前提到,在平滑和混叠之间权衡的问题了。
当重建和采样两个操作在 合并 成重采样过程之后,尽管这些操作和数学法则都没变,但它们两个操作就只能使用同一个滤波器了。 [图 9.53] 展示了重采样滤波器是如何工作的,它需要消除混叠频域, 并且 让频谱尽可能的窄,才能保证重采样的采样率不会过低。
根据上面的结论,一个盒子形状的滤波器可以作为采样和重建的理想滤波器。这样的滤波器可以在两个阶段都起到避免混叠的作用,同时不会让低于 奈奎斯特频率 的那些频率消失。
回忆一下傅里叶变换和逆傅里叶变换,它们其实是理论上相同的过程,所以空间频域上呈现为盒子形状的滤波器,它的傅里叶变换后的函数是
然而我们在实际应用中,不管是采样还是重建都不常使用
对于采样,
对于重建,
要使对象看起来更真实,使用着色即可,也就是使用光来“绘制”表面。本章将介绍最常见的启发式的着色方法。前两个是漫反射和冯-着色,它们是在 20 世纪 70 年代开发的,在大多数图形库中都可以使用。最后一种是艺术着色,它使用艺术惯例为对象指定颜色。这会让人想起工图,在许多应用中都挺有用的。
世界上许多对象的表面,外观可以简单的描述为“无光泽”,这说明该物体没有光泽。例如纸张、未加工的木材和干燥的未抛光的石头。在很大程度上,这样的对象的颜色不会随着视点的变化而变。例如,如果你盯着一张纸上的一个点,你的人在移动,但你的目光固定在该点上,该点的颜色将不会改变。这样的无光泽物体可以被视为 兰伯特对象 。本节会讨论如何实现这类对象的着色。一个关键点是,在应用透视变换后,本章中的所有公式都应在世界坐标中进行计算,而不是在扭曲坐标中进行计算。否则,法线之间的角度将更改,造成不准确的着色。
兰伯特对象的着色遵循兰伯特余弦定律,这个定律规定了曲面的颜色
向量形式是:
[图 10.1] 展示了
通过改变光源的强度或表面的反射率,可以使物体表面变亮或变暗。漫反射系数
上面这个方程的右边是一个 RGB 颜色,每个 RGB 分量的值区间是
这是一个很方便的写法,但它仍然可能导致 RGB 分量越界,因为向量的点积可能是负数。如果乘积为负数,那么说明物体是背对光源的,不应该收到任何光,所以我们将公式改进为:
或者如果你需要 双面照明 ,你可以使用如下公式,尽管看上去好像不太符合物理常识:
上面提到的最大值光照方程(漫反射着色方程)的一个问题是,它的法线面背对着光源的所有点都将是黑色的。在现实生活中,光是四处存在的,不会存在纯黑,此外,通常会有天窗提供“环境”照明。处理这种情况的一种方法是 使用多个光源 。一个常见的技巧是始终在相机(眼睛)处放置一个暗淡的光源,以便所有看得到的地方都能接收到一些光线。另一种方法是使用上一节所述的双面照明。更常见、更好的方法是 添加环境项 。这是添加到上面方程中的一个恒定颜色项:
其实,你可以将环境光颜色看成是场景中所有曲面的平均颜色。如果你要确保计算的 RGB 颜色保持在范围
如果我们将本章第一个方程应用于由 三角形 组成的物体上,它看上去就会有很多小面。但我们实际上需要的是用三角形近似表示曲面,所以为了防止出现一堆小面的外观,我们可以将表面法向量放在三角形的顶点上,然后对各个顶点计算光照(见 [图 10.4] )这样会在三角形的每个顶点上产生一种颜色,然后我们就可以使用重心坐标对三角形中间的颜色进行 插值 。见本书[8.1.2 节]。
三角形顶点着色有一个问题,那就是我们需要获取法向量。其实许多模型都会提供法线。如果你想细分自己的平滑模型,那么可以在创建三角形时就创建法线。如果呈现的多边形模型在顶点处没有法线,并且你还希望能平滑着色,那么就可以通过一些启发式方法计算法线。最简单的方法是:平均三角形的每个顶点的法线,并在顶点处使用该平均后的法线。不过平均后的法线不一定是单位长度,因此在将其用于着色之前,应先 单位化。
有些表面本质上虽然类似于哑光表面,但它们也具有 高光 。这类表面的例子有磨砂瓷砖、光泽油漆和白板。随着视点的移动,高光也会在曲面上移动。这意味着我们必须在方程中加入一个朝向相机的单位向量
我们想在正确的位置添加一些与光源颜色相同的模糊“光点”。如 [图 10.5] 所示,点的中心应绘制在眼睛方向
给定一个
但如果你真用了这个方程,你会发现两个问题:一个是 点乘积可能是负数 ,这可以添加一个 if 语句来判定和解决。但另外一个问题更严重:这样产生的高光比现实的高光 大得多 。虽然光强最大的地方是对的,饭范围实在是太大。所以我们找到一个好方法,给式子添加一个平方项,这样可以减小光照范围:
这里指数
为了完善等式,我们首先要计算单位向量
这里点乘积是用来计算
还有一种神奇方法,我们上面算出来的冯-着色公式需要判断正负值,这种方法省去了这个麻烦,我们也不用计算
这样,如 [图 10.8] 计算光线的问题就转变成计算半程向量和表面法线之间角度的问题了:
同样的,这里的
在实际应用中,我们想要大多数材质除了具有高光外,还具有漫反射的外观。我们可以将之前讲过的两个方程结合起来吗,得到:
如果我们要让高光的亮度可调整,就可以加入一个控制项
与几何相同的兰伯特表面相比,具有高光的平滑曲面往往会快速改变颜色。因此,法向量处的着色可能会产生伪影。因此我们可以在多边形上对法向量进行插值,然后在每个像素处应用不同向量的冯-着色,这样就能减少这些问题。这种方法让在三角面不小的几何体也能获得良好的渲染效果。
回想一下 [第三章] 的内容,当我们对一个三角形进行光栅化的时候,需要通过
相似的,表面法向量也可以这样插值:
这样插值过后,上面计算颜色的最终公式就可以用于每个像素了,它们计算出来的颜色就会不一样。注意,我们计算出的
兰伯特着色 和 冯-着色 方法基于启发式的方法,是想要模拟真实世界中物体的外观。艺术化着色注重的是模仿人类艺术家的绘画。这种着色方法在许多应用中似乎都更有用。例如,汽车厂商会让艺术家为车辆说明书绘制图例。但这比使用“真实”的照片要贵得多,因此当需要一些特定类型的渲染结果时,艺术家的着色方法就会更好。在本节中,我们将展示如何画出描边画,让画看上去人类绘制的图像。创建这样的图像通常称为 非照片级真实感渲染 ,但我们将避免使用该术语,因为许多非照片级真实感技术这个词现在被用烂了。
本节将 silhouette 翻译为描边,实际上在本章的前半段主要讲的是针对有阴影的边界的描边(剪影描边),后半是介绍多面体描边。
在绘画中,我们能发现和现实世界不一样的地方在于 描边 ,如果有一组三角形有公共边,且一侧的三角形对着观察者,另外一组背对着观察者,我们会画一条边,让它们隔开。这种情况用向量描述就是:
在这里
我们还希望能够将多面体的可见边画出来。为了实现这个功能,我们可以使用 [第 12 章] 的隐藏表面方法,先渲染背景颜色,再将每个三角形的边描黑。这个方法其实也可以画出剪影描边,但对于一个光滑的表面,我们不需要画出黑边,不然就一坨黑了。尽管我们想要在大于某个角度的时候绘制 折痕 。我们可以设置阈值,绘制折痕 (crease) :
当画家给线条着色时,他们通常使用暗光着色,这样可以给表面留下一些有起伏的感觉,接着再为物体绘制颜色。朝向一个方向的表面将使用冷色调(如蓝色)进行着色,而朝向相反方向的曲面将使用暖色调(如橙色)进行着色。通常这些颜色不会过饱和,也不会过暗。这样,黑色的轮廓就能很好地展现出来。总的来说,这实现了卡通般的效果。要实现这样的效果,可以设置“暖”色光
颜色
有挺多的可用
[图 10.9] 展示了传统 冯-着色 和这写 艺术家着色 的区别。
-
这一章介绍的阴影看上去都是假货,不是真正的阴影。这样说对吗?
其实是对的。然而,它们的生成方法是经过精心设计的,而且在实践中证明是有用的。从长远来看,我们可能会有更好的动机算法,包括物理、心理学和色调映射等。然而,图像质量的改善可能得慢慢来。
-
我讨厌使用乘方 pow() 函数。在冯-着色中怎么避免用它呢?
一个简单操作是只是用 2 的倍数进行乘方。实际上,对于大多数应用程序来说,使用 pow 并不会怎么样。当然你也可以使用查找表,但一般来说快不了多少。