PyTorch图像去雨实战:从合成数据到模型部署全流程解析
2026/9/15 19:50:32 网站建设 项目流程

第一次把图像去雨模型跑通,是在一个凌晨。我盯着输出图看了很久,雨纹确实淡了,但树叶纹理全糊成一团,像上了一层磨砂滤镜。那时候我才意识到,图像去雨这活儿,远不是"找个网络、丢数据、train"这么简单。后来我花了一整周时间把合成数据、网络结构、损失函数和评测方式全部返工,才算摸清门道。

网上关于图像去雨的PyTorch实现代码不少,但大多分两类:一类是直接搬公开仓库,跑个实验就完事;另一类是站在很高的角度讲论文里的数学推导,真到自己写代码时还是会卡住。这篇博文不一样,我把自己完整跑通的实现过程全部写出来:从合成带雨训练集开始,到设计一个参数少但可用的卷积网络,再到训练、评测、推理部署,每一段代码都带详细注释。整套流程全部使用PyTorch实现,照着敲就能跑,适合刚入门底层图像恢复任务、以及想做去雨方向课程设计或毕设的同学参考。

1. 图像去雨到底在解决什么问题,以及为什么用PyTorch重写实现

1.1 雨纹干扰的本质:它不是普通噪声,而是有方向的结构信号

图像去雨属于底层图像恢复任务,和去噪、去模糊、去雾是同一个大类。很多人把雨纹当成一种"密度较高的椒盐噪声"来处理,这是最大的误区。雨纹在成像上的表现是:细长的、有一定方向和透明度的高亮线条,它们在空间上稀疏但结构感极强,且不同景深位置的雨纹模糊程度还不一样。

正是这种结构化特征,让通用去噪模型在雨图上表现很差。高斯去噪模型假设噪声在像素间是独立的,而雨纹的像素点之间有强烈的空间相关性——相邻像素在方向和亮度上高度一致。换句话说,通用去噪会把雨纹当成"需要保留的边缘细节",结果越去雨,雨纹越清晰,或者把真正的物体边缘给磨掉。这也是为什么需要专门设计面向雨纹结构的小网络,而不是套一个大号的通用CNN。

另外一个容易被忽略的点是,雨纹往往叠加在背景纹理之上,这给网络出了道难题:网络要区分"高亮的雨线"和"高亮的物体边缘",两者在局部形态上可能很接近。解决这个问题的思路之一是扩大感受野,让网络能在更大的上下文里判断一个像素是否属于雨线。具体怎么做,会在第三章的网络设计里展开。

1.2 开发环境与依赖版本说明

我选PyTorch来实现这套代码,原因很实际:动态图调试太方便了,网络中间任何一层的输出都能直接print出来看,而且数据加载、GPU训练、模型导出这套生态非常成熟,社区里找任何一个小模块都有现成参考。

我的环境配置如下,仅供对照参考:

  • Python 3.9
  • PyTorch 1.12.1(CUDA 11.3版本,CPU也能跑,只是慢)
  • torchvision 0.13.1
  • opencv-python 4.6.0
  • numpy 1.23.0
  • tqdm 4.64.0
import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import Dataset, DataLoader import cv2 import numpy as np import os import random from tqdm import tqdm # 固定随机种子,保证每次实验结果可复现 def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) set_seed(42)

注意:OpenCV在读取图片时返回的是BGR通道顺序,而PyTorch训练时一般用RGB。这个坑会导致训练时颜色错乱、推理时输出偏色,所以我在代码里统一用cv2.cvtColor(img, cv2.COLOR_BGR2RGB)做转换。

2. 训练数据从零合成:模拟雨纹背后的成像逻辑

2.1 为什么没直接下载公开数据集,而是自己合成雨图

公开的图像去雨数据集确实不少,比如Rain100L、Rain100H、DID-MDN配套数据,但实际用起来有几个烦人的问题:文件格式不统一,有的是.mat文件,有的是多张图打包压缩;每张图的尺寸差异大,需要额外写脚本统一处理;部分数据集下载源不稳定。

更关键的是,自己合成雨图能精确控制雨纹密度、方向、长度和透明度,后面做消融实验时可以单独验证"模型对斜向雨的鲁棒性"或者"雨纹太密集时效果如何下降"。这种控制力是现成数据集给不了的。

