简介:面向计算机视觉目标检测开发者与高校课程设计/毕业设计人群,这份压缩包提供了一套基于YOLOv9的工地工人反光衣识别检测系统。系统内含完整Python源码、详细运行教程、训练好的模型权重以及评估指标曲线,覆盖从环境配置、数据集准备、YAML修改、模型训练到测试的全流程,可直接运行示例图片,也可另备数据重新训练,适用于工地安全监控中的反光衣穿戴检测。资源共187个文件,以83个py脚本、31个yaml配置、3个pt模型文件为主,另附ipynb、json、csv等辅助与说明材料,整体压缩包约76.27MB,结构清晰。目前已有230人学习使用,代码经过实测无误,评估指标曲线能直观反映损失与精度的变化过程,便于使用者对照教程复现结果并开展二次优化。
1. 反光衣识别:为什么监控视角下YOLOv9是能直接落地的方案
工地反光衣识别比安全帽更磨人:反光衣在白天是带银灰条的背心,到了夜间或隧道才会因逆反射材料突然亮起来,时亮时暗的材质特性让常规检测模型很难用一个统一模式去学习。YOLOv9这类基于PGI的单阶段检测器,在保留弱纹理目标细节上更有优势,我做反光衣识别检测系统时直接把它作为主力模型。
这套系统的python源码、训练好的模型、评估指标曲线正好是一条完整交付链,适合智慧工地项目的视觉工程师、细分检测的算法团队,也适合0基础python编程起步的学生跟跑全流程。
先说清预期:监控视角下反光衣识别的难点不是“看得到人”,而是“反光带过曝、被压暗、被遮挡时模型还认不认得那几条条纹”。后续每一步都围绕“现场不掉链子”展开。
2. 数据准备与标注:反光衣检测精度上限的一半都在这里
任何检测模型的上限在数据层面就先定下了。YOLOv9的模型能力是在标注质量的基础上做加分,不会无中生有地解决反光衣边缘模糊、类别混乱这些数据问题。这一章按我做智慧工地项目的顺序展开:数据来源、切帧脚本、标注规范与质量检查。
2.1 反光衣数据集要什么:类别、数量与监控视角的三个常见误区
先讲类别设计。工程上我推荐只做单类检测,类别名就叫 vest,界定标准统一为“可见反光材料区域”。不要拆出“红反光衣”“黄反光衣”,工地上服装颜色随项目采购变化,而反光材料的视觉特征反而是统一的。尽量别把“未穿反光衣”做成 no_vest 负类,负类会让训练复杂度翻倍,而且正负样本边界难收敛。没穿反光衣的工人自然落到背景,靠推理时的置信度阈值就能拦下大部分误报。
数量上,一个能拿到现场试用的反光衣检测模型,需要约 3000 到 5000 个标注实例。注意“实例”不等于“图片”:监控视频是连续帧源,同一个人的同一个姿态在相邻帧里反复出现,去重后可能只剩几百张有效图片。模型在这几百张图上反复学,学到的是场景背景而不是反光衣特征,换一个摄像头就失效。这是很多初版模型标定 mAP 不低、现场一测就翻车的直接原因。
监控视角下还有三个常见误区。第一,只从白天画面切帧。反光衣存在的意义恰恰在夜间、雨雾、逆光,这些在常规施工照片里占比极低,要主动找几路现场夜间录像来补。第二,执着于原图分辨率。1080p 摄像头在 50 米外,人物宽度不过几十像素,直接用原图训练会让显存暴涨;推荐先用 imgsz=640 的 yolov9c 训一个基线,再把失败样本集中用 1280 微调。第三,把反光衣框成整个人形。监控视角下人会斜穿画面、被塔吊遮挡,人形框宽高比千变万化,而反光条只是一个局部特征,框应紧贴可见反光区域,否则模型学到的是“人形”而不是“反光衣”。
公开数据集可以当补充,但要过一遍筛选。Roboflow 等平台上有不少 PPE 数据集,下载后先看它们的拍摄机位:俯拍占比高、人物密集、且包含夜间帧的才能直接混入训练;以半身特写为主的数据集和监控视角差异太大,混进去反而干扰特征分布。
2.2 用 Python 把工地监控视频切成训练帧:采样间隔与清晰度
我一般不会从现场直接导长视频给标注工具,而是先做一个切帧脚本,把几路历史监控录像转成训练图片。下面的脚本按时间间隔采样,并把长边限制到 1280,这样后续标注和训练都更可控。
import cv2 import os video_path = "site_camera_01.mp4" save_dir = "train_frames" os.makedirs(save_dir, exist_ok=True) cap = cv2.VideoCapture(video_path) fps = cap.get(cv2.CAP_PROP_FPS) frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) print(f"fps={fps:.2f}, total={frame_count}") skip_frames = int(fps * 2) # 每2秒取一帧,避免连续帧几乎一样 count = 0 while True: ret, frame = cap.read() if not ret: break if count % skip_frames == 0: # 把长边缩到1280以内,减少标注和训练压力 h, w = frame.shape[:2] max_len = 1280 if max(h, w) > max_len: scale = max_len / max(h, w) frame = cv2.resize(frame, (int(w * scale), int(h * scale))) cv2.imwrite(os.path.join(save_dir, f"frame_{count:06d}.jpg"), frame) count += 1 cap.release()这段代码做了两件事。第一,skip_frames 按 fps 动态计算,等于每 2 秒取一帧,保证同一个工人以不同姿态出现多次,又不产生大量内容相同的相邻帧。第二,长边 resize 到 1280,1080p 原图直接送入标注工具会拖慢效率,而反光衣这类小目标在 1280 分辨率下已经足够看清反光条。如果摄像头有几十路,建议先按时间段抽样,专门挑早晚高峰和夜间各抽一段,而不是把全天录像全量切一遍。
切出来的帧要先清理一遍黑屏、花屏和运动模糊严重的画面。常见做法是用 ffmpeg 的 freeze-detection 或者直接人眼快速翻一遍,模糊帧作为正样本会直接教模型“反光衣是糊的”,对收敛没有帮助。这一步通常能筛掉 10% 到 20% 的帧。
清理完按 8:1:1 分成 train、val、test。关键约束是:val 和 test 必须来自不同时间段或不同摄像头的视频,不能在同一个视频里随机拆。否则评估指标会虚高,因为模型把同场景背景背下来了,而不是真正泛化了反光衣特征。
2.3 标注规范与质量检查:反光衣为什么比安全帽更容易被漏标
标注工具用 AnyLabeling 或 X-AnyLabeling 都可以,这类工具有 SAM 辅助分割打底,先把目标区域抠出来再转成 YOLO 格式。但反光衣在暗光下经常和水泥墙面混在一起,SAM 分割出来很可能是一团灰,我习惯先把帧的 Gamma 拉高确认反光带边界再落框。
YOLO 格式的标签是 txt,每行五个数字:类别、框中心的归一化 x 和 y、框的归一化宽和高。反光衣标注时只要坚持“框贴反光材料可见区域”,生成的 txt 天然符合小目标分布,模型训练时也不会被大面积背景干扰。
标注质量上要记住一句话:漏标比错标更要命。错标是框偏了几个像素或类别边界模糊,模型在训练时通过邻近样本学到平均形状;漏标则直接告诉模型“这个地方没有反光衣”,训练出来的模型会把另一个穿反光衣的工人当背景。这种隐性错误在 mAP 曲线上几乎看不出来,只能靠视频实测发现。
检查标注质量可以跑一个简单的统计脚本,专门看框的宽高比分布:
import os import numpy as np label_dir = "labels/train" wh_list = [] for fn in os.listdir(label_dir): if not fn.endswith(".txt"): continue with open(os.path.join(label_dir, fn), "r") as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue w = float(parts[3]) # 归一化后的宽度 h = float(parts[4]) # 归一化后的高度 wh_list.append((w, h)) wh = np.array(wh_list) print("平均高宽比:", (wh[:, 1] / (wh[:, 0] + 1e-6)).mean()) print("高宽比大于2.5的框占比:", (wh[:, 1] / (wh[:, 0] + 1e-6) > 2.5).mean())这个脚本统计的是归一化坐标,所以 w 和 h 的值都在 0 到 1 之间。如果高宽比大于 2.5 的框占比超过七成,说明当前数据集里竖构图占绝对主导,横向斜穿画面的样本不足,模型对横向姿态的召回率会偏低。同理,把框的坐标映射回原图后统计平均亮度,能判断夜间样本占比是否过高或过低,这类检查结果直接指导下一轮补数据的方向。
3. 用 YOLOv9 源码训练反光衣模型:命令、参数与评估指标曲线
系统交付的另一半是训练与评估。这一章从环境选型开始,到训练命令、参数选择,最后落到评估指标曲线的读法,几步走完。
3.1 环境选型:ultralytics 仓库、CUDA 和显卡的匹配关系
YOLOv9 的工程实现,常见做法是直接用 ultralytics 这一套统一代码库。它把训练、验证、导出都收敛成一行命令,模型定义、损失函数、数据增强都封装好了,做反光衣这种单类检测不需要自己造轮子。先确认 torch 能正常调用 GPU:
import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else "no gpu")is_available() 返回 False 的常见原因,是装成了 CPU 版的 torch。很多人在这一步翻车:用 pip 默认源装 torch,装出来的是 CPU 版本,3090 插在那里但训练全程跑 CPU。装好之后先跑这几行,确认输出里有显卡型号再继续。
显存大小决定模型尺寸。反光衣检测我用 yolov9c 作为默认基线,它在精度和速度之间最平衡;边缘盒子或高帧率场景选 yolov9s;对离线分析精度要求极高时选 yolov9e,但训练时间和显存占用会明显上涨。别一上来就上最大模型,反光衣是单类任务,yolov9c 的容量已经足够,模型再大只会增加部署负担。
| 模型 | 显存占用 | 适用场景 |
|---|---|---|
| yolov9s | 约 2GB | 边缘盒子、高帧率 |
| yolov9c | 约 6-8GB | 通用基线,推荐 |
| yolov9e | 12GB 以上 | 精度优先的离线分析 |
3.2 训练命令拆解:epochs、imgsz、batch、patience 怎么定
训练前先把数据配置写好。反光衣数据集的 data yaml 如下:
# reflective_vest.yaml path: ./reflective_vest_dataset train: images/train val: images/val test: images/test nc: 1 names: 0: vestpath 指定数据集根目录,train/val/test 相对根目录写。nc 为 1 表示单类,names 里的 0: vest 要和标注文件里的类别索引严格对应。然后启动训练:
yolo train \ data=reflective_vest.yaml \ model=yolov9c.pt \ epochs=200 \ imgsz=640 \ batch=16 \ patience=30 \ device=0 \ project=runs/vest \ name=train_baseline \ pretrained=True \ cache=True命令里值得关注的参数集中在下表:
| 参数 | 反光衣推荐 | 理由 |
|---|---|---|
| epochs | 200 | 单类任务 150 轮后容易过拟合,200 轮给足收敛余量 |
| imgsz | 640 基线 / 1280 微调 | 监控小目标多,先 640 快速迭代,再 1280 精修 |
| batch | 按显存能承受的最大值 | batch 越大训练噪声越小,但要关注学习率配套 |
| patience | 30 | 30 轮没提升就提前停,省时间 |
| cache | True | 反光衣数据帧多,缓存到内存能明显缩短训练时间 |
| pretrained | True | 用 COCO 预训练权重做迁移学习,绝不从零开始 |
batch 和学习率的关系要单独说一句。ultralytics 默认按 batch 做学习率线性缩放,所以直接给 batch 通常没问题;但如果自定义了 lr,batch 减半的同时也要把 lr 减半,否则损失曲线会在原地震荡。显存不够时,优先把 batch 降到 8 并配合 cache=True,而不是换更大的模型。
还要提醒一个迁移学习的坑:用 yolov9c.pt 预训练权重时,data yaml 里的 nc=1 会和 COCO 的 80 类冲突。ultralytics 会在加载时自动重建 detect 头,所以不要手动去改 yolov9c.yaml 的 nc,改了反而会报维度不匹配。让框架自己处理即可。
训练完成后,用验证命令在 test 集上确认最终表现:
yolo val \ model=runs/vest/train_baseline/weights/best.pt \ data=reflective_vest.yaml \ imgsz=640 \ batch=163.3 训练好的模型怎么挑:评估指标曲线不只是看 mAP
训练结束后的 runs/vest/train_baseline/weights/ 目录下会有 best.pt 和 last.pt。best.pt 是 val 集上 mAP 最高那一轮的权重,last.pt 是最后一轮的权重。反光衣场景我几乎只用 best.pt,因为 patience 机制通常让 last.pt 停在过拟合边缘,best.pt 更贴近泛化最优。
评估指标曲线在 results.png 里,但更原始的数据在 results.csv 里,字段包括 train/box_loss、val/box_loss、metrics/precision、metrics/recall、metrics/mAP50、metrics/mAP50-95。读曲线不能只看最终数值,要看趋势:
- val/box_loss 在尾部回升,说明模型开始背训练集,过拟合确认。
- precision 高、recall 低,模型偏保守,漏检多,适合对误报容忍度低的场景。
- recall 高、precision 低,模型偏激进,误报多,适合安全监管这类“宁多勿漏”的场景。
- mAP50 和 mAP50-95 差距大,说明框位置不稳定,检测到反光衣只是“碰上了”。
想自动挑出最佳 epoch,用几行 Python 读 results.csv:
import pandas as pd df = pd.read_csv("runs/vest/train_baseline/results.csv") df["mAP50"] = df["metrics/mAP50(B)"] best_idx = df["mAP50"].idxmax() print(df.loc[best_idx])这个脚本输出的是 mAP50 最大的那一行,里面包含那一轮的 epoch 号、loss 和各类指标。反光衣这类安全用品,最终选模型时建议以 recall 优先——漏掉一个穿反光衣的工人,比多框一个背景严重得多。实际交付时我常常选的是 recall 曲线和 precision 曲线交叉点附近那一轮权重,而不是严格按 mAP50 最大值来选。
4. 反光衣识别落地避坑:现场把模型打回原形的五个问题
模型在标定数据上 mAP 不低,不代表现场能用。监控视角的反光衣识别,坑大多不在模型结构,而在数据分布和部署细节。下面五条踩坑记录按出现频率从高到低排序,全部来自实际交付过程。
4.1 夜间补光灯直射:反光带过曝成白团导致漏检
现象:白天和夜间的检测效果都能接受,但镜头正对出入口且有红外补光灯时,反光带反光亮度极高,过曝成一团白色,模型频繁漏检。
原因:反光衣的逆反射材料在大角度补光下,动态范围远超普通物体,而训练数据大多来自正常曝光的监控画面,模型没见过“高光白斑”这种形态,自然不敢出框。
解决:在增广管线里加 RandomBrightnessContrast,把亮度上限拉高到接近过曝的程度;更有效的是收集夜间补光灯直射的样本,把过曝区域作为正样本标进数据集。推理时还可以加一道预处理,把像素值大于 240 的区域先压回正常范围再送进模型,给检测器一个相对温和的输入。我用 OpenCV 实现时就是先 split 通道,对大于阈值的位置做 clip 后再 merge,几行代码的事。
4.2 GPU 利用率只有四成,epoch 跑得比预期慢一倍
现象:用 3090 训练,nvidia-smi 显示利用率在 40% 上下波动,一个 epoch 耗时比预期多一倍。
原因:数据加载是瓶颈。反光衣数据集大量来自连续帧,JPEG 解码和磁盘读取让数据处理吃紧,GPU 在空等数据。多机多卡训练时这个问题更明显,主卡数据队列一堵,整个集群的同步等待全都拉满。
解决:训练命令里补上 cache=True,把数据集缓存进内存,再确认 num_workers 不为 0,按 CPU 核数减 1 设置。这两步做完通常能直接缩短 30% 以上的训练时间。另外,Windows 下 num_workers 设大容易触发 DataLoader 的报错,反光衣这种连续帧数据集我建议直接在 Linux 环境里训练,省得跟进程保护机制纠缠。
4.3 迁移学习改 nc 报错:检测头维度对不上
现象:把 yolov9c.pt 相关的模型配置改成 nc=1 后,训练一开始就报 keyerror,或者损失直接变成 nan。
原因:预训练权重是在 COCO 80 类上收敛的,手动改了模型 yaml 里的 nc 后,检测头输出维度和预训练权重对不上。有些使用者还会连模型 yaml 的深度、宽度一起动,问题更难排查。
解决:不手动改模型 yaml 的 nc。让 ultralytics 在训练时根据 data yaml 自动重建 detect head,model 参数仍写 yolov9c.pt,其余交给框架处理。改模型结构的事,等基线跑通之后再考虑。如果你是从网上的老教程里扒的加载代码,也要留意它是不是用 torch.load 手动拼权重,这种写法最容易触发 shape mismatch。
4.4 mAP50 到了 0.92,现场视频一帧有框一帧没有
现象:离线评估 mAP50 有 0.92,拿到现场视频一跑,同一件反光衣时断时续地出框,叠加到画面上就是明显闪烁。
原因:标定集与现场画面的曝光、白平衡、编码压缩不同,模型对亮度波动的鲁棒性不够。视频流里 I 帧清晰、P 帧模糊,模型在模糊帧上召回率下降,也是闪框的来源。
解决:推理时把 confidence 从 0.25 上调到 0.45,避免低置信度框在模糊帧间抖动;NMS 的 iou 阈值保持在 0.5 左右。如果还闪,用流式预测接口读帧,并引入跳帧后对连续 3 帧做结果投票,以多数结果作为最终输出。判断是模糊帧还是场景偏移,可以把视频导出成图片序列,按帧号统计置信度,如果低置信度帧恰好对应 P 帧,那基本就是编码清晰度问题。
4.5 摄像头夜间切黑白模式:灰度画面下模型几乎全漏
现象:现场摄像头晚上自动切黑白模式,反光衣和灰色墙面、灰色管廊在灰度上非常接近,白天训练出来的模型召回率掉到没法用。
原因:模型学到的很多特征集中在颜色通道的对比上,反光衣那一抹荧光黄在 RGB 里饱和度高,网络把大量权重压在 hue 通道上。RGB 输入在黑白模式下只剩亮度信息,原特征全部失效。
解决:训练增广里加灰度化,以 30% 到 50% 的概率随机把彩色帧转成灰度,强迫网络从亮度纹理学习反光条的形态。同时标注时只框“反光材料可见区域”而非整个人,灰度模式下高亮条纹仍然是清晰可辨的个别小区域,召回率能明显回升。有些摄像头白平衡漂移导致偏绿偏蓝,也可以用同样的增广思路去覆盖。
5. 把训练好的模型接到监控 RTSP 流上:置信度阈值与 NMS 的最后一步调优
5.1 用 Python 在 RTSP 流上做实时检测的最小脚本
前几章的坑都填掉之后,就能把 best.pt 接到现场视频流了。常见做法是用 OpenCV 读取 RTSP 流,再通过 ultralytics 的 YOLO 接口做帧推理。先确认依赖:
pip install ultralytics opencv-python推理脚本如下:
from ultralytics import YOLO import cv2 model = YOLO("runs/vest/train_baseline/weights/best.pt") cap = cv2.VideoCapture("rtsp://user:pass@192.168.1.64:554/Streaming/Channels/101") frame_interval = 10 # 每10帧推理一次 idx = 0 while True: ret, frame = cap.read() if not ret: break if idx % frame_interval == 0: results = model.predict(frame, conf=0.45, iou=0.5, imgsz=640, verbose=False) for r in results: for box in r.boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() conf = float(box.conf[0]) cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), 2) cv2.putText(frame, f"vest {conf:.2f}", (int(x1), int(y1) - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) cv2.imshow("reflective-vest", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break idx += 1 cap.release() cv2.destroyAllWindows()frame_interval=10 表示每 10 帧推理一次,大约每 0.3 秒刷新一次结果,既保留了实时性,又减轻模型推理压力。conf=0.45、iou=0.5 沿用上一章验证过的现场参数。如果检测框在高帧率视频里闪烁,把 conf 提到 0.55 或再加帧间投票。
5.2 置信度阈值、IOU 阈值在监控场景的推荐区间
反光衣检测的参数区间和通用目标检测不一样。反光衣漏检代价更高,整体策略是“保守召回”:
| 参数 | 推荐区间 | 说明 |
|---|---|---|
| conf | 0.35~0.55 | 低于 0.3 误报爆炸,高于 0.6 漏检变多 |
| iou | 0.45~0.5 | 高于 0.7 会把密集人群的相邻框合并掉 |
| imgsz | 640 或 1280 | 监控远景小目标优先 1280 |
出入口特写机位画面里人很大,conf 可以锁 0.55;远距离全景机位人很小,conf 必须降到 0.35 才能保证召回。不同机位不要共用一组参数,现场交付时把阈值按摄像头配置化,是比调模型更稳的优化点。
5.3 交付前的一个验证技巧:录一段原视频做旁路对比
最后分享一个验证方法。不要用测试图片集做交付验收,直接把现场原视频录一段,模型推理结果叠加在原画旁边做同屏对比。这样能直观看到哪些帧漏检、哪些帧误报,比 mAP 曲线可信得多。
有一次我把模型交给现场,对方反馈“晚上十点之后开始乱报”。我调出旁路视频才发现,是夜间大门关闭后,门卫的反光背心被探照灯照得过曝,模型把所有高光白斑都当成了反光衣。最后没有改模型,而是在预处理里加了一道高光区域过滤,把超过阈值的像素块先剔除再推理。这类问题在评估指标曲线里永远不会暴露,只有旁路对比能抓到。希望这个习惯也帮到你。
本文还有配套的精品资源,点击获取