☰
MATLAB光学衍射模拟:积分法与FFT法实现及参数调优
2026/10/28 14:35:04 网站建设 项目流程

简介:这份资源面向光学课程学习者、物理实验教学人员及需要做衍射仿真的理工科学生,用 MATLAB 编程复现光学衍射现象,解决实验条件受限、抽象公式难以直观理解的问题。内容以衍射积分与傅立叶变换两条技术路线展开:前者覆盖单缝衍射、杨氏双缝干涉与黑白光栅衍射,后者借助 fft2、fftshift 等函数模拟圆孔、矩孔、三角孔、单缝、黑白光栅及正弦光栅的夫琅和费衍射,并给出完整可运行代码与光强分布图。资源包共 1 个 doc 文档,约 247KB,集中呈现理论推导、程序清单与结果图示,便于对照学习。已有 792 人学习下载,适合作为课堂演示、课程设计或自学衍射光学的参考材料,读者可据此快速搭建仿真脚本、理解衍射图样随孔径与光栅参数变化的规律。

1. 光学衍射 MATLAB 模拟:两套方法把物理实验搬进命令行

做光学课程设计那会儿,实验室的导轨、狭缝片和光屏永远不够用,一组人调半天也未必能等到稳定的条纹。后来某导师丢过来一份 MATLAB 脚本,说“你先在电脑上把条纹跑出来,再去动真仪器”。这份资源就是干这个的:用衍射积分和傅立叶变换两种方法,把单缝衍射、杨氏双缝干涉、黑白光栅衍射、圆孔/矩孔/三角孔衍射、正弦光栅夫琅和费衍射全部用代码复现出来。它适合光学、物理电子、光电信息这类专业的本科生做仿真作业,也适合需要快速验证衍射图样规律的工程开发者。整套东西不依赖任何工具箱,纯 MATLAB 基础函数就能跑,拿到手改几个参数就能看到条纹怎么变。

2. 两套模拟路线:衍射积分与傅立叶变换的架构分工

这份资源的骨架其实很清晰,它没有做成一个大而全的 GUI 工程,而是按物理建模思路分成两条独立的技术路线,每条路线下挂若干具体实验场景。理解这个分工,后面改代码才不会乱。

2.1 衍射积分路线:从光程差累加到屏上光强

衍射积分方法的核心思想是把孔径离散成若干个相干点光源,对接收屏上每一个采样点,累加所有点光源的复振幅贡献,最后取模平方得到光强。这条路线的优点是物理图像直观,你能清楚看到“缝被切成 np 份”这件事;缺点是计算量随采样点数平方增长,参数开大了会明显变慢。

单缝衍射的脚本是这条路线里最基础的一个,先看它的完整结构:

clear lam = 500e-9; % 波长,500nm 绿光 a = 1e-3; % 缝宽 1mm D = 1; % 屏距 1m ym = 3*lam*D/a; % 观察屏半宽,取 3 倍中央条纹半宽 ny = 51; % 屏上采样点数 ys = linspace(-ym, ym, ny); np = 51; % 缝上离散点光源数 yp = linspace(0, a, np); for i = 1:ny sinphi = ys(i)/D; % 小角近似下的衍射角正弦 alpha = 2*pi*yp*sinphi/lam; % 各点光源的相位 sumcos = sum(cos(alpha)); sumsin = sum(sin(alpha)); B(i,:) = (sumcos^2 + sumsin^2)/np^2; % 归一化光强 end N = 255; Br = (B/max(B))*N; % 映射到 0~255 灰度 subplot(1,2,1) image(ym, ys, Br); colormap(gray(N)); subplot(1,2,2) plot(B, ys);

这段代码里几个参数值得说清楚。ym决定了你观察屏画多宽,取3*lam*D/a刚好能包住中央主极大和两侧第一级暗纹,改小了条纹会被裁掉。np是缝的离散精度,51 个点对单缝够用,但如果换成多缝光栅,缝数乘以每缝点数会让循环变重,这时候要么降np,要么把内层循环向量化。B(i,:)那个/np^2是归一化因子,保证中央光强接近 1,方便后面统一映射灰度。

杨氏双缝的脚本换了个思路,不再离散缝,而是直接算两束光到屏上每点的几何路程差:

