☰
菲涅尔全息成像仿真:从FFT原理到数值重建完整指南
2026/9/28 8:22:03 网站建设 项目流程

简介:一个基于MATLAB的全息成像仿真代码包,面向光学工程、信息光学及相关方向的学生与科研人员,旨在通过数值模拟揭示菲涅尔衍射、干涉记录与数值再现的完整物理过程。压缩包内包含3个.m源文件,整体仅2KB,代码结构紧凑,覆盖了从光源参数设定、物光波前构建、干涉图样计算到衍射重建的关键步骤,便于使用者直接运行和修改参数。该资源目前已吸引385人学习下载,适合作为全息成像课程实验或科研预研的参考实现。借助fft2、ifft2、fftshift等频域处理函数以及卷积运算,代码能够清晰展示全息图频谱分布如何影响重建像,并帮助读者掌握MATLAB在波动光学仿真中的典型用法,例如利用meshgrid生成二维光场、通过卷积模拟传播过程等。通过调整波长、距离、孔径等参数,可直观观察重建像质量的变化,进一步理解菲涅尔近似及全息记录条件的适用边界。

1. 全息成像仿真:先把菲涅尔衍射变成可运行的数值实验

全息成像仿真这个标题,十有八九是冲着菲涅尔衍射公式来的:要在一个二维复振幅数组上生成全息图,再把它数值重建回原始物面,整个过程不碰激光器、不碰光学平台,全靠 FFT 把公式翻译成 NumPy。这类实验的目的很直接,就是把「记录振幅与相位 + 数值重建」这一闭环跑通。适合谁?光学、电子、信号处理背景的学生,或者刚转计算成像、需要从原理过渡到可复现代码的工程师。核心参数就四个:波长 λ、采样间隔 Δx、衍射距离 z,以及参考光角度 θ。把它们定对,剩下的事情其实就是几段可复现的矩阵运算。这篇文章按我实际做过的流程来写,代码可以直接抄,参数和坑都标在明处。

2. 菲涅尔衍射数值化:先选对传播模型,再动手写衍射函数

菲涅尔衍射的近似积分长这样:

U(ξ, η) = e^{jkz} / (jλz) ∬ U₀(x, y) · exp[jπ((ξ−x)² + (η−y)²) / (λz)] dxdy

这个式子的意思是:物面每个点发出的球面波,在传播距离 z 后,相位被二次项 (ξ−x)² + (η−y)² 调制。看上去要双重积分,但它的数学结构是卷积,所以计算上只有两条路可走:在空域构造脉冲响应做两次 FFT,或者在频域构造传递函数做一次 FFT。工程上绝大多数菲涅尔仿真代码都选第二条路,因为它快、稳、好写。

2.1 从菲涅尔积分到单次 FFT:传递函数法为什么是默认选择

菲涅尔衍射的频域传递函数是:

H(fx, fy) = e^{jkz} · exp[−jπλz(fx² + fy²)]

物场 U₀ 做一次 FFT,乘上 H,再做一次 IFFT,就得到传播 z 之后的复振幅 U。常见做法是把这个函数封装成独立模块,后续生成全息图和重建都复用同一份代码。下面这个函数是带限角谱法的菲涅尔近似版,我一般把它当默认工具用:

import numpy as np def fresnel_TF(u0, z, lam=632.8e-9, dx=5e-6, band_limit=True): """菲涅尔衍射:传递函数法(单次 FFT)。 u0 : 输入复振幅,二维 numpy 数组 z : 传播距离,单位 m lam : 波长,单位 m dx : 采样间隔,单位 m band_limit : 为 True 时对传递函数做带限,避免近场混叠 """ k = 2 * np.pi / lam ny, nx = u0.shape fx = np.fft.fftfreq(nx, d=dx) # x 方向频率 fy = np.fft.fftfreq(ny, d=dx) # y 方向频率,采样间隔默认相同 FX, FY = np.meshgrid(fx, fy) # 菲涅尔近似的传递函数;第一项是整体相位,不影响振幅图 H = np.exp(1j * k * z) * np.exp(-1j * np.pi * lam * z * (FX**2 + FY**2)) if band_limit: # 只保留角谱法里可传播的分量,防止近场时高频噪声被放大 fmax = 1.0 / (np.sqrt(4 * z**2 / (ny * dx)**2 + 1) * lam) H *= (np.sqrt(FX**2 + FY**2) < fmax) U = np.fft.ifft2(np.fft.fft2(u0) * H) return U

