摘要
一段音乐、一张照片、一部电影,在计算机里都是同一种东西------一串按约定规则排列的字节。但这串字节不是凭空产生的,它是一条清晰因果链的末端:物理世界的连续量 → 传感器换能 → 电信号 → 采样量化 → 编码组织 → 比特流。
本文沿着这条链走一遍,回答三个问题:物理量是怎么被"测量"出来的、为什么这样测量恰好匹配人的感官、以及为什么原始数据量大到必须压缩。每一节给出物理定义、数学推导、可运行代码和实测数据。
本文的特点是:所有数字都来自配套脚本的实测输出,不是估算。涉及的关键结论(Bayer 去马赛克的 PSNR、色度亚采样的代价、帧间压缩的增益)都在真实信号上验证过。
一、介绍:一条统一的因果链
1.1 三个问题的共同答案
声音、图像、视频看似三种东西,但它们的"形成"遵循完全相同的四步:
#mermaid-svg-ewfGozdYApIiKPas{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ewfGozdYApIiKPas .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ewfGozdYApIiKPas .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ewfGozdYApIiKPas .error-icon{fill:#552222;}#mermaid-svg-ewfGozdYApIiKPas .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ewfGozdYApIiKPas .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ewfGozdYApIiKPas .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ewfGozdYApIiKPas .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ewfGozdYApIiKPas .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ewfGozdYApIiKPas .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ewfGozdYApIiKPas .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ewfGozdYApIiKPas .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ewfGozdYApIiKPas .marker.cross{stroke:#333333;}#mermaid-svg-ewfGozdYApIiKPas svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ewfGozdYApIiKPas p{margin:0;}#mermaid-svg-ewfGozdYApIiKPas .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ewfGozdYApIiKPas .cluster-label text{fill:#333;}#mermaid-svg-ewfGozdYApIiKPas .cluster-label span{color:#333;}#mermaid-svg-ewfGozdYApIiKPas .cluster-label span p{background-color:transparent;}#mermaid-svg-ewfGozdYApIiKPas .label text,#mermaid-svg-ewfGozdYApIiKPas span{fill:#333;color:#333;}#mermaid-svg-ewfGozdYApIiKPas .node rect,#mermaid-svg-ewfGozdYApIiKPas .node circle,#mermaid-svg-ewfGozdYApIiKPas .node ellipse,#mermaid-svg-ewfGozdYApIiKPas .node polygon,#mermaid-svg-ewfGozdYApIiKPas .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ewfGozdYApIiKPas .rough-node .label text,#mermaid-svg-ewfGozdYApIiKPas .node .label text,#mermaid-svg-ewfGozdYApIiKPas .image-shape .label,#mermaid-svg-ewfGozdYApIiKPas .icon-shape .label{text-anchor:middle;}#mermaid-svg-ewfGozdYApIiKPas .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ewfGozdYApIiKPas .rough-node .label,#mermaid-svg-ewfGozdYApIiKPas .node .label,#mermaid-svg-ewfGozdYApIiKPas .image-shape .label,#mermaid-svg-ewfGozdYApIiKPas .icon-shape .label{text-align:center;}#mermaid-svg-ewfGozdYApIiKPas .node.clickable{cursor:pointer;}#mermaid-svg-ewfGozdYApIiKPas .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ewfGozdYApIiKPas .arrowheadPath{fill:#333333;}#mermaid-svg-ewfGozdYApIiKPas .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ewfGozdYApIiKPas .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ewfGozdYApIiKPas .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ewfGozdYApIiKPas .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ewfGozdYApIiKPas .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ewfGozdYApIiKPas .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ewfGozdYApIiKPas .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ewfGozdYApIiKPas .cluster text{fill:#333;}#mermaid-svg-ewfGozdYApIiKPas .cluster span{color:#333;}#mermaid-svg-ewfGozdYApIiKPas div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ewfGozdYApIiKPas .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ewfGozdYApIiKPas rect.text{fill:none;stroke-width:0;}#mermaid-svg-ewfGozdYApIiKPas .icon-shape,#mermaid-svg-ewfGozdYApIiKPas .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ewfGozdYApIiKPas .icon-shape p,#mermaid-svg-ewfGozdYApIiKPas .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ewfGozdYApIiKPas .icon-shape .label rect,#mermaid-svg-ewfGozdYApIiKPas .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ewfGozdYApIiKPas .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ewfGozdYApIiKPas .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ewfGozdYApIiKPas :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 物理世界的连续量
声压 / 光辐射
传感器换能
麦克风 / CMOS
连续电信号
电压 / 电流
采样 + 量化
ADC
数字序列
PCM / 像素阵列
压缩编码
比特流
WAV / JPEG / H.264
区别只在被测量的物理量不同 ,以及由此决定的采样方式不同:
| 声音 | 图像 | 视频 | |
|---|---|---|---|
| 物理本质 | 空气压强扰动 | 电磁辐射(可见光) | 同上 + 时间 |
| 自变量 | 时间 ttt(1D) | 空间 (x,y)(x,y)(x,y)(2D) | 空间 + 时间(3D) |
| 换能器件 | 麦克风/耳膜 | CMOS/CCD 像素阵列 | 同上 + 快门 |
| 感官接收器 | 耳蜗基底膜 | 视网膜视锥/视杆 | 同上 + 视觉暂留 |
| 采样维度 | 时间采样 | 空间采样 | 空间采样 + 时间采样 |
| 典型采样率 | 44.1/48 kHz | 每英寸 300~数千 dpi | 空间 + 24~60 fps |
| 原始数据率 | ~1.4 Mbps(立体声) | ~数 MB/张 | ~1.5 Gbps(1080p60) |
关键洞察在最后三列:三者的差别只是采样发生在几个维度上。一旦理解"采样"这件事,三者就是同一个理论的不同实例。
1.2 从"连续"到"离散"必须跨越的三道门槛
任何物理量要变成数字,都要闯三关:
- 时间/空间离散化(采样):连续量在无穷多个点上取值,不可能全存。必须只保留有限个代表点------前提是剩下的点能被推断出来
- 幅度离散化(量化):即使时间点有限,每个点的数值仍是实数,需要映射到一个有限集合
- 编码组织(压缩):把离散化后的数据排成字节,并尽量去掉冗余
前两道门槛由采样定理和量化噪声理论回答(已在《数字信号处理全景》中详述),本文重点关注第一道门槛在被测量物理量上的具体实现,以及第三道门槛为什么不可避免。
1.3 一个贯穿全文的类比:栅栏与像素
把采样想象成透过栅栏看风景 :栅栏的缝隙就是你保留的采样点。如果风景变化比缝隙间距还快,你就会看错------栅栏后面看到的运动方向和真实方向可能相反。这就是混叠(车轮倒转的经典错觉)。
图像采样是同一个道理,只是栅栏从时间维挪到了空间维------这就是摩尔纹的来源。视频则是在两层栅栏(空间 + 时间)后面看风景。
这个类比不是比喻,它精确对应采样定理的数学:若信号最高频率为 fmf_mfm,采样率必须满足 fs>2fmf_s > 2f_mfs>2fm,否则高频分量会"伪装"成低频分量。
二、声音:压强波如何变成一串数字
2.1 声音的物理本质
声音不是"振动"这么笼统的东西,它是空气压强的周期性扰动,以纵波形式传播。描述它的物理量是声压(sound pressure):
p(t)=p0+Δp(t) p(t) = p_0 + \Delta p(t) p(t)=p0+Δp(t)
其中 p0p_0p0 是大气压(约 101325 Pa),Δp(t)\Delta p(t)Δp(t) 是我们要处理的声压扰动 ------注意它通常只有大气压的百万分之一量级。
人耳能感知的声压范围极其宽广:
| 物理量 | 数值 | 说明 |
|---|---|---|
| 听阈(1 kHz) | 2×10−52\times10^{-5}2×10−5 Pa | 20 μPa,参考声压 |
| 正常交谈 | 0.02~0.2 Pa | 60~80 dB SPL |
| 痛阈 | 20 Pa | 120 dB SPL |
| 比值(痛阈/听阈) | 10610^6106 | 幅度差 100 万倍 |
正因为这个巨大的动态范围,声学上使用对数刻度------声压级(SPL):
Lp=20log10(prmspref) dB,pref=2×10−5 Pa L_p = 20\log_{10}\left(\frac{p_{\text{rms}}}{p_{\text{ref}}}\right)\ \text{dB}, \qquad p_{\text{ref}} = 2\times10^{-5}\ \text{Pa} Lp=20log10(prefprms) dB,pref=2×10−5 Pa
这一点决定了后续一切:既然人耳是对数感知,那么音频系统的量化、增益、显示都应该按对数(dB)组织,而不是线性。这是 dB 这个单位在音频里无处不在的根本原因。
2.2 频率、音色与谐波
一个纯音是正弦波:p(t)=Asin(2πft+ϕ)p(t) = A\sin(2\pi f t + \phi)p(t)=Asin(2πft+ϕ)。三个参数分别是:
- 振幅 AAA → 响度
- 频率 fff → 音高
- 相位 ϕ\phiϕ → 单独听不出差别,但影响波形叠加
真实乐器发出的从来不是纯音。一把小提琴拉 A4(440 Hz)时,实际频谱是基频加上一系列泛音(harmonics):
p(t)=∑k=1KAksin(2πkf0t+ϕk) p(t) = \sum_{k=1}^{K} A_k \sin(2\pi k f_0 t + \phi_k) p(t)=k=1∑KAksin(2πkf0t+ϕk)
各谐波的幅度分布 {Ak}\{A_k\}{Ak} 就是"音色"。这是理解一切音频现象的关键:同样的 440 Hz,长笛和小提琴听起来完全不同,唯一的区别就是谐波能量的分布。
python
import numpy as np
def tones(t, f0, harmonics):
"""加法合成:基频 f0 + 若干泛音 (倍频序号, 相对幅度)。"""
x = np.zeros_like(t)
for k, amp in harmonics:
x += amp * np.sin(2*np.pi*f0*k*t + 0.3*k) # 0.3k 给各谐波不同初相
return x / np.max(np.abs(x)) * 0.8
fs, dur = 48000, 1.0
t = np.arange(int(fs*dur)) / fs
flute = tones(t, 440.0, [(1,1.0), (2,0.05), (3,0.02)]) # 近似正弦
violin = tones(t, 440.0, [(1,1.0), (2,0.35), (3,0.42), (4,0.18),
(5,0.22), (6,0.09), (7,0.07)]) # 丰富谐波
实测频谱对比(配套脚本 av_formation_demo.py --only sound 输出):
| 乐器 | 基频 | 主要谐波 | 谱线形状 |
|---|---|---|---|
| 长笛 | 440 Hz | 仅基频 + 极弱 2/3 次 | 单根谱线,其余在 −60 dB 以下 |
| 小提琴 | 440 Hz | 2~7 次均在 −20 dB 内 | 密集梳状谱线 |
同一基频、同一音量,只因谐波分布不同就是两种乐器。MP3/AAC 等音频编码压缩的核心之一,就是丢弃被强谐波掩蔽的弱谐波(第 2.7 节)。
2.3 麦克风:从声压到电压
麦克风是换能器 (transducer),把声压转换为电压。主流三种:
| 类型 | 原理 | 特点 |
|---|---|---|
| 动圈式 | 线圈在磁场中随振膜运动,法拉第电磁感应:e=−NdΦdte = -N\frac{d\Phi}{dt}e=−NdtdΦ | 结实、无需供电、瞬态好;灵敏度低 |
| 电容式 | 振膜与背板构成电容,声压改变间距:C=εA/dC = \varepsilon A/dC=εA/d,ΔV∝Δd\Delta V \propto \Delta dΔV∝Δd | 灵敏度高、频响宽;需幻象供电 |
| MEMS | 半导体工艺刻蚀的微型电容 | 体积极小、可阵列化;手机/耳机标配 |
以电容式为例,电容变化与振膜位移成正比,输出的电压变化即正比于声压:
v(t)=S⋅Δp(t) v(t) = S \cdot \Delta p(t) v(t)=S⋅Δp(t)
SSS 是灵敏度(典型 10~50 mV/Pa)。注意这一步是连续的:麦克风输出的仍是连续时间、连续幅度的模拟电压。它必须被 ADC 离散化才能进入数字域。
2.4 采样与量化:PCM 的诞生
对麦克风输出做 ADC:
- 采样 :44.1 kHz(CD)或 48 kHz(视频音频标准)。依据奈奎斯特定理,fs(即采样频率)>2×20 kHz=40f_s(即采样频率) > 2\times 20\ \text{kHz} = 40fs(即采样频率)>2×20 kHz=40 kHz,留 10% 余量得 44.1 kHz
- 量化:16 bit(CD)或 24 bit(专业)
结果就是 PCM(Pulse Code Modulation)------一串离散整数。码率:
码率=fs×B×C \text{码率} = f_s \times B \times C 码率=fs×B×C
其中 BBB 为位深,CCC 为声道数。实测数据:
| 配置 | 计算 | 实测 |
|---|---|---|
| 48 kHz / 16 bit / 单声道 / 1 秒 | 48000×16×1/8=9600048000\times16\times1/8 = 9600048000×16×1/8=96000 B | 96000 字节 PCM + 44 字节头 |
| CD:44.1 kHz / 16 bit / 立体声 | 44100×16×2=1.4144100\times16\times2 = 1.4144100×16×2=1.41 Mbps | 1411 kbps = 0.17 MB/s,1 分钟约 10.1 MB |
那个 "44 字节头" 就是 WAV 容器的 RIFF 结构(第 6.1 节拆解)。
量化的代价 由位深决定。BBB 位量化下,理论信噪比:
SQNR≈6.02B+1.76 dB \mathrm{SQNR} \approx 6.02B + 1.76\ \text{dB} SQNR≈6.02B+1.76 dB
实测对比:
| 位深 | 量化台阶数 | 理论 SQNR | 实测 SQNR |
|---|---|---|---|
| 8 | 256 | 49.92 dB | 48.37 dB |
| 12 | 4,096 | 74.00 dB | 71.52 dB |
| 16 | 65,536 | 98.08 dB | 96.21 dB |
| 24 | 16,777,216 | 146.24 dB | 144.09 dB |
对这个 1.5~2.5 dB 的差距,最自然的猜测是"误差与信号相关",但实测否定了这个解释:
真正的原因是 headroom。
6.02B+1.766.02B + 1.766.02B+1.76 的推导前提是正弦恰好达到满量程 。实际信号必须留出余量防止削顶,peakpeakpeak 每低 1 dB,SQNR 就实打实少 1 dB:
| 峰值电平 | 相对满量程 | 理论 SQNR(含 headroom) | 实测 SQNR | 偏差 |
|---|---|---|---|---|
| 1.00 | 0.00 dB | 98.08 dB | 98.17 dB | +0.09 dB |
| 0.80 | −1.94 dB | 96.14 dB | 96.21 dB | +0.07 dB |
| 0.50 | −6.02 dB | 92.06 dB | 91.80 dB | −0.26 dB |
| 0.25 | −12.04 dB | 86.04 dB | 85.86 dB | −0.17 dB |
| 0.10 | −20.00 dB | 78.08 dB | 77.98 dB | −0.10 dB |
(16 bit,1 kHz 正弦。)把 headroom 项加回去之后,理论与实测的偏差在 0.26 dB 以内 ------6.02B+1.766.02B+1.766.02B+1.76 是完全精确的,错的是"信号吃满量程"这个隐含假设。
这条结论有直接的工程后果:录音电平与量化位深同等重要。一首峰值只到 −20 dBFS 的母带,即使留了 24 bit 位深,实际可用的动态范围也退回 20 bit 的水平。
2.5 抖动:为什么不相关的噪声好过相关的失真
上一节留了一个问题:真正的坑不在 SQNR 总量,而在量化误差的"性质"。先看一个实测对照(8 bit,不同幅度):
| 测试音 | 位深 | 实测 SQNR | 误差-信号相关系数 | 误差是否逐周期重复 |
|---|---|---|---|---|
| 1000 Hz(48 样本/周期,整数分频) | 8 | 48.37 dB | 0.274 | 是(相邻周期相关 = 1.000) |
| 997 Hz(非整数分频) | 8 | 47.73 dB | 0.014 | 否 |
关键在最后一行的对比:两组的 SQNR 几乎相同,但误差的性质完全不同 。1000 Hz 那个信号的量化误差每个周期一模一样(周期性 = 1.000),误差与信号强相关------它在频谱上表现为一串离散的谐波尖峰,而不是平坦的底噪。人耳对谐波失真的敏感度远高于对宽带噪声的敏感度,所以"SQNR 相同 ≠ 听感相同"。
解决办法叫抖动(dither):量化之前加入极微弱的随机噪声,打破误差与信号的相关性。标准做法是两个独立均匀分布相减(TPDF,三角概率密度),峰峰值 2 LSB:
python
# TPDF 抖动:两个 U(0,1) 之差 -> 三角分布,峰峰 2 LSB
d = (rng.uniform(0, 1, len(t)) - rng.uniform(0, 1, len(t))) * lsb
xq = np.round((x + d) / lsb) * lsb # 加抖后再量化
实测(1 kHz,8 bit):
| 幅度 | 无抖动 最大杂散 | 有抖动 最大杂散 | 无抖动 误差-信号相关 | 有抖动 误差-信号相关 |
|---|---|---|---|---|
| 0.4 LSB | 信号被完全抹掉 | −24.29 dBc | 1.00 | 0.01 |
| 1.0 LSB | −13.95 dBc | −31.54 dBc | 0.28 | 0.00 |
| 8.0 LSB | −33.05 dBc | −50.68 dBc | 0.02 | 0.02 |
| 64.0 LSB | −44.72 dBc | −67.84 dBc | 0.33 | 0.01 |
有两个值得记住的点:
第一,抖动能把肉眼看不到的信号"救回来"。 幅度只有 0.4 LSB 的正弦,不加抖动时输出恒为 0(信息彻底丢失);加了抖动后单样本看着全是噪声,但按周期对齐做同步平均,噪声被 k\sqrt{k}k 压下去,正弦浮现出来:
| 平均次数 kkk | 与真实正弦的相关系数 | 噪声衰减 |
|---|---|---|
| 1 | 0.5663 | 0 dB |
| 16 | 0.9304 | 12.0 dB |
| 256 | 0.9925 | 24.1 dB |
这是抖动最重要的价值:它把确定性的、不可逆的失真,换成了随机的、可以被平均消掉的噪声。前者无论叠加多少次都救不回来(平均 1000 个全零还是全零),后者每多平均一倍就干净 3 dB。这也是测量仪器"多次采样取平均以提高分辨率"的背后原理。
第二,抖动的代价是底噪抬高。 上面的权衡正是:总噪声功率增加约 3 dB(TPDF 抖动的功率是 Δ2/6\Delta^2/6Δ2/6,是量化白噪声 Δ2/12\Delta^2/12Δ2/12 的两倍),换来的却是谐波尖峰下降 17~37 dB。这笔交易在听感上是压倒性划算的------人耳几乎察觉不到底噪轻微抬高,但对单个突出的谐波尖峰极其敏感。
一句话总结:宁可要宽带噪声,也不要确定性失真。
2.6 时频分析:为什么只有 FFT 还不够
对一段音乐做 FFT,能得到整段的频率组成,但丢失了时间信息------无法回答"某个音在第几秒出现"。实测一段 1 秒的信号(扫频 200→2000 Hz + 恒定 440 Hz):
全局 FFT 主峰(Hz): [439, 440, 441, 1100]
-> 只知道 200~2000 区间都有能量,但无法回答『440Hz 出现在第几秒』
解决办法是 STFT(短时傅里叶变换) :把信号切成重叠的小段,每段加窗后分别做 FFT,把结果沿时间排起来,得到一张二维图------语谱图(spectrogram)。
python
import scipy.signal as sig
f, t, Z = sig.stft(x, fs=fs, window="hann", nperseg=1024, noverlap=512)
# Z[i, j]: 第 j 个时间窗、第 i 个频率的能量
代价是时间分辨率与频率分辨率不可兼得 ,权衡由窗长 LLL 控制。实测:
| 窗长 LLL | 频率分辨率 Δf\Delta fΔf(Hann 主瓣) | 时间分辨率 Δt\Delta tΔt(窗时长) | 乘积 Δf⋅Δt\Delta f \cdot \Delta tΔf⋅Δt |
|---|---|---|---|
| 256 | 750.0 Hz | 5.33 ms | 4.00 |
| 1024 | 187.5 Hz | 21.33 ms | 4.00 |
| 4096 | 46.9 Hz | 85.33 ms | 4.00 |
注意最后一列:Δf⋅Δt\Delta f\cdot\Delta tΔf⋅Δt 恒定为 4(Hann 窗),与窗长无关。这不是巧合,而是傅里叶分析的不确定性原理:
Δt⋅Δf≥14π (理论下限),Hann 窗实测 Δt⋅Δf=4 \Delta t \cdot \Delta f \geq \frac{1}{4\pi}\ \text{(理论下限)},\qquad \text{Hann 窗实测 } \Delta t\cdot\Delta f = 4 Δt⋅Δf≥4π1 (理论下限),Hann 窗实测 Δt⋅Δf=4
窗长变长,频率看得更清,但时间更模糊;反之亦然。没有任何办法同时获得两者。 这个结论和时间分辨率/频率分辨率的物理限制等价,也是上一篇《数字信号处理全景》中"窗函数的主瓣宽度决定能否分辨"的同一个事实。
工程上的取舍很实际:语音识别用 20~30 ms 窗(时间要准),音乐分析用 50~100 ms 窗(频率要清)。
2.7 感知编码:为什么 MP3 能把 CD 压掉 90%
CD (Audio CD(CD-DA,音乐 CD) ,就是我们以前听音乐的激光唱片,它里面是无压缩的 PCM 音频。) 的 1411 kbps 中,有相当大一部分是人耳听不到的。心理声学给出两个可利用的事实:
1. 绝对听阈:低于某频率/强度的声音根本听不见。
2. 掩蔽效应 (masking):一个强音存在时,会抬高邻近频率的听阈,使落在门限以下的弱音变得听不见。掩蔽的范围随强度增大而展宽,且向高频侧扩展得比低频侧更宽(称为"向上掩蔽")。
编码器据此工作:把信号分到若干子带(用滤波器组或 MDCT),计算每个子带的掩蔽门限,把量化噪声控制在门限以下即可。低于门限的信息直接丢弃。
这就是为什么 MP3 128 kbps 能达到接近 CD 的主观音质------它丢的不是"不重要的数据",而是结构上无法被感知的数据。这与图像压缩用 DCT + 量化丢弃人眼不敏感的高频是完全对称的思路。
三、静止图像:光如何变成像素矩阵
3.1 光的物理与可见范围
光是电磁波。可见光只是电磁波谱中极窄的一段:
| 波段 | 波长 | 感知 |
|---|---|---|
| 紫外 | < 380 nm | 不可见 |
| 紫~蓝 | 380~495 nm | 可见 |
| 绿 | 495~570 nm | 可见 |
| 红 | 620~750 nm | 可见 |
| 近红外 | > 750 nm | 不可见(但 CMOS 能"看见") |
一个重要的工程事实 :硅基 CMOS 的光谱响应延伸到约 1100 nm(近红外)。所以所有相机都必须加红外截止滤光片(IR-cut filter),否则拍出来的画面会被不可见的红外光污染------红色物体发白、黑色衣物透出图案,都是这个原因。
3.2 人眼:为什么只需要三个数字
人眼视网膜有两类感光细胞:
| 类型 | 数量 | 光谱响应 | 功能 |
|---|---|---|---|
| 视杆细胞 | ~1.2 亿 | 单一宽谱(峰值 507 nm) | 暗光视觉,无色觉,分辨率低 |
| 视锥细胞 | ~600 万 | 三种:L(≈560nm)、M(≈530nm)、S(≈420nm) | 明视觉,色觉,高分辨率 |
关键结论:因为只有三类视锥细胞,任何颜色都可以用三个数字描述。这就是所有色彩空间都是三维的根本原因。
这里的每一处都有反直觉的细节:
- L 和 M 的光谱曲线高度重叠(峰值只差 30 nm),这解释了为什么红绿难分辨(色盲的主要成因)
- 视锥细胞集中在中央凹(fovea),只占视网膜约 1% 面积,却占据视觉皮层的一半处理资源
- 视杆细胞在暗光下主导,但它没有色觉------这就是为什么月光下看不出颜色
3.3 颜色匹配:从光谱到三刺激值
物理上,一个颜色的完整描述是它的光谱功率分布 S(λ)S(\lambda)S(λ)(一个连续函数)。但人眼只提取三个投影:
L=∫S(λ) lˉ(λ) dλM=∫S(λ) mˉ(λ) dλS=∫S(λ) sˉ(λ) dλ \begin{aligned} L &= \int S(\lambda)\,\bar{l}(\lambda)\,d\lambda \\ M &= \int S(\lambda)\,\bar{m}(\lambda)\,d\lambda \\ S &= \int S(\lambda)\,\bar{s}(\lambda)\,d\lambda \end{aligned} LMS=∫S(λ)lˉ(λ)dλ=∫S(λ)mˉ(λ)dλ=∫S(λ)sˉ(λ)dλ
其中 lˉ,mˉ,sˉ\bar{l}, \bar{m}, \bar{s}lˉ,mˉ,sˉ 是视锥细胞的响应曲线。
这一步是有损的、不可逆的:两个光谱功率分布完全不同的物体(同色异谱 metamers),只要三个刺激值相同,人眼就分辨不出来。整个显示工业建立在这个"漏洞"上------显示器不需要重现真实光谱,只需让三个刺激值匹配。
由 CIE 1931 XYZ 到线性 sRGB 的标准变换:
RGB=3.2406−1.5372−0.4986−0.96891.87580.04150.0557−0.20401.0570XYZ \begin{bmatrix} R \\ G \\ B \end{bmatrix} = \begin{bmatrix} 3.2406 & -1.5372 & -0.4986 \\ -0.9689 & 1.8758 & 0.0415 \\ 0.0557 & -0.2040 & 1.0570 \end{bmatrix} \begin{bmatrix} X \\ Y \\ Z \end{bmatrix} RGB = 3.2406−0.96890.0557−1.53721.8758−0.2040−0.49860.04151.0570 XYZ
3.4 相机:小孔、透镜与光电转换
成像 :理想针孔模型下,三维点 (X,Y,Z)(X,Y,Z)(X,Y,Z) 投影到成像平面:
x=fXZ,y=fYZ x = f\frac{X}{Z}, \qquad y = f\frac{Y}{Z} x=fZX,y=fZY
fff 为焦距。真实镜头遵循薄透镜公式 1f=1u+1v\frac{1}{f} = \frac{1}{u} + \frac{1}{v}f1=u1+v1,并引入畸变------这也是相机标定(张正友标定法)要解决的问题。
光电转换 :CMOS 像素的核心是光电二极管。光子入射产生电子-空穴对,量子效率 η\etaη 表示每个光子平均产生多少电子。产生的光生电流:
Iph=η⋅Popthν⋅q I_{ph} = \eta \cdot \frac{P_{opt}}{h\nu} \cdot q Iph=η⋅hνPopt⋅q
其中 PoptP_{opt}Popt 是入射光功率,hνh\nuhν 是单光子能量,qqq 是电子电荷。累积的电荷被转换为电压、放大、再 ADC。这一步引入的噪声很重要:
| 噪声源 | 特性 | 表现 |
|---|---|---|
| 散粒噪声 | σ∝N\sigma \propto \sqrt{N}σ∝N ,NNN 为光子数 | 暗光噪点,物理极限无法消除 |
| 读出噪声 | 与信号无关的固定量 | 暗部噪声底 |
| 热噪声(暗电流) | ∝\propto∝ 曝光时间、温度 | 长曝光热噪点 |
| PRNU | 像素响应不一致 | 固定图案噪声 |
散粒噪声服从泊松分布,这意味着暗光下的噪点不是工艺缺陷,而是量子力学的必然------光子到达本身是随机过程。
曝光、散粒噪声与 ISO 的真相
因为服从泊松分布,标准差恰好是均值平方根:σ=N\sigma = \sqrt{N}σ=N 。于是信噪比有一个极简的闭式:
SNR=NN=N⟹SNRdB=10log10N \mathrm{SNR} = \frac{N}{\sqrt{N}} = \sqrt{N} \quad\Longrightarrow\quad \mathrm{SNR_{dB}} = 10\log_{10} N SNR=N N=N ⟹SNRdB=10log10N
实测(每块 40000 像素统计):
| 信号电子数 NNN | 理论 std =N=\sqrt{N}=N | 实测 std | 理论 SNR | 实测 SNR | 叠加读出噪声 3 e⁻ 后 |
|---|---|---|---|---|---|
| 20,000 | 141.42 | 141.41 | 43.01 dB | 43.01 dB | 43.01 dB |
| 5,000 | 70.71 | 70.96 | 36.99 dB | 36.96 dB | 36.95 dB |
| 1,000 | 31.62 | 31.58 | 30.00 dB | 30.01 dB | 29.98 dB |
| 200 | 14.14 | 14.20 | 23.01 dB | 22.97 dB | 22.78 dB |
| 50 | 7.07 | 7.07 | 16.99 dB | 17.00 dB | 16.31 dB |
| 10 | 3.16 | 3.16 | 10.00 dB | 9.99 dB | 7.19 dB |
| 2 | 1.41 | 1.42 | 3.01 dB | 2.99 dB | −4.39 dB |
这张表有两个直接读出的事实:
- 光线每暗 100 倍,SNR 掉 20 dB 。这是硬约束,与传感器工艺无关------这也是为什么手机夜景模式必须靠多帧叠加(叠 kkk 帧相当于把 NNN 乘 kkk,SNR 涨 10log10k10\log_{10}k10log10k)
- 读出噪声只在暗部有存在感 。N≥1000N \ge 1000N≥1000 时几乎看不出差别;N=2N=2N=2 时它直接把信号埋掉(+2.99 → −4.39 dB)
由此可以澄清一个流传很广的误解:提高 ISO 到底有没有用?
答案是"只在暗处有用,而且有上限"。关键在于读出噪声位于增益之后(增益放在 ADC 之前),所以提高模拟增益会降低它折算回输入端的贡献;但散粒噪声与信号一起被放大,比例不变。实测:
| 模拟增益 A(ISO 倍数) | 饱和电子数 | N=5000N=5000N=5000(亮) | N=50N=50N=50(中) | N=5N=5N=5(极暗) |
|---|---|---|---|---|
| 1 | 30,000 | 36.99 dB | 15.94 dB | 1.66 dB |
| 4 | 7,500 | 37.01 dB | 16.93 dB | 6.34 dB |
| 16 | 1,875 | ---(已过曝) | 16.98 dB | 6.96 dB |
| 32 | 938 | ---(已过曝) | 17.00 dB | 7.00 dB |
规律非常清晰:
- 亮部(N=5000N=5000N=5000) :增益从 1 提到 4,SNR 只从 36.99 变到 37.01 dB(0.02 dB)------高 ISO 不产生任何新信息,只会让画面更早过曝
- 极暗(N=5N=5N=5) :1.66 dB → 7.00 dB,提升 5.3 dB------这才是高 ISO 真正有用的地方
- 上限就是散粒噪声 :N=50N=50N=50 的平台值是 17.00 dB,恰好 =10log1050= 10\log_{10}50=10log1050;N=5N=5N=5 是 7.00 dB =10log105= 10\log_{10}5=10log105。两条曲线的终点都被 10log10N10\log_{10}N10log10N 这个量子极限钉死
- 代价是动态范围 :饱和电子数按 1/A1/A1/A 下降,A=8 时原本能容纳 30000 电子的像素 3750 电子就顶到头了
所以"高 ISO 画质差"的完整说法是:高 ISO 本身不损失 SNR,但它压缩了动态范围(容易死白),而且一旦到了极限,剩下的就只有散粒噪声------那是物理,不是bug。
3.5 彩色滤光阵列:为什么每个像素只有一种颜色
理想方案是每个像素用三个传感器分别测 R/G/B。但那样成本高三倍、体积大三倍、良率更低。
工程上的解法是 Bayer 滤光阵列(Bryce Bayer,柯达,1976):在传感器前加一层微型滤色膜,每个像素只透过一种颜色:
RGRG⋯GBGB⋯RGRG⋯GBGB⋯⋮⋮⋮⋮⋱ \begin{matrix} R & G & R & G & \cdots \\ G & B & G & B & \cdots \\ R & G & R & G & \cdots \\ G & B & G & B & \cdots \\ \vdots & \vdots & \vdots & \vdots & \ddots \end{matrix} RGRG⋮GBGB⋮RGRG⋮GBGB⋮⋯⋯⋯⋯⋱
注意 G 占了一半像素------这不是随意的:人眼的亮度感知主要来自中波长(绿),所以 G 的空间分辨率决定了我们感知的"清晰度"。这是 Bayer 阵列最重要的设计考量。
于是每个像素失掉了 2/3 的颜色信息,需要通过 去马赛克(demosaicing) 插值恢复。实测:
Bayer 采样:每像素只留 1 个通道,数据量降为 1/3(196608 -> 65536)
去马赛克 vs 原图 PSNR = 37.66 dB
分通道 PSNR: R=36.13 G=40.14 B=37.62 dB
G 通道的 PSNR 明显好于 R/B(40.14 vs 36.13/37.62),因为 G 的采样密度是其他两个通道的两倍。这个实测结果直接验证了前面"Bayer 阵列重视绿通道"的设计动机。
去马赛克的经典算法是双线性插值。对不同通道要用不同的插值核:
- R/B :已知点在 (偶,偶)(偶,偶)(偶,偶) / (奇,奇)(奇,奇)(奇,奇),间距为 2。缺失位置的最近同类点可能在对角或正交方向,核为 14121242121\frac{1}{4}\begin{bmatrix}1&2&1\\2&4&2\\1&2&1\end{bmatrix}41 121242121
- G :已知点构成 quincunx 晶格,缺失点的最近同类点就是上下左右 4 个,核为 14010101010\frac{1}{4}\begin{bmatrix}0&1&0\\1&0&1\\0&1&0\end{bmatrix}41 010101010
python
import numpy as np
from scipy.ndimage import convolve
# R/B 用同一核:对"正交缺失"和"对角缺失"都给出正确的邻域均值
KR = np.array([[1,2,1],[2,4,2],[1,2,1]], float) / 4.0
KG = np.array([[0,1,0],[1,0,1],[0,1,0]], float) / 4.0
def demosaic_chan(full, is_known, K):
"""把只含已知采样的通道补满。"""
known_only = np.where(is_known, full, 0.0)
interp = convolve(known_only, K, mode="nearest")
wgt = convolve(is_known.astype(float), K, mode="nearest")
return np.where(is_known, full, interp / np.maximum(wgt, 1e-9))
# 用已知权重做归一化是关键:直接卷积会在只剩对角邻居时把均值算小一倍
这里有个容易写错的地方:必须除以"有效邻居的权重和" 。若直接用固定核卷积,在只剩对角两个邻居的位置,结果会比正确值小一半(因为核权重和为 4,但只有两个位置贡献了值)。正确做法是像上面那样用 is_known 掩膜卷积做归一化。
升级:色差域插值,以及"镜头低通"为什么是必需品
双线性插值隐含了一条假设:相邻像素的变化是平缓的 。这条假设在彩色边缘处直接失效------在红方块的边界上横向取平均,就会跨边界把红和背景混成伪色。经典改进是色差域插值 (利用"同一物体表面色差变化缓慢"的先验):先补全高分辨率的 G,再只插值平缓的 (R−G)(R-G)(R−G) 和 (B−G)(B-G)(B−G),最后还原 R=G+(R−G)R = G + (R-G)R=G+(R−G):
python
def chroma_domain(rgb, masks):
"""色差域插值:先补全 G,再插值色差,最后还原 R/B。"""
mr, mg, mb = masks
Ghat = np.where(mg, rgb[..., 1], interp(mg, rgb[..., 1], KG)) # 全分辨率 G
out = np.zeros_like(rgb)
out[..., 1] = Ghat
for c, m in ((0, mr), (2, mb)):
d = np.where(m, rgb[..., c] - Ghat, 0.0) # 已知位置上的色差
out[..., c] = Ghat + np.where(m, d, interp(m, d, KR))
return out
实测(同一物理设置下的算法对比):

