ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

基于YOLOv10的工地运输车识别检测系统设计与实现

基于YOLOv10的工地运输车识别检测系统设计与实现

1. 项目背景与整体思路

1.1 这个项目到底解决什么问题

工地运输车识别检测系统,本质上是一个针对施工现场出入口、料场、主干道等固定点位部署的智能视觉监控方案。前几年我在做安防类项目时,接触过不少工地管理方的真实需求——他们最头疼的不是“有没有摄像头”,而是“摄像头拍到了,但靠人盯着看根本不现实”。一个工地几十路视频画面,保安不可能24小时轮轴转盯着每一路,更不要说还要准确记录每辆运输车的进出时间、车型、数量,这些数据最后都要汇总成台账,用于结算、调度和安全管理。

我基于YOLOv10做的这套系统,解决的就是“用算法代替人眼”这个核心问题。系统能做到:视频画面中一旦出现自卸车、搅拌车、渣土车这类目标,自动框出来并打上类别标签;通过简单的前后端逻辑,还能统计某个时段内各类车辆的进出数量;配合UI界面,管理人员打开电脑就能看到实时检测画面和统计数据,不用再盯着原始监控瞎猜。

这套系统最适合三类人来参考学习:一是做安防、智慧工地项目的工程师,可以直接改改用;二是计算机视觉方向的学生,作为毕业设计或课程项目非常合适,因为从数据集到模型训练再到界面部署是一条完整链路;三是工地的信息化管理人员,虽然不用自己写代码,但了解这套系统能做什么、怎么部署,对选型也有帮助。

1.2 为什么选YOLOv10而不是其他版本

YOLO系列发展到v10这一代,最大的变化是端到端这三个字。v8及之前的版本在推理时都要经过NMS(非极大值抑制)后处理,NMS的作用简单说就是把重叠的检测框合并成一个——一张图上同一个目标可能被预测出好几个框,NMS负责挑出最好的那个。但这步操作会拖慢推理速度,而且它的参数(比如IoU阈值)调起来很玄学,调不好就容易出现漏检或重复框。

YOLOv10在结构上用了无NMS的设计,通过一种叫“一致匹配度量”的机制,让模型在训练阶段就学会自己分辨哪些预测框是冗余的,推理时直接输出最终结果。实测下来,用相同的数据集训练,v10在保持mAP不降的前提下,推理速度比v8快了差不多一倍。对工地这种需要接多路视频流的场景来说,速度就是实打实的价值——单路视频能跑到50到60帧的话,一台普通的工控机就能同时处理四五路画面,不用堆硬件。

另外,v10的模型体积控制得也比较好。官方提供的n/s/m/l/x几个版本里,最小nano版权重文件只有不到6MB,部署到边缘设备上行得通;我用在工地项目里选的是s版,尺寸和精度平衡下来最合适。如果你打算在无GPU的老机器上跑CPU推理,nano版也是个不错的选择。

1.3 系统整体架构与关键技术选型

这个项目的完整技术栈说起来其实很朴素,每一样都是当下视觉项目里的常规选项,但搭在一起就是一个能落地的小系统:

  • 模型训练:YOLOv10s,输入分辨率固定为640×640,训练用自己整理的数据集
  • 开发语言:Python 3.10,深度学习框架用PyTorch 2.x,GPU训练
  • 界面框架:PySide6(Qt的Python绑定)。选它不选Tkinter或者Streamlit,是因为PySide6做桌面应用界面够漂亮、控件齐全,而且打包成exe之后在工地办公室的Windows电脑上双击就能跑,不用装Python环境
  • 部署方式:本地模型文件推理,视频源支持本地文件、RTSP流、USB摄像头三种模式

整体流程分三条线:数据线(采集图片 -> 标注 -> 清洗 -> 增强)、训练线(划分数据集 -> 配置参数 -> 训练 -> 评估指标)、部署线(加载模型 -> 接入视频源 -> 推理展示 -> 统计记录)。下面我把每条线拆开讲,重点是我实际操作中遇到的坑和总结的经验。

2. 数据集的构建与标注

2.1 工地运输车类别是怎么定的

