ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

双像素脱焦去模糊DPDD:全分辨率与子孔径视图实战

双像素脱焦去模糊DPDD:全分辨率与子孔径视图实战 简介这份资源面向从事图像复原、计算摄影与去模糊研究的开发者与学习者围绕双像素脱焦去模糊DPDD数据集提供配套代码实现。数据集由500组拍摄场景构成包含500幅离焦模糊原图、1000幅双像素子孔径视图及500幅全焦点清晰参考图分辨率均为6720×4480可用于训练与评估脱焦去模糊模型。压缩包共71个文件约54.47MB以gif动态对比图、py脚本、npy数据文件、md说明文档及png示意图为主涵盖模型定义、数据处理、指标计算与主流程等模块并附有DPDNet模型检查点与动画结果示例便于直观查看复原效果。目前已有94人学习下载。读者可据此复现双像素去模糊流程理解子孔径视图与全焦点图像间的映射关系并借助现成脚本快速搭建训练与评测环境适合具备一定深度学习基础、希望深入计算摄影方向的中高级学习者参考。1. 双像素脱焦去模糊6720×4480 全分辨率与子孔径视图到底在解决什么手机拍一张逆光人像背景高光糊成一团人脸边缘发虚放大到 100% 全是涂抹感——这是脱焦defocus叠加运动模糊的典型翻车现场。双像素脱焦去模糊DPDDDual-Pixel Defocus Deblurring数据集要干的事就是把这种「拍糊了还能救回来」的问题从一张图变成一个可训练、可复现的工程任务。它同时提供 6720×4480 全分辨率图像和对应的子孔径视图sub-aperture views让模型不仅看到「糊成什么样」还能看到「光从哪个方向来」从而把脱焦和运动模糊拆开处理。这套数据适合三类人做手机影像算法的工程师想验证双像素传感器能不能在 RAW 域做去模糊做计算摄影的研究者需要高分辨率、带物理先验的配对数据以及想把去模糊模型落到端侧的产品团队关心全分辨率下的显存和延迟。核心痛点在于普通去模糊数据集只给「模糊-清晰」一对图模型只能靠猜DPDD 多给了左右子孔径视图相当于把「景深方向」的信息也塞进网络脱焦区域的空间变化就有了约束。6720×4480 这个分辨率不是炫技它对应的是手机主摄直出 3000 万像素以上的真实场景低分辨率训出来的模型直接上高分辨率会崩所以全分辨率训练和推理的工程细节必须提前想清楚。2. DPDD 的数据组织与子孔径视图从双像素传感器到可训练张量2.1 双像素传感器为什么能提供子孔径视图双像素Dual-Pixel传感器的每个像素下面有两个光电二极管分别接收左右两侧的光。对焦准确时左右两个子像素的信号一致脱焦时左右信号出现相位差这个相位差就是深度线索。把左子像素全部取出来组成一张图右子像素组成另一张图就得到左右子孔径视图。它们和主图左右相加共享同一个场景但视角有微小偏移类似一个极短基线的双目。DPDD 数据集通常按场景组织每个场景包含全分辨率主图模糊、全分辨率清晰参考图、左子孔径视图、右子孔径视图以及可能的视差/深度辅助信息。常见做法是把这些图按 2×2 或 4×4 的 patch 切分因为 6720×4480 整图直接进网络显存吃不消。切分时要保证子孔径视图和主图的空间对齐否则相位线索就废了。注意子孔径视图和主图必须来自同一次曝光、同一组像素任何后处理缩放都会破坏相位关系。2.2 用 Python 读取并检查 DPDD 样本的目录结构假设你已经拿到 DPDD 的原始目录常见结构是每个场景一个文件夹里面按blurred、sharp、left、right分。下面这段代码做三件事遍历场景、读取全分辨率图、检查左右子孔径与主图尺寸是否一致。import os import cv2 import numpy as np root /data/DPDD # 替换为你的数据集根目录 scenes sorted(os.listdir(root))[:5] # 先看前5个场景 for scene in scenes: scene_dir os.path.join(root, scene) blur cv2.imread(os.path.join(scene_dir, blurred.png), cv2.IMREAD_UNCHANGED) sharp cv2.imread(os.path.join(scene_dir, sharp.png), cv2.IMREAD_UNCHANGED) left cv2.imread(os.path.join(scene_dir, left.png), cv2.IMREAD_UNCHANGED) right cv2.imread(os.path.join(scene_dir, right.png), cv2.IMREAD_UNCHANGED) print(scene, blur, blur.shape, sharp, sharp.shape, left, left.shape, right, right.shape) # 检查左右子孔径相加是否近似等于主图线性响应下 if blur is not None and left is not None and right is not None: recon cv2.addWeighted(left.astype(np.float32), 0.5, right.astype(np.float32), 0.5, 0) diff np.mean(np.abs(recon - blur.astype(np.float32))) print( mean abs diff between (leftright)/2 and blurred:, diff)逻辑说明cv2.IMREAD_UNCHANGED保留原始位深DPDD 常见 16 位 PNG用默认的 8 位读取会丢动态范围。cv2.addWeighted做平均而不是直接相加避免溢出。diff用来验证左右子孔径是否真的对应主图如果 diff 很大可能是场景不匹配或读取通道顺序错了OpenCV 默认 BGR。参数说明root指向数据集根目录scenes[:5]只是抽样实际训练要全量遍历。如果图像是 16 位后续归一化要除以 65535 而不是 255。2.3 全分辨率切 patch尺寸、重叠与子孔径同步6720×4480 直接切 256×256 的 patch会得到约 26×17442 个不重叠块。但脱焦模糊在边缘和纹理区域差异大不重叠切分可能让某些 patch 全是平滑背景训练时梯度贡献低。常见做法是加 50% 重叠或者按场景内容做加权采样。def extract_patches(img, patch_size256, stride128): 从 HxW 或 HxWxC 图像中提取 patch返回列表 if img.ndim 2: img img[..., None] H, W, C img.shape patches [] coords [] for y in range(0, H - patch_size 1, stride): for x in range(0, W - patch_size 1, stride): patch img[y:ypatch_size, x:xpatch_size, :] patches.append(patch) coords.append((y, x)) return patches, coords # 对 blur/left/right/sharp 用同一组坐标切分 blur_patches, coords extract_patches(blur) left_patches, _ extract_patches(left) right_patches, _ extract_patches(right) sharp_patches, _ extract_patches(sharp) print(patch count:, len(blur_patches), first patch shape:, blur_patches[0].shape)逻辑说明stride128配合patch_size256实现 50% 重叠。四个图必须用同一个extract_patches调用保证坐标一致。如果某张图尺寸和其他不一致coords会对不齐后续训练直接错位。参数说明patch_size根据显存选256 是 8GB 显存的稳妥值stride越小重叠越多样本量翻倍但训练 epoch 变慢。全分辨率推理时不需要切 patch但要用滑动窗口加权重融合避免拼接缝。3. 深度处理代码实现从数据加载到去模糊网络训练3.1 用 PyTorch Dataset 封装 DPDD 多输入样本DPDD 的输入不是单张图而是模糊主图 左子孔径 右子孔径输出是清晰图。Dataset 要保证三者同步切分、同步增强。下面是一个最小可用封装。import torch from torch.utils.data import Dataset import cv2 import numpy as np import os class DPDDDataset(Dataset): def __init__(self, root, patch_size256, stride128, augmentTrue): self.root root self.patch_size patch_size self.stride stride self.augment augment self.samples [] # (scene, y, x) for scene in sorted(os.listdir(root)): scene_dir os.path.join(root, scene) blur_path os.path.join(scene_dir, blurred.png) if not os.path.exists(blur_path): continue img cv2.imread(blur_path, cv2.IMREAD_UNCHANGED) H, W img.shape[:2] for y in range(0, H - patch_size 1, stride): for x in range(0, W - patch_size 1, stride): self.samples.append((scene, y, x)) def __len__(self): return len(self.samples) def _read(self, path): img cv2.imread(path, cv2.IMREAD_UNCHANGED) if img is None: raise FileNotFoundError(path) if img.ndim 2: img img[..., None] img img.astype(np.float32) / 65535.0 # 16位归一化 return img def __getitem__(self, idx): scene, y, x self.samples[idx] scene_dir os.path.join(self.root, scene) ps self.patch_size blur self._read(os.path.join(scene_dir, blurred.png))[y:yps, x:xps] left self._read(os.path.join(scene_dir, left.png))[y:yps, x:xps] right self._read(os.path.join(scene_dir, right.png))[y:yps, x:xps] sharp self._read(os.path.join(scene_dir, sharp.png))[y:yps, x:xps] # 简单增强随机水平翻转三个输入和标签同步 if self.augment and np.random.rand() 0.5: blur blur[:, ::-1] left left[:, ::-1] right right[:, ::-1] sharp sharp[:, ::-1] # HWC - CHW blur torch.from_numpy(blur.transpose(2, 0, 1).copy()) left torch.from_numpy(left.transpose(2, 0, 1).copy()) right torch.from_numpy(right.transpose(2, 0, 1).copy()) sharp torch.from_numpy(sharp.transpose(2, 0, 1).copy()) return {blur: blur, left: left, right: right, sharp: sharp}逻辑说明self.samples在初始化时把所有 patch 坐标算好避免每个 epoch 重复扫描大图。_read里除以 65535 是假设 16 位如果你的 DPDD 是 8 位改成 255。增强只做了水平翻转因为垂直翻转会破坏子孔径的左右相位关系——左视图翻到右边就变成右视图了这是血泪经验。参数说明patch_size和stride要和前面切分逻辑一致augment在验证集设为 False。如果显存够可以把patch_size提到 384 或 512但 batch size 要相应降。3.2 一个能跑通的双分支去模糊网络结构网络设计上常见做法是主图走一个编码器左右子孔径走一个轻量分支提取相位特征然后在中间层融合。下面是一个简化版能跑通、能出梯度方便你在此基础上改。import torch.nn as nn import torch.nn.functional as F class SubApertureFusion(nn.Module): def __init__(self, in_ch3, mid_ch32): super().__init__() self.conv_l nn.Conv2d(in_ch, mid_ch, 3, padding1) self.conv_r nn.Conv2d(in_ch, mid_ch, 3, padding1) self.fuse nn.Conv2d(mid_ch * 2, mid_ch, 1) def forward(self, left, right): l F.relu(self.conv_l(left)) r F.relu(self.conv_r(right)) # 相位差特征左右相减保留脱焦方向信息 diff l - r concat torch.cat([l, r, diff], dim1) return self.fuse(concat) class DPDDNet(nn.Module): def __init__(self): super().__init__() self.sub SubApertureFusion(3, 32) self.enc1 nn.Conv2d(3 32, 64, 3, padding1) self.enc2 nn.Conv2d(64, 128, 3, stride2, padding1) self.enc3 nn.Conv2d(128, 256, 3, stride2, padding1) self.dec2 nn.ConvTranspose2d(256, 128, 2, stride2) self.dec1 nn.ConvTranspose2d(128, 64, 2, stride2) self.out nn.Conv2d(64, 3, 3, padding1) def forward(self, blur, left, right): sub_feat self.sub(left, right) x torch.cat([blur, sub_feat], dim1) e1 F.relu(self.enc1(x)) e2 F.relu(self.enc2(e1)) e3 F.relu(self.enc3(e2)) d2 F.relu(self.dec2(e3)) d1 F.relu(self.dec1(d2 e2)) residual self.out(d1 e1) return torch.clamp(blur residual, 0, 1) # 残差学习输出清晰图逻辑说明SubApertureFusion里显式计算了l - r的差值这个差值就是左右子孔径的相位差对脱焦区域敏感。主分支用残差学习网络只学「模糊图到清晰图的增量」比直接回归清晰图更容易收敛。torch.clamp保证输出在 [0,1]。参数说明mid_ch32是子孔径分支的宽度太大显存涨得快enc2、enc3的 stride2 做下采样扩大感受野对脱焦这种大范围模糊有用。如果训练时 loss 不降先把mid_ch降到 16确认数据管道没问题再往上加。3.3 训练循环与损失函数L1 为主感知损失为辅去模糊任务常用 L1 损失因为它对异常值比 L2 稳。如果追求视觉质量可以加一个 VGG 感知损失但会拖慢训练。下面是一个最小训练循环。from torch.utils.data import DataLoader import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) dataset DPDDDataset(/data/DPDD, patch_size256, stride128, augmentTrue) loader DataLoader(dataset, batch_size4, shuffleTrue, num_workers4) model DPDDNet().to(device) optimizer optim.Adam(model.parameters(), lr1e-4) criterion nn.L1Loss() for epoch in range(10): model.train() total_loss 0.0 for batch in loader: blur batch[blur].to(device) left batch[left].to(device) right batch[right].to(device) sharp batch[sharp].to(device) pred model(blur, left, right) loss criterion(pred, sharp) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() print(fepoch {epoch}, loss {total_loss / len(loader):.4f})逻辑说明batch_size4配合 256 patch在 8GB 显存上大约占 6GB 左右。num_workers4加速数据读取但如果你用的是机械硬盘可以降到 2。lr1e-4是 Adam 的常用起点loss 震荡就降到 5e-5。参数说明epoch先跑 10 轮看趋势DPDD 全量数据通常要 100 轮以上。如果 loss 降到 0.01 以下但视觉还是糊说明模型容量不够或感受野不足考虑加深网络或增大 patch。4. 全分辨率推理与子孔径对齐踩坑与排查4.1 全分辨率直接推理为什么会爆显存6720×4480 的图单精度浮点3 通道一张就是 6720×4480×3×4 ≈ 361MB。网络中间特征图如果是 64 通道就是 7.7GB再加梯度就爆了。所以推理必须用滑动窗口或者用torch.no_grad()加半精度。torch.no_grad() def infer_full_res(model, blur, left, right, patch_size512, stride256): model.eval() H, W blur.shape[:2] output np.zeros((H, W, 3), dtypenp.float32) weight np.zeros((H, W, 1), dtypenp.float32) for y in range(0, H - patch_size 1, stride): for x in range(0, W - patch_size 1, stride): b torch.from_numpy(blur[y:ypatch_size, x:xpatch_size].transpose(2,0,1)[None]).float().cuda() l torch.from_numpy(left[y:ypatch_size, x:xpatch_size].transpose(2,0,1)[None]).float().cuda() r torch.from_numpy(right[y:ypatch_size, x:xpatch_size].transpose(2,0,1)[None]).float().cuda() pred model(b, l, r)[0].cpu().numpy().transpose(1,2,0) output[y:ypatch_size, x:xpatch_size] pred weight[y:ypatch_size, x:xpatch_size] 1.0 return output / np.maximum(weight, 1e-6)逻辑说明weight累加每个像素被覆盖的次数最后做平均避免拼接缝。stride256配合patch_size512有 50% 重叠融合更平滑。torch.no_grad()省显存float().cuda()在 patch 级别转 GPU不要整图转。参数说明patch_size推理时可以比训练大因为不需要存梯度512 在 8GB 卡上单张 patch 约 1.5GB 特征安全。如果还是爆降到 384。4.2 子孔径视图对齐翻车的三种典型现象现象一推理结果出现左右重影。原因左右子孔径在切 patch 时坐标错位或者增强时只翻转了主图没翻转子孔径。解决检查 Dataset 里四个图的切分坐标是否来自同一个extract_patches调用增强必须同步。现象二脱焦区域去模糊后出现方向性伪影。原因子孔径融合时直接相加没有保留相位差。解决像 3.2 那样显式计算l - r或者用可变形卷积对齐左右特征。现象三全分辨率推理边缘变暗。原因滑动窗口边缘覆盖次数少平均后权重低。解决在infer_full_res里对边缘做 padding或者用高斯权重代替均匀权重。提示验证对齐是否正确的快速方法——把左子孔径和右子孔径相减脱焦区域应该出现明显的明暗条纹如果全黑说明左右图完全一样数据读取错了。4.3 训练不收敛时的排查顺序先查数据用 2.2 的代码打印diff如果(leftright)/2和blurred差很多说明数据配对有问题。再查归一化16 位图除以 255 会得到大于 1 的值网络输出被 clamp 后梯度消失。然后查学习率Adam 1e-4 不降就试 5e-5还不行就 1e-5。最后查损失L1 对模糊图直接回归清晰图如果 loss 卡在 0.1 左右说明网络在学均值需要加残差连接或换感知损失。常见误用把blurred和sharp搞反了输入清晰图输出模糊图loss 也能降但推理时完全没用。训练前务必可视化一个 batch确认输入是糊的、标签是清晰的。5. 把 DPDD 用出效果从数据增强到端侧部署的进阶技巧数据增强上除了水平翻转还可以做随机裁剪、亮度抖动、加高斯噪声。但子孔径视图的相位关系对几何变换敏感旋转和垂直翻转要慎用。我一般只做水平翻转和亮度抖动亮度抖动幅度控制在 ±0.1太大模拟的是曝光变化而不是脱焦。验证方法上不要只看 PSNR 和 SSIM。脱焦去模糊的视觉质量更看边缘锐度和伪影。我习惯在验证集里挑 5 张有文字、有高光、有细纹理的图训练每 10 轮就推理一次肉眼对比。文字边缘出现振铃就是过度锐化高光区域出现色块就是子孔径融合权重不对。端侧部署时全分辨率推理的延迟主要花在滑动窗口的重复计算上。一个实用技巧是先用低分辨率模型预测一个粗略的清晰图再在高分辨率上只对脱焦区域做精细推理。脱焦区域可以通过左右子孔径的相位差图快速定位——相位差大的地方就是脱焦严重的地方相位差接近零的地方直接跳过。这样能把 6720×4480 的推理时间从几十秒压到几秒。还有一个容易忽略的点DPDD 的清晰参考图本身可能带有轻微噪声或压缩伪影训练时如果 loss 降得太低模型会去拟合这些伪影。我一般会在 loss 里加一个小的 TV 正则或者对 sharp 做一次轻微的降噪再当标签。这个习惯让我少走了很多「指标好看但肉眼更糊」的弯路。希望帮到你。本文还有配套的精品资源点击获取
返回列表