clear lam = 500e-9; a = 2e-3; % 双缝间距 2mm D = 1; ym = 5*lam*D/a; % 双缝干涉条纹更密,观察范围取 5 倍 n = 101; ys = linspace(-ym, ym, n); for i = 1:n r1 = sqrt((ys(i)-a/2).^2 + D^2); % 到上缝距离 r2 = sqrt((ys(i)+a/2).^2 + D^2); % 到下缝距离 phi = 2*pi*(r2-r1)/lam; % 相位差 B(i,:) = sum(4*cos(phi/2).^2); % 双光束干涉光强 end N = 255; Br = (B/4.0)*N; subplot(1,2,1) image(ym, ys, Br); colormap(gray(N)); subplot(1,2,2) plot(B, ys);

这里a是缝间距而不是缝宽,ym的系数从 3 变成 5 是因为双缝条纹间距是lam*D/a,比单缝中央宽度小,观察范围要放大才能看到足够多级。B(i,:) = sum(...)里的sum其实对单点没意义,原代码保留它可能是为了和向量化写法兼容,实际跑的时候可以去掉,不影响结果。

光栅衍射是这条路线里公式最完整的一个,把单缝衍射因子和多缝干涉因子乘在一起:

clear lam = 500e-9; N = 2; % 缝数,改大能看到更细的干涉峰 a = 2e-4; % 缝宽 0.2mm D = 5; d = 5*a; % 光栅常数,5 倍缝宽 ym = 2*lam*D/a; n = 1001; ys = linspace(-ym, ym, n); for i = 1:n sinphi = ys(i)/D; alpha = pi*a*sinphi/lam; % 单缝衍射因子 beta = pi*d*sinphi/lam; % 多缝干涉因子 B(i,:) = (sin(alpha)./alpha).^2 .* (sin(N*beta)./sin(beta)).^2; B1 = B/max(B); end NC = 255; Br = (B/max(B))*NC; subplot(1,2,1) image(ym, ys, Br); colormap(gray(NC)); subplot(1,2,2) plot(B1, ys);

N是缝数,原代码给 2 其实更像双缝,改成 5 或 10 才能看到光栅特有的细密主极大。d = 5*a这个比例决定了缺级位置,当d/a为整数时,第d/a级主极大正好落在单缝衍射暗纹上,条纹会“缺”掉,这是光栅衍射最值得观察的现象。alpha和beta在sinphi=0处会出现 0/0,MATLAB 会给出 NaN,但image和plot对单个 NaN 不敏感,实际图上看不出来,严谨点可以加一句alpha(alpha==0)=eps。

2.2 傅立叶变换路线:把屏函数当信号做频谱分析

第二条路线完全换了个视角:夫琅和费衍射的强度分布等于孔径函数的功率谱,所以直接把孔径图像读进来做二维 FFT,取模、移频、显示,就得到衍射图样。这条路线的优势是能处理任意形状的孔径,圆孔、三角孔、字母图案都行,不用推导解析公式。

核心流程就四步,代码很短但每步都有讲究:

clear a = imread('aperture.bmp'); % 读入黑白孔径图,建议二值或灰度 figure(1) imshow(a, []) % 先看原图,确认孔径形状正确 afft = fft2(a); % 二维离散傅立叶变换 aabs = abs(afft); % 取模得到振幅谱 aabss = fftshift(aabs); % 直流分量移到中心 figure(2) imshow(aabss, []) colormap(gray); colorbar figure(3) plot(aabss) % 看中心截面的光强分布 figure(4) meshc(aabss) % 三维网格看频谱形态

imread读进来的矩阵如果是 RGB 三通道,fft2会报错,常见做法是先rgb2gray再二值化。fftshift这一步不能省,不做的话直流分量在四个角上,图看起来是“四角亮中间暗”,和实际衍射图样对不上。imshow(aabss, [])里的空方括号是自动缩放显示范围,不加的话高动态范围的频谱会显示成一片黑。meshc那一步是锦上添花,用来观察频谱的立体形态,正式出图可以不要。

正弦光栅的生成是这条路线的配套小工具,先造一张余弦灰度图存成 BMP,再喂给上面的 FFT 流程:

clear all xm = 10*pi; ys = xm; xs = linspace(-xm, xm, 500); B = cos(xs) + 1; % 余弦条纹,范围 0~2 N = 255; Br = B/2*N; % 映射到 0~255 image(xs, ys, Br); colormap(gray(N));

cos(xs)+1保证灰度非负,/2*N把范围压到 0~255。存图的时候注意用imwrite存成灰度 BMP,不要用截图,截图会引入抗锯齿和压缩噪声,FFT 出来的频谱会多出不该有的高频分量。正弦光栅的频谱理论上只有三个亮点(零级和正负一级),如果跑出来一堆杂散斑,八成是存图环节出了问题。