频域网格用np.fft.fftfreq生成,返回的是从负 Nyquist 到正 Nyquist 的频率值,单位是 1/m。meshgrid之后 FX 和 FY 形成二维频域坐标,这是 FFT 类仿真里最容易写错的一行,很多人忘了频率轴单位,导致结果左右翻转。带限条件里那个ny*dx是全息面宽度,通频带下限由 z 和面宽共同决定,这是带限角谱法(BL-ASM)的经典做法,比我之前用固定半径截断要稳得多。

这个函数对近场和远场都适用,只有一个代价:它在频域做了截断,近场传播时会损失部分高频信息。但对于全息图生成和重建这个场景,信号带宽远小于截止频率,实际影响可以忽略。以我常用的参数为例:λ=632.8nm,Δx=5μm,N=2048,z=0.3m,截止频率在 26300 1/m 左右,而物面的有效信息带宽只有几千 1/m,留了充足余量。

2.2 空域脉冲响应法:两次 FFT 的对照,帮我抓出过不少 bug

不是所有场景都适合传递函数法。当 z 特别小,小到接近 Δx 量级时,带限角谱的截止频率会降得很低,导致结果锐度明显变差。这个时候空域脉冲响应法更直观,因为它的核函数长在空域,物理意义清楚:菲涅尔衍射的脉冲响应是 h(x, y) = e^{jkz} / (jλz) · exp[jπ(x² + y²) / (λz)],把它和物场做循环卷积即可。我通常用下面这个函数作为第一个函数的交叉验证:

