ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于YOLOv8的沥青路面裂缝检测系统:龟裂、纵裂、横裂识别与中英文双版实现

基于YOLOv8的沥青路面裂缝检测系统:龟裂、纵裂、横裂识别与中英文双版实现

沥青路面的裂缝检测,这几年在基础设施维护里已经算刚需中的刚需。传统人工巡检靠人眼一路扫过去,效率低不说,长时间盯着路面看,漏检、误记在所难免。后来有人改用边缘检测、阈值分割这类传统图像算法,背景干净些还能跑,一遇到阴影、油渍、新旧路面接缝,误报率直接起飞。这个项目做的就是用 YOLOv8 这套近期更新的检测框架去替代传统方案,把三类最常见的道路裂缝——龟裂、纵向裂缝、横向裂缝——做成一个开箱可用的检测系统,同时附带完整源码与效果演示,代码和界面都做了中英文双版。适合刚入门目标检测的学生、搞交通养护的工程师,以及想拿真实业务场景练手 YOLOv8 的朋友。

1. 项目概述与核心价值拆解

1.1 三类裂缝怎么分,工程上为什么这么分

先把这个最容易被忽略的点说清楚。很多人拿到裂缝数据集第一反应就是“检测裂缝”,把所有裂缝当成一类去训,结果模型确实能检出裂缝,但养护部门没法用——因为不同裂缝的成因、危害程度和处理方式完全不一样。

龟裂,也叫网状裂缝或鳄鱼裂纹,形态是一块区域里出现互相连通的多边形裂纹,看起来像鳄鱼皮,通常由路面结构疲劳引起,多出现在行车轮迹带上。这类裂缝一旦出现,说明路面结构层已经受损,处理往往是大面积修补,不是简单灌缝。

纵向裂缝,走向与行车方向大致平行,可能是施工接缝处理不好,也可能是路基不均匀沉降拉裂了面层。它长而细,有时能贯穿整段路的长度,现场跑视觉检测时最容易因为断断续续被拆成好几段,误判成几段独立病害。

横向裂缝,走向与行车方向垂直,最常见的原因是温度收缩,属于典型的环境型病害。它的处理优先级通常低于龟裂,但如果不控制,雨水顺着裂缝下渗会加速基层损坏,慢慢地也会演化成龟裂。

三类裂缝在语义上就是三种不同的养护决策入口。检测系统如果只输出“有没有裂缝”,那跟没做差不多;只有把类别分开,才能直接对接后续的修补建议、成本估算、优先级排序。这也是这个项目坚持做 3 类而不是 1 类的根本原因。

1.2 为什么选 YOLOv8,而不是边缘检测或更老的框架

传统图像处理方案在裂缝检测上有两个绕不开的硬伤:一个是阴影、水渍、刹车印在灰度特征上和裂缝高度相似,阈值分割经常把它们一起检出来;另一个是裂缝的宽度变化很大,边缘检测对细小裂隙的响应极不稳定,换一个光照条件参数就得重新调。

YOLOv8 这一代相对 YOLOv5 有几个实打实的改进。骨干网络里的 C2f 模块通过更多分支的残差连接让梯度流更丰富,对小目标、细长目标的特征提取更稳;检测头换成了 anchor-free 的解耦结构,分类和回归分支分开处理,不再依赖预置 anchor,极端长宽比的目标也能直接预测;标签分配使用 TaskAlignedAssigner,按分类得分和框质量的综合对齐度来分正负样本,比旧版的静态匹配合理很多。

团队实际对比过同一份数据训 YOLOv5 和 YOLOv8,在不改任何 trick 的情况下,YOLOv8s 的 mAP50 大概能高出 2 到 3 个百分点,小裂缝的召回率提升更明显。再加上官方生态把训练、验证、导出封装成一条命令,复现成本很低。对于这种“要快速落地并且长期维护”的项目,选它是最稳妥的。

1.3 中英文双版不是包装,是协作刚需

我见过不少开源项目只有中文 README,代码注释也是中文,结果海外开发者想提 issue 都费劲,项目根本传播不开。反过来,纯英文版本对国内一线养护人员完全不友好,他们想看的是“这个类名对应哪种裂缝”“这个参数怎么调”,不是英文界面。

这个项目的中英文双版落在三个层面:一是项目文档,README、训练说明、部署指南各维护一份中英双语;二是代码内部实现一个翻译映射模块,推理时通过参数切换输出中文“龟裂”还是英文“Alligator Cracking”;三是数据标签本身,在数据集配置里把类别名和中文名做绑定,可视化时能直接画中文框。实践下来这个成本很低,但对协作效率和传播范围帮助非常大。

2. 数据集构建与标注实操

2.1 数据来源与筛选标准