做检测项目第一步永远不是选模型,而是定类别、整理数据。我一开始也想当然,以为工地运输车就分类成“卡车、罐车、挖掘机”就行,但落到实际业务里才发现这样太粗了——工地需要的车辆统计口径非常明确,管理方关心的是具体车型,因为不同的车对应不同的作业环节和计费方式。

我最终定下来的类别是:自卸车(渣土车)、混凝土搅拌车、挖掘机、装载机、普通运输货车这五类。前两类是“运输车”里的绝对主力,渣土外运和混凝土浇筑离不开它们;后面三类里挖掘机和装载机严格说属于工程机械,但它们在工地内部道路、出入口也会频繁出现,如果漏掉会让整车的统计不完整,所以也加进去了。交管和安监部门例行检查时关注的也是这些车辆的资质、冲洗情况、超载情况,识别出车型后才能继续做后续规则判断(比如渣土车没盖篷布这种细分场景,属于更高阶的功能,当前版本没做)。

2.2 图片数据的来源与采集

数据集总共用了4862张图片,我分几个渠道整理出来的:

第一,网上公开图片。从公开数据集网站和图片搜索引擎上,用每个类别的中英文关键词分别拉取,这一步大概占了1300多张。注意要多样化搜——白天、夜晚、雨天、雾天、不同省份的绿牌和黄牌车、不同品牌和涂装,这些细节都影响模型的泛化能力。光搜“自卸车”一种叫法是不够的,还要搜“渣土车”“翻斗车”“泥头车”,同一类车不同地区的叫法差异很大,反映到图片上就是外形差异。

第二,公开的车辆检测数据集。有很多开源的车辆检测数据集,里面包含卡车、公交车等大类,虽然没有直接用工地场景的数据,但可以用程序把卡车类的样本筛选出来,作为“普通运输货车”类别的基础数据,再补充一些工地场景的样本。这一步能快速扩充数量,但要注意风格差异带来的负面影响——公开数据集里很多是城市道路场景,背景和工地差别明显,所以这类数据我只用了约1000张,而且后期做了充分的场景混合。

第三,视频抽帧。从几个短视频平台搜索工地相关的视频素材,下载后用OpenCV按每3秒一帧抽成图片。视频抽帧有个好处是能拿到连续角度变化的同一辆车,对提升模型对姿态变化的鲁棒性很有帮助。但视频质量参差不齐,很多压缩痕迹明显、分辨率低的帧要直接扔掉。这部分补充了约1300张。

还有一小部分是用手机和运动相机在朋友的工地拍摄的,但这步要根据自己的实际条件来,不是必须的。自己拍的数据最大的价值是场景完全真实——工地的扬尘、车上的泥土、复杂光照,这些是网图里没有的。有条件的一定要拍,即使只有一两百张,对模型在真实场景下的表现都有明显帮助。

2.3 标注工具和标注实操

标注工具我用的是LabelImg,界面极简、上手快,画框、选类别、保存是全部操作。也有新的工具支持自动标注、团队协作这些高级功能,但自己做一个几千张的数据集,LabelImg完全够了,不用在这个环节过度投资时间。

标框的原则直接影响模型效果,我的经验有三条:

框要紧贴目标轮廓,但不需要精确到像素级。标注的时候沿着目标的最小外接矩形拉框就行。需要注意的关键点是,对自卸车和搅拌车这种有突出部件(后视镜、轮胎、搅拌筒)的车型,仍然把整个目标完整框进去,可以稍微留出1到2个像素的边距,但绝不是把周围的地面、围栏也框进来。

遮挡目标要慎重标注。如果一辆车开进画面时被电线杆挡住了一半,或者远处的车被前面的车挡住大半个车身,这类样本建议要么删掉、要么用一个很小的框只标可见的部分。绝对不能把被遮挡的部分也框进去——模型学到的是“一坨模糊的区域=车”,这会在推理时产生大量误检。我处理的原则是:遮挡面积超过50%就放弃标注,遮挡小于30%的完整框注,30%到50%之间的按可见部分拉小框。

对极度模糊的目标直接跳过。有些图片是网上下载的低分辨率压缩图,目标在图上只有十几个像素大小,人眼都很难判断具体车型。这种图标注进去只是给模型添乱,不如直接不用。

