多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于YOLOv8的人员状态检测:4943张图训练跑睡抽烟打电话跌倒模型

基于YOLOv8的人员状态检测:4943张图训练跑睡抽烟打电话跌倒模型 简介人员状态检测数据集面向安全监控与行为识别场景供目标检测研究者与开发者使用包含跑、睡、抽烟、打电话、跌倒5个类别适合用于YOLO、SSD等目标检测模型的训练与效果验证。压缩包为7z格式共2000个文件其中包含1999个XML标注文件与1个TXT使用说明整体大小约232.8MB标注采用Pascal VOC格式每个XML均包含目标类别与矩形框坐标可直接转换为YOLO等常用格式省去格式转换环节。该数据集基于4943张样本制作累计标注5790个目标框其中sleep类框数最多1771个Running次之1724个类间分布覆盖常见异常行为适合开展类别均衡性研究与数据扩充实验数据标注通过labelImg完成矩形框边界准确可直接用于模型输入。需注意数据集中约3/4为增强样本使用时应结合原始数据评估模型表现。目前已有490人浏览学习下载后可省去大量人工标注时间尤其适合需要快速构建行人异常行为检测实验的研究者和开发者。1. 用4943张标注图给“跑、睡、抽烟、打电话、跌倒”建模型这份数据集够用在哪“人员状态跑睡抽烟打电话跌倒检测数据集4943张5类别.7z”第一眼像普通网盘资源实际上它解决的是安防监控里最急的一个问题摄像头全天盯着但人是看不过来的。工地有人躺下睡觉、加油站有人抽烟打电话、养老院老人跌倒这些事单靠人眼盯屏漏掉一次就可能出大事故。这份数据集把跑、睡、抽烟、打电话、跌倒五类人员状态做成了带框标注的监督样本合计4943张图打包成7z。适合谁做智慧工地、智慧园区、加油站安全预警或养老看护的算法工程师和学生。花半小时解压盘点再用yolov8训练自己的数据集跑两版就能判断这份标注质量值不值得作为底料继续投入。2. 解压到抽检训练前先花半小时把数据集的底细摸清2.1 先解压再盘点7z文件和目录里到底有什么数据集以7z压缩包形式分发第一步不是急着解压而是先看包内结构。用命令行在Linux下执行# 先列内容head 只取前30行确认包内是平铺还是分好目录 7z l 人员状态跑睡抽烟打电话跌倒检测数据集4943张5类别.7z | head -30这一步很关键。7z l只列出包内文件列表不解压能让你先看到顶层目录。常见有两种情况一种是压缩包内已经分好images/train、images/val、labels/train、labels/val解压后直接能用另一种是所有图片和一个labels目录平铺在一起后面要自己写脚本划分。前者省事后者也不难但提前知道能省掉解压后重新整理的麻烦。确认结构后再真正解压# -o 指定输出目录注意 -o 后面紧跟路径不要加空格 mkdir -p person_status 7za x 人员状态跑睡抽烟打电话跌倒检测数据集4943张5类别.7z -o./person_status这里有个细节7za是 p7zip 的独立版可执行文件有些发行版默认只装了7z如果提示命令不存在先装 p7zip-full。完整版7z支持的压缩算法比7za更全解压报“不支持的压缩算法”时优先换命令而不是换工具。Windows 用户直接用 7-Zip 或 Bandizip 右键解压即可不用碰命令行如果你习惯在 PyCharm 里用 subprocess 调 7z注意参数要写成列表形式不要把整条命令拼成一个字符串。解压完成后用tree person_status -L 2看一眼目录层级重点核对两个东西图片和标签是否同名同前缀train 和 val 是否都已经有对应目录。数据集的目录结构决定后续 data.yaml 怎么写这一步省不得。2.2 标签格式与类别分布用一行脚本确认5类都在目标检测数据集的标注文件有格式门槛这份数据集既然面向YOLO系训练标签大概率是 YOLO 格式的 txt每行class_id x_center y_center width height坐标全部归一化到 [0,1]。拿到数据后先别信简介自己统计一遍最踏实# count_labels.py: 统计 train/labels 下每个类别出现的标注次数 import os from collections import Counter label_dir person_status/labels/train counter Counter() for fn in os.listdir(label_dir): if not fn.endswith(.txt): continue with open(os.path.join(label_dir, fn), encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) 5: # 标准YOLO格式是5个字段 counter[int(parts[0])] 1 for cls_id in sorted(counter): print(fclass {cls_id}: {counter[cls_id]} instances)这段脚本的逻辑很简单遍历标签目录逐行读 txt取每行第一个字段累加计数。跑完你可能会发现五类数量并不均衡比如“睡”只有两三百条而“走”或“跑”有上千条这很正常。数量不均衡直接影响后续训练的类别权重设置先心里有数第四章再细说处理方案。如果某个类别连几十条都不到那训练时这一类基本学不出泛化能力需要靠复制样本或额外补充数据救场。2.3 抽帧目检把“跑”和“跌倒”从标注里挑出来看统计数字只能说明“有多少”不能说明“标得对不对”。目标检测数据集最怕的不是数量少而是标注框质量差——框偏移半个身位、类别张冠李戴、漏标严重。用 OpenCV 把标签画回图上抽查二十张比任何统计脚本都有说服力import cv2 img_dir person_status/images/train label_dir person_status/labels/train names [running, sleeping, smoking, calling, falling] # 必须与data.yaml的names顺序一致 def draw_one(img_path, label_path): img cv2.imread(img_path) h, w img.shape[:2] with open(label_path, encodingutf-8) as f: for line in f: cls_id, x_c, y_c, bw, bh line.split() x1 int((float(x_c) - float(bw) / 2) * w) y1 int((float(y_c) - float(bh) / 2) * h) x2 int((float(x_c) float(bw) / 2) * w) y2 int((float(y_c) float(bh) / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, names[int(cls_id)], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) return img # 随机抽10张画框保存到 preview 目录 import os, random random.seed(42) os.makedirs(preview, exist_okTrue) txts random.sample(os.listdir(label_dir), 10) for i, lf in enumerate(txts): img_path os.path.join(img_dir, lf.replace(.txt, .jpg)) if not os.path.exists(img_path): print(missing image for:, img_path) # 这里会暴露图片与标签不同名的问题 continue cv2.imwrite(fpreview/{i:02d}.jpg, draw_one(img_path, os.path.join(label_dir, lf)))坐标归一化的框要乘回图片宽高这是新手最容易搞错的点。抽检时重点看三类问题一是“跑”的框是否只框住上半身跑步姿态下肢变化大框如果太小会漏掉腿影响召回二是“跌倒”和“睡觉”的标注是否出现同一张图两种标法这俩在姿态上确实接近标注不一致会把模型训练到“精神分裂”三是图片里有没有单张包含多个人的场景如果有标注是不是把所有人都框了漏人会让模型在密集场景里漏检。抽检结果如果整体一致性好这份数据能直接用如果发现大量错标老老实实花时间清洗脏数据喂进去后面所有调参都是白费。3. 用YOLOv8把数据集跑通目录组织、标签核验与最小训练命令3.1 目录组织与data.yaml第一张要核对的牌Ultralytics YOLOv8 是目前训练自己的数据集合适的工具预训练权重丰富、配置简单、单卡就能跑。训练前数据集要组织成它认得的目录结构person_status/ images/ train/ val/ labels/ train/ val/如果压缩包内是平铺结构用下面的脚本按 8:2 随机划分所有文件重新移动到对应目录# split_train_val.py import os, random, shutil base person_status img_src os.path.join(base, images_all) # 平铺的图片目录 lbl_src os.path.join(base, labels) # 平铺的标签目录 random.seed(42) all_txts [f for f in os.listdir(lbl_src) if f.endswith(.txt)] random.shuffle(all_txts) val_n max(1, int(len(all_txts) * 0.2)) for split, files in [(train, all_txts[val_n:]), (val, all_txts[:val_n])]: for lf in files: stem os.path.splitext(lf)[0] # 标签移动到对应 split 目录 shutil.move(os.path.join(lbl_src, lf), os.path.join(base, labels, split, lf)) # 图片可能因为来源不同存在 jpg/png 混用按需尝试三种后缀 for ext in [.jpg, .jpeg, .png]: p os.path.join(img_src, stem ext) if os.path.exists(p): shutil.move(p, os.path.join(base, images, split, stem ext)) break else: print(missing image for, lf) # 标签存在但图片缺失必须处理脚本里的for...else是我个人习惯如果图片三种后缀都没找到else 分支会触发并打印缺图文件名避免静默丢样本。划分后写 data.yaml# person_status/data.yaml: 路径写绝对路径names 顺序就是标注txt里的class_id path: /home/your_name/person_status train: images/train val: images/val nc: 5 names: 0: running 1: sleeping 2: smoking 3: calling 4: fallingnames 的顺序必须和标签 txt 第一列数字一一对应。这一点怎么强调都不为过如果实际标注里 0 是 sleeping 而你写成了 running训练时 loss 会正常下降但推理出来的框会张冠李戴而且这类错误非常隐蔽——指标好看业务全错。所以 data.yaml 里 names 的顺序一定要先拿第五章抽检时的画框脚本确认过再定。3.2 YOLOv8最小训练脚本从预训练权重迁移到5类模型目录就绪、YAML 写好后训练脚本可以只用十几行。第一次跑不求精度求“能跑通”# train.py from ultralytics import YOLO # yolov8s.pt 是官方预训练权重COCO 80类迁移到5类人员状态 model YOLO(yolov8s.pt) model.train( dataperson_status/data.yaml, epochs100, imgsz640, # 输入分辨率640是显存与精度的平衡点 batch16, # 根据显卡显存调爆显存就减半 patience20, # val loss 连续20轮不降就早停 cacheTrue, # 小数据集全部缓存进内存省反复读盘 projectruns/person_status, nameexp1, )imgsz640是 YOLOv8 的默认推理尺寸对跑步、睡觉这种大目标足够但“抽烟”“打电话”里的手部目标很小后面第四章会讲怎么调。patience是早停机制个人建议不要关掉小数据集训练几十轮后就开始过拟合早停能帮你保住验证集上最好的权重而不是最后一轮的。cacheTrue对 4943 张这种量级非常有用图片全量载入内存后每个 epoch 省掉大量磁盘IO训练速度快不少。如果你的内存只有16G担心缓存爆内存可以改成cacheram效果一样且失败时自动退回磁盘模式。第一次训练建议看控制台日志里的几个关键数字all这一列的 mAP50 是否在提升、每个类别的 recall 是否都有变化。如果跑了 50 轮某个类别的 recall 一直是 0别急着加训练轮数先回去看那个类别的标注质量。3.3 loss曲线怎么读哪些信号说明模型在“背题”训练结束后结果目录runs/person_status/exp1/下有results.csv里面按轮次记录了 box_loss、cls_loss、dfl_loss 以及验证集指标。用几行代码看趋势# read_curve.py import pandas as pd df pd.read_csv(runs/person_status/exp1/results.csv) # 只看验证集损失和验证集mAP两列避免思考train loss是否下降 val_cols [c for c in df.columns if val in c and (loss in c or mAP in c)] print(df[val_cols].tail(20))读曲线时有个基本判断顺序验证集 box_loss 和 cls_loss 都持续下降说明模型还在学train loss 下降但 val loss 回升说明过拟合早停已经在 patience 窗口内帮你停下来val loss 从头到尾几乎是一条直线说明学习率可能太高或数据本身有问题。另一个值得关注的是mAP50-95和mAP50的差距数据集存在大量姿态相近的类别时mAP50 好看但 mAP50-95 很低说明模型的定位精度一般框的位置连续性差放到视频里会出现抖动。这个信号提示你后面做部署时不要单帧判断要加时序平滑。4. 类别不均衡、小目标和跌倒遮挡三个必调参数的取舍逻辑4.1 类别不均衡怎么调看整体指标不如看每类的recall五类人员状态样本量几乎不可能均衡。“跑”和“睡”可能各占上千张“抽烟”“打电话”因为目标小、标注成本高可能只有两三百张。YOLOv8 默认按均匀分布对待每个类别样本少的类在 loss 里贡献少训练结果往往是对多类过度拟合、对少类欠拟合。这时候先别急着找权重参数。Ultralytics 的 YOLOv8 没有直接暴露class_weights这样的开关常见做法在两个方向一是从数据侧做类别平衡把少类样本在数据目录里复制两到三份用数量把 loss 权重拉平二是从评估侧入手不看整体 mAP单独看每个类别的 recall。训练完用如下方式输出每类指标from ultralytics import YOLO model YOLO(runs/person_status/exp1/weights/best.pt) metrics model.val(dataperson_status/data.yaml) # 直接打印每个类别的召回率 print(metrics.recall) # 按class id顺序排列 print(metrics.ap50_index) # 按class id顺序排列的AP50哪个类别 recall 明显低于其他类就针对那一类复制样本或补充数据。复制样本要注意不要复制到验证集只复制 train 部分而且复制后要在 data.yaml 的 train 路径下重新扫描确保样本数量统计是新的。这个办法粗糙但有效比改 loss 公式更可控。真正把类别权重写进训练逻辑的方案是自定义 Dataset 做重采样技术含量高但对这份数据量级来说收益不大。4.2 小目标漏检抽烟和打电话是“手指头”用imgsz和切图治抽烟、打电话这两个类别的目标区域集中在手部。监控视角下一个人全身框可能有几百像素高但手部区域可能只有三四十像素。YOLOv8 在 640 分辨率下特征图对这样的小目标不敏感最常见的手段是提高输入分辨率参数作用场景建议起始值调整说明imgsz960抽烟/打电话小目标漏检640 → 960显存不够就把 batch 减半不要同时开大图和大batchclose_mosaic10跌倒/睡姿被mosaic切碎10最后10轮关闭mosaic让模型见完整目标cacheTrue小数据集反复读盘耗时True内存不到16G改用ram失败自动退回磁盘模式patience20防过拟合20–30验证集连续不降则早停保住best.pt提高 imgsz 到 960 后显存占用会明显上涨8G 显存的卡配 batch8 基本是极限如果显存还是不够另一种思路是把大图切块推理就是把监控原图切成 640 或 960 的块分别推理再合并结果等于把“分辨率”拆成“多路检测”落地实现时可以借助 SAHI 这类开源切图工具。用 imgsz960 重新训练一版你会发现手部小目标的 recall 有明显提升但“跑”“跌倒”这类大目标指标基本不变这是因为大目标本来就检得到小目标才是瓶颈。4.3 跌倒样本的特有难点mosaic开关时机和空间增强跌倒检测在这份数据集里属于“姿态极端”的类别跌倒瞬间人体比例和正常站立差异巨大且经常伴随遮挡——比如老人倒地后身体局部被桌椅挡住。这类样本对数据增强特别敏感最常见的翻车现场是开了 mosaic 和 mixup 之后跌倒样本被拼图切碎模型根本学不到完整的人体姿态。Ultralytics YOLOv8 在训练后期默认会关闭 mosaic你可以手动控制这个时机。close_mosaic10表示最后 10 轮不再使用 mosaic 增强模型在最后阶段看到的是完整的、未被切开的目标这对跌倒这种强姿态类别特别重要。另外两个值得调的是角度扰动和水平翻转degrees默认 0对跌倒检测可以设degrees5给轻微角度变化让模型对倒地姿态的旋转更鲁棒但不要设太大超过 15 度会让“站立”“跌倒”的边界更模糊fliplr0.5水平翻转可以开跑步、睡觉、跌倒这些姿态在镜面对称后语义不变等于白送一倍训练样本。还有个容易被忽略的参数是hsv_h和hsv_s。这些数据集里的图片可能来自不同摄像头色温差异极大训练时把色调扰动开到 0.02、饱和度扰动开到 0.7能减少模型对特定摄像头色彩的依赖。后期在部署场景里发现一个摄像头效果差另一个好多半就是色彩泛化没做好。5. 复现避坑解压乱码、类别错位、mAP虚高的5条血泪记录5.1 解压与数据准备层的两个翻车现场坑1文件名乱码导致标签与图片失联。现象解压后图片名变成“鈽炴枃_001.jpg”labels 目录里却是正常的英文字符训练时大量图片找不到对应标签。原因压缩包在 Windows 下生成时文件名用了 GBK 编码Linux 默认按 UTF-8 解码导致乱码。解决Windows 用 Bandizip 勾选“自动修正文件名编码”Linux 下用7z解压后用convmv -f GBK -t UTF-8 -r person_status批量转换文件名编码。检验是否修好用前面 2.3 的画框脚本跑十张不报 missing 就说明对上了。坑2训练报错“unable to read image”。现象某些图片文件扩展名是 .jpg 但内容实际是 PNG 编码OpenCV 能扛住但 Ultralytics 的 dataloader 偶发失败。原因数据集从多次采集拼接图像编码被误存为 jpg。解决训练前写一个脚本把所有图片统一重编码顺手把 EXIF 方向信息也清掉——手机拍摄的竖构图 jpg 经常带 EXIF 旋转标记有的解码库读出来是旋转前的数据画框全偏。统一转换的脚本只要十几行# normalize_images.py import cv2, glob for fmt in [*.jpg, *.jpeg, *.png, *.bmp]: for p in glob.glob(fperson_status/images/train/{fmt}): img cv2.imread(p) if img is None: os.remove(p) # 实在读不出来的图直接剔掉 else: cv2.imwrite(os.path.splitext(p)[0] .jpg, img)这步做完图片目录里只剩一种 jpg 格式dataloader 再没报过类似问题。5.2 训练与评估层的三个深层坑坑3类别编号对不上指标正常但推理全错。现象训练过程 loss 正常下降、mAP50 0.8但部署时把“跌倒”全部识别成“睡觉”。原因data.yaml 的 names 顺序与 label 文件里的 class_id 不一致比如 labels 里 0 是 sleeping而 YAML 里 0 写了 running。解决这个坑只能靠画框核对——随机抽 20 张标注画框后看框上的类别名和实际画面是否吻合。不要凭简介里的类别列表猜顺序类别顺序的唯一依据是标签 txt 第一列数字对应的实际内容。坑4测试集 mAP 很高现场摄像头漏检惨重。现象验证集上 mAP50 0.82拿到园区监控视频一跑跌倒漏检率奇高。原因数据集里的图大多是平视视角现场监控普遍是 45 度俯视跌倒姿态在俯视下看起来完全不像平视。这种视角偏移不会反映在 mAP 里因为测试集和训练集视角一致。解决从现场机位截取 50 到 100 帧标注后加进训练集做微调只要两三轮就有效果同时推理时把conf从默认 0.25 调到 0.35 左右现场场景宁可漏报也不要被误报淹没。坑5类间混淆严重“睡”和“跌倒”互相抢。现象躺着打电话的人被频繁识别成跌倒被褥挤压的人脸被识别成睡。原因跌倒和睡觉的姿态高度相似关键差别其实在环境——床上有被褥、地上是地板但检测模型的感受野如果只截取人体局部看不到环境上下文。解决训练时不要用过度裁剪的增强策略mosaic关晚一点让模型有机会看到更大的场景推理时用连续帧状态机单帧识别结果先排队连续五六帧都是同一类别再触发报警能把瞬时的误检滤掉。6. 闭环验证让模型在真实监控视频流里少放空枪6.1 从单帧检测到事件上报加一个连续帧确认逻辑模型训练完不要只跑测试集拉一段真实监控视频做闭环验证。先确认单帧效果再用连续帧逻辑压制误报# run_site_video.py import cv2 from collections import deque from ultralytics import YOLO model YOLO(runs/person_status/exp1/weights/best.pt) cap cv2.VideoCapture(site_camera.mp4) # 只检测跌倒业务上误报成本最高其他类先不管 fall_hits deque(maxlen15) while cap.isOpened(): ok, frame cap.read() if not ok: break res model.predict(frame, conf0.35, classes[4], verboseFalse) fall_hits.append(len(res[0].boxes) 0) # 15帧窗口内至少6帧命中且当前帧命中才视为一次跌倒事件 if fall_hits[-1] and sum(fall_hits) 6: print(fall confirmed at frame:, cap.get(cv2.CAP_PROP_POS_FRAMES)) # 这里接报警、截图或推送都行 cap.release()deque(maxlen15)维护最近 15 帧的检测结果窗口内超过 6 帧命中再确认事件这样既不会因为单帧误检乱报警又能在 1 秒内作出反应。conf0.35比默认值高这是我在现场被误报教育后的习惯监控场景的报警宁可晚一点、少一点也不能让值班员十分钟收到十条假警。真正确认模型能落地是在这段视频上跑完并且误报次数人工数得过来。我现在的习惯是训练完先在测试集上看指标再找一段现场视频看行为最后跑上一整夜统计误报率。项目上线前这三步全过心里才有底。希望帮到你。本文还有配套的精品资源点击获取
返回列表