☰
MATLAB光学仿真:从衍射积分到FFT的完整实现与避坑指南
2026/10/21 3:39:27 网站建设 项目流程

简介:《信息光学作业.zip》是一份面向信息光学课程学习者与MATLAB编程实践者的作业资源包,主要围绕傅里叶变换、图形用户界面(GUI)设计与衍射模拟等核心内容展开,适合需要将光学理论转化为可运行代码的读者参考。压缩包整体约22.71MB,内容以MATLAB脚本和交互界面代码为主,可帮助使用者快速理解空间域与频域之间的转换关系,并通过可视化界面调整参数、观察衍射图样变化。已有194人进行了浏览学习。通过学习包内代码,读者能够掌握fft2等函数在光学频谱分析中的应用思路,了解菲涅尔衍射与夫琅禾费衍射的数值求解方法,同时学习如何利用MATLAB构建直观的交互工具来展示光场分布。压缩包将理论知识与编程实践结合,既能巩固信息光学中的核心概念,也有助于提高MATLAB程序设计与算法实现的综合能力。

1. 这份“信息光学作业.zip”里藏着的是一条完整的MATLAB衍射仿真链路

在信息光学这门课上,解析公式大家背得很熟,真正动手写作业时却常常翻车:直接按积分公式循环求值慢得离谱,换成FFT又总在缩放、坐标、单位这些边角上栽跟头。这份MATLAB作业包,把一条“从复振幅构造,到FFT频谱转换,再到观察面强度输出”的完整链路拆成了可运行的独立函数:孔径生成、透镜相位、夫琅禾费与菲涅尔传播各占一个文件,参数在命令行里改,运行后直接出图。适合赶作业找参考的人,也适合想做光学仿真、打算在此基础上扩展空间滤波和全息仿真的开发者。它不是黑魔法,是一份能照着敲出结果的仿真底稿。

2. 项目结构与设计思路:为什么衍射计算没有直接堆积积分公式

2.1 压缩包里的角色分配:脚本、函数和报告怎么分工

写光学作业最容易犯的错,是建一堆脚本文件来回复制代码。这份作业用了最朴素的职责分离,目录结构大概是这个样子:

文件作用依赖
main_demo.m总入口脚本,负责设定波长、距离、孔径尺寸并调用传播函数出图全部函数
aperture.m生成矩形孔、圆孔、光栅、随机相位板的复振幅透过率无
lens_pass.m用二次相位因子模拟薄透镜对波前的作用无
fraunhofer_fft.m夫琅禾费衍射的单次FFT实现,输出观察面坐标与光强无
fresnel_1fft.m菲涅尔域单次FFT传播,输入面乘一个二次相位,输出面再乘一个无
report.pdf实验报告部分,交代物理模型和公式推导—

这种划分说明了两个习惯:一是物理量单位在参数传入时显式给出,函数内部不猜单位;二是传播函数只做矩阵运算、不做绘图,绘图统一留在脚本里。这样后面做二次开发时,可以只改调用逻辑,不动函数内部。

实际跑的时候,我建议先只跑main_demo.m,再把aperture.m换着参数调用,理解了每个函数之后再去碰传播函数。很多数学软件作业的通病是“一整块代码干所有事”,这份作业把“生成光场”和“计算衍射”分开,排查问题时定位会快很多。

2.2 从衍射积分到FFT:一次空间频率替换省掉了上万次循环

夫琅禾费衍射的标准形式是:设孔径处复振幅为U0(x0,y0),在距离z处的观察面上,光场近似为

U(x,y) ∝ ∫∫ U0(x0,y0) exp[−j2π(x·x0 + y·y0)/(λz)] dx0 dy0

这是一个傅里叶变换的形式,只是核里坐标换成了空间频率fx = x/(λz)、fy = y/(λz)。数值上不必真的叠加积分,直接用二维FFT做一次矩阵变换,得到的就是频域分布,再映射回观察面坐标。这就是作业里“单次FFT”一次运算出图的底层依据。