整个数据集的标注花了大约一周,每天标完一批就用脚本快速抽检,发现标注质量问题的马上纠正,避免错误习惯扩散到后面的数据。

2.4 数据清洗与增强

标完框之后还有一步很多人容易忽略——数据清洗。我用一个简单的Python脚本对标注文件做了一次系统性检查:

import os import xml.etree.ElementTree as ET # 检查标注文件中是否存在越界框或类别异常 def check_annotations(ann_dir): total_boxes_above_threshold = 0 width_sum = 0 for root, dirs, files in os.walk(ann_dir): for f in files: if not f.endswith('.xml'): continue tree = ET.parse(os.path.join(root, f)) img_width = int(tree.find('./size/width').text) img_height = int(tree.find('./size/height').text) for obj in tree.findall('object'): xmin = int(float(obj.find('bndbox/xmin').text)) ymin = int(float(obj.find('bndbox/ymin').text)) xmax = int(float(obj.find('bndbox/xmax').text)) ymax = int(float(obj.find('bndbox/ymax').text)) # 检查框是否越界 if xmin < 0 or ymin < 0 or xmax > img_width or ymax > img_height: print(f'[{f}] 越界框: ({xmin},{ymin},{xmax},{ymax})') print('检查完成') check_annotations('annotations')

越界框的原因通常是标注时窗口缩放后手一抖拉出去了,这种框会让训练过程出现严重的学习偏差,必须修正。另外我还统计了每个类别的宽高比分布——比如自卸车常见的长宽比在1.5到2.2之间,如果发现某张图的标注比是0.8,那基本上就是标错了位置或者框住了两个目标的一部分。

数据增强我用的策略是分两层的:

第一层是在线增强,也就是训练时实时做的随机变换。YOLOv10工程里的超参数文件里配置了:hsv_h=0.015(色调扰动)、hsv_s=0.7(饱和度扰动)、hsv_v=0.4(明度扰动)、translate=0.1(平移)、scale=0.5(缩放)、fliplr=0.5(水平翻转)。这些参数不是随手填的,工地场景里面光照变化大——晴天暴晒、阴天背光、灯光昏暗的夜间,这些扰动就是在模拟真实世界的各种光照条件。fliplr对车辆检测来说是安全的方向,不影响坐标语义(路标、文字才会因为翻转产生语义问题,车辆不会)。

第二层是离线增强,对数据量少的类别做额外补充。具体做法是:把对应的图片做随机旋转(5到15度小角度)、高斯模糊(模拟雨天和扬尘)、亮度调整(模拟黎明黄昏),生成了约800张增强图片,让各类别数量更均衡。一个值得注意的点是:旋转不能超过90度,否则自卸车、搅拌车倒立着的样本会严重误导模型学“朝向”的特征,反而有害无益。

最终数据集分布是:自卸车1650张、搅拌车1280张、挖掘机700张、装载机520张、普通货车1350张,总计约4862张。划分比例是训练集80%、验证集10%、测试集10%。

3. 环境搭建与模型训练

3.1 依赖环境配置

训练环境我用的是自己的工作站,配置是CPU为12核的AMD处理器、显卡为近几年的中高端N卡(显存12GB)、32GB内存。如果显存小于8GB,训练yolov10s的640分辨率也能跑,只是batch size要相应调小。

以下是与本次训练匹配的不带版本号或保守版本号的搭法,实际操作时可以核对版本兼容性:

Python环境用conda创建独立虚拟环境是首选,这一步无论如何都值得做——项目之间依赖版本冲突是Python开发里最常见的噩梦,隔离环境能从根上避免:

安装PyTorch时,到官网根据自己的显卡CUDA版本选择对应的安装命令,这步是最容易装错的。装完之后一定要在Python里执行下面这行做验证,看到显示GPU可用再继续:

import torch print(torch.cuda.is_available()) # 输出True才代表GPU可用

然后拉取YOLOv10的工程源码,创建项目根目录,目录结构核心是:

project_root/ ├── datasets/ │ └── construction_truck/ │ ├── images/ │ │ ├── train/ │ │ └── val/ │ └── labels/ │ ├── train/ │ └── val/ ├── runs/ │ └── detect/ ├── models/ ├── ui/ └── utils/

