简介:全息图生成是光学与计算成像交叉领域的重要技术,这份压缩包提供了一个基于MATLAB的简易实现示例,适合希望快速上手数字全息仿真的学生、工程师及科研人员。压缩包内仅包含1个m脚本,包体大小仅1KB,但代码覆盖了从光源模拟、物体光场构建、干涉记录到再现可视化的完整流程。实现中借助meshgrid构造相干光波的二维波前,通过傅里叶变换在空间频率域完成干涉计算,并能直观输出模拟的全息图及再现像。代码结构简洁、注释清晰,运行后可对比原始物体与再现像,帮助理解全息图如何编码和重建三维信息,也可作为课程实验、课题预研或进一步扩展复杂全息算法的起点,是理解计算成像原理的良好范例。目前已有199人学习下载,值得光学方向学习者参考。
1. quanxitu.zip 与全息图生成:拿到压缩包之前,先把概念对齐
搜 quanxitu.zip 这个关键词的人,通常不是想从头读一遍光学教科书,而是手里有一批图片,想生成能在空间光调制器(SLM)上播放的全息图。quanxitu 就是“全息图”的拼音,压缩包里要落地的事,在学术上叫计算全息(CGH:Computer-Generated Hologram),在工程上最常见的是迭代相位恢复:给定重建面的目标强度,反算出全息面上的二维相位分布,使这个相位的傅里叶变换在重建面逼近目标图案。网上的实现并不少,但很多人把 Gerchberg-Saxton(GS)算法跑出来之后,发现重建图像是花的、发灰的、中间带一个大亮斑,于是开始怀疑算法本身。问题大多数出在参数和输入预处理,不在原理。这篇按“物理含义 → Python 实现 → 彩色与批处理 → 验证排错”的顺序,给出一套能用 numpy 直接跑起来的方案,并把迭代次数、初始相位、补零边界、平方根校正这几个最影响结果的参数讲透。
2. 从二维图像到计算全息:为什么全息图生成绕不开相位恢复
2.1 光学全息与计算全息的分工
全息图生成不是 Photoshop 滤镜,也不是把图片像素反色。普通屏幕只能显示光的强度,而全息图记录的是波前里的相位信息。光学全息通过物光和参考光的干涉,把相位差转化为明暗条纹;计算全息没有实物,也不需要防震台,它是先在计算机里算出一个复振幅分布,再把这个分布加工成相位图、振幅图或两者混合的图案。实际项目里,因为相位型空间光调制器(SLM)对相位调制更高效,纯相位 CGH 占绝大多数。
| 路线 | 信息获取方式 | 重建手段 | 输出载体 | 主要误差源 |
|---|---|---|---|---|
| 光学全息 | 干涉记录真实物光波前 | 参考光照明全息图 | 胶片、光刻掩膜、传感片 | 振动、散射噪声、记录介质分辨率 |
| 计算全息 | 从目标图案反算相位 | FFT / 衍射传播模拟 | SLM、激光打标路径、掩膜文件 | 相位求解、量化误差、SLM 像素结构 |
从工程角度看,计算全息的最大优势是迭代成本低:改一张目标图,重新跑一遍数值计算就行,不需要重新搭光路。这也正是 quanxitu.zip 这类代码包存在的理由——批量把图片转成全息图,供后续光学实验或加工使用。
2.2 相位型全息图与 GS 迭代的数学含义
你在全息图生成代码里看到的“相位图”,并不是随便一组随机黑白条纹。对一个纯相位 Fourier 全息图来说,全息面上每个像素的振幅固定为常数,设相位分布为 φ(x, y),那么重建面的复振幅就是:
F(u, v) = FFT{ exp(i φ(x, y)) }
注意这里约束的只有重建面的强度 |F(u, v)|²,相位是自由的。于是问题变成:已知傅里叶变换的模(目标图像的某一变换),反推原函数的相位。这正是典型的相位恢复问题,Gerchberg-Saxton 算法是解决这类问题最经典的一把钥匙。
GS 的一次迭代分四步:
- 全息面:取当前相位解,振幅固定为 1,构成复振幅 exp(iφ)。
- 正向 FFT 到重建面,保留完整的复振幅。
- 重建面约束:把振幅替换为目标振幅,相位继续保留。
- 逆 FFT 回到全息面,只取相位,丢掉振幅。
这样交替投影的本质,是在“全息面相位自由、振幅恒为 1”和“重建面振幅等于目标”这两个约束之间来回修正。因为强度约束是非凸的,GS 不能保证全局最优,所以初始相位很重要。常见的项目做法是固定迭代次数,用几个随机种子各跑一遍,取重建误差最小的那组。全息图生成里的“玄学”,很多时候就是这一步产生的。
3. 用 Python 跑通 GS 算法:最小实现、参数表和收敛判断
3.1 最小可运行的 GS 相位恢复实现
下面这个函数是全息图生成的核心,输入是一张已经预处理好的目标振幅图,输出是相位全息图和误差曲线。我一般把它独立放在一个cgh.py里,后续彩色、批处理都复用这一个函数。
import numpy as np def gerchberg_saxton(target, iterations=100, seed=42): """计算相位型 Fourier 全息图。 target: 目标振幅,float32,范围 0~1。 返回 (phase, errors)。 """ rng = np.random.default_rng(seed) h, w = target.shape[:2] initial_phase = rng.uniform(-np.pi, np.pi, (h, w)).astype(np.float32) phase = initial_phase.copy() errors = [] for _ in range(iterations): # 全息面复振幅:振幅固定为 1,只保留相位 holo = np.exp(1j * phase) # 正向传播:从全息面到重建面 field = np.fft.fftshift(np.fft.fft2(holo)) amplitude = np.abs(field) field_phase = np.angle(field) # 重建面约束:替换振幅,保留相位 field_c = target.astype(np.float32) * np.exp(1j * field_phase) # 逆传播:回到全息面,只取相位 holo_c = np.fft.ifft2(np.fft.ifftshift(field_c)) phase = np.angle(holo_c) # 用归一化 RMSE 看收敛趋势,不参与迭代 norm_amp = amplitude / (amplitude.max() + 1e-12) norm_tgt = target / (target.max() + 1e-12) errors.append(np.sqrt(np.mean((norm_amp - norm_tgt) ** 2))) return phase, np.array(errors)这里fftshift的处理是很多人容易写错的地方。numpy 的fft2把零频放在左上角,为了让你最后看到的重建图案在画面中央,我在正向变换后加了一次fftshift;回到全息面时,对应地要先ifftshift再ifft2。生成和验证必须使用同一套 shift 约定,否则重建图会跑到四个角上。
| 参数 | 常用范围 | 说明 | 我的默认值 |
|---|---|---|---|
| iterations | 30 ~ 300 | 前 20~30 次收敛最快,后面收益递减 | 100 |
| seed | 任意整数 | 决定随机初始相位,影响局部最优 | 42 |
| target dtype | float32 | uint8 会截断振幅精度 | float32 |
| 画布尺寸 | 目标尺寸的 2 倍左右 | 抑制边缘溢出,降低频谱泄漏 | 1024 |
3.2 直接跑 GS 为什么是一团噪声:补边、平方根与动态范围
很多人第一次跑上面的函数,拿一张 500×500 的 PNG 直接作为 target,得到的结果非常脏。这不是算法崩了,而是目标图像没有做预处理。
第一,补零。FFT 默认是周期延拓的,目标图顶到画布边缘时,边缘处的不连续会衍射到重建图里。常见做法是把目标放到一个更大的全零画布中央,比如 512×512 的图放到 1024×1024 的背景上。这段代码放在调用gerchberg_saxton之前:
def center_pad(image, canvas_size=1024): h, w = image.shape[:2] canvas = np.zeros((canvas_size, canvas_size), dtype=np.float32) y0, x0 = (canvas_size - h) // 2, (canvas_size - w) // 2 canvas[y0:y0 + h, x0:x0 + w] = image return canvas补边不只是“加个黑框”,它改变了频率采样间隔,给散射噪声提供了更多分布空间,这是全息图生成中最廉价也最有效的清晰度提升手段。
第二,平方根校正。如果输入的 target 是图像灰度,它代表的是强度,也就是重建面上的 |F|²;而 GS 里约束的是振幅 |F|。直接用灰度作为振幅,等于把图像开了平方再显示,结果会整体发灰、高光溢出。常见做法是对灰度开根号:
gray = gray.astype(np.float32) / 255.0 target = np.sqrt(np.clip(gray, 0, 1))第三,随机初始相位。若全息面初始相位全为 0,能量全部集中到零频,重建中心会有一个刺眼的亮斑。seed改成不同的值,重建质量会有明显差异;我一般会在 1、42、2024 三组里挑误差最小的相位图。
4. 彩色与批处理:把单张全息图生成变成可交付的工程
4.1 RGB 三通道全息图生成:拆分、生成、合并
真实场景里几乎不会只处理灰度图。彩色全息图生成的常见做法是按波长分开处理:把图片拆成 R、G、B 三个通道,每个通道单独跑一遍 GS,再决定如何在 SLM 上组合。三种组合方式中,代码层面最简单的是时间复用:红、绿、蓝三张相位全息图轮流显示,配合对应颜色的 LED 快速切换,人眼靠视觉暂留看到彩色。空间复用则是把 SLM 分成三个区域,每个区域生成一个通道的全息图,最后用合色棱镜叠加;这种方式会牺牲分辨率,但不需要同步电路。
下面这段代码把一张普通图片转成三张相位图:
from PIL import Image import numpy as np def channel_to_phase(gray, canvas_size=1024, iterations=80, seed=42): # gray: 单通道强度图,范围 0~1 amp = np.sqrt(np.clip(gray, 0, 1)) canvas = center_pad(amp, canvas_size) phase, _ = gerchberg_saxton(canvas, iterations=iterations, seed=seed) return phase def rgb_to_phases(image_path, canvas_size=1024): img = np.asarray(Image.open(image_path).convert("RGB")).astype(np.float32) / 255.0 phases = [] for c in range(3): phases.append(channel_to_phase(img[:, :, c], canvas_size=canvas_size)) return phases注意center_pad在这里直接复用了上一节的函数;三通道共享同一canvas_size和seed会让三个通道的重建坐标对齐,省去后期配准的麻烦。如果三个通道分辨率不一致,SLM 上会看到明显的颜色错位。
保存相位全息图时,SLM 通常只接受 8 bit 灰度。相位 0 和 2π 是等价的,因此要先对相位取模,再映射到 0~255:
encoded = ((phase % (2 * np.pi)) / (2 * np.pi) * 255).astype(np.uint8) Image.fromarray(encoded).save("holo_r.png")4.2 quanxitu.zip 里的常见批处理流水线
这类以 quanxitu 命名的压缩包,里面的脚本一般不是只处理一张图,而是批量处理序列帧。把流水线固定成四段比较稳妥:读图、预处理、算相位、量化保存。下面这个脚本会把input/下所有 PNG 各生成三张 RGB 相位图,存到holo_out/:
import glob from pathlib import Path out_dir = Path("holo_out") out_dir.mkdir(exist_ok=True) for path in sorted(glob.glob("input/*.png")): name = Path(path).stem phases = rgb_to_phases(path, canvas_size=1024) for idx, channel in enumerate("rgb"): phase = phases[idx] encoded = ((phase % (2 * np.pi)) / (2 * np.pi) * 255).astype(np.uint8) Image.fromarray(encoded).save(out_dir / f"{name}_{channel}.png") print(f"{name} done")glob的sorted不是可选项,而是必要步骤。文件系统天然不保证读取顺序,序列帧一旦乱序,后续做视频播放或动态全息显示时就会花屏。如果你想在 GPU 上加速,可以把gerchberg_saxton里的for循环换成 PyTorch 的torch.fft,同一个结构可以向量化成 batch 版本,但参数学起来比 numpy 慢不少。
5. 重建验证不是肉眼看相位图:量化误差与 3 个排错技巧
5.1 用相同的 FFT 约定验证结果
相位全息图保存成 PNG 后,光看灰色条纹看不出好坏。一定要在代码里做一次数值重建:
def intensity_from_phase(phase): field = np.fft.fftshift(np.fft.fft2(np.exp(1j * phase))) return np.abs(field) ** 2 reconstructed = intensity_from_phase(phase) reconstructed /= reconstructed.max()验证时最容易踩的坑是 shift 约定不一致。生成阶段用了fftshift,验证阶段也必须用同样操作;验证用的phase应该是和gerchberg_saxton返回的原始相位同样尺寸、同样补边处理的数组,而不是保存后的 8 bit 量化版本。量化后的重建会多出零阶衍射斑和条纹噪声,这是正常现象,不代表算法坏了。
5.2 三个最常见的失败模式
第一,重建图像跑到四角或呈镜像分布。先检查生成和验证两端的fftshift/ifftshift是否成对。对偶数尺寸数组,fftshift和ifftshift效果一样;遇到奇数尺寸图,两者正好差一个像素,这会让全息图出现可见的错位。处理图像时我一般强制把输入 resize 成偶数尺寸,从源头避免这问题。
第二,中心亮斑压过图像细节。常见原因是目标图直接用了强度而不是振幅,零频分量过大;或者初始相位取了全零。按 3.2 的做法,先np.sqrt,再换一个随机种子重跑,通常就能把能量从中心摊开。
第三,细节发虚、边缘糊。先看是不是没补边;补了边还糊,就试试降低迭代次数。GS 迭代到后期会把能量集中到高频噪声上,主观视觉反而更差。我一般用errors曲线确定拐点:如果第 40 次之后误差下降不到 5%,就取第 40 次的相位。
如果重建亮度不均匀,可以在这个 GS 骨架上引入加权修正:每次迭代后用“期望强度 / 实际重建强度”更新目标振幅权重,权重公式取w *= (desired / actual) ** 0.6就能显著改善激光加工类场景里的中心过亮问题。这个技巧不改变迭代结构,只是让 GS 从追求全局最小误差改成追求区域均匀性,实作上比换算法更稳。
本文还有配套的精品资源,点击获取