菲涅尔近似则比夫琅禾费多一项输入面的二次相位,对应程序里就是在FFT之前先对矩阵逐点乘一个exp[jk(x0²+y0²)/(2z)],输出面再乘一个同样的二次相位。这个设计让函数短小、职责清楚、便于测试。缺点也很明显——FFT隐含周期延拓假设,边界处理不好就会出现第5章要展开的混叠伪影。

2.3 参数链路:波长、尺寸、采样点怎么传进每一个函数

作业里有一个约定:三个物理量总是绑在一起传——波长lambda、孔径面物理尺寸L、离散点数N。观察距离z按传播函数单独传。

参数类型用途
lambda标量,单位m决定相位因子和坐标映射比例
L标量,单位m孔径平面总宽度,决定空间采样间距dx = L/N
N整数矩阵维数,直接决定运算量和频谱分辨率
z标量,单位m传播距离,决定观察面坐标范围

这里有个容易忽略的点:FFT输出频率轴的间距是1/L,而不是1/(N·dx)。两者数值上相等,但代码里写法完全不同。如果把fx误写成(-N/2:N/2-1)/(N*dx),在dx由L/N定义时数值恰好对,一旦在后面加了矩形窗、补零或图像裁剪,网格对不上就全错。所以函数内一律用L推导频域步长,这是值得保留的规范。

我一般在作业的函数头部注释里写明“输入单位都是国际单位制”,因为数学软件不检查单位,米和毫米混用产生的错误通常是几倍量级,肉眼很难看出来。用结构体统一管理参数会更优雅,但作业这种简单场景里,直接在main_demo.m里定义常量、用注释标单位,已经足够避免大部分问题。

3. 核心模块拆解:从入射光场到观察面光强的完整链路

3.1 构造复振幅:矩形孔、圆孔、光栅一张矩阵搞定

第一个要写的通常是生成孔径透过率的函数。作业里最常用的写法是先建立坐标网格,再用逻辑表达式生成透过率矩阵。注意这里得到的是振幅透过率,还没带上相位,光线垂直入射时相位项为0。

function U0 = aperture_rect(N, L, a, b) % 矩形孔复振幅透过率 % N : 矩阵大小 % L : 孔径平面物理宽度(m) % a, b: 矩形孔径的宽度和高度(m) dx = L / N; x = (-N/2 : N/2-1) * dx; [X, Y] = meshgrid(x, x); U0 = double(abs(X) <= a/2 & abs(Y) <= b/2); end

meshgrid(x, x)生成 N×N 的坐标网格,abs(X) <= a/2这类逻辑判断直接得到0/1掩膜,double转成数值矩阵。如果要做光栅,就是在x方向加一条周期判断:

function U0 = aperture_grating(N, L, d, duty) % 一维矩形光栅:周期d,占空比duty dx = L / N; x = (-N/2 : N/2-1) * dx; [X, ~] = meshgrid(x, x); U0 = double(mod(X + d/2, d) < duty * d); end

mod(X + d/2, d)把坐标循环到半开区间,再和duty*d比较就得到周期性透过率。孔径生成的通用套路是:任何能从网格坐标算出来的形状都可以这么写,圆孔就是X.^2 + Y.^2 <= r^2处取1。这里有个细节值得留意:圆孔边缘是像素阶梯近似的,半径至少覆盖10个像素以上,否则FFT结果里会混入明显的矩形孔径衍射成分。

3.2 夫琅禾费衍射:一次FFT出图,但要盯住三步操作

function [x, y, I] = fraunhofer_fft(U0, lambda, z, L) % 夫琅禾费衍射(单次FFT) % U0 : 入射复振幅矩阵 % lambda: 波长(m) % z : 观察距离(m) % L : 孔径平面物理宽度(m) N = size(U0, 1); dx = L / N; % 空间采样间隔 fx = (-N/2 : N/2-1) / L; % 频域采样间隔取1/L [FX, FY] = meshgrid(fx, fx); Uf = fftshift(fft2(ifftshift(U0))) * dx^2; % ifftshift把原点挪到矩阵左上角,fft2算频谱,fftshift把原点挪回中心 % dx^2是连续傅里叶积分到离散求和的面积元缩放 xi = FX * lambda * z; % 频域坐标映射到观察面 eta = FY * lambda * z; I = abs(Uf).^2; % 光强 = 振幅模平方 [x, y] = meshgrid(xi, eta); end

