2. 深度学习去马赛克

任务目标

本案例训练一个轻量级U-Net模型,将单通道RAW Bayer图像(RGGB模式)转换为三通道RGB图像,并与双线性插值去马赛克方法进行PSNR和SSIM对比。核心目的是验证深度学习去马赛克相比传统插值方法的优势。

环境与依赖

需要Python 3.8+,安装以下依赖:

pip install torch torchvision numpy opencv-python scikit-image

数据准备与预处理

使用公开数据集(如MIT-Adobe FiveK或自建数据集)中的RGB图像,按以下流程生成训练对:

  1. 将RGB图像裁剪为128×128的patch;
  2. 模拟CFA采样:从RGB中提取RGGB Bayer模式,生成单通道RAW图像;
  3. 输入为RAW单通道图(归一化到[0,1]),标签为原始RGB三通道图。

Bayer采样核心逻辑:

def rgb_to_bayer(rgb):
    h, w, _ = rgb.shape
    bayer = np.zeros((h, w), dtype=np.float32)
    bayer[0::2, 0::2] = rgb[0::2, 0::2, 0]  # R
    bayer[0::2, 1::2] = rgb[0::2, 1::2, 1]  # G
    bayer[1::2, 0::2] = rgb[1::2, 0::2, 1]  # G
    bayer[1::2, 1::2] = rgb[1::2, 1::2, 2]  # B
    return bayer

U-Net模型实现

采用轻量级U-Net结构,输入1通道,输出3通道,编码器3层下采样,解码器对应上采样,跳跃连接保留细节:

import torch
import torch.nn as nn
import torch.nn.functional as F

class DoubleConv(nn.Module):
    def __init__(self, in_ch, out_ch):
        super().__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(in_ch, out_ch, 3, padding=1),
            nn.BatchNorm2d(out_ch),
            nn.ReLU(inplace=True),
            nn.Conv2d(out_ch, out_ch, 3, padding=1),
            nn.BatchNorm2d(out_ch),
            nn.ReLU(inplace=True)
        )
    def forward(self, x):
        return self.conv(x)

class UNet(nn.Module):
    def __init__(self, in_channels=1, out_channels=3, features=32):
        super().__init__()
        self.enc1 = DoubleConv(in_channels, features)
        self.enc2 = DoubleConv(features, features*2)
        self.enc3 = DoubleConv(features*2, features*4)
        self.pool = nn.MaxPool2d(2)
        self.bottleneck = DoubleConv(features*4, features*8)
        self.up3 = nn.ConvTranspose2d(features*8, features*4, 2, stride=2)
        self.dec3 = DoubleConv(features*8, features*4)
        self.up2 = nn.ConvTranspose2d(features*4, features*2, 2, stride=2)
        self.dec2 = DoubleConv(features*4, features*2)
        self.up1 = nn.ConvTranspose2d(features*2, features, 2, stride=2)
        self.dec1 = DoubleConv(features*2, features)
        self.out = nn.Conv2d(features, out_channels, 1)

    def forward(self, x):
        e1 = self.enc1(x)
        e2 = self.enc2(self.pool(e1))
        e3 = self.enc3(self.pool(e2))
        b = self.bottleneck(self.pool(e3))
        d3 = self.dec3(torch.cat([self.up3(b), e3], dim=1))
        d2 = self.dec2(torch.cat([self.up2(d3), e2], dim=1))
        d1 = self.dec1(torch.cat([self.up1(d2), e1], dim=1))
        return torch.sigmoid(self.out(d1))

训练与评估脚本

import numpy as np
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
from skimage.metrics import peak_signal_noise_ratio as psnr
from skimage.metrics import structural_similarity as ssim

