基于PyTorch实现U-Net的路面裂缝检测系统

1. 引言

1.1 路面裂缝检测的背景与意义

路面作为交通运输的基础设施,其健康状况直接关系到行车安全与舒适性。随着使用年限增长及自然环境因素(如温度变化、雨水侵蚀、车辆荷载)的影响,路面会出现不同程度的损坏,其中裂缝是最常见、最早期的一种病害形式。裂缝如果不及时修补,会进一步扩展形成坑槽、沉陷等严重问题,增加修复成本并威胁交通安全。因此,定期对路面进行裂缝检测,评估路面状况,对于道路养护决策具有重要的实际意义。

传统的路面裂缝检测主要依赖人工巡检,即由专业人员目视检查或借助简单工具测量。这种方式存在诸多弊端:效率低下、主观性强、成本高昂,且对交通干扰大。近年来,随着计算机视觉和深度学习技术的飞速发展,基于图像的路面裂缝自动检测成为研究热点,旨在利用摄像头采集路面图像,通过算法自动识别并分割裂缝区域,实现高效、客观、低成本的检测。

1.2 传统检测方法的局限性

早期的裂缝检测方法主要基于图像处理技术,如边缘检测(Canny、Sobel)、阈值分割(Otsu)、形态学处理等。这些方法通常针对裂缝的灰度特征(裂缝较暗)或纹理特征设计手工特征,但在复杂路面背景(如阴影、油污、车道线、光照不均)下鲁棒性较差,容易产生大量误检或漏检。此外,传统方法难以处理裂缝形态的多样性(宽度、方向、连续性变化),泛化能力有限。

1.3 深度学习在图像分割中的应用

深度学习的兴起为图像分割带来了革命性变化。卷积神经网络(CNN)能够自动从数据中学习层次化特征,极大地提升了分割精度。全卷积网络(FCN)首次实现了端到端的像素级分类,随后涌现出许多优秀的语义分割架构,如SegNet、DeepLab系列、PSPNet等。其中,U-Net因其独特的编码器-解码器结构和跳跃连接,在小样本医学图像分割任务中表现出色,并迅速扩展到其他领域,包括路面裂缝检测。

1.4 U-Net的优势及本文目标

U-Net具有以下优点:

  • 对称结构:编码器逐步提取上下文特征,解码器逐步恢复空间细节,符合分割任务需求。

  • 跳跃连接:将编码器的高分辨率特征与解码器的上采样特征融合,保留了边缘等细节信息,有助于裂缝这种细长目标的精确分割。

  • 数据效率高:即使在训练数据有限的情况下也能取得良好效果,适合裂缝数据集规模不大的场景。

  • 灵活性强:可轻松替换编码器为预训练骨干网络(如ResNet),进一步提升性能。

本文旨在基于PyTorch框架,从零开始实现一个完整的U-Net路面裂缝检测系统,涵盖数据集准备、模型搭建、训练、评估、优化及部署的全流程。通过详细的代码注释和理论解释,帮助读者深入理解U-Net的工作原理及裂缝检测任务的技术要点,最终构建一个能够准确分割路面裂缝的实用系统。


2. 图像分割与U-Net原理

2.1 图像分割任务定义

图像分割是将图像划分成若干个具有特定语义含义的区域,每个像素被赋予一个类别标签。在路面裂缝检测中,这是一个二类语义分割问题,即区分每个像素属于“裂缝”或“背景”。输出是与输入图像相同尺寸的掩模(mask),其中裂缝区域像素值为1,背景为0(或255)。

2.2 语义分割与实例分割的区别

  • 语义分割:仅区分类别,不区分同一类别的不同个体。裂缝检测中只需知道哪些像素是裂缝,无需区分每一条裂缝。

  • 实例分割:在语义分割基础上,进一步区分个体实例,如分别标记不同的裂缝。对于裂缝这种连通区域,有时也需要区分不同裂缝,但语义分割已满足大部分需求。

2.3 U-Net架构详解