这段代码有三个关键点。第一,ifftshift必须在fft2之前,否则矩阵原点不在 FFT 约定的 (1,1) 位置,等价于乘了一个线性相位,结果频谱里会出现规律性细纹。第二,xi = FX * lambda * z是把无单位的频域索引映射成有物理意义的观察面坐标,这一步写错、写漏都会让“条纹间距对不上”。第三,光强只取模平方,不关心绝对幅值,所以常数因子1/(λz)和球面相位因子可以省略,这在实际作业里足够用。

3.3 菲涅尔单次FFT:比夫琅禾费多了两个二次相位

function [x, y, U] = fresnel_1fft(U0, lambda, z, L) % 菲涅尔单次FFT传播 % U0 : 入射复振幅矩阵 % lambda: 波长(m) % z : 传播距离(m) % L : 输入平面物理宽度(m) N = size(U0, 1); dx = L / N; k = 2 * pi / lambda; x0 = (-N/2 : N/2-1) * dx; [X0, Y0] = meshgrid(x0, x0); % 第一步:输入面上叠加二次相位 U1 = U0 .* exp(1j * k / (2 * z) * (X0.^2 + Y0.^2)); fx = (-N/2 : N/2-1) / L; [FX, FY] = meshgrid(fx, fx); % 第二步:傅里叶变换,并乘面积元dx^2 U2 = fftshift(fft2(ifftshift(U1))) * dx^2; % 第三步:输出面坐标与二次相位 xi = FX * lambda * z; eta = FY * lambda * z; [X, Y] = meshgrid(xi, eta); U = exp(1j * k * z) / (1j * lambda * z) ... .* exp(1j * k / (2 * z) * (X.^2 + Y.^2)) .* U2; end

菲涅尔传播里坐标映射的关键是:输出面的网格间距变为λz/L,也就是说传播距离越大,输出面的物理范围越宽,但采样点总数不变,等效于用越来越粗的网格去看越来越大的视野。所以菲涅尔单次FFT不适合短距离传播——z太小时输出网格密集、视野窄,容易截断信号。这个问题我一般轻描淡写带过,但真正调参时它比公式推导更常坑人,第5章会专门展开。

3.4 透镜相位与4f系统:把焦距写进脉冲响应

透镜本身不改变振幅,只改变相位。薄透镜近似的相位变换写成:

function U_out = lens_pass(U_in, f, lambda, L) % 薄透镜相位变换 % f: 透镜焦距(m),汇聚透镜取正 N = size(U_in, 1); dx = L / N; x = (-N/2 : N/2-1) * dx; [X, Y] = meshgrid(x, x); k = 2 * pi / lambda; t = exp(-1j * k / (2 * f) * (X.^2 + Y.^2)); % 负号对应汇聚透镜,波前向中心收缩 U_out = U_in .* t; end

这里符号约定要统一:对汇聚透镜取负号,对发散透镜取正号,不同教科书可能相反,但只要聚焦模拟里能观察到能量在焦点汇聚,就说明符号自洽。有了透镜函数,可以拼出最基本的4f成像系统:

% 输入面 -> 透镜1 -> 频谱面 -> 透镜2 -> 输出面 U1 = lens_pass(U0, f, lambda, L); [~, ~, U_spectrum] = fresnel_1fft(U1, lambda, f, L); U2 = lens_pass(U_spectrum, f, lambda, L); [~, ~, U_out] = fresnel_1fft(U2, lambda, f, L);

4f系统的价值在于频谱面可以直接做空间滤波——在U_spectrum上乘一个圆孔掩膜,再完成后半段传播,就可以直观演示阿贝-波特实验:挡住高频分量,输出面只剩低频轮廓;挡住低频,边缘反而增强。这个实验在作业报告里是加分项,代码量却只有几行,很适合二次开发时扩展。

