做溺水检测这个方向,说难不难,说简单也真不简单。难点不在模型——现在的目标检测框架一个比一个成熟,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 | 英文标签 | 中文含义 | 标注实例数量(约) |
|---|---|---|---|
| 0 | swimmer | 正常游泳者 | 1260 |
| 1 | drowning | 溺水者 | 580 |
数字能说明不少问题:溺水者实例明显少于正常游泳者,这是真实场景的客观规律,但也意味着类别不平衡是训练时要重点处理的。图像分辨率以1920x1080为主,也有少量1280x720和手机拍摄的近景样本,这样的分辨率结构能训练模型适应不同质量的输入源。
每张图片的标注框都经过人工校准,我对明显不准确的框做了二次调整。不过我必须强调,标注数据集是件主观性很强的工作,特别是“溺水”这个状态,不同标注者可能理解不同。在这个数据集里,“溺水”定义为头部大部分没入水中、身体姿态挣扎或失去自主行动能力、有明显呛水特征的目标;而正常游泳者指头部露出水面、身体姿态正常游动的目标。这类定义必须写在文档里,不然回头发现标注口径不统一,哭都来不及。
2.2 VOC与YOLO两种格式的核心差异
我同时提供了VOC和YOLO两种标注格式,目的是让使用不同训练框架的人都可以直接上手,不需要折腾格式转换。
VOC格式实际是Pascal VOC项目定义的标注规范,每个图片对应一个XML文件,里面记录了图片尺寸、路径、目标类别以及边界框坐标。坐标是绝对值,单位是像素。
<annotation> <folder>images</folder> <filename>pool_00123.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>drowning</name> <bndbox> <xmin>512</xmin> <ymin>780</ymin> <xmax>786</xmax> <ymax>971</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格式 |
|---|---|---|
| 文件格式 | XML | TXT |
| 坐标类型 | 绝对像素坐标 | 归一化相对坐标 |
| 边界框表示 | xmin, ymin, xmax, ymax | x_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 \ data=swim_drowning.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=640 \ batch=16 \ patience=20 \ project=swim_detect \ name=exp1几个参数我解释一下为什么这么设:
epochs=150:895张图片的训练集规模不算大,150轮足够模型收敛。如果过早过拟合,patience=20会在连续20轮验证集指标没有提升时自动停止训练。
imgsz=640:这是速度和精度的平衡点。如果监控画面里人像很小,可以考虑imgsz=1280,但会明显增加显存占用和推理耗时。我实测在多数泳池场景下640经足够,画面是1080p的可以用1280试试。
batch=16:根据显卡显存调整。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 model=runs/detect/train/weights/best.pt source=test_video.mp4我测了一段我自己的泳池测试视频后发现,白天光线充足时,正常游泳者和溺水者基本都能稳定框出,只有一个人在画面里特别小、加上大量水花遮挡时,模型偶尔会漏检。这个问题的根本原因是训练集中小目标样本不够多,后面做数据增强时可以考虑针对性补强。
推理阶段还有一个实用技巧:把检测结果输出成标注视频文件,用save=True参数,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_size=8, dataset=dict( type=dataset_type, data_root=data_root, ann_file='ImageSets/Main/train.txt', data_prefix=dict(sub_data_root=''), pipeline=train_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_ok=True) 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.pt(nano版本),参数量小很多,推理也更快,但精度会有所下降,适合验证流程。如果算力和存储都比较宽裕,上yolov8m.pt甚至yolov8l.pt也可以,训练时间会更长,但对小目标的检测效果会有提升。我这边实测下来,s版本是这套数据性价比最高的选择。
6.2 数据增强与部署落地经验
训练阶段尽量开启YOLO默认的马赛克增强。马赛克增强会把四张图拼成一张,相当于变相扩充了单张图片里的小目标样本,对泳池这种大场景特别友好。但是要留意,马赛克增强在最后10到20轮最好关掉,否则模型一直看到拼接图,部署时看到正常单张图会产生轻微性能下降。
部署阶段如果要做实时推理,常见的做法是把模型导出成TensorRT格式,在GPU上运行,延迟可以压到个位数毫秒。如果是边缘设备,用OpenVINO或者ONNXRuntime做加速,也能做到接近实时的效果。落地时别忘了做帧间隔控制,不用每帧都推理,每秒处理3到5帧足够覆盖溺水检测的响应要求。
最后再分享一个小技巧。如果你要在真实泳池里长期部署,不要指望一劳永逸。不同泳池的镜头高度、角度、水面反射情况差异很大,建议在目标场地采集半小时到一小时视频,从中抽帧补充标注,做一次轻量级的微调。这套流程走通之后,你在任何新场景都能快速复制,数据集的边际价值只会越来越大。