所以我采用了最直观的合成方案:在干净图像上随机绘制多方向、多长度的半透明线条来模拟雨纹。这个方案不完全等同于真实雨图成像,但作为训练数据已经足够让模型学到"什么是雨纹、什么不是雨纹"。

2.2 雨纹生成算法与带注释的完整代码

雨纹模拟的核心逻辑分三步:生成随机方向与长度的线段、将线段绘制到掩码图上、对掩码做高斯模糊后与原图融合。高斯模糊这一步很关键,直接画硬线条会得到边缘锐利的假雨纹,网络学到的特征和真实雨纹不匹配。

def add_rain(image, num_drops=30, max_length=30, angle_range=(-30, 30)): """ 在干净图像上合成雨纹 image: HWC格式RGB图像,取值范围0-255,dtype=uint8 num_drops: 雨线数量 max_length: 雨线最大像素长度 angle_range: 雨线角度范围[-30, 30],模拟自然降雨的倾角 """ h, w, _ = image.shape # 雨线掩码图,先画线再做高斯模糊 rain_mask = np.zeros((h, w, 3), dtype=np.float32) for _ in range(num_drops): # 随机选择雨线的起点 x0 = np.random.randint(0, w) y0 = np.random.randint(0, h) # 随机雨线长度,控制在max_length的30%-100%之间 length = np.random.randint(int(max_length * 0.3), max_length + 1) # 随机雨线倾角,在给定的范围内 angle = np.deg2rad(np.random.uniform(angle_range[0], angle_range[1])) # 计算终点:雨线通常向下倾斜,所以dx和dy都取正值 dx = int(length * np.sin(angle)) dy = int(length * np.cos(angle)) x1 = min(w - 1, max(0, x0 + dx)) y1 = min(h - 1, max(0, y0 + dy)) # 在掩码图上画一条粗度为1的白色线段 # 后续通过高斯模糊把线变成有厚度的雨带 cv2.line(rain_mask, (x0, y0), (x1, y1), (1.0, 1.0, 1.0), 1) # 高斯模糊让雨线变得柔和,模拟雨滴在镜头中的散射效果 rain_mask = cv2.GaussianBlur(rain_mask, (5, 5), 1.0) # 雨纹是亮度增强,所以是加法而非乘法 # alpha是雨纹强度系数,取0.6-1.0之间的随机值 alpha = np.random.uniform(0.6, 1.0) rainy_image = image.astype(np.float32) + alpha * rain_mask * 255.0 rainy_image = np.clip(rainy_image, 0, 255).astype(np.uint8) return rainy_image

合成雨图的几个参数需要说下:num_drops控制雨的密度,太少了模型学不到东西,太多了模型会把背景细节当成雨纹一并抹掉,我实测在30左右比较平衡;max_length控制雨线长度,这是和真实雨图差别最大的参数,真实图中近处雨滴的轨迹长且清晰,远处则短而模糊,但作为基础版本,固定长度范围已经够用了。

2.3 Dataset与DataLoader实现:数据增强的细节决定训练稳定性

有了单张图合成雨纹的函数,接下来把它们串成PyTorch标准的Dataset接口。这一步很多人会忽略数据增强的作用:这里做了随机裁剪和随机水平翻转,理由很简单,如果让网络每轮都看到完整的原始尺寸图,它很容易记住某一批图的位置特征,导致验证集效果虚高,泛化能力却很一般。

