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

第1章:为什么MTF并非来自软件按钮

镜头设计程序会让光学显得简单得令人怀疑。

你构建一个镜头。输入几个曲率半径、厚度、玻璃、波长和视场点。点击一个按钮。一幅图出现了。

也许是一个点列图。也许是光线像差曲线。也许是波前图。也许就是那幅许多光学设计师既期待又畏惧的图:MTF曲线。

这条曲线看起来很有权威。它有坐标轴,有弧矢和子午方向的线条,一边是空间频率,另一边是调制传递值。如果曲线保持得很高,设计看来很有希望。如果曲线过早下降,则某个环节出了问题。这幅图似乎用清晰的工程语言在说话:

这个镜头能很好地保持对比度。
这个镜头在精细细节上损失了对比度。
这个视场点比中心差。
这个波长的表现不同。
这个设计还不够完善。

这些信息都很有用。但问题在于:

看到一条MTF曲线并不等同于理解它是怎么来的。

这并非对光学设计软件的抱怨。优秀的软件会隐藏繁琐的工作,在生产工作中这是一种馈赠。你不想每次调整曲率半径时都要手动计算每一个光线与表面的交点。你不想每次检查衍射性能时都自己写一遍快速傅里叶变换。你希望工具能快速、可靠地完成这些计算。

但学习与生产不同。

对学习而言,这种便利可能变成一堵墙。软件给出了结果,但计算的中间过程却消失了。你可能学会辨认一条好的MTF曲线,却不知道这条曲线是如何构建的。你可能知道PSF代表点扩散函数,OPD代表光程差,MTF代表调制传递函数,但可能仍然不知道一个镜头参数是如何变成这些量的。

这个缺口正是本书要讨论的主题。

我们要打开这个黑盒子。

不是一次性全部打开。那样不公平。这个盒子里装着几何、向量数学、光线追迹、一阶光学、波前、采样、傅里叶变换、归一化、单位和优化。如果我们在第一章就把所有这些抛给读者,那等于什么也没教。我们只是用一个黑盒子换了另一个更吓人的黑盒子。

因此,我们从路线图开始。

你点击MTF按钮后出现的曲线并不是由按钮创造的。它是一条计算链的终点:

镜头参数
→ 表面和材料
→ 实际光线追迹
→ 近轴参考
→ 光瞳采样
→ 光程差
→ 光瞳函数
→ 点扩散函数
→ 光学传递函数
→ 调制传递函数

你现在还不需要理解每一步。眼下只需注意这条链的结构。MTF位于接近末端的位置。它并不是悬在光学系统之上的孤立量度。它依赖于镜头几何、玻璃、波长、孔径、视场点、像面、波前误差以及数值采样方法。

这就是为什么单条MTF曲线既有强大之处,也有危险之处。

强大,因为它将众多效应浓缩成一张易读的图。

危险,因为这种浓缩可能让你忘记底层的机制。

曲线是结果,不是起点

假设我们有一个镜头。

在软件层面,这个镜头可能显示为一个表格:

表面   曲率半径   厚度   材料   半口径
0     无穷远    无穷远  air   —
1      50.0     5.0   N-BK7  10.0
2     -50.0     45.0  air    10.0
3     无穷远    0.0   image  —

这不只是记账。它是计算的起点。

每一行告诉程序光线可能遇到什么样的表面,这个表面在什么位置,光线到达后应该如何偏折,以及下一个表面在何处等待。一个实际的光学设计可能包含更多的表面、非球面形状、孔径、求解、镀膜、非球面项、多个配置和多个波长。但基本思路是一样的:

镜头不只是一张图。它是为计算而准备的一组结构化的输入。

这是我们需要建立的第一个习惯。

当你看到一张镜头布局图时,要问:它是用什么数据产生的?

当你看到一条光线追迹时,要问:光线是怎样被表示的?

当你看到一张点列图时,要问:那些点是从哪里来的?

当你看到一条MTF曲线时,要问:是怎样一组计算创建了它?

这个习惯比本章开头的任何单个公式都更重要。一旦你知道公式属于哪里,学起来会更容易。

计算链的一个小小预览

在接触实际光学系统之前,我们先看一下这条链中刻意简化的一小段。

在后续章节中,我们将根据追迹的光线计算波前误差。我们不会取巧。我们将构建光线模型,与表面求交,计算折射方向,将实际光线与参考光线进行比较,并在整个光瞳上构建OPD。

