YOLOv9反光衣识别实战:从数据标注到RTSP流部署的完整方案
发布时间:2026/9/28 19:48:01 作者:尧图编辑部 阅读量:1,286

简介面向计算机视觉目标检测开发者与高校课程设计/毕业设计人群这份压缩包提供了一套基于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 米外人物宽度不过几十像素直接用原图训练会让显存暴涨推荐先用 imgsz640 的 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_okTrue) cap cv2.VideoCapture(video_path) fps cap.get(cv2.CAP_PROP_FPS) frame_count int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) print(ffps{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, fframe_{count:06d}.jpg), frame) count 1 cap.release()这段代码做了两件事。第一skip_frames 按 fps 动态计算等于每 2 秒取一帧保证同一个工人以不同姿态出现多次又不产生大量内容相同的相邻帧。第二长边 resize 到 12801080p 原图直接送入标注工具会拖慢效率而反光衣这类小目标在 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 能正常调用 GPUimport 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通用基线推荐yolov9e12GB 以上精度优先的离线分析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 \ datareflective_vest.yaml \ modelyolov9c.pt \ epochs200 \ imgsz640 \ batch16 \ patience30 \ device0 \ projectruns/vest \ nametrain_baseline \ pretrainedTrue \ cacheTrue命令里值得关注的参数集中在下表参数反光衣推荐理由epochs200单类任务 150 轮后容易过拟合200 轮给足收敛余量imgsz640 基线 / 1280 微调监控小目标多先 640 快速迭代再 1280 精修batch按显存能承受的最大值batch 越大训练噪声越小但要关注学习率配套patience3030 轮没提升就提前停省时间cacheTrue反光衣数据帧多缓存到内存能明显缩短训练时间pretrainedTrue用 COCO 预训练权重做迁移学习绝不从零开始batch 和学习率的关系要单独说一句。ultralytics 默认按 batch 做学习率线性缩放所以直接给 batch 通常没问题但如果自定义了 lrbatch 减半的同时也要把 lr 减半否则损失曲线会在原地震荡。显存不够时优先把 batch 降到 8 并配合 cacheTrue而不是换更大的模型。还要提醒一个迁移学习的坑用 yolov9c.pt 预训练权重时data yaml 里的 nc1 会和 COCO 的 80 类冲突。ultralytics 会在加载时自动重建 detect 头所以不要手动去改 yolov9c.yaml 的 nc改了反而会报维度不匹配。让框架自己处理即可。训练完成后用验证命令在 test 集上确认最终表现yolo val \ modelruns/vest/train_baseline/weights/best.pt \ datareflective_vest.yaml \ imgsz640 \ batch163.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.csvimport 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 在空等数据。多机多卡训练时这个问题更明显主卡数据队列一堵整个集群的同步等待全都拉满。解决训练命令里补上 cacheTrue把数据集缓存进内存再确认 num_workers 不为 0按 CPU 核数减 1 设置。这两步做完通常能直接缩短 30% 以上的训练时间。另外Windows 下 num_workers 设大容易触发 DataLoader 的报错反光衣这种连续帧数据集我建议直接在 Linux 环境里训练省得跟进程保护机制纠缠。4.3 迁移学习改 nc 报错检测头维度对不上现象把 yolov9c.pt 相关的模型配置改成 nc1 后训练一开始就报 keyerror或者损失直接变成 nan。原因预训练权重是在 COCO 80 类上收敛的手动改了模型 yaml 里的 nc 后检测头输出维度和预训练权重对不上。有些使用者还会连模型 yaml 的深度、宽度一起动问题更难排查。解决不手动改模型 yaml 的 nc。让 ultralytics 在训练时根据 data yaml 自动重建 detect headmodel 参数仍写 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:pass192.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, conf0.45, iou0.5, imgsz640, verboseFalse) 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, fvest {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_interval10 表示每 10 帧推理一次大约每 0.3 秒刷新一次结果既保留了实时性又减轻模型推理压力。conf0.45、iou0.5 沿用上一章验证过的现场参数。如果检测框在高帧率视频里闪烁把 conf 提到 0.55 或再加帧间投票。5.2 置信度阈值、IOU 阈值在监控场景的推荐区间反光衣检测的参数区间和通用目标检测不一样。反光衣漏检代价更高整体策略是“保守召回”参数推荐区间说明conf0.35~0.55低于 0.3 误报爆炸高于 0.6 漏检变多iou0.45~0.5高于 0.7 会把密集人群的相邻框合并掉imgsz640 或 1280监控远景小目标优先 1280出入口特写机位画面里人很大conf 可以锁 0.55远距离全景机位人很小conf 必须降到 0.35 才能保证召回。不同机位不要共用一组参数现场交付时把阈值按摄像头配置化是比调模型更稳的优化点。5.3 交付前的一个验证技巧录一段原视频做旁路对比最后分享一个验证方法。不要用测试图片集做交付验收直接把现场原视频录一段模型推理结果叠加在原画旁边做同屏对比。这样能直观看到哪些帧漏检、哪些帧误报比 mAP 曲线可信得多。有一次我把模型交给现场对方反馈“晚上十点之后开始乱报”。我调出旁路视频才发现是夜间大门关闭后门卫的反光背心被探照灯照得过曝模型把所有高光白斑都当成了反光衣。最后没有改模型而是在预处理里加了一道高光区域过滤把超过阈值的像素块先剔除再推理。这类问题在评估指标曲线里永远不会暴露只有旁路对比能抓到。希望这个习惯也帮到你。本文还有配套的精品资源点击获取