这里有个很关键的细节:YOLO系列训练时标签和图片必须严格分开存放,图片在images目录下,对应的txt标签在labels目录下,目录名必须是train和val这种约定的名字,否则训练脚本找不到数据会直接报错。

3.2 训练参数配置与单卡训练记录

YOLOv10的工程里,数据配置文件需要自己写一个data.yaml,内容如下:

path: datasets/construction_truck train: images/train val: images/val nc: 5 names: 0: dump_truck 1: concrete_mixer 2: excavator 3: loader 4: cargo_truck

注意path字段我用的是相对路径,训练时在项目根目录下执行命令即可。如果用绝对路径,以后项目拷贝到别的机器上就要改配置,很麻烦。

训练采用YOLOv10官方推荐的命令,核心参数设置为:

python train.py --model yolov10s.pt --data data.yaml --epochs 150 --batch-size 16 --imgsz 640 --device 0

这里每个参数都值得解释一下:

  • epochs设150:不是越大越好,这个数据规模下,训练到80轮左右loss就开始收敛放缓,150轮是为了让模型充分拟合且有充足余量观察过拟合拐点
  • batch-size设16:这个是12GB显存的容量边界,再大就要爆显存了。小batch训练会更稳定,但总迭代步数会增多,16是一个均衡值
  • imgsz设640:用默认分辨率。增大到1280会提升小目标检测精度,但训练和推理速度大幅下降;减小到320会把小目标的特征损失太多。640在两者之间最平衡
  • device设0:指定用第一张GPU。如果机器上有多张卡,可以设成0,1,2,3做多卡训练

训练期间可以打开tensorboard或者直接在终端看日志,重点观察三个曲线:box_loss(框回归损失)持续下降、cls_loss(分类损失)持续下降、val/box_loss(验证集损失)不能比训练损失高出太多。如果验证损失在训练后期掉头上升,就是过拟合信号,可以提前用early stopping机制停止。

我这轮训练从开始到结束大约耗时4小时左右,最终结果在验证集上的表现是:mAP50约0.94、mAP50-95约0.76。这个指标在5类目标中,挖掘机因为数据量相对少、外形变化大,单类mAP会偏低一点(约0.88);自卸车和搅拌车因为外形特征非常明显,单类mAP都超过了0.96。

3.3 测试集评估与模型选择

训练完成后,用测试集里的图片做一次正式评估,能更客观地衡量模型在“从来没见过的数据”上的表现。执行:

python predict.py --model best.pt --source datasets/construction_truck/test/ --save_txt --save_conf

测试阶段我特别关注的是两种错误:

一个是漏检。比如场景里一辆搅拌车被建筑物影子挡住了一半,如果模型漏掉了,把这段视频挑出来看看是不是图上目标太模糊、太小。漏检问题八成出在数据上——训练集里缺少类似场景的样本,解决办法不是调参数,而是补充数据。

另一个是误检。比如把挖掘机的铲斗单独识别成了装载机,或者把蓝色雨棚的铁柱子识别成搅拌车,这类问题常见于模型对纹理特征不够敏感,容易把高饱和度颜色区域误判为目标。处理方法除了补充负样本(没有目标的纯场景图片,标注为空),也可以检查是不是类别特征的区分度不够。

我对比了不同版本的模型文件,最后决定用s版本。n版本(nano)在测试集上mAP50掉了大概7个点,但推理速度快了40%;如果部署环境是Jetson这种嵌入式平台,n版本反而是更优解。台式机和工控机用户直接用s版本即可。

4. UI界面设计与系统集成

4.1 界面功能模块拆解

界面是整个项目的门面,也是让外行人觉得“这是个系统”而不是“几个脚本”的关键。我用PySide6做了桌面端界面,整体布局采用左中右三栏结构:

左侧是配置面板:视频源选择(支持本地视频、摄像头、RTSP流三种方式)、置信度阈值滑条(默认0.5,可运行中动态调整)、模型选择下拉框、一个“开始/停止检测”按钮。

中间是实时画面:显示原视频帧和标注框叠加后的结果。左上角是当前的FPS,右上角是当前画面中各类目标的实时计数。

