ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

全分辨率双像素脱焦去模糊:DPDD数据集实战与子孔径视差利用

2026/10/2 14:06:10 拓冰建站 浏览量
全分辨率双像素脱焦去模糊:DPDD数据集实战与子孔径视差利用 简介这份资源面向从事图像复原、计算摄影与去模糊研究的开发者与学习者围绕双像素脱焦去模糊DPDD数据集提供配套代码实现。数据集由500组拍摄场景构成包含500幅离焦模糊原图、1000幅双像素子孔径视图以及500幅全焦点清晰参考图分辨率均为6720×4480可用于训练与评估脱焦去模糊模型。压缩包共71个文件约54.47MB以gif动态对比图、py脚本、npy数据文件、md说明文档及png示意图为主其中脚本涵盖模型定义、数据处理、指标计算与主流程npy文件保存训练与验证所需的源图、目标图数据gif则直观展示去模糊前后的动态效果。已有94人学习关注。借助该资源读者可获取完整的DPDD数据处理与模型训练代码理解子孔径视图与全焦点图像之间的映射关系并参考目录中的配置与说明快速复现实验流程适合作为脱焦去模糊方向的入门实践与二次开发基础。1. 双像素脱焦去模糊为什么 6720×4480 的全分辨率与子孔径视图值得你折腾拿到「双像素脱焦去模糊」这个题目时我第一反应不是去翻论文而是先想清楚一件事手机里那个「先拍照后对焦」的功能背后到底存了什么数据。双像素Dual Pixel传感器把每个像素拆成左右两个光电二极管一次曝光同时拿到两幅存在微小视差的子孔径视图。当镜头没对准焦平面时左右视图之间会出现与脱焦程度成正比的位移这个位移就是深度线索也是去模糊的关键。DPDD 数据集把这件事做到了极致6720×4480 的全分辨率图像配上对应的子孔径视图和清晰参考图让你能在真实尺度上验证算法而不是在 512×512 的缩略图上自欺欺人。这个方向适合两类人一类是做计算摄影、手机影像算法的工程师想把手里的去模糊模型从仿真数据迁移到真实双像素数据另一类是做深度估计、图像复原的研究者需要一套带子孔径视图的高分辨率基准来验证多视图融合思路。它解决的核心问题是脱焦模糊不是均匀的不同深度区域的模糊核不同单张图去模糊本质上是病态问题而子孔径视图提供了额外的视差约束让问题变得可解。全分辨率意味着你不能靠下采样糊弄过去显存、分块、对齐误差都会被放大。接下来我会按「数据怎么读、模型怎么搭、训练怎么稳、坑在哪」的顺序把这条链路拆开讲清楚。2. DPDD 数据集的目录结构、子孔径视图与全分辨率读取2.1 先搞清楚 DPDD 里到底有什么DPDD 的典型组织方式是按场景分文件夹每个场景下包含若干组数据。常见做法是每组数据里有三样东西一张全分辨率脱焦图source、一张对应的清晰参考图target、以及一组子孔径视图。子孔径视图通常是 2×2 或 4×4 的网格每个格子对应传感器上不同位置的光电二极管组合它们之间的视差直接反映脱焦位移。全分辨率 6720×4480 意味着单张 RGB 图约 90MBuint8如果按 float32 加载直接翻四倍不做分块或内存映射批量训练基本跑不起来。我一般会先写一个脚本把目录扫一遍确认命名规律和文件数量而不是假设所有场景结构一致。下面这段代码做两件事递归列出所有图像文件按场景分组并打印每组的分辨率和通道数帮你快速判断数据是否完整、有没有混入不同尺寸的图。import os from pathlib import Path from PIL import Image import numpy as np def scan_dpdd(root): root Path(root) scenes {} for img_path in root.rglob(*): if img_path.suffix.lower() not in {.png, .jpg, .jpeg, .tif, .tiff}: continue # 用父目录名作为场景标识常见做法是 scene_xxx scene img_path.parent.name scenes.setdefault(scene, []).append(img_path) for scene, files in scenes.items(): print(f场景 {scene}: {len(files)} 个文件) for f in files[:3]: # 只看前三个避免刷屏 with Image.open(f) as im: arr np.array(im) print(f {f.name} 尺寸{im.size} 模式{im.mode} dtype{arr.dtype}) return scenes scenes scan_dpdd(./DPDD)逻辑说明rglob(*)递归遍历所有文件用后缀过滤掉非图像文件。scene img_path.parent.name假设同一场景的图放在同一目录下这是 DPDD 最常见的组织方式。打印尺寸和 dtype 是为了确认全分辨率图确实是 6720×4480子孔径视图可能是单通道或三通道取决于数据集版本。参数上如果你发现某些场景文件数明显偏少先别急着训练大概率是下载不完整或解压出错。2.2 子孔径视图的读取与对齐检查子孔径视图是 DPDD 区别于普通去模糊数据集的核心。常见做法是把 2×2 子孔径拼成一张大图或者存成独立文件。读取时要注意两点一是子孔径之间的视差方向要和脱焦图的空间方向一致二是如果数据集提供了视差图或深度图要确认它和子孔径的坐标系对齐。我习惯先做一次可视化检查把四个子孔径视图的差值图打印出来如果差值图呈现明显的水平或垂直条纹说明视差方向正确如果是一片噪声可能是读取顺序错了。import matplotlib.pyplot as plt def check_subaperture(sub_paths): # sub_paths 按左上、右上、左下、右下顺序传入 imgs [np.array(Image.open(p).convert(L), dtypenp.float32) for p in sub_paths] h, w imgs[0].shape # 计算相邻子孔径的绝对差视差越大差值越亮 diff_h np.abs(imgs[0] - imgs[1]) # 水平方向视差 diff_v np.abs(imgs[0] - imgs[2]) # 垂直方向视差 fig, axes plt.subplots(1, 3, figsize(15, 5)) axes[0].imshow(imgs[0], cmapgray) axes[0].set_title(左上子孔径) axes[1].imshow(diff_h, cmaphot) axes[1].set_title(水平视差差值) axes[2].imshow(diff_v, cmaphot) axes[2].set_title(垂直视差差值) for ax in axes: ax.axis(off) plt.show() # 假设子孔径文件按顺序命名 check_subaperture([ ./DPDD/scene_001/sub_LT.png, ./DPDD/scene_001/sub_RT.png, ./DPDD/scene_001/sub_LB.png, ./DPDD/scene_001/sub_RB.png, ])逻辑说明转灰度是为了降低计算量视差检查不需要颜色信息。diff_h和diff_v分别反映水平和垂直方向的子孔径位移脱焦越严重差值图越亮且边缘越宽。如果差值图几乎全黑说明这组数据可能已经接近合焦或者子孔径文件顺序不对。参数上convert(L)会丢失颜色但检查视差足够如果你要训练彩色去模糊模型后续读取时保留 RGB 通道即可。2.3 全分辨率图像的内存映射读取6720×4480 的图直接Image.open再np.array会瞬间吃掉几百 MB 内存批量加载必然爆。常见做法是用内存映射memory mapping或者分块读取。我一般会先把全分辨率图转成.npy格式然后用np.load(..., mmap_moder)按需读取块这样训练时只把当前 batch 需要的块载入内存。def convert_to_npy_memmap(img_path, npy_path): with Image.open(img_path) as im: arr np.array(im, dtypenp.uint8) # 保存为 npy后续可以用 mmap_mode 读取 np.save(npy_path, arr) print(f已保存 {npy_path}形状{arr.shape}大小{arr.nbytes / 1e6:.1f}MB) def load_block(npy_path, y0, y1, x0, x1): # mmap_moder 不会把整个文件读进内存 arr np.load(npy_path, mmap_moder) block np.array(arr[y0:y1, x0:x1]) # 只复制需要的块 return block convert_to_npy_memmap(./DPDD/scene_001/source.png, ./cache/scene_001_source.npy) block load_block(./cache/scene_001_source.npy, 0, 512, 0, 512) print(块形状:, block.shape)逻辑说明np.save保存的是原始 uint8 数组不压缩读取时mmap_moder建立磁盘到内存的映射np.array(arr[y0:y1, x0:x1])才真正把块复制进内存。参数上块大小建议取 512 或 768太小会导致频繁 IO太大又失去分块意义。注意.npy文件会占用和原图相近的磁盘空间6720×4480×3 约 90MB如果场景很多提前规划磁盘。提示如果数据集本身提供了子孔径视图的全分辨率文件同样建议转成.npy并建立索引否则每次训练都重新解码 PNG 会拖慢数据加载。3. 从子孔径视差到脱焦去模糊模型输入构造与训练策略3.1 子孔径视图怎么喂给网络子孔径视图不是简单的多帧堆叠它们之间存在已知的几何关系。常见做法有两种一是把 2×2 子孔径按通道拼接得到一个 12 通道输入每个子孔径 3 通道让网络自己学习视差到模糊核的映射二是先计算子孔径之间的视差图把视差图作为额外条件输入。前者实现简单后者可解释性更强但依赖视差估计精度。我一般先用通道拼接跑 baseline因为 DPDD 的子孔径视图已经对齐网络比较容易学到有用特征。import torch from torch.utils.data import Dataset class DPDDDataset(Dataset): def __init__(self, scene_list, source_npy, target_npy, sub_npy_list, patch_size512): self.scenes scene_list self.source_npy source_npy self.target_npy target_npy self.sub_npy_list sub_npy_list # 四个子孔径的 npy 路径 self.patch_size patch_size def __len__(self): return len(self.scenes) def __getitem__(self, idx): scene self.scenes[idx] src np.load(self.source_npy[scene], mmap_moder) tgt np.load(self.target_npy[scene], mmap_moder) subs [np.load(p, mmap_moder) for p in self.sub_npy_list[scene]] h, w src.shape[:2] ps self.patch_size # 随机裁剪保证 source/target/子孔径同一位置 y0 np.random.randint(0, h - ps) x0 np.random.randint(0, w - ps) src_patch np.array(src[y0:y0ps, x0:x0ps], dtypenp.float32) / 255.0 tgt_patch np.array(tgt[y0:y0ps, x0:x0ps], dtypenp.float32) / 255.0 sub_patches [ np.array(s[y0:y0ps, x0:x0ps], dtypenp.float32) / 255.0 for s in subs ] # 子孔径按通道拼接: [4, H, W, 3] - [H, W, 12] sub_cat np.concatenate(sub_patches, axis-1) # 转成 CHW src_t torch.from_numpy(src_patch).permute(2, 0, 1) tgt_t torch.from_numpy(tgt_patch).permute(2, 0, 1) sub_t torch.from_numpy(sub_cat).permute(2, 0, 1) return src_t, sub_t, tgt_t逻辑说明mmap_moder保证不一次性加载全图随机裁剪时四个子孔径和 source/target 用同一组y0, x0避免空间错位。np.concatenate(..., axis-1)把四个子孔径沿通道维拼接得到 12 通道输入。参数上patch_size512是显存和感受野的折中如果你用 24GB 显存可以尝试 768如果只有 8GB降到 256 并配合梯度累积。注意归一化用/255.0如果你后续用 ImageNet 预训练权重要改成对应的均值和方差。3.2 损失函数为什么只做 L1 会糊脱焦去模糊如果只用 L1 或 L2 损失结果会偏平滑高频细节丢失。常见做法是 L1 加感知损失perceptual loss再加一点梯度损失。感知损失用 VGG 特征梯度损失约束边缘。DPDD 有清晰参考图所以可以直接算监督损失不需要对抗训练也能出不错的结果。我一般会先用 L1 跑通确认数据管道没问题再加感知损失否则一开始就上复杂损失出问题很难定位是数据还是损失。import torch.nn as nn import torchvision.models as models class PerceptualLoss(nn.Module): def __init__(self): super().__init__() vgg models.vgg16(weightsmodels.VGG16_Weights.IMAGENET1K_V1).features # 取前 16 层覆盖浅层到中层特征 self.vgg vgg[:16].eval() for p in self.vgg.parameters(): p.requires_grad False def forward(self, pred, target): # 输入范围 [0,1]VGG 期望 ImageNet 归一化 mean torch.tensor([0.485, 0.456, 0.406], devicepred.device).view(1,3,1,1) std torch.tensor([0.229, 0.224, 0.225], devicepred.device).view(1,3,1,1) pred_n (pred - mean) / std target_n (target - mean) / std feat_pred self.vgg(pred_n) feat_target self.vgg(target_n) return nn.functional.l1_loss(feat_pred, feat_target) def total_loss(pred, target, perc_loss, w_l11.0, w_perc0.1): l1 nn.functional.l1_loss(pred, target) perc perc_loss(pred, target) return w_l1 * l1 w_perc * perc, l1.item(), perc.item()逻辑说明vgg[:16]取到第三个池化层之前特征图分辨率还比较大能捕捉边缘和纹理。eval()和requires_gradFalse确保 VGG 不参与训练。参数上w_perc0.1是常见起点太大容易引入 VGG 自身的纹理伪影太小则感知提升不明显。如果你发现训练初期损失震荡先把w_perc降到 0.01等 L1 稳定后再加回来。3.3 训练时的分块推理与拼接全分辨率推理不能整图送进网络必须分块。分块有两个坑块与块之间的边界会出现接缝以及子孔径视图在边界处的视差可能不连续。常见做法是块之间保留 overlap推理后只取中心区域再拼接。overlap 一般取 patch 的 1/8 到 1/4。def inference_full_res(model, src_full, sub_full, patch512, overlap64): model.eval() h, w src_full.shape[:2] output np.zeros((h, w, 3), dtypenp.float32) weight np.zeros((h, w, 1), dtypenp.float32) stride patch - overlap for y in range(0, h, stride): for x in range(0, w, stride): y1 min(y patch, h) x1 min(x patch, w) y0 max(0, y1 - patch) x0 max(0, x1 - patch) src_p torch.from_numpy(src_full[y0:y1, x0:x1]).permute(2,0,1).unsqueeze(0).float() / 255.0 sub_p torch.from_numpy(sub_full[y0:y1, x0:x1]).permute(2,0,1).unsqueeze(0).float() / 255.0 with torch.no_grad(): pred model(src_p, sub_p)[0].permute(1,2,0).cpu().numpy() # 只取有效区域边界处用权重平滑 output[y0:y1, x0:x1] pred weight[y0:y1, x0:x1] 1.0 output output / np.maximum(weight, 1e-6) return np.clip(output, 0, 1)逻辑说明stride patch - overlap保证相邻块有重叠y0 max(0, y1 - patch)处理边缘块避免越界。累加后除以权重实现平滑拼接。参数上overlap64对 512 的块约 12.5%如果接缝明显可以加到 128但推理时间会线性增加。注意weight初始化为零边缘块可能只被覆盖一次除法时用np.maximum防止除零。注意分块推理时子孔径视图的视差在块边界可能被截断如果模型对边界敏感可以在块周围额外扩展一圈上下文推理后再裁掉。4. 避坑与排查全分辨率双像素去模糊的 5 个血泪教训4.1 子孔径视图顺序错乱导致模型学反现象训练损失能下降但验证集输出出现方向性模糊水平边缘比垂直边缘糊得更厉害。原因子孔径文件命名不统一有的场景是 LT/RT/LB/RB有的场景是 0/1/2/3读取时按文件名排序可能把左右搞反。解决写一个校验脚本对每组子孔径计算水平差值和垂直差值的均值如果水平差值远小于垂直差值说明左右可能反了。统一重命名后再训练。4.2 全分辨率图直接 resize 到 512 训练推理时全分辨率崩掉现象训练时损失很低推理全分辨率时出现大量网格状伪影。原因训练时 resize 改变了脱焦模糊核的空间尺度网络学到的模糊核尺寸和全分辨率不匹配。解决训练时也从全分辨率随机裁剪 patch不要 resize。如果显存不够用梯度累积或混合精度而不是降分辨率。4.3 内存映射文件在 DataLoader 多进程下报错现象np.load(..., mmap_moder)在num_workers0时偶尔抛出OSError: [Errno 24] Too many open files。原因每个 worker 都打开一份 mmap文件描述符耗尽。解决把num_workers降到 2 或 4或者在__getitem__里用with open上下文管理确保每个样本处理完关闭映射。更稳妥的做法是预先把 patch 索引存成列表训练时按索引读取。4.4 感知损失权重过大导致颜色偏移现象加了感知损失后PSNR 反而下降输出图整体偏灰或偏黄。原因VGG 特征对颜色敏感感知损失在优化纹理的同时把颜色也带偏了。解决把感知损失只加在亮度通道或者降低权重到 0.01 并配合颜色恒常性损失。我一般会先跑一版纯 L1记录颜色分布加感知损失后再对比直方图。4.5 分块推理接缝明显块边界出现亮线现象全分辨率输出在块与块交界处有可见亮线或暗线。原因块边界处卷积感受野不完整网络输出在边界不可靠简单平均无法完全消除。解决增大 overlap 到 128并且只取每块中心 3/4 区域参与拼接边缘区域丢弃。如果还有残留可以在拼接后用导向滤波做一次后处理。5. 进阶技巧用子孔径视差做自适应分块与质量验证走到这里数据能读了模型能训了坑也踩得差不多了。最后分享一个我实际用下来最省事的技巧用子孔径视差图指导分块大小。脱焦模糊在不同深度区域差异很大近处脱焦严重、视差大远处接近合焦、视差小。如果整图用统一 patch 大小近处块可能感受野不够远处块又浪费算力。我的做法是先算一张粗略的视差图对视差大的区域用 768 的块视差小的区域用 384 的块推理时间能省 20% 左右接缝问题也因为块边界落在平滑区域而减轻。验证阶段别只看 PSNR 和 SSIM。全分辨率去模糊的最终评判是视觉质量我习惯做两件事一是把输出图和参考图都切成 512 的块随机抽 20 块并排显示肉眼过一遍二是算梯度幅值的直方图如果输出图的梯度分布比参考图窄太多说明高频细节还是丢了需要调损失权重。下面这段代码做梯度直方图对比帮你快速判断细节保留程度。import cv2 def gradient_hist(img, bins50): gray cv2.cvtColor((img * 255).astype(np.uint8), cv2.COLOR_RGB2GRAY) gx cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize3) gy cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize3) mag np.sqrt(gx**2 gy**2) hist, edges np.histogram(mag, binsbins, range(0, 100)) return hist, edges pred_hist, _ gradient_hist(pred_full) tgt_hist, _ gradient_hist(tgt_full) # 对比两个直方图如果 pred 在高梯度区间明显偏低说明细节丢失 print(预测图高梯度像素占比:, pred_hist[30:].sum() / pred_hist.sum()) print(参考图高梯度像素占比:, tgt_hist[30:].sum() / tgt_hist.sum())逻辑说明Sobel 算梯度幅值range(0, 100)覆盖大部分自然图像梯度高梯度区间30 以上反映边缘和纹理。如果预测图的高梯度占比不到参考图的一半说明模型输出偏平滑。参数上bins50够用ksize3是标准 Sobel 核。这个指标比 PSNR 更直观尤其适合调损失权重时快速对比。我自己的习惯是每次改完损失或数据管道先跑 100 个 iteration看梯度直方图有没有改善再决定要不要跑完整训练。这样能省下大量等 GPU 的时间。全分辨率双像素去模糊不是那种一次调通就完事的任务数据对齐、分块策略、损失权重每一环都会影响最终观感但只要把子孔径视图用对它比单图去模糊的上限高得多。希望帮到你。本文还有配套的精品资源点击获取