数据是裂缝检测项目里最容易翻车的一环。公开的路面病害数据集可以拿到一批,但类别分布往往不均匀,横向裂缝样本多,龟裂样本少。我这次的做法是公开数据结合自己采集,采集时用手机和车载相机分别在晴天、阴天、逆光、树荫遮挡几种条件下拍,尽量覆盖不同路面材质,包括沥青路、水泥路、修补过的新旧接缝区域。

采集完不能直接标注,得先筛一遍。筛掉三类垃圾图:一是严重运动模糊的,裂缝边缘都糊成一团,标了也没意义;二是曝光过度的,裂缝被强光洗白,肉眼都看不清;三是完全无目标的纯路面图,这类图可以留少量当负样本,但不要太多,否则模型容易产生“什么都检不出来”的保守倾向。

数量上我的经验是每类至少要有 600 到 1000 个目标框,能到 1500 更好。不是说有 2000 张图就够了,而是目标框数量要够,因为龟裂是区域型的,一张图里可能只有一个大框;纵向裂缝是一段一框,一张图里可能有七八个框。按框数而不是按图数来评估覆盖度,更容易发现自己数据的短板。

2.2 三类裂缝的标注规范,每条都是踩坑换来的

标注环节决定了模型上限,这里值得多说几句。

龟裂的标注最容易犯的错误是只框局部裂纹网。龟裂往往是一整片区域,裂纹互相连通,正确做法是把整个网状区域用一个外接矩形框住,哪怕矩形里包含一些完好的路面也没关系,让模型学的是“这一片是龟裂”而不是“这一小块裂纹是龟裂”。如果一块大的龟裂区域被拆成好几个框,模型训练时正样本的重叠部分会反复出现,损失计算会偏向局部特征,反而学不到整体模式。

纵向裂缝和横向裂缝的问题是另一个方向,它们太细了。框一定要紧贴裂缝的走向,宽度可以稍微放宽一到两像素,但不能宽太多,否则框里绝大多数像素都是正常路面,正样本纯度太低;如果窄到只有一个像素宽,回归目标的长宽比极端到几十比一,YOLOv8 的回归头也很难收敛。实操里我按“框住裂缝主体走向,宽度比裂缝实际宽度宽 2 到 4 像素”这个标准来统一。

还有一个非常容易被忽略的点:标注的确定性。同一类裂缝在不同图里,有人习惯框长一点,有人习惯框短一点,这种风格差异会导致回归头无所适从。团队标注前应该定好“裂缝断裂超过 20 像素必须分开框还是合并框”“树荫下半截看不见的裂缝要不要框”这类规则,宁可规则笨一点,也要所有人执行一致的规则。

2.3 数据增强策略与划分方式

YOLOv8 内置的增强已经很强,主要调的是增强的组合方式。默认开启的 Mosaic 拼接对裂缝来说是好事,它能把不同路面的上下文拼在一起,强迫模型学裂缝本身的特征而不是过度依赖背景纹理。

有两个参数我单独调整过。一个是 flipud,垂直翻转我保持默认关闭,因为车载相机拍出来的道路图像,上方天空、下方路面这个空间分布是有稳定语义的,垂直翻转会让模型对上下文的分布产生不必要的混淆。另一个是 hsv 增强,饱和度抖动我调到了 0.5 而不是默认的 0.7,因为沥青路面偏灰,饱和度过高容易把灰度路面搞出奇怪的色调偏移。

数据集划分要特别注意一点:同一个路段连续拍的多帧图像,必须全部归到同一个集合里,不能这一帧在训练集、下一帧在验证集。否则验证集里会出现训练集的近亲样本,指标虚高,换到陌生路段立刻现原形。按拍摄时间戳或路段编号做分组划分,是避免数据泄漏最直接的办法。

3. 训练配置与核心参数详解

3.1 环境搭建与依赖版本

代码层面推荐直接基于官方 ultralytics 包,当前稳定版即可,太老的版本对 C2f 和各损失项的实现有差异,复现结果不稳定。PyTorch 用 2.x,Python 3.9 以上,GPU 显存建议至少 8G,因为裂缝图通常要用到 640 以上的输入尺寸,batch 太小会明显影响 BN 层的统计。

环境装完第一步先小规模冒烟测试,拿 20 张图训 5 个 epoch,确认 loss 在下降、能正常产出权重文件,再动全量数据。这一步看着多余,实际上能帮你过滤掉大部分环境问题,而不是把时间浪费在训了几百个 epoch 之后才发现版本不兼容。

3.2 训练参数怎么选,每个参数背后的逻辑

直接给一份我实测下来比较稳的配置参考。

