1. 传统光学设计流程的三个硬伤:时间、局部极小、割裂
1.1 时间都花在哪了
光学设计和仿真这个领域,过去十年给人的印象是稳定,甚至有点老气:打开 Zemax 或 CODE V,敲一个初始结构,设置评价函数,跑局部优化,看点列图和 MTF,手动改几个半径和空气间隔,再跑一轮。运气好,几周能出一版能看的镜头;运气不好,同一个局部极小值能困你半年。
我以前带项目的时候就见过一个硕士,整整两个月都在调一颗双高斯镜头的第三个空气间隔。每次优化曲线停在同一个地方,他就手动把空气间隔从 0.5 改成 0.8,再跑,再停。你说他不努力吗?不是,是传统流程本身就长这样:大部分时间根本不是花在“设计”上,而是花在“跟评价函数搏斗”和“反复尝试初始结构”上。做光学设计、仿真的硕博士应该都能感受到,这套流程的瓶颈已经不在计算速度,而在方法论本身。
1.2 局部极小值:手电筒式的搜索
光学系统的评价函数几乎都是高度非凸的。RMS 波前差、RMS 弥散斑半径、畸变、MTF,这些指标对镜头参数(曲率半径、厚度、折射率、非球面系数)的依赖关系非常复杂,处处都是坑。
传统的局部优化就像一个只拿手电筒的人在山谷里找最低点:你只能看到脚下一小片区域,沿着最陡的下坡方向走,走到走不动就停。问题是,这个“谷底”往往不是全局最低点,甚至不是物理上可用的解。全局优化算法(比如遗传算法、模拟退火)能稍微改善,但本质还是在黑箱里瞎摸,计算量巨大,而且很难把“可制造性”“公差敏感度”“后端算法”这些信息塞进搜索过程。
1.3 设计、仿真、算法三者是割裂的
更麻烦的是,传统流程里“光学设计”和“光学仿真”是两件事,甚至经常是两个团队的事。光学设计只负责让镜头本身的像质尽量好,仿真负责验证照明、杂散光、热效应,算法团队再拿着镜头的 PSF 去做图像复原。每一步都单独优化,没有一个全局的评判标准。
这在传统成像系统里还能勉强工作,但到了计算成像、AR/VR、车载感知这类场景,问题就会被放大:镜头可以故意做成有像差的,靠后端的算法把图像“算”回来,这样系统整体体积更小、成本更低、性能反而更好。这种跨光学和算法的全局最优,靠 Zemax 手动优化是找不到的,必须换一套思路。
2. 趋势的核心:可微光线追迹与端到端优化
2.1 可微光线追迹到底是什么意思
要理解这股趋势,先得搞明白一个词:可微光线追迹(differentiable ray tracing)。
传统光线追迹是逐条光线、逐面地做折射/反射计算,每一步都是确定性的浮点运算。听起来好像也有导数,但传统光学软件里的“优化”并不直接利用这些导数——它们大多用差商近似来估计梯度,也就是扰动一个参数、再跑一次追迹、看评价函数变多少。参数一多,效率就很低,梯度也不准。
可微光线追迹不一样。它是把光线追迹的整个过程,用 PyTorch 或 TensorFlow 这样具有自动微分能力的框架来实现。光线打到曲面上的交点、按 Snell 定律算出的出射方向、到达像面的坐标,全部变成计算图里的一个个节点。你只需要把最终的评价函数(比如 RMS 弥散斑半径)算出来,然后 backward() 一下,所有光学参数对评价函数的梯度就自动算出来了。
说得直白一点:整个光学仿真系统变成了一个大号神经网络,只不过中间层是“曲率、厚度、折射率”而不是“卷积核”。你可以用 Adam、SGD 这些深度学习的优化器去优化镜头参数,收敛行为和收敛速度都完全不同。
2.2 端到端设计:光学和算法联合优化
可微追迹最有价值的地方,不是让你用 PyTorch 重新写一遍 Zemax 的功能,而是让“光学 + 仿真 + 算法”可以变成一个整体来优化。
举例来说,计算成像系统里通常有一段图像复原网络。传统做法是先固定镜头设计,再训练网络去还原镜头产生的模糊图像。端到端做法是把镜头参数也扔进网络里,镜头初始像差搞得大一点没所谓,算法会去适应这个像差,最终评判的是“解码后的最终图像质量”而不是中间那个模糊像。镜头变轻了、系统变便宜了、最终效果反而更好。
同样的逻辑也适用于照明设计:把 LED 光源的排布、自由曲面透镜的面型、甚至传感器算法的阈值一起优化,目标是最终的探测性能,而不是单独一个照度均匀性指标。
2.3 与传统仿真流程的差别
我整理了一张表,方便你把传统方式和可微端到端方式放在一起对比:
| 维度 | 传统光学设计/仿真 | 可微端到端优化 |
|---|---|---|
| 优化目标 | RMS波前差、RMS弥散斑、畸变等中间像质指标 | 系统最终性能(解码图像质量、识别准确率) |
| 梯度获取 | 差分近似 / 局部搜索 | 反向传播,解析梯度 |
| 约束处理 | 优化完再人工检查 | 直接写成损失项或约束层 |
| 光学与算法 | 两者分开迭代 | 同时优化,互相适应 |
| 适用场景 | 传统镜头、成熟构型 | 计算成像、自由曲面、非传统光路/new构型 |
| 调试成本 | 低,软件成熟 | 高,需要自己建模,坑更多 |
这张表不是要否定传统工具。Zemax/CODE V 在工程落地和公差分析上依然不可替代,但“趋势”已经在明确地往右侧倾斜,尤其是学术前沿和新兴产品方向。
3. 上手路线:从 Zemax 自动化到跑通一个可微系统
3.1 先别急着扔掉手里的老朋友
很多硕博士一开始会觉得,要不我直接放弃 Zemax,全面转用深度学习库来做光学设计?我的建议相反:Zemax 和 CODE V 不要扔,它们是验证结果、做公差分析和衔接工厂加工的最后一环。正确姿势是把它们变成你工作流里的“验证后端”,把可微优化放在前面做探索。
如果暂时不想迈大步,可以先做 Zemax 的 Python 自动化,用 ZOSAPI 或 PyZDDE 这类接口批量改参数、批量跑优化、批量读点列图。这一步本身就能帮你节省大量时间,还能让你逐步习惯用代码控制光学设计。
3.2 可微光学库怎么选
这一两年开源社区出现了不少“可微光学”“端到端光学设计”相关的库。你直接在 GitHub 上搜 differentiable optics、differentiable ray tracing、end-to-end lens design,就能找到一批。常被提到的包括学术界开源的 dO、以及提供可微元件建模的 torchoptics 这类库。
环境准备其实不复杂:
- Python 3.10 以上,Linux 或 Windows 都行;
- 装好 PyTorch,带 GPU 更舒服,CPU 跑小实验也能忍;
- pip 安装你选中的可微光学库,按仓库 README 操作;
- 建议在 Google Colab 上先跑通一个官方 demo,而不是一上来就配自己的环境。
3.3 一个最小可跑的核心代码骨架
不同库 API 不同,但整体逻辑高度类似。下面这段代码是核心骨架,你换成具体仓库的 API 就能跑:
import torch import torch.nn as nn # 以某个可微光学库为例 from differentiable_optics import SphericalLens, OpticalSystem, sample_rays class MyLens(nn.Module): def __init__(self): super().__init__() # 曲率半径和厚度作为可学习参数 self.curvature = nn.Parameter(torch.tensor(0.012)) self.thickness = nn.Parameter(torch.tensor(5.0)) def forward(self, rays): # 传入一束光线,返回追迹到像面的结果 lens = SphericalLens( curvature=self.curvature, thickness=self.thickness, n=1.5168, # K9玻璃折射率,固定不变 semi_diameter=12.7 ) system = OpticalSystem([lens], image_plane_z=55.0) return system.trace(rays) model = MyLens() # 三个视场角入射:0度、0.05弧度、0.1弧度 rays = sample_rays(field_angles=[0.0, 0.05, 0.1], wavelength=0.55, pupil_samples=64) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) for step in range(300): optimizer.zero_grad() traces = model(rays) # 用RMS弥散斑半径作为主要损失 loss = traces.rms_spot_radius() # 再加一个总长约束:超过60mm就要罚 loss = loss + 0.1 * torch.relu(traces.systems[0].total_length() - 60) loss.backward() optimizer.step()这段代码的逻辑就是:把镜头参数当作神经网络权重,把光学指标当作损失函数,然后让 Adam 去更新曲率和厚度。第一次跑通这个流程,你就会明白为什么大家说“光学仿真正在变成可微编程”。
3.4 为什么推荐同时补一点 ABCD 矩阵和薄透镜模型
别觉得这类可微光线追迹库是黑箱,自己动手推导一遍最简单的薄透镜系统会非常有帮助。用 ABCD 矩阵表示光线在自由空间传播和透镜折射,配合 PyTorch 的自动微分,你甚至不需要完整光追就能感受“梯度如何穿过光学系统”。这个热身练习的成本极低,但能帮你建立对可微光的直觉,后面调试复杂系统时会少踩很多坑。
4. 实测记录:把一个三片式物镜从零优化出来的过程
4.1 设计任务与初始结构
光说不练没有说服力。我在这里描述一个典型的端到端实验,你可以当作自己的第一个复现课题。
设计任务:
- 焦距 50 mm;
- F 数 2.8;
- 半像高 10 mm;
- 波长 0.486 μm 到 0.656 μm;
- 系统总长不超过 60 mm;
- 目标:全视场 RMS 弥散斑半径小于 30 μm。
初始结构我用了一个近似的 Cooke 三片式原始参数(示意值,不是专利精确值),你可以从公开专利库里找更靠谱的起始点。初始结构大概长这样:
| 面序号 | 曲率半径 (mm) | 厚度/空气间隔 (mm) | 玻璃 |
|---|---|---|---|
| 1 | 18.7 | 3.5 | K9 |
| 2 | -24.5 | 4.2 | 空气 |
| 3 | -12.3 | 3.0 | F2 |
| 4 | 22.6 | 3.5 | 空气 |
| 5 | -16.4 | 4.8 | K9 |
| 6 | -32.8 | 后面接像面 | - |
初始 RMS 弥散斑差不多在 50 μm 上下,明显不达标。
4.2 损失函数是怎么拆的
端到端优化的核心工作是“把约束翻译成损失”。我这次用了四个损失项:
loss = (w1 * rms_spot + w2 * relu(total_length - 60) + w3 * (focal_length - 50) ** 2 + w4 * penalty_edge_thickness) # 边缘厚度不能小于0.5mmrms_spot是主目标,控制像质;relu(total_length - 60)是软约束,总长超过 60 mm 就罚,没超过就不管;(focal_length - 50)^2是焦距锁定项,防止优化把焦距随风跑;penalty_edge_thickness是加工可行性约束,防止镜片边缘厚度变成负的。
权重 w1 到 w4 需要根据量级调。RMS 弥散斑半径是微米级,总长是毫米级,如果不归一化,梯度会被大数量级的项带走,训练直接跑飞。
我习惯先把所有约束项归一化到差不多 1 的量级,再按重要性调权重。比如 RMS 设为主项 1.0,焦距偏差项给 50.0(因为平方之后 0.1 mm 偏差大概是 0.01,乘 50 变成 0.5),总长惩罚给 0.5,边缘厚度惩罚给 2.0。具体数字要自己试,但思路就是这个思路。
4.3 跑出来的收敛记录
一次典型的收敛记录大概是这样的:
| 指标 | 初始值 | 可微优化 500 步 | 可微优化 1500 步 |
|---|---|---|---|
| RMS 弥散斑半径 | 49.6 μm | 24.1 μm | 18.2 μm |
| 系统总长 | 62.5 mm | 59.8 mm | 58.7 mm |
| 焦距偏差 | -2.3 mm | 0.4 mm | 0.1 mm |
| 边缘厚度最小值 | 0.2 mm | 0.5 mm | 0.8 mm |
可以看到,优化前期像质收敛特别快,后面主要是约束项在起作用。这就是可微优化的特点:你能直接看到各项损失在怎么拉扯,也能很方便地在中间干预。
4.4 拿同样初始结构去跑 Zemax 局部优化,对比一下
我也用同样的初始结构喂进 Zemax 的 local optimization 作为对比。局部优化跑 50 轮之后,RMS 弥散斑也能降到 20 μm 左右,看起来和可微优化差不多。
但差异在哪里呢?Zemax 那边要让总长、焦距和边缘厚度同时满足,我得不停手动改评价函数权重,改一次跑一次,非常痛;而可微优化这边,约束一旦写成损失项,就是自动跟着梯度走的。另外,如果我的目标不是“镜片像质好”,而是“最终图像经过神经网络还原后识别得准”,Zemax 这条路基本走不通,因为没有地方让你把神经网络接进去做联合优化。
5. 踩坑清单:可微光学设计最容易翻车的四个地方
5.1 优化出非物理解
这是刚上手时的重灾区。可微优化并不知道“光学系统应该是什么样”。它为了压低损失,可能会把曲率半径优化到几毫米、镜片厚度变成负数、镜片直接纠缠在一起。从数学上看损失很小,从物理上看完全是垃圾。
解决办法很简单,但也容易被忽略:每次迭代后都要检查物理约束,把违反约束的参数拉回来,或者在损失函数里加入足够强的惩罚项。不要指望 Adam 自己会“守规矩”,可微优化器没有这个良心。记得像我前面代码里那样,把relu形式的软约束变成 optimizer 的一部分,而不要事后手动改参数。
5.2 玻璃库是离散的,没法直接反传梯度
你在可微系统里通常把折射率当成连续变量来优化,优化完发现折射率是 1.5237,而玻璃库里根本没有这种玻璃。这就是典型的“仿真很丰满,现实很骨感”。
我的处理方式是分两步走:第一步在可微系统里用连续折射率找结构趋势;第二步把折射率吸附到 K9、F2、H-K9L 这类常用玻璃上,再丢回 Zemax/CODE V 做一次精修。也有研究组在做可微玻璃选择层,用 Gumbel-Softmax 这类离散优化技巧来选玻璃,但目前还没到工程成熟期。
5.3 仿真和实物的差距比你想象的大
可微光线追迹通常只考虑几何光线和理想表面,尘埃、镀膜、杂散光、热膨胀、实际加工公差统统不在计算图里。你千辛万苦把 RMS 弥散斑优化到 0.8 μm,拿去加工回来实测 8 μm,这不是模型错了,而是你只算了一半。
所以可微优化出来的结果不要直接当成最终设计。正确流程是:可微优化做全局探索 -> 结构定型 -> 转入传统工具做公差分析和杂散光分析 -> 必要时再做一次传统优化收尾。趋势不是要替代 Zemax,而是在 Zemax 前面加一个更聪明的“前锋”。
5.4 损失项权重不平衡,训练飘了
我前面提过这个坑,但值得单独再写一遍:光学指标是多尺度的。斑点半径是微米级,系统总长是毫米级,焦距是几十毫米级。如果把这些项直接相加,大的项会把小的项淹没。训练初期你可能看到总长在下降,但 RMS 弥散斑一点没动,其实是被损失权重绑架了。
经验做法是先把各项归一化到可比范围,再按重要性调权。我还会用学习率衰减,前 200 步主要让像质快速收敛,后面逐步提高约束项的权重,这种调度在传统光学优化里是没有的,但深度学习社区有很多现成技巧可以借鉴。
6. 对在读硕博士的实在建议:怎么把这股趋势变成自己的优势
6.1 守住传统基本功,别当只会敲 PyTorch 的人
可微优化会火,但不会让像差理论失效。谁来决定一个初始结构值不值得优化?还是靠你对像差平衡、光焦度分配、玻璃组合的直觉。这几年我看到一个现象:很多学生代码写得飞起,但拿到一个初始结构完全不知道它为什么长这样,也判断不了优化结果是不是真的合理。
我的建议是把传统光学设计课程和像差理论学扎实,Zemax/CODE V 至少熟练一个。它们是你在可微优化结果里“挑毛病”的眼睛,也是你最后跟加工厂对接时唯一通用的语言。
6.2 补上三块拼图,就够了
从能力拼图的角度看,你不需要成为全能选手,但有三块是绕不开的:
- 可微编程基础:能看懂 PyTorch 的计算图和 backward 机制,会写自定义损失函数;
- 至少一个可微光学库:在 GitHub 上选一个活跃仓库,把它的示例代码全部跑通,改造成自己的实验;
- 一个完整的验证闭环:从可微优化到 Zemax 精修,再到公差分析,最好亲自走一遍。
这三块都补齐之后,你已经比绝大多数只懂传统光学设计或只懂深度学习的人多了一个维度。
6.3 从一个小而明确的课题切入,别想一口吃成胖子
如果你正在挑课题或者写大论文,我最真诚的建议是:不要一上来就挑战“完全自动设计一颗从零开始的自由曲面镜头”这种宏大目标。切入点的选择决定了你能不能在三五个月内看到正反馈。
我觉得比较合适的起步课题有这几个:
- 在现有镜头结构上,加一层可微的衍射光学元件(DOE),用端到端方式优化 DOE 面型,让系统总长明显缩短;
- 用一个神经网络做光学仿真的代理模型,代替蒙特卡洛公差分析,把公差风险大的系统一次性筛掉;
- 做一个小型计算成像系统,镜头和复原网络联合优化,哪怕相机只有一枚塑料镜片,效果也会很有故事性。
这些课题的共同特点:传统方法做不动、可微优化能发力、实验结果容易量化对比。我见过不少学生用这类课题发了不错的期刊论文,也顺带把编程能力练出来了。
最后再分享一个小技巧:做这类实验时,务必要把中间结果全部存下来,不要只看最终曲线。可微优化的中间结构有时候带着很强的“非人直觉”,主镜片全放到前面、最后一片几乎没光焦度、或者镜片弯向完全相反但像质反而更好——这些结构在传统设计里几乎不会出现,却是新构型灵感的来源。我自己就靠翻中间优化日志,救回来过两个被判了死刑的初始方案。趋势已经摆在这了,真正把它变成自己优势的,往往就是愿意在这些细节上多走一步的人。