
路面坑洞检测这个题目我在两年前接手过一个市政养护单位的小项目当时他们的做法还是人工巡检车慢慢开、两个人盯着路面看一天下来也就巡三四十公里漏检率高得离谱。后来用YOLOv8 Python PyQt5搭了一套自动检测系统巡检车装个普通工业相机边跑边出结果效率直接翻了几倍。这套东西说穿了不复杂YOLOv8负责从图像里框出坑洞PyQt5负责把检测结果做成一个能用鼠标点的桌面软件Python 把两头串起来。目标是让刚学完深度学习入门课、懂点 Python 语法的人也能照着把整套东西跑起来——从数据集标注一直到打包成一个双击就能打开的 exe。今天我把整个过程中的设计取舍、参数账、踩过的坑全部摊开讲一遍尤其是那些教程里不会写、只有自己调过才知道的细节。1. 路面坑洞检测这件事先想清楚再动手1.1 需求拆解为什么不用传统图像处理刚接触这个方向的人第一反应往往是坑洞不就是路面上一块深色的区域吗用阈值分割、边缘检测不就完事了。我一开始也这么想过写了一段 OpenCV 代码灰度化、高斯模糊、Canny 边缘、形态学闭运算在几张晴天干燥的路面图上效果确实不错能画出个大概轮廓。但换到实际巡检视频里就崩了路面有阴影、有修补痕迹、有井盖、有积水反光、有车道线磨损阈值一调就全军覆没。传统方法的根本问题是规则是死的场景是活的你没法用几行 if-else 穷举所有光照和材质组合。目标检测的思路完全不同。它不关心坑洞长什么样这种人为总结的特征而是让模型自己从成千上万张标注图里学。我给它看两万张坑洞它自己会总结出边缘不规则、内部偏暗、有深度感、和周围沥青有明显纹理差异这些说不清但学得会的特征。这就是深度学习在这个任务上碾压传统方法的根本原因——特征工程从人转移到数据上。具体到这个系统需要满足的需求有这么几条输入可以是单张图片、一段录制视频、或者摄像头实时流输出是在原图上画框并标注置信度界面上要能调置信度阈值和 IoU 阈值还要能保存检测结果和统计数量。这四条看着简单但每一条都会牵扯到后面代码结构的设计。1.2 为什么选 YOLOv8 而不是两阶段检测器检测算法分两大流派。一类是两阶段代表是 Faster R-CNN 系列先出候选框再分类回归精度高但速度慢一类是单阶段代表就是 YOLO 系列一次前向就出结果速度快。路面巡检这个场景对速度是有硬要求的——巡检车按 40 公里时速跑相机 30 帧每秒你要是每帧处理要 200 毫秒视频就会严重丢帧很多坑洞根本没被看到。YOLOv8 是 Ultralytics 在 2023 年推出的版本相比 YOLOv5 有几个实打实的变化。第一是 Anchor-Free取消了预设锚框改成直接预测中心点到边界的距离。这一步的意义在于省掉了聚类锚框这个玄学环节你换数据集不用再跑 k-means 调 anchor对新手特别友好。第二是解耦头分类分支和回归分支分开成两套卷积两个任务的冲突变小了。第三是 TaskAlignedAssigner 正样本分配策略把分类得分和定位精度联合起来决定哪个预测框负责哪个真值框比纯 IoU 匹配合理。实际对比我也做过同一个坑洞数据集YOLOv5s 跑出来 mAP50 大概 0.86YOLOv8s 能到 0.89推理速度在 GTX 1660 Ti 上还快了几毫秒。如果你要往边缘设备上放yolov8n只有 320 万参数、8.7 GFLOPs权重文件 6MB 出头在 RK3588 这类的板子上也能跑到十几帧这对车载部署是刚需。1.3 整个系统分成四块先画清楚边界动手写代码之前我把系统切成了四块后面所有工作都往这四块里塞避免写着写着结构乱掉模块职责关键依赖数据模块图片采集、标注、格式转换、划分labelImg、OpenCV、shutil训练模块加载预训练权重、训练、验证、导出ultralytics、PyTorch推理模块加载权重、单帧/批量推理、后处理ultralytics、OpenCV、NumPy界面模块文件选择、参数调节、结果显示、导出PyQt5、QThread这样切的好处是每一块都可以单独测试。数据模块不依赖模型你可以先用几张图跑通标注格式推理模块不依赖界面可以先用命令行脚本验证模型能不能出框界面模块可以先用一张假图模拟推理结果来调试布局。哪一块出问题一眼就能定位到不用在几千行代码里大海捞针。注意很多人一开始就把训练和界面写在一个文件里结果模型一加载界面就卡死或者改个界面布局要重跑训练。职责分离这件事在原型阶段看着多余到后期改需求的时候能救命。2. 环境与数据90% 的失败都埋在这一步2.1 环境配置版本锁死比装最新版重要深度学习项目最容易翻车的地方不是模型是环境。我的建议就一条不要装最新版装互相兼容的版本。下面这套组合我在三台机器上都跑通过直接抄就行。先说显卡驱动和 CUDA。用nvidia-smi看右上角那个 CUDA Version它代表驱动最高能支持的 CUDA 版本不是你必须装的版本。比如显示 12.2那你装 CUDA 11.8 或 12.1 都没问题。PyTorch 官方给的对应关系一定要看torch 2.x对应cu118或cu121装错了就会出现torch.cuda.is_available()返回 False 的经典问题。# 建议用 conda 建独立环境Python 版本选 3.9 或 3.10 conda create -n pothole python3.9 -y conda activate pothole # 安装 PyTorchCUDA 11.8 版本注意 --index-url 别写错 pip install torch2.0.1 torchvision0.15.2 --index-url https://download.pytorch.org/whl/cu118 # 验证 python -c import torch; print(torch.__version__, torch.cuda.is_available())然后是 ultralytics 和 PyQt5pip install ultralytics8.0.196 opencv-python4.8.1.78 pip install PyQt55.15.9PyQt5 的安装时长一直是新手抱怨的点pip install PyQt5有时候要等好几分钟因为它在下载几十兆的 wheel 包。如果卡住不动换个国内镜像源就快了pip install PyQt55.15.9 -i https://pypi.tuna.tsinghua.edu.cn/simple还有一个坑opencv-python和opencv-contrib-python不能同时装后装的会把前者的部分文件覆盖掉导致cv2导入时报 DLL 加载失败。如果你不确定装过哪个先pip list | grep opencv看一眼有多个就全部卸掉重装一个。提示环境装完立刻跑一次yolo detect predict modelyolov8n.pt sourcebus.jpg能出图说明整条链路是通的。这一步花两分钟能省掉后面两小时的排查。2.2 数据集采集与标注标注质量决定上限模型的天花板是数据决定的这句话在坑洞检测上体现得特别明显。我一开始图省事从网上凑了几千张结果模型在阴天和湿滑路面上的表现惨不忍睹后来补采了雨天、黄昏、逆光三类场景各一千多张mAP 直接涨了 6 个点。采集渠道有几个装个手机支架在车上录视频再抽帧这是最省事的公开数据集可以用 RDD2022 这类道路病害数据集里面按国家分了子集病害分成纵向裂缝、横向裂缝、龟裂和坑洞几类把坑洞那部分捞出来能用实在不够就自己骑车绕城拍重点是多拍不同光照和不同路面材质。标注用 labelImg 就够了选 YOLO 格式输出的是每行类别id 中心x 中心y 宽 高的归一化文本。坑洞标注有几个经验点必须说第一边界模糊的坑洞要往外留 2 到 3 个像素。坑洞边缘是渐变的从完好沥青到破损有个过渡带你要是贴着最深处标模型的回归目标就会偏小检出的框会明显缩水。第二积水覆盖的坑洞照样标。积水是坑洞的伴随现象不标的话模型会学到有水的不是坑洞这个错误关联实际巡检里雨天漏检率会飙升。第三小于 10×10 像素的忽略掉。这种目标在 640 分辨率下连一个特征点都不到标了只会给模型添噪声而且实际养护也用不着修这么小的坑。第四子类别别乱加。有人想把坑洞分成轻微、中等、严重三类除非你有明确的判定标准和足够的样本量否则别做。类别一多每类的样本量就被稀释模型反而学不好。先从单一类别pothole跑通再说。标注完之后我习惯跑一个脚本做一遍体检统计每个类别的框数量、框的宽高分布、有没有宽高为 0 的脏数据。这一步能提前发现标注时手滑画出来的异常框。2.3 YAML 配置与目录结构格式错一个字母就白跑YOLOv8 的数据集目录结构是固定的别自己发挥datasets/pothole/ ├── images/ │ ├── train/ (约 70%) │ ├── val/ (约 20%) │ └── test/ (约 10%) ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── pothole.yamlimages和labels下的文件名必须一一对应只是后缀不同.jpg对.txt。新手最常见的错误是把 labels 放到了 images 里面或者文件夹名字写成image模型找不到标签就会把所有图当成背景训练出来的模型一个框都不出。pothole.yaml长这样path: /home/user/datasets/pothole train: images/train val: images/val test: images/test names: 0: pothole注意path建议写绝对路径相对路径在切换工作目录的时候会失效。names的键必须从 0 开始连续写成1: pothole会报索引错误。如果你从别的格式比如 COCO 的 json、labelme 的 xml转过来names的顺序一定要和标注文件里的类别 id 对上对不上就是全错模型会认认真真地把坑洞学成背景。2.4 数据增强与划分策略别让验证集骗了你YOLOv8 训练时默认开了 Mosaic、随机缩放、随机翻转、HSV 色彩抖动这一套增强。Mosaic 是把四张图拼成一张等于一个 batch 里塞进了四种场景对小数据集特别有效。但有两个开关需要你手动想清楚fliplr水平翻转默认 0.5对坑洞没问题路面左右翻转后语义不变。但flipud垂直翻转默认是 0千万别打开——颠倒了的路面在现实中不存在模型会学到错误的上下文。mosaic在最后 10 个 epoch 建议关掉用close_mosaic10。因为 Mosaic 拼出来的图边缘会有拼接痕迹而且目标的尺度分布和真实图片不一样训练末期还开着会让最终模型对正常图片的适应性变差。我自己试过同样的数据加不加close_mosaic10验证集 mAP 差 1.5 个点左右。数据划分上千万不要随机按 8:2 分完就完事。如果同一段路的连续抽帧被随机分到训练集和验证集那验证集里就会出现和训练集几乎一模一样的图指标虚高得厉害你自己骗自己。正确的做法是按拍摄路段或按时间段划分比如 A 路段的所有图进训练集B 路段的进验证集C 路段的进测试集。这样验证集才是真正的没见过的数据指标才有参考意义。我当时第一次跑就是随机分的mAP 0.94 高兴了半天换成按路段分之后掉到 0.87这才是真实水平。3. 训练参数背后的账要算清楚3.1 从预训练权重出发别从零开始权重选择上除非你的数据集超过十万张否则永远从预训练权重开始。COCO 上训出来的yolov8n.pt或yolov8s.pt已经学到了通用边缘、纹理、形状特征你只需在它的基础上微调。从零训和从预训练训的差距有多大我在同一个数据集上对比过从零训到 mAP50 0.85 需要大概 300 个 epoch从预训练开始 80 个 epoch 就到了时间差了将近四倍。模型尺寸怎么选n最快最省显存适合边缘部署s精度和速度平衡得最好我大部分项目都用它m及以上在单张图几十个坑洞这种场景里提升不明显但显存直接翻倍。GTX 1660 Ti 这种 6GB 显存的卡跑yolov8simgsz640batch8是稳的batch16会在训练中途 OOM。训练命令就一行yolo detect train \ modelyolov8s.pt \ datadatasets/pothole/pothole.yaml \ epochs100 \ imgsz640 \ batch8 \ device0 \ workers4 \ projectruns/pothole \ namev8s_640 \ close_mosaic10 \ patience303.2 关键超参数每个数字都有账学习率 lr0。默认 0.01这个值是给 SGD 优化器配的。如果你改用 AdamWoptimizerAdamW必须降到 0.001否则损失会剧烈震荡甚至发散。还有一个线性缩放规则值得记一下lr 0.01 × batch_size / 64。你 batch 用 8理论学习率就是 0.00125。当然这是经验公式微调时我通常取比它略大一点点的值比如 0.002收敛更快。batch size。不是越大越好。小 batch 的梯度噪声大反而有正则化效果小数据集上用 8 或 16 往往比 64 泛化更好。而且 batch 大了学习率也得跟着调否则等于变相减小了有效步长。显存不够就用batch-1让 ultralytics 自动按显存占用 60% 来推一个值这个功能挺好用。imgsz。默认 640输入会被缩放到 640×640 并且做 letterbox 填充。这个值直接影响小目标检出率。坑洞如果普遍偏小比如占原图不到 3%把 imgsz 提到 960 或 1280 效果会明显改善代价是显存和耗时按平方增长——640 到 1280计算量涨四倍。另一种省资源的做法是保持 640 训练推理时用imgsz1280通常也能捡回一部分小目标但要注意训练和推理尺度差太多会掉点最好在验证集上比一比。freeze。冻结主干网络的前 N 层通常在你数据集很小少于两千张的时候用比如freeze10冻结前 10 层。好处是防止小数据把预训练特征冲垮坏处是你的数据分布如果和 COCO 差很远冻结太多会导致学不动。我的经验分界线是五千张以上不冻结两千张以下冻结前 10 层中间自己试。warmup 和余弦退火。warmup_epochs3让学习率从很小的值慢慢升到设定值避免一开始就把预训练权重破坏掉。cos_lrTrue打开余弦退火学习率按余弦曲线衰减末期小学习率能让模型更稳地落进局部最优。这两个基本属于免费涨点没什么理由不开。3.3 损失曲线怎么读三种异常形态训练跑起来之后runs/pothole/v8s_640/下面会生成results.csv和一堆曲线图。判断训练健不健康看三条线就够了。box_loss 和 cls_loss 同步下降说明分类和定位都在学这是正常状态。如果 box_loss 一直在降而 cls_loss 平着不动多半是类别标注有问题比如 id 对不上或者全标成了同一个类。验证损失开始上升但训练损失继续下降这是过拟合的典型信号说明模型开始背训练集了。对策有几个加数据、开更强的增强比如把mixup从 0 调到 0.15、加weight_decay、或者干脆早点停。patience30就是干这个的验证指标 30 个 epoch 不涨就自动停。损失出现 NaN 或者突然炸到几百八成是学习率太大或者数据里有脏标签。先查标注文件有没有坐标超过 1.0 的值再降学习率。我在一次训练里遇到过坐标写成1.02的情况模型的两个 epoch 后 loss 直接变 NaN排查了半天才想起来查标注。想看曲线图的话ultralytics 自带的results.png里把三条 loss 和三个指标画在一起了也可以用 pandas 读results.csv自己画那样能放大看细节。我习惯在训练结束后单独画一张 mAP50 和 mAP50-95 的对比图能直观看出模型在宽松阈值和严格阈值下的差距有多大。3.4 指标解读与阈值调优mAP 高不等于好用训练完跑验证yolo detect val modelruns/pothole/v8s_640/weights/best.pt datadatasets/pothole/pothole.yaml输出里几个指标要分清。Precision是你报出来的框里有多少是真的Recall是真的框里你找出来多少mAP50是 IoU 阈值 0.5 时的平均精度mAP50-95是把 IoU 从 0.5 到 0.95 每隔 0.05 都算一遍再平均。工程上我更关心 Recall因为漏检一个坑洞的代价是路面继续恶化甚至爆胎而误检一个最多是养护工白跑一趟。验证时那个conf阈值非常关键。默认是 0.001为了画 PR 曲线用的但实际部署时你要选一个工作点。我一般会画一条 Precision-Recall 曲线找 F1 最大的那个点。实测下来坑洞检测在conf0.25、iou0.45附近比较均衡能拿到 0.88 左右的 F1。如果场景更看重不漏检把 conf 降到 0.15Recall 能到 0.95代价是 Precision 掉到 0.8 左右多出来的误检人工筛一下就行。还有个细节NMS 的 iou 阈值。相邻的两个坑洞如果挨得近iou 设太大会被 NMS 合并成一个设太小又会出现同一个坑洞被框两次。iou0.45是个比较稳的起点密集坑洞场景可以调到 0.5 到 0.6。4. PyQt5 界面把模型变成能交付的软件4.1 界面布局左侧参数、中间画布、底部状态界面设计上我坚持一个原则常用操作一屏内可触达不用翻菜单。最终布局是经典的左中右结构。左侧从上到下依次是文件选择区图片、视频、摄像头三个按钮、参数区置信度滑块、IoU 滑块、显示标签复选框、控制区开始、暂停、停止。中间是主显示区用QLabel承载视频帧。右侧可以放检测结果列表把每一帧检测到的坑洞数量和平均置信度打出来。底部是一条状态栏显示当前帧号、FPS 和处理耗时。参数区用QSlider加QDoubleSpinBox联动滑块拖动时数字跟着变数字改了滑块也跳这样既能快速拖又能精确定值。信号是valueChanged绑到一个更新内部变量的槽函数上别在推理循环里直接读控件值那样会反复触发 UI 事件。有一点值得说分辨率适配。现在很多笔记本是 2K、4K 屏默认 DPI 缩放下 Qt 控件会变得极小或者模糊。解决办法是在QApplication创建之前设置属性from PyQt5.QtCore import Qt QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps)另外如果你在不同分辨率的机器间分发布局尽量用QVBoxLayout、QHBoxLayout这类相对布局少用setGeometry写死坐标否则换个屏幕就错位。4.2 线程分离界面卡死是最大的体验杀手这是整个界面开发里最重要的一条推理必须放在子线程。如果你在主线程里写while True: ret, frame cap.read(); results model(frame)界面会直接无响应因为 Qt 的事件循环被你的死循环堵住了窗口连重绘都做不了标题栏会显示未响应。正确做法是继承QThread把推理循环放到run()里结果通过pyqtSignal发回主线程class DetectThread(QThread): frame_ready pyqtSignal(np.ndarray, list) stats_ready pyqtSignal(int, float) def __init__(self, model, source, conf, iou): super().__init__() self.model model self.source source self.conf conf self.iou iou self._running True def run(self): cap cv2.VideoCapture(self.source) while self._running and cap.isOpened(): ret, frame cap.read() if not ret: break t0 time.time() results self.model.predict(frame, confself.conf, iouself.iou, verboseFalse) boxes results[0].boxes annotated results[0].plot() self.frame_ready.emit(annotated, boxes.data.tolist()) self.stats_ready.emit(len(boxes), 1.0 / (time.time() - t0 1e-6)) cap.release() def stop(self): self._running False这里有个必须注意的点信号里传的 numpy 数组是引用主线程处理的时候如果子线程已经把这块内存改了或者释放了你会看到花屏或者程序崩溃。稳妥做法是在 emit 之前frame.copy()一下代价是一次内存拷贝几毫秒而已但能避免那种偶发的、极难复现的崩溃。还有一个容易忽略的地方参数更新。滑块改了 conf子线程怎么知道直接读控件会跨线程访问 UI 对象不安全。正确的做法是定义一个update_params方法在主线程调用它更新线程内的成员变量。因为 Python 的赋值是原子的这种简单变量的跨线程读写不会出问题。4.3 图像转换从 BGR 到 QImage 的那几行代码OpenCV 读进来的是 BGR 顺序的 numpy 数组Qt 要的是 RGB 的QImage中间转换写错是新手最常见的能跑但颜色发蓝的原因def cvimg_to_qpixmap(frame, target_w, target_h): 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) # 必须 copy()否则 rgb 被回收后 QImage 会指向野内存 pixmap QPixmap.fromImage(qimg.copy()) return pixmap.scaled(target_w, target_h, Qt.KeepAspectRatio, Qt.SmoothTransformation)QImage的构造函数不拷贝数据它只是引用你传进去的那块内存。函数返回后rgb被垃圾回收qimg就变成了野指针表现是画面时好时坏、偶尔花屏。加上.copy()就彻底解决了。缩放用KeepAspectRatio保持宽高比不要拉伸否则检测框会变形看着别扭。SmoothTransformation比FastTransformation慢一点但画质好很多视频流里如果帧率不够可以换成快速模式。还有一个界面类问题是QOpenGLWidget在某些机器上显示空白。这个不是代码 bug是 Qt 的 OpenGL 渲染后端和显卡驱动没配合好。临时验证的办法是在程序启动最前面加一句os.environ[QT_OPENGL] software强制走软件渲染画面就能出来了。长期方案是更新显卡驱动或者在main函数里用QSurfaceFormat显式设定版本和 profile。我遇到过两次都是这么解决的。4.4 打包成 exe让不懂 Python 的人也能用交付的时候对方不会装 Python 环境得打包。PyInstaller 是主流选择但 ultralytics 和 PyQt5 这两个库打包起来坑不少。pyinstaller -D -w main.py \ --name PotholeDetector \ --add-data runs/pothole/v8s_640/weights/best.pt;weights \ --hidden-import ultralytics \ --hidden-import ultralytics.nn.tasks \ --collect-all ultralytics \ --collect-all PyQt5 \ --exclude-module matplotlib \ --exclude-module torchvision几个关键点。第一模型权重文件要随包带上--add-data的原生写法在 Linux 和 Windows 上分隔符不一样Windows 用分号Linux 用冒号。第二ultralytics有动态导入的模块PyInstaller 静态分析扫不到必须用--hidden-import或者--collect-all手动带上。第三代码里读文件路径要区分打包前后import sys, os def resource_path(rel): base getattr(sys, _MEIPASS, os.path.abspath(.)) return os.path.join(base, rel)sys._MEIPASS是 PyInstaller 解压临时目录的路径打包后才有这个属性。用这个函数读权重文件开发时和打包后都能找到。打包出来的目录动辄一两 G主要是 PyTorch 贡献的。想瘦身可以用 CPU 版的 PyTorch少掉几百兆的 CUDA 库代价是推理慢十倍左右看你的部署场景选。5. 常见问题速查我踩过的坑5.1 环境类问题速查表现象大概率原因解决办法torch.cuda.is_available()返回 False装成 CPU 版 torch或 CUDA 版本不匹配用官方 index-url 重装对应 cu 版本ImportError: DLL load failedopencv 装重复了卸载所有 opencv 包只装一个训练报CUDA out of memorybatch 或 imgsz 太大降 batch或batch-1自动推No labels found目录结构不对或 yaml 路径错检查 images/labels 是否同级对应PyQt5 安装卡住不动源太慢换清华或阿里镜像源运行 yolo 命令提示找不到环境没激活或 script 目录不在 PATH用python -m ultralytics替代5.2 训练类问题几个反直觉的现象训练一开始 loss 就是 0。别高兴这通常是标注全丢了模型把所有东西都当背景损失自然是 0。检查 labels 目录是不是空的或者 txt 文件里的内容是不是被某种工具清掉了。mAP 一直卡在 0.5 上不去。先看验证集和训练集的分布是不是差太远比如训练全是晴天验证全是雨天再看 imgsz 是不是太小导致小坑洞检不出来最后看标注框是不是普遍偏大或偏小。测框大小有个简单办法算所有标注框的面积占整图面积的比例坑洞这个任务通常集中在 1% 到 8% 之间如果你算出来平均只有 0.3%那说明目标太小得提分辨率。训练速度忽快忽慢。多半是workers设太多CPU 数据加载和 GPU 抢资源。workers一般设成 CPU 核心数的 60% 到 80%8 核机器设 4 到 6 就行。还有可能是磁盘 IO 慢把数据集放到 SSD 上会明显改善。5.3 界面与显示类问题画面颜色发蓝发红一定是 BGR 和 RGB 搞反了看 4.3 节那段转换代码。点了开始按钮界面就假死推理没放子线程或者放了子线程但信号连接用了DirectConnection等于还是在主线程执行。视频播到一半卡住不动cap.read()返回了 False 但循环没退出。视频读到末尾会返回 False要在循环里判断并跳出或者做循环播放处理。检测框抖动厉害是逐帧独立检测导致的相邻帧结果没有关联。可以做一次简单的框平滑比如对连续 3 帧的框做加权平均视觉上会稳很多。这属于锦上添花不影响功能。5.4 检测效果不理想按这个顺序排查第一步永远是拿训练集里的图去推理。如果训练集上都不准那是训练本身有问题如果训练集准而新图上不准那是泛化问题得补数据。这个二分法能帮你省掉大量瞎调参的时间。第二步按场景分类统计漏检。把测试集按白天/夜间、干燥/积水、近距离/远距离分几组分别算召回率看短板在哪一组。我那次项目里夜间和远距离两组的召回率只有 0.6其他组都在 0.9 以上定位非常清晰之后就针对性地补了这两类数据。第三步看漏检框和真值框的 IoU 分布。如果漏检的框其实 IoU 在 0.3 到 0.5 之间说明模型其实找到了只是定位不够准可以放宽 NMS 或者调低 conf。如果 IoU 都在 0.1 以下那就是真没找到得从数据和分辨率上想办法。6. 实测经验与后续扩展6.1 几条个人心得数据比模型重要模型比调参重要。这句话我反复验证过。同样一套代码数据从 3000 张扩到 12000 张覆盖了更多场景mAP 能涨十几个点换个更大的模型涨两三个点调学习率和增强参数再榨出一两个点。时间该花在哪一目了然。先跑通再优化。我见过不少人一开始就纠结用不用注意力机制、要不要换更复杂的结构结果两周过去连 baseline 都没跑出来。先用yolov8n 默认参数跑一遍拿到一个能出框的模型之后再逐步替换升级每一步都有对比才知道改动是真有效还是自我感觉良好。测试集要留到最后用。调参的时候看验证集就够了测试集每动一次就污染一次看完指标你就不自觉地往那个方向调了。把测试集捂到交付前最后看一眼那个数字才是真实水平。推理速度的瓶颈往往不在模型。我做过一次耗时拆解yolov8s在 640 分辨率下单帧前向大概 12 毫秒但画框、转 QImage、缩放到界面尺寸、刷新控件加起来要 20 多毫秒。想要整体流畅这些周边代码也得优化比如复用 QPixmap 对象、减少不必要的.copy()、把缩放交给 Qt 的硬件加速来做。6.2 这套东西还能往哪扩展最直接的扩展是导出成 ONNX 或 TensorRT 加速。用yolo export modelbest.pt formatonnx opset12 simplifyTrue导出的 ONNX 模型在 ONNX Runtime 下推理比 PyTorch 原生快 1.5 到 2 倍再往上走 TensorRT在 NVIDIA 的板子上能快 3 倍以上。部署到嵌入式平台比如 RK3588 这类芯片上时官方一般会提供模型转换工具链流程是把 PyTorch 权重转 ONNX 再转成芯片专用格式中间要注意算子兼容性遇到不支持的算子要么换版本要么自己写插件。功能上可以加坑洞面积估算和分级。把检测框的像素尺寸结合相机标定参数换算成实际面积超过某个阈值就标红预警。这需要标定相机内参和安装高度公式是实际尺寸 像素尺寸 × 物距 / 焦距简单但好用。再往前走一步把多帧检测结果和 GPS 位置关联起来就能生成一张路面病害分布图交给养护部门排工期这个东西的实用价值比单帧检测高得多。再远一点的方向是时序跟踪。给每个坑洞分配一个 ID跟踪它在连续帧里的位置变化这样同一辆车跑一遍就能算出每个坑洞被拍到几次、平均置信度多少比单帧判断可靠得多。实现上可以用 ByteTrack 这类轻量跟踪器接在检测后面代码量不大效果提升明显。我现在还在用的一个组合是YOLOv8s 做检测 ByteTrack 做跟踪 PyQt5 做界面巡检车时跑 40 公里出头单帧端到端耗时 35 毫秒左右一天能覆盖一百多公里路面。这套东西没有什么高深的技术关键在于每个环节都老老实实地把数据、参数和边界情况处理干净了剩下的就是耐心。