U-Net由Olaf Ronneberger等人在2015年提出,最初用于医学图像分割。其架构呈U形对称,分为左侧的编码器(contracting path)和右侧的解码器(expanding path)。

https://lmb.informatik.uni-freiburg.de/people/ronneber/u-net/u-net-architecture.png

2.3.1 编码器(下采样路径)

编码器由多个卷积块和下采样层组成,每个卷积块通常包含两个3×3卷积(padding=1保持尺寸不变)和ReLU激活函数,然后接一个2×2最大池化层进行下采样(步长为2),将特征图尺寸减半,同时通道数加倍。这种设计逐渐提取图像的深层语义特征,但空间分辨率降低。

以输入尺寸为572×572的单通道灰度图为例(原始U-Net论文的输入尺寸),第一层卷积后得到568×568的特征图(无padding),但实际实现中常使用padding保持尺寸,简化计算。

2.3.2 解码器(上采样路径)

解码器通过上采样逐步恢复图像分辨率,并与编码器中对应层的特征图进行拼接(跳跃连接)。上采样通常采用转置卷积(或双线性插值+卷积),将特征图尺寸加倍,同时通道数减半。拼接后的特征图经过两个3×3卷积和ReLU激活,进一步融合信息。最后,通过一个1×1卷积将通道数映射为目标类别数(二分类为1或2)。

2.3.3 跳跃连接的作用

跳跃连接将编码器的浅层细节特征(如边缘、纹理)直接传递到解码器,弥补下采样过程中丢失的空间信息,使分割结果边界更精细。对于裂缝这种细长结构,跳跃连接至关重要。

2.3.4 网络各层参数计算

假设输入为单通道图像(H×W),编码器各层尺寸变化如下(以padding=1保持尺寸为例):

层级操作输出尺寸(H×W)通道数
输入-H×W1
编码器块1Conv3x3, ReLU, Conv3x3, ReLUH×W64
下采样1MaxPool2x2H/2 × W/264
编码器块2Conv3x3, ReLU, Conv3x3, ReLUH/2 × W/2128
下采样2MaxPool2x2H/4 × W/4128
编码器块3Conv3x3, ReLU, Conv3x3, ReLUH/4 × W/4256
下采样3MaxPool2x2H/8 × W/8256
编码器块4Conv3x3, ReLU, Conv3x3, ReLUH/8 × W/8512
下采样4MaxPool2x2H/16 × W/16512
瓶颈层Conv3x3, ReLU, Conv3x3, ReLUH/16 × W/161024

解码器:

层级操作输出尺寸(H×W)通道数
上采样1转置卷积(或上采样+卷积)H/8 × W/8512
拼接1与编码器块4的输出拼接(通道数512+512=1024)H/8 × W/81024
解码器块1Conv3x3, ReLU, Conv3x3, ReLUH/8 × W/8512
上采样2转置卷积H/4 × W/4256
拼接2与编码器块3的输出拼接(通道数256+256=512)H/4 × W/4512
解码器块2Conv3x3, ReLU, Conv3x3, ReLUH/4 × W/4256
上采样3转置卷积H/2 × W/2128
拼接3与编码器块2的输出拼接(通道数128+128=256)H/2 × W/2256
解码器块3Conv3x3, ReLU, Conv3x3, ReLUH/2 × W/2128
上采样4转置卷积H×W64
拼接4与编码器块1的输出拼接(通道数64+64=128)H×W128
解码器块4Conv3x3, ReLU, Conv3x3, ReLUH×W64
输出层Conv1x1H×W1(或2)

注意:实际实现中,输入图像尺寸通常会被调整为32的倍数(如256×256),以便下采样过程整除。

2.4 U-Net的变体与发展

  • ResUNet:在U-Net中引入残差连接,缓解梯度消失,加深网络。

  • Attention U-Net:在跳跃连接中加入注意力门控,自动聚焦目标区域。

  • U-Net++:通过嵌套的跳跃连接和密集连接,聚合不同尺度的特征。

  • UNet3+:引入全尺度的跳跃连接,进一步融合多尺度特征。

