第13章:OTF与MTF:将PSF转换为频率响应
我们以一条曲线开启了这本书。
MTF曲线出现在光学设计软件中,仿佛是一个最终结论:这里锐利,那里薄弱,某个视场较好,另一个视场较差。它看起来清晰、有权威。只需读出够多的信息,就足以带来风险。
但MTF曲线并非天生就是曲线。
它是一条计算链条的终点。
现在,这条链条已不再隐藏:
镜头处方
→ 光线追迹
→ 光程长度
→ OPD
→ 光瞳函数
→ PSF
→ OTF
→ MTF
在第12章中,我们从复光瞳函数计算了PSF。PSF告诉我们一个理想点如何在像面上扩散。
现在我们问一个不同的问题:
如果物体包含不同空间频率的图案,
光学系统对每种频率的传递有多强?
这个问题引出了光学传递函数,即OTF。
MTF,即调制传递函数,是OTF的幅值。
最简洁的形式是:
这两行很简单。它们的含义却不小。
本章中,PSF将不再仅仅是一个点的像,而成为频率响应。
1. MTF因何存在
PSF回答:
系统对一个点做了什么?
MTF回答:
系统对不同细节尺寸的对比度做了什么?
物体可被看作包含许多空间频率。
大而缓慢的变化属于低空间频率。精细细节属于高空间频率。
例如:
- 一大块灰色区域是低频;
- 粗的黑白条纹图形是较低频率;
- 非常细的条纹图形是较高频率;
- 微小纹理、发丝、织物编织或精细印刷文字含有高频信息。
光学系统对这些频率的传递是不均等的。
低频对比度可能很好地保留。高频对比度可能被削弱。在某一点上,精细细节可能不再以有效对比度传递。
这正是MTF曲线所展示的。
典型的MTF曲线在零频处始于近1,并随频率升高而下降。
这并不意味着每个系统在所有方向上都平滑下降。真实系统可能存在结构、方向差异、视场依赖、波长依赖以及采样问题。但第一个思维模型是:
MTF告诉我们对比度传递如何随空间频率变化。
这个句子有用,但不完整。本章的后续部分将使其可计算。
2. 将PSF视为冲激响应
要理解为何PSF的傅里叶变换得到OTF,我们需要一个来自成像理论的概念。
对于非相干、平移不变的成像系统,图像可以建模为由PSF模糊的物体。
用简洁的符号表示:
其中 表示卷积。
这意味着:
最终图像是通过将每个物点替换为PSF的副本,
并将所有这些副本叠加而构成的。
这是一个强有力的概念。
若PSF狭窄,点保持集中,图像看起来锐利。
若PSF宽阔,每个物点扩散得更广,图像失去精细细节。
卷积定理进而告诉我们:
PSF的傅里叶变换就是系统的传递函数:
因此OTF告诉我们物体的每个空间频率成分如何被光学系统改变。
这就是PSF和OTF是同一成像行为的两个视角的原因:
PSF:空域模糊
OTF:频域传递
它们并非悬浮于空间的独立测量量。它们是一对傅里叶变换对。
3. OTF是复数的;MTF是其幅值
OTF通常是复数的:
它具有:
- 幅值;
- 相位。
幅值就是MTF:
相位通常被称为相位传递函数,即PTF:
许多光学设计讨论聚焦于MTF,因为对比度传递极为有用。但MTF丢掉了OTF的相位部分。
这不是一个小细节。
MTF告诉你每个空间频率下剩下多少对比度。但它不能完全告诉你结构如何移动、是否出现相位反转,或不对称模糊如何影响图像外观。
这是本书的一个核心判断:
MTF很重要,但并非图像质量故事的全部。
这不是反对MTF的论点。而是反对将一条曲线视为包含了所有光学真值。
PSF、OTF相位、视场依赖、波长依赖、畸变、传感器采样和图像处理都可能重要。
目前,我们将直接从PSF计算OTF和MTF。然后我们将学习如何解读结果,而不盲目崇拜它。
4. 归一化:MTF为何始于1
零频成分表示平均亮度传递。对于一个总能量为1的归一化PSF:
零频处的OTF值也应为1:
因此零频处的MTF是:
在数值代码中,由于FFT惯例,可能会出现小幅缩放差异。最安全的实践方法是用零频值对OTF进行归一化:
然后:
这使得MTF从1开始。
在代码中,将FFT输出移位使零频居中后:
center = (otf.shape[0] // 2, otf.shape[1] // 2)
otf = otf / otf[center]
mtf = np.abs(otf)
这一小步归一化能避免许多困惑。
如果MTF不从1开始,不要立即更改光学模型。首先检查归一化。
5. 从PSF计算OTF和MTF
我们直接基于上一章的代码构建。
保持相同的概念流程:
光瞳函数
→ PSF
→ OTF
→ MTF
以下是最简OTF/MTF函数:
import numpy as np
def compute_otf_mtf(psf):
"""
从归一化或未归一化的PSF计算光学传递函数和调制传递函数。
Parameters
----------
psf : 2D array
点扩散函数。
Returns
-------
otf : 2D complex array
归一化后的光学传递函数。
mtf : 2D array
调制传递函数。
"""
otf = np.fft.fftshift(
np.fft.fft2(
np.fft.ifftshift(psf)
)
)
center = (otf.shape[0] // 2, otf.shape[1] // 2)
if np.abs(otf[center]) > 0:
otf = otf / otf[center]
mtf = np.abs(otf)
return otf, mtf
该结构与PSF计算相似,但对象不同。
在第12章中:
复光瞳的FFT → 复像场
平方幅值 → PSF
此处:
PSF的FFT → OTF
幅值 → MTF
不要将这两步混淆。
一个常见的错误计算是取光瞳的FFT并将其幅值称为MTF。这跳过了PSF到OTF的关系,将相干场传播与非相干图像传递混淆了。
本章的正确链条是:
先得到PSF。
再得到OTF。
然后得到MTF。
6. 复用PSF代码
为完整起见,以下是我们所需的一组小型工作函数。
这不是一个完整的光学设计程序。这是暴露出来的计算骨架。
import numpy as np
import matplotlib.pyplot as plt
def make_pupil_grid(n=256):
coord = np.linspace(-1.0, 1.0, n)
x, y = np.meshgrid(coord, coord)
rho = np.sqrt(x**2 + y**2)
theta = np.arctan2(y, x)
mask = rho <= 1.0
return x, y, rho, theta, mask
def pupil_function_from_opd(opd, mask, wavelength, amplitude=None):
if amplitude is None:
amplitude = mask.astype(float)
phase = 2.0 * np.pi * opd / wavelength
pupil = amplitude * np.exp(1j * phase)
pupil[~mask] = 0.0 + 0.0j
return pupil, phase
def pad_array_centered(array, pad_factor=2):
if pad_factor == 1:
return array
n, m = array.shape
new_n = n * pad_factor
new_m = m * pad_factor
padded = np.zeros((new_n, new_m), dtype=array.dtype)
start_n = (new_n - n) // 2
start_m = (new_m - m) // 2
padded[start_n:start_n + n, start_m:start_m + m] = array
return padded
def compute_psf(pupil, pad_factor=4, normalize=True):
padded_pupil = pad_array_centered(pupil, pad_factor=pad_factor)
field = np.fft.fftshift(
np.fft.fft2(
np.fft.ifftshift(padded_pupil)
)
)
psf = np.abs(field) ** 2
if normalize:
total = np.sum(psf)
if total > 0:
psf = psf / total
return psf, field
def compute_otf_mtf(psf):
otf = np.fft.fftshift(
np.fft.fft2(
np.fft.ifftshift(psf)
)
)
center = (otf.shape[0] // 2, otf.shape[1] // 2)
if np.abs(otf[center]) > 0:
otf = otf / otf[center]
mtf = np.abs(otf)
return otf, mtf
现在我们可以制作一个完美的圆形光瞳并计算其PSF和MTF。
wavelength = 550e-9
x, y, rho, theta, mask = make_pupil_grid(n=256)
zero_opd = np.zeros_like(rho)
perfect_pupil, _ = pupil_function_from_opd(
opd=zero_opd,
mask=mask,
wavelength=wavelength
)
perfect_psf, _ = compute_psf(
perfect_pupil,
pad_factor=4
)
perfect_otf, perfect_mtf = compute_otf_mtf(perfect_psf)
此时,perfect_mtf 是一个二维数组。
这一点很重要。MTF并非天生就是一条曲线。它首先是一个二维频率响应:
我们熟悉的曲线通常是该二维函数的一个切片。
这个切片可以是水平的、垂直的、弧矢的、子午的、径向的或平均的,依上下文而定。
曲线并非全貌。它是一个选定的视图。
7. 显示二维MTF
首先我们查看二维MTF。
def show_mtf_2d(mtf, title="MTF"):
plt.figure(figsize=(5, 4))
plt.imshow(mtf, origin="lower", vmin=0, vmax=1)
plt.colorbar(label="MTF")
plt.title(title)
plt.xlabel("Frequency sample x")
plt.ylabel("Frequency sample y")
plt.tight_layout()
plt.show()
show_mtf_2d(perfect_mtf, title="Diffraction-limited 2D MTF")
对于完美的圆形光瞳,二维MTF应是径向对称的。它在中心最高,然后向截止频率递减。
这是衍射受限PSF在频域的对应物。
PSF展示了空间中类艾里斑的模糊图案。
MTF展示了该模糊如何降低不同空间频率的对比度。
一个并不比另一个更真实。它们是同一成像行为的两种表示。
PSF窄
→ 高频传递更强
PSF宽
→ 高频传递更弱
这种关系不仅仅是视觉上的。它源自傅里叶变换。
8. 提取简单的MTF曲线
大多数光学设计软件不仅显示二维MTF图,通常还展示一维曲线。
对于轴上旋转对称系统,穿过MTF中心的水平或垂直切片可能足以进行简单演示。
我们提取中心切片。
def central_mtf_slices(mtf):
"""
提取穿过中心的水平和垂直MTF切片。
Returns
-------
freq_index : 1D array
相对于零频的采样索引。
horizontal : 1D array
沿水平频率轴的MTF。
vertical : 1D array
沿垂直频率轴的MTF。
"""
n, m = mtf.shape
cy = n // 2
cx = m // 2
horizontal = mtf[cy, :]
vertical = mtf[:, cx]
freq_index = np.arange(m) - cx
return freq_index, horizontal, vertical
现在只绘制正频部分:
def plot_mtf_slices(mtf, title="MTF slices"):
freq_index, horizontal, vertical = central_mtf_slices(mtf)
center = len(freq_index) // 2
freq_positive = freq_index[center:]
horizontal_positive = horizontal[center:]
vertical_positive = vertical[center:]
plt.figure(figsize=(6, 4))
plt.plot(freq_positive, horizontal_positive, label="Horizontal slice")
plt.plot(freq_positive, vertical_positive, "--", label="Vertical slice")
plt.xlabel("Frequency sample index")
plt.ylabel("MTF")
plt.ylim(0, 1.05)
plt.title(title)
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.show()
plot_mtf_slices(perfect_mtf, title="Diffraction-limited MTF slices")
对于理想圆形光瞳,两条曲线应几乎相同。小的数值差异可能来自采样。
这是我们得到的第一条MTF曲线。
它仍以采样索引为单位,而不是每毫米线对。这对理解链条是可以接受的。物理频率单位需要坐标缩放,第14章将花更多时间讨论这一点。
目前,重点在于:
这条曲线来自二维MTF的一个切片,
二维MTF来自PSF的傅里叶变换,
PSF来自光瞳函数的傅里叶变换。
这就是打开的黑箱。
9. 加入像差并观察MTF下降
现在创建一个带像差的光瞳并比较MTF曲线。
再次使用教学用的OPD图:
def synthetic_opd(rho, theta, mask, wavelength):
"""
创建一个教学用的OPD图(单位:米)。
这是教学波前,并非完整光学设计结果。
"""
opd = np.zeros_like(rho)
defocus = 0.50 * wavelength
astigmatism = 0.25 * wavelength
opd += defocus * (2.0 * rho**2 - 1.0)
opd += astigmatism * rho**2 * np.cos(2.0 * theta)
opd[~mask] = 0.0
return opd
计算带像差的PSF和MTF:
aberrated_opd = synthetic_opd(
rho=rho,
theta=theta,
mask=mask,
wavelength=wavelength
)
aberrated_pupil, _ = pupil_function_from_opd(
opd=aberrated_opd,
mask=mask,
wavelength=wavelength
)
aberrated_psf, _ = compute_psf(
aberrated_pupil,
pad_factor=4
)
aberrated_otf, aberrated_mtf = compute_otf_mtf(aberrated_psf)
现在比较中心切片:
def plot_mtf_comparison(mtf_a, mtf_b, label_a, label_b, title="MTF comparison"):
freq_index, h_a, v_a = central_mtf_slices(mtf_a)
_, h_b, v_b = central_mtf_slices(mtf_b)
center = len(freq_index) // 2
freq_positive = freq_index[center:]
plt.figure(figsize=(7, 4))
plt.plot(freq_positive, h_a[center:], label=f"{label_a} horizontal")
plt.plot(freq_positive, v_a[center:], "--", label=f"{label_a} vertical")
plt.plot(freq_positive, h_b[center:], label=f"{label_b} horizontal")
plt.plot(freq_positive, v_b[center:], "--", label=f"{label_b} vertical")
plt.xlabel("Frequency sample index")
plt.ylabel("MTF")
plt.ylim(0, 1.05)
plt.title(title)
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.show()
plot_mtf_comparison(
perfect_mtf,
aberrated_mtf,
label_a="Perfect",
label_b="Aberrated",
title="Perfect vs aberrated MTF"
)
带像差的MTF应低于完美MTF,尤其是在较高空间频率处。
这不是任意的惩罚。它源于PSF中的能量扩散。
更宽或结构更复杂的PSF降低了系统保留精细细节对比度的能力。
链条是:
光瞳相位误差
→ PSF改变
→ OTF改变
→ 降低或方向依赖的MTF
这就是一条原本看起来像软件报告般的曲线背后的计算故事。
10. 方向很重要:弧矢与子午MTF
光学设计软件通常绘制弧矢和子午MTF曲线。
这些名称起初可能显得神秘,但概念是几何的。
对于一个离轴视场点,存在一个包含以下内容的平面:
- 光轴;
- 主光线;
- 视场点。
这就是子午或径向方向。
与之垂直的方向是弧矢方向。
在理想轴上旋转对称的情况下,弧矢和子午行为相同。离轴时,它们可能显著不同。
这种差异并非装饰性的。像散、彗差及视场依赖的像差常常对不同方向产生不同影响。
在我们简化的居中阵列中,我们可以将一条中心切片视为子午方向,而将垂直切片视为弧矢方向,以此模拟这个想法。
这仅是一个教学简化。
def sagittal_tangential_slices_simple(mtf):
"""
对居中MTF阵列的简化弧矢/子午切片。
对于真实的离轴视场,弧矢和子午方向应根据视场点和光轴定义。
这里我们用垂直和水平中心切片作为教学代理。
"""
n, m = mtf.shape
cy = n // 2
cx = m // 2
tangential = mtf[cy, :] # 水平代理
sagittal = mtf[:, cx] # 垂直代理
freq_index = np.arange(m) - cx
return freq_index, sagittal, tangential
现在绘制它们:
def plot_sagittal_tangential_simple(mtf, title="Sagittal and tangential MTF"):
freq_index, sagittal, tangential = sagittal_tangential_slices_simple(mtf)
center = len(freq_index) // 2
freq_positive = freq_index[center:]
plt.figure(figsize=(6, 4))
plt.plot(freq_positive, sagittal[center:], label="Sagittal proxy")
plt.plot(freq_positive, tangential[center:], "--", label="Tangential proxy")
plt.xlabel("Frequency sample index")
plt.ylabel("MTF")
plt.ylim(0, 1.05)
plt.title(title)
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.show()
plot_sagittal_tangential_simple(
aberrated_mtf,
title="Simplified sagittal/tangential MTF"
)
在真实镜头分析中,不应随意将水平和垂直切片重命名为弧矢和子午切片,而不了解视场几何。
真正的定义依赖于视场点。
更安全的说法是:
弧矢和子午MTF是二维频率响应的方向性切片,
其定义相对于视场几何。
软件为了方便隐藏了该几何关系。在学习计算时,我们应保持其可见。
11. 频率单位:仅有采样索引是不够的
我们的图形目前使用频率采样索引。这足以验证计算链条,但不足以进行工程解释。
真实的MTF曲线通常相对于空间频率绘制:
每毫米线对
或等效地:
每毫米周期数
线对是指一条亮线加一条暗线,因此每毫米线对和每毫米周期数在实际上常被混用。
为物理地标注频率轴,我们需要PSF在像面的采样间距。
若PSF的采样间距为:
则频率采样为:
在 NumPy 中:
freq = np.fft.fftshift(np.fft.fftfreq(n, d=dx_image))
其中 dx_image 若以毫米为单位,则频率单位为周期/毫米。
这是一个辅助函数:
def mtf_frequency_axis(n, dx_image):
"""
为MTF数组创建移位后的频率轴。
Parameters
----------
n : int
采样点数。
dx_image : float
像面采样间距。
Returns
-------
freq : 1D array
空间频率采样,单位为dx_image单位的倒数。
"""
return np.fft.fftshift(np.fft.fftfreq(n, d=dx_image))
如果 dx_image 单位是毫米,频率就是周期/毫米。
然后正频MTF图可以使用物理单位:
def plot_mtf_with_frequency_axis(mtf, dx_image, title="MTF"):
n, m = mtf.shape
cy = n // 2
cx = m // 2
freq = mtf_frequency_axis(m, dx_image)
mtf_slice = mtf[cy, :]
plt.figure(figsize=(6, 4))
plt.plot(freq[cx:], mtf_slice[cx:])
plt.xlabel("Spatial frequency [cycles per unit]")
plt.ylabel("MTF")
plt.ylim(0, 1.05)
plt.title(title)
plt.grid(True)
plt.tight_layout()
plt.show()
这个函数在结构上是正确的,但它需要一个真实的 dx_image。
这正是许多错误潜入之处。
PSF的采样间距取决于:
- 光瞳采样;
- 波长;
- 焦距或像空间缩放;
- 零填充;
- FFT惯例。
我们在第12章引入了简单的角坐标辅助方法。第14章将仔细返回此问题,因为频率轴错误是生成看似可信但错误的MTF图的最快途径之一。
目前,请牢记此警告:
计算MTF数组很简单。
正确标注频率轴需要细致的采样记账。
12. 衍射受限截止频率
现在介绍一个值得引入的物理频率尺度。
对于非相干衍射受限的圆形孔径(在空气中),像空间的截止空间频率通常写作:
其中:
- 是截止频率;
- 是波长;
- 是F数。
如果波长以毫米为单位,则 的单位是每毫米周期数。
同一概念也可用数值孔径书写:
这些公式很有用,因为它们给出了衍射受限非相干MTF降至零的近似上限频率。
例如,若:
且:
则:
这并不意味着每个f/5.6的真实镜头在325周期/毫米附近都能提供优秀对比度。它意味着衍射受限的截止尺度大约在那个数值附近。
像差、离焦、制造误差、传感器采样和图像处理都会影响实际可用的部分。
一个谨慎的表述是:
衍射截止给出物理上限。
实际MTF曲线告诉我们在此上限以下还剩余多少对比度。
这是一个很好的例子,说明应该如何解读MTF:不是将其视为神秘分数,而是视为逐频对比度传递。
13. MTF为何可能下降而PSF看起来并不糟糕
有时PSF图看起来并不显著恶化,但MTF曲线已明显下降。
这并不矛盾。
PSF是一幅空间图像。我们的眼睛倾向于聚焦于中心核心和可见拖尾。能量的微小重新分布仍会影响高频对比度。
MTF对PSF如何与不同频率的正弦图案相互作用很敏感。
PSF的微小展宽就能降低高频MTF,即使PSF在显示屏上仍然“看起来相当锐利”。
这是MTF成为如此有用的工程指标的原因之一。
它能揭示对比度损失,而这种损失从PSF图像中难以仅靠肉眼判断。
但相反的问题也存在。一条MTF曲线可能隐藏了模糊的空间特征。两个系统在选定频率下可能具有相似的MTF,但PSF形状不同。一个可能产生对称模糊;另一个可能产生方向性拖尾或光晕。
因此我们应将PSF和MTF一起解读:
PSF显示点能量去向何处。
MTF显示对比度随空间频率如何存留。
两种视图都不会使对方过时。
14. OTF中的相位:MTF隐藏的部分
由于MTF是OTF的幅值,它丢弃了OTF相位。
我们来计算并显示OTF的相位:
def show_otf_phase(otf, title="OTF phase"):
phase = np.angle(otf)
plt.figure(figsize=(5, 4))
plt.imshow(phase, origin="lower", cmap="twilight")
plt.colorbar(label="Phase [rad]")
plt.title(title)
plt.xlabel("Frequency sample x")
plt.ylabel("Frequency sample y")
plt.tight_layout()
plt.show()
如果你的 Matplotlib 安装不支持 "twilight" 颜色映射,请使用默认颜色映射:
def show_otf_phase_default(otf, title="OTF phase"):
phase = np.angle(otf)
plt.figure(figsize=(5, 4))
plt.imshow(phase, origin="lower")
plt.colorbar(label="Phase [rad]")
plt.title(title)
plt.xlabel("Frequency sample x")
plt.ylabel("Frequency sample y")
plt.tight_layout()
plt.show()
对于对称的居中PSF,OTF相位可能很简单。对于偏移或不对称的PSF,相位可能携带重要信息。
以下是一个小演示。
如果我们平移一个PSF,其MTF幅值可能保持不变,而OTF相位会改变。
shifted_psf = np.roll(perfect_psf, shift=10, axis=1)
shifted_otf, shifted_mtf = compute_otf_mtf(shifted_psf)
plot_mtf_comparison(
perfect_mtf,
shifted_mtf,
label_a="Original",
label_b="Shifted",
title="MTF before and after PSF shift"
)
MTF曲线可能看起来几乎相同。但图像位置发生了变化。
该平移携带在相位中,而非幅值。
这是一个关于MTF局限性的清晰例子:
MTF可以说对比度得到了保留。
它可能不会告诉您图像已经移动了。
在许多镜头质量情境中,MTF仍然极具价值。但如果您需要完整的图像形成、配准、相位行为或不对称效应,就不能忽视OTF相位和PSF形状。
15. MTF与对比度
MTF为何对应于对比度?
设想一个具有正弦强度变化的物体:
其中:
- 是平均强度;
- 是调制或对比度;
- 是空间频率。
经过成像系统后,同一频率可能以减小的调制出现:
在该频率处的MTF为:
这就是MTF的对比度传递含义。
若MTF为1,该频率的对比度得以保留。
若MTF为0.5,调制度减半。
若MTF接近0,该频率几乎未被传递。
这就是MTF比许多原始光学量更容易与图像细节联系起来的原因。
点斑尺寸告诉我们一些信息。OPD告诉我们一些信息。PSF告诉我们一些信息。但MTF直接询问:
在此细节尺寸下还剩下多少对比度?
这就是它成为标准图像质量指标的原因。
但请记住边界:
MTF描述空间频率的对比度传递。
它并不描述图像的每个感知或几何属性。
这一区别使该指标保持有用,而不使其变得神奇。
16. 完整的PSF到MTF最小脚本
这是一个紧凑的脚本,运行了完美光瞳和带像差光瞳的完整路径。
import numpy as np
import matplotlib.pyplot as plt
def make_pupil_grid(n=256):
coord = np.linspace(-1.0, 1.0, n)
x, y = np.meshgrid(coord, coord)
rho = np.sqrt(x**2 + y**2)
theta = np.arctan2(y, x)
mask = rho <= 1.0
return x, y, rho, theta, mask
def pupil_function_from_opd(opd, mask, wavelength, amplitude=None):
if amplitude is None:
amplitude = mask.astype(float)
phase = 2.0 * np.pi * opd / wavelength
pupil = amplitude * np.exp(1j * phase)
pupil[~mask] = 0.0 + 0.0j
return pupil, phase
def synthetic_opd(rho, theta, mask, wavelength):
opd = np.zeros_like(rho)
defocus = 0.50 * wavelength
astigmatism = 0.25 * wavelength
opd += defocus * (2.0 * rho**2 - 1.0)
opd += astigmatism * rho**2 * np.cos(2.0 * theta)
opd[~mask] = 0.0
return opd
def pad_array_centered(array, pad_factor=4):
if pad_factor == 1:
return array
n, m = array.shape
new_n = n * pad_factor
new_m = m * pad_factor
padded = np.zeros((new_n, new_m), dtype=array.dtype)
start_n = (new_n - n) // 2
start_m = (new_m - m) // 2
padded[start_n:start_n + n, start_m:start_m + m] = array
return padded
def compute_psf(pupil, pad_factor=4, normalize=True):
padded_pupil = pad_array_centered(pupil, pad_factor=pad_factor)
field = np.fft.fftshift(
np.fft.fft2(
np.fft.ifftshift(padded_pupil)
)
)
psf = np.abs(field) ** 2
if normalize:
total = np.sum(psf)
if total > 0:
psf = psf / total
return psf, field
def compute_otf_mtf(psf):
otf = np.fft.fftshift(
np.fft.fft2(
np.fft.ifftshift(psf)
)
)
center = (otf.shape[0] // 2, otf.shape[1] // 2)
if np.abs(otf[center]) > 0:
otf = otf / otf[center]
mtf = np.abs(otf)
return otf, mtf
def central_mtf_slices(mtf):
n, m = mtf.shape
cy = n // 2
cx = m // 2
horizontal = mtf[cy, :]
vertical = mtf[:, cx]
freq_index = np.arange(m) - cx
return freq_index, horizontal, vertical
def plot_mtf_comparison(mtf_a, mtf_b, label_a, label_b):
freq_index, h_a, v_a = central_mtf_slices(mtf_a)
_, h_b, v_b = central_mtf_slices(mtf_b)
center = len(freq_index) // 2
freq_positive = freq_index[center:]
plt.figure(figsize=(7, 4))
plt.plot(freq_positive, h_a[center:], label=f"{label_a} horizontal")
plt.plot(freq_positive, v_a[center:], "--", label=f"{label_a} vertical")
plt.plot(freq_positive, h_b[center:], label=f"{label_b} horizontal")
plt.plot(freq_positive, v_b[center:], "--", label=f"{label_b} vertical")
plt.xlabel("Frequency sample index")
plt.ylabel("MTF")
plt.ylim(0, 1.05)
plt.title("MTF comparison")
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.show()
wavelength = 550e-9
x, y, rho, theta, mask = make_pupil_grid(n=256)
zero_opd = np.zeros_like(rho)
perfect_pupil, _ = pupil_function_from_opd(
opd=zero_opd,
mask=mask,
wavelength=wavelength
)
aberrated_opd = synthetic_opd(
rho=rho,
theta=theta,
mask=mask,
wavelength=wavelength
)
aberrated_pupil, _ = pupil_function_from_opd(
opd=aberrated_opd,
mask=mask,
wavelength=wavelength
)
perfect_psf, _ = compute_psf(perfect_pupil, pad_factor=4)
aberrated_psf, _ = compute_psf(aberrated_pupil, pad_factor=4)
perfect_otf, perfect_mtf = compute_otf_mtf(perfect_psf)
aberrated_otf, aberrated_mtf = compute_otf_mtf(aberrated_psf)
plot_mtf_comparison(
perfect_mtf,
aberrated_mtf,
label_a="Perfect",
label_b="Aberrated"
)
与第1章的代码预览相比,这个脚本现在应感觉不那么神秘了。
在本书开头,这些行看起来像是一个快捷方式:
otf = np.fft.fftshift(np.fft.fft2(psf))
mtf = np.abs(otf)
现在它们在链条中有了自己的位置。
它们并非任意的傅里叶变换。它们是根据非相干成像系统的点扩散函数计算其频率响应。
这就是使用代码与理解代码之间的区别。
17. Optiland对比工作流程
在真实的Optiland工作流程中,进行实际设计工作时,MTF通常应使用库自带的分析工具计算。
我们NumPy版本的教学价值不同。它让我们能检查工具在概念上必须执行的操作。
一个经验证的工作流程概览如下所示:
# 经验证的工作流程概览。
# 检查您安装的Optiland版本以获取确切的API名称。
# 1. 构建或加载光学系统。
# optic = ...
# 2. 选择视场、波长、孔径和焦点设置。
# field = ...
# wavelength = ...
# 3. 使用Optiland的PSF分析。
# psf_result = ...
# 4. 使用Optiland的MTF分析。
# mtf_result = ...
# 5. 如果采样PSF可用,从PSF重建教学用MTF。
# psf_array = ...
# otf_numpy, mtf_numpy = compute_otf_mtf(psf_array)
# 6. 比较:
# - MTF归一化
# - 频率轴
# - 弧矢/子午定义
# - 波长与视场设置
# - 采样与填充
比较不仅仅是匹配图形。它关乎提出正确的问题:
- 这条MTF是针对哪个视场点的?
- 使用了哪个波长或波长权重?
- 绘制的曲线是弧矢、子午、径向还是平均的?
- 频率轴是以周/毫米、周/度还是归一化单位?
- PSF是基于衍射还是几何的?
- MTF是多色还是单色的?
- 像面是否重新对焦?
- 使用了何种归一化?
这时开放工具的教育价值就变得清晰了。
您不仅仅是按下一个按钮。您可以检查输入,复现简化的部分,并理解为何结果会变化。
这并不会使工具变得不必要。它使工具变得不那么神秘。
18. 仔细阅读MTF曲线
一旦知道了MTF如何计算,您就可以带着更好的问题阅读曲线。
低频区域
低频对应大范围对比度。
如果低频MTF不佳,图像可能显得整体平坦或模糊。在合理的光学系统中,低频MTF通常较高,除非存在严重像差、散射、离焦或其他对比度降低效应。
中频区域
中频通常对应视觉上重要的细节。
对于相机镜头,该区域能强烈影响感知的锐度。一个能良好保留中等空间频率的镜头,即使高频响应不完美,也可能看起来清晰。
高频区域
高频对应精细细节。
高频MTF对衍射、像差、焦点、采样和制造敏感。它通常最先下降。
但不应孤立解读高频MTF。一个系统可能拥有可接受的高频响应,但在其他方面表现不佳,如场曲、畸变、眩光或方向性模糊。
截止频率
截止频率是指对比度传递基本为零的频率。
对于非相干衍射受限成像,这与波长和数值孔径有关。在实际成像系统中,有用对比度可能在理论截止频率之前就变得过低。
一个细心的读者不仅会问:
曲线在哪里结束?
还会问:
在该应用重要的频率处还剩下多少对比度?
这个与应用相关的问题通常更有用。
19. MTF为何强大但并非万能
MTF成为标准指标,是因为它将大量成像行为压缩到一条可读曲线上。
这是它的优势。
也是它的危险所在。
一条单一的MTF曲线可能无法告诉您:
- 模糊是对称还是不对称;
- PSF是否有长拖尾;
- 是否存在图像位移;
- 相位行为是否重要;
- 畸变如何影响几何形状;
- 颜色通道如何不同;
- 性能如何随视场变化;
- 传感器采样如何与光学模糊相互作用;
- 图像处理如何改变最终对比度。
即使多条MTF曲线也不能完全替代查看PSF、点列图、波前、场曲、畸变和系统处方。
因此更好的习惯是:
将MTF用作频域摘要,
而非光学系统的完整描述。
这是本书开头那个尖锐主张的审慎版本:
能够阅读MTF曲线不等于理解它从何而来。
现在我们可以确切说出它从何而来。
它来自PSF。
PSF来自复光瞳函数。
光瞳函数来自振幅和OPD。
OPD来自通过光学系统计算的光程差。
这条链条才是本书的真正主题。
20. OTF与MTF计算中的常见错误
错误1:直接从光瞳取得MTF
错误做法:
mtf = np.abs(np.fft.fft2(pupil))
这不是非相干MTF。
光瞳的变换给出复像场。您仍需PSF:
field = np.fft.fft2(pupil)
psf = np.abs(field) ** 2
otf = np.fft.fft2(psf)
mtf = np.abs(otf)
顺序至关重要。
错误2:忘记OTF归一化
如果MTF不从1开始,在解读结果前先检查归一化。
otf = otf / otf[center]
mtf = np.abs(otf)
错误3:混淆频率索引与物理频率
数组索引并非周/毫米。
要获得物理频率,需要像面采样间距。
freq = np.fft.fftshift(np.fft.fftfreq(n, d=dx_image))
难点在于正确得知 dx_image。
错误4:无视场几何知识而将水平和垂直切片称为弧矢与子午
对于真实离轴视场,弧矢和子午方向是相对于光轴和视场点定义的。
水平与垂直切片仅为教学代理,除非已知坐标系。
错误5:忽略波长和视场点
MTF随波长和视场位置变化。
单条曲线并不是在所有条件下“镜头的MTF”。它是特定配置下的MTF。
错误6:将MTF视为最终真理
MTF很强大。但它并不完整。
应结合PSF、波前、点列图、视场图及成像任务知识使用它。
21. 本章对完整链条的补充
我们现在可以写出从波前到MTF的完整路径:
OPD图
→ 光瞳相位
→ 复光瞳函数
→ PSF
→ OTF
→ MTF
新增的步骤是:
以及:
在代码中:
otf = np.fft.fftshift(np.fft.fft2(np.fft.ifftshift(psf)))
otf = otf / otf[center]
mtf = np.abs(otf)
这就是按钮背后的曲线。
虽非完整故事,却是基本的计算骨架。
我们现在已经抵达了第1章所指向的终点。本书的其余部分将使这一结果更安全、更实用,并与真实光学设计联系更紧密。
第14章将放慢脚步,做些非常必要的事:审视此计算可能出错的方式。
因为到了现在,我们已知足够去犯令人印象深刻的错误。
采样、FFT移位、频率轴、归一化、填充和单位,都可能产生看起来合理但含义错误的MTF图。
因此,在转向优化之前,我们需要一章救援章节。
章末小结
PSF描述空间模糊。OTF描述频率响应。MTF是OTF的幅值。
对于非相干、平移不变的成像:
在代码中:
otf = np.fft.fftshift(np.fft.fft2(np.fft.ifftshift(psf)))
otf = otf / otf[center]
mtf = np.abs(otf)
正确归一化后,MTF从1开始。它通常随空间频率升高而下降,显示出对比度传递如何对更精细细节减弱。
二维MTF可切片为曲线。弧矢和子午曲线是相对于视场几何定义的方向性切片;水平与垂直切片仅为简化代理,除非坐标系已知。
MTF之所以强大,是因为它按空间频率总结对比度传递。它的局限在于丢弃了OTF相位,并将空间模糊行为压缩到选定曲线中。
本章最重要的结果不仅仅是公式。而是这条链条:
PSF
→ OTF
→ MTF
现在,MTF曲线不再是一个神秘的输出。它是PSF的计算结果,而PSF本身又是光瞳函数与波前误差的计算结果。
生成的验证图

来源与验证说明
此处的OTF/MTF公式假定为非相干、平移不变的成像,并具有正确归一化的PSF。真实光学设计软件可能根据额外惯例计算衍射MTF、几何MTF、多色MTF、随视场变化的曲线或特定方向的弧矢/子午曲线。本章在添加这些生产细节之前,保持了核心傅里叶关系的可见性。