Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第13章:OTF与MTF:将PSF转换为频率响应

我们以一条曲线开启了这本书。

MTF曲线出现在光学设计软件中,仿佛是一个最终结论:这里锐利,那里薄弱,某个视场较好,另一个视场较差。它看起来清晰、有权威。只需读出够多的信息,就足以带来风险。

但MTF曲线并非天生就是曲线。

它是一条计算链条的终点。

现在,这条链条已不再隐藏:

镜头处方
→ 光线追迹
→ 光程长度
→ OPD
→ 光瞳函数
→ PSF
→ OTF
→ MTF

在第12章中,我们从复光瞳函数计算了PSF。PSF告诉我们一个理想点如何在像面上扩散。

现在我们问一个不同的问题:

如果物体包含不同空间频率的图案,
光学系统对每种频率的传递有多强?

这个问题引出了光学传递函数,即OTF

MTF,即调制传递函数,是OTF的幅值。

最简洁的形式是:

OTF=F{PSF} \mathrm{OTF} = \mathcal{F}{\mathrm{PSF}}

MTF=OTF \mathrm{MTF} = |\mathrm{OTF}|

这两行很简单。它们的含义却不小。

本章中,PSF将不再仅仅是一个点的像,而成为频率响应。


1. MTF因何存在

PSF回答:

系统对一个点做了什么?

MTF回答:

系统对不同细节尺寸的对比度做了什么?

物体可被看作包含许多空间频率。

大而缓慢的变化属于低空间频率。精细细节属于高空间频率。

例如:

  • 一大块灰色区域是低频;
  • 粗的黑白条纹图形是较低频率;
  • 非常细的条纹图形是较高频率;
  • 微小纹理、发丝、织物编织或精细印刷文字含有高频信息。

光学系统对这些频率的传递是不均等的。

低频对比度可能很好地保留。高频对比度可能被削弱。在某一点上,精细细节可能不再以有效对比度传递。

这正是MTF曲线所展示的。

典型的MTF曲线在零频处始于近1,并随频率升高而下降。

这并不意味着每个系统在所有方向上都平滑下降。真实系统可能存在结构、方向差异、视场依赖、波长依赖以及采样问题。但第一个思维模型是:

MTF告诉我们对比度传递如何随空间频率变化。

这个句子有用,但不完整。本章的后续部分将使其可计算。


2. 将PSF视为冲激响应

要理解为何PSF的傅里叶变换得到OTF,我们需要一个来自成像理论的概念。

对于非相干、平移不变的成像系统,图像可以建模为由PSF模糊的物体。

用简洁的符号表示:

Iimage(x,y)=Iobject(x,y)PSF(x,y) I_\text{image}(x,y) = I_\text{object}(x,y) * \mathrm{PSF}(x,y)

其中 * 表示卷积。

这意味着:

最终图像是通过将每个物点替换为PSF的副本,
并将所有这些副本叠加而构成的。

这是一个强有力的概念。

若PSF狭窄,点保持集中,图像看起来锐利。

若PSF宽阔,每个物点扩散得更广,图像失去精细细节。

卷积定理进而告诉我们:

F{Iimage}=F{Iobject}F{PSF} \mathcal{F}{I_\text{image}} = \mathcal{F}{I_\text{object}} \cdot \mathcal{F}{\mathrm{PSF}}

PSF的傅里叶变换就是系统的传递函数:

OTF(fx,fy)=F{PSF(x,y)} \mathrm{OTF}(f_x,f_y)=\mathcal{F}{\mathrm{PSF}(x,y)}

因此OTF告诉我们物体的每个空间频率成分如何被光学系统改变。

这就是PSF和OTF是同一成像行为的两个视角的原因:

PSF:空域模糊
OTF:频域传递

它们并非悬浮于空间的独立测量量。它们是一对傅里叶变换对。


3. OTF是复数的;MTF是其幅值

OTF通常是复数的:

OTF(fx,fy)=OTF(fx,fy)eiΦ(fx,fy) \mathrm{OTF}(f_x,f_y) = |\mathrm{OTF}(f_x,f_y)|e^{i\Phi(f_x,f_y)}

它具有:

  • 幅值;
  • 相位。

幅值就是MTF:

MTF(fx,fy)=OTF(fx,fy) \mathrm{MTF}(f_x,f_y)=|\mathrm{OTF}(f_x,f_y)|

相位通常被称为相位传递函数,即PTF:

PTF(fx,fy)=arg(OTF(fx,fy)) \mathrm{PTF}(f_x,f_y)=\arg(\mathrm{OTF}(f_x,f_y))