现在,我们从这条链的半途开始。假设我们已经有了光瞳函数。这意味着在出瞳面上我们知道两件事:

  1. 哪些区域允许光线通过;
  2. 光学系统对这部分光线施加了怎样的相位延迟。

在标量傅里叶光学中,一个简化的光瞳函数可以写为:

P(x, y) = A(x, y) exp(i 2π W(x, y))

这里 A(x, y) 是孔径振幅掩模。在光瞳内部它可以为1,外部为0。W(x, y) 是以波长为单位的波前误差。指数函数将波前误差转为光学相位。

如果觉得这太快了,不用担心。我们会在波前相关章节中仔细回到这一点。现在,我们只想看到一点:PSF和MTF并非神秘之物。它们是可以被计算的。

下面是一个最小化的Python预览:

import numpy as np
import matplotlib.pyplot as plt

# A small square computational grid.
N = 256
u = np.linspace(-1.0, 1.0, N)
X, Y = np.meshgrid(u, u)
rho = np.sqrt(X**2 + Y**2)

# Circular pupil: light passes inside the unit circle.
aperture = rho <= 1.0

# A toy wavefront error in waves.
# This is not a full lens model yet. It is only a simple phase pattern.
defocus_waves = 0.5
W = defocus_waves * (2.0 * rho**2 - 1.0)

# Pupil function: amplitude mask times phase.
pupil = aperture * np.exp(1j * 2.0 * np.pi * W)

# PSF: squared magnitude of the Fourier transform of the pupil function.
amplitude = np.fft.fftshift(np.fft.fft2(np.fft.ifftshift(pupil)))
psf = np.abs(amplitude) ** 2
psf = psf / psf.sum()

# OTF: Fourier transform of the PSF.
otf = np.fft.fftshift(np.fft.fft2(np.fft.ifftshift(psf)))

# MTF: normalized magnitude of the OTF.
mtf = np.abs(otf)
mtf = mtf / mtf.max()

# A simple horizontal slice through the MTF.
center = N // 2
plt.plot(mtf[center, :])
plt.xlabel("sample index")
plt.ylabel("normalized modulation")
plt.title("A toy MTF slice from a toy pupil function")
plt.show()

这段代码还不是一个镜头设计程序。它没有玻璃库,没有曲面,没有斯涅尔折射,没有像面求解,没有视场角,没有物理频率轴,也没有精细的采样校准。很好。它本来就不该做得超过它的本分。

但它说明了一件重要的事情。

图上最终的曲线不是魔术变出来的。它来自数据。数据来自光瞳。光瞳包含了孔径和相位。相位代表了波前误差。PSF来自光瞳场的傅里叶变换。OTF来自PSF的傅里叶变换。MTF来自OTF的模值。

即使在这个玩具例子中,曲线也有它的来龙去脉。

这就是我们贯穿全书的态度。

当一个结果出现时,我们会追问它是由什么产生的。

软件路径与学习路径

真正的光学设计软件比上面的简短脚本做得要多得多。

它必须知道如何表示一个光学系统。它必须在一系列表面上追迹光线。它必须计算光线与表面的交点。它必须利用局部表面法线应用斯涅尔定律。它必须处理光阑、光瞳、波长、视场点和像面。它必须提供一阶量,如有效焦距和F数。它必须计算像差、波前、PSF、MTF以及优化的评价函数。

开源工具在此很有用,因为它们让我们可以比较两种理解层次:

最小教学代码
实际光学设计库

第一层是我们自己的小型代码。它不完整,但透明。它能让我们看到算法的骨架。

第二层是一个真正的库。它可以处理更多情况,具有更好的内部组织,并将计算与实际的光学设计工作流连接起来。