参数推荐值说明
modelyolov8s.pt精度和训练的平衡点,龟裂小目标也能带得动
imgsz640默认值,细长裂缝可以试 1024,但显存和时间成本会涨
epochs300配合早停机制,数据量小的情况下 300 足够收敛
batch16显存允许就尽量大,BN 更稳定
optimizerAdamW对学习率不那么敏感,省调参
lr00.002AdamW 下常用的初始值,太高前期很容易震荡
lr_final0.0001收敛后期固定这个终值
fliplr0.5水平翻转,可用
flipud0.0保持默认关闭,理由上文提过

预训练权重选 yolov8s 而不是更小的 n,原因是 n 的骨干网络参数太少,对裂缝这种弱纹理目标的特征表达明显偏弱。如果部署设备性能紧张,可以先用 s 训练再蒸馏或量化到小模型,直接用小模型从头训效果通常不理想。

训练样本的 yaml 文件长这样:

path: C:/crack_dataset train: images/train val: images/val nc: 3 names: 0: alligator 1: longitudinal 2: transverse

启动训练直接用官方命令行:

yolo detect train data=crack.yaml model=yolov8s.pt epochs=300 imgsz=640 batch=16 lr0=0.002 optimizer=AdamW project=run_crack

3.3 训练过程中的监控与判断

训练跑起来之后不要干等,重点盯两组曲线。一组是 train 的 box_loss、cls_loss、dfl_loss,三个都应该稳定下降;另一组是验证集的 mAP50 和 mAP50-95,上升曲线会在某个 epoch 变平甚至掉头。

如果 train loss 还在降但 val mAP 连续二三十个 epoch 不涨,基本就是过拟合信号,这时候优先降低模型规模或者增强数据增强,而不是继续加 epoch。官方训练过程会自动保存 best.pt,判断标准是验证集 mAP50-95 最大,不是最后一个 epoch 的权重,所以最后选权重时直接拿自动保存的 best 就好,没必要自己重新挑。

训练时长参考:8G 显存、batch 16、640 分辨率下,几百张图的量级,s 模型大概几小时到十几小时能跑完,取决于 GPU 型号。别为了省时间把 imgsz 降到 320,裂缝的长宽比信息会在下采样时丢失一大截,召回率会非常难看。

4. 推理实现与效果呈现

4.1 评估指标:为什么这里 mAP50 比 mAP50-95 更适用

目标检测的标准评估里大家都会看 mAP50 和 mAP50-95,但裂缝场景有个特殊性:裂缝框的长宽比极端,尤其是纵向和横向裂缝,框哪怕上下偏移几个像素,IoU 就会掉到 0.7 以下。mAP50-95 是在 0.5 到 0.95 多个阈值下取平均,对这类极端框的回归精度要求极其苛刻,实际工程中几乎不可能达到高分,强行去追只会逼着你把框标得越来越宽,反而污染数据。

所以这个项目的核心评估指标我定成 mAP50,辅助看每个类别的 precision 和 recall,再配合一组 PR 曲线判断阈值取多少合适。不是说 mAP50-95 没用,而是对“现场能不能用”来说,mAP50 更能反映真实体验——框稍微偏差不超过 10 个像素,对养护决策没有任何影响。

分类别看指标是必须做的,只看整体均值会掩盖龟裂这个短板的真实水平。如果龟裂的 recall 明显低于另外两类,优先去补龟裂的样本和合理的标注框,而不是盲目调模型结构。

4.2 推理脚本与中英文标签切换实现

推理阶段直接用官方 Python API 会很干净,但中文标注这个细节需要自己处理。OpenCV 的 putText 画不了中文,所以我的做法是先用 PIL 画完中文标签再转回 numpy 数组。

完整的推理流程拆成三步:加载 best.pt,跑 predict,然后把检测结果送进绘制函数。中英文切换依靠一个翻译字典:

LANG_DICT = { "en": { "alligator": "Alligator Cracking", "longitudinal": "Longitudinal Crack", "transverse": "Transverse Crack" }, "zh": { "alligator": "龟裂", "longitudinal": "纵向裂缝", "transverse": "横向裂缝" } }

绘制函数用 PIL 处理中文:

from PIL import Image, ImageDraw, ImageFont import numpy as np def draw_boxes_with_lang(img, results, lang="zh"): pil_img = Image.fromarray(img) draw = ImageDraw.Draw(pil_img) font = ImageFont.truetype("simhei.ttf", 22) for box, cls_id, conf in zip(results.boxes.xyxy, results.boxes.cls, results.boxes.conf): x1, y1, x2, y2 = [int(v) for v in box] class_name = results.names[int(cls_id)] label = LANG_DICT[lang][class_name] draw.rectangle((x1, y1, x2, y2), outline=(0, 255, 0), width=2) draw.text((x1, y1 - 24), f"{label} {conf:.2f}", font=font, fill=(0, 255, 0)) return np.array(pil_img)