class BayerDataset(Dataset):
    def __init__(self, rgb_images):
        self.rgb_images = rgb_images  # list of HxWx3 float32 arrays in [0,1]
    def __len__(self):
        return len(self.rgb_images)
    def __getitem__(self, idx):
        rgb = self.rgb_images[idx]
        bayer = rgb_to_bayer(rgb)
        bayer_t = torch.from_numpy(bayer).unsqueeze(0)
        rgb_t = torch.from_numpy(rgb.transpose(2,0,1))
        return bayer_t, rgb_t

def train(model, loader, epochs=30, lr=1e-3):
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model.to(device)
    opt = optim.Adam(model.parameters(), lr=lr)
    loss_fn = nn.L1Loss()
    model.train()
    for epoch in range(epochs):
        total_loss = 0
        for x, y in loader:
            x, y = x.to(device), y.to(device)
            opt.zero_grad()
            pred = model(x)
            loss = loss_fn(pred, y)
            loss.backward()
            opt.step()
            total_loss += loss.item()
        print(f'Epoch {epoch+1}/{epochs}, Loss: {total_loss/len(loader):.4f}')

def evaluate(model, test_rgb):
    """test_rgb: list of HxWx3 float32 arrays"""
    device = next(model.parameters()).device
    model.eval()
    psnr_unet, ssim_unet = [], []
    psnr_bilinear, ssim_bilinear = [], []
    with torch.no_grad():
        for rgb in test_rgb:
            bayer = rgb_to_bayer(rgb)
            # U-Net prediction
            x = torch.from_numpy(bayer).unsqueeze(0).unsqueeze(0).to(device)
            pred = model(x).squeeze(0).cpu().numpy().transpose(1,2,0)
            pred = np.clip(pred, 0, 1)
            # Bilinear interpolation (OpenCV demosaicing)
            bayer_8u = (bayer*255).astype(np.uint8)
            bilinear = cv2.cvtColor(bayer_8u, cv2.COLOR_BayerBG2RGB).astype(np.float32)/255.0
            # Compute metrics
            psnr_unet.append(psnr(rgb, pred, data_range=1.0))
            ssim_unet.append(ssim(rgb, pred, channel_axis=2, data_range=1.0))
            psnr_bilinear.append(psnr(rgb, bilinear, data_range=1.0))
            ssim_bilinear.append(ssim(rgb, bilinear, channel_axis=2, data_range=1.0))
    print(f'U-Net    - PSNR: {np.mean(psnr_unet):.2f} dB, SSIM: {np.mean(ssim_unet):.4f}')
    print(f'Bilinear - PSNR: {np.mean(psnr_bilinear):.2f} dB, SSIM: {np.mean(ssim_bilinear):.4f}')

运行方式

# 1. 准备数据(示例:加载RGB图像列表)
# rgb_list = [cv2.cvtColor(cv2.imread(f), cv2.COLOR_BGR2RGB).astype(np.float32)/255.0 for f in image_files]

# 2. 划分训练/测试集
# train_rgb, test_rgb = rgb_list[:800], rgb_list[800:]

# 3. 创建数据加载器
# dataset = BayerDataset(train_rgb)
# loader = DataLoader(dataset, batch_size=16, shuffle=True)

# 4. 训练
# model = UNet()
# train(model, loader, epochs=30)

# 5. 评估
# evaluate(model, test_rgb)

验证方法与通过标准

运行上述训练脚本后,观察测试集上的评估输出。判定训练成功的标准:

  • 训练损失随epoch稳定下降,最终L1损失低于0.05;
  • U-Net的PSNR比双线性插值高至少2dB,SSIM高至少0.02;
  • U-Net输出图像无明显伪彩色(边缘无红蓝交错),细节纹理清晰。

若未达到上述指标,可增加训练epoch数至50或调整学习率为5e-4。

常见问题

  • Bayer模式匹配:OpenCV的COLOR_BayerBG2RGB对应RGGB排列,需与rgb_to_bayer中的采样位置一致;
  • 显存不足:降低batch_size至8或裁剪patch为64×64;
  • 输出偏灰:检查是否对输出做了sigmoid激活,且标签归一化在[0,1]区间。