class RainDataset(Dataset): """ 从干净图像目录读取图片,在训练时动态生成雨图 """ def __init__(self, clean_img_dir, patch_size=224, is_train=True): self.clean_img_dir = clean_img_dir self.image_paths = [ os.path.join(clean_img_dir, f) for f in os.listdir(clean_img_dir) if f.endswith(('.png', '.jpg', '.jpeg', '.bmp')) ] self.patch_size = patch_size self.is_train = is_train def __len__(self): return len(self.image_paths) def __getitem__(self, idx): # 读取干净图像并转为RGB img_path = self.image_paths[idx] clean = cv2.imread(img_path, cv2.IMREAD_COLOR) if clean is None: raise ValueError(f"图片读取失败: {img_path}") clean = cv2.cvtColor(clean, cv2.COLOR_BGR2RGB) if self.is_train: # 训练时:随机裁剪到patch_size,并随机水平翻转 h, w, _ = clean.shape if h < self.patch_size or w < self.patch_size: clean = cv2.resize(clean, (self.patch_size, self.patch_size), interpolation=cv2.INTER_AREA) else: x = np.random.randint(0, w - self.patch_size + 1) y = np.random.randint(0, h - self.patch_size + 1) clean = clean[y:y + self.patch_size, x:x + self.patch_size] if np.random.rand() > 0.5: clean = cv2.flip(clean, 1) # 合成雨图 rainy = add_rain(clean) # BGR顺序转RGB,这里已经转好了 # 转成Tensor并归一化到[0, 1]区间 clean_tensor = torch.from_numpy(clean.transpose(2, 0, 1)).float() / 255.0 rainy_tensor = torch.from_numpy(rainy.transpose(2, 0, 1)).float() / 255.0 return rainy_tensor, clean_tensor
# 使用方式 train_dataset = RainDataset(clean_img_dir="./data/clean_images/train", patch_size=224, is_train=True) val_dataset = RainDataset(clean_img_dir="./data/clean_images/val", patch_size=224, is_train=False) train_loader = DataLoader(train_dataset, batch_size=8, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_dataset, batch_size=8, shuffle=False, num_workers=2, pin_memory=True)

提示:验证时我没有做随机裁剪而是直接resize到224x224,是为了保证每次验证的输出尺寸一致,从而计算稳定的PSNR和SSIM。如果你希望验证结果更严谨,也可以使用滑窗裁剪再把所有结果拼回来。

3. 网络模型设计:让网络学习“雨纹残差”而不是直接生成背景

3.1 残差学习的出发点:擦黑板思维

网络结构上,我选择残差学习的思路。直接让网络输出干净背景图当然可以,但这对网络要求太高:它需要同时完成"识别雨纹区域"和"生成被遮挡的背景纹理"两件事,尤其是背景生成部分,本质上是一个图像重建问题,需要网络有很强的生成能力,计算开销和过拟合风险都会上升。

残差学习的思路完全不同。雨图可以近似看成"干净背景 + 雨纹叠加",那么从雨图中减去雨纹就得到干净背景。网络只需要预测雨纹部分,最后用输入减去预测结果即可。这就像擦黑板:我们不需要完整重画黑板上被粉笔字覆盖的内容,只需要把粉笔字擦掉,底下的内容自然就露出来了。

好处是显而易见的:网络要预测的目标从"整张真实图像"变成一个稀疏的、结构化的雨纹图,训练难度大幅下降。这个思路在图像去噪中同样有效——预测噪声残差比预测干净图像更容易收敛。

3.2 网络结构与带注释的PyTorch代码

网络主体是一个4层卷积堆叠的小网络。我没有选择很深的结构,原因有两个:去雨任务中雨纹是局部特征,感受野不需要覆盖整幅图像;层数太多容易丢失高频细节,而高频细节里有大量的背景纹理。

class DerainNet(nn.Module): """ 一个用于图像去雨的简单卷积基线网络 输入: 3通道雨图 (B, 3, H, W) 输出: 3通道去雨图 (B, 3, H, W) """ def __init__(self): super(DerainNet, self).__init__() # 四层卷积,通道数从3 -> 32 -> 32 -> 32 -> 3 # 卷积核大小统一用3x3,padding=1保证特征图尺寸不变 self.net = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(32, 32, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(32, 32, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(32, 3, kernel_size=3, padding=1), ) # kaiming初始化能保证前向传播时激活值不会快速消失或膨胀 for m in self.modules(): if isinstance(m, nn.Conv2d): nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') if m.bias is not None: nn.init.constant_(m.bias, 0) def forward(self, x): # 网络只学习雨纹残差 residual = self.net(x) # 输入减去残差得到去雨结果 return x - residual

感受野计算一下:三层3x3卷积堆叠后最远能看到7x7的区域,这个大小对捕获一条宽1-2像素、长十几像素的雨线来说刚刚够用。如果你发现模型对长雨线效果不好,可以再加一层卷积,感受野会扩大到9x9。

注意:网络最后没有任何激活函数。因为残差值既可能是正数(雨纹增亮区域),也可能是负数(如果雨纹带轻微阴影效果),线性输出比ReLU更合理。

