1. 项目整体设计思路与方案选型
1.1 为什么是YOLOv8:从痛点反推技术路线
道路坑洼检测这个事,要是没接触过的人可能觉得不就“找个坑”嘛,但真做起来坑比想象中多——目标尺寸差异大、光照变化剧烈、路面纹理干扰强、还要考虑车端实时处理的延迟需求。我用过传统图像处理方案,比如灰度阈值分割加形态学滤波,晴天阴影下误检率高得没法看;也试过训练一个两阶段检测器,精度上去但帧率实在提不动。综合下来,基于YOLOv8做目标检测是最稳的路线,没有之一。
先说YOLO系列本身。从v5到v8这个生态,已经把“训练-验证-导出-部署”整条链路打磨得很顺。v8和之前版本最大的区别在于C2f模块的引入和Anchor-Free检测头的使用,前者增强了多尺度特征融合时的梯度流,后者省掉了锚框聚类那套预处理逻辑,训练时少一个需要调的超参数,对新手特别友好。另外官方仓库自带预训练权重,模型从零开始训和用预训练迁移的差距,在小数据集上非常明显,道路坑洼如果自己采集标注,数据量往往不大,这时候直接拿COCO上训好的权重作为起点就非常关键。
再结合坑洼检测这个场景本身的特点:单帧图像里的坑可能就几个像素,也可能占据画面四分之一;有的坑边缘清晰,有的坑被树叶半盖着。YOLOv8的多尺度特征金字塔在不同层级上分头检测大小目标,对这个“尺寸跨度大”的问题是天然适配的。实际测试下来,v8s模型在GTX 1660这档显卡上推理一张1080p图大概在12-15毫秒,完全能满足视频帧率的实时性要求。
1.2 系统架构与功能模块划分
这套系统的整体架构,我习惯画成四层:数据层、训练层、推理层、应用层。数据层解决的是“拿什么训”的问题,包括公开数据集筛选清洗、自采图像的标注、数据增强策略;训练层跑的是YOLOv8的模型训练和评估;推理层负责加载权重、处理输入输出;应用层就是用户实际看到的UI界面,提供图片检测、视频检测、实时摄像头预览和结果导出。
模块拆分上,我建议按这个思路组织代码目录:
text复制pothole_detection/
├── dataset/
│ ├── images/
│ │ ├── train/
│ │ └── val/
│ └── labels/
│ ├── train/
│ └── val/
├── pothole_data.yaml
├── train.py
├── detect.py
├── ui_main.py
├── models/
│ ├── best.pt
│ └── best.onnx
└── requirements.txt
train.py负责训练,detect.py是独立推理脚本,ui_main.py加载训练好的模型并把检测逻辑封装成后台线程,这样界面点击按钮后不会因为推理计算量大而卡死。模块之间通过模型文件和标准输入输出解耦,任何一个环节有改动都不影响其他部分。
这里要提一个很多人忽视的点:UI界面和推理逻辑一定要分开写。如果直接在界面回调函数里跑模型推理,视频流场景下主线程会被阻塞,窗口直接未响应。我一开始就是图省事把模型预测写进了按钮点击事件里,结果播放视频时候框卡成PPT,后来改成QThread后台处理才解决。这个细节后面展开说。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与依赖安装
2.1 硬件与软件版本要求
先说硬件底线。训练阶段,GPU显存至少要6G,8G以上比较舒适。8G显存跑YOLOv8s,batch size设到16,输入图640x640,刚刚好不爆显存。如果只有CPU,训练一个epoch都要等很久,体验很差,但也不是完全不能跑——把模型换成yolov8n,batch size降到8,我实测在较新的笔记本CPU上一个epoch也要好几分钟,整个训练周期会拖到一天以上,所以有显卡尽量用显卡。
没有NVIDIA显卡的话,还能选择用在线平台训练,现在几个主流平台对新用户都有免费额度,足够把训练跑完再本地做推理。这算是实在的备选方案。
软件版本上列一个我实测稳定的组合:
| 组件 | 推荐版本 | 备注 |
|---|---|---|
| Python | 3.10.x | 3.8-3.11均可,3.10最省心 |
| PyTorch | 2.x | 2.0以上稳定性最好 |
| CUDA | 11.8 / 12.1 | 与PyTorch对应 |
| ultralytics | 8.2.x | 安装最新稳定版即可 |
| OpenCV | 4.9.x | 需要同时装opencv-python-headless避免冲突 |
| PyQt5 | 5.15.x | UI界面开发 |
强调一点:尽量用虚拟环境,别直接装在系统Python里。深度学习项目依赖互相踩踏的情况太常见了,我之前在某台机器上因为conda环境和系统环境混用,结果torch版本升级后opencv调用崩溃,排查了半天。用虚拟环境,删了重建也就几分钟的事。
2.2 虚拟环境配置与ultralytics安装
终端操作其实就这几步:
bash复制conda create -n pothole python=3.10 -y
conda activate pothole
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118
pip install ultralytics opencv-python pyqt5
装完后验证一下:
bash复制python -c "from ultralytics import YOLO; print(YOLO.__version__)"
能打印出版本号就说明基础环境OK。如果打印不出来报DLL错误,八成是PyTorch的CUDA版本和显卡驱动不匹配。这种情况去显卡驱动管理页面更新驱动到最新版,再重装PyTorch就能解决。
这里还推荐装两个辅助工具:标注工具labelImg(负责画框标注)和numpy/pandas(做数据统计和分析)。有些同学会问坑洼检测能不能直接不标注用预训练模型,答案是不能用——COCO的80类跟坑洼完全不沾边,正规做法还是得自建数据集。
3. 数据集的构建与预处理
3.1 坑洼数据来源与采集策略
数据集是这个项目的核心资产,模型效果好不好,七成取决于数据而非网络结构。道路坑洼的数据来源主要有三条路:现有开源数据集、自己组织采集、两者混合。
开源数据集这块,网上能找到几个专门的路面缺陷集合,总体数量不算多,类别也比较杂,有的是坑洼(pothole),有的是裂缝(crack),有的是修补痕迹(patch)。用这些数据的时候要注意两点:一是看图像的拍摄角度,俯拍数据和车载视角数据在分布上有明显区别;二是看标注类别是否统一,有的数据集把坑洼和裂缝混在一起标注,直接影响训练效果。如果没有找到合适的数据集文件,可以自己用手机、行车记录仪、运动相机去拍,但拍摄要覆盖不同天气状况——晴天强光、阴天柔和光、雨后积水反光,这些都会显著改变坑洼的视觉表现。
我自己实际组织采集时,总结了一条经验:宁可数据量少一点,也要保证场景多样性。同一段路上同一个坑,换个角度拍就是两张分布差异很大的样本。标注的时候,不要只挑“一眼就看得出来是坑”的图,也要保留那些边缘模糊、被落叶遮挡一半的困难样本。我第一版数据集就是只选了清晰样本,结果训练出的模型在正常行驶视角下漏检率很高,后来加入困难样本之后mAP提升非常明显。
还有就是训练集和验证集的数据分布要一致。如果训练集全是晴天数据,验证集全是雨天数据,那评估指标一定难看。按场景比例划分数据集是最稳妥的,比如总计1000张图里晴天600张、阴天250张、雨天150张,训练集和验证集都按这个比例去分。
3.2 标注格式与目录结构
YOLO格式的标注文件是txt,每一行代表一个目标,格式为:
text复制class_id center_x center_y width height
其中坐标全部归一化到0到1之间。比如一张宽w高h的图上,某个坑的边界框左上角坐标是(x1, y1),右下角是(x2, y2),那么:
text复制center_x = ((x1 + x2) / 2) / w
center_y = ((y1 + y2) / 2) / h
width = (x2 - x1) / w
height = (y2 - y1) / h
这个转换刚开始容易算错,我的建议是直接用标注工具导出,别手写换算。labelImg本身就是这个格式,设置好类别名(比如只写一个pothole)后,画框保存自动生成txt,不会出错。
数据集目录结构按YOLO惯例来:
text复制dataset/
├── images/
│ ├── train/ # 训练集图片
│ └── val/ # 验证集图片
└── labels/
├── train/ # 训练集txt标注
└── val/ # 验证集txt标注
图片和标注文件必须同名,只是扩展名不同(jpg/png对应txt),否则训练时加载不到标签。这一步作为自查项写在前面,因为文件不匹配是新人最常踩的坑之一。
3.3 数据增强与data.yaml配置
数据增强在ultralytics框架中是自动做的,训练时默认开启左右翻转、马赛克增强,但有些增强对坑洼检测不一定有利。比如上下翻转——道路图像有明显的“上方是远处、下方是近处”的语义,上下翻转之后跟真实路况完全不符。好在YOLOv8默认没有开上下翻转,只有左右翻转,这个保留是合理的。马赛克增强倒是个双刃剑,小数据集下它变相扩充样本数量,非常管用,但引入的拼接边缘可能让模型学到假特征,所以如果你发现模型在验证集上指标正常但实际推理表现差,可以尝试把mosaic关闭再训一次对比。
训练传输文件pothole_data.yaml长这样:
yaml复制path: dataset
train: images/train
val: images/val
nc: 1
names: ['pothole']
如果数据集路径用的是绝对路径,path字段就写绝对路径的上一级;如果整个项目跟代码放一起,相对路径最方便。这里有一个容易翻车的地方:儒略历和Windows路径分隔符。data.yaml里统一用正斜杠/,别混用反斜杠,否则Windows下运行时炸路径错误。
4. 模型训练与调优
4.1 预训练权重与训练参数选择
拿到整理好的数据集,下一步就是训练。先选模型规格,YOLOv8提供了n/s/m/l/x五个版本,体积和精度依次递增:
| 模型 | 参数量 | 推理速度 | 适用场景 |
|---|---|---|---|
| yolov8n | 3.2M | 最快 | 嵌入式/边缘设备 |
| yolov8s | 11.2M | 快 | 实时推理(推荐) |
| yolov8m | 25.9M | 中等 | 精度优先 |
| yolov8l | 43.7M | 慢 | 服务器端高精度 |
| yolov8x | 68.2M | 最慢 | 竞赛/高指标场景 |
道路坑洼检测属于小目标相对集中的任务,但不需要极致精度到x级别。我建议直接用yolov8s.pt做预训练权重。如果数据量特别少(比如几百张),用yolov8n反而更不容易过拟合,因为参数量少,记忆能力弱一些,泛化能力相对好。
训练参数上,我给出一个实际跑通过的配置:
python复制from ultralytics import YOLO
model = YOLO("yolov8s.pt")
results = model.train(
data="pothole_data.yaml",
epochs=100,
imgsz=640,
batch=16,
lr0=0.01,
lrf=0.01,
momentum=0.937,
weight_decay=0.0005,
patience=20,
device=0,
)
epochs设为100不是必须的,我见过很多次训练到第40个epoch就已经收敛,后面全是振荡。patience参数的意思是连续多少个epoch验证集指标不提升就提前停止,设成20可以省不少时间。如果你赶时间,设epochs=50配合patience=15也够用。
批次大小的选择要参考显卡显存。8G显存跑batch=16没问题,如果爆显存先把batch降到8,或把imgsz降到480。YOLOv8训练时默认会自动做自适应缩放,所以imgsz不一定是最终推理尺寸,但会和最终效果强相关。
4.2 训练指标解读与过拟合控制
训练结束后会在runs/detect/train目录下生成一堆结果文件,重点看三个东西:F1曲线、PR曲线、混淆矩阵。PR曲线越靠近右上角越好,F1曲线峰值对应的置信度阈值就是后续推理时最合适的置信度设置值。很多人直接默认conf=0.25,其实最优阈值是要从训练结果里读出来的,这是不看训练报告的人容易漏掉的关键一步。
指标上,坑洼检测这个任务用mAP50衡量比较现实,一般单一类别目标检测,mAP50到0.8就算可用了。如果mAP50只有0.5以下,别急着调网络结构,先检查数据:标注框有没有漏标、边界框有没有偏大偏小、类别有没有混入背景。数据问题排在模型问题前面,这条规律十次有九次都成立。
过拟合的表现是训练集损失一直降,验证集损失先降后升。处理手段从强到弱依次是:增加数据(尤其困难样本)、调低epochs、加数据增强、换更小模型。增加数据永远是最优解,其他都是缓解手段。我试过最夸张的一次,数据量从800翻到1800,mAP直接涨了8个点。
4.3 模型导出与验证
训练完成后的best.pt就是最优权重。后续要接UI实时推理的话,建议导出ONNX格式,这样即使没装深度学习框架的机器也能用onnxruntime跑推理:
bash复制yolo export model=runs/detect/train/weights/best.pt format=onnx imgsz=640
导出前先快速验证一下模型效果:
bash复制yolo predict model=runs/detect/train/weights/best.pt source=test.jpg conf=0.25
生成的结果保存在runs/detect/predict里,仔细看有没有漏检、误检。推理阶段如果发现FP很多,就把conf调高一点;如果FN很多,就把conf调低一点。这个阈值是推理时候最值得调的一个旋钮。
5. UI界面设计与用户交互实现
5.1 UI功能需求与布局规划
这个项目的UI不用做得花哨,核心需求就四个:选图片检测、选视频检测、摄像头实时预览、结果保存。我用的PyQt5做界面,原因很简单:跟OpenCV的图像格式兼容性最好,信号槽机制适合多线程,界面开发速度快。
界面布局分三块区域:
- 左侧功能面板:按钮组(选择图片、选择视频、摄像头检测、保存结果)、参数设置区(置信度阈值滑条、IoU阈值滑条)
- 中间预览区:原图和检测结果图并列显示,方便对比
- 底部状态栏:显示检测数量、平均推理耗时、当前模型文件
这样布局的好处是用户操作路径很短:选文件、调阈值、看结果、导数据,几个动作全部在一屏内完成。参数滑条直接调conf和iou在做算法调试时特别实用,不用反复改代码重启。
5.2 核心逻辑与线程处理
这里写一下UI的核心代码思路,直接套用是可以的:
python复制import sys
import cv2
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QLabel, QFileDialog, QSlider, QVBoxLayout, QHBoxLayout, QWidget
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from ultralytics import YOLO
class DetectThread(QThread):
frame_ready = pyqtSignal(object)
def __init__(self):
super().__init__()
self.model = YOLO("models/best.pt")
self.conf = 0.25
self.iou = 0.45
self.cap = None
self.running = False
def run(self):
self.running = True
while self.running:
ret, frame = self.cap.read()
if not ret:
break
results = self.model.predict(frame, conf=self.conf, iou=self.iou, imgsz=640, verbose=False)
annotated = results[0].plot()
self.frame_ready.emit(annotated)
self.cap.release()
def stop(self):
self.running = False
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("道路坑洼检测系统")
self.thread = None
self._init_ui()
核心思想是:推理放到DetectThread里跑,通过pyqtSignal把处理完的帧传回主线程刷新QLabel。这样即使模型推理耗时较长,窗口也不会卡死。这个方案实测播放30fps的视频,界面帧率能到25fps左右,基本流畅。
至于静态图片检测,不必起线程,直接在主线程预测一下就返回了。视频检测和摄像头实时检测必须起线程,否则QT的事件循环被长时间阻塞会提示无响应。
5.3 检测结果可视化与数据导出
可视化的实现非常简单,YOLOv8的结果对象自带plot方法,直接返回画好框的numpy数组。再转成QPixmap显示:
python复制def show_frame(self, frame):
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
h, w, ch = rgb.shape
bytes_per_line = ch * w
qimg = QImage(rgb.data, w, h, bytes_per_line, QImage.Format_RGB888)
self.result_label.setPixmap(QPixmap.fromImage(qimg).scaled(
self.result_label.size(), Qt.KeepAspectRatio
))
数据导出这块,我建议导出CSV而不是图片。CSV里记录检测时间、图片文件名、坑洼数量、每个坑的置信度。后续如果要做数据分析或者生成巡检报告,CSV直接就能用。界面上也放一个“导出统计报告”按钮,点击后生成当次检测的汇总信息,这个功能在实际项目汇报的时候非常加分。
6. 常见问题排查与避坑实录
6.1 训练阶段的典型报错
我把实际运行中碰过的问题整理成一个速查表,按出现频率排:
| 报错信息 | 根本原因 | 解决方案 |
|---|---|---|
| CUDA out of memory | batch/imgsz过大,显存不足 | 调小batch,从16降到8,或imgsz从640降到480 |
| FileNotFoundError: xxx label | 图片和标注文件名称不一致 | 检查同名匹配,图片扩展名与txt一一对应 |
| Assertion error: no labels in dataset | 标注都变成空文件,或路径配置错误 | 用脚本检查每个txt是否有内容,确认data.yaml路径 |
| Dataloader worker (pid) crashed | Windows下num_workers默认数与内存冲突 | 训练时设workers=0,或减小workers值 |
| NaN loss | 学习率过高导致梯度爆炸 | 调低lr0,例如从0.01降到0.001 |
还有一个很隐蔽的坑:数据集图片格式混合,比如同一目录下既有jpg又有png,而标注文件的对应全是按jpg命名,导致png图片加载不到标签。建议统一图片扩展名,或者在data.yaml里用glob表达式限定。
6.2 推理与UI阶段的常见问题
推理阶段最典型的问题就是“模型在验证集上表现不错,但实际视频里到处乱框”。这种情况十有八九是置信度阈值设置得太低。解决办法很简单:把confidence slider往上拉,直到误检消除。值得强调的是,不同场景下的最优阈值差异很大,例如阴影下的坑洼区域边界不清晰,默认阈值0.25容易误检。所以UI里加滑条这个设计不是花架子,是真的有实际作用。
UI运行时的另一个常见问题是视频播放卡顿。排查思路要分清是推理慢还是界面刷新慢。推理慢就换小模型或用ONNX加速;界面刷新慢则往往是格式转换耗时太长,把分辨率缩放后再显示能明显改善。还有一个容易忽略的点:OpenCV的VideoCapture默认用CPU解码视频,如果视频分辨率是4K,解码本身就很占资源,可以先缩放再送进模型推理。
6.3 避坑经验
几个不试不知道的坑:
- 中文路径问题。训练数据、模型路径里有中文时,某环境下会读取失败,练出来的模型也可能无法正常保存。训练和推理的整个工作目录最好用纯英文路径,从头规避。
- 不要把训练集和验证集交叉混合。有人图方便把所有图片放一起按比例随机拆,但同一条路上的坑在不同帧里可能同时出现在训练集和验证集,导致验证指标虚高。正确做法是按视频或时间段划分,确保同一场景的数据不会被拆散。
- 导出ONNX之后一定要用onnxruntime单独验证一次,而不是直接沿用ultralytics的predict结果。两种推理方式的预处理和后处理细节有细微差别,输出概率分布一致但耗时和数值略有差异。我之前遇到过训练模型正常但ONNX推理结果为空的情况,排查发现是归一化顺序不同导致的。
这个项目整体跑通之后,做一个可用的道路坑洼检测系统就不难了。我实际做完这套系统的体会是:深度学习项目真正的重活不是训练模型本身,而是数据治理和工程封装。数据质量决定了模型天花板,而像界面、导出、部署这些工程细节决定了这个系统能不能真正被人用起来。后来我又在坑洼检测的基础上尝试把模型换成分割系列去识别坑洼轮廓,效果是另一番光景,但这套数据流程和工程框架完全复用,这是最初把结构和逻辑理清楚的价值。
