简介基于MATLAB的光学项目案例合集面向光学工程、电子科学与技术等专业的学生与科研人员用于掌握利用数值计算工具完成光学仿真、成像设计及数据分析的完整流程。整套资料共三十个文件以脚本文件为主另配有使用说明文本、位图与高光谱图像样本、参考文献等辅助内容压缩包整体约二十三兆字节。案例覆盖发光二极管二次光学透镜设计依据非成像光学原理生成二维点坐标可直接导入三维建模软件完成从数值计算到实体模型的完整链路同时包含光学系统调制传递函数的仿真与参数化处理、光线在空气和玻璃中的色散动画、频散现象计算程序以及高光谱数据处理示例能够帮助读者理解波动光学、几何光学与工程实现之间的联系。每个案例均附有说明文档代码结构清晰、注释完整方便直接运行、学习算法思想并迁移到自己的项目中。已有二百四十一人学习下载适合希望结合工程实战巩固光学理论并提升编程能力的中高级学习者。1. 基于MATLAB的光学项目案例从压缩包到可复现的仿真链路很多人拿到一个光学项目案例包第一反应是双击主脚本直接点运行结果大概率碰到一堆红色报错。原因通常不在代码本身而在于你还没理清这个案例里的“光学链路”从光源参数怎么定义到光场用什么矩阵表示再到评价函数需要哪些输入。一个基于MATLAB的光学项目案例本质上是一个完整的小型仿真系统解压后看到的.m、.mat、.fig文件之间有着严格的依赖关系。我一般会先花十分钟读 README、查工具箱依赖、看主脚本前 30 行然后再开始跑。这篇博文就按这个思路讲清楚怎么把一个光学案例包从解压、理解、运行到最终改造成自己的仿真框架适合光学工程师、研究生以及做机器视觉后端时需要复核光学仿真的开发者。2. 光学案例包的典型构成与MATLAB中的建模逻辑2.1 案例包里通常有哪些文件和它们的分工一个常见的光学项目案例包解压后大概逃不掉这几类文件主脚本main.m控制仿真流程子函数文件如propagate.m、aperture.m封装核心算法.mat数据文件里保存玻璃折射率、系统结构、或者实验采集的图像另外还有.fig图形文件和 README。我最重视的是 README 和主脚本的参数区它们决定了整个仿真能不能复现。打开一个陌生案例我会先用ls或资源管理器看文件规模再用文本编辑器读 README。如果没有 README就直接看主脚本开头 30 行。这里有个经验旧案例的.mat文件可能是多版本之前保存的直接load后变量名经常和脚本对不上。所以我会先用whos(-file, xxx.mat)查看变量名再决定要不要手动转存。2.2 从物理世界到矩阵光场的复振幅表示MATLAB 里做光学仿真几乎所有计算都围绕一个核心原则光场是一个二维的复振幅矩阵。矩阵的行列对应空间位置元素值代表该点的振幅和相位。比如模拟一个平面波垂直入射到方形孔径L 5e-3; % 网格边长 5mm N 1024; % 采样点数 dx L / N; % 空间步长 x -L/2 : dx : L/2-dx; [X, Y] meshgrid(x, x); a 1e-3; % 孔径边长 1mm ap abs(X) a/2 abs(Y) a/2; Uin ap; % 入射光场单位振幅这段代码里Uin是一个 1024×1024 的逻辑矩阵逻辑值 1 代表孔径透光区后续所有衍射计算都从它开始。需要重点说明的是N和L两个参数N最好取 2 的幂次因为接下来要用 FFT 相关算法L的取值决定频谱分辨率太小会截断边缘衍射信息太大又会让中心目标只占几个像素。我常用孔径宽度的 510 倍作为L然后观察结果边界有没有异常亮纹再微调。2.3 用 .mat 数据文件管理光学系统参数案例里经常把波长lambda、透镜曲率半径、非球面系数、像差权重等放在.mat文件中。标准做法是data load(system_params.mat); whos(-file, system_params.mat) % 先探明文件里有什么 disp(data.lambda) % 检查波长单位和数值这里有个细节load(file.mat)会把变量直接导入工作区而data load(...)返回一个结构体。在多个案例脚本之间调用时我强烈建议使用后一种避免一个同名变量悄悄覆盖另一个。比如脚本里原本有lambda又加载了一个包含lambda的.mat顺序不对就直接污染了后续计算。提示波长单位是最常见的坑。632.8e-9是 632.8nm但很多案例代码里写的是0.6328意思就变成了微米。所有长度单位不统一结果差距几个数量级。2.4 关键计算参数对照表与混叠检查光学案例中物理参数和 MATLAB 变量之间通常存在一张对应表我在开始仿真前会先填好它物理量符号MATLAB 变量常见取值波长λlambda400700 nm采样点数NN51220482 的幂网格边长LL孔径宽度的 510 倍传播距离zz根据衍射模型数值孔径NANA0.11.0如果案例里有透镜系统采样间隔必须小于lambda/(2*NA)否则频谱混叠会直接毁掉 MTF 或点列图。我一般会在脚本里写一个断言assert(dx lambda / (2 * NA), 采样过粗产生混叠请增加 N 或缩小 L);这个断言会在参数不对时立刻报错比等图像出现莫尔条纹后拍脑袋猜原因高效得多。3. 在MATLAB中复现光学案例的具体步骤与代码3.1 环境准备工具箱依赖与路径设置光学案例经常会用到 Image Processing Toolbox、Signal Processing Toolbox、Optimization Toolbox 等。起手先做一次环境检查required {Image Processing Toolbox, Signal Processing Toolbox}; v ver; installed {v.Name}; for i 1:numel(required) if ~any(strcmp(installed, required{i})) warning(缺少工具箱%s, required{i}); end end这段代码把必备工具箱逐个核对缺失时会报警。比直接运行到imresize或fir1时才报错要直观。路径问题也很常见解压后的函数文件不一定在当前目录所以接着执行addpath(genpath(pwd)); % 把当前目录及其子目录加入搜索路径注意genpath会把所有子目录加进来如果里面有旧的.git或大文件夹会比较慢。我更倾向于在 README 里指定的目录上执行addpath。3.2 读取并可视化案例中的测量数据案例包里的.mat往往是 CCD 干涉条纹、点扩散函数或者相位图。拿到手的第一步是把它显示出来确认数据没有翻转或缺失res load(measured_data.mat); figure; imagesc(res.phase); axis image; colormap(gray); colorbar; title(Phase map); xlabel(pixel); ylabel(pixel);imagesc是光学案例中最常用的可视化函数它能把矩阵的数值范围映射到颜色。这里我会额外检查一个细节res.phase是弧度还是波长单位。如果是干涉仪测出来的相位有的案例存的是波数有的存的是原始条纹灰度两种数据后续处理路径完全不同。复现案例的关键步骤可以总结为下面这张表我每次跑新案例都会按这个顺序过一遍步骤操作常见卡点1解压并读 READMEREADME 版本和代码不匹配2检查工具箱缺少某个函数导致Undefined function3检查.mat变量变量名冲突、单位错误4运行主脚本到第一个断点语法错误、路径不对5显示中间光场光场全 0 或全 NaN3.3 以夫琅禾费衍射为例跑通核心仿真多数案例里都有一段夫琅禾费衍射或菲涅尔衍射的代码因为这是光学仿真的基石。我通常这么写lambda 632.8e-9; L 5e-3; N 1024; dx L / N; x -L/2 : dx : L/2-dx; [X, Y] meshgrid(x, x); z 0.5; % 传播距离 0.5m a 1e-3; ap abs(X) a/2 abs(Y) a/2; Uin ap; k 2 * pi / lambda; fx X / (lambda * z); fy Y / (lambda * z); Ufar exp(1i*k*z) / (1i*lambda*z) * ... exp(1i*k/(2*z) * (X.^2 Y.^2)) .* ... fftshift(fft2(Uin)) * (dx^2);逻辑说明fft2计算离散傅里叶变换结果位于频域频率间隔是1/Lfx和fy是物理频域坐标通过X/(lambda*z)映射到观察面dx^2是面积微元用于离散积分。fftshift的作用是把零频分量从矩阵边缘移到中心否则看到的衍射图案会被切成四块。跑通后用imagesc(abs(Ufar).^2)能看到中心亮斑和周围的旁瓣条纹。这个代码段里最容易改错的是z的单位。如果z写成 500就变成 500 米而不是 0.5 米结果会从衍射图案变成极小一个光点。我在参数区会强制写单位注释并建议你也这么做。3.4 修改案例参数并验证输出主脚本跑通后第一步是“破坏性验证”故意修改一两个参数看结果是否合理变化。比如把波长从 632.8nm 改成 450nm衍射条纹间隔应该变密。我用批处理方式来做lambda_list [450e-9, 532e-9, 650e-9]; for i 1:numel(lambda_list) lambda lambda_list(i); run(main_simulation_script.m); saveas(gcf, sprintf(result_%dnm.png, round(lambda*1e9))); end这个循环有一个隐患如果主脚本内部重新定义了lambda外部赋值就失效了。为了减少这种覆盖我在复用案例时会把可变参数全部收敛到一个params结构体里然后通过函数参数传递而不是依赖全局变量或脚本内赋值。4. 光学案例中的评价函数与常见指标的MATLAB实现4.1 点列图与均方根半径光学设计案例中点列图是判断成像质量最直接的指标。假如已经有了每个光线在像面的落点坐标x_px、y_pxRMS 半径计算如下centroid_x mean(x_px); centroid_y mean(y_px); r sqrt((x_px - centroid_x).^2 (y_px - centroid_y).^2); rms_radius sqrt(mean(r.^2));RMS 半径越小说明光线聚焦越集中。很多案例代码里会直接用std()来求但前提是质心已经在坐标原点否则结果偏大。我的习惯是先计算质心再算 RMS这样和 Zemax 报告里的数值才能对上。4.2 MTF 计算与可视化调制传递函数MTF是光学系统分辨率的重要判据。常见做法是从点扩散函数psf出发做傅里叶变换otf fftshift(fft2(ifftshift(psf))); mtf abs(otf) / abs(otf(1)); mid floor(size(mtf, 1) / 2) 1; mtf_horiz mtf(mid, :);注意ifftshift和fftshift必须成对出现。psf通常是以中心为对称的直接fft2会得到一个相位偏移后的 OTF先用ifftshift把中心移到矩阵左上角再fft2之后用fftshift还原坐标。我第一次写这段时把顺序搞反了导致 MTF 曲线在高频段断崖式下跌。下面是一张光学案例里经常用到的指标速查表指标典型实现需要的工具箱RMS 半径手写均值计算无PSFfft2或卷积无MTFfft2 归一化无波前像差 RMSstd相位图统计工具箱光强分布abs(U).^2无这张表可以作为你检查案例代码时的大纲看看作者是否用了和预期一致的公式。4.3 参数扫描与结果归档的工程化写法光学案例最终都要回答一个问题某个参数变化时性能指标怎么变我会把所有结果存放在结构体数组里results struct(z, {}, rms, {}, mtf50, {}); z_list 0.45:0.01:0.55; for zi z_list Uimg my_propagate(Uin, zi); rms_val compute_rms(Uimg); mtf50 compute_mtf50(Uimg); results(end1) struct(z, zi, rms, rms_val, mtf50, mtf50); end之后画图只需arrayfun或plot([results.z], [results.rms])。这种写法避免了每次循环只保留最后一个结果的尴尬也让后续导出 CSV 或生成报告变得容易。4.4 用并行计算加速批量仿真当参数扫描达到上千组时for循环会明显变慢。如果机器有多核Parallel Computing Toolbox 里的parfor是最直接的优化z_list 0.45:0.001:0.55; rms_vals zeros(size(z_list)); parfor i 1:numel(z_list) rms_vals(i) compute_rms(my_propagate(Uin, z_list(i))); end这里要求my_propagate和compute_rms都是纯函数不共享变量。另外Uin作为大型矩阵会在每个 worker 里复制一份内存占用是原来的 worker 数倍。遇到内存不足时报错我会把Uin拆块或者改用parfeval按需提交任务。5. 把案例改造成自己的工具参数化、界面与验证技巧5.1 参数化封装让主脚本变成可调用函数从案例里学到核心算法后我非常建议立刻做一次“去脚本化”改造把主脚本变成一个函数所有输入都收进结构体function result my_diffraction_sim(params) lambda params.lambda; z params.z; N params.N; ... result.U Ufar; result.mtf mtf; end这样做的好处是后期可以直接对接 Optimization Toolbox把params作为优化变量自动找最佳离焦量或最佳波长组合。改造时注意把figure、disp等副作用从函数里拆出去否则每次优化迭代都会弹出图像窗口拖慢循环。5.2 用App Designer搭一个光学参数调节面板如果需要给团队或实验人员使用我会用 App Designer 做一个简单界面。不必做复杂布局一个波长滑块、一个传播距离输入框、一个坐标轴控件就够了。回调里读取滑块值更新params.lambda然后调用my_diffraction_sim最后把abs(Ufar).^2画到坐标轴上。核心逻辑大概十几行关键点是确保每次回调都从app对象读取最新参数而不是从工作区抓残留变量。5.3 验证结果可靠性的三个技巧最后说三个我跑光学案例时必做的验证它们能帮你确认不是“跑起来”而是“跑对了”。第一能量守恒检查。入射光场和出射光场的总能量应保持恒定偏差超过几个百分点就意味着采样或边界处理有问题energy_in sum(abs(Uin(:)).^2) * dx^2; energy_out sum(abs(Ufar(:)).^2) * dx^2; fprintf(能量误差: %.2f%%\n, abs(energy_out/energy_in - 1)*100);第二与理论解析解对比。以单缝夫琅禾费衍射为例暗纹位置满足sin(theta) m * lambda / a。拿代码输出结果的中心截面和这个公式对比坐标能对上的才说明 FFT 频域映射没问题。第三缩放一致性测试。把网格边长L放大一倍重新运行观察光场的空间分布宽度是否按预期缩小。如果结果毫无变化说明某些参数被硬编码了。把这三个验证放在validate_result.m文件里每次改参数后运行一遍能省下大量调试时间。本文还有配套的精品资源点击获取