这些变体在裂缝检测中也有应用,可根据实际需求选择。


3. 路面裂缝数据集介绍与预处理

3.1 公开数据集概览

3.1.1 CrackForest数据集(CFD)
  • 来源:北京航空航天大学

  • 规模:118张路面图像,分辨率约480×320

  • 特点:包含沥青路面裂缝,标注为像素级二值掩模,背景复杂(有树叶、阴影等)

  • 下载:需从官网申请或学术渠道获取

3.1.2 DeepCrack数据集
  • 来源:武汉大学

  • 规模:537张图像,分辨率544×384

  • 特点:包含多种裂缝类型(横向、纵向、网状),标注精细,分为训练集和测试集

  • 下载:GitHub或论文主页

3.1.3 GAPs路面裂缝数据集
  • 来源:德国

  • 规模:约1960张图像,分辨率1920×1080

  • 特点:包含多种路面类型(沥青、混凝土),标注为裂缝骨架或区域

  • 下载:官网注册下载

3.1.4 其他数据集
  • CRACK500:500张街景图像,由手机拍摄,标注粗糙但规模大。

  • CrackTree:包含206张图像,附有裂缝拓扑结构。

本文以DeepCrack为例进行讲解,因为它标注质量高、规模适中且公开易获取。

3.2 数据集特点与标注格式

大多数裂缝数据集提供原始图像和对应的二值掩模图像(通常为8位单通道,裂缝区域像素值为255,背景为0)。有些数据集还提供骨架标注(裂缝中心线)或实例标注,但二值掩模是最常用的。

3.3 数据预处理流程

3.3.1 图像归一化与尺寸调整
  • 归一化:将图像像素值缩放到[0,1]或标准化为均值为0、方差为1。常用方法:除以255。

  • 尺寸调整:由于U-Net下采样要求输入尺寸为2的倍数(最好是32的倍数),通常将图像统一缩放到256×256或512×512。注意:调整尺寸可能会改变裂缝的宽细比例,需权衡。也可以采用裁剪(crop)或填充(padding)的方式保留原始比例。

3.3.2 数据增强策略

数据增强能有效增加训练样本多样性,防止过拟合,尤其对于裂缝这种类别不平衡严重的任务(裂缝像素占比通常小于5%)。常用增强操作:

  • 空间变换:随机水平翻转、垂直翻转、旋转(90°、180°、270°)、仿射变换。

  • 色彩变换:亮度调整、对比度调整、高斯噪声、模糊。

  • 弹性变形:模拟路面不平整导致的图像扭曲,对裂缝这种细长结构特别有效。

  • 裁剪与缩放:随机裁剪、随机缩放后裁剪。

注意:对图像进行的空间变换必须同步应用于对应的掩模,确保对齐。可使用Albumentations库轻松实现。

3.3.3 数据集的划分

将数据集按比例(如70%训练,15%验证,15%测试)划分。由于裂缝图像通常较少,可采用交叉验证或使用固定的公开划分(如DeepCrack已提供)。


4. 开发环境配置

4.1 硬件要求

  • GPU:建议NVIDIA显卡(显存≥4GB),用于加速训练。

  • 内存:≥8GB。

  • 存储:≥20GB(用于存放数据集和模型)。

4.2 软件环境

4.2.1 Python与虚拟环境

推荐使用Python 3.8+,并通过conda或venv创建虚拟环境:

bash

conda create -n crack_detection python=3.8
conda activate crack_detection
4.2.2 PyTorch安装

根据CUDA版本选择安装命令,访问 PyTorch官网 获取。例如CUDA 11.3:

bash

pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu113
4.2.3 依赖库安装

bash

pip install numpy opencv-python matplotlib albumentations tqdm tensorboard scikit-learn

5. U-Net模型PyTorch实现

我们将按照模块化方式构建U-Net,便于理解和修改。