右侧是统计面板:显示本次检测会话的累计统计表——每一类车辆的出现次数,按小时分桶的车辆进出趋势图(用matplotlib嵌入Qt的图形画布实现),以及导出识别结果到CSV的按钮。

这个界面设计有一个很直接的业务逻辑:工地管理人员不关心置信度得分和IoU这种技术概念,他们需要的是**“有多少辆车经过、什么时候经过、都是什么车”**。所以统计面板其实比实时画面更重要,做的时候我把统计部分放在了与画面等宽的位置,而不是缩在角落里。

4.2 实时检测线程的实现细节

PySide6最大的坑在于耗时的推理操作绝对不能放在UI主线程里——界面会直接卡死,表现为窗口无响应、鼠标转圈,严重的甚至会崩溃。正确做法是:用QThread开一个后台线程专门跑YOLO推理循环,推理完成后通过信号把结果传回主线程更新界面。

from PySide6.QtCore import QThread, Signal import cv2 from ultralytics import YOLO class DetectionThread(QThread): # 信号:帧图像、检测结果 frame_ready = Signal(object, list) error_occurred = Signal(str) def __init__(self, model_path, source): super().__init__() self.model = YOLO(model_path) # 加载模型 self.cap = cv2.VideoCapture(source) self.running = True self.conf_thres = 0.5 def run(self): while self.running and self.cap.isOpened(): ret, frame = self.cap.read() if not ret: break results = self.model(frame, conf=self.conf_thres, verbose=False)[0] # 把带框的帧和检测数据通过信号发出去 self.frame_ready.emit(frame, results.boxes) # 控制帧率,避免界面刷新过高 self.msleep(30) self.cap.release() def stop(self): self.running = False self.wait()

几个线程设计的细节补充一下:

第一,msleep(30)就是把推理线程的循环频率限制在30fps左右。RTSP流或普通视频文件本身就不到30fps,不需要无限快循环去浪费CPU。CPU紧张时笔记本风扇狂转、界面卡顿,大多就是少写了这行休眠。

第二,frame_ready信号传的是NumPy数组和检测框对象列表。在Qt的信号槽机制里,传递Python对象没问题,但传大尺寸图像时要留意queued connection模式会拷贝一份数据,如果频繁传递大图会有性能开销。优化做法是先缩小显示尺寸,再把小图发给界面线程——检测在原始分辨率下做,显示的图缩小到720p,画面清晰度几乎没有肉眼差异,但内存占用和处理开销能降一半。

第三,界面里的停止按钮要调用的不是terminate()而是stop()——terminate()会直接杀死线程,可能造成摄像头句柄没有释放、下次打开时摄像头被占用打不开的问题,这是个极易踩的坑。

4.3 识别结果的CSV导出与统计台账

很多项目做到识别出框就结束了,但真正做到业务闭环,导出功能是刚需。工地管理员每天要写进度的台账,有车辆出入记录可以省很多事。我的导出逻辑是这样设计的:

