做微透镜阵列相关仿真这几年,我最大的感受是:Zemax和MATLAB单拎出来都不够用。Zemax能把光追得明明白白,但它不太擅长处理"这个像素属于哪个微透镜视角"这类计算成像问题;MATLAB做图像处理和算法验证很强,可你要它自己追一遍几万根光线,写出来的脚本又慢又啰嗦。硬把两者拼在一起用,又会遇到数据格式不匹配、坐标定义对不上、导出数据一堆坑的问题。这篇文章就把我实际搭建的一套"Zemax光学仿真 + MATLAB数据后处理"流程完整拆开,覆盖微透镜阵列建模、光场相机仿真、Shack-Hartmann波前传感器仿真、以及传感器数据提取这几个环节,希望能给正在做相关课题的同学提供一条可以直接参考的路线。
1. 为什么我把微透镜阵列的光学仿真拆成Zemax和MATLAB两段来做
先说个实际场景。之前我带的一个项目要做一台紧凑型光场相机原型机,核心器件就是主镜头后面贴一块微透镜阵列,再往后是CMOS传感器。光路上看结构很简单,主镜头+Lenslet+Sensor三层,但真正开始验证方案的时候发现,你没法只用一套软件打通全链路。
Zemax非常擅长处理的是"光怎么走"——光阑放在哪、像差多大、哪个面引入了多少彗差,这些它能追得很准。但光场相机真正关心的其实是"同一个物点发出的光,经过不同微透镜后,落在传感器上哪些像素范围内",这就涉及到对传感器二维图像按微透镜网格做重新排布,属于典型的计算成像后处理问题,Zemax不提供这种功能。MATLAB反过来,它有强大的矩阵运算和图像处理工具箱,可以做光场重聚焦、数字对焦、波前重建之类的算法,但让MATLAB去模拟一套包含衍射效应的微透镜阵列光路,既不直观计算量也大。
所以我的方案是:用Zemax负责"物理正确",用MATLAB负责"数学正确"。Zemax负责搭建光学模型、追迹光线、生成探测器上的辐照度分布图,然后把这些图和数据导出;MATLAB负责把Zemax的输出读进来,做质心提取、相位斜率计算、波前重建、光场解码等后续处理。两段流程之间的接口就是"探测器上的强度分布图",这个数据本身不依赖任何独特格式,就是一张二维灰度图,两边都认识,通用性很好。
1.1 两者的分工逻辑
具体的分工边界我总结成下面的表格,做联合仿真之前先把这个想清楚,后面会省很多事:
| 任务类型 | 使用工具 | 原因 |
|---|---|---|
| 光学系统结构设计、光线追迹、像差评估 | Zemax | 物理光学模型成熟,追迹速度快,结果可直接用 |
| 微透镜阵列的几何排布、间距、焦距设计 | Zemax | 可以通过面型参数精确控制每个透镜单元 |
| 探测器上的辐照度分布(光斑阵列图) | Zemax | 探测器Viewer能直观给出强度分布,支持导出 |
| 光斑质心提取、亚像素定位 | MATLAB | 图像处理函数丰富,容易批量处理 |
| 波前斜率计算、波前重建、Zernike拟合 | MATLAB | 数值算法灵活,便于自定义重建算法 |
| 光场解码、数字重聚焦、深度估计 | MATLAB | 计算成像算法的标准实现平台 |
这个分工并不是绝对的,但有一条原则值得记住:凡是涉及到"对光的传播规律做物理建模"的部分,交给Zemax;凡是涉及到"对探测器输出做数学处理和信息解算"的部分,交给MATLAB。如果你非要用Zemax做质心提取,用MATLAB追光线,能跑通但效率是很低的,属于给自己挖坑。
1.2 一条完整的数据流路径
我建议在开始做之前先明确数据流路径。我的习惯是这样:
- 先在Zemax里搭好系统模型,确定所有面参数(曲率半径、厚度、材料、半直径);
- 设置好光源、视场、波长,切换到探测器Viewer,查看光斑阵列分布;
- 确认光学设计没问题后,把探测器图像数据导出为文本或图像文件;
- 打开MATLAB,读取Zemax导出的数据,转成强度矩阵;
- 对强度矩阵做微透镜网格对齐、质心提取、斜率计算等操作;
- 根据应用目标做进一步处理(光场相机就做光场解码/重聚焦,波前传感器就做波前重建)。
1.3 我需要什么数据、Zemax能导出什么
这个地方我踩过比较深的坑。Zemax的探测器Viewer界面看起来是一个很直观的彩色图,但如果想用MATLAB做定量处理,直接截图是绝对不行的。截图会丢失像素坐标和强度数值,你没法保证CMOS仿真图上每个像素对应的物理尺寸。
正确的做法是让Zemax导出数据矩阵本身。在探测器Viewer窗口里可以设置输出文本格式,得到"D:\路径下.txt文件",里面是一个二维数组,数组的行列对应探测器上的采样网格,数值是归一化的辐照度。用MATLAB的load或importdata命令直接读进来,转成double矩阵就可以做后续计算了。这个环节看起来不起眼,但它是整条联合仿真链路能否走通的关键接口。
2. Zemax里微透镜阵列的建模:序列与非序列的选型权衡
微透镜阵列的建模,最核心的一个决策是"用序列模式还是非序列模式"。这个问题没有标准答案,取决于你要仿真什么系统。
如果只是做波前传感器(Shack-Hartmann)的仿真,微透镜阵列后面跟着的是传感器,光线经过每个微透镜后会聚到同一个焦平面上,没有复杂的分光、反射、散射效应,用序列模式就够了。序列模式的好处是追迹速度快、系统结构简单清晰、像差分析工具全部可用。但如果要仿真的是光场相机,主镜头和微透镜阵列之间可能还有孔径光阑、中继镜组,或者你想观察光线在微透镜边缘的散射、反射、鬼像,那就得用非序列模式。
2.1 序列模式建模微透镜阵列
在序列模式下,微透镜阵列的建模通常用"User Defined Surface"或者"Binary Optics"面型。最常用的是在Zemax的Surface Type里选择"Binary Optic 2",通过相位多项式来模拟微透镜的相位分布。对于周期性的微透镜阵列,你可以设置一个基底面加上相位型微透镜,用多项式的系数来拟合每个微透镜的起伏。
不过用Binary Optic建模有个局限性,它模拟的是"相位调制",不是真实的几何面型,所以在分析边缘光线、衍射效应、以及光线在微透镜边界上的行为时会有偏差。如果研究重点是微透镜阵列的衍射效应、点扩散函数、以及相邻微透镜之间的串扰,我个人更推荐直接在非序列模式下用实际的面型阵列来建模。
2.2 非序列模式建模微透镜阵列
在非序列模式(Non-Sequential Mode)下,微透镜阵列可以用一组"Object"组合而成。一种做法是使用"Array Object"功能,直接生成二维规则排列的微透镜阵列。Zemax的非序列模块支持用户自定义光学面的阵列,你可以把单个微透镜的几何参数(曲率半径、口径、厚度)设置好后,通过阵列复制得到整个微透镜阵列。
这样做最大的好处是每个微透镜都是真实的面型,你可以直接看到光线在微透镜边缘的折射、全反射、以及相邻透镜间的串扰,仿真结果更接近实际。缺点是非序列模式的追迹速度会慢一些,特别是微透镜数量多、光线数量大的时候。
一个简化技巧:如果微透镜阵列规模很大(比如100×100个),全阵列追迹非常耗时。可以先仿真一个中心区域的子阵列(比如5×5个微透镜),用这个结果验证算法和处理流程,再逐步扩展到全阵列。这个方法在做光场相机传感仿真时特别实用。
2.3 参数设计实例:典型微透镜阵列的系统参数
为了让例子更具体,我在这里给出一组我实际用过的微透镜阵列参数,你们可以参考调整:
| 参数 | 数值 | 说明 |
|---|---|---|
| 微透镜节距(Pitch) | 150 μm | 相邻微透镜中心的距离 |
| 微透镜焦距 | 1.0 mm | 决定光斑聚焦位置 |
| 微透镜口径 | 150 μm | 方形孔径,填充因子接近100% |
| 基底材料 | N-BK7 | 常见光学玻璃 |
| 中心波长 | 550 nm | 可见光波段 |
| 阵列规模 | 100×100 | 完整阵列,或截取子区域仿真 |
| 传感器像元尺寸 | 2.5 μm | 每个微透镜下覆盖约60×60像素 |
这个参数组合实际上是参考了很多商用光场相机的设计思路。微透镜节距150μm,每个微透镜下覆盖60×60个像元,意味着你可以对每个微透镜的图像做子像素级别的处理,分辨率足够做数字重聚焦和深度估计。这个设计的具体实现过程,在后面的光场相机仿真部分我会详细展开。
3. 光场相机完整仿真链路的搭建过程
光场相机和普通相机的核心区别在于:普通相机记录的是"光的强度",光场相机记录的是"光的分布",也就是同一物点发出的光经过不同方向的微透镜后,被传感器不同位置的像素接收。这样后期可以通过数字方式改变焦点位置,甚至恢复场景的深度信息。
在Zemax里仿真光场相机,我的搭建流程可以概括为"主镜头系统设计—微透镜阵列放置—探测器设置—光源与采样设置—数据导出"五步。
3.1 系统架构:主镜头、微透镜阵列和传感器的相对位置
光场相机系统的基本结构是:物面 → 主镜头 → 微透镜阵列(位于主镜头像面附近)→ 传感器(位于微透镜阵列后约一个微透镜焦距处)。有一个概念很重要:微透镜阵列不是放在主镜头的焦平面上,而是放在主镜头的像平面上,这样每个微透镜看到的主镜头光瞳图像才会形成一个完整的光场采样。
举个例子,假设主镜头的焦距是50mm,系统设计让物点经主镜头后在像面上形成一个清晰的像。微透镜阵列放在这个像面上,相当于把一整幅图像"切"成了很多小单元,每个微透镜后方1mm处就是传感器。传感器上每个微透镜覆盖的范围记录的是该位置不同方向上的光线信息,这些信息就是光场数据。
3.2 在Zemax里设置对象、光源和探测器
在非序列模式下,整个系统可以分解为以下几个Object:
- Source Object:用Collimated Source或者Source Ray模拟物方光线。如果要仿真一个Extended Scene(扩展场景),可以用Source File或者Source Image,直接把一幅环境图像作为输入,这特别适合光场相机的场景仿真。
- 主镜头:用标准透镜(Lens Element)或者Even Asphere面型来建模。
- 微透镜阵列:用阵列对象来放置大量微透镜单元。
- 探测器:用Detector Rectangle模拟CMOS传感器。
光源设置这块特别提醒一下:光场相机仿真时,通常需要在物方设置多个视场角或不同方向的光线,否则你得到的探测器图无法体现出"不同视角通过不同微透镜"的效果。我的习惯是至少设置3个视场角(如0°、±2°),或者用扩展光源来模拟具有一定角宽度的场景。
3.3 如何查看和导出原始光场数据
仿真完成后,用Detector Viewer查看结果——你会看到一个由许多个小圆斑组成的阵列图像,每一个小圆斑就是经过一个微透镜后形成的子图像。这就是原始光场图(Raw Light Field Image)。
有人第一次看到这个图会很困惑:这看起来就是一幅模糊的网格图,根本看不出场景。这是因为光场相机的原始数据必须要经过解码和重排才能恢复出可理解的正视图和视角图。在Zemax里得到的是"编码后的图像",而MATLAB要做的正是"解码"。
导出时需要注意几个设置:
- 探测器分辨率要设置得足够高,否则每个微透镜下的光斑会采样不足。以150μm节距的微透镜为例,如果传感器像元是2.5μm,理论上一个微透镜下是60×60像素。这个仿真在Zemax里要对应设置探测器的像素尺寸,不要用默认的大像素。
- 导出格式选择文本矩阵,保存后是一个二维数组。
- 同时记下探测器的物理尺寸和像素数,MATLAB处理时需要用这个信息做坐标换算。
4. 波前传感器的仿真与数据导出
波前传感器和光场相机在硬件结构上其实高度相似,都是"微透镜阵列+传感器"的组合。区别在于,光场相机关注的是场景的光线方向分布,而波前传感器关注的是入射波前的相位信息。Shack-Hartmann波前传感器的原理是:入射波前经过微透镜阵列后,在每个微透镜的焦平面上形成光斑;如果入射波前是理想的平面波,那么所有光斑都在各自微透镜光轴的焦点位置;如果波前存在像差,光斑相对于参考位置会发生偏移,偏移量与局部波前斜率成正比。
这个原理决定了在Zemax里怎么做仿真:给一个带有像差的入射波前,经过微透镜阵列,在探测器上形成一组会移动的光斑,导出光斑图像,然后在MATLAB里计算每个光斑的质心偏移,再重建入射波前。
4.1 Shack-Hartmann波前传感器的工作原理和仿真需求
波前传感器的仿真难点在于入射波前的设置。Zemax里模拟理想平面波很简单,聚焦一个点源无穷远即可;但要模拟带有球差、彗差或高阶像差的波前,需要额外设置。我有两种比较常用的做法:
- 在物方放置一个含像差的透镜,让平面波经过这个透镜后产生预期的波前畸变;
- 或者使用Zernike相位面(Zernike Phase Surface)直接在微透镜阵列前引入相位扰动。
第二种方法更灵活,因为你可以直接指定Zernike系数,比如"第4项离焦0.5μm,第7项彗差0.2μm",然后用这个已知波前来验证MATLAB重建算法是否正确。整个流程形成一个闭环:设定已知波前→仿真得到光斑偏移→MATLAB重建波前→与设定波前对比误差。
4.2 在Zemax中设置微透镜阵列和畸变波前
系统搭建方式可以简化为:
- 在微透镜阵列前设置一个Zernike Phase Surface,输入波前畸变参数;
- 微透镜阵列本身用非序列或序列模式建模;
- 在微透镜阵列的焦面位置放置探测器,记录光斑阵列。
这里面有一个参数匹配问题要特别注意:微透镜的F数决定了光斑尺寸。对于焦距1mm、口径150μm的微透镜,F数约为6.7,理想焦点光斑尺寸(艾里斑直径)在550nm波长下大约是2.44×λ×F数≈2.44×0.55μm×6.7≈9.0μm,相当于3~4个传感器像元。这个尺寸既不会太小导致采样不足,也不会太大导致相邻微透镜光斑串扰,设计时要注意控制。
4.3 导出光斑阵列数据
导出方式类似光场相机,用探测器Viewer导出文本矩阵。但这里有一个关键点:导出的光斑图需要包含足够的背景信息,尤其是局部波前倾斜比较大的情况,光斑偏移量可能超过一个微透镜对应像元范围,如果探测器视场设置得太小,光斑跑到探测器边缘外,质心提取就会出现偏差。采样规则是:探测器范围至少是单个微透镜对应像元范围的2~3倍,这样即使光斑偏移量很大也能完整记录下来。
5. MATLAB端的数据处理:质心提取、波前重建与数字重聚焦
Zemax已经把光斑阵列或光场原始图像给了MATLAB,接下来就是整个联合仿真的重头戏:如何把这些二维强度分布变成有物理意义的结果。
5.1 从Zemax导出数据到MATLAB的格式转换
这一步主要是数据读入和预处理。假设Zemax导出的文件名是"sensor_data.txt",用MATLAB读取:
% 读取Zemax探测器导出的文本数据 data = load('sensor_data.txt'); % 或 importdata I = double(data); % 确保为double类型 % 归一化 I = I / max(I(:));读进来之后先做质量检查:看数据矩阵的大小是否和Zemax探测器的像素设置一致;看最大值是否在合理范围;看背景噪声是否过大。检查完以后,一般需要做一个阈值处理,把强度低于一定阈值的像素置零,这样可以减少微弱噪声对质心提取的影响,这个步骤在波前传感器的处理中尤其重要。
5.2 光场相机数据:微透镜网格对齐与光场解码
光场数据的核心操作是"网格对齐"——也就是确定传感器上每个像素应该归入哪个微透镜的哪个子孔径像素。这一步对网格对齐的精度要求很高,因为微透镜阵列在真实加工和安装时会存在微小的旋转和平移误差,仿真环境下虽然理想,但考虑到实际系统,我的做法是先用一张均匀照明图来标定每个微透镜中心的精确位置。
标定的过程可以这样做:
% 假设Iraw是原始光场图,pitch_px是每个微透镜对应的像素数 % 通过找每个微透镜区域的质心来确定网格中心 % 然后用imresize或interp2把原始图像重采样到均匀网格网格对齐后,光场可以用一个四维数组表示:L(u,v,s,t),其中(s,t)是空间维(对应微透镜位置),(u,v)是视角维(对应微透镜下子孔径像素位置)。得到四维光场后,可以通过在空间维做积分获得不同视角的图像,也可以通过重聚焦公式获得对焦在不同深度的图像。
5.3 波前传感器数据:质心提取算法
波前传感器数据处理的第一步是对每个微透镜对应的子区域计算光斑质心。这里要特别说明的是,质心提取的精度直接决定了波前重建的精度,所以算法选择上要讲究。
最基本的质心算法是直接计算灰度权重质心:
function [cx, cy] = centroid(I_sub) % I_sub: 单个微透镜对应区域的强度分布 [Y, X] = meshgrid(1:size(I_sub,2), 1:size(I_sub,1)); total = sum(I_sub(:)); cx = sum(sum(I_sub .* X)) / total; cy = sum(sum(I_sub .* Y)) / total; end实际处理中,为了提高精度,我通常会先对子区域做阈值处理,去除背景噪声;再对光斑进行高斯拟合,以拟合中心作为质心位置。实验和仿真都表明,高斯拟合法在光斑接近高斯形状时的定位精度通常比直接质心法高,能到0.01像素甚至更高。不过要注意的是,如果波前畸变大导致光斑形状明显不对称,高斯拟合反而会引入偏差,这时候用加权质心法更稳妥。
5.4 从质心偏移到波前重建:Zernike拟合与区域法
得到每个微透镜的光斑质心偏移量(Δx, Δy)后,局部波前斜率可以通过下面的关系得到:∂W/∂x = -Δx / f_MLA,∂W/∂y = -Δy / f_MLA,其中f_MLA是微透镜焦距。
重建波前的方法有两大类:
- 区域法(Zonal):直接对斜率数据进行数值积分。优点是无需先验假设,缺点是噪声影响大。
- 模式法(Modal):用Zernike多项式拟合斜率数据。先构造Zernike多项式的导数作为基函数,在最小二乘意义下求解系数,然后用Zernike系数重建波前。优点是抗噪声能力强、结果平滑,也是更主流的做法。
MATLAB里做Zernike拟合可以用我写的这个简单版本的核心思想:将Zernike多项式的x、y偏导数矩阵构造出来,然后做线性最小二乘拟合。需要注意Zernike多项式的归一化方式要统一,否则拟合出的系数会不标准。
5.5 数字重聚焦的算法实现
光场相机拿到光场四维数据后,数字重聚焦是最能体现计算成像价值的操作。原理很简单:对光场进行剪切(Shear)然后积分。数学上,重聚焦公式通常写成:
% LF是四维光场数组 % alpha是重聚焦系数,1表示聚焦在原平面 % u, v是视角维索引 resampled = zeros(size(LF, 3), size(LF, 4)); for u = 1:size(LF, 1) for v = 1:size(LF, 2) offset = (1 - 1/alpha) * [u - centerU, v - centerV]; shifted = imtranslate(squeeze(LF(u, v, :, :)), offset); resampled = resampled + shifted; end end resampled = resampled / (size(LF, 1) * size(LF, 2));这个操作本质上是对不同视角的图像做不同大小的平移后取平均。当平移量正好补偿景深差时,目标深度处的细节会变得最清晰。
6. 参数匹配、误差来源与我自己的几个经验
联合仿真做得越多,越能感觉到"软件之间的接口设置"往往比"光学设计本身"更容易出问题。以下几点是我反复踩过坑之后沉淀下来的经验,写出来供你们参考。
6.1 最关键的设计参数匹配问题
微透镜阵列焦距的选择,直接决定传感器上光斑尺寸和系统的角度分辨率。有一个经验公式大家可以记一下:微透镜F数应该与主镜头F数匹配。比如主镜头F数是4.0,微透镜阵列的F数最好也在4.0左右。如果不匹配,会导致光斑在传感器上过大或者过小,过大会串扰到相邻微透镜,过小则无法对主镜头光瞳充分采样。
6.2 采样率对处理结果的影响
在Zemax里设置探测器分辨率时,尽量保证每个微透镜下的光斑至少覆盖3×3以上像素,更好是5×5到7×7。采样率太低,质心提取精度会显著下降;太高则追迹时间变长,而且相邻像素之间的强度差值会小于噪声水平,继续提高已经没有意义。
6.3 仿真耗时和精度之间的平衡
非序列模式下追迹几百万根光线往往要跑很久,我常用的策略是:先用少量光线(比如10万根)做快速验证,确认系统结构和探测器设置没有明显错误;然后逐步增加光线数量,观察探测器图像的变化,直到光斑轮廓足够平滑再停止。这样既能保证精度,又不至于从一开始就陷入漫长的等待。
6.4 需要特别注意的坐标系和单位问题
Zemax导出数据里的坐标通常是"探测器上的像素索引",而MATLAB处理时要换算成物理尺寸。单位问题我专门吃过亏:Zemax里长度单位可以设置成mm或μm,导出的文本矩阵本身不带单位信息,如果不去记录和换算,在计算质心偏移对应的波前斜率时会把数值算错好几个量级。个人建议:从仿真一开始就固定所有长度单位,并写进代码注释,比如"单位:μm,焦距1.0mm = 1000μm",能避免后面排查半天找不到原因的尴尬。
7. 一个实际处理案例:从含畸变波前到重建结果
光说不练假把式,我拿之前做过的一个例子来完整走一遍流程,方便大家理解参数量级和算法效果。
假设入射波前有两个Zernike分量:离焦(Z4,系数0.5μm)和初级球差(Z11,系数0.2μm),微透镜阵列参数和前面表格一致(pitch=150μm, f=1mm),探测器的像元尺寸2.5μm,每个微透镜对应60×60个像素。
Zemax仿真的探测器图导入MATLAB后:
- 先对整幅图像按150μm/2.5μm=60像素的周期做网格划分;
- 对每个60×60子区块求质心,得到一组质心偏移量;
- 用偏移量除以焦距得到斜率;
- 用Zernike模式法做最小二乘拟合,得到重建的Zernike系数。
最终重建结果:离焦系数重建为0.49μm(误差2%),球差系数重建为0.21μm(误差5%)。这个精度对于大多数自适应光学应用来说完全够用了。误差主要来源是探测器像素采样离散化和质心提取算法的精度极限。
这里还要提醒一个容易忽略的点:微透镜阵列边缘的子区域,光斑可能因为视场边缘照度下降而变暗,导致质心提取不稳定。实际处理时我的习惯是剔除边缘一圈微透镜的数据,或者单独降低它们的权重,不要让边缘效应影响整体拟合结果。
8. 总结外的几句实在话
做微透镜阵列的Zemax + MATLAB联合仿真,本质上不是两套软件的拼接,而是一条从物理光学建模到计算成像算法验证的闭环链路。Zemax产出高精度的光学仿真数据,MATLAB把这些数据变成可量化、可优化的物理量,二者缺一不可。
这套流程的扩展空间也很大:光场相机端可以延伸到深度估计、三维重建、全聚焦成像;波前传感器端可以延伸到自适应光学闭环、大气湍流补偿、人眼像差测量。仿真验证过的算法以后也可以迁移到真实硬件上,我在实际使用中发现的规律是:只要仿真阶段的数据流路径是清晰的,真实系统的搭建和联调就会顺畅很多。反过来,如果仿真阶段的数据流是乱的,那硬件阶段大概率要返工。
如果你们在自己的项目里也遇到"Zemax出的图不知道怎么用MATLAB处理"或者"微透镜阵列参数不知道怎么设计"这类问题,可以按照这篇文章的顺序先跑通一条最小化的链路——比如用3×3微透镜阵列验证——再逐步扩展。不要一开始就做100×100的全阵列,那是浪费时间和耐心的节奏。