简介这份资源面向计算机、人工智能及相关专业的在校学生与教师提供一套基于Unet网络实现天文图像降噪的完整Python项目源码适合用作课程设计、毕业设计或深度学习入门实践。项目思路清晰先在原始天文图像上叠加噪声将噪声图作为训练集、原图作为标签借助Unet完成端到端降噪训练训练平台为Google Colab并配有数据生成脚本用于筛选黑色率低于阈值的样本。压缩包共209个文件以png图像、npy数据文件为主另含py脚本、ipynb笔记本、xml与说明文档等整体约35.94MB目录结构便于按数据、代码、结果分块查阅。目前已有383人学习下载。代码均经测试运行成功附带项目说明与README读者可据此复现训练流程、理解噪声构造与网络搭建细节并在此基础上修改以适配其他图像复原任务。1. 天文图像降噪为什么不能直接套自然图像那套从一张满是热噪的深空帧说起天文图像的降噪和日常修图完全是两码事。你拿手机拍的夜景噪点主要是读出噪声和压缩伪影用现成的降噪模型跑一遍肉眼看着干净就行。但天文图像不一样它的信号极其微弱——一个遥远星系的光子可能只有几个到几十个计数而传感器暗电流、热噪声、宇宙射线打在CCD上形成的亮斑全都混在一起。你如果直接拿自然图像训练的降噪网络往上套结果往往是把暗弱的星点当噪声抹掉或者把背景压成一块死板的平滑区域信噪比没提升多少科学信息先丢了。这就是基于Unet网络实现对天文图像的降噪处理这个方向要解决的问题。Unet本身是为生物医学分割设计的编码器-解码器结构跳跃连接能把浅层的高频细节直接送到解码端这个特性恰好适合天文图像——星点、星系边缘这些高频信息需要保留而背景噪声需要压制。用Python搭一套Unet降噪流程配合天文图像特有的噪声模型做训练是目前业余天文爱好者和天文数据预处理环节里比较务实的做法。这篇文章面向的是手里有FITS或TIFF格式天文帧、想用深度学习做降噪但不知道从哪下手的人从数据准备、网络搭建、训练参数到推理验证一步步拆开讲。2. 天文图像降噪的数据准备与噪声建模别拿RGB图直接喂网络2.1 天文图像的格式转换与归一化天文图像最常见的格式是FITS每个像素值是16位或32位整数代表光子计数或ADU。直接把这些值送进网络会出问题——数值范围可能从0到65535而网络权重初始化是按小数值设计的。常见做法是先转成浮点再做归一化。import numpy as np from astropy.io import fits def load_fits_as_float(path): # 读取FITS文件取第一个HDU的数据 with fits.open(path) as hdul: data hdul[0].data.astype(np.float32) # 用百分位数裁剪极端值避免宇宙射线亮斑拉偏归一化 low, high np.percentile(data, [0.5, 99.5]) data np.clip(data, low, high) # 归一化到0-1 data (data - low) / (high - low 1e-8) return data这段代码的关键在百分位裁剪。天文帧里经常有宇宙射线打出的亮斑像素值可能是正常星点的几十倍如果直接用min-max归一化整个图像的动态范围会被这几个坏点吃掉。用0.5%和99.5%分位裁剪能保住暗弱信号的同时去掉极端值。归一化后的图像再切成256×256或512×512的patch太小了星点周围上下文不够太大了显存吃不消。2.2 噪声建模合成训练对还是真实配对Unet降噪是有监督任务需要成对的干净图像和噪声图像。天文领域几乎没有真正的干净参考帧所以训练数据通常靠合成。思路是拿一张质量较好的天文图像当干净参考人为加入符合传感器特性的噪声生成噪声图像。def add_astronomical_noise(clean, read_noise5.0, dark_current0.02, exposure300): # 泊松噪声模拟光子到达的随机性信号越弱噪声越明显 signal clean * exposure poisson_noise np.random.poisson(signal).astype(np.float32) # 高斯噪声模拟读出噪声单位是电子 read np.random.normal(0, read_noise, clean.shape).astype(np.float32) # 暗电流噪声也服从泊松分布与曝光时间成正比 dark np.random.poisson(dark_current * exposure, clean.shape).astype(np.float32) noisy poisson_noise read dark # 重新归一化回0-1 noisy noisy / (exposure 1e-8) return np.clip(noisy, 0, 1)参数说明read_noise是读出噪声典型CCD在3到10电子之间dark_current是暗电流制冷CCD可以低到0.01电子/像素/秒exposure是曝光时间单位秒。这三个参数决定了噪声的强度和分布形态。如果你的实际数据来自CMOS相机读出噪声可能更低但热噪声更明显需要相应调整。合成数据的质量直接决定网络学到什么——噪声模型太简单网络在真实数据上就翻车。注意不要用自然图像数据集加高斯噪声来训练天文降噪网络。自然图像的纹理和天文图像的星点-背景结构差异太大网络会学到错误的先验。3. Unet网络搭建跳跃连接怎么改才不丢星点3.1 基础Unet结构在降噪任务上的适配标准Unet是四层下采样、四层上采样每层两个3×3卷积加ReLU。用于降噪时输入输出都是单通道或三通道图像最后一层不加softmax直接用线性输出。但直接搬原版Unet有个问题下采样用最大池化会丢失暗弱星点的位置信息上采样用转置卷积容易产生棋盘伪影。import torch import torch.nn as nn class DownBlock(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.LeakyReLU(0.1, inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.LeakyReLU(0.1, inplaceTrue) ) # 用步长为2的卷积代替最大池化保留更多位置信息 self.downsample nn.Conv2d(out_ch, out_ch, 3, stride2, padding1) def forward(self, x): feat self.conv(x) return feat, self.downsample(feat) class UpBlock(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() # 用双线性插值上采样避免转置卷积的棋盘伪影 self.up nn.Upsample(scale_factor2, modebilinear, align_cornersTrue) self.conv nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.LeakyReLU(0.1, inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.LeakyReLU(0.1, inplaceTrue) ) def forward(self, x, skip): x self.up(x) # 跳跃连接编码器特征与解码器特征拼接 x torch.cat([x, skip], dim1) return self.conv(x)这里有两个改动值得说。第一下采样用stride2的卷积而不是MaxPool卷积下采样保留了更多空间信息对暗弱星点更友好。第二上采样用双线性插值而不是转置卷积转置卷积在低分辨率特征图上容易产生周期性伪影天文图像背景本来就暗这些伪影会非常显眼。LeakyReLU的负斜率设0.1比ReLU更适合低信噪比输入避免暗区神经元完全死掉。3.2 损失函数选择MSE会让星点变糊降噪任务最常用的损失是MSE或L1。但天文图像有个特点星点只占极少像素背景占绝大多数。如果用MSE网络会倾向于把所有像素往背景均值拉星点被平滑掉。常见做法是MSE和L1加权组合再加一个梯度损失项。class HybridLoss(nn.Module): def __init__(self, alpha0.5, beta0.1): super().__init__() self.alpha alpha self.beta beta self.mse nn.MSELoss() self.l1 nn.L1Loss() def forward(self, pred, target): # MSE保证整体亮度一致 loss_mse self.mse(pred, target) # L1对星点等稀疏结构更敏感 loss_l1 self.l1(pred, target) # 梯度损失用Sobel算子提取边缘约束星点不被抹平 sobel_x torch.tensor([[-1,0,1],[-2,0,2],[-1,0,1]], dtypetorch.float32).view(1,1,3,3).to(pred.device) sobel_y sobel_x.transpose(2,3) grad_pred_x torch.nn.functional.conv2d(pred, sobel_x, padding1) grad_pred_y torch.nn.functional.conv2d(pred, sobel_y, padding1) grad_target_x torch.nn.functional.conv2d(target, sobel_x, padding1) grad_target_y torch.nn.functional.conv2d(target, sobel_y, padding1) loss_grad self.l1(grad_pred_x, grad_target_x) self.l1(grad_pred_y, grad_target_y) return self.alpha * loss_mse (1 - self.alpha) * loss_l1 self.beta * loss_gradalpha控制MSE和L1的权重beta控制梯度损失强度。经验值alpha0.5、beta0.1在多数天文降噪任务里表现稳定。如果发现输出星点偏暗把beta调大到0.2如果背景出现块状伪影把alpha调大到0.7。这个损失函数比纯MSE训练慢一些但星点保真度明显更好。4. 训练与推理显存不够、过拟合、推理速度慢怎么破4.1 训练配置与显存优化天文图像patch通常取256×256batch size在8到16之间。如果显存不够有三个方向可以调一是把patch降到128×128但要注意星点周围上下文是否足够二是用梯度累积模拟大batch三是把Unet的通道数从64起步降到32起步。from torch.utils.data import DataLoader, Dataset import torch.optim as optim class AstroDataset(Dataset): def __init__(self, clean_patches, noise_params): self.clean clean_patches self.noise_params noise_params def __len__(self): return len(self.clean) def __getitem__(self, idx): clean self.clean[idx] noisy add_astronomical_noise(clean, **self.noise_params) # 转成tensor增加通道维度 clean_t torch.from_numpy(clean).unsqueeze(0).float() noisy_t torch.from_numpy(noisy).unsqueeze(0).float() return noisy_t, clean_t # 训练循环关键部分 model UNet(in_ch1, out_ch1, base_ch32).cuda() optimizer optim.Adam(model.parameters(), lr1e-4) scheduler optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience5, factor0.5) criterion HybridLoss(alpha0.5, beta0.1) for epoch in range(100): model.train() for noisy, clean in dataloader: noisy, clean noisy.cuda(), clean.cuda() pred model(noisy) loss criterion(pred, clean) optimizer.zero_grad() loss.backward() # 梯度裁剪防止梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() scheduler.step(loss)学习率1e-4配Adam是稳妥起点。ReduceLROnPlateau在验证损失不降时自动降学习率patience5意味着连续5个epoch没改善就降一半。梯度裁剪max_norm1.0对Unet这种深网络很有必要天文图像噪声强梯度容易爆。base_ch32比原版Unet的64省一半显存降噪任务对通道数没那么敏感32通常够用。4.2 推理阶段的拼接与后处理训练时用的是patch推理时整张天文图像可能远大于patch尺寸。常见做法是滑窗推理再拼接但直接拼会在边界出现接缝。用重叠滑窗加余弦窗加权融合能消除接缝。def infer_full_image(model, image, patch_size256, overlap32): model.eval() h, w image.shape output np.zeros_like(image) weight np.zeros_like(image) # 生成余弦窗边界处权重低中心高 window np.outer(np.hanning(patch_size), np.hanning(patch_size)) step patch_size - overlap with torch.no_grad(): for i in range(0, h - patch_size 1, step): for j in range(0, w - patch_size 1, step): patch image[i:ipatch_size, j:jpatch_size] patch_t torch.from_numpy(patch).unsqueeze(0).unsqueeze(0).float().cuda() pred model(patch_t).cpu().squeeze().numpy() output[i:ipatch_size, j:jpatch_size] pred * window weight[i:ipatch_size, j:jpatch_size] window # 归一化避免边界权重不足导致变暗 output output / (weight 1e-8) return outputoverlap取patch_size的1/8到1/4256的patch用32或64重叠都行。hanning窗在边界处趋近0中心趋近1加权融合后接缝自然消失。如果图像尺寸不能被step整除边缘区域需要单独处理或者padding后再裁切。提示推理前把模型设为eval模式并禁用梯度否则BatchNorm的统计量会随推理数据变化输出不稳定。5. 避坑与排查天文降噪训练里最容易翻车的五件事5.1 损失降了但星点没了现象训练损失稳定下降验证集PSNR也不错但肉眼一看星点全糊了背景像被磨皮。原因MSE损失主导网络学会了输出背景均值星点这种稀疏高频信号被当作异常值压制。解决把HybridLoss里alpha降到0.3提高L1和梯度损失权重或者在损失里对星点区域加权用目标图像的梯度幅值当权重图。5.2 合成噪声和真实噪声差距大现象合成数据上PSNR到35dB换真实FITS帧一跑噪声没降多少还引入了块状伪影。原因合成噪声模型太理想真实传感器还有固定模式噪声、坏列、非线性响应。解决从真实数据里估计噪声参数用暗帧和偏置帧标定读出噪声和暗电流或者在合成时加入随机坏列和固定模式噪声。更彻底的做法是用Noise2Noise思路拿同一目标的多帧短曝光做训练对。5.3 显存溢出但不想降patch现象batch size8、patch256时CUDA out of memory。原因Unet中间层特征图通道数大256×256×256的特征图占显存很凶。解决用混合精度训练torch.cuda.amp自动把部分计算转成float16显存能省30%到40%或者用梯度检查点用计算时间换显存。这两个方法可以叠加用。5.4 推理速度慢到没法批量处理现象单张4K天文图像推理要几十秒一晚上拍几百帧根本跑不完。原因滑窗推理逐patch串行GPU利用率低。解决把滑窗切成batch一起送GPU一次推理几十个patch或者用TorchScript导出模型去掉Python开销。如果还慢考虑把Unet通道数减半降噪任务对模型容量要求没那么高。5.5 训练集和验证集来自同一晚同一目标现象验证损失比训练损失还低但换一晚的数据就崩。原因同一晚的数据噪声特性、大气透明度、目标高度角都相似模型学到了当晚的特定噪声模式而不是通用降噪能力。解决按观测夜划分训练集和验证集不同夜的数据噪声统计不同验证结果才有参考意义。如果数据量不够至少按目标区域划分避免同一片天区同时出现在训练和验证里。6. 用真实天文帧验证降噪效果从PSNR到星等测量的落地检查训练完模型PSNR和SSIM只是参考真正要验证的是降噪后天文信息有没有保住。我一般会做三步检查。第一步拿一张有已知星等参考的星场降噪前后分别做孔径测光看星等偏差有没有超过0.05等。如果偏差大说明降噪改变了星点流量模型不可用。第二步看背景RMS。降噪后背景标准差应该明显下降但如果降得太狠背景直方图会变成一根尖峰说明噪声被过度平滑暗弱天体也被抹掉了。第三步目视检查星系边缘和星点衍射环这些结构如果变模糊说明网络的高频保留能力不够。from photutils.aperture import CircularAperture, aperture_photometry def check_photometry(original, denoised, positions, radius5): # 对原始和降噪图像分别做孔径测光 apertures CircularAperture(positions, rradius) phot_orig aperture_photometry(original, apertures) phot_deno aperture_photometry(denoised, apertures) # 计算星等差异 flux_orig phot_orig[aperture_sum] flux_deno phot_deno[aperture_sum] mag_diff -2.5 * np.log10(flux_deno / flux_orig) return mag_diffmag_diff的绝对值如果普遍小于0.05说明降噪对测光影响可接受。如果某些星点偏差大检查是不是落在patch边界上重叠融合没做好。另外降噪后的图像如果还要做天体测量星点质心位置偏移也要检查一般要求小于0.1像素。我自己的习惯是每次训练完新模型先拿三张不同夜的验证帧跑一遍测光检查再决定要不要上真实数据处理流程。这个步骤花不了多少时间但能避免把有系统偏差的模型用到科学产出里。希望帮到你。本文还有配套的精品资源点击获取