前言图像去雾是计算机视觉领域的一个重要问题雾天条件下拍摄的图像通常对比度低、颜色失真严重影响后续视觉任务的性能。本文将详细介绍一种经典的单图像去雾算法——Color Attenuation Prior (CAP)该算法由Zhu等人提出发表于CVPR 2015。与传统的基于暗通道先验DCP的方法不同CAP算法利用颜色衰减先验建立雾天图像中像素的饱和度、亮度与场景深度之间的线性关系从而恢复出清晰的无雾图像。论文地址A Fast Single Image Haze Removal Algorithm Using Color Attenuation Prior仓库代码GitHub - JiamingMai/Color-Attenuation-Prior-Dehazing: MATLAB codes for the paper A Fast Single Image Haze Removal Algorithm using Color Attenuation Prior · GitHub完整python代码可在此处获得PGS2Net/models/baseline/cap at main · Auorui/PGS2Net算法原理在计算机视觉中雾天图像的形成通常用大气散射模型来描述图像去雾的目标就是从观测图像中恢复出、和。CAP算法的核心假设是在雾天图像中场景深度与像素的饱和度和亮度之间存在线性关系。更具体地通过统计分析和回归可以得到其中是像素的亮度是像素的饱和度回归系数随机误差项。根据论文中的实验数据回归系数分别为0.1217790.959710−0.780245。这个先验的直观理解是在雾天场景中远处的物体由于受到更多大气散射的影响饱和度降低而亮度升高。一旦获得深度图d(x)透射率可以通过下式计算其中 β 是用户定义的散射系数控制去雾强度。大气光 A 通常从雾最浓的区域估计。在CAP中选择深度图中最亮的0.1%像素然后选择其中RGB向量范数最大的像素作为大气光值。获得透射率 t 和大气光A后通过求解大气散射模型得到清晰图像代码实现我们使用了原论文的图作为测试下面为使用python的实现import cv2 import numpy as np from scipy.ndimage import minimum_filter class CAP: Color Attenuation Prior Dehazing def __init__( self, beta1.0, radius60, epsilon1e-3, depth_radius15, top_percent0.001, t_min0.05, seed0, ): self.radius radius self.epsilon epsilon self.depth_radius depth_radius self.beta beta self.top_percent top_percent self.t_min t_min self.seed seed staticmethod def _to_float(img): if img.dtype np.float32: return img return img.astype(np.float32) / 255. def _guided_filter(self, I, p): r 2 * self.radius 1 eps self.epsilon I self._to_float(I) p p.astype(np.float32) Ir, Ig, Ib I[:, :, 0], I[:, :, 1], I[:, :, 2] Ir_mean cv2.blur(Ir, (r, r)) Ig_mean cv2.blur(Ig, (r, r)) Ib_mean cv2.blur(Ib, (r, r)) Irr_var cv2.blur(Ir * Ir, (r, r)) - Ir_mean * Ir_mean eps Irg_var cv2.blur(Ir * Ig, (r, r)) - Ir_mean * Ig_mean Irb_var cv2.blur(Ir * Ib, (r, r)) - Ir_mean * Ib_mean Igg_var cv2.blur(Ig * Ig, (r, r)) - Ig_mean * Ig_mean eps Igb_var cv2.blur(Ig * Ib, (r, r)) - Ig_mean * Ib_mean Ibb_var cv2.blur(Ib * Ib, (r, r)) - Ib_mean * Ib_mean eps Irr_inv Igg_var * Ibb_var - Igb_var * Igb_var Irg_inv Igb_var * Irb_var - Irg_var * Ibb_var Irb_inv Irg_var * Igb_var - Igg_var * Irb_var Igg_inv Irr_var * Ibb_var - Irb_var * Irb_var Igb_inv Irb_var * Irg_var - Irr_var * Igb_var Ibb_inv Irr_var * Igg_var - Irg_var * Irg_var cov Irr_inv * Irr_var Irg_inv * Irg_var Irb_inv * Irb_var Irr_inv / cov Irg_inv / cov Irb_inv / cov Igg_inv / cov Igb_inv / cov Ibb_inv / cov p_mean cv2.blur(p, (r, r)) Ipr_mean cv2.blur(Ir * p, (r, r)) Ipg_mean cv2.blur(Ig * p, (r, r)) Ipb_mean cv2.blur(Ib * p, (r, r)) Ipr_cov Ipr_mean - Ir_mean * p_mean Ipg_cov Ipg_mean - Ig_mean * p_mean Ipb_cov Ipb_mean - Ib_mean * p_mean ar Irr_inv * Ipr_cov Irg_inv * Ipg_cov Irb_inv * Ipb_cov ag Irg_inv * Ipr_cov Igg_inv * Ipg_cov Igb_inv * Ipb_cov ab Irb_inv * Ipr_cov Igb_inv * Ipg_cov Ibb_inv * Ipb_cov b p_mean - ar * Ir_mean - ag * Ig_mean - ab * Ib_mean ar cv2.blur(ar, (r, r)) ag cv2.blur(ag, (r, r)) ab cv2.blur(ab, (r, r)) b cv2.blur(b, (r, r)) return ar * Ir ag * Ig ab * Ib b def _cal_depth_map(self, img): np.random.seed(self.seed) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) s hsv[:, :, 1].astype(np.float32) / 255. v hsv[:, :, 2].astype(np.float32) / 255. sigma 0.041337 noise np.random.normal( 0, sigma, (img.shape[0], img.shape[1]) ) depth ( 0.121779 0.959710 * v - 0.780245 * s noise ) depth_refine minimum_filter( depth, (self.depth_radius, self.depth_radius) ) return depth_refine, depth def _estimate_airlight(self, img, depth): img self._to_float(img) h, w depth.shape n int(np.ceil(self.top_percent * h * w)) idx np.argsort(depth.reshape(-1)) pixels img.reshape(-1, 3) candidates pixels[idx[-n:]] mag np.linalg.norm(candidates, axis1) A candidates[np.argmax(mag)] return A def recover(self, img): depth_refine, depth_pixel self._cal_depth_map(img) depth_refine self._guided_filter(img, depth_refine) transmission np.exp(-self.beta * depth_refine) transmission np.clip(transmission, self.t_min, 1) A self._estimate_airlight(img, depth_refine) I self._to_float(img) J (I - A) / transmission[..., None] A return np.clip(J, 0, 1) def __call__(self, img): return self.recover(img) if __name____main__: cap CAP( radius60, epsilon1e-3, depth_radius15, beta1.0 ) I cv2.imread(input.png) J cap(I) cv2.imwrite( output.png, (J * 255).astype(np.uint8) )torch实现如下import torch import torch.nn as nn import torch.nn.functional as F import numpy as np class CAP(nn.Module): Color Attenuation Prior Dehazing PyTorch实现输入输出为torch张量 def __init__( self, beta: float 1.0, guided_filter_radius: int 60, min_filter_radius: int 15, eps: float 1e-3, top_percent: float 0.001, t_min: float 0.05, ): Args: beta: 大气散射系数控制去雾强度 guided_filter_radius: 引导滤波半径 min_filter_radius: 最小值滤波半径 (对应depth_radius) eps: 引导滤波正则化参数 top_percent: 大气光估计时选取最亮区域的百分比 t_min: 最小透射率 super(CAP, self).__init__() self.beta beta self.guided_filter_radius guided_filter_radius self.min_filter_radius min_filter_radius self.eps eps self.top_percent top_percent self.t_min t_min def rgb_to_hsv(self, rgb: torch.Tensor) - torch.Tensor: RGB转HSV (模拟OpenCV的cv2.COLOR_RGB2HSV) Args: rgb: [B, 3, H, W], 值范围 [0, 1] Returns: hsv: [B, 3, H, W], H:[0,1], S:[0,1], V:[0,1] # 分离RGB通道 r, g, b rgb[:, 0:1, :, :], rgb[:, 1:2, :, :], rgb[:, 2:3, :, :] # 计算最大值、最小值 max_val, _ torch.max(rgb, dim1, keepdimTrue) min_val, _ torch.min(rgb, dim1, keepdimTrue) delta max_val - min_val # 计算饱和度 S (OpenCV公式) s torch.where(max_val 0, delta / max_val, torch.zeros_like(max_val)) # 计算亮度 V v max_val # 计算色调 H (OpenCV RGB2HSV公式) h torch.zeros_like(max_val) # 当R为最大值时 mask_r (r max_val) (delta 0) h torch.where(mask_r, ((g - b) / delta) % 6, h) # 当G为最大值时 mask_g (g max_val) (delta 0) h torch.where(mask_g, ((b - r) / delta) 2, h) # 当B为最大值时 mask_b (b max_val) (delta 0) h torch.where(mask_b, ((r - g) / delta) 4, h) h h / 6.0 # 归一化到[0,1] h torch.where(delta 0, torch.zeros_like(h), h) return torch.cat([h, s, v], dim1) def guided_filter(self, guide, target, radius40, eps1e-3): guide: 引导图 (B, C, H, W) 通常用灰度图或原图 target: 待滤波图 (B, 1, H, W) 即透射率图 B, C, H, W guide.shape # 转换为灰度引导图 if guide.shape[1] 3: guide_gray 0.299 * guide[:, 0:1, :, :] 0.587 * guide[:, 1:2, :, :] 0.114 * guide[:, 2:3, :, :] else: guide_gray guide # 确保target和guide_gray尺寸一致 if target.shape[2] ! H or target.shape[3] ! W: # 如果尺寸不一致调整target尺寸 target F.interpolate(target, size(H, W), modebilinear, align_cornersFalse) # 均值滤波可用平均池化替代 def box_filter(x, r): # 使用更高效的实现 kernel torch.ones(1, 1, 2 * r 1, 2 * r 1).to(x.device) / (2 * r 1) ** 2 return nn.functional.conv2d(x, kernel, paddingr, groupsx.shape[1]) mean_g box_filter(guide_gray, radius) mean_t box_filter(target, radius) mean_gt box_filter(guide_gray * target, radius) mean_gg box_filter(guide_gray * guide_gray, radius) var_g mean_gg - mean_g * mean_g cov_gt mean_gt - mean_g * mean_t a cov_gt / (var_g eps) b mean_t - a * mean_g mean_a box_filter(a, radius) mean_b box_filter(b, radius) return mean_a * guide_gray mean_b def compute_depth_map(self, rgb: torch.Tensor) - torch.Tensor: 计算深度图 Args: rgb: [B, 3, H, W], RGB图像, 值范围 [0, 1] Returns: depth: [B, 1, H, W] 深度图 B, C, H, W rgb.shape # 1. RGB转HSV hsv self.rgb_to_hsv(rgb) s hsv[:, 1:2, :, :] # 饱和度 v hsv[:, 2:3, :, :] # 亮度 # 2. 计算深度图 (添加高斯噪声模拟随机性) sigma 0.041337 noise torch.randn_like(v) * sigma depth 0.121779 0.959710 * v - 0.780245 * s noise # 3. 最小值滤波 - 使用更稳健的实现 r self.min_filter_radius # 使用unfold操作实现最小值滤波确保尺寸不变 pad r depth_padded F.pad(depth, (pad, pad, pad, pad), modereflect) depth_refine -F.max_pool2d(-depth_padded, kernel_size2 * r 1, stride1, padding0) # 计算裁剪后的尺寸 crop_h depth_refine.shape[2] - 2 * pad crop_w depth_refine.shape[3] - 2 * pad # 如果尺寸不匹配进行裁剪 if crop_h ! H or crop_w ! W: # 计算裁剪的起始位置 start_h (depth_refine.shape[2] - H) // 2 start_w (depth_refine.shape[3] - W) // 2 depth_refine depth_refine[:, :, start_h:start_h H, start_w:start_w W] else: depth_refine depth_refine[:, :, pad:pad H, pad:pad W] # 限制范围 depth_refine torch.clamp(depth_refine, 0, 1) return depth_refine def estimate_atmosphere(self, rgb: torch.Tensor, depth: torch.Tensor) - torch.Tensor: 估计大气光 Args: rgb: [B, 3, H, W] RGB图像 depth: [B, 1, H, W] 深度图 Returns: A: [B, 3, 1, 1] 大气光值 B, C, H, W rgb.shape # 展平 depth_flat depth.view(B, -1) rgb_flat rgb.view(B, C, -1) # 计算最亮像素的数量 (0.1%) n_bright max(1, int(self.top_percent * H * W)) # 获取最亮像素的索引 _, indices torch.topk(depth_flat, n_bright, dim1) # 收集候选大气光像素 Acand torch.zeros((B, n_bright, C), devicergb.device) for b in range(B): Acand[b, :, :] rgb_flat[b, :, indices[b]].permute(1, 0) # 计算每个候选像素的RGB向量范数 Amag torch.norm(Acand, dim2) # 选择范数最大的像素 _, max_idx torch.max(Amag, dim1) A torch.zeros((B, C, 1, 1), devicergb.device) for b in range(B): A[b, :, 0, 0] Acand[b, max_idx[b], :] return A def forward(self, x: torch.Tensor) - torch.Tensor: 完整去雾流程 Args: x: [B, 3, H, W] 或 [3, H, W], RGB图像, 值范围 [0, 1] 如果输入范围是[-1, 1]会自动转换到[0, 1] Returns: J: [B, 3, H, W] 或 [3, H, W] 去雾后的RGB图像 # 检查输入范围并转换 if x.min() 0: # 检测到输入是[-1,1]范围 x (x 1) / 2 # 转换到[0,1] # 确保是4维张量 if x.dim() 3: x x.unsqueeze(0) single_image True else: single_image False # 1. 计算深度图 depth self.compute_depth_map(x) # 2. 引导滤波细化深度图 depth_refined self.guided_filter(x, depth) # 3. 计算透射率 transmission torch.exp(-self.beta * depth_refined) transmission torch.clamp(transmission, minself.t_min, max1.0) # 4. 估计大气光 A self.estimate_atmosphere(x, depth) # 5. 图像复原 J (x - A) / transmission A J torch.clamp(J, 0, 1) # 恢复原始形状 if single_image: return J.squeeze(0) return J if __name__ __main__: model CAP( beta1.0, guided_filter_radius60, min_filter_radius15, eps1e-3 ) import cv2 img cv2.imread(input.png) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 转换为RGB img_tensor torch.from_numpy(img).float() / 255.0 img_tensor img_tensor.permute(2, 0, 1).unsqueeze(0) with torch.no_grad(): output model(img_tensor) output_np output.squeeze(0).permute(1, 2, 0).numpy() output_np np.clip(output_np * 255, 0, 255).astype(np.uint8) output_np cv2.cvtColor(output_np, cv2.COLOR_RGB2BGR) cv2.imwrite(output_cap.png, output_np)此处torch的实现不如直接对图像进行处理可能是引导滤波和其中的一些核心实现的问题。总结Color Attenuation Prior是一种简洁而有效的单图像去雾算法。它通过建立颜色衰减与场景深度的统计模型避免了复杂的物理参数估计实现了高效的去雾处理。虽然在某些极端场景下可能存在局限性但CAP算法凭借其计算效率和良好的去雾效果在实际应用中仍有重要价值。