折回来的鬼
采样率不够的时候,大部分人预期的结果是「高频变模糊」或者「高频没了」。都不是。高频既不模糊也不消失,它换一个身份走进低频区,然后住下来——伪装得完美到,事后再高明的算法也认不出它是外来户。这一章给出这本书里最不讲道理的一组数。
两段完全不同的声音:一段是 1400 Hz 的纯正弦,一段是 400 Hz 的纯正弦。放给人听,一个尖,一个闷,谁都分得出来。
现在用 1000 Hz 采样率去采它们(注意:1000 Hz 对 1400 Hz 来说远远不够,奈奎斯特只到 500 Hz)。各采一千个数。
问:这两串数,最大差多少?
先看这组数
fs = 1000 Hz,各采 1000 个点
a[n] = sin(2π · 1400 · n/1000)
b[n] = sin(2π · 400 · n/1000)
✓ 逐点最大差 ⟨2.39 × 10⁻¹²⟩
严格逐位相等的点数 1 / 1000
(其余 999 个点差在 10⁻¹² 量级,是 sin 的舍入,不是信号的差别)
而这两个信号在连续时间上呢:
✗ 最大差 1.9768 ← 峰值只有 1,这已经是最大可能的差别
连续时间上,它们几乎处处不同,差到不能再差。在采样点上,它们是同一串数。
为什么:又是整数圈
这件事在纸上只有一行:
在采样时刻 t = n/fs 上,把频率加上 fs:
sin( 2π(f + fs) · n/fs )
= sin( 2πf·n/fs + 2π·n )
= sin( 2πf·n/fs ) ← 因为 n 是整数,多转的正好是 n 整圈
于是:频率相差 fs 的整数倍的两个正弦,
在采样点上给出**完全相同**的一串数。
1400 = 400 + 1000 → 采出来和 400 Hz 一模一样。
又是「整数圈」。第 3 章那条恒等式的三个前提里,第二条(m 不是 N 的倍数)在这里被破坏了:1400 Hz 的圈和 400 Hz 的圈,在这个采样格点上根本不是两个圈,是同一个圈。
这也是「别名」(alias)这个词的来历——它不是一个近似值,是同一个东西的另一个名字。
采样不足的时候,高频不会消失,它会以一个低频的身份出现在你的数据里,并且和真的那个低频完全不可区分。
「不可区分」这四个字是字面意思:不是难分辨,是信息论意义上没有区别。你手里那串数对应无穷多个原信号,采样这个动作已经把它们的差别抹平了。任何声称能事后分开它们的算法,都只是在偷偷引入额外假设。
折回来的落点:一张表
高频会折到哪儿?规则是:先对 fs 取余,如果超过一半就用 fs 减掉它。形象地说,频率轴在 0 和 fs/2 这两处装了镜子,超出去的部分被来回反射回来:
fs = 1000 Hz,奈奎斯特 = 500 Hz 真实频率 采出来长什么样 ────────────────────────────── 100 Hz → 100 Hz ✓ 本来就在范围内 400 Hz → 400 Hz ✓ 600 Hz → ⟨400 Hz⟩ ← 折了 900 Hz → ⟨100 Hz⟩ 1100 Hz → ⟨100 Hz⟩ 1400 Hz → ⟨400 Hz⟩ 1600 Hz → ⟨400 Hz⟩ 1900 Hz → ⟨100 Hz⟩ 2400 Hz → ⟨400 Hz⟩ ────────────────────────────── 注意 400 / 600 / 1400 / 1600 / 2400 全都折到了同一个地方。 它们在你的数据里已经加成了一个数,加法不可逆。 换成 CD 的采样率 44100 Hz,奈奎斯特是 22050 Hz: 一个 30000 Hz 的成分 → ⟨14100 Hz⟩ 正好落在人耳最敏感的区段附近
这张表解释了一个常见的困惑:为什么混叠产生的假频率听起来那么难听。因为它和真实的谐波结构完全没有关系——一个 3000 Hz 的谐波折回来可能落在 1100 Hz 上,而原声里 1100 Hz 上什么都没有。它是一个纯粹外来的音,和其他音之间没有任何和谐关系。这就是低码率数字音频那种「金属味」的一部分来源。
谱上看得更直接。以 1000 Hz 采 1400 Hz,做一次变换:
1000 点的变换,找最高的那根谱线: 峰落在 bin 400 = 400.0 Hz 而 1400 Hz 那个位置上:什么都没有(它根本不在这台机器能表示的范围里)
银幕上那些倒转的车轮
这本书里最容易亲眼看见的一次混叠。电影每秒 24 帧,也就是以 24 Hz 采样这个世界。一个有 12 根辐条的轮子,转过 1/12 圈就和没转一样——于是「辐条经过某个位置」这件事的频率是转速的 12 倍。
12 根辐条,24 帧/秒 隐形周期 = 24 / 12 = 2.0 转/秒 ← 每转到这个速度的整数倍,轮子看起来就是静止的 真实转速 银幕上看起来 ───────────────────────────── 1.6 转/秒 → ⟨−0.40⟩ 转/秒 倒着慢慢转 1.8 转/秒 → ⟨−0.20⟩ 转/秒 倒得更慢 2.0 转/秒 → 0.00 转/秒 完全静止 2.2 转/秒 → 0.20 转/秒 正着慢慢转 3.9 转/秒 → ⟨−0.10⟩ 转/秒 又开始倒 4.0 转/秒 → 0.00 转/秒 又静止了
请注意 1.6 → −0.40 这一行:轮子明明在正着转,银幕上却在倒转。这不是错觉,也不是快门的问题——那是数据本身在说谎,而且说得毫无破绽。如果你只有这段视频,没有任何别的信息,你无法判断那个轮子到底往哪边转。
顺带一提,日光灯下用肉眼也能看到这个效果:日光灯每秒闪 100 次(50 Hz 交流,一周期亮两次),于是它对高速旋转的东西做了一次 100 Hz 的采样。老式车间要求机床附近用白炽灯或者错相位的多路日光灯,防的就是这个——一个高速旋转的刀盘在日光灯下可能看起来是静止的。
抗混叠滤波器(anti-aliasing filter)这个名字容易让人以为它是「用来消除混叠的」。不是。它是用来防止混叠发生的,而且必须装在采样之前,是一个模拟电路。
已经混进去的东西,没有任何数字滤波器能拿出来——数据里那个 400 Hz 的成分,是「真 400 Hz」和「折回来的 1400 Hz」的和,而你不知道各占多少。这一条在图形学里同样成立:一张已经出现了摩尔纹的照片,事后做任何处理都不可能恢复出原来的布料纹理。抗混叠必须在采样的那一刻完成,事后是不可逆的。
唯一的例外是你能重新采一次——比如换一个采样率再采一遍,两次的折叠位置不同,联立就能解出来。这正是医学成像和射电天文里「多速率采样」的思路。但它需要重新访问那个物理现象,不是对旧数据做处理。
三行看见这一章的全部内容:
import math fs = 1000 a = [math.sin(2*math.pi*1400*n/fs) for n in range(1000)] b = [math.sin(2*math.pi* 400*n/fs) for n in range(1000)] print(max(abs(x-y) for x, y in zip(a, b))) # 2.39e-12
折叠规则也是三行:
def fold(f, fs):
m = f % fs
return m if m <= fs/2 else fs - m
for f in [100, 400, 600, 900, 1100, 1400, 1600, 1900, 2400]:
print(f, '→', fold(f, fs))
如果你有耳朵可用,这个实验值得真的听一次:用 Audacity 生成一个从 100 Hz 扫到 20000 Hz 的扫频音,把工程采样率设成 8000 Hz(不要勾选任何重采样质量选项),你会听见音调升到 4000 Hz 之后掉头往下走,来回反弹。那正是上面那张表在耳朵里的样子。
python3 -c "import math;fs=1000;print(max(abs(math.sin(2*math.pi*1400*n/fs)-math.sin(2*math.pi*400*n/fs)) for n in range(1000)))"
在线跑:python.org/shell。想看图:Desmos 上画 sin(2π·1400x) 和 sin(2π·400x),再把 x 限制在 n/1000 这些点上。
- 摩尔纹。拍屏幕、拍格子衬衫时出现的那些彩色波纹,就是二维的混叠:布料的高空间频率被传感器的像素网格折回成了一个低频图案。相机上那块「光学低通滤波器」(OLPF)就是抗混叠滤波器的光学版——它故意把画面弄糊一点点。近年不少相机把它去掉了,换取锐度,代价就是偶尔出摩尔纹。
- 游戏里的 MSAA / FXAA。「抗锯齿」这个名字翻译得不太好,它英文就是 anti-aliasing。锯齿的成因和摩尔纹完全一样:几何边缘是无限高频的,而屏幕像素是一次采样。MSAA 的做法是「采样前先多采几次再平均」,本质上是在采样点上做局部积分。
- 数字示波器上的假波形。用 100 MSa/s 的示波器看一个 60 MHz 的方波,屏幕上会显示一个漂亮的、频率完全不对的低频波。示波器不会报警,因为它没有办法知道。这是入门工程师最容易栽的一个跟头。
- 业务数据的周期陷阱。「每周一取一次数」对一个有周内波动的指标来说是 1/7 Hz 采样;如果再叠上一个双周发版节奏,两个周期一混叠,就会在报表上长出一条根本不存在的「季度趋势」。周期性数据的采样间隔,永远要先问一句它和已知周期是什么关系。
「采样率低一点没关系,先采下来,事后用数字滤波器把那些假的高频滤掉就行。」
滤不掉。折回来的东西不在高频区了——它就在低频区,和真信号并排坐着,长得一模一样。你的数字滤波器只能看到「400 Hz 处有多少能量」,看不到这些能量里有多少是外来的。
上面那组数就是证据:两串数最大差 2.39 × 10⁻¹²。任何算法拿到的输入都是同一份,输出当然只能是同一个。这不是算法水平问题,是信息已经不存在了。
正确的顺序永远是:先在模拟域把高频砍掉,再采样。这也解释了为什么高端 ADC 常用「过采样 + 数字抽取」——先用远高于需要的采样率采(这时模拟滤波器可以做得很温和,因为过渡带极宽),采完在数字域用一个陡峭的滤波器把高频砍干净,再降采样。数字滤波器可以很陡,代价是延迟;模拟滤波器要陡就很贵。过采样是拿存储和算力去换模拟电路的钱。
判据是:问「这个高频成分,在它被记下来之前有没有被处理过」——没有的话,你后面做什么都晚了。
正确答案是 C:最大差 2.39 × 10⁻¹²。数学上是同一串数,那点差别是 sin 在做大角度求值时的舍入。
这一次换域给出的不只是解释,是整张地图:为什么折叠点是 fs/2(那是两个副本的交界)、为什么会来回反射(副本是左右对称的,因为实信号的谱共轭对称)、为什么折回来的分量相位取共轭(那正是共轭对称的意思)、为什么抗混叠必须在采样之前(副本一旦相加就再也拆不开)。
四个结论,一张图。如果你只在时域里想这件事,这四条是四条互不相干的规则;换到频域,它们是同一张图看四眼。
这一章的一句话
采样不足的时候,高频不会消失也不会模糊,它会顶着一个低频的名字住进你的数据里,和真的那个低频逐位相同——所以抗混叠必须发生在采样之前,事后没有任何算法能补救。
前两章处理的是「一眼一眼地看」。下一章处理另一条限制:你只能看有限长的一段。这条限制的代价用一个公式就能说完:分辨率 = 1/T。它会导出一个让很多人意外的结论——想把 1000 Hz 和 1002 Hz 分开,提高采样率一点用都没有,把 1024 个点补零到 8192 个也一点用都没有。唯一的办法是多听 0.5 秒。