3. 从零跑通:环境准备、脚本组织与出图验证

拿到这份资源后,最省事的做法不是把代码一段段复制到命令窗口,而是按实验场景拆成独立脚本文件,每个文件对应一张图,跑完一个看一个。下面按实际操作的顺序讲。

3.1 环境与目录组织

MATLAB 版本没有硬性要求,R2014b 之后的都可以,因为用到的fft2、fftshift、imshow、meshc都是基础函数,不需要 Image Processing Toolbox 之外的额外工具箱。如果imshow报未定义,检查一下是不是装的精简版,补装 Image Processing Toolbox 即可。

目录建议这样组织,每个脚本独立可跑,互不干扰:

optical_diffraction/ ├── integral/ │ ├── single_slit.m │ ├── double_slit.m │ └── grating.m ├── fourier/ │ ├── aperture_fft.m │ └── sine_grating_gen.m └── images/ ├── circle.bmp ├── rectangle.bmp └── sine_grating.bmp

integral下放衍射积分的三个脚本,fourier下放 FFT 流程和光栅生成脚本,images放孔径素材。这样改参数的时候不会误伤别的实验,也方便对比两种方法对同一个物理场景的结果。

3.2 逐脚本运行与参数调整

先跑single_slit.m,确认能出图。正常情况下左边是灰度条纹图,中间亮两边暗,右边是光强曲线,中央主极大占大部分能量。如果图是纯黑或纯白,检查Br的归一化,B/max(B)在B全零时会出 NaN,说明循环里alpha算错了,回头查lam、a、D的单位是不是统一成了米。

再跑double_slit.m,应该看到等间距的明暗条纹,间距约lam*D/a。把a从2e-3改成1e-3,条纹间距应该翻倍,这是验证代码正确性最快的方法。如果改了a条纹没变,说明你改的是别处的同名变量,MATLAB 工作区里残留了旧值,养成每个脚本开头clear的习惯就是为了防这个。

grating.m跑之前先把N从 2 改成 5,否则看到的是双缝而不是光栅。改完应该能看到主极大变细、主极大之间出现次级亮纹。再把d从5*a改成4*a,缺级位置会从第 5 级移到第 4 级,这是光栅衍射最直观的验证点。

FFT 路线先跑sine_grating_gen.m生成sine_grating.bmp,存到images目录,然后把aperture_fft.m里的imread路径改成这张图。跑完看figure(2),应该只有中心一个亮斑加左右各一个对称亮斑,共三个。如果看到一条亮线或者一片噪点,回到存图那步查灰度映射。

3.3 出图与结果对照

两种方法对同一个物理场景的结果可以互相印证。比如单缝衍射,积分法给出的是sinc^2形状的光强曲线,FFT 法对一张矩形孔径图做变换,取中心截面也应该得到sinc^2。把两条曲线画在同一张图上对比,如果峰位和零点位置对得上,说明两套代码都写对了。

% 对比积分法与 FFT 法的单缝光强截面 % B_integral 来自 single_slit.m 的 B % B_fft 来自 aperture_fft.m 中心行取模 figure plot(ys/max(ys), B_integral/max(B_integral), 'b-', ... linspace(-1,1,length(B_fft)), B_fft/max(B_fft), 'r--') legend('积分法','FFT法') xlabel('归一化屏坐标') ylabel('归一化光强')

横坐标做了归一化处理,因为两种方法的采样网格不一样,直接画会在不同尺度上。纵坐标也归一化到峰值 1,方便比较形状。如果两条曲线在主极大宽度上差很多,检查 FFT 法里孔径图的物理尺寸和积分法里的a是否对应,FFT 的频谱尺度依赖像素数和像素物理尺寸,这一点原代码没有显式处理,二次开发时需要补上。

提示:跑 FFT 路线时,孔径图的分辨率不要低于 256×256,太小的图频谱分辨率不够,条纹会糊成一片。

4. 核心代码拆解:参数怎么改、现象怎么读

这一章把几个关键脚本的参数影响讲透,方便你拿到手之后按自己的实验需求调整,而不是只会跑默认值。

4.1 波长与缝宽对条纹宽度的影响

单缝衍射中央主极大的半角宽度约lam/a,屏上宽度约2*lam*D/a。这三个参数里,lam改大条纹变宽,a改大条纹变窄,D改大条纹变宽。代码里ym = 3*lam*D/a就是跟着这个关系走的,你改lam或a之后ym会自动适配,不用手动调观察范围。