4. 跑通与验证:从MATLAB命令行到物理可读的仿真结果

4.1 最小可运行Demo:先拿圆孔看一张图

拿到压缩包后,不需要改动任何函数,直接新建一个脚本,把参数和调用串起来,保存成run_demo.m放到同一个目录下执行。

% run_demo.m clear; close all; lambda = 632.8e-9; % He-Ne激光波长 632.8nm L = 4e-3; % 孔径平面宽度 4mm N = 2048; % 矩阵大小,FFT用2的幂最快 z = 1.0; % 观察距离 1m r = 0.5e-3; % 圆孔半径 0.5mm x = (-N/2 : N/2-1) * L/N; [X, Y] = meshgrid(x, x); U0 = double(X.^2 + Y.^2 <= r^2); [xo, yo, I] = fraunhofer_fft(U0, lambda, z, L); imagesc(xo(1,:)*1e3, yo(:,1)*1e3, I); axis image; colormap hot; xlabel('x (mm)'); ylabel('y (mm)'); title('圆孔夫琅禾费衍射');

运行后应当看到典型的艾里斑图样:中心亮斑,周围一圈圈暗环,强度从中心向外单调衰减,且暗环之间没有多余的直线条纹。N=2048 时一次FFT在普通笔记本上也就是几十毫秒,肉眼感知不到等待时间。如果图谱里出现规则细网纹或者边缘振铃,先回去检查ifftshift是否写成了fftshift,这是最常见的翻车点。

4.2 用解析解验证数值解:艾里斑和sinc函数怎么对

仿真结果能不能自证正确,不能只靠“看着像”。作业里推荐的对拍方法是拿圆孔的艾里斑解析公式做对照。圆孔直径为D时,第一个暗环的角半径为θ₁ ≈ 1.22λ/D,在距离z的观察面上对应半径r₁ = z·θ₁。取D = 1mm、λ = 632.8nm、z = 1m,算出r₁ ≈ 0.772mm,然后从模拟结果矩阵里找第一个径向极小值的位置。

验证对象解析公式模拟结果应有的特征
圆孔夫琅禾费衍射第一暗环半径1.22 λ z / D中心亮斑外围出现同心暗环,首个暗环半径与公式偏差小于5%
矩孔夫琅禾费衍射强度sinc²(a·x/(λz))·sinc²(b·y/(λz))x方向过零点间隔为λz/a,y方向间隔为λz/b
菲涅尔短距离传播光场能量守恒输出面总能量与输入面总能量差小于1%

检查方法不复杂:在模拟得到的I矩阵里,以矩阵中心为圆心,沿径向取一条强度剖面,找第一个极小值点对应的像素索引,再换算成物理坐标。如果这个值和解析值对不上,问题基本出在频域坐标fx的写法上,而不是FFT本身。

4.3 调参的物理含义:改波长、距离、孔径时先看哪个量

改动参数计算结果的直观变化需要重点检查的项
lambda增大衍射条纹整体展宽,艾里斑半径变大相位因子k = 2π/λ变化,菲涅尔近似是否仍成立
z增大观察面坐标范围变宽,条纹更宽菲涅尔单次FFT输出网格变粗,是否出现欠采样
L增大频域间隔1/L变小,观察面网格更细孔径在矩阵中所占像素比例变小,边缘阶梯化
N增大采样更密,计算变慢,物理结果不变是否引入纯数值混叠,内存占用是否成倍增长

调参的血泪经验是:一次只改一个参数。很多人上来把lambda、z、L全改了,图样变了也说不清是哪个变量导致的。更合理的流程是固定lambda和L,先调z观察条纹展宽是否满足规律,再固定观察距离改孔径形状,逐步建立对模拟代码的信任。

5. 避坑与常见问题排查:FFT衍射模拟的踩坑记录

5.1 现象:衍射图样边缘出现规则细网纹

