传统道路巡检依赖人工,效率低、主观性强。本文记录一个基于深度学习的道路巡检系统完整实现:用 YOLOv5 训练道路病害(裂缝、坑槽、标线磨损等)检测模型,用 Flask 封装推理服务,用 PyQt 做桌面前端界面。从需求、设计、训练、后端、前端到联调,把完整流程和原理讲清楚。
一、项目背景与目标
1.1 业务背景
道路巡检的痛点:
- 人工巡检效率低:靠人眼观察,一天跑不了多少公里;
- 主观性强:不同巡检员对病害程度判断不一致;
- 数据难回溯:纸质记录难以统计和分析;
- 响应慢:发现病害到上报、派单周期长。
1.2 项目目标
做一个桌面端系统,实现:
- 图像/视频检测:上传道路图片或视频,自动识别病害类型和位置;
- 实时检测:接入摄像头或 RTSP 流,实时检测;
- 结果可视化:在图像上框出病害位置,标注类别和置信度;
- 统计分析:统计各类病害数量,导出报告;
- 轻量化部署:桌面应用,普通巡检员也能上手。
1.3 技术选型
| 模块 | 技术 | 选型理由 |
|---|---|---|
| 目标检测 | YOLOv5 (PyTorch) | 精度高、速度快、工程成熟 |
| 推理后端 | Flask | 轻量、易部署、RESTful 接口 |
| 前端界面 | PyQt5 | 原生桌面体验、跨平台、控件丰富 |
| 图像处理 | OpenCV | 读写视频流、画框 |
| 通信 | HTTP (requests) | 前后端解耦,接口清晰 |
为什么要分前后端:模型推理放后端,前端只负责交互和展示。这样模型更换(比如升级到 YOLOv8)只改后端,前端不用动;也方便后续把 Flask 部署到服务器,前端远程调用。
二、系统整体架构
┌───────────────────────────────────────────────┐
│ PyQt5 桌面前端 │
│ ┌─────────┐ ┌─────────┐ ┌───────────────┐ │
│ │图片/视频 │ │实时摄像头│ │检测结果展示 │ │
│ │上传模块 │ │输入模块 │ │(画框+统计) │ │
│ └────┬────┘ └────┬────┘ └───────┬───────┘ │
│ │ │ │ │
│ └────────────┴───────┬───────┘ │
│ │ requests(HTTP) │
└────────────────────────────┼─────────────────┘
▼
┌───────────────────────────────────────────────┐
│ Flask 推理后端 │
│ ┌─────────────────────────────────────────┐ │
│ │ 路由层:/detect_image /detect_video /...│ │
│ ├─────────────────────────────────────────┤ │
│ │ 模型层:YOLOv5 Detector(加载 pt 权重) │ │
│ ├─────────────────────────────────────────┤ │
│ │ 处理层:预处理→推理→NMS→画框→编码 │ │
│ └─────────────────────────────────────────┘ │
└───────────────────────────────────────────────┘
数据流:前端把图像编码为 base64(或直接 multipart 上传)→ Flask 接收 → YOLOv5 推理 → 返回带框的图像 base64 + 检测结果 JSON → 前端解码显示。
三、数据集准备与标注
3.1 道路病害类别定义
根据巡检需求,定义常见病害类别:
| 类别 ID | 名称 | 说明 |
|---|---|---|
| 0 | crack | 裂缝(纵向/横向/网状) |
| 1 | pothole | 坑槽 |
| 2 | marking_wear | 标线磨损 |
| 3 | rutting | 车辙 |
| 4 | patch | 修补痕迹 |
3.2 数据采集
- 来源:公开数据集(如 RDD2020、Crack500)+ 实地拍摄;
- 数量:每类至少 500 张,总数据集 3000+ 张;
- 多样性:不同光照、不同天气、不同路面材质。
3.3 数据标注
用 LabelImg 或 Labelme 标注,输出 YOLO 格式标签(归一化的 cx cy w h)。
标注规范:
- 框紧贴病害边缘,不要太大也不要太小;
- 被遮挡的病害按可见部分标注;
- 一个框只能属于一个类别,多病害分多个框。
3.4 数据集划分与目录
按 8:1:1 划分训练集、验证集、测试集:
road_defect/
├── images/
│ ├── train/ # 训练图片
│ ├── val/ # 验证图片
│ └── test/ # 测试图片
└── labels/
├── train/ # 每张图同名 .txt
├── val/
└── test/
3.5 数据集配置文件
yaml
# data/road_defect.yaml
path: /data/road_defect
train: images/train
val: images/val
test: images/test
nc: 5
names: ['crack', 'pothole', 'marking_wear', 'rutting', 'patch']
四、YOLOv5 模型训练
4.1 环境准备
bash
# 克隆 YOLOv5
git clone https://github.com/ultralytics/yolov5
cd yolov5
pip install -r requirements.txt
4.2 训练命令
用 yolov5s 预训练权重做迁移学习:
bash
python train.py \
--img 640 \
--batch 16 \
--epochs 200 \
--data data/road_defect.yaml \
--weights yolov5s.pt \
--hyp data/hyps/hyp.scratch.yaml \
--project runs/train \
--name road_defect
关键参数说明:
--img 640:输入分辨率 640×640,病害细节多时可升到 1280;--batch 16:显存够就往大设;--epochs 200:病害数据量不大,200 轮足够收敛;--weights yolov5s.pt:迁移学习,加速收敛、提升精度;--hyp:超参数,用默认 scratch 即可。
4.3 训练过程监控
- 关注
mAP@0.5和mAP@0.5:0.95曲线; - 训练曲线在
runs/train/road_defect/results.png; - 出现过拟合时(train loss 降、val loss 升),停止训练取最优权重。
4.4 模型评估与导出
bash
# 在测试集上评估
python val.py --data data/road_defect.yaml --weights runs/train/road_defect/weights/best.pt
# 导出为 ONNX,方便部署
python export.py --weights runs/train/road_defect/weights/best.pt --include onnx
训练完成后,best.pt 就是部署用的权重。
五、Flask 推理后端
5.1 项目结构
flask_backend/
├── app.py # Flask 应用入口,路由定义
├── detector.py # YOLOv5 检测器封装
├── requirements.txt # flask, torch, opencv-python, pillow
└── weights/
└── best.pt # 训练好的权重
5.2 检测器封装(detector.py)
把 YOLOv5 模型加载和推理封装成一个类,方便 Flask 调用:
python
import torch
import cv2
import numpy as np
class Detector:
def __init__(self, weights_path, device='cuda'):
# 加载模型,本地权重优先
self.model = torch.hub.load(
'ultralytics/yolov5', 'custom',
path=weights_path, device=device
)
self.model.conf = 0.25 # 置信度阈值
self.model.iou = 0.45 # NMS IoU 阈值
def detect(self, img_bgr):
"""输入 BGR 图像,返回带框图像和检测结果列表"""
# YOLOv5 需要 RGB
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
results = self.model(img_rgb)
# 渲染检测框到图像上(内部已实现)
rendered = results.render()[0] # RGB
rendered_bgr = cv2.cvtColor(rendered, cv2.COLOR_RGB2BGR)
# 解析检测结果
detections = []
for *xyxy, conf, cls in results.xyxy[0]:
detections.append({
'class': results.names[int(cls)],
'confidence': round(float(conf), 3),
'bbox': [int(x) for x in xyxy]
})
return rendered_bgr, detections
设计要点:
- 模型在 Flask 启动时加载一次,不要每次请求都加载(否则慢 100 倍);
detect方法接收 OpenCV 的 BGR 图像,返回画好框的 BGR 图像 + 结构化结果。
5.3 Flask 服务(app.py)
python
from flask import Flask, request, jsonify
import cv2
import numpy as np
import base64
from detector import Detector
app = Flask(__name__)
# 全局只加载一次模型
detector = Detector(weights_path='weights/best.pt', device='cuda')
def b64_to_img(b64_str):
"""base64 转 OpenCV 图像"""
img_data = base64.b64decode(b64_str)
nparr = np.frombuffer(img_data, np.uint8)
return cv2.imdecode(nparr, cv2.IMREAD_COLOR)
def img_to_b64(img_bgr):
"""OpenCV 图像转 base64"""
_, buffer = cv2.imencode('.jpg', img_bgr)
return base64.b64encode(buffer).decode()
@app.route('/detect_image', methods=['POST'])
def detect_image():
"""单张图片检测接口"""
data = request.get_json()
img_bgr = b64_to_img(data['image'])
# 推理
rendered, detections = detector.detect(img_bgr)
return jsonify({
'image': img_to_b64(rendered),
'detections': detections,
'count': len(detections)
})
@app.route('/health', methods=['GET'])
def health():
"""健康检查"""
return jsonify({'status': 'ok'})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)
5.4 启动后端
bash
pip install flask torch opencv-python pillow
python app.py
# 服务跑在 http://127.0.0.1:5000
5.5 接口设计说明
| 接口 | 方法 | 输入 | 输出 |
|---|---|---|---|
/health |
GET | 无 | {status: ok} |
/detect_image |
POST | {image: base64} |
{image: base64, detections: [...], count: n} |
用 base64 而不是文件上传的原因:视频流和实时帧检测时,数据在内存里,base64 传输更方便统一;文件上传还要处理临时文件清理。
六、PyQt 前端界面
6.1 前端功能模块
- 图像检测:选择图片,调用后端,显示结果;
- 视频检测:选择视频,逐帧检测,实时显示;
- 实时检测:摄像头/RTSP 流,持续检测;
- 结果统计:统计各类病害数量,表格展示;
- 保存功能:保存检测后的图片/视频。
6.2 界面布局
┌─────────────────────────────────────────────────┐
│ 菜单栏 / 工具栏 │
├────────────────┬────────────────────────────────┤
│ │ │
│ 检测结果 │ 图像显示区(QLabel) │
│ 统计表格 │ 显示带框的检测结果 │
│ │ │
├────────────────┴────────────────────────────────┤
│ 按钮栏:选择图片 / 选择视频 / 打开摄像头 / 保存 │
└─────────────────────────────────────────────────┘
6.3 核心代码(main_window.py)
python
import sys
import cv2
import base64
import requests
import numpy as np
from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel,
QPushButton, QVBoxLayout, QWidget,
QFileDialog, QTableWidget, QTableWidgetItem)
from PyQt5.QtGui import QPixmap, QImage
from PyQt5.QtCore import Qt, QTimer
API_URL = 'http://127.0.0.1:5000'
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle('道路巡检系统')
self.resize(1200, 800)
# 图像显示区
self.image_label = QLabel('请选择图片或视频')
self.image_label.setAlignment(Qt.AlignCenter)
# 结果表格
self.table = QTableWidget(0, 3)
self.table.setHorizontalHeaderLabels(['类别', '置信度', '位置'])
# 按钮
self.btn_image = QPushButton('选择图片')
self.btn_video = QPushButton('选择视频')
self.btn_camera = QPushButton('打开摄像头')
self.btn_save = QPushButton('保存结果')
# 布局
layout = QVBoxLayout()
layout.addWidget(self.image_label)
layout.addWidget(self.table)
layout.addWidget(self.btn_image)
layout.addWidget(self.btn_video)
layout.addWidget(self.btn_camera)
layout.addWidget(self.btn_save)
container = QWidget()
container.setLayout(layout)
self.setCentralWidget(container)
# 信号绑定
self.btn_image.clicked.connect(self.detect_image)
self.btn_video.clicked.connect(self.detect_video)
self.btn_camera.clicked.connect(self.detect_camera)
self.cap = None # 视频捕获对象
self.timer = QTimer() # 定时读取帧
# ---------- 图像处理工具 ----------
def ndarray_to_qpixmap(self, img_bgr):
"""OpenCV BGR 转 QPixmap 用于显示"""
rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
h, w, c = rgb.shape
qimg = QImage(rgb.data, w, h, c * w, QImage.Format_RGB888)
return QPixmap.fromImage(qimg)
def call_detect(self, img_bgr):
"""调用 Flask 后端推理,返回结果图像和检测列表"""
_, buffer = cv2.imencode('.jpg', img_bgr)
b64 = base64.b64encode(buffer).decode()
resp = requests.post(f'{API_URL}/detect_image', json={'image': b64})
data = resp.json()
result_img = cv2.imdecode(
np.frombuffer(base64.b64decode(data['image']), np.uint8),
cv2.IMREAD_COLOR
)
return result_img, data['detections']
# ---------- 图片检测 ----------
def detect_image(self):
path, _ = QFileDialog.getOpenFileName(self, '选择图片', '', 'Images (*.jpg *.png)')
if not path:
return
img = cv2.imread(path)
result, detections = self.call_detect(img)
self.image_label.setPixmap(self.ndarray_to_qpixmap(result))
self.update_table(detections)
# ---------- 视频检测 ----------
def detect_video(self):
path, _ = QFileDialog.getOpenFileName(self, '选择视频', '', 'Videos (*.mp4 *.avi)')
if not path:
return
self.cap = cv2.VideoCapture(path)
self.timer.timeout.connect(self.next_frame)
self.timer.start(30) # 约 33 FPS
def next_frame(self):
"""读取下一帧并检测"""
ret, frame = self.cap.read()
if not ret:
self.timer.stop()
self.cap.release()
return
result, detections = self.call_detect(frame)
self.image_label.setPixmap(self.ndarray_to_qpixmap(result))
self.update_table(detections)
# ---------- 实时摄像头检测 ----------
def detect_camera(self):
self.cap = cv2.VideoCapture(0) # 默认摄像头;RTSP 填流地址
self.timer.timeout.connect(self.next_frame)
self.timer.start(30)
# ---------- 结果表格 ----------
def update_table(self, detections):
self.table.setRowCount(len(detections))
for i, d in enumerate(detections):
self.table.setItem(i, 0, QTableWidgetItem(d['class']))
self.table.setItem(i, 1, QTableWidgetItem(str(d['confidence'])))
self.table.setItem(i, 2, QTableWidgetItem(str(d['bbox'])))
if __name__ == '__main__':
app = QApplication(sys.argv)
win = MainWindow()
win.show()
sys.exit(app.exec_())
6.4 设计要点
- QTimer 驱动视频流:用定时器每 30ms 取一帧,避免阻塞 UI 线程;
- 前后端解耦:前端只发 HTTP 请求,不直接加载模型,模型升级不影响前端;
- 统一的
call_detect:图片、视频、摄像头最终都调这个方法,逻辑复用; - 表格实时更新:每帧检测完刷新表格,展示当前帧的病害统计。
七、核心原理讲解
7.1 YOLOv5 检测链路
输入图像 640×640
│
▼
Backbone 提取特征 → Neck 融合多尺度 → Head 输出三尺度张量
│
▼
解码:每个锚框预测 (x, y, w, h, obj, classes)
│
▼
置信度过滤(obj > 0.25)
│
▼
NMS 去重(同类 IoU > 0.45 的只保留最高分)
│
▼
输出检测框列表 + 在原图上画框
7.2 Flask 服务原理
Flask 是一个轻量 WSGI 框架,@app.route 装饰器把 URL 映射到处理函数。本项目中:
- Flask 进程启动时加载 YOLOv5 模型到显存;
- 每次请求复用同一个模型,只做前向推理;
- 模型加载在全局作用域,所有请求共享,避免重复加载。
为什么用 base64 :HTTP 传输的是文本,图像是二进制,base64 是把二进制转文本的标准方式,前端用 cv2.imencode 编码,后端用 cv2.imdecode 解码。
7.3 PyQt 信号槽机制
PyQt 的核心是信号槽(Signal-Slot):
- 信号:事件发生时发出(如按钮点击
clicked、定时器timeout); - 槽:接收信号并执行的函数;
- 用
connect绑定:self.btn_image.clicked.connect(self.detect_image)。
视频播放就是用 QTimer.timeout 信号周期性触发 next_frame 槽,实现逐帧检测。
7.4 实时检测的性能瓶颈
逐帧 HTTP 请求会有网络开销,实测单帧推理 + 传输约 100-300ms(取决于显卡和图像尺寸)。优化方向:
- 后端批处理:把多帧打包一次推理(但增加延迟);
- 降低分辨率:视频帧缩到 640 再送后端;
- 本地推理:如果前端机器有显卡,直接在前端加载模型,省掉 HTTP;
- 跳帧检测:每 2-3 帧检测一次,中间帧复用上次结果。
八、完整运行步骤
8.1 启动后端
bash
cd flask_backend
python app.py
# 看到 Running on http://0.0.0.0:5000 表示启动成功
8.2 启动前端
bash
# 另开一个终端
pip install PyQt5 opencv-python requests
python main_window.py
8.3 功能测试流程
| 功能 | 操作 | 预期结果 |
|---|---|---|
| 图片检测 | 点"选择图片",选道路病害图 | 显示带框图像,表格列出病害 |
| 视频检测 | 点"选择视频",选巡检视频 | 逐帧播放带框画面,表格实时更新 |
| 实时检测 | 点"打开摄像头" | 摄像头画面实时带框检测 |
| 健康检查 | 浏览器访问 http://127.0.0.1:5000/health |
返回 {status: ok} |
8.4 打包部署(可选)
用 PyInstaller 打包前端成 exe:
bash
pyinstaller -F -w main_window.py
# 生成 dist/main_window.exe
后端可以打包成 Docker 镜像,部署到服务器。
九、典型坑点
坑点 1:模型加载慢,每次请求都重新加载
如果把 Detector() 写在路由函数里,每次请求都会重新加载模型,延迟几秒到几十秒。
对策:在 Flask 应用启动时(全局作用域)加载一次,所有请求共享。
坑点 2:图像颜色空间搞反
OpenCV 读的是 BGR,YOLOv5 内部用 RGB,Qt 显示用 RGB。三处转换容易出错,导致画面偏色。
对策 :统一约定:detector.detect 接收 BGR,内部转 RGB 推理,返回 BGR;前端显示时转 RGB。
坑点 3:视频检测时 UI 卡死
如果在主线程里循环读帧,UI 会卡死无法响应。
对策 :用 QTimer 定时取帧,或开 QThread 子线程处理视频帧,主线程只负责显示。
坑点 4:base64 传输大数据很慢
高分辨率图像转 base64 后体积膨胀约 33%,4K 图像一次传输要几百毫秒。
对策:前端先把图像缩放到 1280 或 640 再编码;或改用 multipart 文件上传避免 base64 膨胀。
坑点 5:Flask 调试模式导致模型加载两次
debug=True 时 Flask 会开 reloader,模型可能加载两次,占双倍显存。
对策 :生产环境 debug=False,或启动时加 use_reloader=False。
坑点 6:YOLOv5 权重路径找不到
torch.hub.load('ultralytics/yolov5', 'custom', path=...) 如果本地没有 yolov5 源码,会尝试联网下载。
对策 :提前 git clone yolov5 仓库,用 source 参数指定本地路径:
python
self.model = torch.hub.load('/path/to/yolov5', 'custom',
path='weights/best.pt',
source='local')
坑点 7:GPU 显存不足
如果同时跑后端推理 + 前端视频,显存可能不够。
对策 :用 --img 640 或更小、用 yolov5n/s、或后端改用 CPU 推理(device='cpu')。
十、项目总结与扩展方向
10.1 项目价值
- 把人工巡检变成 AI 辅助检测,效率提升数倍;
- 病害位置、类型、置信度结构化输出,便于统计和追溯;
- 前后端解耦,模型和界面独立迭代。
10.2 可扩展方向
- 病害严重程度分级:按框面积或长度分轻/中/重;
- GPS 定位:结合巡检车 GPS,在地图上标注病害位置;
- 历史数据统计:数据库存储每次检测结果,做趋势分析;
- 告警推送:发现严重病害自动通知运维人员;
- 模型迭代:用实际巡检数据持续微调模型,提升准确率。
10.3 知识体系串联
这个项目用到的核心技术:
- YOLOv5:目标检测模型,负责"看"出病害;
- Flask:Web 框架,把模型能力封装成 HTTP 接口;
- PyQt:桌面 UI,负责人机交互和结果展示;
- OpenCV:图像处理,负责视频流读写和画框;
- base64 / requests:前后端数据传输。
理解了这套架构,换成其他检测场景(安全帽检测、车牌识别、缺陷检测)只需换数据集和权重,框架不用改。