在本书中,Optiland将作为我们主要的开源参考实现。其当前文档将其描述为一个基于Python的光学设计、分析与优化开源框架,支持近轴、实际和偏振感知光线追迹,波前分析,PSF/MTF分析,散射,优化,可视化以及可微分机器学习工作流。该文档也指出项目仍在积极开发中,而这恰好是我们将原理与某一特定API版本分开的原因。(optiland.readthedocs.io

最后这句话很重要。

Optiland不是本书的主角。

计算链才是主角。

Optiland在这里很有价值,因为它给了我们可以检查、可以执行的机制。我们不是要记住某一个库的API;我们是在学习任何一个严肃的光学设计工具都必定以某种形式执行的计算。

例如,当前的Optiland示例展示了加载示例镜头、绘制布局、追迹光线、创建点列图以及计算PSF和MTF的简洁调用。一个小预览可能像这样:(optiland.readthedocs.io

from optiland.samples.objectives import CookeTriplet
from optiland.analysis import SpotDiagram
from optiland.psf import FFTPSF
from optiland.mtf import FFTMTF

lens = CookeTriplet()

# Draw the optical layout.
lens.draw()

# A geometric ray-based analysis.
spot = SpotDiagram(lens)
spot.view()

# A diffraction-based PSF calculation.
psf = FFTPSF(lens, field=(0, 0), wavelength="primary")
psf.view(projection="2d", num_points=256)

# An MTF calculation.
mtf = FFTMTF(lens)
mtf.view()

这种代码让现代光学计算变得平易近人。但我们应该谨慎。如果我们就此停下,一个简短的库调用就会变成一个新的黑盒子。

因此,我们通常会以两种方式阅读这类代码。

第一种阅读是实践性的:

好,这就是我向库请求PSF或MTF的方式。

第二种阅读是计算性的:

这个库必须已经知道了什么才能回答?

为了让 FFTPSF(lens, field=(0, 0), wavelength="primary") 工作,程序必须能够访问一个定义好的光学系统、一个视场点、一个波长、一个孔径、一种构建波前信息的方法以及一种采样策略。为了让 FFTMTF(lens) 工作,它最终必须生成或使用与PSF或OTF等价的信息,然后将其归一化并显示对比度随空间频率的变化。

调用之所以简短,是因为库已经将工作组织好了。

但工作依然在那里。

MTF实际上表达什么

让我们暂停一下,让物理意义变得更具体一些。

一个完美的黑白条纹图案具有很高的对比度。暗条纹是暗的,亮条纹是亮的。如果一个光学系统对这个图案成像很差,亮条纹会渗入暗条纹。精细的细节会变成一团灰色的糊状。调制降低了。

MTF测量的是在不同空间频率下有多少对比度得以保留。

低空间频率意味着宽大的细节:大字母、粗条纹、图像中缓慢的变化。

高空间频率意味着精细的细节:细线条、小纹理、锐利的边缘。

一个光学系统可能在很好地保留低频对比度的同时损失掉高频对比度。这就是为什么一张图像可能看起来很清晰,但仍缺乏精细细节。这也是为什么用简单的“清晰/不清晰”来评判光学设计是过于粗糙的。

MTF给我们一个更结构化的提问:

在这一视场点,
对这个波长或波长加权,
沿这个方向,
在这个空间频率下,
还剩下多少调制?

这个问题已经充满了条件。不存在自由漂移的MTF。MTF总是针对某一系统、某一视场、某一波长条件,采用某一采样和计算方法。

这也是初学者感到困惑的原因之一。曲线看起来像一个单一对象,但它有着许多隐藏的输入。

这里我们可以放慢脚步。

你现在还不需要记住每一个条件。只需记住,当你看到一条MTF曲线时,你看到的并不是某种绝对意义上的“镜头质量”。你看到的是在给定假设下对镜头的一个计算视图。

这使得曲线更有用,而不是更无用。这意味着我们可以提出更好的问题。

如果MTF在轴外下降,那罪魁是像散?彗差?场曲?采样问题?离焦?像面位置不好?波长加权?真正的设计限制?

仅凭曲线本身不能回答所有这些问题。但一旦我们理解了产生这条曲线的链,我们就可以逆向追踪问题。

第一个黑盒子:镜头参数

每一次计算都始于输入。在光学设计中,最重要的输入就是镜头参数。

镜头参数是以数据形式写下的光学系统。它告诉我们光依次遇到的表面序列。一个简单的表面项可能包含:

曲率半径
到下一个表面的厚度
该表面之后的材料
半口径
圆锥常数
表面类型

在镜头图中,你看到的是弯曲的玻璃。

在参数中,计算机看到的是指令。

一个半径为 R 的球面不仅仅是一个漂亮的边界。它是一个几何对象。当一条光线靠近它时,程序必须求解光线与球面的交点问题。求得交点后,程序必须计算表面法线。然后,它必须利用表面两侧的折射率来计算出射方向。

这意味着最终的MTF曲线取决于许多听起来很基础的早期决定:

我们是如何储存表面的?
我们是如何计算交点的?
在这个波长我们用了哪个折射率?
光阑在哪里?
采样了哪些光线?
像面在哪里?

这就是为什么第2章不会从像质开始。它会从镜头参数开始。

我们将把镜头从图纸中取出,并将其写成一种数据结构。这听起来可能不如MTF那么有吸引力,但这是正确的起点。如果光学系统没有被清晰地表示,那么后续的每一步计算都会变得模糊。

第二个黑盒子:光线追迹

当镜头被表示出来之后,程序就开始追迹光线。

一条光线可以写为:

r(t) = r0 + t d

这里 r0 是起始点,d 是方向矢量,t 是沿光线的距离参数。这个简短的方程是光学计算中沉默的勤劳者之一。

为了将一条光线追迹通过一个镜头,程序反复询问:

这条光线在下一个表面的什么位置碰触?
该点的法线是什么?
该表面前后的折射率是什么?
根据斯涅尔定律,折射后的方向是什么?
光线是否通过了孔径?
它落在了像面的什么位置?

从概念上讲这并不神秘。它就是一系列几何与向量运算。

但在教科书中,这部分很容易被跳过,因为相关代数并不总是优雅的。在软件中也很容易被隐藏,因为用户通常想要的是输出,而不是求交函数。因此,许多学习者会从“斯涅尔定律存在”直接跳到“软件进行光线追迹”,而中间的实际计算却缺失了。

我们不会跳过它。

第4、5、6章是本书的根系。我们将定义一条光线,计算它碰到平面或球面的位置,并计算折射方向。与PSF和MTF相比,这些章节可能显得不够精彩,但这正是黑盒子首次被打开的地方。

如果光线追迹不清楚,那之后的一切都只是仪式。

如果光线追迹清楚了,点列图、光线像差曲线和波前就会变得不那么神秘。

第三个黑盒子:从光线到波前

点列图可以从光线交点构建出来。在光瞳上追迹许多光线,让它们落在像面上,画出这些点。这已经能告诉我们很多关于几何模糊的信息。

但PSF和衍射MTF需要另一种表示形式。

它们需要波前信息。

这是许多学习者能感觉到但说不出的桥梁。几何光学谈论光线。傅里叶光学谈论复振幅、光瞳函数、衍射图案和变换。一个实用的光学设计程序必须把这些世界连接起来。

这种连接通常通过光程和光程差来实现。

大致来说,光程衡量光在具有折射率的介质中传播的相位行程。OPD将实际光程与参考光程进行比较。一旦在整个光瞳上知道了OPD,就可以将其转化为相位。一旦知道了整个光瞳上的相位,我们就能形成光瞳函数。一旦有了光瞳函数,我们就可以计算PSF。

那一刻,前面那段玩具代码就开始合理了。

玩具代码从光瞳函数开始。在全书中,我们将从这个光学系统出发,赢得那个光瞳函数。

这条链将变为:

在镜头中追迹光线
→ 计算光瞳上的光程差
→ 将OPD转化为相位
→ 构建光瞳函数
→ 傅里叶变换得到PSF

初看起来,这好像很繁重。确实很多。但难度并不在于哪一个单一步骤是魔法。难度在于这些步骤属于不同的思维工具箱。

几何给我们光线。

物理光学给我们相位。

数值计算给我们数组和FFT。

光学工程给我们解读。

本书就是将那些工具箱缝合在一起。

第四个黑盒子:从PSF到MTF

一旦我们有了PSF,MTF就近了,但在人的感受中它仍然不是自动的。

PSF告诉我们一个点物体在像中如何扩展。一个理想的点会保持为一个点。一个实际的光学系统则会因衍射、像差、离焦、采样等因素使其扩展。

光学传递函数,即OTF,描述了系统如何传递空间频率内容。在简化的计算术语中:

OTF = PSF的傅里叶变换
MTF = OTF的模值

通常我们会将MTF归一化,使零频值为1。这样我们就得到一条调制从1开始并通常随空间频率增加而下降的曲线。

这种紧凑的关系是MTF在被呈现时会让人觉得简单的原因之一。但这种简单性具有欺骗性。PSF已经包含了之前所有因素导致的后果。MTF继承了这一切。

一条糟糕的MTF曲线绝不仅仅是“MTF不好”。

它可能在告诉你关于像差的信息。或是焦点。或是视场位置。或是孔径。或是波长。或是采样。或是计算设置中的错误。

这就是为什么本书不把MTF作为一开始的一个独立章节。我们从问题开始,但我们不会假装答案是即时的。

我们将在第13章恰当地到达MTF。到那时,这条曲线应该不再感觉像软件的神谕了。它应该感觉像一次你可以检查的计算的最终图表。

本书将有何不同

许多光学书籍在解释原理方面极其出色。许多软件手册在解释命令方面极其出色。两者都有价值。但它们之间有一个缺口。

这个缺口看起来像这样:

我知道公式。
我知道软件按钮。
但我不知道连接这两者的计算。

本书就生活在这个缺口里。

对于每一个主要的思想,我们将使用三个层次。

第一,光学原理:

我们正在解决怎样的物理或几何问题?

第二,计算形式:

哪些变量、方程、数组和算法代表了那个问题?

第三,可执行代码:

我们能否自己写一个小版本,然后用真实工具进行比较?

小版本很重要。它能防止我们把Optiland或任何库作为一个替代的黑盒子。

与真实工具的比较也很重要。它能防止我们编写从未接触实际光学设计的玩具代码。

你可以把这本书想成两条轨道:

轨道A:最小化的Python实现
轨道B:Optiland参考工作流

轨道A教授骨架。

轨道B展示骨架如何存在于一个更完整的机体中。

我们会小心地在两者之间移动。

关于代码示例的提醒

光学代码对约定非常敏感。

单位是重要的。符号约定是重要的。表面索引是重要的。波长单位是重要的。坐标系统是重要的。FFT归一化是重要的。视场是以角度还是物高指定是重要的。频率是每毫米的线对数还是归一化采样索引是重要的。

这意味着在光学中阅读代码示例应当比阅读普通的编程片段更加仔细。

当我们写几行NumPy时,通常我们是在默默地选择一种约定。当我们调用一个库时,这个库早已为我们选好了许多约定。学习计算光学的一个很大部分就是学会提问:

单位是什么?
什么被归一化了?
原点在哪里?
我们在哪一个平面上?
哪个是主波长?
这是哪个视场点?
图中的坐标轴实际意味着什么?

这听起来可能过于琐碎。其实不然。这些问题正决定了你是在理解一幅图,还是仅仅在欣赏它。

稍后,第14章将专门讲解这一类麻烦:采样、归一化、FFT移位、频率轴及单位转换。那一章可能会节省你许多小时的困惑。它不华丽,但它温情。

学完本章后你应该能做到什么

学完本章后,你还无法从一个真实的镜头参数出发完整实现MTF计算。这要求太高了。

但你应该能够说出比下面这句更精确的话:

软件计算MTF。

你应该能说出:

MTF曲线是一条链的终点。系统从一个镜头参数开始。光线通过表面和材料被追迹。程序在某一视场和波长下构建几何与波前信息。光瞳函数导向PSF。PSF导向OTF。OTF的模值给出MTF。

这已经是一大步了。

你已经从结果观察者变为过程思考者。

这种转变会改变你阅读每一张光学图的方式。

一张点列图不再只是一张散点图。它是许多追迹光线落在像面上的记录。

一条光线像差曲线不再只是一条诊断曲线。它是横贯光瞳的横向光线误差的一种结构视面。

一张OPD图不再只是彩色装饰。它是连接光线几何与波相位的桥梁。

一个PSF不再只是一幅模糊图像。它是由光瞳场产生的强度分布。

一条MTF曲线不再只是一个按钮输出。它是图像对比度传递的频域总结。

这就是本书的整个方向。

我们并不是试图让光学设计看起来容易。那是不诚实的。光学设计的要求很高。

但要求高并不等于神秘。

很多神秘感来自缺失的步骤。一旦这些步骤变得可见,这门学科就会变得更可教。当然还是很难,是的。但不再被密封着。

前方的路

在下一章,我们将回到这条链的开端。

在我们追迹光线之前,在我们计算波前之前,在我们再次谈及PSF或MTF之前,我们需要回答一个朴素的问题:

一个镜头参数到底是什么?

不是作为PDF表格。

不是作为一张图。

而是作为数据。

我们将把光学系统拆解为曲率半径、厚度、材料、孔径、波长和视场。然后我们会写一个简单镜头的Python小表示。之后,当一条光线穿过系统时,它将不再是在模糊的示意图中穿行。它将是在我们可以检查的数据中穿行。

那也是计算光学开始的地方。

不是从MTF按钮开始。

而是从使得按钮成为可能的输入开始。

生成的验证图

由第1章计算生成的一个玩具MTF切片

来源与验证说明

本章中面向软件的陈述是版本限定的。配套项目固定于 optiland==0.6.0 ;PyPI单独记录了该版本,与可能独立更新的公共Read the Docs页面分开。本章的核心主张不依赖于某一次Optiland的API调用。它依赖于稳定的计算链:镜头参数、追迹、波前或光瞳表示、PSF、OTF和MTF。