但有个细节:ny固定为 51 时,如果ym变得很大,屏上采样间隔变粗,条纹边缘会出现锯齿。常见做法是把ny提到 201 或 501,代价是循环变慢。对单缝来说 201 足够,对光栅这种条纹细密的场景,n给到 1001 是合理的,原代码光栅脚本里n=1001就是这个原因。

4.2 光栅常数与缝数的组合效应

光栅衍射图样的两个特征——主极大间距和主极大宽度——分别由d和N控制。主极大位置满足d*sinphi = k*lam,所以d越大主极大越密;主极大宽度反比于N,N越大主极大越细。代码里beta = pi*d*sinphi/lam控制间距,sin(N*beta)/sin(beta)控制锐度。

改N的时候注意,N增大后sin(N*beta)/sin(beta)在beta接近pi整数倍时会出现数值不稳定,分母趋零导致 Inf。实际跑的时候如果看到曲线突然冒尖,把beta加一个极小量eps兜底:

beta = pi*d*sinphi/lam + eps; % 防止 sin(beta)=0 除零

这个eps对结果影响可以忽略,但能避免图上出现孤立的异常尖峰,出图干净很多。

4.3 FFT 频谱的物理尺度换算

FFT 法最容易翻车的地方是频谱坐标没有物理意义。fft2出来的矩阵索引是像素序号,不是角度或屏坐标。要把它和积分法的结果对上,需要做换算:频率分辨率df = 1/(Nx*dx),其中Nx是孔径图横向像素数,dx是像素物理尺寸。衍射角正弦sinphi = lam*f,屏坐标y = D*sinphi。

原代码没有做这步换算,所以figure(3)的横轴是索引而不是物理坐标。二次开发时补上这段,两种方法的结果才能真正叠在一张图上比较:

[Ny, Nx] = size(a); dx = 1e-5; % 假设像素物理尺寸 10um,按实际改 df = 1/(Nx*dx); f = (-Nx/2:Nx/2-1)*df; % 移频后的频率轴 sinphi = lam*f; y_axis = D*sinphi; % 对应的屏坐标

dx这个值必须和生成孔径图时的实际尺寸一致,否则换算出来的屏坐标是错的。如果孔径图是从画图工具截的,没有物理尺寸信息,那就只能做定性比较,不能做定量对照。

4.4 灰度映射与显示范围

所有脚本最后都有一句Br = (B/max(B))*N,把光强映射到 0~255。这一步看着简单,但max(B)取的是全局最大值,如果图里有异常尖峰(比如前面说的除零),归一化后其他条纹会被压得很暗,看起来像只有中心一个点。

稳妥的做法是用分位数归一化,避开极端值:

Bmax = prctile(B(:), 99.5); % 取 99.5 分位作为归一化上限 Br = min(B/Bmax, 1) * N;

prctile需要 Statistics Toolbox,没有的话用sort手动取分位也行。这样即使有个别异常点,整体条纹对比度仍然正常。

5. 避坑与排查:五个真实会卡住你的问题

5.1 图像全黑或全白

现象:跑完脚本,image出来的图要么纯黑要么纯白,看不到条纹。

原因:Br归一化时max(B)为 0 或 Inf,导致整个矩阵被压成同一个值。常见触发点是alpha或beta在零点除零产生 NaN,max忽略 NaN 后返回正常值,但B里混了 NaN,image显示时整行失效。

解决:在循环里对alpha、beta加eps兜底,或者在归一化前用B(isnan(B)) = 0清掉 NaN。跑完先max(B(:))和min(B(:))看一眼,确认数值范围正常再画图。

5.2 改了参数图没变化

现象:把a从1e-3改成5e-4,条纹宽度纹丝不动。

原因:MATLAB 工作区残留了上一次运行的变量,新脚本里如果没写clear,或者你是在命令窗口逐段粘贴而不是整文件运行,旧值会覆盖新值。另一种可能是你改的变量名和脚本里实际用的不一致,比如脚本里是a你改成了slit_width。

解决:每个脚本开头强制clear,养成整文件F5运行的习惯,不要逐段执行。改完参数后在脚本里加一句disp(a)确认生效。

5.3 FFT 频谱四角亮中间暗

现象:aperture_fft.m跑出来figure(2)是四个角亮、中间暗。

原因:忘了fftshift,或者fftshift作用在了错误的矩阵上。fft2的直流分量在矩阵左上角,不移频的话显示出来就是四角亮。

