
简介面向深度学习与光学交叉方向学习者这份压缩包提供了一个完整的波前重建系统实现适用于期末大作业、毕业设计或科研入门。系统以卷积神经网络和残差U-Net为核心利用仿真数据训练模型实现从波前传感器数据到畸变波前的准确重建弥补传统Zernike拟合等方法依赖先验知识、对噪声敏感的不足。压缩包共46个文件大小1.83MB包含20个MATLAB脚本负责波前模拟、Zernike系数计算与梯度差分等、7个Python脚本覆盖模型定义、训练、测试与结果输出另有6张结果示意图、模型缓存及相关配置文件目录结构清晰。资源已吸引41人学习说明其具备一定参考价值。通过SH_simulation与SH_resUNet_demo两大模块使用者可复现从模拟波前生成、数据集构建到残差U-Net训练评估的完整链路并在此基础上改进网络结构或扩展真实数据实验是一份兼具教学与工程意义的实战资源。1. 为什么拿到「基于深度学习的波前重建系统.zip」别急着跑先读这三处波前重建在自适应光学、定量相位成像和显微成像里是绕不开的一步。传统 Shack–Hartmann 处理链路要把光斑质心偏移换算成子孔径斜率再解一个规模不小的线性方程组低信噪比时矩阵条件数很差重建结果容易振铃。基于深度学习的波前重建系统把这条链路压缩成一个端到端网络输入光斑图像或质心偏移向量输出 Zernike 系数甚至直接输出二维相位图。标题里带.zip说明它是以项目包形式分发的通常含训练脚本、仿真数据生成器、权重文件和运行说明。适合有光学或成像背景的工程师与研究生想用 PyTorch 快速跑通一个可复现的端到端方案不想从头推导矩阵重建那套数学。2. 从波前传感器到网络输出先定三条路线再谈损失函数深度学习改造波前重建的前提是把物理模型拆清楚。常见的入口是 Shack–Hartmann 波前传感器有时也会直接面对哈特曼光斑图、全息强度图或相位恢复问题。我一般不是一上来就写网络而是先回答一个问题网络到底要输出什么才能接上后续的波前校正或定量分析。2.1 Shack–Hartmann 传感器的传统重建为什么依赖高信噪比在 SH 传感器里微透镜阵列把入射波前切成若干子孔径每个子孔径内的光斑在探测器上形成一个质心。波前存在畸变时光斑质心会相对理想平面波位置偏移。若相位图为 φ子孔径焦距为 f照明波长为 λ那么 x 方向的质心位移近似为Δx f·(λ / 2π)·(∂φ/∂x)再除以像素尺寸就得到像素单位的偏移量。传统重建的第一步是质心提取第二步是把所有子孔径斜率装进一个向量 g然后用模式法解系数a (D^T D αI)^(-1) D^T g其中 D 是响应矩阵每一列是某个 Zernike 模式在子孔径位置上的斜率分布α 是 Tikhonov 正则化系数。这个流程在亮场、高信噪比下很稳但当光斑被噪声淹没、子孔径处有坏点、或者湍流很强导致光斑超出子孔径范围时质心提取出现野值后面矩阵求逆就会把误差放大。深度学习在这种场景下的价值不是替代质心提取而是把「光斑图直接映射到系数」这个高维非线性关系学出来并且在仿真数据里把噪声分布也囊括进去。2.2 深度模型到底替换了哪一段三种常见切法我把基于深度学习的波前重建系统按输入输出切成三种路线。第一种从整幅光斑图回归 Zernike 系数。输入是 SH 传感器拍到的光斑阵列图输出是前 1020 阶 Zernike 系数。这种方式替代了「质心提取 斜率重建 模式拟合」的整条链路鲁棒性最强但网络必须自己学会定位光斑训练数据量需求也最大。第二种从质心偏移向量回归 Zernike 系数。先把光斑质心提取出来得到一个 (2×N) 的向量再用 MLP 或小型 CNN 回归系数。这种方式只替代后端模式重构模型小、训练快适合要部署到嵌入式设备的场景。缺点是前置质心提取出错后面网络再强也救不回来。第三种从单帧强度图重建二维相位图。这通常对应的是定量相位成像或傅里叶叠层成像里的相位恢复问题网络输出一个与输入同尺寸的相位图。这个方向更接近图像翻译任务常用 UNet 结构。我自己的选型建议是如果手里有稳定的 SH 硬件优先做第一种端到端因为它对质心野值天然不敏感如果是纯仿真验证和快速发论文第二种就够用如果目标是显微成像里的定量相位恢复第三种才是主赛道。2.3 输出 Zernike 系数还是二维相位图决定项目复杂度Zernike 系数输出的优点是维度低、物理意义明确且直接能当自适应光学闭环的控制信号。缺点是把波前限定在有限阶模态里存在截断误差。二维相位图输出能保留高空间频率细节但逐像素重建的维度很高训练时需要更强的正则化而且孔径外的无效区域会白白增加模型负担。实际工程里我常做双分支主干网络共享特征一个分支回归低阶 Zernike 系数另一个分支用轻量解码器输出残余相位图。这样闭环控制用系数分支定量分析用相位图分支两不误。不过刚上手时别做这么复杂先从单分支回归 Zernike 系数开始把数据管线和训练流跑通再考虑加输出头。3. 造数据从 Zernike 相位仿真到压缩成可复现的数据集 zip深度学习的波前重建系统最划算的地方就是训练数据可以用物理仿真无限生成不需要真实硬件拍成千上万张光斑图。但仿真代码必须和实验配置对齐否则模型在实验室数据上会直接翻车。这一章我把仿真到打包的过程拆开写。3.1 用 Zernike 多项式生成波前相位图第一步是生成带标签的波前相位。常用做法是随机采样一组 Zernike 系数然后合成相位图。需要注意 Noll 索引顺序不同论文里 Zernike 排列顺序不一样如果从网上东拼西凑代码很容易在顺序上踩坑。下面的 Python 代码生成低阶 Zernike 模式的二维图并把系数加权合成波前相位import numpy as np from math import factorial # 前 15 阶 Noll 序号对应的 (n, m)顺序是固定的 NOLL_TABLE [ (0, 0), (1, 1), (1, -1), (2, 0), (2, -2), (2, 2), (3, -1), (3, 1), (3, -3), (3, 3), (4, 0), (4, 2), (4, -2), (4, 4), (4, -4), ] def zernike_map(n, m, size128, pupil_radius0.95): 生成单个 Zernike 模式返回 (size, size) 的归一化相位图 yy, xx np.mgrid[-1:1:size*1j, -1:1:size*1j] r np.sqrt(xx**2 yy**2) theta np.arctan2(yy, xx) mask r pupil_radius r_safe np.where(mask, r, 0.0) theta_safe np.where(mask, theta, 0.0) R np.zeros_like(r) m_abs abs(m) for k in range((n - m_abs) // 2 1): c (-1)**k * (factorial(n - k) / (factorial(k) * factorial((n m_abs)//2 - k) * factorial((n - m_abs)//2 - k))) R c * r_safe ** (n - 2*k) if m 0: z np.sqrt(n 1) * R elif m 0: z np.sqrt(2 * (n 1)) * R * np.cos(m_abs * theta_safe) else: z np.sqrt(2 * (n 1)) * R * np.sin(m_abs * theta_safe) return z * mask def compose_phase(coeffs, size128): 把前 15 阶系数合成相位图单位是弧度 phase np.zeros((size, size)) for j, a in enumerate(coeffs, start1): n, m NOLL_TABLE[j - 1] phase a * zernike_map(n, m, sizesize) return phase这段代码有两点要注意。第一pupil_radius0.95不是 1.0是为了避免圆形孔径边界正好落在像素边缘时产生半个像素的归属争议第二theta 的零点方向是 x 轴如果后续和光斑仿真或实验装置的对轴方向不一致重建出的像散方向会差 90 度。我习惯在生成每组数据后顺手把合成相位图存一份 PNG 预览用眼睛确认模式方向没问题再大规模生成。3.2 根据相位图模拟 SH 光斑图有了相位图下一步是模拟 Shack–Hartmann 光斑图。这一步的核心是把局部相位梯度换算成光斑位移。下面的代码用两层循环遍历子孔径把每个光斑画成高斯型亮斑并加上可配置的噪声def simulate_spot_image(phase, subap_pitch16, focal_len20e-3, pixel_size5e-6, wavelength0.85e-6, snr10): 由相位图生成一张模拟 SH 光斑图。 phase: 弧度制相位图; subap_pitch: 子孔径间距(像素); focal_len: 微透镜焦距(m); pixel_size: 探测器像元(m); wavelength: 波长(m); snr: 信噪比(倍数)。 h, w phase.shape img np.zeros((h, w), dtypenp.float32) # 子孔径中心按网格排布但要避开边界 centers range(subap_pitch // 2, h - subap_pitch // 2, subap_pitch) gy, gx np.mgrid[-1:1:subap_pitch*1j, -1:1:subap_pitch*1j] for cy in centers: for cx in centers: # 取子孔径内的相位用中心差分求平均斜率 sub phase[cy - subap_pitch//2 : cy subap_pitch//2, cx - subap_pitch//2 : cx subap_pitch//2] dy, dx np.gradient(sub) slope_x dx.mean() slope_y dy.mean() # 位移公式dx_px f * (lambda / 2pi) * slope / pixel_size dx_px focal_len * wavelength / (2 * np.pi) * slope_x / pixel_size dy_px focal_len * wavelength / (2 * np.pi) * slope_y / pixel_size # 高亮斑本身大约 3 像素放在子孔径中心偏移后的位置 sy cy int(round(dy_px)) sx cx int(round(dx_px)) if 0 sy h and 0 sx w: img[sy-1:sy2, sx-1:sx2] \ np.exp(-((gx)**2 (gy)**2) / (2 * 1.0**2)) # 加泊松噪声的近似以 snr 控制信号与噪声的比例 img img / img.max() noise np.random.poisson(img * snr) / snr return np.clip(noise, 0, 1).astype(np.float32)位移公式里最容易出错的地方是slope_x的单位。np.gradient在网格间距为 1 时返回的是“每像素弧度”而公式里的斜率应该是“每米弧度”所以严格说还要除以实际采样间距。如果你在实验里光斑图是真实相机拍到的这条仿真代码的标定就需要用实测标定数据校正一次。我的习惯是仿真阶段先以“相对像素位移”为准把常数全部约掉等部署到实验台再标定绝对尺度。3.3 数据打包把训练集、验证集和配置一起 zip 封存数据集不是一堆.npy堆在文件夹里就完事必须带上能复现的配置。常见目录结构是这样wavefront_dataset/ ├── train/ │ ├── 000000_spot.npy │ ├── 000000_phase.npy │ └── 000000_zernike.npy ├── val/ ├── test/ ├── config.yaml # 波长、焦距、像素、Zernike 阶数 ├── split.json # 数据划分索引 └── sha256sum.txt把目录压成 zip 时我一般用不带换行的绝对路径并在打包后立刻做完整性校验# 打包当前实验目录排除日志和缓存 zip -r wavefront_dataset.zip train val test config.yaml split.json \ -x *.log -x __pycache__/* # 查看包内前若干条路径确认没有把无关文件打进去 unzip -l wavefront_dataset.zip | head -n 30 # 做完整性测试并生成校验值 unzip -t wavefront_dataset.zip sha256sum wavefront_dataset.zip sha256sum.txt这一套下来压缩包解压后就能直接复现训练。有一点血泪经验数据包里的config.yaml比代码还重要。很多 zip 项目过几个月再打开代码能跑但谁都不记得当时用的是 532 nm 还是 850 nm 激光仿真模型跟实验台对不上模型自然失效。4. PyTorch 实现一个能用的 CNN 波前重建模型有多简单很多人以为波前重建的深度学习模型要很复杂实际上一个小型 CNN 就够用。因为 SH 光斑图具有很强的规律性光斑位置本身就是局域信息卷积网络天然适合提取。真正决定效果的往往不是网络深度而是输入归一化、输出编码和损失函数。4.1 输入光斑图、输出 Zernike 系数的轻量 CNN我一般从 15 阶 Zernike 系数回归起步。输入是单通道 128×128 光斑图输出是长度为 15 的向量。网络结构用三层卷积加一个全连接回归头import torch import torch.nn as nn class SpotZernikeNet(nn.Module): def __init__(self, n_zernike15, dropout0.3): super().__init__() self.backbone nn.Sequential( nn.Conv2d(1, 16, 5, padding2), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(16, 32, 5, padding2), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.ReLU(), nn.AdaptiveAvgPool2d(6), ) self.regressor nn.Sequential( nn.Flatten(), nn.Linear(64 * 6 * 6, 128), nn.ReLU(), nn.Dropout(dropout), nn.Linear(128, n_zernike), ) def forward(self, x): return self.regressor(self.backbone(x))这里的关键在AdaptiveAvgPool2d(6)。它把任意输入尺寸的特征图统一池化到 6×6模型就能接受 64×64 或 128×128 的输入不用改结构。第二个关键点是回归头不要加 Tanh 之类的激活函数Zernike 系数的取值范围不稳定末尾直接线性输出让网络自己学幅度。4.2 损失函数必须考虑系数量级差异如果直接对全部 15 个系数的预测值和真值做均方误差低阶像差离焦、像散的量级经常比高阶像差大几倍模型会优先拟合低阶项高阶项被忽略。常见做法是用训练集统计每个系数的标准差把误差归一化后再求平均import numpy as np import torch # 训练集中所有样本的 Zernike 系数shape (N, 15) coeff_sigma np.std(training_coeffs, axis0) coeff_sigma[coeff_sigma 1e-6] 1.0 coeff_sigma_t torch.tensor(coeff_sigma, dtypetorch.float32) def zernike_loss(pred, target): # 逐系数归一化的均方误差 return ((pred - target) ** 2 / coeff_sigma_t).mean()这种损失函数直观且稳定。但注意它有一个坑如果某个高阶系数在训练集里几乎不变标准差接近 0强行归一化会让该系数主导梯度。所以我设置了下限 1e-6。另一个做法是做系数标准化也就是把每个样本的系数减均值除标准差再回归效果等同但推理时要记得反标准化。4.3 训练参数学习率、批量、验证策略的常规取值这套网络参数量不大用 GPU 训练几十个 epoch 就能收敛。我常用的配置是Adam 优化器初始学习率 1e-3 到 3e-4批量大小 64配合余弦退火或 StepLR 在 30 个 epoch 后降学习率。model SpotZernikeNet(n_zernike15) optimizer torch.optim.Adam(model.parameters(), lr3e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50) for epoch in range(50): model.train() for spots, coeffs in train_loader: pred model(spots) loss zernike_loss(pred, coeffs) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() # 每个 epoch 结束后在验证集上算一次相位图残差注意验证集千万别按随机划分了事。波前重建的难点是泛化到更强的像差如果训练和验证都落在同一个湍流强度范围模型可能只是记住了振幅分布。我会按湍流强度分层采样低强度样本放训练中高强度放验证更极端的放测试这样指标才有参考价值。5. 避坑记录复现深度学习波前重建系统最常见的 5 个坑这一章是纯踩坑汇总。以下每一条都是我在给光学项目组排查模型失效时反复遇到的按「现象 → 原因 → 解决」写清楚。5.1 质心坐标单位不统一预测结果系统性偏差现象训练和验证 loss 都正常但把模型放到另一次仿真数据上重建出的像散方向完全不对幅值整体偏小。原因仿真光斑图用的是相对像素位移但训练标签里的 Zernike 系数是按弧度制相位生成的。位移公式里焦距、像素尺寸、波长的单位换算出错导致模型输入分布与标签分布不一致。解决先把位移计算收敛到“像素差异”这种纯几何量所有常数约掉代码里不出现混合单位。具体做法是让位移公式里的focal_len * wavelength / (2 * np.pi * pixel_size)保持为一个标定常数单独写在 config 里不要散落在各处。5.2 孔径掩膜中心错半格低阶像差被污染现象模型单独预测离焦系数时还算准但合成的相位图带明显的一阶像散条纹肉眼可见不对称。原因np.mgrid[-1:1:128j]产生的像素网格关于 0 对称但离散像素计数时孔径掩膜的中心在 (63.5, 63.5)不是 (63, 63)。这个半像素偏移在 Zernike 多项式里等效于引入一个小量 tilt训练时会被模型吸收成系统偏差。解决生成所有 Zernike 模式时先用np.roll或重新构造网格把孔径中心对齐到整数像素坐标size // 2。更稳妥的办法是直接用像素坐标做网格例如x (np.arange(size) - (size - 1) / 2) / (size / 2)保证中心在零点。5.3 用相位图 RMSE 训练却用 Strehl 比做验收现象训练时损失一直接近收敛但逐渐逼近相位图整体偏移导致评价指标不佳。原因相位图在很多优化问题里存在全局常数项和 tilt 自由度MSE 无法分辨无物理意义的整体偏移。如果模型输出的相位图和真值差一个常数MSE 不小但 Strehl 这类指标几乎不受影响反过来模型拟合了伪影但 Strehl 下降用 MSE 看却没什么变化。解决训练损失里显式去掉 piston、tip、tilt 三个分量再算 MSE或者验证时把评价指标和训练目标统一起来。我自己的默认做法是训练用 Zernike 系数回归评价用相位图残差 RMSE 和 Strehl 同时报两套指标放一张表里看。5.4 输入归一化参数没有随项目打包现象模型在仿真验证集上跑得很好一接到实验台相机数据输出直接爆掉预测系数比正常值大两个量级。原因训练时对输入光斑图做了(x - mean) / std的归一化但没把这两组统计量存进权重包。测试数据范围完全不同仍按训练时的 mean/std 处理输入分布失调。解决把归一化参数写进模型配置文件或者干脆在模型内部用一个输入预处理层持有均值和方差。这样导出 TorchScript 或 ONNX 时归一化逻辑跟着模型走部署端不需要再手动处理。5.5 zip 解压后路径过长或文件名被改动现象Windows 下解压后数据加载脚本报目录不存在检查发现系统把一部分文件路径截断了。原因项目 zip 内部目录层级很深加上训练数据文件名带时间戳总路径长度超过 Windows 默认 260 字符限制另外某些解压工具会自作主张展开嵌套目录把文件挪到别处。解决解压前先看unzip -l列出的最深层路径判断是否接近 260 字符。Windows 上解压到一个短路径根目录比如C:\wf\而不是放在桌面层层嵌套。Linux 上解压后先清理__MACOSX等系统残留再跑数据脚本。6. 最后一关把模型从 zip 里验出来、导出去网络训练完不是终点真正的考验是“盲测和部署”这两件事。我见过太多项目在训练集里指标漂亮换一套湍流仿真就现原形。所以我的习惯是所有验证都在独立的盲测集上做。6.1 盲测不只看 loss要看相位残差重新生成一批训练流程中没见过的湍流样本湍流强度跨度加大Zernike 系数取值范围比训练集宽 30%。模型推理得到系数后合成预测相位图和真值相位图做差分pred_phase compose_phase(pred_coeffs[i].cpu().numpy(), size128) true_phase compose_phase(target_coeffs[i].cpu().numpy(), size128) residual pred_phase - true_phase residual residual * pupil_mask # 只统计孔径内 rmse np.sqrt(np.mean(residual**2))如果盲测 RMSE 比验证集高一半以上说明模型过拟合了训练参数分布不要直接投向生产环境。6.2 导出 TorchScript 或 ONNX让部署端不再依赖训练代码训练完的模型必须能脱离训练框架运行否则实验台的采控程序很难调用。PyTorch 导出有两种常用方式TorchScript 适合 PyTorch 星系内的部署ONNX 适合跨语言调用。导出前记得把归一化层一起带上scripted torch.jit.script(model.cpu()) scripted.save(wavefront_recon.pt)导出 ONNX 时固定输入尺寸为 1×1×128×128dummy torch.randn(1, 1, 128, 128) torch.onnx.export(model.cpu(), dummy, wavefront_recon.onnx, opset_version17, input_names[spot], output_names[zernike])6.3 一份最有效的验证检查表我做这类项目时最后收尾流程固定是五步先看包内 config 是否与实验参数一致再在原始光斑图预览上确认子孔径网格和孔径掩膜然后跑盲测集看相位残差云图而不是只看数字接着在 ONNX Runtime 或 TorchScript 上做一次与 PyTorch 前向结果的数值一致性比对最后把训练随机种子、归一化统计量和模型 md5 一起写进备注文件封存进 zip。我第一次复现别人的波前重建 zip 项目时在坐标单位上翻了车整整排查了两天。后来养成一个习惯凡是拿到手的项目包第一件事先创建一份config/unit.yaml把波长、像素尺寸、子孔径间距、微透镜焦距、Zernike 阶数全部抄进去再开始动网络。这个习惯帮我省下的排错时间远比写网络的时间更多。希望上面这些坑点和检查流程能帮你在自己的波前重建系统上少走几步弯路。本文还有配套的精品资源点击获取