def fresnel_IR(u0, z, lam=632.8e-9, dx=5e-6): """菲涅尔衍射:空域脉冲响应法(两次 FFT)。 适用于采样条件 z >= N*dx^2/lambda 的场景, 近场小于该距离时结果会出现环形伪影。 """ k = 2 * np.pi / lam ny, nx = u0.shape y = (np.arange(ny) - ny // 2) * dx # 空域坐标要对称,不能从 0 开始 x = (np.arange(nx) - nx // 2) * dx Y, X = np.meshgrid(y, x) h = np.exp(1j * k * z) / (1j * lam * z) * np.exp(1j * np.pi * (X**2 + Y**2) / (lam * z)) Hf = np.fft.fft2(np.fft.ifftshift(h)) # 原点移到数组中心后再 FFT U = np.fft.ifft2(np.fft.fft2(u0) * Hf) return U * dx * dx # 卷积积分里的微元 dx*dy

注意两个细节。第一,空域坐标要用np.arange(ny) - ny // 2,也就是让原点落在数组正中心,而不是从 0 开始,否则核函数的相位中心会错位,结果是整体平移一个像素且相位全乱。第二,循环卷积做完之后要乘一个 dx²,这是把离散求和还原成连续积分的缩放因子。这两个错误都不会让图像变黑,只是重建像位置偏移或者振幅整体缩放,排查起来特别费时间。

我用这两个函数做过一致性测试:同一个输入物场、同一组参数,两种方法输出的振幅差别在 1e-6 量级,相位差别在 1e-4 量级。这个一致性给了我很大信心。但要注意,空域脉冲响应法有个前置条件,z 必须大于等于 N·Δx²/λ,否则核函数的高频相位在相邻像素间变化超过 π,FFT 循环卷积会把高频折叠成低频,形成一圈一圈的环状伪影。第 4 章会专门讲这个坑,这里先记下。

3. 从物面到全息面:离轴干涉、记录与数值重建

衍射仿真只是第一步,全息成像仿真真正的主菜是把物光与参考光干涉,记录强度全息图,再数值重建出原始物场。这一节从参考光设计开始,一路做到三幅像分离,代码跑通后你会看到零级项、实像和孪生像在频域里的分布。

3.1 参考光设计:离轴角先定上限,再算分离度

离轴全息的参考光在仿真里是一个平面波:R(x, y) = exp(j2π(fx·x + fy·y))。fx 和 fy 是参考光在频域的位置,它和入射角的关系是 fx = sinθ/λ。记录下来的全息图强度包含三项:物光自干涉项 |U|²、参考光强度 |R|²、交叉干涉项 U·R* 和 U*·R。前两项都落在零频附近,第三项和第四项分别出现在 +fx 和 -fx 附近。要让重建像和零级光分离,参考光频率必须离原点足够远。

参考光的频率上限由采样率决定。奈奎斯特条件是 fx < 1/(2Δx),工程上我会再留一倍余量,取 fx ≤ 1/(4Δx),对应条纹周期至少 4 个像素。下限则由物体带宽决定:物体空间尺寸越小,频谱越宽,参考光频率要大于物体最高频率加上零级项半径,一般取几十个频域网格以上。下面是一个参数计算表,方便直接套用:

参数数值说明
波长 λ632.8 nm氦氖激光
采样间隔 Δx5 μm模拟 CMOS 像元
采样点数 N2048单边点数
面宽 L10.24 mmN·Δx
衍射距离 z0.3 m物面到全息面
参考光频率 fx12600 1/m对应偏轴角约 0.008 rad
频率网格间隔 Δf97.7 1/m1/L
参考光频移129 格fx/Δf

在这个配置下,零级项占中心约几十格,实像频移 129 格,孪生像在 258 格,三者完全分得开。如果参考光频率太小,比如只有 20 格,零级项和实像在频域就会重叠,重建出来一团亮斑糊住像;如果频率太大,比如超过 100000 1/m,全息图上的条纹周期小于 2 个像素,直接混叠成摩尔纹。这两头我在第 4 章都会给出具体翻车现象。

3.2 记录与重建三步闭环:从物面复振幅到频域滤波取实像

物面我用一个组合图形:一个圆孔加一个矩形孔,放在视场中心偏下一点,比纯字母更可控,不需要依赖字体渲染。代码如下:

def make_object(N=2048, dx=5e-6): """生成测试用物面振幅:半径 80 像素的圆孔 + 40x40 像素方孔。""" y = (np.arange(N) - N // 2) * dx x = (np.arange(N) - N // 2) * dx Y, X = np.meshgrid(y, x) obj = np.zeros((N, N), dtype=np.complex128) circle = (X**2 + Y**2) < (80 * dx)**2 rect_x = (np.abs(X) < 40 * dx) & (np.abs(Y - 200 * dx) < 20 * dx) obj[circle | rect_x] = 1.0 return obj

物面大小是 10.24mm,圆孔直径只有 0.8mm,占比很小。这个比例是刻意的:物体尺寸小,频谱就宽,参考光分离窗口的时候不容易和零级项打架。如果你把物体铺满整个视场,重建时边缘会和零级光晕混在一起,肉眼看着像蒙了一层雾。

记录和重建的完整流程如下,这是整个仿真最核心的一段:

lam = 632.8e-9 dx = 5e-6 N = 2048 z = 0.3 fx = 12600.0 # 参考光 x 方向空间频率,1/m fy = 0.0 # 1. 物面复振幅 -> 菲涅尔传播 -> 全息面复振幅 U0 = make_object(N, dx) UH = fresnel_TF(U0, z, lam, dx, band_limit=True) # 2. 参考光与干涉记录:强度全息图 Yc = (np.arange(N) - N // 2).reshape(-1, 1) Xc = (np.arange(N) - N // 2).reshape(1, -1) R = np.exp(1j * 2 * np.pi * (fx * Xc * dx + fy * Yc * dx)) I_H = np.abs(UH + R)**2 # 记录全息图强度 # 3. 重建:全息图乘参考光 -> 反向传播 z -> 频域窗口提取实像 E = I_H * R U_back = fresnel_TF(E, -z, lam, dx, band_limit=True) # 反向传播 F = np.fft.fftshift(np.fft.fft2(U_back)) yy, xx = np.mgrid[0:N, 0:N] mask = ((xx - N//2)**2 + (yy - N//2)**2) < (40)**2 # 0 频附近半径 40 格 U_clean = np.fft.ifft2(np.fft.ifftshift(F * mask))

第一步的正向传播用了第 2 章的fresnel_TF,把物面复振幅传到全息面。第二步构造参考光时,Xc和Yc的单位是像素,乘以 dx 后才变成米,这个单位换算漏掉的话,参考光频率会被放大 5μm 倍,全息图条纹密到完全无法识别。第三步最关键:E = I_H * R之后,实像分量 U_H·|R|² 落在零频,零级项被搬到 +fx,孪生像被搬到 +2fx。所以频域滤波的窗口应该放在原点,而不是 fx 处。这一步我第一次做反了,取 +fx 附近的窗口,结果只滤出零级光的模糊亮斑,重建像怎么调都出不来。后来把频谱图打出来才明白,实像就在原点,是我自己把频移方向搞反了。

重建结果可以从三个数判断:重建像的振幅最大值对应圆孔中心,圆孔边缘锐利,方孔四角清晰;背景噪声在 1e-4 量级;实像质心与物面原点的偏移不超过 1 个像素。这三个条件都满足,这个全息成像仿真闭环就算真正跑通了。如果只有肉眼看着像,说明你可能只是碰巧调出了一个能看的参数,换成别的物体马上翻车。

4. 菲涅尔全息仿真避坑:采样、距离与角度三座山

这一类仿真翻车,九成翻在三个地方:传播距离 z 不满足采样条件、参考光角度超了奈奎斯特、零级项没滤干净。剩下的翻车大多来自数据类型和内存管理。下面五条是我自己踩过、也在帮别人看代码时反复见到的坑,每条按现象、原因、解决的顺序写清楚。

4.1 距离 z 太小,重建像四周出现一圈圈环状波纹

现象:把 z 从 0.3m 改成 0.02m,重建像主体还在,但周围多出一圈一圈同心圆环,像水波纹一样,而且怎么调整滤波半径都消不掉。

原因:菲涅尔空域脉冲响应在 z 很小时相位变化极快。采样条件要求 z ≥ N·Δx²/λ,代入 N=2048、Δx=5μm、λ=632.8nm,z_min = 0.081m。z=0.02m 远小于这个值,脉冲响应相邻像素的相位差超过 π,循环卷积把本该折叠的高频分量映射成了低频,于是出现环状条纹。这是典型的频谱混叠,不是算法写错。

解决:先算 z_min 再定距离。如果因为实验场景限制必须用小 z,就把采样点数 N 降下来,或者改用带限角谱法。带限角谱法的band_limit=True能缓解一部分,但缓解不了太多,它只是把超高频通道关掉,信息本身已经丢了。最可靠的做法是保持 z ≥ N·Δx²/λ,宁可让物体在视场里小一点。

4.2 参考光角度调大后,全息图出现斜向摩尔纹

现象:为了把三个像拉得更开,把参考光频率从 12600 1/m 调到 90000 1/m。全息图看起来像有条纹,但重建像变成一格一格的斜纹,完全看不出物体形状。

原因:90000 1/m 对应的条纹周期约 11μm,而采样间隔是 5μm,每个周期不到 2.3 个像素,已经逼近奈奎斯特极限。频域里,实像和孪生像的频谱发生重叠混叠,重建出来自然是一团乱纹。90000 1/m 对应的 θ = arcsin(fx·λ) = arcsin(0.057) ≈ 0.057 rad,而极限角度是 λ/(2Δx) = 0.063 rad,已经贴着边了。

解决:参考光频率不要超过奈奎斯特的一半,即 fx ≤ 1/(4Δx)。对应本组参数是 50000 1/m,我一般取 10000~20000 1/m 这个区间,既保证分离度,又留足采样余量。按这个区间算出来的条纹周期都大于 30μm,约 6 个像素以上,重建质量稳定得多。

4.3 重建像中心一团亮斑,物体像被糊在光晕里

现象:物体重建出来了,边缘也清晰,但中心有一个很亮的圆斑,物体像正好压在上面,对比度差到几乎看不清。

原因:这是零级项没滤干净。零级项包含 |U|² 和 |R|² 两部分,它们在全息图里能量最大,重建后集中在原点附近,尺寸比物体像大得多。如果物体本身放在视场中心,零级光斑就会直接盖住实像。

解决:两步走。第一,滤波器半径要大于物体带宽但小于参考光频移量。以本组参数为例,物体半宽约 80 像素,对应频谱半径约二三十格,滤波半径取 40 格就够,同时零级频移到 129 格,完全不会被窗口圈进来。第二,如果物体必须放中心、零级光又实在太强,可以在记录时做三步相移,把 |U|² 和 |R|² 消掉,只剩干涉项。常见做法是采集相位差 120° 的三张全息图,加减组合后得到纯干涉场,仿真里只需要把参考光相位写成参数循环三次,代价是时间翻三倍,但效果立竿见影。

4.4 全息图存成 PNG 再读回来,重建全是噪点

现象:仿真跑完,把强度全息图I_H用plt.imsave()存成 PNG,下次直接读图重建,结果背景噪声大,像被砂纸磨过一样。

原因:全息图的干涉条纹动态范围很大,暗条纹和亮条纹差几个数量级,而 PNG 只有 8bit 量化,256 个灰度级根本装不下完整的条纹细节。量化误差在重建时会被反向传播的高通特性放大,变成高频噪点。更隐蔽的问题是很多同学保存前做了归一化,把最大值压到 255,暗条纹直接变成 0,相当于把干涉信息截断了。

解决:仿真链路里不要存图,全程用 numpy 的复数数组传递数据;必须持久化时用np.savez_compressed()保存浮点复数场,或者保存I_H.astype(np.float32),读取后用双线性插值恢复尺寸再重建。如果只是要一张示意图,存 PNG 无所谓,但别拿它当重建输入。这是我交过学费的一条,后来所有全息数据都改成 npz 存档,再没出过这类问题。

4.5 N 从 1024 改到 4096 后内存溢出,结果还不可复现

现象:为了看得更清楚,把 N 改成 4096,程序报 MemoryError。调回 1024 后重建结果每次都有一点不同,特别是背景噪声。

原因:N=4096 时,单个复数数组占 4096²×16 字节 = 268MB,fresnel_TF里中间变量至少有三个这样的数组,加上全息图和重建场,峰值内存轻松超过 1.5GB。至于每次结果不同,是因为全息图强度、重建场里有随机初始化的部分,或者用了某些依赖随机数的操作,没有固定随机种子。FFT 本身是确定的,但程序里只要有一个np.random调用,背景噪声就会变。

解决:N 一般取 2048 就足够教学演示,再大就分块计算,或者用np.empty预先分配内存,减少临时数组。同时固定np.random.seed(0),并把所有中间结果用np.allclose()做回归测试。我现在的习惯是每次跑完把重建像的峰值位置和背景均值打出来,如果两次运行这两个数不一致,一定是有随机源混进来了,优先排查而不是继续调参。

5. 像质验证与调参顺序:让重建结果可量化、可复现

重建像看着清晰并不等于正确。我会用三个量化手段验证结果,任何一个不达标都会优先怀疑参数而不是算法。第一个是闭合性测试:把物面场正向传播 z,再反向传播 z,理论上应该回到物面本身。用归一化均方误差衡量:

U0 = make_object(N, dx) U_forward = fresnel_TF(U0, z, lam, dx, band_limit=True) U_roundtrip = fresnel_TF(U_forward, -z, lam, dx, band_limit=True) err = np.linalg.norm(np.abs(U_roundtrip) - np.abs(U0)) / np.linalg.norm(np.abs(U0)) print(f"round-trip NRMSE = {err:.2e}")

闭合性误差在 1e-5 量级说明传播函数本身没问题,误差大于 1e-2 就要检查距离和带限条件。第二个是质心偏移校验:离轴全息中实像中心相对零级中心的偏移量理论值是 z·tanθ,用重建像振幅做质心加权,实测偏移和理论值的差应该小于一个像素。这个校验能一次性抓出参考光频率单位写错、频移方向写反、滤波窗口选错三类 bug。

第三个是参数扫描,最具参考价值。把 z 从 0.02m 扫到 0.6m,对每个 z 做完整记录和重建,计算重建像与原物的相关系数,你会看到结果分三段:z 小于 0.08m 时相关系数骤降,这是采样条件失效;0.1m 到 0.5m 之间是平缓平台;超过 0.6m 后物体在频域衰减过大,相关系数缓慢下降。这个曲线让我彻底明白,菲涅尔仿真的 z 不是随便给一个就行,它有一个由 Δx、N、λ 共同决定的「甜蜜区间」。

我现在的习惯是把 λ、Δx、N、z、fx 五个参数写成常量块放在脚本最顶部,每次只动一个变量,跑完先看闭合性误差,再看质心偏移,最后才看图像。遇到重建糊了,先回归测试确认代码没改坏,再依次排查采样条件、参考光角度和零级滤除。这个顺序帮我少走了很多弯路,也把「调参数看运气」变成了可解释的流程。这套方法不只是应付课设,放到真实的计算全息、数字全息显微镜项目里,排查思路完全一样。希望帮到你。

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

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

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

立即咨询