简介:这份MATLAB仿真资源围绕全息成像中的菲涅尔衍射模型,面向光学工程、图像处理方向的学生或研究人员,用于理解全息记录与再现的数值实现方法。压缩包包含3个m脚本,整体仅2KB,脚本覆盖光源设置、物体模型、干涉图样计算与衍射重建等核心步骤;借助meshgrid构建波前,通过fft2/ifft2、fftshift完成频域变换,还可利用卷积操作模拟衍射过程,代码精简但流程完整。目前已有386人学习下载,适合作为课程设计、科研预研或自学的可运行样例。借助该资源可掌握用离散傅里叶变换近似菲涅尔衍射积分的方法,理解干涉条纹如何编码物体振幅与相位信息,并复现从全息记录到三维再现的完整数值流程,为深入探索计算全息与相干成像提供扎实基础。
1. 全息成像仿真在MATLAB里到底怎么跑:三个脚本还原记录到再现全流程
拿到CX11_2压缩包里的CX11_2.m、CX11_2_1.m、CX11_2_2.m三个脚本时,大多数人第一反应是直接双击主程序看能不能出图。但全息成像仿真和普通的光学仿真不一样,它自带一套完整的物理链路:先用菲涅尔衍射把物光传播到记录面,再和参考光干涉生成全息图,最后用再现光照射全息图做第二次衍射重建三维像。三步里任何一步的参数没配对,出来的图就是一团噪点,根本不是物体原来的样子。这篇笔记就围绕这三个脚本,把全息记录、菲涅尔衍射近似、fft2/ifft2的频域处理链路、以及记录距离和采样间隔的联动关系拆开讲,最后给出五条我实际跑下来最容易翻车的踩坑记录。适合正在做光学仿真课程设计、或者想把MATLAB全息仿真从“能出图”推进到“知道为什么出这个图”的读者。
2. 先把物理模型立住:菲涅尔衍射的近似边界与单次FFT实现
2.1 为什么要用菲涅尔近似:瑞利-索末菲积分算不动
全息记录的本质是让物光和参考光在记录面上干涉。物光从物体表面传播到记录面,严格来说要用瑞利-索末菲衍射积分描述,那是一个对整个物面的二重积分,核函数里带球面波因子,数值上要用二维卷积逐点算。物体尺寸一大、采样点一多,这种直接积分的计算量是灾难级的,跑一次要几分钟甚至几十分钟。
菲涅尔衍射做的事情是把这个球面波核做二项式展开,保留到二次项,把卷积形式简化成一个带二次相位因子的傅里叶变换。这样原本的卷积运算就变成了一次FFT加两次相位乘法,计算速度快了几个数量级。代价是适用条件受限:衍射距离要满足菲涅尔数条件,也就是观察距离远大于物体和记录面的孔径尺寸,且传播角度不能太大。在全息仿真的典型场景里,物体是毫米量级、距离是几十到几百毫米量级,菲涅尔近似完全够用。
这就是为什么项目文件里大概率能看到fft2、ifft2、fftshift这些函数,而不是看到嵌套循环做积分。理解了这一点,三个脚本的代码结构就很好猜了:核心就是傅里叶变换和相位因子的乘法,其余都是边缘处理。
2.2 S-FFT衍射模型的数学形式与MATLAB映射
菲涅尔衍射的单次FFT形式(S-FFT)是这么写的:设物面复振幅为U₀(x₀, y₀),传播距离z后,观察面的复振幅为
U(x, y) = exp(jkz) / (jλz) · exp[jk(x²+y²)/(2z)] · FFT{ U₀(x₀, y₀) · exp[jk(x₀²+y₀²)/(2z)] }
这里FFT是从物面坐标变换到频域,频域坐标恰好对应观察面的空间坐标。这个形式的好处是只用一次FFT,速度极快,而且物面到观察面的距离z直接以参数形式出现在相位因子里,改距离只需要改一个数字,非常适合做参数扫描。
在MATLAB里映射成代码,标准写法是:
function U = sfft_propagate(U0, z, lambda, L0) % U0: 物面复振幅分布 % z: 衍射距离 % lambda: 波长 % L0: 物面侧支持域尺寸(物理宽度,单位与lambda一致) N = size(U0, 1); % 采样点数,假设方形网格 dx = L0 / N; % 物面采样间隔 k = 2 * pi / lambda; fx = (-N/2 : N/2-1) / L0; % 频域坐标,对应观察面空间频率 [X, Y] = meshgrid(fx, fx); % 第一步:物面乘二次相位因子,等效于球面波前分解为平面波 U1 = U0 .* exp(1j * k / (2 * z) * (X.^2 + Y.^2) * L0^2 / N); U2 = fftshift(fft2(ifftshift(U1))); % FFT后频谱中心移到原点 U3 = exp(1j * k * z) / (1j * lambda * z) ... .* exp(1j * k / (2 * z) * (X.^2 + Y.^2) * L0^2 / N) .* U2; U = U3; end这里有个细节容易写错:meshgrid(fx, fx)生成的坐标数组对应的是频域坐标,但二次相位因子里需要的x²+y²是空间坐标,差别在于要不要乘L0²/N这个缩放因子。这段代码里X、Y的单位是“周期/mm”这类倒数单位,所以乘L0^2/N把它换算回空间坐标平方量纲。实际项目中如果直接照抄教材公式不换算,FFT出来的图就永远是错位的。另一个容易忽略的是ifftshift和fftshift要成对用:输入到fft2之前先ifftshift把原点挪到数组起点,变换完再fftshift把零频挪回中心,少一个都不行。
2.3 参考光设置:离轴与同轴的区别
全息记录要产生干涉条纹,必须有一束参考光。参考光的设置直接决定了全息图的频域结构,也决定了再现的时候能不能把三个像分离。
同轴全息的参考光和物光在记录面法线方向一致,干涉条纹是同心圆环,全息图的频谱里零级像和正负一级像叠在一起,再现时三个像都在光轴附近,互相干扰严重。离轴全息让参考光以一个角度θ斜入射,干涉条纹变成密集的平行条纹调制,全息图的频谱里三个像被载频分开了,只要载频足够高,就能在频域里把想要的像滤出来。
代码里设置离轴参考光,常见做法是构造一个平面波乘线性相位因子:
theta = 0.02; % 参考光倾角,单位弧度,小角度即可 R = exp(1j * k * (X * theta)); % 线性相位因子,沿x方向倾斜 % R = exp(1j * k * (X * theta + Y * theta_y)); % 二维倾斜同理theta取多大是个经验问题:太小了三个像分不开,太大了条纹频率超过采样极限会混叠。以波长632.8nm、像素间距5μm为例,theta超过约0.0079弧度(即λ/2dx)就开始接近采样极限,所以一般取0.002到0.006弧度比较稳。改theta的时候要同步检查全息图频谱,看正一级像有没有和零级像重叠。
2.4 参数表:波长、距离、采样间隔的联动约束
三个脚本能跑通,但要出质量好的重建像,下面几个参数必须联动调整。用表列出来:
| 参数 | 典型值 | 约束关系 |
|---|---|---|
| 波长λ | 632.8nm(He-Ne激光) | 决定相位因子里的波数k,换波长必须同步改所有相位因子 |
| 记录距离z | 50~300mm | 决定二次相位因子的弯曲程度,必须满足菲涅尔近似条件 |
| 采样点数N | 512~2048 | 决定FFT的分辨率,N太小频谱混叠,N太大内存翻倍 |
| 物面支持域L0 | 2~10mm | 决定采样间隔dx=L0/N,dx必须小于λz/L0才能满足采样定理 |
| 参考光倾角θ | 0.002~0.006 rad | 必须大于物体空间带宽的一半才能分离真实像 |
这五个参数不是独立选的。菲涅尔衍射的采样条件要求物面和记录面的支持域尺寸满足关系:L_record = λz / dx,也就是说一旦选了物面采样间隔dx和距离z,记录面的物理尺寸就被锁定了。如果代码里物面和记录面用同一个L,那z就不能随便改,改大了记录面实际支持域变小,物体的边缘信息会被截掉。这是全息仿真里最隐蔽的坑,后面避坑章节会单独展开。
3. 三个脚本的角色拆解:记录、再现与主控参数传递
3.1 CX11_2.m主控脚本:把记录和再现串起来
从文件名能推测,CX11_2.m是主控脚本,CX11_2_1.m和CX11_2_2.m分别对应全息记录和全息再现两个阶段。这种拆分方式是课程设计里最常见的主控+子函数结构,好处是每个阶段可以独立调参,不用每次跑全流程。
主控脚本的职责是定义全局参数、生成物体模型、调用两个子阶段、最后出图。典型结构是:
clear; clc; close all; lambda = 632.8e-6; % 波长,单位mm,He-Ne激光 L0 = 5; % 物面支持域尺寸,单位mm N = 1024; % 采样点数 z = 120; % 记录距离,单位mm dx = L0 / N; % 采样间隔 % 物体模型:两个矩形孔,振幅型物体 [x, y] = meshgrid(linspace(-L0/2, L0/2, N)); obj = zeros(N, N); obj(abs(x - 0.5) < 0.2 & abs(y) < 0.3) = 1; % 右矩形 obj(abs(x + 0.5) < 0.2 & abs(y) < 0.3) = 1; % 左矩形 % 记录阶段 H = CX11_2_1(obj, z, lambda, L0); % 再现阶段 recon = CX11_2_2(H, z, lambda, L0);注意这里lambda用毫米做单位,632.8nm写成了632.8e-6mm,这个单位处理在光学仿真里很容易乱。我的习惯是全部统一成毫米:波长毫米、尺寸毫米、距离毫米,所有公式里的单位自然抵消,不会出现10⁻⁹级别的数值误差导致相位因子溢出。
主控脚本里还有个容易忽略的点:物体模型通常是二维矩阵,但全息仿真处理的是复振幅,如果是振幅型物体,直接用0/1或0~1的灰度值赋值即可;如果是相位型物体,要写成exp(1j * phase)。项目里的物体大概率是振幅型的,因为课程设计最常用双矩形或字母图来验证算法。
3.2 CX11_2_1.m记录阶段:干涉条纹怎么算出来
记录阶段的核心是两步:先把物面复振幅用菲涅尔衍射传播到记录面,再和参考光干涉求强度分布。
function H = CX11_2_1(obj, z, lambda, L0) N = size(obj, 1); k = 2 * pi / lambda; dx = L0 / N; % 第一步:菲涅尔衍射传播到记录面 U_o = sfft_propagate(obj, z, lambda, L0); % 第二步:构造离轴参考光 fx = (-N/2 : N/2-1) / L0; [X, ~] = meshgrid(fx, fx); theta = 0.004; % 参考光倾角 R = exp(1j * k * X * theta); % 线性相位即平面波斜入射 % 第三步:干涉求强度,得到全息图 H = abs(U_o + R).^2; end这段代码的物理含义是:物光U_o传到记录面后是一个复振幅分布,和参考光R叠加后,abs(...)^2取强度。干涉项体现在数学上就是|U_o|² + |R|² + U_o·conj(R) + conj(U_o)·R,前两项是零级项,后两项才是记录了物体相位信息的真实像和共轭像。全息图H是一个实值灰度图,但它内部编码了物体的相位——这正是全息“记录全部信息”的关键。
跑这段代码最常见的错误是忘记对物光做衍射传播,直接把obj当作记录面光场和参考光干涉。那样得到的全息图只记录了物体的振幅,没有相位信息,再现出来的像永远是对称的、模糊的。判断自己有没有做对,可以看全息图:如果不加参考光倾角,条纹是同心圆环;如果加了倾角,条纹是带方向的栅格状,而且物体会让栅格产生局部变形。
3.3 CX11_2_2.m再现阶段:从全息图还原物体
再现是记录的可逆过程:用一束光照射全息图,再做一次衍射传播。这里关键抉择是用参考光照射还是用参考光的共轭光照射——两者得到的是共轭像和实像的区别。
function recon = CX11_2_2(H, z, lambda, L0) N = size(H, 1); k = 2 * pi / lambda; fx = (-N/2 : N/2-1) / L0; [X, ~] = meshgrid(fx, fx); theta = 0.004; % 用参考光照射全息图 R = exp(1j * k * X * theta); U_illum = H .* R; % 透射光场 = 全息图强度 × 再现光 % 传播z距离到像面 recon = sfft_propagate(U_illum, -z, lambda, L0); % 注意这里用-z,代表光从记录面向后传播到原物体位置 amp = abs(recon); end再现函数里两个细节值得说明。第一,H .* R是再现的核心操作:全息图H是实值强度分布,乘以再现光R后,干涉项里的conj(U_o)·R·R变成conj(U_o)·exp(2jkxθ),对应共轭像;U_o·conj(R)·R变成U_o,对应真实像。这就是全息再现的数学本质。第二,传播距离用的是-z而不是z,因为真实像在记录面的另一侧,距离对称的位置。如果用+z,会在另一个位置得到虚像,而且通常是离焦的模糊像。
sfft_propagate函数里传负距离,相位因子的符号会自动翻转,从发散波变成会聚波,数学上对应共轭光场的反向传播。这个负号不知道卡了多少人——我自己第一次写的时候也觉得“再照一次光应该是继续往前传播”,结果出来一张模糊图,折腾半天才发现距离符号反了。
3.4 重建像判断:实像、虚像与零级像
再现结果里同时存在三个分量:中心亮斑是零级像,两侧各一个物体像,一个实像一个虚像。判断重建是否成功,核心看这两点:
实像是否清晰可辨、和原始物体形状一致。如果是字母物体,实像应该是正立的;虚像是镜像的,但也能看出来是同一个物体。零级像通常是一个中心亮斑,灰度值远高于两侧的像。如果参考光倾角设得不够大,零级像会和实像重叠,看起来就是一片亮光里裹着一个模糊轮廓,怎么调z都没用——这时候要加大theta,而不是改距离。
实际调试过程中的做法是三步走:先显示频谱imagesc(log(abs(fftshift(fft2(H))))),确认三个像的频谱分离;再显示全息图本身,看条纹是否清晰;最后才显示重建像。第二步的频谱检查能直接告诉你theta够不够大、物体信息有没有超出带宽,比直接看重建像定位问题快得多。
4. 把实验参数调出最佳重建质量:波长、距离、采样数的联动
4.1 衍射距离与采样间隔的约束关系
菲涅尔衍射的S-FFT形式有一个隐含的采样约束:观察面的采样间隔dx₂ = λz/L₀,其中L₀是物面支持域尺寸。这个公式说明,一旦物面采样间隔dx和距离z定了,观察面的物理范围L₂ = N·dx₂ = λz/dx就自动被确定了。
如果程序里物面和记录面用的是同一个矩阵尺寸N和同一个物理宽度L₀,那么隐含假设是λz/dx = L₀,也就是z = L₀·dx/λ。超出这个距离的传播,记录面的实际支持域大于L₀,代码里等于只截取了记录面中心L₀宽度的区域,物体边缘的高频信息直接丢失,重建像边缘发虚、细节模糊。
我在实际调试里遇到过这个问题:双矩形物体的两个矩形重建出来边缘有明显振铃,但中心区域很清晰。排查过程发现记录距离z设成了200mm,而满足采样匹配的距离只有约40mm,差了5倍。解决方法有两种:保持z不变,把记录面的物理宽度L_rec设成λz/dx,在衍射计算时单独指定;或者保持L₀不变,反推z的限制范围。项目脚本里如果物面和记录面共用尺寸变量,大概率会遇到这个坑,改的时候要单独检查记录面支持域的计算逻辑。
4.2 物光、参考光的振幅比
全息图的干涉条纹对比度直接影响重建像的信噪比。条纹对比度定义为V = 2|U_o||R|/(|U_o|²+|R|²),V越大条纹越清晰,但V过大会导致记录面的强度动态范围过大,很多细节被量化噪声吞掉。全息术里的经验法则是物光和参考光的强度比在1:1到1:5之间,对应振幅比在1:1到1:2.2左右。
在这个仿真里,物光的振幅是物体透射率决定的,比如双矩形物体透射率是1,那物光振幅就是1。参考光的振幅可以通过R = A * exp(1j * k * X * theta)的系数A控制。我发现很多人直接不写系数,默认A=1,这通常没问题,因为物体透射率也是1,两者恰好1:1。但如果物体是灰度图,透射率最大值是0.5而不是1,那参考光振幅就要降到0.5~0.7,不然条纹对比度太低,再现像就跟蒙了一层雾一样。
另外注意参考光如果是平面波,振幅在记录面上是均匀的;物光经过菲涅尔衍射后,振幅会扩散,记录面中心的物光振幅可能只有边缘的数倍。如果物体很大,物光扩散严重,干涉条纹的边缘对比度会非常低。这种情况我一般把物体尺寸控制在支持域的一半以内,让物光能量集中在中心区域,跟参考光强度匹配。
4.3 用PSNR和肉眼双重判断重建质量
仿真里判断“调好了没”,不能只靠肉眼看重建像。肉眼判断的最大问题是:重建像是一个复振幅场,取abs()之后看的是强度图,有些相位误差在强度图上看不出来,但会导致重建位置偏移或聚焦面移动。
我习惯配合两个定量指标:一个是重建像和原始物体振幅的归一化均方误差(NMSE),另一个是峰值信噪比(PSNR)。在MATLAB里实现很短:
amp_recon = abs(recon); amp_obj = abs(obj); nmse = sum((amp_recon(:) - amp_obj(:)).^2) / sum(amp_obj(:).^2); psnr_val = 20 * log10(max(amp_recon(:)) / sqrt(mean((amp_recon(:) - amp_obj(:)).^2))); fprintf('NMSE = %.4f, PSNR = %.2f dB\n', nmse, psnr_val);这两个指标不是越高越好,因为重建像里包含了零级像和共轭像的区域,这些区域和原始物体完全对不上。正确的做法是只取重建像中心的一块区域(物体所在位置)和原始物体的对应区域对比,或者先用频域滤波把零级和共轭像滤掉再做对比。如果什么都不处理直接算PSNR,数值会很低,但这不代表仿真做错了——这是后处理的问题。
实际调试中PSNR能到15dB以上,肉眼看起来就很清晰了;超过25dB的话,重建像和原始物体几乎看不出差别。改参数时如果PSNR单调上升,说明改动方向对路;如果PSNR先升后降,说明出现了过拟合式的优化——比如加强参考光倾角让三个像分得更开,PSNR会上升,但倾角过头导致条纹频率接近采样极限,频谱混叠会让PSNR急剧下降。所以参数调整要看整个趋势曲线,而不是只看最终一张图。
5. 全息仿真避坑指南:5个最常见的翻车现场
5.1 现象:重建像边缘出现周期性的条纹状噪声,物体中心清晰但四周模糊
原因:物面支持域和记录面支持域用的是同一个矩阵尺寸,但菲涅尔衍射在距离z变化时,记录面的实际物理范围会按λz/dx缩放。距离一超过匹配值,记录面中心截取区域小于实际光场范围,物体边缘的高频信息被截断,在频域上表现为频谱边缘的跳变,重建时产生振铃。
解决:检查衍射函数的坐标网格是不是独立于物面。记录面和物面都保留自己的支持域尺寸变量,记录面的尺寸按L_rec = lambda * z / dx算出,不要直接复用L0。如果脚本里共用了同一个L0,在调用衍射函数前单独定义一个L_rec传入。
5.2 现象:频谱图里零频不在中心,重建像有明显的倾斜和错位
原因:FFT前少用了ifftshift。MATLAB的fft2把原点放在数组左上角(索引1,1),fftshift把原点移到中心(索引N/2+1,N/2+1)。衍射程序里如果输入到fft2之前没做ifftshift,相当于把原点在左上角的光场拿去变换,变换后即使做了fftshift,频谱的原点位置也是错的,二次相位因子的中心对不上。
解决:在进入fft2之前对输入矩阵做ifftshift,变换完成后再做fftshift。两个shift必须成对出现。写成代码就是U2 = fftshift(fft2(ifftshift(U1))),顺序别反。
5.3 现象:重建像完全模糊,无论怎么调z都看不清物体轮廓
原因:衍射距离z超出了采样条件允许的范围,或者物面采样间隔dx太大。菲涅尔衍射的采样条件要求dx必须小于λz/L_rec,否则衍射场在频域上混叠。如果z设得特别小(比如1mm),λz/L_rec变得很小,而dx没跟着缩小,记录面的光场就是欠采样的。
解决:用采样条件反推参数范围。保持N不变时,z_min = dx·L_rec/λ。实际调试中如果z必须设小,就同步增大N,保持dx = L0/N足够小。我一般会让dx小于λz/(2L_rec),留出两倍余量,这样重建质量更稳。
5.4 现象:再现像中间一团亮斑,物体像被亮斑盖住只能看到边缘
原因:零级像(直透光和全息图的平均透射光)能量太强,把一级像淹没了。全息图H = |U_o+R|²里有|U_o|²和|R|²两个零级项,|R|²是常数项在全息图上是均匀背景,再现时这部分能量直接透射形成中心亮斑。参考光振幅比物光大得越多,亮斑越强。
解决:一种方案是减小参考光振幅,让物光和参考光强度比接近1:1,零级项的能量会下降;另一种方案在频域滤除零级:对全息图做FFT,把中心区域的频谱置零后再IFFT回来,再乘参考光做再现。第二种方案效果更直接,代价是滤波半径要调,滤多了会损失物体低频信息。
5.5 现象:全息图条纹看起来是断的,重建像上有横竖交错的网格噪声
原因:保存或显示全息图时把复振幅转成了uint8类型,导致强度值被截断到0~255且量化步长过大。干涉条纹的对比度如果集中在很小的动态范围内,量化误差会直接抹掉条纹细节。MATLAB里用imwrite保存全息图时默认转uint8,这个坑特别隐蔽。
解决:显示用imagesc(H)配合colormap gray,它能自动把数据映射到显示范围,不改变数据本身;保存则用imwrite(mat2gray(H), 'hologram.bmp'),先归一化再保存。如果要进行数值计算,全程保持double类型,只在最终显示时转换。
6. 一个能验证算法正确性的习惯:用已知相位物体做闭环测试
判断自己的全息仿真链路有没有问题,最直接的办法是做闭环验证:放一个已知物体进去记录、再现,把重建结果和原始物体对比。这个做法相当于给整个仿真链路做一个黑盒测试——如果输出和输入能对得上,说明记录和再现两个阶段没有系统性错误。
闭环验证的流程分四步。第一步,准备一个已知物体,振幅型用双矩形,相位型用阶梯相位板,记录下它的复振幅分布;第二步,用CX11_2_1记录,得到全息图;第三步,用CX11_2_2再现,得到重建复振幅场;第四步,把重建像的中心区域和原始物体做定量对比。
% 闭环验证脚本(在三个函数之外单独运行) lambda = 632.8e-6; L0 = 5; N = 1024; z = 120; % 用相位型物体:中心一个π相位圆斑,周围是0相位 [x, y] = meshgrid(linspace(-L0/2, L0/2, N)); phase = zeros(N, N); phase(x.^2 + y.^2 < 1) = pi; obj_phase = exp(1j * phase); % 纯相位物体,振幅恒为1 H = CX11_2_1(obj_phase, z, lambda, L0); recon = CX11_2_2(H, z, lambda, L0); % 对比重建像的相位分布和原始相位 recon_phase = angle(recon) .* (abs(recon) > 0.1); % 只看振幅足够大的区域 mask = (abs(recon) > 0.1); phase_error = angle(conj(obj_phase) .* recon ./ abs(recon + eps)); phase_error = phase_error .* mask; rms_phase = sqrt(mean(phase_error(mask).^2)); fprintf('RMS phase error: %.3f rad\n', rms_phase);这段代码里conj(obj_phase) .* recon是相位对比的标准技巧:将重建复振幅乘以原始物体复振幅的共轭,乘积的相位就是两者相位差。RMS相位误差在0.1弧度以内,说明重建链路基本准确;如果误差在0.5弧度以上,检查参考光倾角是否过大或记录距离是否超出采样匹配范围。
闭环验证的价值在于它能区分“算法错”和“参数差”。参数差是调出来的,算法错是改不出来的——比如记录阶段忘记衍射传播,闭环验证一看相位误差直接爆表,立刻能定位到是记录还是再现的问题。我也遇到过一种特殊情况:闭环验证通过,但换一个物体就重建失败。后来发现是物体尺寸变了导致频谱展宽,原参考光倾角对应的载频不够分离三个像了。所以闭环验证只证明“当前参数下链路正确”,换物体、换距离、换波长都得重新验证。
从那以后我每次拿到别人写的全息仿真脚本,第一件事就是跑一遍闭环验证而不是直接看重建图。这个习惯帮我节省了大量排查时间——它就是全息仿真的后悔药,链路通了,后面调参才有意义。希望帮到你。
本文还有配套的精品资源,点击获取