import csv from datetime import datetime class StatisticsRecorder: def __init__(self, save_dir): self.save_dir = save_dir self.events = [] def record_event(self, class_name, conf_score): self.events.append({ 'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'class': class_name, 'confidence': round(conf_score, 3) }) def export_csv(self, filename=None): if not filename: filename = f"truck_records_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" path = f"{self.save_dir}/{filename}" with open(path, 'w', newline='', encoding='utf-8-sig') as f: writer = csv.DictWriter(f, fieldnames=['time', 'class', 'confidence']) writer.writeheader() writer.writerows(self.events) return path

这里有一个从实际使用中总结的经验:CSV文件必须用utf-8-sig编码导出,不能用普通的utf-8。因为工地管理人员的电脑基本装的是Windows系统,默认用Excel打开CSV,普通utf-8文件里的中文类别名会显示成乱码,而utf-8-sig在文件开头加了BOM标记,Excel才能正确识别编码。这个细节虽然小,但如果你交付给客户之后客户反馈“字是乱码的”,那可真就是翻车现场了。

另一个经验是导出的时间戳要用本地时间而不用UTC时间。很多后台程序习惯用UTC方便跨时区处理,但工地台账是给本地的人看的,用UTC导出的时间会差8小时,结算对账的时候极其容易出纠纷。

4.4 界面打包发布

PySide6项目交付给没有Python环境的电脑时,需要打包成exe。我用的是PyInstaller,打包命令核心如下:

pyinstaller -F -w -n ConstructionTruckDetector main.py --add-data "models/best.pt;models" --hidden-import cv2 --hidden-import matplotlib

-F是打包成单文件,-w是去掉控制台黑窗口。打包过程中遇到的两个实际问题分享一下:

一是模型文件附加不进去。YOLO模型文件需要通过--add-data放进资源目录,但PyInstaller把代码和资源打包后,运行时当前路径变了,代码里要用相对路径找不到模型。解决方法是写一个通用的路径解析函数:

import sys import os def resource_path(relative_path): """PyInstaller打包后资源路径的兼容处理""" base_path = getattr(sys, '_MEIPASS', os.path.abspath('.')) return os.path.join(base_path, relative_path) # 加载模型时用 model_path = resource_path('models/best.pt')

二是打包体积偏大的问题。PySide6本身带了一大堆Qt库,PyTorch的CPU推理版本也在几百MB,打出来的exe轻松超过400MB。这可以理解,毕竟是深度学习应用的桌面端。如果客户接受,直接给exe即可;如果觉得太大,可以用UPX压缩壳减小一部分体积(但对PyTorch这种巨型二进制效果有限),更有效的办法是压缩模型精度——用int8量化把模型大小从25MB压到6MB。

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

5.1 推理速度太慢怎么办

我在实际部署中遇到过客户电脑配置很低——十年前的老办公机,CPU是i3双核,没有独立显卡。在这种机器上,YOLOv10s的Python推理速度只有不到5帧每秒,几乎没法看。记录几个有效的提速手段:

  • 换模型替代:优先换yolov10n版本,速度和精度此消彼长是客观规律,但n版本在这种场景下能跑到12帧左右
  • ONNX格式推理:把PyTorch模型导出成ONNX,再用onnxruntime做推理,不需要GPU也能提升1.5到2倍速度。导出命令很简单:
from ultralytics import YOLO model = YOLO('best.pt') model.export(format='onnx', imgsz=640, opset=12)
  • 限制输入分辨率:推理前把视频帧缩小到480P再喂给模型。检测小目标会变难,但对工地车辆这种大目标来说,损失不大
  • 跳帧检测:设定每3帧检测一次,中间2帧直接沿用上一帧的结果画框。做监控统计场景这个技巧非常好用,人眼根本看不出延迟,但计算量下降到三分之一

5.2 检测结果出现大量小碎框和重复框

这个问题多出现在现场光照突然变化(比如太阳从云层后面出来导致画面整体过曝)时。排查思路要分步来:

先看是不是置信度阈值太低。默认0.5阈值下理论不该有大量低频碎框,但如果之前在界面上手滑把滑条拖到了0.25以下,什么奇怪的框都会冒出来。先把阈值调回到0.4到0.5区间试试。

然后检查是不是画面缩放导致的压扁。有些摄像头画面是4:3的,被界面拉伸成16:9显示,视觉上目标拉宽了,模型对长宽比变化比较敏感,会产生置信度下降和误检。解决方式是把界面里的图像用setScaledContents(False)关闭拉伸,按原始比例显示,不足的地方用黑边填充。

最后查模型是不是训练时数据没混合好。如果训练集里网上找的图片占大头,现场真实图片占比少,模型在真实工地场景下特征分布偏移明显,碎框就会多。解决方式只能回炉补数据,在最终部署场地录一小段视频加入数据集重新训练,效果立竿见影。

5.3 同一辆车来回触发重复计数怎么去重

统计场景里最尴尬的问题就是:一辆车停在画面里不动,系统每隔几秒就记一条“经过”记录,统计台账就会被刷爆。我做过一个简单的逻辑来规避:

在检测线程里,对每个目标框,用IoU匹配方式与上一帧的活跃目标列表做关联。如果当前帧的某个目标框与上一帧某个框的IoU大于0.5,视为同一个目标,编号不变;只有新建的目标框(没有与任何已有目标匹配上)才触发计数,并且给它分配一个tracking_id。

def match_detections(self, detections, prev_detections, iou_threshold=0.5): matched_ids = [] for det in detections: best_iou = 0 best_prev = -1 for i, prev in enumerate(prev_detections): iou = self.calc_iou(det['box'], prev['box']) if iou > best_iou: best_iou = iou best_prev = i if best_prev >= 0 and best_iou > iou_threshold: det['tracking_id'] = self.active_tracks[best_prev] matched_ids.append(best_prev) else: det['tracking_id'] = self.next_track_id() self.counts[det['class']] += 1 # 只在这里计数 return matched_ids

用这个思路配合界面上清除统计按钮,5分钟的测试视频可以准确统计出车辆数量,多重框、重复计数的现象基本消除。如果现场要求更高的轨迹追踪和跨画面追踪,就需要引入ByteTrack或DeepSORT这类追踪算法了,但就普通进出统计场景,IoU匹配的轻量方案完全够用。

5.4 搬迁到新电脑后程序无法启动

打包好的exe在开发机上跑得好好的,拷到客户电脑上双击没反应,这个问题的头号原因是缺少Visual C++运行库,PyInstaller打包的程序和Python解释器都依赖系统底层的运行库,新装系统的电脑几乎都没有。

解决办法有两个:一是把运行库的安装包一起拷给客户,装好之后再运行程序;二是在打包时用--uac-admin选项申请管理员权限,同时代码里尽量不依赖需要管理员权限的路径(比如把记录文件写到用户文档目录而不是C盘根目录)。

另外还有一个容易发现不了的问题:exe所在路径含中文。PyInstaller打包的exe如果放在中文路径下,PyTorch的某些内部路径处理会乱掉,程序启动崩溃。迁移到新电脑时告知使用者:放在纯英文路径下的目录运行。

5.5 视频流无法打开或断流

工地现场经常用IP摄像头通过RTSP协议推流,但RTSP流非常容易断——摄像头掉线、网络波动、设备重启都会导致视频流断开。我在代码里写了自动重连机制:

def safe_capture_open(self, source, retry_seconds=5): cap = cv2.VideoCapture(source) if cap.isOpened(): return cap # 尝试重连5次,每次等待5秒 for attempt in range(5): self.log_message(f'视频流连接失败,{retry_seconds}秒后重试(第{attempt + 1}/5次)') self.sleep(retry_seconds) cap = cv2.VideoCapture(source) if cap.isOpened(): return cap raise RuntimeError(f'视频流连接失败,请检查网络和摄像头状态: {source}')

重连时还要注意不能把UI线程也一起卡住——重连逻辑也要放在后台线程里执行,界面上弹提示框就好,不能让用户感觉整个程序“死了”。

6. 从模型到实际交付的最后一步

最后把整个项目串起来说一下我的体会。做这个工地运输车检测系统,最容易出现的误区是把精力全压在模型精度上,反复调参只是为了mAP多涨零点几个百分点。但回头看,真正让一个系统在工地上被用起来的关键,反而是数据整理是否认真、界面交互是否顺手、统计结果是否可靠这些看起来土气但实际每天都在用的细节。我在对接演示的时候,对方最感兴趣的从来不是模型结构多先进,而是“这个记录能不能导成Excel报表”“断网的时候会不会丢数据”。

给你几个可操作的建议:如果你是个学生,拿到这个项目做毕业设计,在数据集构建和系统功能完整度上多花时间,答辩时老师问“你这个系统的创新点”,你可以实打实回答——构建了工地车辆专用数据集,设计了带接口的完整应用系统,而不只是跑通了一个预训练模型。如果你是在做工程项目,建议第一步先跑通最小闭环——用预训练的yolov8模型先试跑视频验证可行性,再投入时间整理自己的数据集,避免数据集做到一半才发现方向错了。

这轮项目做下来,我对深度学习应用落地最深的感触就是:算法本身的门槛在降低——开源模型越来越强、框架越来越简化,真正拉开差距的都是工程化的能力:数据是否保质保量、界面是否符合使用者习惯、有没有周到的异常处理机制。能把三者都做到位,一个看起来“技术含量一般”的项目,交付之后的实际价值也是实打实的。

RELATED READING

延伸阅读

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