这张表藏着三条比算法本身更重要的经验:
1. 算法好不好,取决于先验成不成立,而不是算法本身更"高级"。 我用一个可以直接算出来的量作为预报器:
先验强度=∣∇(R−G)∣∣∇G∣ \text{先验强度} = \frac{|\nabla(R-G)|}{|\nabla G|} 先验强度=∣∇G∣∣∇(R−G)∣
它衡量"色差比亮度平滑多少"。远小于 1 时(第一行 0.250)色差域插值大涨 3.61 dB,其中 R 通道从 30.86 跳到 35.37 dB(彩色边缘的伪彩被显著压制 );大于 1 时(第三行 1.767,即三通道毫不相干的图)反而掉了 2.06 dB。同一个算法,换素材就翻转结论------这也是评测去马赛克时必须用真实(或统计等效自然)图像的原因。顺带承认:上文那个 37.66 dB 的素材正是第三行这种"通道独立"的对抗性情形,它适合用来暴露伪彩,但不适合用来给算法排名。
2. 镜头的光学低通不是缺陷,而是让问题可解的前提。 看第二行:同一场景只要跳过镜头模糊这一步,双线性直接从 31.95 dB 崩到 16.30 dB (掉 15.65 dB),远大于任何算法改进带来的收益。原因在于 CFA 采样率只有全分辨率的 1/2~1/4,若场景本身还带着超过 CFA 奈奎斯特的细节,采样时就混叠了,去马赛克无论用什么算法都不可能还原------信息在采样那一刻就已经丢失。
这条对做 ISP pipeline 的人是硬约束:去马赛克的上游必须先有抗混叠滤波(镜头衍射 + OLPF 光学低通滤波片)。试图事后用更强的插值算法补救,是在解一个已经被采样破坏的问题。
3. G 通道是质量的瓶颈。 两种算法的 G 通道 PSNR 都是 35.52 dB、完全一样------因为色差域插值沿用了同一个 Ghat。误差被 G 通道质量绑定,G 插错了全通道一起错。这也从另一方面解释了为什么 Bayer 阵列要把一半像素给 G。
3.6 YCbCr 与色度亚采样
RGB 三个通道高度相关(一张照片里,亮的地方通常三个通道都亮)。更高效的表达是把亮度 和色度分离:
Y=0.299R+0.587G+0.114BCb=−0.169R−0.331G+0.500B+128Cr=0.500R−0.419G−0.081B+128 \begin{aligned} Y &= 0.299R + 0.587G + 0.114B \\ Cb &= -0.169R - 0.331G + 0.500B + 128 \\ Cr &= 0.500R - 0.419G - 0.081B + 128 \end{aligned} YCbCr=0.299R+0.587G+0.114B=−0.169R−0.331G+0.500B+128=0.500R−0.419G−0.081B+128
权重 0.299/0.587/0.114 不是随手定的------它们来自人眼视锥细胞对三种波长的相对敏感度。Y 承载了绝大部分感知信息,色度只是一层"染色"。
既然人眼对色度细节不敏感,就可以用更低的分辨率存色度,这就是色度亚采样:
| 格式 | 含义 | 每像素位数 |
|---|---|---|
| 4:4:4 | 色度全分辨率 | 24 bpp |
| 4:2:2 | 色度水平减半 | 16 bpp |
| 4:2:0 | 色度水平垂直都减半 | 12 bpp |
实测(256×256 合成图):
色度亚采样 4:2:0:采样数 196608 -> 98304,每像素 12.0 bpp(RGB24 为 24 bpp)
4:2:0 重建(最近邻重复)PSNR = 30.83 dB
4:2:0 重建(双线性) PSNR = 31.72 dB
需要解释这个 30.83 dB :它偏低,是因为本测试图刻意包含了高饱和的锐利色块(红色圆形),属于色度亚采样的最坏情形 。自然图像的色度变换平缓,典型 PSNR 在 40 dB 以上。这也说明一个实践要点:锐利的彩色边缘(尤其是纯红/纯蓝的细线、字幕)在 4:2:0 下会明显劣化------这是字幕发虚的根本原因,不是播放器的问题。
顺带注意到双线性上采样比最近邻好 0.89 dB,代价仅一次卷积------实际编解码器都用插值而非重复。
3.7 Gamma:为什么存储和显示不是线性的
光的物理量与像素值的关系,被故意做成非线性。原因是人眼的亮度感知近似对数------在暗处能分辨的亮度差,比在亮处小得多。
若用 8 bit 线性存储光强,暗部会因码字不足出现色带(banding)。解决办法是先在采集端做 gamma 编码:
Vcode=L1/γ,γ≈2.2 V_{\text{code}} = L^{1/\gamma}, \qquad \gamma \approx 2.2 Vcode=L1/γ,γ≈2.2
显示端再做 L=VcodeγL = V_{\text{code}}^{\gamma}L=Vcodeγ 还原。实测在 8 bit 下,暗部区间的可用码字数:
| 亮度区间 | 线性量化级数 | Gamma 编码后的级数 |
|---|---|---|
| L∈0,0.05L \in 0, 0.05L∈0,0.05 | 14 级 | 65 级 |
暗部分到的码字多了 4.6 倍。这就是 gamma 存在的唯一理由------把有限的码字重新分配给人眼更需要的地方。
现代 HDR 标准用更复杂的传递函数(PQ/HLG),但思想一脉相承:按感知分配码字,而不是按物理量均匀分配。
3.8 空间采样与混叠:摩尔纹从哪来
第 1.3 节的"栅栏"类比在图像上可以直接验证。图像是二维空间采样,同样受采样定理约束。
对一个频率为 fff(cycles/pixel)的正弦条纹图像做 3 倍抽取 (每隔 3 个像素取 1 个,不做低通滤波),新的采样率变为 1/3,新的奈奎斯特频率为:
fNyq′=12M=16≈0.167 cyc/px f_{\text{Nyq}}' = \frac{1}{2M} = \frac{1}{6} \approx 0.167\ \text{cyc/px} fNyq′=2M1=61≈0.167 cyc/px
超过这个值的空间频率必然混叠。实测:
| 原始频率 | 理论折叠后频率 | 实测表观频率 | 结论 |
|---|---|---|---|
| 0.05 cyc/px | 0.050 | 0.050 | 正常 |
| 0.15 cyc/px | 0.150 | 0.150 | 正常(刚好在奈奎斯特内) |
| 0.30 cyc/px | 0.033 | 0.033 | 混叠 |
| 0.40 cyc/px | 0.067 | 0.067 | 混叠 |
实测值与理论折叠频率 ∣f−kfsample∣|f - k f_{\text{sample}}|∣f−kfsample∣ 精确吻合。注意 0.30 和 0.40 这两个"很密"的条纹,抽取后反而变成了很稀疏的低频图案 ------这正是拍照时纱窗、栅栏、条纹衬衫出现水波纹(摩尔纹)的原因:密集条纹折叠成了低频图案,而你看到的"图案"在原图上根本不存在。
python
import numpy as np
M = 3
img = 0.5 + 0.5*np.sin(2*np.pi*f*x)[None, :] # 频率 f 的条纹
dec = img[:, ::M] # 3 倍抽取,未低通 ← 错误做法
# 正确做法:先抗混叠低通,再抽取
from scipy.signal import firwin, lfilter
lp = firwin(65, 1/(2*M), window=('hann',)) # 截止在 1/(2M)
filt = lfilter(lp, 1.0, img, axis=1)
dec_ok = filt[:, ::M]
这个错误在图像缩放、MIP 贴图生成、视频下采样里每天都在发生:直接隔点采样而不先低通。第 4.4 节提到的"隔行素材必须先去隔行",本质上也是同一个问题------隔行在时间/空间上抽了一半的样,直接当逐行处理就是混叠。
四、视频:在时间轴上再采样一次
4.1 视频 = 图像序列 + 时间采样
视频本质上是在时间轴上的第三次采样(前两次是图像的横纵)。这个再采样带来两个后果:
1. 数据量爆炸
码率=W×H×bpp×ffps×C \text{码率} = W \times H \times \text{bpp} \times f_{\text{fps}} \times C 码率=W×H×bpp×ffps×C
实测:1920×10801920\times10801920×1080、YUV420 8bit、60 fps 的原始码率:
1920×1080×12×60=1.49 Gbps 1920 \times 1080 \times 12 \times 60 = 1.49\ \text{Gbps} 1920×1080×12×60=1.49 Gbps
这是千兆网络的 1.5 倍。原始视频在存储和传输上完全不可用------这就是为什么视频必须压缩,没有任何商量余地。
2. 时间维度的采样也有奈奎斯特
帧率就是时间采样率。24 fps 意味着时间采样率 24 Hz,依据采样定理能表示的运动频率上限是 12 Hz。快速运动超过这个上限就会产生时间混叠------车轮倒转、螺旋桨反转、频闪效应都来源于此。
这也是为什么电影用快门角度限制(通常 180°,即每帧曝光 1/48 秒):不完全是为了艺术,更是为了引入运动模糊,在时间上起到抗混叠低通滤波的作用。
4.2 帧率与扫描方式
| 帧率 | 来源 / 用途 |
|---|---|
| 24 fps | 电影,历史成本妥协,奠定"电影感" |
| 25 fps | PAL 制电视,50 Hz 市电的一半 |
| 29.97 fps | NTSC,彩色副载波兼容遗留问题 |
| 50/60 fps | 高帧率电视,运动更流畅 |
| 120+ fps | 游戏、慢动作拍摄 |
隔行扫描 (interlaced)是 CRT 时代的带宽妥协:每帧只传奇数行或偶数行(一个 field),两场拼成一帧。代价是运动物体出现梳状伪影。数字时代已基本淘汰,但广播电视的大量素材仍是隔行的------处理隔行素材时必须先做去隔行(deinterlace),否则后续的帧间压缩会因上下场错位而效率暴跌。这是很多"旧素材转码后很糊"的根源。
4.3 视频为什么能压缩:三类冗余
原始数据之所以能被压掉两个数量级,是因为它包含三类冗余:
| 冗余类型 | 来源 | 对应技术 |
|---|---|---|
| 空间冗余 | 相邻像素高度相关 | 帧内预测、DCT 变换 + 量化 |
| 时间冗余 | 相邻帧几乎一样 | 运动估计 + 运动补偿 + 残差编码 |
| 视觉冗余 | 人眼对高频/色度细节不敏感 | 量化矩阵、色度亚采样、率失真优化 |
| 统计冗余 | 符号出现概率不均 | 熵编码(CAVLC / CABAC) |
4.4 压缩的核心:预测 + 残差
现代视频编码(H.264/HEVC/AV1/VVC)都是同一个混合框架:
输入帧 ──> 预测(帧内 or 帧间)──> 残差 = 原图 - 预测
│
v
DCT/整数变换 ──> 量化 ──> 熵编码 ──> 比特流
关键思想是:不直接编码像素值,而是编码"预测之后的残差"。因为预测通常很准,残差的能量远小于原图,量化后大部分系数变成 0。
为验证这个想法到底有多大收益,配套脚本实现了一个最小编码器(8×8 DCT + 均匀量化 + 经验熵估计),在 176×144 的 32 帧合成序列上实测:
| 方案 | 每帧比特 | bpp | PSNR (Y) | 压缩比 |
|---|---|---|---|---|
| 帧内 DCT(I 帧) | 27,396 | 1.081 | 45.16 dB | 22.2× |
| 帧间差分(无运动补偿) | 10,633 | 0.420 | 45.85 dB | 57.2× |
| 帧间 + 运动补偿(P 帧) | 6,039 | 0.238 | 46.52 dB | 100.7× |
结论极其清晰 :运动补偿的 P 帧只需要帧内 I 帧 22% 的码率,同时 PSNR 还高了 1.36 dB。时间冗余的消除带来 4.5 倍额外增益。
残差能量的变化解释了原因:
帧差能量(std)= 6.37,运动补偿后残差能量(std)= 0.81
→ 残差能量降为原来的 12.7%
残差能量降到 1/8,编码它所需的比特数自然大幅下降。这就是运动估计为什么是整个视频编码中计算量最大也最关键的模块。
为什么还要做 DCT:能量的重新分配
预测之后还有一步关键操作------把残差块做 8×8 DCT。不做变换直接量化行不行? 答案藏在"能量去哪了"里。
DCT 的作用是把空间域的能量重新分配到少数低频系数上。实测(每条曲线都先按块做了能量归一化,所以差异纯粹来自频谱形状,与信号强弱无关):
本实验使用与第 4.4 节完全相同的帧对(序列第 4→5 帧),残差统计量交叉核对一致:帧差 ∣|∣std∣|∣ = 6.37,运动补偿后 = 0.81,降为 0.127。
| 保留系数数 kkk(zigzag 序) | 白噪声 | 自然图像 | 帧差(未补偿) | 运动补偿残差 |
|---|---|---|---|---|
| 1 | 1.64% | 87.62% | 2.34% | 2.53% |
| 3 | 4.76% | 88.88% | 6.00% | 7.57% |
| 10 | 15.54% | 91.35% | 17.81% | 18.58% |
| 21 | 32.69% | 93.88% | 35.60% | 35.69% |
| 64 | 100.00% | 100.00% | 100.00% | 100.00% |
读这张表要抓两个极端:
- 自然图像:仅 DC 一项就占了 87.62% 的能量。这意味着"存 1 个数、丢 63 个数"就已经还原了绝大部分内容------这就是 JPEG 和 I 帧能压到 1 bpp 量级的根本原因
- 白噪声 :kkk 从 1 到 21 只累计到 32.69%,几乎线性增长。对白噪声做变换毫无收益,因为它的能量本来就均匀铺满整个频带
换句话说:DCT 本身不减少信息,它只是把信息挪到一起,好让量化可以"一刀切" 。这是整个变换编码的核心思想------变换不是压缩,为量化创造条件才是变换的目的。
再结合每个信号块的实际强度(8 bit 灰度,Q=8):
| 素材 | 块内 RMS | Q=8 时非零系数/块 |
|---|---|---|
| 自然图像 | 47.71 | 52.15 |
| 帧差(未补偿) | 5.88 | 0.82 |
| 运动补偿残差 | 1.14 | 0.12 |
| 白噪声 | 0.99 | 0.00 |
这一列才是最终码率的真身:在同样的量化步长下,自然图像每块有 52 个系数活着,而运动补偿残差只剩 0.12 个------相差 434 倍。
注意这里有个容易被忽略的细节:运动补偿残差的频谱比原图平得多 (k=10k=10k=10 时 18.58% vs 91.35%,见上一张表),单看这一点它是"更难压"的------它的块内 RMS 只有 1.14,已经接近白噪声对照组的 0.99,也就是说运动补偿做完后剩下的东西基本就是逐帧不相关的传感器噪声了,几乎没有任何结构可供利用。
但它的总能量又小得多(RMS 1.14 vs 47.71,相差 42 倍)。最终码率 = 频谱平坦度 × 总能量,第二个因子压倒性地赢了,所以 P 帧还是便宜 4.5 倍。
顺带解释了编码器里的一个设计选择:为什么现代编码器对残差倾向用小变换块(4×4)、对帧内块倾向用大块(甚至 64×64)?因为平坦的频谱下小块变换反而更省------没有集中的能量可供一个 64×64 的变换去"搜集"。HEVC/AV1 里那些灵活的块划分,本质上就是在为每个残差的特性匹配最合适的"能量收集器"。
4.5 率失真关系
改变量化步长 Q,可以得到典型的率失真曲线(实测):
| Q | 帧内 bpp | 帧内 PSNR | P 帧 bpp | P 帧 PSNR |
|---|---|---|---|---|
| 4 | 1.801 | 49.54 dB | 2.135 | 48.09 dB |
| 8 | 1.292 | 46.99 dB | 0.309 | 46.98 dB |
| 12 | 1.087 | 45.17 dB | 0.248 | 46.67 dB |
| 24 | 0.922 | 42.44 dB | 0.207 | 46.01 dB |
| 48 | 0.773 | 38.15 dB | 0.179 | 45.11 dB |
这张表藏了一个反直觉但真实的现象 :在 Q=4(接近无损)时,P 帧的码率反而比 I 帧更高(2.135 vs 1.801 bpp)。原因是:
- 运动矢量本身要占码率,而在高质量档位,这部分开销无法通过残差压缩的收益抵掉
- 本实验的合成序列每帧重新生成随机噪声,噪声在时间上不相关,无法被运动补偿预测------这是帧间预测的 worst case
一旦量化粗一点(Q ≥ 8),P 帧的优势立刻爆发:Q=8 时从 1.292 降到 0.309 bpp(4.2 倍),且画质几乎不变。
这解释了一个工程经验:帧间压缩在中等及以下码率区间收益最大;追求接近无损时,帧内压缩反而可能更划算。
4.6 GOP 与帧类型
| 帧类型 | 参考关系 | 压缩率 | 作用 |
|---|---|---|---|
| I 帧(Intra) | 不参考任何帧 | 最低 | 随机访问点、防止误差传播 |
| P 帧(Predicted) | 参考前面的帧 | 中 | 主力 |
| B 帧(Bi-predictive) | 双向参考 | 最高 | 进一步压缩,代价是延迟 |
一组连续的帧构成 GOP (Group of Pictures),典型结构 IBBPBBPBBPBBPBB。
由此引入一个重要概念:DTS 与 PTS 分离 。因为 B 帧需要参考后面的帧,所以编码顺序 ≠ 显示顺序:
显示顺序: I B B P B B P
1 2 3 4 5 6 7
编码顺序: I P B B P B B
1 4 2 3 7 5 6
每个帧有两个时间戳:PTS (Presentation Time Stamp,何时显示)和 DTS(Decoding Time Stamp,何时解码)。搞混这两个是播放器音画不同步、seek 不准的常见原因。
五、图示:三者在"维度"上的统一
把三者的形成过程画在一起:
声音: ═══════●═══════●═══════●═══════●══════>
t0 t1 t2 t3 一维时间采样
图像: ●──●──●──●
│ │ │ │ 二维空间采样
●──●──●──● (CFA 在每个点上只取 1/3 颜色)
●──●──●──●
视频: ●──●──●──● ●──●──●──● ●──●──●──●
│ │ │ │ → │ │ │ │ → │ │ │ │
●──●──●──● ●──●──●──● ●──●──●──●
帧 k 帧 k+1 帧 k+2 三维:再加时间采样
三者的理论完全相同(采样定理),差别只在采样发生的维度数和每个物理量对应的"分辨率需求"。
5.1 一次完整的旅程:把前文所有环节串起来
最后把散落在各章的环节串成两条完整的链,每一步都标注"发生了什么、为什么、代价是什么"。
一条声音的链:一个 440 Hz 音符
| 站点 | 发生了什么 | 数量级 / 代价 |
|---|---|---|
| 琴弦振动 | 440 Hz 基频 + 2~7 次泛音(第 2.2 节),声压扰动约 20 mPa | 音色由谐波分布决定 |
| 麦克风振膜 | 声压 → 位移 → 电容变化 → 电压(灵敏度 ~20 mV/Pa) | 输出仍是连续模拟量 |
| 前置放大 | 把 mV 级信号抬到 ADC 量程,此时就要定 headroom | 每留 1 dB,SQNR 少 1 dB(2.4 节) |
| ADC 采样 | 48 kHz,每个周期 109 个样点 | 奈奎斯特 24 kHz,覆盖人耳上限 |
| ADC 量化 | 16 bit,理论 98.08 dB(若吃满量程) | 误差要么是谐波要么是噪声(2.5 节) |
| 抖动 | 加 TPDF 噪声 2 LSB 峰峰 | 底噪 +3 dB,谐波尖峰 −17~37 dB |
| 存储 | PCM 16 bit 立体声 = 1411 kbps | 1 分钟 ≈ 10 MB |
| 感知编码 | 掩蔽门限以下的谐波直接丢弃 | MP3 128 kbps,压缩 11× |
一条图像的链:一张夕阳照片
| 站点 | 发生了什么 | 数量级 / 代价 |
|---|---|---|
| 场景光学 | 连续光谱 + 连续空间分布;镜头完成第一次抗混叠低通 | 跳过它,PSNR 崩 15.65 dB(3.5 节) |
| CMOS 曝光 | 光子 → 电子,泊松到达 | 亮部 ~20000 e⁻(43 dB),暗部 ~200 e⁻(23 dB) |
| 读出 | 电子 → 电压 → ADC | 读出噪声 ~3 e⁻,只在暗部可见 |
| Bayer CFA | 每像素只留 1/3 颜色,G 占一半 | 数据量降到 1/3 |
| 去马赛克 | 色差域插值恢复 RGB | +3.61 dB(相对双线性) |
| Gamma 编码 | V=L1/2.2V = L^{1/2.2}V=L1/2.2 | 暗部码字多 4.6 倍 |
| YCbCr + 4:2:0 | 24 bpp → 12 bpp | 色度边缘略糊(3.6 节) |
| JPEG | 8×8 DCT:DC 一项就占 87.62% 能量 | 量化丢高频,熵编码收尾 |
两条链的形状是一样的:前半段是物理(换能 + 采样),后半段是感知(把比特从感知不到的地方挪走)。视频只是把第二条链在时间轴上再跑一遍,并用运动补偿把"没变的部分"省掉(实测 4.5× 增益)。
理解了这两条链,就拿到了读懂任何音视频格式的钥匙:遇到一个新格式,只要问三个问题------它采样了哪个物理量、它把哪些比特判为"感知不到"、它用什么先验去预测没存的数。
六、源码拆解:容器格式与像素布局
理论讲完,落到具体字节。这一节用 C++ 手写 WAV 和 BMP,展示"文件里到底存了什么"。
6.1 WAV:RIFF 容器的 44 字节头
WAV 不是编码格式,它是装 PCM 的容器。结构是标准的 RIFF(Resource Interchange File Format)嵌套块:
| 偏移 | 长度 | 内容 | 值(48k/16bit/单声道) |
|---|---|---|---|
| 0 | 4 | "RIFF" | 0x52494646 |
| 4 | 4 | ChunkSize = 文件长 − 8 | 36 + data_len |
| 8 | 4 | "WAVE" | |
| 12 | 4 | "fmt " | |
| 16 | 4 | fmt 块长度 | 16(PCM) |
| 20 | 2 | 音频格式 | 1 = PCM |
| 22 | 2 | 声道数 | 1 |
| 24 | 4 | 采样率 | 48000 |
| 28 | 4 | 字节率 = fs×ch×bytes | 96000 |
| 32 | 2 | 块对齐 = ch×bytes | 2 |
| 34 | 2 | 位深 | 16 |
| 36 | 4 | "data" | |
| 40 | 4 | data 长度 | 96000 |
| 44 | ... | PCM 样本(小端) |
cpp
// 关键点 1:WAV/BMP 都是 little-endian,必须逐字节写入
static void put_u32_le(std::vector<uint8_t>& b, uint32_t v) {
b.push_back(v & 0xFF);
b.push_back((v >> 8) & 0xFF);
b.push_back((v >> 16) & 0xFF);
b.push_back((v >> 24) & 0xFF);
}
// 关键点 2:不要直接 fwrite 一个 struct!
// 编译器会按对齐规则插入 padding,结构体大小和文件布局对不上。
// 逐字段 serialize 才是可移植做法。
实测输出:
WAV out_tone.wav 头部 44 字节, 数据 96000 字节, 共 96044 字节
理论: 1秒 × 48000 × 2字节 = 96000 字节 PCM + 44 头 = 96044
6.2 BMP:三件容易踩的事
BMP 是最简单的图像格式(24bit 未压缩),但有三个陷阱:
cpp
int stride = ((w * 3 + 3) / 4) * 4; // (a) 每行必须 4 字节对齐
// ...
for (int y = h - 1; y >= 0; --y) { // (c) 自下而上存储
for (int x = 0; x < w; ++x) {
uint8_t px[3] = { row[x*3+2], row[x*3+1], row[x*3+0] }; // (b) BGR!
fwrite(px, 1, 3, f);
}
if (padding) fwrite(pad.data(), 1, padding, f);
}
- (a) 行对齐:每行字节数必须是 4 的倍数,不足补零。这是为了 DMA 对齐的历史遗留
- (b) 通道顺序是 BGR,不是 RGB
- © 自下而上:第一行数据其实是图像的最后一行
实测(高度都是 60):
BMP out_grad_100.bmp 100x60 原始 18000 字节 -> 文件 18054 字节 (每行补齐 0 字节)
BMP out_grad_101.bmp 101x60 原始 18180 字节 -> 文件 18294 字节 (每行补齐 1 字节, 共浪费 60 字节)
宽度从 100 变成 101,每行就多浪费 1 字节 ------因为 100×3=300100\times3=300100×3=300 恰好被 4 整除,而 101×3=303101\times3=303101×3=303 需要补到 304。这个细节在写图像 I/O 时不注意就会出现"倾斜的图像"(每行错位一点点)。
6.3 YUV 到 RGB:定点化与范围陷阱
视频解码最后一环是 YUV→RGB 上屏。用整数定点避免浮点开销:
cpp
// BT.601「有限范围」(Y:16~235, UV:16~240) 整数实现
int yy = Y[y*w+x] - 16; // 注意减去 16
int u = U[(y/2)*(w/2) + x/2] - 128;
int vv = V[(y/2)*(w/2) + x/2] - 128;
int r = (298 * yy + 409 * vv + 128) >> 8; // 298/256 ≈ 1.164
int g = (298 * yy - 100 * u - 208 * vv + 128) >> 8;
int b = (298 * yy + 516 * u + 128) >> 8;
// 技巧:+128 后 >>8 等价于「除以 256 并四舍五入」,比真除法快一个数量级
实测验证:
定点实现左上像素 RGB = (245, 72, 130)
参考浮点 BT.601 有限范围: R=245.3 G=71.8 B=130.4 <- 一致
(若误用全范围系数: R=212.9 G=60.6 B=112.0 <- 明显偏色)
这里藏着视频工程里最常见的一个坑:BT.601 有「有限范围」(Y 去除了 16 偏移,系数 1.164/1.596...)和「全范围」(Y 不减 16,系数 1.402/1.772...)两套约定。混用会导致整体偏色。实测同一组 YUV,用错范围得到的 R 从 245 变成 213(偏了 32/255 ≈ 12.5%),肉眼看起来就是"画面发灰、肤色不对"。
判断方法很简单:如果 Y=16 处对应纯黑、Y=235 对应纯白,就是有限范围(电视/视频标准);Y=0 为黑、Y=255 为白则是全范围(JPEG/计算机图形)。YUV 数据源来自摄像头/广播电视就用有限范围。
七、踩坑点清单
按"物理量 → 比特流"的链条顺序整理:
1. 麦克风/扬声器的相位不要忽略
两个麦克风录同一声源,如果相位搞反(或接线反了),叠加时会互相抵消。立体声录音、阵列麦克风必须确认相位一致性。
2. 音频必须用对数刻度思考
音量翻倍对应 +6 dB,感知"响一倍"约 +10 dB。用线性值做混音/淡入淡出会得到"前半段变化剧烈、后半段几乎没动"的糟糕体验。淡入淡出应在 dB 域线性插值。
3. 采样率不匹配导致音高偏移
用 48 kHz 设备播放按 44.1 kHz 采样的音频且未重采样,音高会升高 48/44.1≈8.8%48/44.1 \approx 8.8\%48/44.1≈8.8%。这个错误在通信 VoIP 里极常见------听起来"声音有点尖"。
4. SQNR 的理论值别忘了加 headroom 项
6.02B+1.766.02B+1.766.02B+1.76 只在正弦恰好吃满量程时成立。信号峰值低 1 dB,SQNR 就实打实少 1 dB(第 2.4 节实测:留 −20 dB headroom 时 16 bit 的 98.08 dB 只剩 77.98 dB)。录音电平和位深同等重要,一个峰值只到 −20 dBFS 的 24 bit 母带,实际动态范围退回 20 bit。
4b. 用整数分频的单音测出来的谐波失真不是"正常水平"
1000 Hz @ 48 kHz 这样每周期恰好整数个样点的测试音,量化误差会逐周期完全重复(实测相邻周期相关系数 = 1.000),退化成谐波尖峰;换成 997 Hz 相关系数就降到 0.014、最大杂散改善 13 dB。测 THD+N 时务必让测试频率避开 fs 的整数分之一,否则测的是最坏情况。
4c. 做深度位深截断时一定要加抖动
把 24 bit 母带直接截到 16 bit 而不加 TPDF 抖动,低电平细节会变成与信号强相关的谐波失真而不是噪声------前者无法通过多次平均消除,后者可以(第 2.5 节实测:0.4 LSB 信号加抖动后同步平均 256 次,相关系数回到 0.9925)。母版处理的最后一步永远是抖动。
4d. 8 bit 图像不要在线性光空间处理
暗部只有十几个码字(第 3.7 节实测:线性量化下 L∈0,0.05L\in0,0.05L∈0,0.05 只有 14 级),做 gamma 调整、叠加会立刻出现色带。正确做法是转换到线性空间运算,再转回 gamma 空间存储。顺序错了就会出现"越调越花"。
4e. 不要指望高 ISO 带来更多信息
参见第 3.4 节实测:亮光下增益从 1 提到 4,SNR 只变了 0.02 dB,而饱和电子数按 1/A1/A1/A 下降。高 ISO 只在极暗处有用(N=5N=5N=5 时能从 1.66 dB 提升到 7.00 dB),且上限被散粒噪声 10log10N10\log_{10}N10log10N 钉死。
5. 去马赛克的评测素材必须物理正确
两个陷阱:一是缺了镜头光学低通 会让 PSNR 从 31.95 崩到 16.30 dB(第 3.5 节实测,掉 15.65 dB,远大于算法改进的收益);二是用三通道互相独立的合成图 会让色差域插值这类算法被误判为"更差"。正确做法是用真实照片或通道相关的自然图像,并先用 ∣∇(R−G)∣/∣∇G∣|\nabla(R-G)|/|\nabla G|∣∇(R−G)∣/∣∇G∣ 确认先验是否成立。
5b. Bayer 去马赛克必须归一化邻居权重
如第 3.5 节所强调,固定核卷积在对角缺失位置会少算一半。这是个"看起来能跑、结果偏暗"的隐蔽 bug。
6. 4:2:0 会毁掉锐利的彩色边缘
纯色细线、字幕、UI 截图在这种亚采样下会明显发糊。做屏幕/文字内容时应该用 4:4:4,这也是为什么远程桌面、游戏串流对色度分辨率特别敏感。
7. YUV 范围混淆导致整体偏色
有限范围 vs 全范围,见第 6.3 节。这是"解码出来颜色不对"的第一排查项。
8. 隔行素材必须去隔行后再做帧间压缩
否则上下场错位会让运动估计失效,压缩效率暴跌且出现梳状伪影。很多"老片转码后画质变差"根因在此。
9. PTS 和 DTS 不是一回事
有 B 帧时二者必然不同。用 PTS 做解码调度会崩溃或音画不同步。
10. 关键帧间隔(GOP)不是越长越好
GOP 越长压缩率越高,但:随机访问延迟增大、错误传播范围增大、seek 需要解更多帧。直播通常设 1~2 秒一个 IDR,点播可以到 10 秒。
11. 容器格式 ≠ 编码格式
MP4/MKV/TS 是容器,H.264/HEVC/AV1/AAC 是编码格式。.mp4 里可能装的是 H.265,mkv 里可能装的是 H.264。报错 "不支持的文件格式" 时要分清是容器不支持还是编码不支持------用 ffprobe 而不是凭扩展名判断。
12. 别用 rmvb/avi 时代的直觉判断码率
现代编码格式效率差异巨大:同画质下 H.265 比 H.264 省约 40%,AV1 又省约 20%。拿 H.264 时代的码率标准套 AV1 会得出完全错误的结论。
八、效果对比
8.1 原始数据量对比
| 内容 | 原始数据率 | 压缩后典型码率 | 压缩比 |
|---|---|---|---|
| CD 音质立体声 | 1411 kbps | MP3 128 kbps / AAC 96 kbps | 11~15× |
| 1200 万像素照片 | 36 MB(RGB24) | JPEG 3~5 MB | 7~12× |
| 1080p60 视频 | 1.49 Gbps | H.264 5 Mbps / H.265 3 Mbps | 300~500× |
视频的压缩比远高于图像,因为它多吃了一维时间冗余------这与第 4.4 节的实测(P 帧比 I 帧省 4.5 倍)完全吻合。
8.2 感知 vs 客观指标的背离
一个重要提醒:本文大量使用 PSNR,但它不能很好反映主观感受。
| 失真类型 | PSNR 表现 | 主观感受 |
|---|---|---|
| 整体模糊 | PSNR 降 | 明显劣化 |
| 块效应 | PSNR 较高 | 主观很差 |
| 色度亚采样 | PSNR 30~40 dB | 除彩色边缘外几乎无损 |
| 振铃 | PSNR 略降 | 边缘有"重影",很扎眼 |
实践建议:PSNR 用于快速比较同一算法的变体、判断趋势很有用,但做画质调优时必须配主观观看。SSIM、VMAF 与主观评价的相关性更好,其中 VMAF 是目前流媒体行业的事实标准。
九、结论
9.1 核心结论回顾
| 问题 | 答案 |
|---|---|
| 声音是什么 | 空气压强的周期扰动;音高 = 基频,音色 = 谐波能量分布 |
| 为什么听觉用对数 | 感知的动态范围达 10610^6106,天然对数刻度 |
| 图像为什么三个数字 | 人眼只有三类视锥细胞,颜色是三维投影 |
| 为什么每个像素只有一色 | Bayer CFA;G 占一半是因为亮度感知主要来自中波长 |
| 为什么用 YCbCr | 亮度和色度可分离,便于对色度降采样 |
| 为什么有 gamma | 把有限的码字按感知重新分配,暗部多分 4.6 倍 |
| 视频为何必须压缩 | 1080p60 原始 1.49 Gbps,超过千兆网 |
| 压缩为何有效 | 三类冗余;运动补偿实测带来 4.5× 增益 |
| 为什么压缩前要做 DCT | 变换本身不压数据,而是把能量重分配到少数系数,让量化可以"一刀切" |
| 为什么实测 SQNR 总比理论低 | 6.02B+1.766.02B+1.766.02B+1.76 要求正弦吃满量程;每留 1 dB headroom 就少 1 dB |
| 为什么母版处理最后一步是抖动 | 把不可逆的谐波失真换成可被平均消掉的随机噪声 |
| 为什么暗光一定有噪点 | 光子到达是泊松过程,SNR=10log10N\mathrm{SNR}=10\log_{10}NSNR=10log10N 是量子极限 |
| 为什么高 ISO 救不了画质 | 动态范围按 1/A1/A1/A 下降,SNR 上限被散粒噪声钉死 |
| 所有压缩技术的共同点 | 把比特花在人能感知的地方 |
9.2 一条主线
回头看,从物理量到比特流的每一步,都是在利用"感官的局限":
- 音频只采到 20 kHz,是因为人耳听觉上限就在那里
- 低于某个频率/强度的声音不予保留,是因为超过了绝对听阈
- 量化到 8 bit,是因为人耳分辨不出更细的动态
- 每个像素只存一种颜色,是因为人眼对色度细节不敏感
- 色度亚采样到 1/4,同样的理由
- gamma 非线性量化,是按亮度感知密度重新分配码字
- 丢弃被掩蔽的谐波与高频,是因为它们根本感知不到
所有多媒体压缩的本质,都是"把比特花在人能感知的地方"。 理解这一点,就理解了为什么同样"看起来差不多"的两个编码器,效率能差一倍------它们对这个"该花在哪"的判断不同。
9.3 延伸方向
按本文的链条,下一步可以深入:
- 生理层面:耳蜗基底膜的频率-位置映射(正是 FFT 的生物实现)、视觉皮层 V1 的方向选择性(正是 Gabor 小波,启发了整个方向滤波器理论)
- 彩色 science:色域映射、ICC 色彩管理、HDR 的 PQ/HLG 传递函数
- 现代编码器:HEVC 的 CTU 四叉树、AV1 的 Superblock、VVC 的 MTT;率失真优化如何做决策
- 神经压缩:用神经网络做端到端图像/视频编码,正在挑战传统混合框架
附录:配套代码
本文所有数值来自两个脚本的实测输出。
Python :av_formation_demo.py
bash
pip install numpy scipy matplotlib
python av_formation_demo.py # 跑全部 9 组实验
python av_formation_demo.py --only sound # 声音:加法合成、WAV、位深 SQNR、headroom
python av_formation_demo.py --only image # 图像:Bayer、4:2:0、gamma
python av_formation_demo.py --only video # 视频:帧内/帧间编码、运动补偿、率失真
python av_formation_demo.py --only timefreq# STFT:时频分辨率权衡
python av_formation_demo.py --only alias # 空间混叠:摩尔纹
python av_formation_demo.py --only dither # 抖动:误差相关性、谐波失真、同步平均恢复
python av_formation_demo.py --only noise # 传感器:散粒噪声标度律、读出噪声、ISO
python av_formation_demo.py --only dct # DCT 能量集中曲线与量化后非零系数统计
python av_formation_demo.py --only demosaic# 去马赛克:双线性 vs 色差域插值
| 实验 | 命令 | 产出 |
|---|---|---|
| A 声音 | --only sound |
A1_spectrum.png(长笛 vs 小提琴谱线)、位深 SQNR 表、headroom 校正表 |
| A2 抖动 | --only dither |
A2_dither.png( sub-LSB 恢复、谐波 vs 底噪谱)、误差相关性表 |
| A3 时频 | --only timefreq |
A3_stft.png(语谱图)、Δf⋅Δt=4\Delta f\cdot\Delta t=4Δf⋅Δt=4 实测表 |
| B 图像 | --only image |
B1_bayer_420.png(原图/去马赛克/4:2:0 对比)、gamma 级数表 |
| B2 混叠 | --only alias |
B2_alias.png(四种频率的抽取结果)、理论 vs 表观频率表 |
| B3 噪声 | --only noise |
B3_sensor_noise.png(SNR--电子数曲线、ISO 曲线)、两张噪声表 |
| B4 去马赛克 | --only demosaic |
B4_demosaic.png(算法对比)、先验强度与 PSNR 表 |
| C 视频 | --only video |
C1_residual.png(帧差 vs 运动补偿残差)、三种方案码率/PSNR、率失真表 |
| D 变换 | --only dct |
D1_dct_energy.png(能量集中曲线、系数矩阵热力图)、平坦度与 RMS 表 |
其中 B3/B4/D 三组使用了一幅统计特性等效于自然图像的合成素材 (二维 1/f 纹理 + 乘性色度模型),而不是依赖联网下载的真实照片------这样做的好处是完全可复现,且参数(通道相关性、是否加光学低通)可以精确控制。生成函数 natural_image() 与 emerge_lens() 都在脚本里。
C++ :av_format_demo.cpp
bash
g++ -O2 -std=c++17 av_format_demo.cpp -o av_format_demo
./av_format_demo
输出 WAV 头部/数据大小、BMP 行对齐浪费、YUV→RGB 定点值与参考值对比,并生成 out_tone.wav、out_grad_100.bmp、out_grad_101.bmp 供十六进制查看。
环境:Python 3.13 + NumPy 2.5.4 + SciPy 1.18.1,g++ (MinGW-w64) 。文中所有数值均在此环境实测。