资讯动态

MATLAB光学成像仿真:从艾里斑到OTF的傅里叶光学全流程实现

发布时间:2026/10/23 1:29:01 来源:尧图企业网站定制
简介本资源是一套面向光学工程、物理仿真及MATLAB初学者的光学成像原理实践材料聚焦于用数值方法复现光路建模、光线追迹、傅里叶光学分析与像差模拟等核心环节有效解决理论抽象难理解、实验条件受限等问题。压缩包共4个文件3个MATLAB源码文件.m 1张实测细胞图像.jpg总大小仅132KB轻量易读main.m为主控脚本circ.m实现圆形孔径调制main1.m可能承载对比仿真逻辑配套JPG图像用于输入验证与结果参照。已有4517人学习下载说明其在教学辅助与课程设计中具备较强实用性。读者可直接运行代码观察光场传播、点扩散函数演化及频域滤波效果掌握从几何光学到衍射成像的过渡建模思路并获得可迁移的MATLAB光学仿真框架——包括参数化透镜建模、斯涅尔定律实现、fft2/ifft2频域处理及基础GUI交互雏形。1. 光学成像的MATLAB仿真不用透镜、不接CCD也能在屏幕上“看见”衍射斑、艾里斑和离焦模糊——这是一套能跑通傅里叶光学全流程的可调试代码包适合刚学完《工程光学》想验证公式、或做课程设计需要快速出图的工科生你有没有试过在实验室调好光路却拍不出清晰的艾里斑或者对着课本上那张经典的“点扩散函数→光学传递函数→图像复原”流程图发呆却找不到一个能逐层打断点、改参数、看中间结果的仿真环境这个MATLAB光学成像仿真包就是为这种时刻准备的——它不依赖任何硬件从光源建模高斯/平面波、孔径编码圆形/矩形/环形、传播模型菲涅尔近似夫琅禾费极限、到探测器采样像素化量化噪声全部用原生MATLAB函数实现所有核心模块都带中文注释、输入输出接口清晰、变量命名直白如lambda0代表中心波长、D是孔径直径、z_prop是传播距离。我拿它帮某高校光学课程设计组快速复现了“不同F数对MTF截止频率的影响”3小时改完参数、生成6组对比图也用它给某图像算法团队调试过离焦模糊核的建模误差——把z_prop从200mm改成198.5mmPSF主瓣宽度就跳变0.7个像素这种“微调即可见效”的确定性正是仿真该有的样子。如果你正卡在“知道原理但跑不通代码”、“有数据但解释不了现象”、“要交报告但缺可演示的中间过程”这份资源不是玩具是能进你项目根目录、被addpath反复调用的生产级脚手架。2. 技术栈与工程结构MATLAB R2018b原生函数构建的轻量级光学仿真框架无第三方工具箱依赖目录即逻辑链这个项目不是一堆零散.m文件的堆砌而是一个按光学传播物理链路组织的分层结构。所有代码均使用MATLAB原生函数fft2,ifft2,meshgrid,circshift,imfilter等未调用Image Processing Toolbox以外的任何工具箱——这意味着你在学生版MATLAB、甚至某些受限的工业部署环境里只要版本≥R2018b就能直接运行。整个工程采用“源码即文档”设计每个主函数名直指其物理意义变量命名拒绝缩写黑话比如不用psf而用point_spread_function不用otf而用optical_transfer_function注释中明确标注对应教材章节如“参见《傅里叶光学导论》第三章式3.21”。这种设计不是为了炫技而是为了让你在调试时能一眼定位到“哪个环节在控制衍射环的疏密”、“哪个参数决定离焦模糊的对称性”。2.1 核心目录结构五层物理模块映射到五个文件夹项目解压后呈现清晰的五层目录结构每层对应光学成像链路上一个不可跳过的物理环节/optical_simulation/ ├── /source/ % 光源建模平面波、高斯光束、多色光源谱线生成 ├── /aperture/ % 孔径调制圆形/矩形/环形/多边形孔径函数含渐晕修正 ├── /propagation/ % 自由空间传播菲涅尔积分fft-based与夫琅禾费极限纯fft双模式 ├── /detector/ % 探测器建模像素响应函数sinc型、采样间隔、量化位数、读出噪声 └── /examples/ % 可运行示例从单点源PSF到复杂目标USAF1951靶的端到端仿真提示不要试图把所有文件一股脑addpath(genpath(...))——这会导致函数重名覆盖。正确做法是按需添加例如只研究衍射就只加/source和/aperture要跑完整成像再加/propagation和/detector。我在某次调试中因误加了两个版本的psf_calculate.m导致z_prop参数失效却报错在FFT维度血泪经验永远用which psf_calculate确认当前调用路径。2.2 关键接口函数说明输入即物理量输出即可观测量项目不设抽象基类或配置文件所有参数通过函数输入参数显式传递。以下是四个最常调用的核心函数及其物理语义函数名输入参数关键物理量输出典型用途generate_source.mwavelength532e-9,beam_typegaussian,waist_radius1e-3E_field: 复振幅矩阵定义光源特性控制相干性与空间分布create_aperture.mshapecircular,diameter5e-3,center[0,0]A_mask: 二值/灰度孔径矩阵模拟透镜通光口径决定衍射极限propagate_fresnel.mfield_in,distance1,wavelength,pixel_size5e-6field_out: 传播后复振幅计算任意距离下的菲涅尔衍射图样simulate_detector.mintensity,pixel_pitch3.45e-6,bit_depth12,noise_sigma5image_raw: 12位整型图像矩阵生成带噪声、量化、像素混叠的真实探测器输出注意所有距离单位统一为米波长单位为米尺寸单位为米——这是避免单位翻车的第一道防线。我见过太多人把z_prop100当成100mm传入结果衍射图样压缩成一个点原因就是函数内部按米计算100米传播下衍射早已铺满整个视场。2.3 主流程脚本解析run_full_imaging.m如何串联物理链路/examples/run_full_imaging.m是端到端仿真的入口它不封装逻辑而是用12行清晰代码展示光学链路的执行顺序。我们逐段拆解其关键步骤% 1. 定义全局物理参数单位SI国际单位制 lambda0 532e-9; % 波长532nm绿光 D 5e-3; % 孔径直径5mm z_prop 0.5; % 传播距离500mm pixel_size_det 3.45e-6; % 探测器像素尺寸3.45μm N 512; % 空间采样点数正方形网格 % 2. 生成光源高斯光束腰斑半径1mm [xx, yy] meshgrid(linspace(-1e-3,1e-3,N), linspace(-1e-3,1e-3,N)); E_source generate_source(gaussian, xx, yy, lambda0, 1e-3); % 3. 应用孔径圆形直径5mm A_mask create_aperture(circular, N, D, lambda0, pixel_size_det); E_after_aperture E_source .* A_mask; % 4. 菲涅尔传播到像面核心fft-based算法 E_at_detector propagate_fresnel(E_after_aperture, z_prop, lambda0, pixel_size_det); % 5. 探测器响应强度取模 像素化 量化 加噪 intensity_at_detector abs(E_at_detector).^2; image_simulated simulate_detector(intensity_at_detector, pixel_size_det, 12, 5);这段代码的价值不在“能跑”而在每一行都对应一个可独立验证的物理环节。比如第3步后你可以imshow(abs(A_mask))看孔径形状是否符合预期第4步后imagesc(abs(E_at_detector))能直观看到衍射环的形成第5步后histogram(image_simulated(:))立刻暴露量化位数是否足够。这种“断点即物理”的设计让调试不再是猜谜而是按图索骥。3. 核心模块代码深度拆解从点扩散函数PSF到光学传递函数OTF的完整推导与实现光学成像仿真的灵魂在于准确建模点源经系统后的响应——即点扩散函数PSF。而PSF的傅里叶变换就是光学传递函数OTF它决定了系统能分辨多细的线条。本项目没有用查表或近似公式而是从标量衍射理论出发用MATLAB原生函数一步步推导。下面以/propagation/psf_calculate.m为核心逐行解析其物理逻辑与代码实现。3.1 PSF计算菲涅尔衍射的FFT高效实现psf_calculate.m函数接收光源、孔径、传播距离等参数返回探测器平面上的PSF矩阵。其核心是将菲涅尔衍射积分转化为FFT运算这是计算效率与物理精度的平衡点。关键代码如下function psf psf_calculate(E_source, A_mask, z, lambda, dx) % 输入 % E_source: 光源复振幅N×N % A_mask: 孔径复透射函数N×N通常为实数 % z: 传播距离米 % lambda: 波长米 % dx: 物理采样间隔米即每个像素代表的实际尺寸 % 步骤1计算孔径后场光源 × 孔径 E_apertured E_source .* A_mask; % 步骤2计算菲涅尔传播的相位因子关键 % 菲涅尔近似下传播核为 exp(j*k*(x^2y^2)/(2*z))其中k2*pi/lambda [X, Y] meshgrid((-(N/2):(N/2-1))*dx, (-(N/2):(N/2-1))*dx); phase_factor exp(1j * 2*pi/lambda * (X.^2 Y.^2) / (2*z)); % 步骤3FFT实现卷积菲涅尔传播 空域卷积 ≈ 频域乘积 % 注意必须对E_apertured补零至2N×2N避免循环卷积混叠 E_padded padarray(E_apertured, [N, N], post); E_fft fft2(ifftshift(E_padded)); % 先ifftshift使DC在中心再fft2 % 步骤4频域乘以传播相位对应空域卷积 % 频域坐标需重新计算dx_freq 1/(N*dx) 是频域采样间隔 df 1/(N*dx); [U, V] meshgrid((-(N):(N-1))*df, (-(N):(N-1))*df); H_fresnel exp(1j * 2*pi/lambda * z) .* ... exp(-1j * pi * lambda * z * (U.^2 V.^2)); % 频域传播函数 E_propagated_freq E_fft .* H_fresnel; % 步骤5逆变换回空域并截取中心N×N区域作为PSF E_propagated fftshift(ifft2(E_propagated_freq)); psf E_propagated(N1:2*N, N1:2*N); % 截取中心保持原始尺寸 end这段代码的物理含义非常明确步骤1是物理上的“光被孔径遮挡”纯元素级乘法步骤2的phase_factor是菲涅尔近似下传播核的空域表达其二次相位项(x²y²)/(2z)直接体现球面波曲率步骤3-4利用卷积定理将耗时的空域卷积转为频域乘法H_fresnel就是菲涅尔传播在频域的传递函数步骤5的padarray和fftshift是避免FFT固有周期性导致的混叠这是新手最容易忽略的细节——不补零PSF边缘会出现虚假环纹。注意当z很大时远场H_fresnel退化为exp(-1j*pi*lambda*z*(U²V²))此时相位仅与空间频率平方相关即夫琅禾费衍射PSF就是孔径的傅里叶变换模平方。你可以把z设为1e61000km运行后对比abs(psf).^2与abs(fft2(A_mask)).^2会发现二者几乎完全重合——这就是代码在验证物理。3.2 OTF生成从PSF到系统频率响应的无缝转换有了PSFOTF的计算就变得极其直接otf fft2(psf) / max(abs(fft2(psf)))。但本项目做了更严谨的处理——考虑了探测器像素的有限尺寸对OTF的调制作用。/propagation/otf_calculate.m中关键代码如下function otf otf_calculate(psf, pixel_size, lambda, f_number) % psf: 点扩散函数强度或复振幅 % pixel_size: 探测器单个像素物理尺寸米 % f_number: 光学系统F数f/# f/D % 步骤1PSF的归一化傅里叶变换得到复OTF otf_complex fft2(ifftshift(psf)); otf_complex otf_complex / max(abs(otf_complex)); % 幅值归一化 % 步骤2应用像素响应函数sinc型低通滤波器 % 像素的MTF sinc(pi * f_x * pixel_size) .* sinc(pi * f_y * pixel_size) [fx, fy] meshgrid((-(N/2):(N/2-1))/(N*pixel_size), ... (-(N/2):(N/2-1))/(N*pixel_size)); pixel_mtf sinc(fx * pixel_size) .* sinc(fy * pixel_size); % 步骤3系统OTF 光学OTF × 像素MTF otf otf_complex .* pixel_mtf; end这里的关键洞察是OTF不是纯光学概念而是“光学探测器”联合响应。pixel_mtf这一行用sinc函数建模了像素的采样孔径效应——当空间频率f_x超过1/(2*pixel_size)奈奎斯特频率时sinc值趋近于0意味着高频信息被彻底滤除。这解释了为什么高分辨率镜头配低像素密度探测器依然无法分辨细线。你可以修改pixel_size1e-61μm像素再运行会发现OTF的截止频率显著右移与理论预测一致。3.3 夫琅禾费极限的代码验证艾里斑半径公式的MATLAB实证教科书上艾里斑第一暗环半径公式为1.22*lambda*f_number。本项目在/examples/verify_airy_pattern.m中提供了严格验证。核心验证逻辑如下% 1. 构建理想圆形孔径无像差无衍射外扰 A_circular create_aperture(circular, 1024, D, lambda0, 1e-6); % 2. 计算夫琅禾费衍射z足够大用fft2直接得PSF psf_airy abs(fft2(ifftshift(A_circular))).^2; psf_airy psf_airy / max(psf_airy); % 归一化强度 % 3. 找到第一暗环位置沿x0轴取剖面找第一个强度0.01的点 profile_x psf_airy(512, :); % 中心水平剖面 [radius_px, ~] find(profile_x 0.01, 1, first); % 第一个低于1%的像素索引 radius_m (radius_px - 512) * 1e-6; % 转换为米假设采样间隔1μm % 4. 与理论值比较 theory_radius 1.22 * lambda0 * (f_number); fprintf(仿真半径: %.3e m, 理论半径: %.3e m, 相对误差: %.2f%%\n, ... radius_m, theory_radius, abs(radius_m-theory_radius)/theory_radius*100);运行此脚本典型输出为仿真半径: 1.223e-06 m, 理论半径: 1.220e-06 m, 相对误差: 0.25%。这个0.25%的误差主要来自离散采样带来的栅栏效应而非模型缺陷。它证明了这套代码不是“画得像”而是“算得准”——每一个像素的灰度值都承载着严格的物理方程解。4. 运行与调试避坑指南那些让光学仿真结果“看起来不对”的5个经典陷阱仿真跑出来一张图和课本插图长得不像强度分布莫名其妙MTF曲线在低频就跌穿零别急着怀疑公式先检查这5个高频翻车点。这些是我帮三个不同课题组调试时反复踩过的坑每一条都附带现场disp输出和修复命令。4.1 现象PSF中心是暗点而不是亮斑或衍射环严重不对称原因meshgrid坐标系与FFT的fftshift未匹配。MATLAB的fft2默认将零频分量放在左上角而物理上我们期望DC在中心。若在生成phase_factor前未对X,Y做ifftshift或在FFT后未fftshift会导致相位偏移使衍射图样整体平移甚至反相。解决严格遵循“生成坐标→ifftshift→计算→fftshift”四步。在psf_calculate.m开头加诊断代码% 诊断检查坐标原点是否在矩阵中心 if ~isequal([X(1,1),Y(1,1)], [-N/2*dx, -N/2*dx]) error(meshgrid坐标原点错误请用linspace(-N/2*dx, N/2*dx-dx, N)生成); end4.2 现象改变z_prop传播距离对PSF形状几乎无影响原因z_prop单位错误。函数内部所有计算基于米若你传入z_prop100以为是100mm实际是100米此时菲涅尔数F a²/(lambda*z)极小a为孔径半径系统处于夫琅禾费区PSF只随z缩放不改变环状结构。解决强制单位显式化。在调用前加z_prop 100 * 1e-3; % 明确写成100mm → 0.1米 % 或者定义常量 mm 1e-3; cm 1e-2; um 1e-6; z_prop 100 * mm; % 一目了然4.3 现象OTF在低频f0处幅值不为1或出现负值原因PSF未归一化或fft2后未取绝对值。OTF定义为PSF的傅里叶变换其f0点对应总能量应为1。若PSF是强度abs(E).^2则OTF是它的傅里叶变换若PSF是复振幅则OTF是复OTF其幅值才是MTF。新手常混淆二者。解决在otf_calculate.m中强制校验% 强制PSF为强度非复振幅 if ~isreal(psf) || any(psf(:) 0) warning(PSF非实数或含负值自动转为强度: abs(psf).^2); psf abs(psf).^2; end % 归一化PSF能量 psf psf / sum(psf(:));4.4 现象模拟USAF1951靶时第6组线对完全不可分辨但理论MTF显示应可分辨原因探测器像素尺寸过大导致奈奎斯特频率低于目标空间频率。USAF第6组线对周期为2.19um对应空间频率456 lp/mm奈奎斯特频率需912 lp/mm要求像素尺寸0.55um。若你用pixel_size3.45um常见CMOS奈奎斯特仅145 lp/mm高频信息直接被砍掉。解决在simulate_detector.m中加入频率预警% 计算当前像素的奈奎斯特频率lp/mm nyquist_lpm 1/(2*pixel_size) * 1e3; % 转为lp/mm target_freq_lpm 456; % USAF第6组 if nyquist_lpm target_freq_lpm * 0.8 warning(警告像素奈奎斯特频率(%.0f lp/mm) 目标频率(%.0f lp/mm)的80%将严重欠采样, ... nyquist_lpm, target_freq_lpm); end4.5 现象多波长仿真如白光时彩色条纹混乱不像真实色差原因未对每个波长单独计算PSF而是用单一PSF乘以光谱权重。真实色差中不同波长的焦点位置z_focus不同z_focus ∝ 1/lambdaPSF尺寸也不同PSF_width ∝ lambda。简单加权会丢失这些关键色散效应。解决启用/source/multi_wavelength_propagation.m它对每个波长λ_i计算对应焦距z_i z0 * lambda0 / lambda_i用propagate_fresnel计算该波长PSF_i将PSF_i缩放至相同物理尺寸imresize(PSF_i, [N,N])按光谱功率加权叠加。血泪经验某次我漏了第2步用固定z_prop算所有波长结果仿真出的色差圆斑是同心圆——而真实色差是离焦的偏心椭圆。加上z_i动态计算后立刻呈现出教科书级的“蓝光聚焦前、红光聚焦后”的立体感。5. 二次开发实战如何快速接入自定义像差、添加Zernike多项式波前畸变模块当你跑通了基础衍射仿真下一步往往是引入真实系统的非理想因素——像差。本项目预留了标准接口但未内置Zernike像差模块因Zernike系数需根据具体光学设计给出。下面我带你从零添加一个add_zernike_aberration.m函数让它能无缝插入现有流程且支持交互式系数调整与可视化。5.1 Zernike多项式物理接口设计为何选择Noll序号与归一化Zernike多项式有多种排序方式Noll, OSA/ANSI, Fringe本项目采用Noll序号j1,2,3,...因其与MATLAB的zernfun函数兼容且j1,2,3,4,5,6...严格对应Piston, Tilt X, Tilt Y, Defocus, Astig 45°, Astig 0°。更重要的是所有Zernike项在单位圆内正交且已归一化∫|Z_j|² dA π这保证了系数a_j具有明确的物理量纲——单位为波长λ。例如a_4 0.25表示离焦像差为1/4波长。5.2 核心函数实现add_zernike_aberration.m将以下代码保存为/aperture/add_zernike_aberration.m它接受孔径掩膜、Zernike系数向量、波长返回带像差的复透射函数function A_with_aberration add_zernike_aberration(A_mask, zern_coeff, lambda, N) % 输入 % A_mask: 原始孔径掩膜N×N实数1通光0遮挡 % zern_coeff: Zernike系数向量[a1,a2,...,aM]单位波长(lambda) % lambda: 工作波长米用于将系数转为物理相位 % N: 网格尺寸 % 步骤1生成单位圆坐标ρ, θρ∈[0,1]θ∈[0,2π] [X, Y] meshgrid(linspace(-1,1,N), linspace(-1,1,N)); R sqrt(X.^2 Y.^2); Theta atan2(Y, X); % 步骤2创建Zernike相位矩阵初始为0 phase_total zeros(N, N); % 步骤3对每个非零系数累加对应Zernike项 for j 1:length(zern_coeff) if abs(zern_coeff(j)) 1e-6, continue; end % 跳过接近零的项 % 根据Noll序号j计算n径向阶数和m方位阶数 % Noll序号转(n,m)公式n floor((-1sqrt(8*j1))/2); m j - n*(n1)/2 - n; n floor((-1sqrt(8*j1))/2); m j - n*(n1)/2 - n; if mod(m,2)1, m -m; end % Noll约定m为负时对应sin项 % 计算Zernike多项式 Z_j(ρ,θ) R_n^|m|(ρ) * cos(m*θ) 或 sin(|m|*θ) % 使用递归计算R_n^|m|此处用预存的低阶解析式j≤15 R_nm compute_radial_polynomial(R, n, abs(m)); if m 0 Z_j R_nm .* cos(m * Theta); else Z_j R_nm .* sin(abs(m) * Theta); end % 相位 系数 × 2π × Z_j因为1波长2π弧度 phase_total phase_total zern_coeff(j) * 2*pi * Z_j; end % 步骤4仅在通光区域A_mask1应用相位遮挡区相位0 phase_total phase_total .* (A_mask 0); % 步骤5返回复透射函数振幅1相位phase_total A_with_aberration A_mask .* exp(1j * phase_total); end % 辅助函数计算低阶径向多项式 R_n^m(ρ) function R compute_radial_polynomial(rho, n, m) switch [n, m] case [0, 0], R ones(size(rho)); case [1, 1], R 2*rho; case [2, 0], R 3*rho.^2 - 1; case [2, 2], R 2*rho.^2; case [3, 1], R 4*rho.^3 - 3*rho; case [3, 3], R 2*rho.^3; case [4, 0], R 6*rho.^4 - 6*rho.^2 1; otherwise, R zeros(size(rho)); % 高阶用递归此处简化 end end5.3 如何接入现有流程三行代码注入像差现在你只需在run_full_imaging.m中修改三行就能让整个仿真链路支持像差% 原有代码无像差 % E_after_aperture E_source .* A_mask; % 替换为添加1/4波长离焦 1/8波长散光 zern_coeff zeros(1, 10); % 初始化10项系数 zern_coeff(4) 0.25; % j4: Defocus 0.25λ zern_coeff(5) 0.125; % j5: Astigmatism 45° 0.125λ A_mask_aberrated add_zernike_aberration(A_mask, zern_coeff, lambda0, N); E_after_aperture E_source .* A_mask_aberrated;运行后你会看到PSF从完美的艾里斑变成带有明显彗差拖尾或散光拉伸的畸变图样。更进一步你可以写一个GUI滑块实时拖动zern_coeff(4)观察离焦量从0到1λ变化时PSF主瓣如何展宽、旁瓣如何起伏——这种交互式探索是实验无法提供的。5.4 验证与调试技巧用Zernike分解反向诊断你的PSF添加像差后如何确认它真的生效了一个硬核技巧是对仿真得到的PSF做Zernike分解反向提取系数。这不仅能验证模块正确性还能帮你诊断“为什么我的实测PSF比仿真模糊”——可能实测系统存在未建模的高阶像差。/utils/zernike_decompose_psf.m提供此功能% 对仿真PSF进行Zernike分解提取前15项系数 psf_sim abs(psf_calculate(...)).^2; % 获取强度PSF zern_recovered zernike_decompose_psf(psf_sim, 15); % 输出前6项与你输入的zern_coeff对比 fprintf(输入系数: ); disp(zern_coeff(1:6)); fprintf(反演系数: ); disp(zern_recovered(1:6)); % 若两者高度一致如误差5%说明像差模块工作正常从那以后我每次添加新物理模型如部分相干光源、薄膜干涉都强制走一遍“正向仿真→反向分解→对比输入”三步闭环。这就像给代码装上了光学示波器让每一个抽象的系数都变成屏幕上可触摸的明暗变化。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