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

文章详情

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

目标检测实战:溺水检测数据集构建与YOLOv8训练全解析

目标检测实战:溺水检测数据集构建与YOLOv8训练全解析 做溺水检测这个方向说难不难说简单也真不简单。难点不在模型——现在的目标检测框架一个比一个成熟YOLO拉起来就能跑真正的痛点在数据。COCO、VOC这些公开数据集里根本没有“溺水”这个类别想从零开始标一套又费时间又容易翻车。为了把项目往前推我整理了一套目标检测专用的游泳者溺水数据集一共895张图片带VOC和YOLO两种标注格式类别就是正常游泳者和溺水者这两类。这篇内容不打算只甩个下载链接我会把数据集的构成、两种格式的差异、怎么用它训练自己的YOLOv8模型、怎么在MMDetection这类非YOLO框架里走通VOC格式以及我自己踩过的坑全部讲清楚。无论你是正在做泳池安防、水上乐园智能化还是拿它练手学习目标检测都有参考价值。1. 为什么需要专门的溺水检测数据集1.1 公开数据里找不到“溺水”这个类别先聊一个很多人没意识到的问题通用目标检测数据集再大也覆盖不了所有场景。COCO有80个类别VOC有20个类别里面有人、有杯子、有汽车唯独没有“游泳者”和“溺水者”这种细分动作类别。道理很简单标注团队不可能为每个垂直场景都准备类别标签他们更关心通用物体识别。结果就是你直接拿COCO预训练权重去做溺水检测模型在泳池画面上基本是抓瞎的。它把游泳的人识别成“person”没问题但“person”这个语义太宽泛了救生员、游客、漂浮的救生圈、泳池边发呆的人都会被算进去更别说判断“这个人是不是正在溺水”。这时候就需要一个垂直数据集让模型只关注两类目标正常游泳和溺水同时通过边界框把目标位置定准。这个需求不是小众的。泳池、海滨浴场、水上乐园甚至一些高端小区会所都有实时溺水预警的需求。溺水发生的时间窗口很短几分钟内不干预就会出大事人工盯着十几个监控画面根本不现实。用目标检测模型做第一层筛选框出可能的溺水者再推送人工复核是目前比较务实的落地方案。1.2 场景特殊性决定了通用模型不够用我最初也想过直接在监控视频上跑YOLOv8用默认的80类模型检测person再判断姿态结果很快被打脸。水面场景有几个非常头疼的干扰因素第一是水面反光。阳光照在水面上会产生大量高光区域这些区域在图像里和人体边缘的对比度差检测框容易漂移。第二是水花遮挡。人在挣扎时会激起水花水花会挡住一部分肢体这时如果模型没见过这种遮挡模式很容易漏检。第三是视角问题。泳池监控通常是俯拍和COCO数据集里大量平视角度的训练样本差异很大模型的泛化能力会明显下降。第四是目标尺度问题。一个泳池几十米人在画面里可能只有几十个像素高属于典型的小目标检测场景。这些问题都指向同一个结论必须有专门覆盖这些场景的标注数据模型才有可能在真实环境中可用。这也是我整理这套溺水检测数据集的核心原因——把场景特殊性用标签表达出来让模型站在一个相对正确的起跑线上。2. 数据集内容与标注格式深度拆解2.1 图片构成与标注概览这套数据集共包含895张图片统一使用RGB三通道图像。图片来源以泳池监控视角为主兼顾海滨浴场、水上乐园等场景覆盖白天、黄昏、夜间不同光照条件同时包含俯拍和平拍两种典型角度少量近景特写用于补充细节。类别就两类我建议按英文标签管理避免后面对接框架时出编码问题类别ID英文标签中文含义标注实例数量约0swimmer正常游泳者12601drowning溺水者580数字能说明不少问题溺水者实例明显少于正常游泳者这是真实场景的客观规律但也意味着类别不平衡是训练时要重点处理的。图像分辨率以1920x1080为主也有少量1280x720和手机拍摄的近景样本这样的分辨率结构能训练模型适应不同质量的输入源。每张图片的标注框都经过人工校准我对明显不准确的框做了二次调整。不过我必须强调标注数据集是件主观性很强的工作特别是“溺水”这个状态不同标注者可能理解不同。在这个数据集里“溺水”定义为头部大部分没入水中、身体姿态挣扎或失去自主行动能力、有明显呛水特征的目标而正常游泳者指头部露出水面、身体姿态正常游动的目标。这类定义必须写在文档里不然回头发现标注口径不统一哭都来不及。2.2 VOC与YOLO两种格式的核心差异我同时提供了VOC和YOLO两种标注格式目的是让使用不同训练框架的人都可以直接上手不需要折腾格式转换。VOC格式实际是Pascal VOC项目定义的标注规范每个图片对应一个XML文件里面记录了图片尺寸、路径、目标类别以及边界框坐标。坐标是绝对值单位是像素。annotation folderimages/folder filenamepool_00123.jpg/filename size width1920/width height1080/height depth3/depth /size object namedrowning/name bndbox xmin512/xmin ymin780/ymin xmax786/xmax ymax971/ymax /bndbox /object /annotation一个XML文件里可以有多组object代表同一张图片里的多个目标。VOC格式最大的优点是直观、可读打开就能看明白框的位置但缺点是文件量大解析麻烦一点。YOLO格式则是Ultralytics系列框架的标准输入格式每张图片对应一个TXT文件每一行表示一个目标格式是五个数字1 0.3380 0.8106 0.1427 0.0884 0 0.6213 0.6540 0.1135 0.1231这五个数分别表示类别ID、归一化中心点x、归一化中心点y、归一化宽度、归一化高度。所谓归一化就是像素坐标除以图片宽高结果都在0到1之间。两种格式的核心差异我用表格梳理一下对比维度VOC格式YOLO格式文件格式XMLTXT坐标类型绝对像素坐标归一化相对坐标边界框表示xmin, ymin, xmax, ymaxx_center, y_center, width, height文件体积较大很小可读性直观可读不直观需要换算适用框架MMDetection、Detectron2、Faster R-CNN等YOLO系列、部分新框架如果你自己手上有数据要在这两种格式之间转换推荐用图像处理库解析XML后统一换算后面我会给一份可复用的转换脚本。2.3 数据切分的建议方案895张图片不能全扔进去训练一定要留验证集和测试集否则无法判断模型真实泛化能力。我按8:1:1的经典比例做了划分训练集715张 验证集90张 测试集90张这里有一个很容易踩的坑切分之前必须先按来源打乱而不是直接按文件名顺序切。如果图片是按场景排序的前80%可能全是泳池A的画面验证集又集中在泳池B这种“运气差”的切分会让你以为模型效果很好一上线就翻车。最稳妥的办法是随机打乱以后再切然后固定随机种子保证每次复现结果一致。另外如果某个场景或者某一段视频抽帧的图片特别多建议只保留部分相似帧把冗余的图片去掉后再切分防止模型在训练时“记住”相似画面而不是学到通用特征。3. 基于YOLO格式的完整训练流程3.1 目录结构组织与yaml配置拿到YOLO格式的数据后第一件事是整理目录结构。Ultralytics YOLO对目录有约定最好直接按它的要求组织省得后面配置出错。dataset/ ├── images/ │ ├── train/ │ │ ├── pool_00001.jpg │ │ ├── pool_00002.jpg │ │ └── ... │ ├── val/ │ │ └── ... │ └── test/ │ └── ... └── labels/ ├── train/ │ ├── pool_00001.txt │ ├── pool_00002.txt │ └── ... ├── val/ │ └── ... └── test/ └── ...关键在于图片文件夹和标签文件夹必须是兄弟目录名字分别叫images和labels同一张图片的TXT文件名和JPG文件名保持完全一致都是不包含扩展名的纯文件名。我见过很多新手在这里吃亏文件名对不上训练时几乎每张图都报warning白白浪费大量时间。目录准备好以后写一个YAML配置文件指向它path: /your/absolute/path/dataset train: images/train val: images/val test: images/test nc: 2 names: [swimmer, drowning]path建议写成绝对路径避免相对路径导致找不到文件。nc是类别数量names列表的顺序要和标注文件里的类别ID一一对应千万不能调换顺序。3.2 训练命令、参数选择与评测指标我用YOLOv8s作为示例因为它兼顾了精度和推理速度在泳池监控这种需要实时处理的场景比较合适。训练命令如下yolo detect train \ dataswim_drowning.yaml \ modelyolov8s.pt \ epochs150 \ imgsz640 \ batch16 \ patience20 \ projectswim_detect \ nameexp1几个参数我解释一下为什么这么设epochs150895张图片的训练集规模不算大150轮足够模型收敛。如果过早过拟合patience20会在连续20轮验证集指标没有提升时自动停止训练。imgsz640这是速度和精度的平衡点。如果监控画面里人像很小可以考虑imgsz1280但会明显增加显存占用和推理耗时。我实测在多数泳池场景下640经足够画面是1080p的可以用1280试试。batch16根据显卡显存调整。8GB显存用16没问题如果是4GB显存建议降到8否则很容易OOM。训练完成后重点看这几个指标mAP50: 模型在IoU阈值为0.5时的平均精度 mAP50-95: 在0.5到0.95不同IoU阈值下的平均精度更能反映定位质量 precision: 预测的框里真正是对的占比 recall: 所有真实目标里被找出来的占比溺水检测场景里我建议特别关注recall。漏检的代价远高于误报宁可多框几个正常游泳者让后台人工看一眼也不能把真正溺水的人漏过去。所以如果recall偏低优先想办法提recall后续我会讲具体调优手段。3.3 训练效果与推理实测用这套配置训练下来我这边跑出来的结果是mAP50在0.89左右mAP50-95在0.72左右。这个数字看起来不错但实际使用中我更看重单张图的推理速度。用YOLOv8s在GPU上推理单张图像耗时大约3到5毫秒换算过来能跑两百多帧处理监控视频流完全够用。推理命令很简单yolo predict modelruns/detect/train/weights/best.pt sourcetest_video.mp4我测了一段我自己的泳池测试视频后发现白天光线充足时正常游泳者和溺水者基本都能稳定框出只有一个人在画面里特别小、加上大量水花遮挡时模型偶尔会漏检。这个问题的根本原因是训练集中小目标样本不够多后面做数据增强时可以考虑针对性补强。推理阶段还有一个实用技巧把检测结果输出成标注视频文件用saveTrue参数YOLO会自动在画面上画出边界框和类别方便肉眼快速验证效果。4. 从VOC格式到更多检测框架4.1 VOC格式在MMDetection等框架中的用法不少人会问我不一定用YOLO就想用Faster R-CNN或者DETR这类模型跑VOC格式怎么用答案是直接支持。MMDetection等主流检测框架对VOC格式有原生支持只需要把数据集组织成以下结构VOCdevkit/ ├── VOC2007/ │ ├── Annotations/ │ │ ├── pool_00001.xml │ │ └── ... │ ├── JPEGImages/ │ │ ├── pool_00001.jpg │ │ └── ... │ └── ImageSets/ │ └── Main/ │ ├── train.txt │ ├── val.txt │ └── test.txt其中Annotations放XML标注文件JPEGImages放图片ImageSets/Main下放三个TXT文件每个文件里每行写图片名不含扩展名用来划分训练集、验证集和测试集。在MMDetection的配置里通常这样注册数据集dataset_type VOCDataset data_root data/VOCdevkit/VOC2007/ train_dataloader dict( batch_size8, datasetdict( typedataset_type, data_rootdata_root, ann_fileImageSets/Main/train.txt, data_prefixdict(sub_data_root), pipelinetrain_pipeline, classes(swimmer, drowning), ))classes参数要按XML里name标签的顺序写保持和标注文件一致。MMDetection会自行解析VOC格式的XML文件和ImageSets中的划分文件。如果你的数据已经整理成这个结构基本不需要额外写代码。4.2 自己动手实现VOC转YOLO脚本虽然数据集已经提供了两种格式但你自己标注的数据可能只有一套标注文件这时候会需要转换脚本。这里给出一个经过验证的VOC转YOLO脚本按需调整路径直接用import os import xml.etree.ElementTree as ET CLASS_NAMES [swimmer, drowning] def convert_voc_to_yolo(xml_file, out_file): tree ET.parse(xml_file) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in CLASS_NAMES: continue cls_id CLASS_NAMES.index(name) box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) with open(out_file, w) as f: f.write(\n.join(lines)) # 批量转换示例 xml_dir Annotations txt_dir labels os.makedirs(txt_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue stem os.path.splitext(xml_file)[0] convert_voc_to_yolo( os.path.join(xml_dir, xml_file), os.path.join(txt_dir, stem .txt) )这个脚本有几个细节值得注意一是读取XML时用root.iter(object)而不是root.findall(object)能兼容某些XML里object嵌套的情况二是类别名没找到时会直接跳过而不是报错便于排查标注问题三是归一化坐标统一保留6位小数避免精度丢失导致框偏移。5. 常见问题与排查技巧备忘5.1 标注文件与图片对不上这是训练中最容易遇到的问题几乎每周都能在技术群里看到有人问。典型症状是训练日志里刷出来大量类似“WARNING: 1 image has no labels”的提示或者某个epoch的loss一直不下降。排查顺序我建议从三个方向走先看文件名。图片是pool_00001.jpg标签文件就必须是pool_00001.txt包括大小写和前后缀都不能有偏差。再看目录层级。YOLO要求images和labels平级很多用户把labels放进了images目录里导致标注文件根本找不到。最后看路径。YAML配置里的path是绝对路径时确认实际路径和配置文件一致。如果是自采数据建议转格式或者切分时先写一个自动核对脚本统计每张图片是否有对应标签、每个标签是否有对应图片。我习惯用Python的set做差集几十张图就能找出问题。5.2 类别不平衡与数据量不足溺水者实例只有580个远少于正常游泳者训练出来的模型天然偏向检测占比更大的类别。很多人在测试时发现溺水者漏检严重就是这个原因。最简单有效的方案是数据增强YOLO内置的增强策略已经很强包括HSV色域变换、随机翻转、缩放、马赛克增强等。但针对这类真实场景我还建议手动做几件事一是多收集夜间低光照样本这是真实监控场景最容易翻车的环节二是用上下翻转模拟不同摄像头安装高度泳池监控很多是俯拍略微变化视角能提升泛化性三是对小目标样本做过采样如果数据集中目标高度不足32像素的样本较少可以考虑重复参与训练。此外可以调整损失函数中的类别权重让模型对少数类更加敏感。YOLOv8里虽然不像老版YOLO那样直接配置cls weight但可以通过选择不同的损失配置或者在自定义训练脚本里修改。如果用的是其他框架直接给drowning类别更高的损失权重即可。5.3 模型漏检与误报的调优思路先处理漏检。漏检主要来自小目标和遮挡优先尝试把imgsz提到1280让模型在更高分辨率下看细节再检查后处理NMS阈值是不是太高conf_thres如果默认0.25对微小目标可以先降到0.1。不要觉得阈值低会带来大量误报在溺水检测这种场景里宁可多给后台造两个误报框也不能让漏检悄悄发生。再处理误报。误报通常来自水面反光、漂浮物、水花等干扰。这时候需要检查模型是不是只学到了“水面上有异物”而不是“人形的姿态”。可以收集一批误报图片做负样本单独建一个包含水面反光、救生圈、泳道线、漂浮树叶等干扰项的图片类别重新训练或者用负样本做微调。这样做之后模型对干扰项的判断会明显稳下来。5.4 数据标注口径不一致的坑如果一个数据集是多个人合作的标注口径很可能不统一。有人把头部刚入水的动作标成drowning有人认为只有整个人都没入水中才算这个差异会直接影响模型决策边界。我在整理这套数据的时候做了两件事第一写清楚标注规则可以作为文档直接看第二标注完成后随机抽了30张图用手动复核发现不一致的马上让标注者重新评定。这份工作比较花时间但比训练完再发现问题要省力得多。自己做数据集也一样宁可前期多花半天统一标准也不要等到模型训练完才发现标注错了。6. 几点使用心得与建议6.1 预训练权重怎么选用YOLO训练自己的数据集时强烈建议用预训练权重初始化而不是随机初始化。COCO预训练的yolov8s.pt已经学到了大量通用视觉特征比如边缘、纹理、形状等这些特征对任何检测任务都有用。从零开始训练895张图效果会非常差基本不可能收敛到可用的精度。如果显存紧张可以换yolov8n.ptnano版本参数量小很多推理也更快但精度会有所下降适合验证流程。如果算力和存储都比较宽裕上yolov8m.pt甚至yolov8l.pt也可以训练时间会更长但对小目标的检测效果会有提升。我这边实测下来s版本是这套数据性价比最高的选择。6.2 数据增强与部署落地经验训练阶段尽量开启YOLO默认的马赛克增强。马赛克增强会把四张图拼成一张相当于变相扩充了单张图片里的小目标样本对泳池这种大场景特别友好。但是要留意马赛克增强在最后10到20轮最好关掉否则模型一直看到拼接图部署时看到正常单张图会产生轻微性能下降。部署阶段如果要做实时推理常见的做法是把模型导出成TensorRT格式在GPU上运行延迟可以压到个位数毫秒。如果是边缘设备用OpenVINO或者ONNXRuntime做加速也能做到接近实时的效果。落地时别忘了做帧间隔控制不用每帧都推理每秒处理3到5帧足够覆盖溺水检测的响应要求。最后再分享一个小技巧。如果你要在真实泳池里长期部署不要指望一劳永逸。不同泳池的镜头高度、角度、水面反射情况差异很大建议在目标场地采集半小时到一小时视频从中抽帧补充标注做一次轻量级的微调。这套流程走通之后你在任何新场景都能快速复制数据集的边际价值只会越来越大。
返回列表