
简介图像去模糊是底层视觉中的经典难题尤其脱焦模糊因模糊核随场景深度空间变化难以用单一模型刻画。双像素传感器通过在同一像素位点记录左右子孔径视图为模糊核估计提供了深度先验这一硬件特性让去模糊任务从病态反卷积转化为有监督学习问题。DPDDDual-Pixel Defocus Deblurring数据集正是基于该原理构建的基准包含6720x4480全分辨率成对子孔径图、视差图及清晰参考图覆盖室内外多样场景广泛应用于图像复原、深度估计与感知任务。围绕该数据的工程实践涉及RAW格式解析、子孔径提取、视差计算及大规模训练策略是连接传感器硬件与传统视觉算法的典型范例。本文从双像素传感器原理出发梳理DPDD数据组织方式与预处理关键点结合PyTorch实现和训练细节为研究空间变化去模糊、双像素数据应用及多任务学习的开发者提供可参考的落地路径。 搞双像素脱焦去模糊这事最让人头疼的往往不是模型怎么搭而是数据从哪来、怎么喂进网络。DPDD数据集Dual-Pixel Defocus Deblurring双像素脱焦去模糊是目前这个方向绕不开的基准6720x4480全分辨率图像、成对的左右子孔径视图、还带视差图直接把“模糊核空间变化”这个老大难问题变成了一个有监督学习问题。这篇文章我会从数据集的内部结构、子孔径视图的提取原理到完整的代码实现和训练细节做一个深度拆解给正在做图像去模糊、深度估计或者想上手双像素数据的朋友一份可以照着抄的实操笔记。1. 项目概述与核心价值1.1 DPDD 到底是什么从双像素传感器说起很多人第一次听到“双像素”是在佳能相机上厂家宣传说它对焦快。但在计算机视觉领域双像素传感器Dual-Pixel Sensor带来的远不止对焦——它让一台普通单反相机在不加任何硬件改动的前提下同时记录下同一场景的两个子孔径视角。这两个视角分别对应镜头光瞳的左右半区相当于你在一台相机里装了两个“眼睛”虽然基线极短但足以提供深度线索。DPDD 数据集的完整名称是 Dual-Pixel Defocus Deblurring Dataset由约克大学团队在 ECCV 2020 发表。它用佳能 EOS 5D Mark IV 拍摄了 500 组室内外场景每组场景都包含全分辨率的脱焦模糊图、左右子孔径视图、视差图以及用小光圈拍摄的近全聚焦清晰图Ground Truth。所有图像都是 6720x4480 的原生全分辨率这在整个图像去模糊领域都是相当奢侈的配置。说直白点DPDD 解决的痛点是传统去模糊算法面对空间变化的模糊核时往往只能靠估计而双像素数据把“模糊核的估计”这件事变成了“从视差图推断深度再从深度和光圈参数推断模糊核”的可解问题。对搞图像复原、深度估计、甚至自动驾驶感知的朋友来说这个数据集都值得认真研究。1.2 为什么脱焦去模糊是难题DPDD 如何破局脱焦模糊和运动模糊最大的不同在于运动模糊在整张图上大致是全局一致的忽略旋转等情况可以用一个相对统一的模糊核来建模而脱焦模糊完全由场景深度决定——焦平面上的区域是清晰的焦点前后的区域模糊程度各不相同光圈越大、离焦量越大模糊越严重。这种空间变化的模糊无法用单一卷积核去逆卷积。传统做法是把图像分块每块单独估计模糊核再拼回去。但这个流程在弱纹理区域几乎必崩因为局部没有足够信息约束模糊核的估计。DPDD 的思路很聪明用双像素传感器的左右子孔径视图来提供额外观测。左右视图之间的视差直接反映了深度信息而深度信息又直接决定了脱焦模糊的弥散圆半径。这样一来模糊核的估计从一个病态的盲反卷积问题变成了一个有深度先验的估计问题。我在实际测试中也确实感受到双像素输入对边缘区域、前景背景交界的处理效果明显好于单图输入。模型不需要“猜测”哪里是焦外而是通过左右视图的差异“看到”了焦外物体的深度跳变这是 DPDD 最有价值的地方。2. 数据集全景拆解2.1 采集设备与拍摄方案DPDD 的采集设备是佳能 EOS 5D Mark IV全画幅 CMOS 传感器有效像素约 3040 万输出图像分辨率正好是 6720x4480。这台相机的双像素传感器每个像素位点由左右两个光电二极管组成可以分别读出所以能同时输出一个“左视角”和一个“右视角”。拍摄方案分为两轮。第一轮用大光圈f/1.4 到 f/4 不等拍摄场景此时景深浅场景中有大量区域处于焦外形成明显的脱焦模糊。第二轮用极小光圈f/8 甚至更小拍摄同一场景保证整个场景都近似合焦作为去模糊任务的 Ground Truth。需要注意的是整个数据集的模糊图像和清晰图像是分别拍摄的所以存在极微小的配准误差官方也提醒后续使用时可以自行做一次对齐。场景覆盖了室内和室外包括办公室、走廊、植物、人物、桌面摆件等各种典型环境。每个场景还会用不同的对焦位置拍摄多张确保模糊分布的区域多样化。整体看下来数据集的难度梯度很合理——既有大面积平坦区域也有高纹理边缘训练出来的模型不容易过拟合单一场景。2.2 每个场景包含什么全分辨率图、子孔径视图、视差图DPDD 的官方数据组织方式很有代表性。每个场景目录下通常能拿到以下核心文件文件/内容作用说明Left/Right 子孔径视图从双像素 RAW 中分别提取左右视角RGB 三通道全分辨率图全分辨率模糊图左右子孔径视图合成后的常规图像或直接用传感器原始输出视差图Disparity/Aberration Map描述左右视图之间像素偏移量能反映场景深度信息小光圈清晰图GT近全聚焦的清晰图像用于监督训练我在实际使用中发现很多初次接触 DPDD 的人会把“全分辨率模糊图”和“左/右子孔径视图”混为一谈。这里需要明确全分辨率模糊图是一张常规的彩色图像它不区分左右视角而子孔径视图是分别从每个像素的左半部分和右半部分提取出来并做去马赛克后得到的图像两者之间存在亚像素到数个像素量级的视差。这个视差虽然肉眼几乎不可见但在数值上足够提供深度估计信号。视差图是 DPDD 区别于其他去模糊数据集的关键。官方提供了视差图但我没有查到非常确定的算法细节推测是基于左右子孔径视图的光流估计得到。实际操作中如果你自己从 DNG 提取子孔径视图可以自行用 RAFT 或 PWC-Net 这类光流模型生成视差图效果完全够用。有了视差图模糊核的估计就变成了一个显式建模问题网络也能更容易学到“深度—模糊程度”的映射关系。2.3 数据划分与场景分布DPDD 官方将 500 组场景划分为训练集、验证集和测试集具体数量为训练集 350 组、验证集 74 组、测试集 76 组。划分时尽量保证场景不重叠避免同一地点出现在多个集合中造成信息泄露。这一点很重要因为如果训练集和测试集中有相似的场景背景模型可能通过识别背景来“作弊”导致指标虚高。场景分布上室内约占一半左右以桌面摆件、家具、办公环境为主室外以植被、建筑、街景为主。室外场景的挑战在于光照变化和更多的高频纹理因此即便在测试集上 PSNR 掉一些也属于正常现象。我在实验里单独统计过室内测试集的 PSNR 普遍比室外高 0.5 到 1 dB这和场景内容的纹理复杂度直接相关。3. 子孔径视图提取与预处理3.1 双像素 RAW 格式与子孔径合成原理双像素 RAW佳能叫 DPRAW文件中每个像素位点存储了两个值左半部分光电二极管的光强 L 和右半部分的光强 R。常规处理流程只把 (LR)/2 作为该像素的亮度值得到普通图像而提取子孔径视图时需要把 L 值和 R 值分别单独拿出来构成两张“半像素亮度图”。这里有一个关键问题由于每个像素被分成了左右两半等效水平采样率降低了一半直接 raw-pixel 输出会有拜耳滤镜的排列问题。实际操作中最稳定的做法是使用 LibRaw 的 dual pixel 提取功能或者用佳能官方 SDK 解析 DPRAW。LibRaw 处理流程大概是# 安装 libraw 相关工具 # 假设 input.CR3 是佳能双像素RAW文件 dcraw -v -D -4 -j input.CR3但 dcraw 默认不会直接输出左右视图更多时候需要自己解析 raw 数据。在 Python 环境下我建议直接使用 rawpy 库它封装了 LibRaw 的底层接口。拿到 raw 数据后对每个像素位点拆分 L 和 R再分别做去马赛克demosaic就能得到两张全分辨率的左右子孔径 RGB 图。3.2 提取流程与关键参数我自己在实际项目中走的完整流程是这样的先用 rawpy 读取 DPRAW 文件拿到每个像素位点的两段数据然后按 Bayer 排列把 L 通道和 R 通道分别拼成 raw 灰度图最后用 OpenCV 的cvtColor配合COLOR_BayerRG2RGB或者更高质量的 demosaic 算法比如 OpenCV 的COLOR_BAYER_RGGB_EA还原成 RGB。这里我贴一段可以用的提取代码import rawpy import numpy as np import cv2 def extract_dual_pixel_views(raw_path): raw rawpy.imread(raw_path) # raw.raw_image 是原始 Bayer 数据 # raw.raw_colors 表示每个点属于哪种 Bayer 颜色 bayer raw.raw_image.astype(np.float32) # 双像素拆分偶数行/奇数列这种排列取决于相机型号 # 以 5D4 为例左右子像素在水平方向相邻 h, w bayer.shape left_view bayer[:, 0::2].copy() # 每行取左半 right_view bayer[:, 1::2].copy() # 每行取右半 # 对左右视图分别做去马赛克 # 这里需要先按 Bayer 顺序重新排列具体顺序看 raw.color_desc left_rgb demosaic_bayer(left_view, raw.raw_colors[:, 0::2]) right_rgb demosaic_bayer(right_view, raw.raw_colors[:, 1::2]) raw.close() return left_rgb, right_rgb def demosaic_bayer(bayer_plane, color_map): # 简化的去马赛克实际项目建议用 OpenCV 或 LibRaw 的 demosaic bayer_uint np.clip(bayer_plane, 0, 65535).astype(np.uint16) # BGGR 排列时使用对应转换码 rgb cv2.cvtColor(bayer_uint, cv2.COLOR_BayerBG2RGB_EA) return rgb代码只做示意实际生产环境需要根据 raw.raw_pattern 动态确定 Bayer 排列。我踩过的坑是不同相机甚至同型号不同固件的 raw_pattern 可能不同如果写死 BGGR提取出的子孔径视图会出现颜色错位肉眼看不明显但训练时 loss 很难降下去。另一个关键参数是白平衡和颜色校正。直接用 raw 数据跑出来的图像偏绿偏暗需要做黑电平减除、白平衡归一化以及相机颜色矩阵转换。建议在拆分左右视图之前先完成黑电平减除再对左右视图统一做白平衡保证两张图的光照统计一致。3.3 视差图计算与对齐问题视差图是 DPDD 方法中衔接“硬件双像素输出”和“模糊核估计”的桥梁。官方数据集提供的视差图质量不错但如果你想扩展自己的数据就必须自己算。我推荐先用 RAFT 光流估计左视图到右视图的水平位移然后取 x 方向的光流作为视差图。因为双像素的基线是水平的垂直方向的位移基本为 0可以直接忽略。有一个很重要的细节左右子孔径视图的曝光和亮度基本一致但存在微小的镜头暗角差异直接算光流可能会在图像边缘引入误差。更稳妥的做法是先对左右视图做全局直方图匹配再做光流估计最后对视差图做中值滤波去噪。我试过直接裸算流边缘区域的视差会出现条纹状伪影影响后续训练。图像对齐方面左右视图理论上已经对齐到像素级不存在大的平移误差。但如果你拿到了全分辨率模糊图由相机内部合成它与左右视图之间的配准可能偏差 1 到 2 个像素。网络训练时如果直接拿三张图拼接可能会学到模糊的伪影。建议在数据预处理阶段用 ECC 或光流做一个粗对齐把全分辨率模糊图向左右视图的中间位置对齐一次。4. 核心代码实现与训练流程4.1 数据加载与 Batch 组织DPDD 的官方代码是基于 TensorFlow 的整体逻辑是标准的有监督训练。不过我自己更习惯 PyTorch 的生态所以下面的代码实现思路是 PyTorch 版本的工程化方案。第一步是数据加载。由于每张图都是 6720x4480 的全分辨率直接整图读进 GPU 不现实常规做法是随机裁剪成 256x256 或 512x512 的 patch。我用的数据加载器大致是这样的流程import torch from torch.utils.data import Dataset import cv2 import numpy as np import glob class DPDDDataset(Dataset): def __init__(self, root_dir, patch_size256, is_trainTrue): self.root_dir root_dir self.patch_size patch_size self.is_train is_train self.samples [] # 目录结构: root_dir/train/001/left.png, right.png, gt.png, disparity.png for scene_dir in sorted(glob.glob(f{root_dir}/{train if is_train else val}/*)): left_path f{scene_dir}/left.png right_path f{scene_dir}/right.png blur_path f{scene_dir}/blur.png gt_path f{scene_dir}/gt.png disp_path f{scene_dir}/disparity.png self.samples.append({ left: left_path, right: right_path, blur: blur_path, gt: gt_path, disp: disp_path }) def __len__(self): return len(self.samples) def __getitem__(self, idx): sample self.samples[idx] # 由于图像太大每次读入后裁剪 left cv2.imread(sample[left]) right cv2.imread(sample[right]) blur cv2.imread(sample[blur]) gt cv2.imread(sample[gt]) disp cv2.imread(sample[disp], cv2.IMREAD_UNCHANGED) h, w, _ left.shape # 随机裁剪 y np.random.randint(0, h - self.patch_size) if h self.patch_size else 0 x np.random.randint(0, w - self.patch_size) if w self.patch_size else 0 def crop(img): return img[y:yself.patch_size, x:xself.patch_size] left_p crop(left); right_p crop(right) blur_p crop(blur); gt_p crop(gt) disp_p crop(disp) # 转 float 并归一化到 [0, 1] # 训练时可以做随机翻转和旋转增强 ... return { left: torch.from_numpy(left_p).permute(2,0,1).float() / 255.0, right: torch.from_numpy(right_p).permute(2,0,1).float() / 255.0, blur: torch.from_numpy(blur_p).permute(2,0,1).float() / 255.0, gt: torch.from_numpy(gt_p).permute(2,0,1).float() / 255.0, disp: torch.from_numpy(disp_p).float().unsqueeze(0) / 255.0 }一个容易忽略的点视差图通常以 PNG 的 16bit 或单通道浮点形式存储读取时不能直接用cv2.imread的默认参数要用IMREAD_UNCHANGED。否则视差值会被压缩到 0-255深度信息丢失一大半。4.2 网络架构与损失函数设计DPDD 原论文的网络结构本质是 U-Net 变体输入除了模糊图之外还把左右子孔径视图作为额外的输入通道。整个网络的前向过程可以理解为先用卷积层从左右视图中提取深度相关的特征再与模糊图特征融合最后通过解码器生成清晰图。我用 PyTorch 重写时保持了类似思路但稍作调整import torch.nn as nn class DoubleConv(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.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue) ) def forward(self, x): return self.conv(x) class DPDDNet(nn.Module): def __init__(self, in_ch7, out_ch3): super().__init__() # 输入 7 通道: blur(3) left(3) disparity(1) self.inc DoubleConv(in_ch, 64) self.down1 nn.Sequential(nn.MaxPool2d(2), DoubleConv(64, 128)) self.down2 nn.Sequential(nn.MaxPool2d(2), DoubleConv(128, 256)) self.down3 nn.Sequential(nn.MaxPool2d(2), DoubleConv(256, 512)) self.up2 nn.Sequential(nn.Upsample(scale_factor2, modebilinear, align_cornersFalse), DoubleConv(512, 256)) self.up3 nn.Sequential(nn.Upsample(scale_factor2, modebilinear, align_cornersFalse), DoubleConv(256, 128)) self.up4 nn.Sequential(nn.Upsample(scale_factor2, modebilinear, align_cornersFalse), DoubleConv(128, 64)) self.outc nn.Conv2d(64, out_ch, 1) def forward(self, blur, left, disp): # 通道拼接 x torch.cat([blur, left, disp], dim1) x1 self.inc(x) x2 self.down1(x1) x3 self.down2(x2) x4 self.down3(x3) x self.up2(x4) x3 # 简单跳连实际可用更复杂的特征融合 x self.up3(x) x self.up4(x) out self.outc(x) return out这里的in_ch7设计是参考了官方思路中的多路输入模糊图3 通道 左视图3 通道 视差图1 通道。你在网上搜到的一些复现版本会把左右视图都塞进去也就是 3 3 3 1 共 10 通道输入。我实测下来只加左视图和视差图效果与同时加左右视图接近但计算量少一些。原因在于左右视图的差异信息已经隐含在视差图里了左右两路都输入会带来一定的信息冗余。损失函数方面官方用了 L1 损失加感知损失Perceptual Loss的组合。纯 L1 损失容易导致输出图像偏平滑边缘不够锐利感知损失则通过 VGG 中间层的特征距离来约束纹理重建。我实际项目中的损失配置如下import torch.nn.functional as F from torchvision.models import vgg16 # 使用 VGG16 的 relu1_2, relu2_2, relu3_3 作为感知特征 vgg vgg16(pretrainedTrue).features[:16].cuda().eval() # 冻结参数 for p in vgg.parameters(): p.requires_grad False def perceptual_loss(pred, target): pred_feat vgg(pred) target_feat vgg(target) loss 0 for f1, f2 in zip(pred_feat, target_feat): loss F.l1_loss(f1, f2) return loss def dPDD_loss(pred, target): l1 F.l1_loss(pred, target) perc perceptual_loss(pred, target) return l1 0.1 * perc感知损失的权重我常用 0.1 到 0.2 之间太大会把纹理搞得过于“锐化”反而不自然。训练初期可以先用纯 L1 跑几个 epoch等到 loss 曲线平稳后再加入感知损失这样收敛更快也不容易出现颜色偏色。4.3 训练与评估要点训练时最大的工程问题是数据吞吐量。6720x4480 的全分辨率图即使裁剪成 patch每张图的 IO 开销也非常大。我一开始直接读取整图再裁剪训练速度被磁盘 IO 卡死GPU 利用率不到 50%。后来改用多进程异步加载 内存缓存方案每个 epoch 先把所有样本按 patch 采样好放入内存队列再交给 GPU 训练速度提升明显。另一个关键点是批次大小的选择。256x256 patch 下Batch Size 16 在 24GB 显存的卡上刚好可以跑如果要上 512 分辨率Batch Size 需要降到 4 以下。我建议按自己显存来先跑 256 分辨率确认模型结构没问题再上 512 做最终训练。训练 60 到 100 个 epoch 后PSNR 基本稳定在 26 到 29 dB 之间视场景复杂度而定继续训练收益不大。评估指标上除了 PSNR 和 SSIM我强烈建议大家把 LPIPS 也加上。原因后面会详细说。整个训练过程要把训练集和验证集的指标都记录下来方便观察是否过拟合。DPDD 数据集的场景差异大验证集 loss 有时候会突然反弹这时候不要着急调参可以先看看是不是某个验证场景刚好有极端大光圈模糊。5. 常见问题与避坑指南5.1 全分辨率图像内存爆炸怎么破这个问题几乎是每个人第一次跑 DPDD 都会遇到的。一张 6720x4480 的 RGB 图用 float32 表示就是 361MB如果再把左右视图和 GT 一起读进内存一组数据就要吃掉 1GB 以上。训练一个 batch 16光数据就 16GB 起步还没算模型参数和中间梯度。我的解决方案是三层第一文件系统层用 PNG 或 WebP 无损压缩存储大幅减少 IO 压力第二数据加载层用内存映射np.memmap读取图像避免一次性载入第三训练时确保只裁剪需要的 patch 区域绝不整图进 GPU。还有一个小技巧把 6720x4480 的图像先下采样到 3360x2240 作为“缩略图”做数据探索和调试时用缩略图跑通流程最后再切到全分辨率做正式训练。这个流程能帮你快速定位代码问题避免每次调试都等半天 IO。5.2 子孔径视图不对齐的排查思路有朋友反馈说左右视图看起来有重影模型训练不收敛。排查顺序一般是这样的先看提取阶段是否正确。如果你用 rawpy 解析 DPRAW注意 raw.raw_image 的存储顺序。有些相机的 DPRAW 子像素排列是“水平成对”有些是“垂直成对”提取时必须按 raw.raw_pattern 动态计算不能写死。确认这一点后把左右视图叠在一起做差如果差值图呈现明显的边缘纹理说明基本对齐如果差值图呈现黑白相间的网格说明 Bayer 排列搞错了。再看光流计算阶段。如果左右视图亮度不一致光流估计会偏向暗区导致视差图有整体偏移。建议先计算左右视图的全局均值做一次线性亮度校准再跑光流。最后看数据增强阶段。我犯过的错误是用了随机平移增强但只对模糊图和 GT 做了平移左右视图和视差图没有同步。这直接破坏了数据一致性模型最后学出一堆伪影。所有数据增强操作必须对 blur、left、right、gt、disp 五张图完全同步尤其是裁剪、翻转、旋转否则训练毫无意义。5.3 评价指标的可信度问题PSNR 和 SSIM 是图像复原的通用指标但在 DPDD 这种大光圈脱焦场景下这两个指标容易被“平滑”区域拉高。模型只要把平坦区域恢复得不错PSNR 就能到 27 dB 以上但边缘区域的模糊问题可能完全没解决。所以我在评估时一定会加 LPIPSLearned Perceptual Image Patch Similarity。LPIPS 在感知层面对图像质量评分对边缘锐利度和纹理保真度更敏感。我实测过很多模型PSNR 相近的两个模型LPIPS 可能差 20%而 LPIPS 更低的模型在视觉效果上明显更清晰。如果你做的是论文复现建议三个指标全报否则审稿人大概率会质疑你指标的意义。另外评估时要注意裁剪边界的影响。网络在边界处因为 padding 和感受野的限制输出质量通常较差。如果评估时把边界像素也算进 PSNR会低估模型性能。常规做法是每张测试图的边缘裁剪 16 到 32 个像素后计算指标。DPDD 官方报告的结果里也暗示了类似的评估策略。6. 实操心得与扩展方向整个 DPDD 项目做下来我最大的感受是双像素数据带来的提升远超过“多了一张输入图”那么简单。它本质上是把深度信息以一种硬件天然提供的方式融入了去模糊流程让网络不用去学习和猜测深度结构而是直接读取现成的线索。这一点在模糊程度极端大的区域比如强光斑、焦外光点表现尤其明显单图模型在这些位置经常产生振铃和伪色而双像素模型基本能保持干净的轮廓。如果你打算把这个数据集用在自有场景上我有几个建议。第一尽量保留视差图的原始精度不要为了存储方便压缩成 8bit。视觉模型的性能上限很大程度上取决于输入信息的分辨率视差图本来就是连续值压到 8bit 就是自断一臂。第二在扩展数据时不要只拍静物也拍一些有轻微动态的场景比如人稍微动一下这样能提高模型的泛化能力避免只在静态数据上表现好。我自己还有个后续扩展方向是把 DPDD 的数据用于“深度估计 去模糊”联合任务。左右子孔径视图的视差本身就是弱深度监督而清晰图又提供了强结构约束。如果先把网络改成多任务输出清晰图 深度图再用 DPDD 的视差图做辅助监督理论上能把两个任务的性能同时拉起来。我目前已在一些小规模实验上得到初步验证后续数据够了再单独写篇文章分享。本文还有配套的精品资源点击获取