4. 训练循环与损失设计:精度提升的关键细节

4.1 损失函数:从MSE到MSE加SSIM的组合策略

训练图像去雨模型最常用的损失函数是MSE(均方误差)。原因很直接:MSE和PSNR在数学上是对应关系,PSNR越高代表像素级误差越小,而MSE越小PSNR越高。

但只用MSE有个明显问题:它假设每个像素是独立的,不考虑像素间的结构关系。训练后期你会发现PSNR在涨,但视觉上图像变"油"了——细节被抹平,边缘不够锐利。这是因为MSE对局部结构的轻微错位惩罚不够大。

我的建议是加一个简单的SSIM损失作为辅助,SSIM衡量的是两个图像块在亮度、对比度和结构三个维度的相似度,能迫使网络保留更多结构信息。不过SSIM计算代价略高,而且在不同实现里公式细节有差异,初学者可以先只用MSE跑通基线,再逐步加入结构损失。

4.2 训练主循环与Checkpoint保存:每个环节都有代码注释

# 损失函数:MSE,这里不直接定义,而是在训练循环里调用 criterion = nn.MSELoss() model = DerainNet().cuda() optimizer = optim.Adam(model.parameters(), lr=1e-3) scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50, eta_min=1e-6) def train_one_epoch(model, loader, criterion, optimizer, epoch): model.train() total_loss = 0.0 pbar = tqdm(loader, desc=f"Epoch {epoch+1}") for rainy, clean in pbar: rainy = rainy.cuda() clean = clean.cuda() # 前向传播 output = model(rainy) # 计算MSE损失 loss = criterion(output, clean) # 梯度清零,反向传播,更新权重 optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() pbar.set_postfix(loss=loss.item()) return total_loss / len(loader) def validate(model, loader): """ 验证函数:计算模型在验证集上的PSNR 这里不开梯度,节省显存并加速 """ model.eval() total_psnr = 0.0 with torch.no_grad(): for rainy, clean in loader: rainy = rainy.cuda() clean = clean.cuda() output = model(rainy) # PSNR计算是基于0-1范围的均方误差 mse = torch.mean((output - clean) ** 2, dim=[1, 2, 3]) psnr = 10 * torch.log10(1.0 / (mse + 1e-10)) total_psnr += psnr.sum().item() return total_psnr / len(loader.dataset)

然后是完整的训练脚本,我把模型保存、学习率调整都写进去:

epochs = 60 best_psnr = 0.0 for epoch in range(epochs): train_loss = train_one_epoch(model, train_loader, criterion, optimizer, epoch) val_psnr = validate(model, val_loader) # 学习率按照余弦退火调度调整 scheduler.step() print(f"Epoch {epoch+1}, Loss: {train_loss:.6f}, Val PSNR: {val_psnr:.2f}") # 保存验证集上PSNR最高的模型 if val_psnr > best_psnr: best_psnr = val_psnr torch.save(model.state_dict(), "./best_model.pth") print(f"保存新模型,PSNR: {val_psnr:.2f}")

4.3 调参实战经验:几个影响训练质量的隐性因素

第一,batch size的选择。我用8,再大当然可以,但8GB显存的显卡跑224x224输入时,batch size建议不超过16。如果显存不够,优先降低batch size而不是降低图像分辨率,因为裁切尺寸变小会让网络看到的信息变少,泛化性下降。

第二,学习率。用Adam时初始学习率1e-3是比较稳的,但到了40个epoch之后,如果发现loss在缓慢震荡而PSNR不再上升,通常不是模型问题,而是学习率太大了。我用的余弦退火调度器会平滑地把学习率降到1e-6,整体训练曲线很稳定。

第三,如果训练集只有几十张图,网络很容易过拟合。我的经验是,每个epoch做随机裁剪和翻转,等效于把训练集扩大了很多倍,但如果源图太少,还是需要换用ImageNet预训练权重做迁移学习,单纯靠加大增强力度效果有限。

提示:检查过拟合的方法很简单,打印训练集和验证集的PSNR。如果训练集PSNR持续涨、验证集不涨甚至下跌,基本就是过拟合,优先考虑增加数据增强力度或者缩小网络容量。

5. 评测指标与结果可视化:PSNR高不等于效果好

5.1 PSNR和SSIM的纯PyTorch实现,避开sklearn依赖