许多光学设计讨论聚焦于MTF,因为对比度传递极为有用。但MTF丢掉了OTF的相位部分。

这不是一个小细节。

MTF告诉你每个空间频率下剩下多少对比度。但它不能完全告诉你结构如何移动、是否出现相位反转,或不对称模糊如何影响图像外观。

这是本书的一个核心判断:

MTF很重要,但并非图像质量故事的全部。

这不是反对MTF的论点。而是反对将一条曲线视为包含了所有光学真值。

PSF、OTF相位、视场依赖、波长依赖、畸变、传感器采样和图像处理都可能重要。

目前,我们将直接从PSF计算OTF和MTF。然后我们将学习如何解读结果,而不盲目崇拜它。


4. 归一化:MTF为何始于1

零频成分表示平均亮度传递。对于一个总能量为1的归一化PSF:

x,yPSF(x,y)=1 \sum_{x,y}\mathrm{PSF}(x,y)=1

零频处的OTF值也应为1:

OTF(0,0)=1 \mathrm{OTF}(0,0)=1

因此零频处的MTF是:

MTF(0,0)=1 \mathrm{MTF}(0,0)=1

在数值代码中,由于FFT惯例,可能会出现小幅缩放差异。最安全的实践方法是用零频值对OTF进行归一化:

OTFnorm(fx,fy)=OTF(fx,fy)OTF(0,0) \mathrm{OTF}_\text{norm}(f_x,f_y) = \frac{\mathrm{OTF}(f_x,f_y)} {\mathrm{OTF}(0,0)}

然后:

MTF(fx,fy)=OTFnorm(fx,fy) \mathrm{MTF}(f_x,f_y) = |\mathrm{OTF}_\text{norm}(f_x,f_y)|

这使得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并非天生就是一条曲线。它首先是一个二维频率响应:

MTF(fx,fy) \mathrm{MTF}(f_x,f_y)

我们熟悉的曲线通常是该二维函数的一个切片。

这个切片可以是水平的、垂直的、弧矢的、子午的、径向的或平均的,依上下文而定。

曲线并非全貌。它是一个选定的视图。


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的采样间距为:

Δx \Delta x

则频率采样为:

f=fftfreq(N,d=Δx) f = \mathrm{fftfreq}(N, d=\Delta x)

在 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. 衍射受限截止频率

现在介绍一个值得引入的物理频率尺度。

对于非相干衍射受限的圆形孔径(在空气中),像空间的截止空间频率通常写作:

fc=1λN f_c = \frac{1}{\lambda N}

其中:

  • fcf_c 是截止频率;
  • λ\lambda 是波长;
  • NN 是F数。

如果波长以毫米为单位,则 fcf_c 的单位是每毫米周期数。

同一概念也可用数值孔径书写:

fc=2NAλ f_c = \frac{2,\mathrm{NA}}{\lambda}

这些公式很有用,因为它们给出了衍射受限非相干MTF降至零的近似上限频率。

例如,若:

λ=0.00055 mm \lambda = 0.00055\text{ mm}

且:

N=5.6 N = 5.6

则:

fc10.00055×5.6325 cycles/mm f_c \approx \frac{1}{0.00055 \times 5.6} \approx 325\ \text{cycles/mm}

这并不意味着每个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为何对应于对比度?

设想一个具有正弦强度变化的物体:

I(x)=I0[1+mcos(2πfx)] I(x)=I_0\left[1+m\cos(2\pi f x)\right]

其中:

  • I0I_0 是平均强度;
  • mm 是调制或对比度;
  • ff 是空间频率。

经过成像系统后,同一频率可能以减小的调制出现:

Iimage(x)=I0[1+mcos(2πfx+ϕ)] I_\text{image}(x)=I_0\left[1+m'\cos(2\pi f x+\phi)\right]

在该频率处的MTF为:

MTF(f)=mm \mathrm{MTF}(f)=\frac{m'}{m}

这就是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(fx,fy)=F{PSF(x,y)} \mathrm{OTF}(f_x,f_y)=\mathcal{F}{\mathrm{PSF}(x,y)}

以及:

MTF(fx,fy)=OTF(fx,fy) \mathrm{MTF}(f_x,f_y)=|\mathrm{OTF}(f_x,f_y)|

在代码中:

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=F{PSF} \mathrm{OTF}=\mathcal{F}{\mathrm{PSF}}

MTF=OTF \mathrm{MTF}=|\mathrm{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本身又是光瞳函数与波前误差的计算结果。

生成的验证图

MTF切片,由第13章OTF和MTF计算生成

来源与验证说明

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