简介一套面向施工场景的安全隐患检测与跟踪解决方案基于YOLOv8与DeepSORT算法帮助工程管理人员自动识别工人是否佩戴安全帽、穿着反光背心等并实现持续跟踪预警。资源核心包含1206张已标注图像的目标检测数据集同时提供YOLO格式txt与VOC格式xml标签已划分train/val/test附有data.yaml可直接用于YOLOv5、v8、v9、v10、v11、v12等主流框架训练类别覆盖helmet、no-helmet、vest、no-vest、person等贴近施工现场安全巡检需求。集成DeepSORT跟踪算法配套README说明、PDF运行步骤文档及Python脚本覆盖环境配置、数据准备、模型训练与跟踪演示等关键环节便于快速跑通检测与跟踪流程。整个压缩包共2000个文件以998个xml和997个txt标注文件为主体另含md说明、py脚本和pdf教程大小356.67MB目录结构清晰便于按需检索。目前已有93人学习适合计算机视觉初学者及工地安全智能化项目开发者参考实践。1. 施工安全隐患检测与跟踪为什么说这份资源把两件事一次解决了接过安全帽检测项目的人大概都有同感YOLO 把人和头盔框出来只是第一步真正让现场管理者信服的是“这个人没戴安全帽并且已经在基坑边停留了 40 秒”这种跨帧结论。也就是说施工安全监测需要的是检测与跟踪的配合而不是单张图片上的孤立框。这份资源把 yolov8 检测、deepsort 跟踪算法、1206 张标注好的施工场景数据集和训练好的检测模型放在了一起还附带了 track.py、utils.py 和一份运行步骤 PDF。无论是做毕业设计还是准备把模型接到工地摄像头流上验证它都替你省掉了数据标注和预训练这两块最耗时的工作。适合已经跑通 YOLO 基础流程、现在想往工程应用方向走一步的人。2. 从数据到训练1206 张标注图的组织方式与 data.yaml 用法2.1 数据集的双标签形态与类别定义这份资源里最值钱的部分是标注数据集。它同时提供了 YOLO 格式的 txt 标签和 VOC 格式的 xml 标签共 1206 张图像并且已经按 train、val、test 三个目录划分完毕。双格式意味着两件事第一你可以直接用 data.yaml 喂给 yolov5、v8、v9、v10 等任意支持 YOLO 格式的框架不需要做格式转换第二如果你想换用 Detection Transformer 或者自己写数据加载器VOC 的 xml 结构更容易二次解析不至于被锁死在一条技术链路上。类别定义是典型的施工安全四件套加背景helmet安全帽、no-helmet无安全帽、no-vest无安全背心、person人员、vest安全背心。这五个类别覆盖了工地入口和作业面最常见的检查诉求。需要注意 no-vest 和 person 之间天然存在语义重叠——所有 no-vest 的实例同时也具有 person 属性训练时模型靠边界框位置和上下文来区分两者。实际部署时我见过很多团队把 no-vest 的阈值单独调低 0.1因为漏报一个无背心工人的代价比误报高得多。2.2 data.yaml 的目录对齐与修改拿到资源后第一步不是急着跑训练而是检查 data.yaml 里的路径是否对得上你本机的目录结构。常见做法是建一个独立的数据目录把 images 和 labels 按 train、val、test 排列形如dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yaml对应的 data.yaml 配置如下path: /绝对路径/dataset train: images/train val: images/val test: images/test nc: 5 names: 0: helmet 1: no-helmet 2: no-vest 3: person 4: vest这里最关键的是第一行 path。YOLO 系框架对 path 的解析规则在不同版本里有微妙的差别Ultralytics 的版本会先把 path 和 train 拼接成完整路径如果你的 path 末尾多了一个斜杠或者写成相对路径轻则训练集加载为零重则报 FileNotFoundError。我一般直接把 path 写成绝对路径省得在不同目录下执行命令时行为不一致。names 的索引顺序必须和标注文件里的类别编号严格对应不能只改名称不改编号。2.3 训练前先做标签体检坐标越界、空标注与数据均衡在开始训练之前我强烈建议先写一段脚本扫一遍标签文件避免训练到一半发现损失异常。这个步骤很多人会跳过但它恰恰是“yolov8 训练自己的数据集”时最常见的隐性事故源。检查脚本并不复杂核心点是坐标是否在 [0,1] 区间内、是否有空文件、类别编号是否越界。import os from pathlib import Path def check_yolo_labels(label_dir): issues [] total 0 empty 0 for label_path in Path(label_dir).rglob(*.txt): total 1 with open(label_path, r) as f: lines [line.strip() for line in f if line.strip()] if not lines: empty 1 issues.append((empty, str(label_path))) continue for line in lines: parts line.split() if len(parts) ! 5: issues.append((format, str(label_path))) continue cls int(parts[0]) if cls 0 or cls 4: issues.append((class_out_of_range, str(label_path))) try: x, y, w, h map(float, parts[1:]) except ValueError: issues.append((not_numeric, str(label_path))) continue if x 0 or y 0 or w 0 or h 0 or x w / 2 1 or y h / 2 1: issues.append((bbox_out_of_range, str(label_path))) print(f总标签文件数: {total}, 空文件数: {empty}) for issue, path in issues[:20]: print(f[{issue}] {path}) return len(issues) 0 if __name__ __main__: # 替换成你的实际标签目录 check_yolo_labels(dataset/labels/train)这段脚本遍历 labels 目录下的所有 txt 文件逐行解析类别编号和归一化坐标。x、y 是中心点坐标w、h 是宽高所以判断越界的条件是中心点坐标加上宽高的一半仍然落在 [0,1] 区间内。空文件在 YOLO 训练中会直接被忽略但如果数量超过总量的 5%意味着很多图像没有标注目标会导致模型倾向输出低置信度背景训练时表现就是损失曲线很平。另外提一句数据的类别均衡问题。关于五个类别的分布tags 文件里没有直接给出数量但工地场景下 person 和 helmet 的数量通常远超 no-vest。训练时如果发现小类别学不动常见做法是给 no-vest 和 no-helmet 设置更高的 cls 损失权重或者在 dataset.yaml 里用采样器做类别重加权。不要为了追求整体精度而忽略小类别施工安全场景里小类别恰恰是核心业务。3. 接入 deepsort 跟踪从单帧检测到跨帧 ID 稳定3.1 为什么检测之外还需要跟踪单帧检测只能回答“这一帧里哪里有违规”回答不了“这个违规行为持续了多久、是同一人吗”。deepsort 的价值在于把不同帧里的检测框关联到同一个行人 ID 上它的核心是状态预测加外观匹配。目标检测得到每一帧的 bboxdeepsort 用卡尔曼滤波预测每个 ID 在下一帧的位置和速度然后用级联匹配把预测框和真实检测框配对。匹配度由两部分构成运动上的马氏距离和外观上的余弦距离。马氏距离衡量“预测位置和检测位置的统计接近程度”余弦距离则比较目标的外观特征向量。当目标短暂遮挡或快速移动时外观特征起作用当外观相似度高、不同人容易混淆时运动模型起作用。这个资源包里集成了 deepsort 跟踪算法这意味着你不用自己搭 tracker 框架直接在 YOLO 检测的输出上接一层即可。需要注意 deepsort 的权重文件和 YOLO 的权重文件是两套东西YOLO 负责目标检测deepsort 的权重是行人重识别模型提供外观特征。很多人以为资源包里“训练好的模型”同时包含两者实际上 deepsort 的权重需要单独加载视频里跟踪行人用的就是它。3.2 track.py 的完整管线从 detection 到 STrack资源包里的 track.py 是核心代码它把 YOLO 输出转换为 deepsort 的输入再交给 STrack 更新状态。整体管线是读取视频帧 → YOLO 推理得到 bbox 和置信度 → 按置信度阈值过滤 → 将 bbox 坐标转换为 deepsort 要求的 [x, y, w, h] 格式 → 提取检测框对应的外观特征 → 调用 tracker.update() 得到当前帧所有 ID。核心逻辑的常见实现如下import cv2 from ultralytics import YOLO from deep_sort_realtime.deepsort_tracker import DeepSort # 加载模型 det_model YOLO(best.pt) # 检测模型 tracker DeepSort( max_iou_distance0.7, max_age30, n_init3, max_cosine_distance0.3, nn_budget100 ) cap cv2.VideoCapture(test_video.mp4) while cap.isOpened(): ret, frame cap.read() if not ret: break # YOLO 推理conf 阈值设为 0.4 results det_model(frame, conf0.4, verboseFalse) detections [] for result in results: for box in result.boxes: x1, y1, x2, y2 box.xyxy[0].tolist() conf float(box.conf[0]) cls int(box.cls[0]) # deepsort 只关心 bbox 和置信度不需要类别 detections.append(([x1, y1, x2, y2], conf, cls)) # 更新跟踪器 tracks tracker.update_tracks(detections, frameframe) for track in tracks: if not track.is_confirmed(): continue track_id track.track_id x1, y1, x2, y2 track.to_ltrb() # 在这里叠加业务判断如果该 ID 一直没头盔计入告警 cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(frame, fID: {track_id}, (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(frame, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码里有几个值得注意的参数。max_age30 表示一个目标在消失后最多保留 30 帧超过后 ID 被删除。n_init3 表示检测框必须连续出现 3 帧才确认新 ID这能过滤掉误检产生的虚假轨迹。max_cosine_distance0.3 是外观匹配的阈值数值越小要求外观越相似ID 越稳定但值太小会导致同一人穿不同衣服时断轨迹。nn_budget100 表示每个 ID 最多保留 100 个历史外观特征超出后淘汰最旧的避免特征库膨胀拖慢匹配速度。3.3 跟踪器的参数选型与常见误用deepsort 参数不是随便调的每一个都和场景强相关。比如施工工地摄像头通常固定在高处角度俯视行人移动速度慢遮挡多来自脚手架和堆放物。这时候 max_age 可以适当调大因为遮挡恢复后还要认出同一个人而如果是大门口的快照场景人的路径流畅max_age 反而可以小一点防止 ID 残留时间过长导致误报“有人停留”。另外一个常见误用是直接给 tracker 输入 YOLO 的 xyxy 格式而没有统一图像尺寸。deepsort 在原论文里用的是像素坐标但很多复现版本默认输入是 xywh 并且做了归一化处理。使用前花两分钟读一下 track.py 里对输入坐标的预处理函数能省掉后面排查坐标错位的半天时间。资源包里的 utils.py 承担了坐标转换、画框和格式对齐的活建议先跑通 utils 里的接口再改自己的逻辑。4. 复现与部署避坑五个高频翻车点与排查记录4.1 损失函数曲线不下降mAP 却正常现象训练过程中 loss 曲线几乎平着走但验证集 mAP 在稳步上升。很多人慌了以为模型没学进去。原因YOLOv8 默认输出的 loss 包含 box、cls、dfl 三个分量的加权和而数据集里背景占比高正样本 anchor 稀疏时早期 loss 以分割损失为主。loss 不降并不直接等价于没收敛mAP 才是业务指标。解决盯 mAP 而不是盯 loss。用资源包附带的验证集每 10 个 epoch 跑一次验证看各类别的 precision 和 recall。如果 recall 极低但 precision 高降低置信度阈值再看如果 person 类 mAP 很高、no-vest 类几乎为零回到类别均衡问题。4.2 画出来的边框全部挤在图像左上角现象把模型推理结果画在图上时所有框都堆在左上角而且尺寸异常小。训练时 loss 也能降下来看起来像模像样。原因训练和推理的图像尺寸不一致导致的归一化错位。数据标注时如果图像从 VOC 格式转换而来YOLO 的归一化是“中心点 x / 图像宽度”VOC 则是绝对像素坐标。很多人把 VOC 的 xmin 除以宽度后当作中心点两组坐标直接错位。解决检查标签的 x,y 到底是中心点还是左上角。YOLO 格式是 cx, cy, w, hVOC 转 YOLO 需要先算中心点# VOC 中的 bndbox 是 (xmin, ymin, xmax, ymax) # 转换时必须要算中心点而不是直接把 xmin 作为 x cx (xmin xmax) / (2 * image_width) cy (ymin ymax) / (2 * image_height) w (xmax - xmin) / image_width h (ymax - ymin) / image_height验证方式是任取一张训练图把标注框画在原图上叠加显示。如果发现框的位置偏了半截立刻停下检查转换逻辑而不是硬着头皮继续训练。4.3 deepsort 的 ID 频繁跳变一人分饰多个 ID现象同一个工人走过摄像头视野ID 号码跳了五六次。业务上表现为“某人没有安全帽”的告警被重复上报检测端对但跟踪端帮了倒忙。原因三个因素叠加。一是检测框不稳定YOLO 对某个姿态漏检了两帧二是 max_cosine_distance 设得太大比如 0.7导致不同人的外观特征被错误关联三是 iou 阈值过严相邻帧目标轻微移动就判定为不同目标。解决先从检测端排查把置信度阈值降到 0.25减少漏检造成的轨迹中断再把 max_cosine_distance 降到 0.3 左右让外观特征匹配更严格最后适当调大 max_iou_distance 到 0.7。如果仍然跳优先怀疑检测框的宽高比抖动考虑在 tracker 前加一个框平滑而不是继续调 tracker 参数。4.4 推理帧率只有个位数离实时差的远现象模型跑起来 CPU 占用满帧率 5~6完全没法实时推流。常见误区是一股脑地怀疑 YOLO实际上 deepsort 的 ReID 网络也是性能杀手。原因deepsort 的外观特征提取网络对每一帧的每个检测框都要过一次 CNN 特征提取。当画面里有 30 个人时相当于每帧额外跑 30 次小的图像分类网络计算量不可忽视。解决先定位瓶颈。把 tracker 注释掉只跑 YOLO如果帧率能到 25 以上瓶颈就在跟踪端的特征提取。常见解法有三个一是降低 ReID 输入尺度比如把裁剪区域从 128x64 降到 64x32二是隔帧提取外观特征仅对确认轨迹和候选轨迹做特征更新三是在算力确实不足的平台上放弃外观分支只保留运动匹配牺牲一部分 ID 稳定性换取帧率。资源包的 track.py 里如果没有跳帧逻辑自己加一个 frame % 2 0 的判断即可。对于 rk3588 这类边缘盒子我一般优先选择方案二把 ReID 模型量化到 INT8 再部署。4.5 换电脑之后推理结果和原来完全不同现象同一份权重、同一段视频在 A 机器上框位置偏了几像素在 B 机器上完全检测不到目标。原因绝大多数情况是环境依赖的版本错位。opencv 的缩放插值算法、torch 的 CUDA 版本、ultralytics 的版本差异都会引起推理结果漂移。你训练时用的环境配置在别人那里没有完全复刻。解决把 requirements 版本的驻点固定到小版本级别。资源包里的运行步骤 PDF 写了环境配置的完整流程照着走能解决大部分问题。强烈建议用 conda 单独建一个虚拟环境Ultralytics 的版本号精确到 patch不要使用 “latest” 之类的安装方式。环境配置完成后再用一段固定的小视频作为基准测试每次环境改动后跑同一段视频检查输出的一致性。这也是把 yolov8 环境配置过程中的玄学部分变成可验证过程的最直接手段。5. 进阶验证把调试过的模型接入业务逻辑前的两件小事模型精度和跟踪稳定性都调好之后离真正可用还差一步验证跟踪结果能支撑业务规则。这一步不做你手上只有一个“会画框的程序”而不是一套安全监测工具。我习惯在交付或者上线前做两件事。第一件用一段连续视频统计每个 ID 的生命周期。具体逻辑是对视频中的每个 ID 记录首次出现帧和最后消失帧计算持续帧数再换算成秒。这在业务上对应的是“某工人未戴安全帽持续时间”这个量化指标比单帧检测结果更有说服力也是向现场管理者展示工作成果的最直观证据。track_lifecycle {} for frame_idx, tracks in enumerate(all_tracks): for track in tracks: track_id track.track_id if track_id not in track_lifecycle: track_lifecycle[track_id] {first_frame: frame_idx, last_frame: frame_idx} else: track_lifecycle[track_id][last_frame] frame_idx # 按持续帧数排序输出超过 60 帧的长轨迹 long_tracks {k: v for k, v in track_lifecycle.items() if v[last_frame] - v[first_frame] 60} print(f出现超过 60 帧的跟踪目标数: {len(long_tracks)})第二件做一次误报回放。挑一段正常的施工视频把所有跟踪框和 ID 画上后走一遍重点看 no-helmet 和 no-vest 的报警点。正常情况下这些类别不应该在工人正确佩戴装备时被频繁触发。我遇到过的最典型情况是安全帽颜色和背景色接近时比如白色帽配灰白墙模型对 helmet 的置信度掉到阈值以下于是同一人被反复算作违规。这类问题的修正点不在算法而在摄像头机位——换个角度就能让帽子和背景分离。从那以后我每次拿到新的场景数据或调整模型后都会强制跑一遍这两件事统计长轨迹数量然后回放一次误报视频。亲自看一遍检测和跟踪的实际效果比看任何曲线都来得踏实。这个过程能帮你识别出大量在离线指标里看不出来的问题也决定了这套系统是从“能跑”变成“能用”。希望这些拆解和踩坑记录能帮到你省下走弯路的时间。本文还有配套的精品资源点击获取