很多评测代码依赖skimage.metrics.peak_signal_noise_ratiostructural_similarity,但我更推荐自己写一个轻量版本,一来不依赖额外库,二来能彻底理解指标的计算原理。

PSNR的定义是峰值信噪比,对于0-1范围的数据,公式是:

PSNR = 10 * log10(1 / MSE)

def compute_psnr(img1, img2): """ 计算两张0-1范围内图像的PSNR img1, img2: PyTorch Tensor, 形状为 (B, 3, H, W) """ mse = torch.mean((img1 - img2) ** 2) if mse.item() < 1e-10: return 100.0 # 完全一致时给出一个很大的值 psnr = 10.0 * torch.log10(1.0 / mse) return psnr.item()

SSIM稍微复杂一点,我不想贴一个过于臃肿的实现,这里给出一个简化版本,它用高斯窗口计算局部均值和方差,再合成结构相似度:

def gaussian_kernel(size=11, sigma=1.5): """生成一维高斯核并外积成二维核,用于SSIM计算""" coords = torch.arange(size, dtype=torch.float32) - size // 2 g = torch.exp(-(coords ** 2) / (2 * sigma ** 2)) g /= g.sum() return g.outer(g).view(1, 1, size, size) def compute_ssim(img1, img2, window_size=11, sigma=1.5): """ 简化版SSIM,忽略通道合并,逐通道计算后取平均 img1, img2: 0-1范围的Tensor,形状(B, 3, H, W) """ device = img1.device window = gaussian_kernel(window_size, sigma).to(device) # 常数C1, C2防止除零 C1 = 0.01 ** 2 C2 = 0.03 ** 2 # 计算均值 mu1 = torch.nn.functional.conv2d(img1, window, padding=window_size // 2) mu2 = torch.nn.functional.conv2d(img2, window, padding=window_size // 2) mu1_sq, mu2_sq = mu1 ** 2, mu2 ** 2 mu1_mu2 = mu1 * mu2 # 计算方差和协方差 sigma1_sq = torch.nn.functional.conv2d(img1 * img1, window, padding=window_size // 2) - mu1_sq sigma2_sq = torch.nn.functional.conv2d(img2 * img2, window, padding=window_size // 2) - mu2_sq sigma12 = torch.nn.functional.conv2d(img1 * img2, window, padding=window_size // 2) - mu1_mu2 ssim_map = ((2 * mu1_mu2 + C1) * (2 * sigma12 + C2)) / \ ((mu1_sq + mu2_sq + C1) * (sigma1_sq + sigma2_sq + C2)) return ssim_map.mean().item()

5.2 结果可视化:把雨图、去雨图、干净图画一起,比只看数值更靠谱

评估一个去雨模型,不能只盯PSNR。我自己吃过这个亏:模型PSNR到了32dB,但肉眼一看,背景细节已经糊成水彩画了。PSNR只衡量像素差异的全局平均值,它不能反映局部结构是否被抹平。

所以我每次验证都做一张"三连图":左边是雨图输入,中间是模型输出,右边是干净原图。代码很简单:

def save_comparison(rainy, output, clean, save_path): """ 将输入雨图、模型输出、干净原图横向拼接保存 rainy/output/clean: Tensor (3, H, W),0-1范围 """ # 转成numpy并还原到0-255范围 rainy_np = rainy.cpu().numpy().transpose(1, 2, 0) * 255.0 output_np = output.cpu().numpy().transpose(1, 2, 0) * 255.0 clean_np = clean.cpu().numpy().transpose(1, 2, 0) * 255.0 combo = np.hstack([rainy_np, output_np, clean_np]).astype(np.uint8) # 保存为RGB图,注意要转回BGR给cv2 combo_bgr = cv2.cvtColor(combo, cv2.COLOR_RGB2BGR) cv2.imwrite(save_path, combo_bgr)

然后每个epoch结束时挑几张验证图跑一次保存,我就能直观看到模型有没有出现"过度平滑"的现象。如果输出图像像被水洗过一样,那说明要调整损失函数,比如加大SSIM的权重。

6. 推理部署与常见问题排查:从训练到真正能用的一段路

6.1 单张图片推理全流程:注意归一化必须和训练一致