解决:确认aabss = fftshift(abs(fft2(a)))这一串顺序正确,先fft2再abs再fftshift。如果顺序反了,fftshift作用在复数矩阵上虽然也能跑,但后续取模的相位关系会乱。

5.4 正弦光栅频谱出现多余亮斑

现象:理论上只有三个衍射斑,实际跑出来五六个甚至一片散斑。

原因:存图环节引入了噪声。用截图工具保存 BMP 会带抗锯齿,用 JPEG 中转会有压缩伪影,这些都会在频谱里变成高频杂散。另外cos(xs)+1如果量化到 8 位灰度时截断太狠,也会产生谐波。

解决:用imwrite(Br, map, 'sine_grating.bmp')直接写灰度 BMP,不要经过任何图像编辑软件。生成时把xs采样点提到 1000 以上,减少量化误差。

5.5 光栅脚本跑得特别慢

现象:grating.m里n=1001时循环要跑十几秒甚至更久。

原因:外层 1001 次循环,每次内部对alpha、beta做数组运算,虽然单次很快,但累计起来在低配机器上明显。如果再把n提到 5001,时间线性增长。

解决:把外层循环向量化,ys整个作为向量一次性算:

sinphi = ys/D; alpha = pi*a*sinphi/lam; beta = pi*d*sinphi/lam + eps; B = (sin(alpha)./alpha).^2 .* (sin(N*beta)./sin(beta)).^2;

向量化之后n=5001也是毫秒级。原代码用循环可能是教学演示需要,实际用的时候没必要逐点算。

6. 二次开发:把零散脚本收成一个可配置的仿真工具

这套代码目前是“一个实验一个脚本”的形态,跑通没问题,但要做课程设计或者批量出图就有点散。我一般会做一层薄封装,把所有参数收进一个结构体,主函数根据场景名分发到不同计算分支,输出统一的光强矩阵和坐标轴。这样改参数只改一处,出图格式也统一。

核心思路是把“算什么”和“怎么显示”分开:

function [I, y] = diffraction_sim(cfg) % cfg.scene: 'single' | 'double' | 'grating' | 'fft' % cfg.lam, cfg.a, cfg.d, cfg.D, cfg.N, cfg.n 按场景取用 switch cfg.scene case 'single' [I, y] = calc_single_slit(cfg); case 'double' [I, y] = calc_double_slit(cfg); case 'grating' [I, y] = calc_grating(cfg); case 'fft' [I, y] = calc_fft_aperture(cfg); end I = I / max(I(:)); % 统一归一化 end

cfg里给默认值,调用时只覆盖关心的字段。calc_*系列函数就是把前面拆过的脚本主体搬进去,去掉clear和绘图部分,只返回I和y。绘图统一放到外层,想画灰度图就imagesc,想画曲线就plot,想导出就imwrite,不用每个脚本重复写一遍。

验证封装是否正确,最直接的办法是拿封装后的结果和原脚本逐点对比。选单缝场景,cfg用原脚本的默认参数,跑出来I和原B做max(abs(I-B)),应该在1e-12量级,说明只是搬了位置没改逻辑。再换 FFT 场景,和aperture_fft.m的中心截面比,形状对得上就说明分发逻辑没问题。

还有一个值得加的技巧是参数扫描。比如研究缝宽对中央条纹宽度的影响,写个循环扫a从0.2e-3到2e-3,每次调diffraction_sim,把半高全宽记下来画曲线,理论上应该反比于a。这种批量验证比单张图有说服力,也方便写进报告。

a_list = linspace(0.2e-3, 2e-3, 20); fwhm = zeros(size(a_list)); for k = 1:numel(a_list) cfg.a = a_list(k); [I, y] = diffraction_sim(cfg); idx = find(I >= 0.5, 1, 'last') - find(I >= 0.5, 1, 'first'); fwhm(k) = y(idx); end plot(a_list, fwhm, 'o-')

这段跑完如果曲线是漂亮的反比关系,说明整个封装链路是通的,后面加新场景(比如椭圆孔、双圆孔)只需要补一个calc_*分支,不用动主流程。

血泪经验是:封装的时候千万别顺手“优化”物理公式。我有一次觉得sin(alpha)./alpha在零点不优雅,改成了sinc(alpha/pi),结果 MATLAB 的sinc定义是sin(pi*x)/(pi*x),参数差一个pi,条纹宽度全错,排查了半天才定位到这一行。从那以后我每次封装完都强制拿原脚本跑一遍逐点对比,确认数值一致再往下做。希望帮到你。

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

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

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

立即咨询