运行入口暴露一个 --lang 参数,命令行切语言,项目文档里再配一个说明,整个中英文双版的体验就闭环了。

4.3 导出 ONNX/TensorRT 与部署加速

模型训练完只是第一步,实际现场往往跑在边缘设备上,不能直接吃 PyTorch 权重。官方一行命令导出 ONNX:

yolo export model=best.pt format=onnx opset=12 dynamic=False

ONNX 导出后可以继续转 TensorRT 引擎,转完的 engine 在推理时能明显提升帧率:

yolo export model=best.pt format=engine device=0

部署时有几个取舍值得讲。输入尺寸 640 和 1280 相比,帧率可能差 3 到 5 倍,但对细小裂缝的召回影响并不算小。我个人的经验是先跑一段离线测试,确定最小可用的预测质量,再反过来约束部署尺寸,而不是一开始就固守 640。另外 batch 设为 1,对视频流逐帧推理足够,省显存,也避免多 batch 的额外处理延迟。

整体跑下来,n 模型转完 TensorRT 后在普通边缘设备上能到 30 帧上下,s 模型低一些,但已经基本满足巡检车低速行驶实时检测的需求。

5. 常见问题与排查技巧实录

5.1 裂缝检测的两大顽疾:误检阴影、漏检断裂

所有做裂缝检测的人都会碰到阴影误检。树荫在人行道和路面上形成的阴影边缘,灰度梯度和裂缝非常像,模型第一版很容易把这种边界当裂缝检出来。

我的排查思路分两步:第一步看误检样本集中在什么场景,绝大部分是树荫或桥底阴影,说明训练集里这类场景太少,需要专门补充带阴影的正样本和负样本;第二步是调 infer 阈值,把 conf 从 0.25 提到 0.35,误检会少一截但 recall 会有些损失,所以阈值不是越高越好,得结合 PR 曲线找拐点。

裂缝断裂漏检是另一个典型问题。一条纵向裂缝在图像里被树枝遮挡或光照不均分成两截,模型只检出一截,另一截漏了。我的临时方案是把 conf 降到 0.2 再配合低 NMS,然后对检出的框做后处理:同类别、且有重叠区域的两个框,如果中心距小于一定阈值,认为是同一条裂缝的两段,合并成一个框。治本的方案还是数据层面多拍遮挡场景,让模型见过“裂缝被遮挡后肉眼能看出趋势”的样本。

龟裂的小目标漏检,集中在图像远处。远处龟裂区域只有几十个像素,特征非常稀薄。一个有效的手段是训练和推理都用 1024 的输入尺寸,代价是速度和显存;另一个有效手段是把远处小目标作为困难样本单独复制增强,而不是简单合成新图。

5.2 训练不收敛与过拟合的排查记录

这次训练早期出现过一次 loss 曲线震荡不降,我排查下来是学习率问题。当时用 SGD 配合 lr0=0.02,头几个 epoch 直接爆量,mAP 起不来。换成 AdamW 初始学习率 0.002 后,曲线立刻就顺了。

过拟合的情况反而出现在数据量最大的那次实验里,原因是同一路段的多帧被分到了训练集和验证集,前面提过的数据泄漏问题。指标在验证集上很漂亮,但拿来跑一条陌生路段的视频,召回掉得一塌糊涂。最后按路段分组重切数据,指标掉了一点点,但实地表现反而好了,这件事给我印象很深。

5.3 部署现场的帧率与显存调优

部署环境如果不是自己搭的,经常会遇到显存不够。最直接的办法是推理阶段显存优化开关,把每次单帧推理改成明确释放中间张量的模式,实测能压掉不少峰值显存。如果还不行,就只能降输入尺寸,但降之前先确认自己的目标帧率底线,不要为了凑帧率把召回牺牲到不能看。

TensorRT 导出时遇到过精度下降的问题,有些框的置信度整体变低。这不是模型问题,而是动态输入尺寸和静态导出之间的差异造成的,统一到固定尺寸重新导出基本能解决。还有一次发现 ONNX 中间层输出和 PyTorch 不一致,是因为官方包版本更新后算子行为变化了,最终锁定版本重新导出就好了。

最后把整个训练链路自动化,从数据划分、训练到导出,写成一个总控脚本,换数据集跑的时候只改配置不动代码。这样后面做不同路面类型的版本,成本就低很多了。

我个人在这套项目里最大的体会是,模型结构反而是最省心的部分,真正决定效果上限的是数据规则是不是清晰、标注是不是一致、指标评估是不是贴合业务。把这个项目后面接到自动生成养护报告或者积累病害历史数据的系统里,价值还能再放大一圈。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进