5.1 双卷积层(DoubleConv)

python

import torch
import torch.nn as nn

class DoubleConv(nn.Module):
    """(卷积 => BN => ReLU) * 2"""
    def __init__(self, in_channels, out_channels):
        super().__init__()
        self.double_conv = nn.Sequential(
            nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),
            nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True)
        )

    def forward(self, x):
        return self.double_conv(x)

5.2 下采样模块(Down)

python

class Down(nn.Module):
    """下采样:最大池化 + 双卷积"""
    def __init__(self, in_channels, out_channels):
        super().__init__()
        self.maxpool_conv = nn.Sequential(
            nn.MaxPool2d(2),
            DoubleConv(in_channels, out_channels)
        )

    def forward(self, x):
        return self.maxpool_conv(x)

5.3 上采样模块(Up)

上采样有两种常见方式:

  1. 转置卷积(可学习参数)

  2. 双线性插值上采样 + 卷积

这里采用转置卷积。

python

class Up(nn.Module):
    """上采样:转置卷积 -> 拼接 -> 双卷积"""
    def __init__(self, in_channels, out_channels, bilinear=False):
        super().__init__()
        # 如果是双线性插值,则使用上采样+卷积
        if bilinear:
            self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)
            self.conv = DoubleConv(in_channels, out_channels)
        else:
            self.up = nn.ConvTranspose2d(in_channels, in_channels // 2, kernel_size=2, stride=2)
            self.conv = DoubleConv(in_channels, out_channels)

    def forward(self, x1, x2):
        x1 = self.up(x1)
        # 输入可能尺寸不匹配,需要进行裁剪(如果使用原始U-Net无padding则需裁剪,这里由于padding=1,尺寸一致)
        diffY = x2.size()[2] - x1.size()[2]
        diffX = x2.size()[3] - x1.size()[3]
        x1 = nn.functional.pad(x1, [diffX // 2, diffX - diffX // 2,
                                     diffY // 2, diffY - diffY // 2])
        x = torch.cat([x2, x1], dim=1)  # 拼接通道
        return self.conv(x)

5.4 输出层(OutConv)

python

class OutConv(nn.Module):
    def __init__(self, in_channels, out_channels):
        super(OutConv, self).__init__()
        self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=1)

    def forward(self, x):
        return self.conv(x)

5.5 完整U-Net类

python

class UNet(nn.Module):
    def __init__(self, n_channels, n_classes, bilinear=False):
        super(UNet, self).__init__()
        self.n_channels = n_channels
        self.n_classes = n_classes
        self.bilinear = bilinear

        self.inc = DoubleConv(n_channels, 64)
        self.down1 = Down(64, 128)
        self.down2 = Down(128, 256)
        self.down3 = Down(256, 512)
        factor = 2 if bilinear else 1
        self.down4 = Down(512, 1024 // factor)
        self.up1 = Up(1024, 512 // factor, bilinear)
        self.up2 = Up(512, 256 // factor, bilinear)
        self.up3 = Up(256, 128 // factor, bilinear)
        self.up4 = Up(128, 64, bilinear)
        self.outc = OutConv(64, n_classes)

    def forward(self, x):
        x1 = self.inc(x)
        x2 = self.down1(x1)
        x3 = self.down2(x2)
        x4 = self.down3(x3)
        x5 = self.down4(x4)
        x = self.up1(x5, x4)
        x = self.up2(x, x3)
        x = self.up3(x, x2)
        x = self.up4(x, x1)
        logits = self.outc(x)
        return logits

5.6 模型参数量计算

python

model = UNet(n_channels=3, n_classes=1)
total_params = sum(p.numel() for p in model.parameters())
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Total parameters: {total_params}, Trainable: {trainable_params}")

输出示例:Total parameters: 31,042,625, Trainable: 31,042,625(约3100万参数)。


6. 数据加载器(DataLoader)构建

6.1 自定义Dataset类

我们需要继承torch.utils.data.Dataset,实现__len____getitem__方法。

python

import os
import cv2
import numpy as np
from torch.utils.data import Dataset

class CrackDataset(Dataset):
    def __init__(self, images_dir, masks_dir, transform=None):
        self.images_dir = images_dir
        self.masks_dir = masks_dir
        self.transform = transform
        self.images = sorted(os.listdir(images_dir))

    def __len__(self):
        return len(self.images)

    def __getitem__(self, idx):
        img_name = self.images[idx]
        img_path = os.path.join(self.images_dir, img_name)
        mask_path = os.path.join(self.masks_dir, img_name)  # 假设掩模文件名相同

        image = cv2.imread(img_path)
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)  # 转为RGB
        mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)  # 单通道

        # 二值化掩模(假设裂缝区域为255)
        mask = (mask > 0).astype(np.float32)  # 变为0或1

        if self.transform:
            # 使用albumentations,它需要同时传入image和mask
            augmented = self.transform(image=image, mask=mask)
            image = augmented['image']
            mask = augmented['mask']

        # 转换为tensor并归一化
        image = torch.from_numpy(image).permute(2, 0, 1).float() / 255.0
        mask = torch.from_numpy(mask).unsqueeze(0).float()  # 添加通道维度 [1, H, W]

        return image, mask

6.2 配合数据增强

使用Albumentations库定义增强流水线:

python

import albumentations as A
from albumentations.pytorch import ToTensorV2

# 训练集增强
train_transform = A.Compose([
    A.Resize(256, 256),
    A.HorizontalFlip(p=0.5),
    A.VerticalFlip(p=0.5),
    A.RandomRotate90(p=0.5),
    A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.1, rotate_limit=30, p=0.5),
    A.ElasticTransform(alpha=1, sigma=50, alpha_affine=50, p=0.5),
    A.RandomBrightnessContrast(p=0.2),
    A.GaussNoise(var_limit=(10.0, 50.0), p=0.2),
])

# 验证集/测试集只调整大小
val_transform = A.Compose([
    A.Resize(256, 256),
])

注意:Albumentations的ToTensorV2可以将numpy数组转为torch tensor,但我们在自定义Dataset中手动转换了,因此此处不使用。

6.3 创建DataLoader

python

from torch.utils.data import DataLoader

train_dataset = CrackDataset('data/train/images', 'data/train/masks', transform=train_transform)
val_dataset = CrackDataset('data/val/images', 'data/val/masks', transform=val_transform)

train_loader = DataLoader(train_dataset, batch_size=8, shuffle=True, num_workers=4)
val_loader = DataLoader(val_dataset, batch_size=8, shuffle=False, num_workers=4)

7. 损失函数与评价指标

7.1 二分类分割任务损失函数

7.1.1 交叉熵损失

对于二分类,常用带sigmoid的二进制交叉熵损失(BCEWithLogitsLoss),它内部集成了sigmoid,数值稳定性更好。

python

criterion_bce = nn.BCEWithLogitsLoss()
7.1.2 Dice Loss

Dice系数衡量两个集合的相似度,Dice Loss = 1 - Dice。适用于类别不平衡问题。

python

class DiceLoss(nn.Module):
    def __init__(self, smooth=1e-6):
        super(DiceLoss, self).__init__()
        self.smooth = smooth

    def forward(self, logits, targets):
        # logits: [N, 1, H, W], targets: [N, 1, H, W]
        probs = torch.sigmoid(logits)
        # 展平
        probs = probs.view(-1)
        targets = targets.view(-1)
        intersection = (probs * targets).sum()
        dice = (2. * intersection + self.smooth) / (probs.sum() + targets.sum() + self.smooth)
        return 1 - dice
7.1.3 组合损失

常用BCE和Dice损失的加权和,兼顾像素级精度和区域相似性。

python

class CombinedLoss(nn.Module):
    def __init__(self, weight_bce=0.5, weight_dice=0.5):
        super(CombinedLoss, self).__init__()
        self.weight_bce = weight_bce
        self.weight_dice = weight_dice
        self.bce = nn.BCEWithLogitsLoss()
        self.dice = DiceLoss()

    def forward(self, logits, targets):
        loss_bce = self.bce(logits, targets)
        loss_dice = self.dice(logits, targets)
        return self.weight_bce * loss_bce + self.weight_dice * loss_dice

7.2 评价指标

7.2.1 像素准确率

python

def pixel_accuracy(logits, targets):
    probs = torch.sigmoid(logits)
    preds = (probs > 0.5).float()
    correct = (preds == targets).float().sum()
    total = torch.numel(targets)
    return correct / total
7.2.2 IoU(交并比)

python

def iou_score(logits, targets, smooth=1e-6):
    probs = torch.sigmoid(logits)
    preds = (probs > 0.5).float()
    intersection = (preds * targets).sum()
    union = preds.sum() + targets.sum() - intersection
    iou = (intersection + smooth) / (union + smooth)
    return iou
7.2.3 Dice系数

python

def dice_coef(logits, targets, smooth=1e-6):
    probs = torch.sigmoid(logits)
    preds = (probs > 0.5).float()
    intersection = (preds * targets).sum()
    dice = (2. * intersection + smooth) / (preds.sum() + targets.sum() + smooth)
    return dice
7.2.4 精确率与召回率

python

def precision_recall(logits, targets, smooth=1e-6):
    probs = torch.sigmoid(logits)
    preds = (probs > 0.5).float()
    tp = (preds * targets).sum()
    fp = (preds * (1 - targets)).sum()
    fn = ((1 - preds) * targets).sum()
    precision = tp / (tp + fp + smooth)
    recall = tp / (tp + fn + smooth)
    return precision, recall

8. 训练脚本编写

8.1 设置随机种子

python

def set_seed(seed=42):
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    np.random.seed(seed)
    random.seed(seed)
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False

set_seed(42)

8.2 初始化模型、优化器、损失函数

python

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = UNet(n_channels=3, n_classes=1).to(device)

optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=5)
criterion = CombinedLoss(weight_bce=0.5, weight_dice=0.5)

8.3 训练循环

python

num_epochs = 100
best_val_loss = float('inf')

for epoch in range(num_epochs):
    # 训练阶段
    model.train()
    train_loss = 0.0
    for images, masks in train_loader:
        images = images.to(device)
        masks = masks.to(device)

        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, masks)
        loss.backward()
        optimizer.step()

        train_loss += loss.item() * images.size(0)

    train_loss /= len(train_loader.dataset)

    # 验证阶段
    model.eval()
    val_loss = 0.0
    val_iou = 0.0
    with torch.no_grad():
        for images, masks in val_loader:
            images = images.to(device)
            masks = masks.to(device)
            outputs = model(images)
            loss = criterion(outputs, masks)
            val_loss += loss.item() * images.size(0)
            val_iou += iou_score(outputs, masks).item() * images.size(0)

    val_loss /= len(val_loader.dataset)
    val_iou /= len(val_loader.dataset)

    # 学习率调度
    scheduler.step(val_loss)

    # 打印信息
    print(f'Epoch {epoch+1}/{num_epochs}, Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f}, Val IoU: {val_iou:.4f}')

    # 保存最佳模型
    if val_loss < best_val_loss:
        best_val_loss = val_loss
        torch.save(model.state_dict(), 'best_model.pth')
        print('Model saved.')

8.4 使用TensorBoard可视化

python

from torch.utils.tensorboard import SummaryWriter

writer = SummaryWriter('runs/crack_detection_experiment')
# 在训练循环中添加:
writer.add_scalar('Loss/train', train_loss, epoch)
writer.add_scalar('Loss/val', val_loss, epoch)
writer.add_scalar('IoU/val', val_iou, epoch)

8.5 梯度裁剪(可选)

为防止梯度爆炸,可在反向传播后添加:

python

torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

9. 模型测试与可视化

9.1 加载最佳模型

python

model.load_state_dict(torch.load('best_model.pth'))
model.eval()

9.2 在测试集上评估

python

test_dataset = CrackDataset('data/test/images', 'data/test/masks', transform=val_transform)
test_loader = DataLoader(test_dataset, batch_size=1, shuffle=False)

test_iou = 0.0
test_dice = 0.0
test_acc = 0.0
with torch.no_grad():
    for images, masks in test_loader:
        images = images.to(device)
        masks = masks.to(device)
        outputs = model(images)
        test_iou += iou_score(outputs, masks).item()
        test_dice += dice_coef(outputs, masks).item()
        test_acc += pixel_accuracy(outputs, masks).item()

n = len(test_loader)
print(f'Test IoU: {test_iou/n:.4f}, Dice: {test_dice/n:.4f}, Accuracy: {test_acc/n:.4f}')

9.3 可视化预测结果

python

import matplotlib.pyplot as plt

def visualize_predictions(model, loader, device, num_samples=4):
    fig, axes = plt.subplots(num_samples, 3, figsize=(12, num_samples*4))
    for i, (image, mask) in enumerate(loader):
        if i >= num_samples:
            break
        image = image.to(device)
        mask = mask.cpu().numpy().squeeze()
        with torch.no_grad():
            output = model(image)
            prob = torch.sigmoid(output).cpu().numpy().squeeze()
            pred = (prob > 0.5).astype(np.uint8)

        # 显示原图
        img_np = image.cpu().squeeze().permute(1,2,0).numpy()
        axes[i,0].imshow(img_np)
        axes[i,0].set_title('Original Image')
        axes[i,0].axis('off')

        axes[i,1].imshow(mask, cmap='gray')
        axes[i,1].set_title('Ground Truth')
        axes[i,1].axis('off')

        axes[i,2].imshow(pred, cmap='gray')
        axes[i,2].set_title('Prediction')
        axes[i,2].axis('off')

    plt.tight_layout()
    plt.show()

visualize_predictions(model, test_loader, device)

9.4 结果分析

  • 误差分析:观察预测错误的区域,是裂缝漏检(假阴性)还是背景误检(假阳性)。可能原因:裂缝太细、光照不均、阴影干扰等。

  • 阈值调整:有时默认阈值0.5不是最优,可通过验证集调整阈值(如0.3~0.7)来平衡精确率和召回率。


10. 超参数调优与实验

10.1 学习率与优化器

  • 尝试不同初始学习率:1e-3, 1e-4, 1e-5。

  • 优化器:Adam、SGD(带动量)。

  • 学习率调度:StepLR、ReduceLROnPlateau、CosineAnnealingLR。

10.2 批次大小

根据GPU显存调整,通常越大越好(稳定梯度),但受限于显存。可尝试4、8、16。

10.3 数据增强的影响

设计对比实验:不使用增强 vs 使用基础增强(翻转旋转) vs 使用所有增强(包括弹性变形),观察验证集IoU变化。

10.4 损失函数权重

调整BCE和Dice的权重,例如(0.3,0.7)、(0.7,0.3),观察对分割结果的影响。

10.5 不同骨干网络(编码器)

替换编码器为预训练的ResNet34或EfficientNet,利用迁移学习提升性能。实现时需注意跳跃连接的维度匹配。


11. 模型优化与改进方向

11.1 注意力机制(Attention U-Net)

在跳跃连接中加入注意力门(Attention Gate),使网络聚焦于裂缝区域,抑制无关背景。

python

class AttentionGate(nn.Module):
    def __init__(self, F_g, F_l, F_int):
        super(AttentionGate, self).__init__()
        self.W_g = nn.Sequential(
            nn.Conv2d(F_g, F_int, kernel_size=1, stride=1, padding=0, bias=True),
            nn.BatchNorm2d(F_int)
        )
        self.W_x = nn.Sequential(
            nn.Conv2d(F_l, F_int, kernel_size=1, stride=1, padding=0, bias=True),
            nn.BatchNorm2d(F_int)
        )
        self.psi = nn.Sequential(
            nn.Conv2d(F_int, 1, kernel_size=1, stride=1, padding=0, bias=True),
            nn.BatchNorm2d(1),
            nn.Sigmoid()
        )
        self.relu = nn.ReLU(inplace=True)

    def forward(self, g, x):
        g1 = self.W_g(g)
        x1 = self.W_x(x)
        psi = self.relu(g1 + x1)
        psi = self.psi(psi)
        return x * psi

将AttentionGate插入Up模块中,在拼接前对跳跃连接特征进行加权。

11.2 空洞卷积(Dilated Convolution)

在瓶颈层使用空洞卷积扩大感受野,捕获多尺度上下文信息,对裂缝这种多尺度目标有益。

11.3 条件随机场(CRF)后处理

CRF可以细化分割边界,但会增加推理时间。可使用pydensecrf库实现。

11.4 模型剪枝与量化

  • 剪枝:移除不重要的权重,减小模型大小。

  • 量化:将模型参数从float32转为int8,加速推理,适合部署在边缘设备。


12. 系统部署与应用

12.1 模型导出为TorchScript

python

model.eval()
example_input = torch.randn(1, 3, 256, 256).to(device)
traced_script_module = torch.jit.trace(model, example_input)
traced_script_module.save("unet_crack.pt")

12.2 使用ONNX进行跨平台部署

python

torch.onnx.export(model, example_input, "unet_crack.onnx", 
                  export_params=True, opset_version=11,
                  input_names=['input'], output_names=['output'])

12.3 构建简单的Flask API

python

from flask import Flask, request, jsonify
import io
import base64
from PIL import Image
import torchvision.transforms as transforms

app = Flask(__name__)
model = UNet(n_channels=3, n_classes=1)
model.load_state_dict(torch.load('best_model.pth', map_location='cpu'))
model.eval()

def preprocess(image):
    # 将上传的图像转换为模型输入
    transform = transforms.Compose([
        transforms.Resize((256,256)),
        transforms.ToTensor(),
        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])  # 若使用预训练权重
    ])
    return transform(image).unsqueeze(0)

