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×W | 1 |
| 编码器块1 | Conv3x3, ReLU, Conv3x3, ReLU | H×W | 64 |
| 下采样1 | MaxPool2x2 | H/2 × W/2 | 64 |
| 编码器块2 | Conv3x3, ReLU, Conv3x3, ReLU | H/2 × W/2 | 128 |
| 下采样2 | MaxPool2x2 | H/4 × W/4 | 128 |
| 编码器块3 | Conv3x3, ReLU, Conv3x3, ReLU | H/4 × W/4 | 256 |
| 下采样3 | MaxPool2x2 | H/8 × W/8 | 256 |
| 编码器块4 | Conv3x3, ReLU, Conv3x3, ReLU | H/8 × W/8 | 512 |
| 下采样4 | MaxPool2x2 | H/16 × W/16 | 512 |
| 瓶颈层 | Conv3x3, ReLU, Conv3x3, ReLU | H/16 × W/16 | 1024 |
解码器:
| 层级 | 操作 | 输出尺寸(H×W) | 通道数 |
|---|---|---|---|
| 上采样1 | 转置卷积(或上采样+卷积) | H/8 × W/8 | 512 |
| 拼接1 | 与编码器块4的输出拼接(通道数512+512=1024) | H/8 × W/8 | 1024 |
| 解码器块1 | Conv3x3, ReLU, Conv3x3, ReLU | H/8 × W/8 | 512 |
| 上采样2 | 转置卷积 | H/4 × W/4 | 256 |
| 拼接2 | 与编码器块3的输出拼接(通道数256+256=512) | H/4 × W/4 | 512 |
| 解码器块2 | Conv3x3, ReLU, Conv3x3, ReLU | H/4 × W/4 | 256 |
| 上采样3 | 转置卷积 | H/2 × W/2 | 128 |
| 拼接3 | 与编码器块2的输出拼接(通道数128+128=256) | H/2 × W/2 | 256 |
| 解码器块3 | Conv3x3, ReLU, Conv3x3, ReLU | H/2 × W/2 | 128 |
| 上采样4 | 转置卷积 | H×W | 64 |
| 拼接4 | 与编码器块1的输出拼接(通道数64+64=128) | H×W | 128 |
| 解码器块4 | Conv3x3, ReLU, Conv3x3, ReLU | H×W | 64 |
| 输出层 | Conv1x1 | H×W | 1(或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)
上采样有两种常见方式:
-
转置卷积(可学习参数)
-
双线性插值上采样 + 卷积
这里采用转置卷积。
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像素)容易漏检。
573

被折叠的 条评论
为什么被折叠?



