简介:面向偏振图像处理与去噪研究的Python工程包,聚焦利用偏振特性增强图像细节并去除噪声,适用于计算机视觉、遥感成像及自动驾驶等场景中的反光/眩光抑制任务,也适合希望动手实践相关算法的学习者和研究者。压缩包共38个文件,约906KB,以Python脚本(.py)与编译缓存(.pyc)为主,另有数据集样例图片(.jpg)、说明文档(.txt)以及Dockerfile等环境配置,便于复现与二次开发。项目中包含多个模型实现,如PCN、SNA、RGBRN等,并配套数据加载器、预处理变换、评价指标与可视化工具,可直接训练或测试;图片中提供合成数据集与真实世界数据集示例,帮助理解偏振图像在不同噪声水平下的表现。目前已有396人学习浏览。通过研读代码与运行示例,可掌握偏振图像去噪的完整流程,包括数据组织、模型搭建、训练评估及结果可视化,是一份兼具学习与工程参考价值的资源。
1. 偏振图像去噪:为什么普通去噪算法在这里不灵
做偏振图像去噪,用 Python 跑通一个含数据集的完整方案,最大的门槛不是模型,而是偏振数据本身的物理约束。普通去噪算法把偏振图当成灰度图一视同仁,结果边缘保住了,偏振度(DoLP)却偏了,下游偏振去雾、偏振目标检测全跟着翻车。我会从偏振噪声模型讲起,到数据集怎么找、怎么自建,再到模型怎么训、参数怎么调,全程按可复现的步骤来。适合刚接触偏振成像的算法工程师,也适合做遥感去噪想转偏振方向的同学。这篇文章不写玄学,只写能直接跑的代码和血泪经验。
2. 偏振图像的噪声模型与数据集选型:先弄清你要去的是哪种噪
偏振图像去噪和普通图像去噪最大的不同在于:噪声并不是加在每个灰度像素上的,而是先加在四个偏振方向的强度测量上,再通过解调传播到 I/Q/U 和 DoLP/AoP。如果只是对着最终的四通道图做高斯去噪,你会把偏振通道的物理关系破坏掉。
2.1 偏振图像到底特殊在哪:I/Q/U/V 与 DoLP/AoP 四通道表示
分焦平面(DoFP)偏振相机最常见的输出是 0°、45°、90°、135° 四个方向的偏振马赛克,每个像素只采集其中一个方向。所以拿到手的第一件事不是去噪,而是解调。工程上常用这组公式把四张插值后的方向图转成 Stokes 参数:
I = (I0 + I45 + I90 + I135) / 4(近似)
Q = I0 - I90
U = I45 - I135
再由 Stokes 参数得到偏振度和偏振角:
DoLP = sqrt(Q^2 + U^2) / I
AoP = 0.5 * atan2(U, Q)
从这个式子能看出为什么普通去噪会翻车:DoLP 是 Q、U 和 I 的比值。噪声让 Q 和 U 变大或变小,都会让 DoLP 出现系统性偏差。尤其在暗区和边缘,I 很小,Q 和 U 里的一点噪声都会被除法放大成伪偏振。
另一个隐蔽问题是噪声的非高斯性。偏振相机的光电探测过程受散粒噪声支配,光子计数服从泊松分布;读出噪声又叠加了近似高斯分量。常用做法是用高斯 + 泊松混合模型描述,而 BM3D、NLM 这类算法只对高斯噪声最有效。更麻烦的是,四个偏振方向通道上的噪声并不独立,因为它们是同一个像素位置分光后的结果,曝光时间、增益完全一致,噪声之间存在相关性。
所以你在设计数据集或网络输入时,最好保留 I/Q/U 四通道(或加 AoP 共五通道),而不是只留下 DoLP 灰度图。把 DoLP 当目标可以,当输入会损失信息。
2.2 常用数据集怎么选:合成噪声还是真实偏振数据
公开的偏振去噪数据集比普通图像少得多,常见的开源项目会附带一个几百张的仿真数据集,结构通常是 train/clean、train/noisy,每张存成四通道 npy 或 tiff。但真实偏振数据往往没有干净真值,只能靠多帧多曝光自监督或物理模型生成标签。
我一般会先确认数据集里是不是带马赛克原始图。很多下载下来的数据集已经给你插值好了 I/Q/U,省事,但会掩盖插值误差,导致模型学不会坏点扩散。如果目标是做相机端去噪,建议找带原始马赛克的数据;如果只是想验证去噪算法,仿真四通道数据就够。
这里顺便说一句,遥感领域常被提到的 Indian Pines 数据集是高光谱影像,不是偏振图像。它的去噪目标是同时保留 200 多个波段的光谱相关性,和偏振的 4 个物理通道结构完全不同。有人会把高光谱的波段选择、张量去噪方法拿来套偏振,结果偏振角的不连续性会让张量分解直接崩掉,不要混用。
还有一类“一维信号去噪”里常用的小波阈值、SG 滤波,它们对偏振图像的零散像素级噪点有一定效果,但无法处理 2x2 偏振周期上的插值伪影。这类方法我只会用来给 I 通道做个粗参考,不会进主线。
2.3 用普通图像合成偏振数据集:一个最省事的可复现方案
没有相机、也没有现成数据时,最稳的办法是自己合成。先把一张干净图像当作总强度 I 的近似,再人工给定 DoLP 和 AoP 分布,反推四个偏振方向强度。这里的关键是让 DoLP 在 0 到 1 之间,且在不同区域变化,不要整张图一个常数,否则模型只会学求均值。
import numpy as np def synthesize_from_rgb(rgb, dolp_map, aop_map): """ rgb: (H, W, 3),0-1 浮点,作为总强度近似 dolp_map: (H, W),取值 0-1,表示偏振度 aop_map: (H, W),取值范围,建议 -pi/2 到 pi/2 返回四个偏振方向强度,以及 I/Q/U """ I = rgb.mean(axis=2).astype(np.float32) Q = dolp_map * I * np.cos(2 * aop_map) U = dolp_map * I * np.sin(2 * aop_map) I0 = (I + Q) / 2 I45 = (I + U) / 2 I90 = (I - Q) / 2 I135 = (I - U) / 2 return I0, I45, I90, I135, I, Q, U逻辑说明:这里的核心假设是偏振测量满足理想 Stokes 模型,四个方向强度由入射光的 I/Q/U 投影得到。dolp_map 可以用边缘检测结果做阈值,把边缘区域设成 0.3-0.6,平坦区域设成 0.05-0.15,这样更接近真实场景。
参数说明:aop_map 的取值范围直接进入 cos(2a) 和 sin(2a),所以不要用 0-255 的灰度当角度。真实相机的 AoP 通常落在 [-90°, 90°],代码里应按弧度传参。合成后若需要模拟马赛克,可以按 2x2 棋盘采样 I0/I45/I90/I135,再用 2.1 的解调恢复四通道,这样就得到一个自带插值误差的真实管线模拟。
生成干净四通道后,再叠加噪声。对偏振方向强度这种计数型物理量,更贴近真实的是先加泊松噪声,再加高斯读出噪声:
def add_poisson_gaussian(img, photon_per_iref=80, sigma=0.02): # img 归一化到 0-1,光子数映射到 photon_per_iref count = np.random.poisson(img * photon_per_iref) / photon_per_iref noisy = count + np.random.normal(0, sigma, img.shape) return np.clip(noisy, 0, 1)逻辑说明:photon_per_iref 控制信噪比,值越大噪声越小,在 30-100 之间调试比较合适。sigma 控制读出噪声,配合前面的泊松分量做混合。这样生成的数据集能同时覆盖高光和暗区,避免模型只学会滤除高斯噪声。
把上述函数封装成 Dataset 类后,再用于 torch 训练。我一般会顺便把每个样本的 epsilon 加到 1e-8,防止 Q=U=0 时 DoLP 计算出现除零。所谓“含所使用数据集”,在多数开源方案里指的就是这种由脚本生成、可复现的仿真数据集;如果拿到真实原始数据,预处理流程也完全能换上去。
3. 用 Python 搭建偏振去噪基线:从 BM3D 到 CNN 的落地实现
有了数据集,第一步别急着上深度学习。先跑一个传统基线,能帮你确认噪声强度、评价指标是否合理。然后在这个基线上做 CNN,才能看出网络到底赢在哪。
3.1 基线选择:为什么先跑 BM3D 而不是直接上网络
BM3D 在适度高斯噪声下表现仍然很能打,而且不需要训练。问题在于它的块匹配会对高频细节做平滑,对偏振图上的纹理边缘不利。我的做法是把四通道拆成 I、Q、U 分别去噪,最后重新合成 DoLP,而不是直接用 BM3D 处理 DoLP 灰度图。因为 DoLP 是比值量,对 Q/U 的噪声做了除法放大,去噪后极易出现偏置。
import cv2 import numpy as np import bm3d def bm3d_all_channels(I, Q, U, sigma_i=0.05, sigma_q=0.08): # 三个通道各自去噪,sigma 按实际噪声水平配 I_d = bm3d.bm3d(I, sigma_psd=sigma_i, stage_arg=bm3d.BM3DStages.ALL_STAGES) Q_d = bm3d.bm3d(Q, sigma_psd=sigma_q, stage_arg=bm3d.BM3DStages.ALL_STAGES) U_d = bm3d.bm3d(U, sigma_psd=sigma_q, stage_arg=bm3d.BM3DStages.ALL_STAGES) return I_d, Q_d, U_d逻辑说明:BM3D 的 sigma_psd 必须和真实噪声标准差同量级。偏振相机里 Q/U 通道的信噪比通常比 I 低,所以这里给了更大的 sigma。如果你把三个通道传同一个 sigma,Q/U 会欠去噪,DoLP 上全是斑点。
参数说明:stage_arg 可以先用 HARD_THRESHOLDING 快速出结果,确认管线通了再换成 ALL_STAGES。BM3D 对低照度下的泊松噪声能力有限,如果发现暗区出现竖条纹,说明 sigma 估计偏小。
3.2 有监督去噪的 Python 实现:数据加载与训练循环示例
传统基线跑通后,深度学习模型的价值在低信噪比和真实噪声下体现。这里用一个很轻的残差 CNN 做例子,输入是 I/Q/U/sinAoP/cosAoP 五通道,输出是干净五通道。不追求分数,重点是讲清输入通道和损失函数。
import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import Dataset, DataLoader class PolarDenoiser(nn.Module): def __init__(self, in_channels=5): super().__init__() layers = [] for i in range(4): ic = in_channels if i == 0 else 64 layers += [ nn.Conv2d(ic, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), ] layers.append(nn.Conv2d(64, in_channels, 3, padding=1)) self.net = nn.Sequential(*layers) def forward(self, x): return x + self.net(x)逻辑说明:残差连接让网络只需要学“噪声”而不是“含噪到干净”的全量映射,训练更稳。BatchNorm 放在卷积后会显著加速收敛,但推理时要求 BN 统计量已冻结,训练和推理模式切换要记得model.eval()。
训练循环简化如下:
def train_one_epoch(model, loader, optimizer, criterion, device): model.train() running = 0.0 for x, y in loader: x, y = x.to(device), y.to(device) optimizer.zero_grad() out = model(x) loss = criterion(out, y) loss.backward() optimizer.step() running += loss.item() * x.size(0) return running / len(loader.dataset)参数说明:optimizer 用 AdamW,初始学习率 1e-3,gamma 0.1 的 StepLR 在第 30/50 个 epoch 衰减。batch size 建议 16 到 32,patch 取 128x128。千万别把 AoP 通道直接归一化到 0-1,因为 AoP 是角度,存在 π 到 -π 的跳变,直接在跳变处回归会让网络只去拟合跳变,丢失偏振信息。正确做法是拆成 sin(AoP) 和 cos(AoP) 两个通道输入,输出再转回角度,这个数据集构建时就要做好。
3.3 评估指标:PSNR/SSIM 之外还要看 DoLP 误差
训练时主要看 loss,但最后下结论不能只看 PSNR。我评估偏振去噪至少看三个维度的指标。
def psnr_batch(pred, gt, max_val=1.0): mse = torch.mean((pred - gt) ** 2, dim=(1,2,3)) return torch.mean(20 * torch.log10(max_val / torch.sqrt(mse))) def dolp_error(pred, gt, eps=1e-8): pred_i, pred_q, pred_u = pred[:, 0], pred[:, 1], pred[:, 2] gt_i, gt_q, gt_u = gt[:, 0], gt[:, 1], gt[:, 2] dolp_pred = torch.sqrt(pred_q**2 + pred_u**2) / (pred_i + eps) dolp_gt = torch.sqrt(gt_q**2 + gt_u**2) / (gt_i + eps) return torch.mean(torch.abs(dolp_pred - dolp_gt))逻辑说明:PSNR 在五通道上整体计算,严格说要对每个通道分别算再平均。这里的 batch 写法是均方误差等效,够用于训练日志。DoLP 误差用绝对差而不是平方差,原因是 DoLP 在暗区的噪声异常大,平方差会把暗区权重抬得过高,绝对差更接近人眼对偏振伪影的感受。
另外建议把 PSNR 拆到 I、Q、U 单独报告。很多模型在 I 通道 PSNR 涨了 1-2 dB,Q/U 却更差,最后 DoLP 反而变差。Q/U 的 PSNR 低于 I 通道 6-8 dB 是正常的,因为 Q/U 真实强度低、噪声占比高。
4. 参数配置与模型训练:让去噪结果不糊边缘的四个关键设置
网络跑通不难,难的是参数调不对时,结果要么太糊、要么偏振角跳跃。下面四个配置是偏振去噪里最影响效果的地方。
4.1 噪声水平估计与 patch 大小
训练前先做噪声估计,能帮你决定数据增强幅度和损失权重。偏振图像里 I 通道噪声低、Q/U 通道噪声高,所以要对每个通道分别估计。
import numpy as np def estimate_noise_sigma(img): if len(img.shape) == 3: img = img.mean(axis=2) pad = np.pad(img, 1, mode='reflect') lap = pad[1:-1, 1:-1] - 0.25 * ( pad[:-2, 1:-1] + pad[2:, 1:-1] + pad[1:-1, :-2] + pad[1:-1, 2:]) return np.median(np.abs(lap)) / 0.6745参数说明:这个方法用小尺度拉普拉斯算子估计噪声,对 0.01-0.1 范围的噪声比较准。大于 0.1 时说明图像本身纹理太丰富,应换成块匹配法(选最小方差块)。
patch 大小对偏振去噪尤其关键。DoFP 马赛克是 2x2 周期,patch 边长必须是偶数,且最好不要小于 64,否则 patch 边界上四个偏振方向采样位置不完整,网络学到的是“多补一块”而不是真正的去噪。我一般用 128x128,batch size 16,显存不够就 96x96,不要切成 32x32 以下。
4.2 损失函数设计:在偏振通道上做加权
有监督训练最常见的组合是 L1 损失 + 通道权重 + DoLP 项。L1 比 L2 更保边,但收敛会留一点偏置;DoLP 项负责纠正比值偏置。我用的权重如下:
def polar_loss(pred, gt, w_ch=None, w_dolp=0.5): w_ch = w_ch or torch.tensor([1.0, 0.8, 0.8, 0.2, 0.2], device=pred.device) ch_loss = torch.mean(torch.abs(pred - gt), dim=(2, 3)) * w_ch.view(1, -1) ch_loss = torch.mean(ch_loss) dolp_l = dolp_error(pred[:, :3], gt[:, :3]) angle = torch.atan2(pred[:, 4], pred[:, 3]) - torch.atan2(gt[:, 4], gt[:, 3]) angle = torch.atan2(torch.sin(angle), torch.cos(angle)) ang_loss = torch.mean(torch.abs(angle)) return ch_loss + w_dolp * dolp_l + 0.2 * ang_loss说明:这里假设通道顺序是 I/Q/U/sinAoP/cosAoP。Q/U 权重 0.8 是因为它们噪声大,不能压得太狠,否则 DoLP 会被抹平。AoP 的权重 0.2 很小,避免角度跳变占主导。你可以在训练日志里同时打印三个 loss,看 DoLP 项是否在 epoch 中下降。
真实场景中如果 DoLP 偏大,把 w_dolp 调大到 1.0;如果边缘处偏振角出现带状伪影,把 ang_loss 的权重调到 0.3 以上。不要在一个数据集上把权重调到极端,换场景容易翻车。
4.3 训练/推理的 trick:分块推理与边界处理
偏振网络的训练 patch 是中心裁剪,推理时要处理任意尺寸。直接整图推断对显存要求高,滑窗拼接更实用。但滑窗重叠区域的边界权重要设好,否则会出现 128 周期的网格,在 DoLP 图上尤其明显。
def tile_inference(model, img, tile=128, overlap=16, device='cuda'): model.eval() h, w = img.shape[-2:] stride = tile - overlap out = torch.zeros_like(img) weight = torch.zeros_like(img) for y in range(0, h - stride, stride): for x in range(0, w - stride, stride): yy = min(y + tile, h) xx = min(x + tile, w) patch = img[:, :, y:yy, x:xx].to(device) with torch.no_grad(): res = model(patch) # 简单的线性淡入淡出权重 wy = torch.ones(patch.size(2), device=device) wx = torch.ones(patch.size(3), device=device) if yy == h: wy[-overlap:] = torch.linspace(0, 1, overlap, device=device) if xx == w: wx[-overlap:] = torch.linspace(0, 1, overlap, device=device) w_hw = wy.view(-1, 1) * wx.view(1, -1) out[:, :, y:yy, x:xx] += res.cpu() * w_hw.view(1, 1, -1, patch.size(3)) weight[:, :, y:yy, x:xx] += w_hw.view(1, 1, -1, patch.size(3)) return out / weight.clamp(min=1e-8)说明:这段代码的关键是 overlap 要能被 2 整除,否则偏振马赛克的相位在左右两块里不一致。overlap 一般取 tile 的 1/8 到 1/4,我常用 16。边界淡入用的是简单线性权重,更精细可以用余弦窗,但对去噪这种低频任务线性就够。
4.4 实际效果验证方法:用消融实验确认哪一层起作用
模型调完后,先别急着看 PSNR。我会做一组消融:1)只去 I 通道,Q/U 不动;2)只去 Q/U,I 不动;3)全通道不加重建;4)全通道加 DoLP 损失。这样能回答“效果来自哪一步”的问题。
这组消融最好在真实偏振数据上做,因为仿真数据里噪声模型和真实相机还是差一截。如果消融显示加 DoLP 损失后 I 通道 PSNR 掉了但 DoLP 误差涨了,说明损失权重需要降低,因为在边缘处保 DoLP 会让 I 保留更多高频纹理,PSNR 统计上会吃点亏。
5. 避坑指南:偏振图像去噪最常见的 5 个坑与排查方法
偏振去噪的坑不像普通图像那么直观,很多问题只能靠经验和日志逐项排。
5.1 现象:去噪后 DoLP 整体偏大,暗区尤其明显
原因:Q/U 通道的噪声不是零均值,BM3D 或 CNN 分离式去噪后,DoLP 的分子项残留正偏置。很多方法在 I 上做得好,Q/U 欠拟合,分母 I 被滤得更平滑,比值自然变大。
解决:在 Q/U 通道进入网络前做一次全局均值扣除(用含噪数据的 Q 均值估算),或者把 DoLP 误差加入损失函数。如果用的是 BM3D 基线,可以把 sigma_q 调大 1.2 倍,通常能压下偏置。
5.2 现象:用 ImageNet 预训练模型改通道后,迁移效果反而差
原因:预训练第一层卷积核是给 3 通道统计特征设计的,直接改成 4 通道并复制权重会导致 Q/U 通道统计不匹配,训练收敛慢。
解决:第一层卷积随机初始化,或者把 Q/U 通道的初始化权重复用 I 的一半。更推荐的做法是用伪 RGB 先在普通图像上自监督预训练,再在偏振数据集上微调。我的经验是微调时冻结前两层 BN,保留自然图像纹理先验。
5.3 现象:训练损失一直在降,但 DoLP 误差曲线不动
原因:网络学到了一个“把 I 做大”的廉价方案,DoLP 因为分母变大而趋近于一个小常数,视觉上就是整体偏暗或偏灰。
解决:把损失从 L2 换成 L1,加入 DoLP 项;同时监控 Q/U 通道的 RMSE。如果 Q/U 的 RMSE 没降而 I 的 RMSE 在降,基本可以确认是这个原因。
5.4 现象:推理时显存爆掉,或马赛克边缘出现周期性棋盘格
原因:整图推理时用一个很大的 tile,模型感受野覆盖不到全局;或者滑窗 overlap 设成了奇数,导致 2x2 偏振周期在拼接处错位。
解决:显存不够时 tile 设为 256 或 128,overlap 保持偶数。棋盘格出现时先查 overlap 是不是奇数以及 patch 边长是否是 8 的倍数。注意制作马赛克数据集时,尽量把每张图裁剪成偶数尺寸,避免采样到半个相位周期。
5.5 现象:某个固定位置的坏像素导致竖条纹
原因:DoFP 相机由于探测器损伤会出现固定坏点,坏点在插值阶段会顺着相邻同角度像素扩散成一条竖线,去噪网络把竖线当成真实结构保留了。
解决:在预处理阶段用中值滤波检测坏点,阈值设为邻域中值的 5 倍,替换后再进插值。检测要在马赛克图上做,不要在 I 图上做,因为马赛克图上坏点和周围像素差异更大,更容易定位。
6. 进阶:偏振去噪模型的可解释性分析与调优技巧
当模型效果稳定后,我习惯用注意力可视化和通道消融进一步榨干收益。比如给四通道各自设置一个 mask,分别置零 Q 或 U,看 DoLP 误差变化。如果 Q 通道置零后误差下降,说明 Q 通道的噪声模型没配对,不是网络的问题,而是数据集里 Q 和 U 的真实噪声方差不同。另一种做法是把训练好的网络第一层卷积核打印出来,观察是否有明显的方向性模式。偏振相关的核通常会有一个低频模板,这说明网络确实在提取偏振相位。
推理端还有个容易被忽略的技巧:把模型切成 fp16。偏振去噪网络一般很小,fp16 推理在同等显存下能把 patch 从 128 提到 192,省一次滑窗遍历。注意 BatchNorm 在 fp16 下容易飘,用torch.cuda.amp自动转换,输入输出保持 fp32,只在内部算子用 fp16,能保住边缘细节。
最后是我自己的习惯:每次训练完,我保留一份“去噪前 + 去噪后 + DoLP 伪彩图”的三图并排截图,不只看指标。很多指标提升亮眼的模型,伪彩图上边缘会像水彩一样晕开,这类问题只有人眼才看得出来。多留这一步,后面调试网络结构会省很多时间。希望上面这些参数和坑能帮到你。
本文还有配套的精品资源,点击获取