@app.route('/predict', methods=['POST'])
def predict():
    file = request.files['image']
    img = Image.open(file.stream).convert('RGB')
    input_tensor = preprocess(img)
    with torch.no_grad():
        output = model(input_tensor)
        prob = torch.sigmoid(output).squeeze().cpu().numpy()
        pred = (prob > 0.5).astype(np.uint8) * 255
    # 将预测结果编码为base64返回
    result_img = Image.fromarray(pred)
    buffered = io.BytesIO()
    result_img.save(buffered, format="PNG")
    img_str = base64.b64encode(buffered.getvalue()).decode()
    return jsonify({'mask': img_str})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

12.4 桌面应用或移动端集成

  • 桌面应用:可使用PyQt或Tkinter构建图形界面,调用模型进行单张或批量检测。

  • 移动端:通过ONNX Runtime或TensorFlow Lite将模型集成到Android/iOS App中。


13. 总结与展望

13.1 本文实现总结

本文详细介绍了基于PyTorch实现U-Net路面裂缝检测系统的完整流程,包括:

  • U-Net架构原理与代码实现

  • 数据集准备与预处理

  • 数据加载器构建

  • 损失函数与评价指标设计

  • 训练与验证脚本编写

  • 模型测试与可视化

  • 超参数调优与改进方向

  • 系统部署方案

通过以上步骤,读者可以构建一个能够准确分割路面裂缝的深度学习系统。

13.2 当前方法的局限性

  • 数据依赖:需要大量像素级标注数据,标注成本高。

  • 泛化能力:在不同路面类型、光照条件下可能性能下降。

  • 实时性:U-Net参数量较大,在资源受限设备上推理速度可能不足。

  • 细裂缝检测:极细裂缝(宽度1-2像素)容易漏检。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值