模型训练好了,最终要拿去处理任意一张图。推理流程和训练时有几个细节不同,我在这里完整写一遍,同时也解释为什么有些细节容易出错。

def inference_single_image(model, image_path, output_path, use_gpu=True): """ 对单张雨图做去雨推理 """ device = torch.device("cuda" if use_gpu and torch.cuda.is_available() else "cpu") model = model.to(device) model.eval() # 1. 读取图像并转为RGB img = cv2.imread(image_path) if img is None: raise ValueError(f"无法读取图片: {image_path}") img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 2. 保存原图尺寸,推理后要把输出resize回原尺寸 h, w, _ = img.shape img_resized = cv2.resize(img_rgb, (224, 224), interpolation=cv2.INTER_AREA) # 3. 转Tensor并归一化到0-1范围,增加batch维度 input_tensor = torch.from_numpy(img_resized.transpose(2, 0, 1)).float().unsqueeze(0) / 255.0 input_tensor = input_tensor.to(device) # 4. 推理,并禁止计算梯度 with torch.no_grad(): output_tensor = model(input_tensor) # 5. 处理后保存:先转回numpy,clamp到0-1区间,再resize回原始尺寸 output_np = output_tensor.squeeze(0).cpu().numpy().transpose(1, 2, 0) output_np = np.clip(output_np, 0.0, 1.0) * 255.0 output_np = output_np.astype(np.uint8) output_rgb = cv2.resize(output_np, (w, h), interpolation=cv2.INTER_LINEAR) # 6. RGB转BGR存盘 output_bgr = cv2.cvtColor(output_rgb, cv2.COLOR_RGB2BGR) cv2.imwrite(output_path, output_bgr)

这里有个容易踩的坑,如果训练时用了归一化中常见的mean/std操作,推理时也必须用完全相同的mean/std。我这个代码训练时只除以了255,没有做mean/std归一化,所以推理时也保持一致的归一化方式。如果两边不一致,模型输出的颜色会偏移,而且很难发现原因。

6.2 推理阶段常踩的几个坑

输入尺寸不一致的问题。我在上面用了resize到224x224再推理的方法,简单但会丢失细节,尤其在大图上效果不好。更好的做法是滑窗推理:把大图切成若干224x224的patch分别推理,最后拼回原尺寸。代价是推理时间变长,但对细节保留非常明显。

另一个坑是输出图像的暗部偏色。如果训练时合成雨纹的alpha值总是偏大,模型会在真实无雨图片上产生"去雨过猛"的问题,把暗部细节也当成雨纹去掉。这就需要在训练数据里混入一些不含雨纹的干净图片,让模型学会"没有雨纹时保持原样"。

6.3 从基线到更好效果的三条改进路径

这套基线模型的作用是跑通全流程,如果要做更深入的课题,我有三条建议:

第一,把普通卷积块换成残差块或者密集连接块,训练稳定性会有明显提升。第二,在卷积之后加一个简单的通道注意力模块,让网络学会重点处理雨纹出现概率高的通道。第三,把MSE损失换成Charbonnier损失配合SSIM损失,对离群点的抗干扰性更强。

不过我必须强调一点,在追求更先进结构之前,先把数据合成质量和评测流程做扎实。很多人在小改动上纠结半天,结果发现是合成雨纹的程序写错了,这亏我吃过。

写在最后:关于这套代码的一些个人心得

整套流程跑下来,我最想分享的体会是:图像去雨的提升瓶颈往往不在网络结构,而在看不见的细节里。数据合成时雨纹密度和长度的分布决定了模型的上限;训练时损失函数的结构约束决定了输出是不是"耐看";评测时如果不看主观效果,很容易被PSNR欺骗。

我建议每一个做这个方向的同学,都从合成数据开始亲手搭一遍这整套代码,训练过程中每10个epoch保存一次验证输出图,盯着图像看趋势,比只看曲线靠谱得多。跑通之后,你会对残差学习、感受野、损失函数设计这些概念有完全不同的理解。

最后分享一个小技巧:训练接近收敛时,把验证集中效果最差的几张图挑出来看。分析这些图面的共同点,往往能准确指出当前模型的短板——是长雨纹处理不了,还是密集雨纹区域的背景纹理被抹掉。针对短板去调整数据或网络,比盲目堆参数高效得多。

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

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

立即咨询