简介:这是一份面向目标检测学习者的游泳与溺水图像数据集,适合从事水域安全监控、智能救援等场景的算法训练与实验。数据已统一处理为YOLO格式,可直接用于YOLO全系列网络训练,标注采用classes、x_centre、y_centre、w、h的相对坐标形式,类别涵盖游泳、溺水等三类,具体可参考class类别文本文件。资源包共2000个文件,以1999个txt标注文件和1个show.py可视化脚本为主,压缩包约300.66MB,按YOLOv5目录结构组织,包含训练集约5000张、验证集约1400张、测试集约700张图片及对应标签。其中show.py可将预测框绘制在图像上,便于快速核验标注质量与检测效果。目前已有1096人学习下载,适合需要现成水域检测数据、希望省去标注与格式转换环节的开发者与研究者直接上手训练与评估。
1. 游泳溺水检测数据集:7000 张图背后的真实需求与选型判断
做水域安全监控的团队,几乎都绕不开一个尴尬现实:公开的 COCO、VOC 里没有「溺水」这个类,通用目标检测模型扔到泳池边,能把游泳的人框出来,却分不清「正常换气」和「挣扎下沉」。我最早接这类需求时,客户给了一段泳池监控,问能不能自动报警,我拿现成权重跑了一遍,结果浪花、泳帽、反光全被当成目标,误报率高到没法用。问题不在模型,在数据——没有针对性的游泳、溺水图像检测数据集,再强的 backbone 也是空转。
这个标题讲的,就是一套超过 7000 张图片并带标注的水域场景目标检测数据集,核心覆盖「游泳者」和「溺水/异常姿态」两类目标。它解决的是从零采集、从零标注的冷启动问题,适合做智慧泳池、水库防溺、海滨浴场监控的算法工程师,也适合想切入水域安防方向、手里没有标注资源的学生和独立开发者。7000 张这个量级,单类目标检测够用,但怎么用、怎么切分、怎么防止过拟合,才是决定它值不值得投入的关键。下面按「先看清数据长什么样 → 再跑通训练 → 再避开标注和场景的坑」这条线讲透。
2. 拆解 7000 张游泳溺水图像:类别定义、标注格式与场景分布
拿到一个目标检测数据集,第一件事不是急着train.py,而是把标注文件翻出来看类别分布和框的形态。游泳溺水这个场景有个特殊性:溺水目标往往不是完整人体,可能是半沉的头、乱挥的手臂、水面异常波纹,标注边界比常规行人检测模糊得多。这一章先把数据本身讲清楚,再给出可复现的解析脚本。
2.1 类别体系怎么定:游泳者与溺水者的边界
常见做法是把类别压到 2 到 3 类,别一上来搞七八类。我一般会定成swimmer(正常游泳)、drowning(溺水/挣扎)、可选person(岸上人员,用于排除干扰)。为什么不做「自由泳/蛙泳/仰泳」这种细分?因为溺水检测的落地目标是报警,不是泳姿识别,类别越细,标注一致性越差,模型越难收敛。
判断「溺水」的标注准则要在数据集说明里写死,否则不同标注员标准不一,模型学到的就是噪声。我用的准则是三条满足其一:头部持续低于水面超过阈值时间、身体呈垂直下沉姿态、双臂无规律拍打且位置异常。这三条落到标注上,就是框住可见的异常人体部分,而不是硬凑一个完整人体框。
提示:如果数据集只给了
swimmer和drowning两类,岸上人员、泳圈、浮标这些干扰物没有单独标注,训练时要么加背景负样本,要么在推理阶段用 ROI 裁剪把岸上区域排除,否则误报会明显上升。
2.2 标注格式解析:从 XML/JSON 到 YOLO txt 的转换
公开数据集常见三种标注:Pascal VOC 的 XML、COCO 的 JSON、YOLO 的 txt。游泳溺水数据集多数以 VOC 或 YOLO 格式分发。先写个脚本统计类别和框尺寸分布,这一步能提前暴露很多问题。
import os import xml.etree.ElementTree as ET from collections import Counter # 统计 VOC 格式标注的类别分布与框宽高 def parse_voc(anno_dir): cls_counter = Counter() box_sizes = [] for f in os.listdir(anno_dir): if not f.endswith('.xml'): continue tree = ET.parse(os.path.join(anno_dir, f)) root = tree.getroot() for obj in root.findall('object'): name = obj.find('name').text cls_counter[name] += 1 bbox = obj.find('bndbox') w = float(bbox.find('xmax').text) - float(bbox.find('xmin').text) h = float(bbox.find('ymax').text) - float(bbox.find('ymin').text) box_sizes.append((w, h)) return cls_counter, box_sizes cls_counter, box_sizes = parse_voc('./annotations') print('类别分布:', cls_counter) # 小目标占比:宽或高小于 32 像素的框 small = sum(1 for w, h in box_sizes if w < 32 or h < 32) print('小目标占比: %.2f%%' % (100 * small / len(box_sizes)))这段脚本的逻辑很直接:遍历 XML,累计每个类别的实例数,同时记录每个框的宽高。参数上,anno_dir指向标注目录,32这个阈值对应 COCO 对小目标的定义。跑完你会得到两个关键数字——类别是否均衡、小目标占比多少。溺水目标因为常是半沉的头或手臂,小目标占比往往超过 30%,这直接决定后面 anchor 和输入分辨率怎么设。
2.3 场景分布与划分:别让训练集和验证集「串味」
7000 张图如果全来自同一个泳池、同一时段,模型换个场地就崩。划分数据集时,我坚持按「场景 + 时段」分组切分,而不是随机切。具体做法:先给每张图打上scene_id(泳池 A、泳池 B、水库 C)和time_slot(白天/夜晚),然后按 scene_id 做 GroupShuffleSplit,保证同一场景的图不会同时出现在训练和验证里。
import pandas as pd from sklearn.model_selection import GroupShuffleSplit # meta.csv 至少包含 image_id, scene_id 两列 df = pd.read_csv('meta.csv') gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(gss.split(df, groups=df['scene_id'])) train_df = df.iloc[train_idx] val_df = df.iloc[val_idx] print('训练集场景数:', train_df['scene_id'].nunique()) print('验证集场景数:', val_df['scene_id'].nunique())groups参数是关键,它告诉划分器按场景分组,test_size=0.2表示留出 20% 场景做验证。这样验证集上的指标才接近真实换场地的表现。如果数据集本身场景单一,那就要在训练时用强数据增强(随机裁剪、色彩抖动、模拟水面反光)来补,否则 7000 张也救不了泛化。
3. 用 YOLOv8 跑通游泳溺水检测:环境、配置与训练命令
数据看清了,接下来落到能跑通的训练流程。这一章以 YOLOv8 为主线,因为它在小目标和水面反光场景下表现稳定,社区资料也多。整个流程分环境准备、数据配置、训练与推理三步,每步都给可抄的命令和参数解释。
3.1 环境准备与数据目录组织
先建虚拟环境,装 ultralytics。版本上我一般锁一个近期稳定版,避免 API 变动导致脚本报错。
conda create -n drowning python=3.10 -y conda activate drowning pip install ultralytics==8.2.0 opencv-python pandas scikit-learn数据目录按 YOLO 规范组织,images和labels一一对应,txt 里每行是class x_center y_center width height,坐标都归一化到 0 到 1。
dataset/ images/ train/ val/ labels/ train/ val/ data.yamldata.yaml写清路径和类别名:
path: ./dataset train: images/train val: images/val nc: 2 names: ['swimmer', 'drowning']nc是类别数,names顺序必须和 txt 里的 class 索引一致,错一位模型就全学反。VOC 转 YOLO 的脚本网上很多,核心就是读 XML 的框,除以图像宽高做归一化,这里不重复贴,重点提醒:转换后一定要抽查几张,用可视化脚本把框画回图上确认没偏移。
3.2 训练参数怎么设:小目标与类别不均衡的处理
直接上训练命令,再逐参数解释。
yolo detect train \ data=dataset/data.yaml \ model=yolov8s.pt \ epochs=120 \ imgsz=960 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ mosaic=1.0 \ mixup=0.1 \ copy_paste=0.1 \ cls=0.7 \ patience=30 \ device=0imgsz=960比默认 640 大,是为了救小目标——溺水目标常只占几十像素,分辨率太低直接消失。代价是显存和训练时间上升,batch要相应降到 16 甚至 8。mosaic=1.0和copy_paste=0.1是应对样本不足和类别不均衡的增强,copy_paste能把溺水目标复制粘贴到不同水面背景,对稀有类很有效。cls=0.7提高分类损失权重,缓解swimmer远多于drowning时的类别偏置。patience=30是早停,验证指标 30 轮不升就停,省时间。
注意:如果显存不够,优先降
batch,别轻易降imgsz。小目标场景下分辨率是命根子,降到 640 往往 mAP 掉一大截,血泪经验。
3.3 推理与报警阈值:从检测框到「疑似溺水」事件
训练完拿best.pt推理,但检测框不等于报警。真实系统里要加时序逻辑:同一区域连续 N 帧出现drowning且置信度超过阈值,才触发报警,避免单帧误报。
from ultralytics import YOLO import cv2 model = YOLO('runs/detect/train/weights/best.pt') cap = cv2.VideoCapture('pool_test.mp4') hit = 0 THRESH, CONSEC = 0.5, 8 # 置信度阈值与连续帧数 while cap.isOpened(): ret, frame = cap.read() if not ret: break res = model(frame, imgsz=960, conf=THRESH, verbose=False)[0] drowning = [b for b in res.boxes if model.names[int(b.cls)] == 'drowning'] hit = hit + 1 if drowning else 0 if hit >= CONSEC: print('疑似溺水报警') hit = 0 cv2.imshow('frame', res.plot()) if cv2.waitKey(1) & 0xFF == 27: breakconf=THRESH控制单帧灵敏度,CONSEC=8控制时序确认长度。这两个参数要按帧率调:25fps 下 8 帧约 0.3 秒,既能压误报又不至于延迟太久。调参时先固定CONSEC,扫conf从 0.3 到 0.7 看误报和漏报的平衡点,别拍脑袋定。
4. 游泳溺水数据集训练的避坑与排查清单
这个方向我踩过的坑比一般检测任务多,因为水面反光、遮挡、标注主观性三座大山叠在一起。下面 5 条按「现象 → 原因 → 解决」写,都是能直接对照排查的。
现象一:训练 loss 正常下降,但验证集 mAP 卡在 0.3 上不去。原因多半是训练集和验证集场景重叠,或者标注框系统性偏移。解决:先用 2.3 的 GroupShuffleSplit 重划,再抽查 20 张验证图的标注可视化,确认框贴合目标。如果框整体偏,检查 VOC 转 YOLO 时有没有把xmin/ymin和xmax/ymax搞反。
现象二:模型把水面反光、白色浪花大量误检为 drowning。原因是负样本不足,模型没见过「像溺水但不是」的干扰。解决:收集一批纯水面、反光、泳圈、浮标的图作为背景负样本加入训练,或在data.yaml里加一个background类专门吸收这些干扰。我一般会补 500 到 1000 张负样本,误报能降一半以上。
现象三:溺水小目标漏检严重,尤其远处的人头。原因是输入分辨率不够或 anchor 不匹配。解决:把imgsz提到 960 甚至 1280,同时确认标注里小框没有被过滤掉。有些转换脚本会丢弃宽高小于某阈值的框,翻车就翻在这,务必检查转换日志。
现象四:不同泳池表现差异巨大,换场地就崩。原因是场景过拟合,模型学到了背景纹理而非目标特征。解决:训练时开强色彩抖动、随机灰度、模拟水面波纹的增强;如果条件允许,做多场景混合训练,哪怕每个场景只有几百张,也比单场景 7000 张泛化好。
现象五:报警系统误报集中在傍晚逆光时段。原因是逆光下人体变成剪影,和溺水姿态视觉相似。解决:在 ROI 层面按时段切换阈值,逆光时段提高conf到 0.6 以上,或引入红外/热成像作为辅助模态。纯可见光在这个时段本身就不可靠,别硬扛。
5. 从 7000 张到落地:半自动标注与主动学习的省力技巧
数据集用起来之后,真正拉开效率差距的不是模型结构,而是标注迭代方式。7000 张是起点,不是终点——你的目标场景一定有它没覆盖的角落。我的习惯是:先用这套数据训一个基线模型,部署到目标场景跑推理,把低置信度和误报的帧挑出来,人工只修这些「难例」,再回流训练。这就是主动学习的闭环,能把标注成本压到原来的三分之一。
具体操作上,写个脚本按置信度分桶导出难例:
import os from ultralytics import YOLO model = YOLO('best.pt') hard_dir = './hard_examples' os.makedirs(hard_dir, exist_ok=True) for img in os.listdir('./unlabeled'): res = model(f'./unlabeled/{img}', imgsz=960, verbose=False)[0] confs = [float(b.conf) for b in res.boxes] # 有目标但置信度落在 0.2~0.5 之间,视为难例 if confs and min(confs) < 0.5 and max(confs) > 0.2: res.save(filename=f'{hard_dir}/{img}')这段逻辑是:置信度太低说明模型拿不准,太高说明已经学会,中间地带才是标注收益最高的样本。0.2和0.5两个阈值按你的场景调,误报多的场景把下限定高些,漏报多的把上限定低些。导出的难例人工修完,和原数据合并再训一轮,通常两三轮就能把目标场景的 mAP 拉起来。
验证方法上,别只看 mAP。溺水检测是安全场景,漏报的代价远大于误报,所以我会单独统计drowning类的召回率,并画一条「召回率 vs 报警频率」曲线,让业务方自己选工作点。表格里给一组我常用的评估维度:
| 指标 | 含义 | 关注点 |
|---|---|---|
| drowning 召回率 | 真实溺水被检出的比例 | 安全底线,优先保 |
| 误报次数/小时 | 每小时误报警次数 | 影响可用性 |
| 小目标 mAP | 小于 32 像素目标的精度 | 远距离场景关键 |
| 跨场景 mAP 方差 | 不同场地指标波动 | 泛化能力 |
最后说个我自己的习惯:每接手一个新水域场景,先不急着训模型,而是拿这套 7000 张数据训的基线跑一遍现场视频,把误报和漏报各截 50 张存下来,作为这个场景的「体检报告」。这份报告比任何指标都直观,也决定了后面是补数据、调阈值还是换模态。数据集是死的,场景是活的,把两者对齐的功夫,才是这类项目真正值钱的地方。希望帮到你。
本文还有配套的精品资源,点击获取