出图后亮斑周围裹着一层很细的网格状纹理,像打印机的网点。原因是对中心对称的输入矩阵直接调用了fft2,FFT默认把矩阵左上角当原点,矩阵本身的“原点在中心”信息被当成线性相位混进了频谱。解决方式统一为:先ifftshift把原点挪到左上角,FFT 后再fftshift挪回中心。注意前后两个函数不能写反,写反的结果是频谱不仅不回归中心,还会多出一半镜像。

5.2 现象:圆孔暗环半径和解析公式差了几倍

用艾里斑公式验证时,第一暗环的位置总是偏大或偏小。原因多半是观察面坐标xi = FX * lambda * z里的FX网格写错了,例如直接在fftshift(fft2(...))结果上套(-N/2:N/2-1)/(N*dx),当网格被补零或者改变尺寸时就和L脱钩了。解决:频域坐标一律用(-N/2:N/2-1)/L建立,坐标映射只认物理宽度,不认矩阵尺寸。这一步看着小,却是“数值结果没法用”的最大来源。

5.3 现象:菲涅尔输出全白或整片NaN

调用fresnel_1fft时把z设成了极小值,比如 1e-6,结果输出图要么全白,要么整片异常值。原因是二次相位exp(jk/(2z)(x²+y²))里的z在分母上,传播距离过小时相位振荡频率极高,超过了采样间隔能表达的范围,等价于频谱混叠。解决:先用夫琅禾费近似估算最小可用距离,一般让z至少比孔径宽度大两个数量级;如果必须算短距离,改用角谱传播法。

5.4 现象:只改N,结果和之前完全不同

明明物理参数没变,把N从512改成2048后条纹间距、图样形态都变了。这里有两个坑。第一,N变了意味着dx = L/N变了,孔径在像素格点上的阶梯逼近不同,圆孔边缘会略微变化,从而影响高频分量,这个误差理论上随N增大收敛。第二,N变大后频率上限1/(2dx)提高,原本被截断的高频混叠显形了。解决:确认圆孔半径至少占10个像素,并检查采样定理条件dx <= λz / (2 * 孔径宽度)。如果仍然对不上,把两次结果保存成图对比,而不是口算。

6. 二次开发:把一次作业升成可复用的光学仿真小工具

6.1 用结构体统一参数入口

作业版代码靠形参传参,函数一多就容易串位。我的改造习惯是先定义一个参数结构体,然后写一个薄封装函数:

opt = struct('lambda', 632.8e-9, 'L', 4e-3, 'N', 2048, ... 'z', 1.0, 'aperture', 'circ', 'radius', 0.5e-3); function I = run_experiment(opt) U0 = make_aperture(opt); % 按opt.aperture分派 [xo, yo, I] = fraunhofer_fft(U0, opt.lambda, opt.z, opt.L); [xo, yo, I] = normalize_intensity(xo, yo, I); end

这样一来,所有参数集中在一个定义处,后续做参数扫描时可以只改opt.radius,不用重写函数调用。语义上也比五个形参排在一起好读得多。

6.2 批量扫描与自动出图

二次开发时最常用的操作就是扫参。比如想观察不同圆孔半径对艾里斑尺寸的影响,可以写一个循环,把结果自动归档成带物理意义的文件名:

for r = 0.2e-3 : 0.1e-3 : 1.0e-3 opt.radius = r; I = run_experiment(opt); imwrite(mat2gray(I), sprintf('diffraction_r_%.0fum.png', r*1e6)); end

这种做法比手动改参数再截图高效得多,也方便在报告里排版对比。批量跑完后,可以写一个validate_airy脚本,自动计算第一暗环半径并与解析式比较,把“验证”沉淀成可回归的脚本。

6.3 把验证写进脚本,而不是靠肉眼

从那以后我每次改完仿真代码,都强制走一遍“解析解对拍 + 批量扫一个关键参数 + 能量守恒检查”这三步,哪怕只是改了一个坐标写法也会跑完再继续。不是为了显摆严谨,是因为光学仿真数值结果“看着正常”和“真正常”之间的距离远比想象的大。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询