简介:这是面向目标检测开发者和游泳场所安全监控场景的数据集,主要用于溺水行为识别模型的训练与验证。包内共包含2000个文件,以874份xml标注文件与874份txt标签为主体,并配套251张jpg图像与1个yaml配置,压缩包大小约24.69MB。其中txt标签采用yolo格式,xml采用voc格式,分别存储于独立文件夹,便于直接接入yolov5、v7、v8、v9、v10及yolo11等常见框架。数据集已提前完成训练集、验证集划分,可节省自行整理标注的时间成本,适合刚接触yolo系列算法的学习者上手实践,也适合有溺水检测需求的项目开发者快速验证模型效果。截至目前已有302人浏览学习。
1. 这份溺水检测数据集到底解决了什么
做水域安全监控的工程师,大概率都卡在同一个地方:算法在公开数据集上跑得挺好,一换到自家泳池摄像头,误报多到值班员想砸屏幕。原因不复杂——溺水不是一个标准检测目标,它是“人体姿态异常+长时间静止+运动轨迹异常”的组合状态,单靠通用行人检测做不了。这份标题里的“yolo算法-游泳溺水检测数据集-874张图像带标签-溺水.zip”,就是冲着这个痛点来的:一张图里标好谁是正常游泳者、谁是溺水者,直接用 YOLO 系列算法训练检测头,让模型学着区分“正常游动的人”和“姿态不对的人”。
874张图像听起来不多,但对于溺水这种强姿态特征、弱语义差异的任务,正好够把模型从零拉起来再迁移到真实水域。它的直接价值不是“装完就能用”,而是给你一个对齐过标注口径的数据起点。适合谁用?适合已经跑过 YOLO 基础训练、手头有一批自己的水域视频、但不知道从哪开始做异常姿态检测的人。如果你连 YOLO 训练流程都没搭过,建议先用公开行人数据集把整套流程跑通,再回来碰这份数据。
2. 拆解溺水.zip:图像、标签和标注口径检查
拿到压缩包后,第一件事不是解压后立刻开训,而是把“标签到底怎么标、标了几个类、框有多准”查清楚。标注口径不明确,后面所有训练和调参都是空中楼阁。
2.1 先看目录结构:YOLO 格式的标配长什么样
常见的 YOLO 检测数据集压缩包,解压后至少会有这样两层结构:
swim_drowning_dataset/ ├── images/ │ ├── train/ │ │ ├── img_00001.jpg │ │ ├── img_00002.jpg │ │ └── ... │ └── val/ │ ├── img_00020.jpg │ └── ... └── labels/ ├── train/ │ ├── img_00001.txt │ ├── img_00002.txt │ └── ... └── val/ ├── img_00020.txt └── ...文件名里的溺水.zip并不能说明内部结构一定如此,有些打包者会把 train/val 混在一起,有些会用_darknet风格把所有标签放同一个目录。所以拿到包后第一步是跑一个统计脚本,先把家底盘清楚。我一般会写下面这段脚本,几秒钟就能看到这个包的标注全景:
#!/usr/bin/env bash # 统计 images 和 labels 下的文件数,检查图片和标注是否一一对应 echo "图片数量:"; find images -type f | wc -l echo "标签数量:"; find labels -type f | wc -l # 找出有图片没标签、或者有标签没图片的样本,这些是训练时的定时炸弹 for img in $(find images -type f -name "*.jpg"); do base=$(basename "$img" .jpg) if [ ! -f "labels/$base.txt" ]; then echo "缺标签: $img" fi done这段脚本的核心逻辑是:以图片文件名为基准,去labels目录找同名的.txt文件。YOLO 系列严格要求图片和标签同名同前缀,任何一张图缺标签,训练时都会被随机跳过,造成样本量缩水。如果跑完发现缺标签数量超过 20 个,建议先找打包者确认,不要直接进训练流程。
2.2 解析标签内容:一个 txt 里到底写了什么
YOLO 的标注文件是纯文本,每行对应一个目标框,格式固定为五列数字:
class_id x_center y_center width height其中坐标全部归一化到 0~1 区间,width和height是框的宽高占图像宽高的比例,不是像素值。要注意,这里x_center和y_center是框中心点的相对坐标,不是左上角坐标,新手最容易在这个地方换算错。用下面的 Python 脚本可以快速查看某一个标签文件的可读内容:
# 读取单个YOLO标签文件并解析,顺便打印类别分布 from collections import Counter import glob label_files = glob.glob("labels/train/*.txt") class_counter = Counter() total_boxes = 0 for label_path in label_files: with open(label_path, "r") as f: for line in f: parts = line.strip().split() if len(parts) != 5: print(f"格式异常: {label_path} -> {line.strip()}") continue class_id = int(parts[0]) class_counter[class_id] += 1 total_boxes += 1 print(f"共解析 {len(label_files)} 个标签文件,{total_boxes} 个目标框") print("类别分布:", dict(class_counter))这个脚本的价值有两个:一是发现格式异常行,比如坐标越界、列数不对、类别 id 超出预期范围;二是给出类别分布,让你确定数据集到底标了几个类别。如果class_counter里只出现 0 和 1 两个类别,通常意味着 0 是正常游泳者、1 是溺水者;如果只有 0 一个类别,那这个数据集的定位就变成了“只检测溺水者”,训练时负样本全靠背景图。
2.3 标注口径确认:溺水与正常游泳的边界在哪
打开几张图看一眼标注框,重点观察三类样本:正常游泳的人、挣扎中的人、已经静止漂浮的人。一个合格的数据集,溺水者的标注框应该框住整个人体,而不是只框头部或手臂;正常游泳者的框应该同样完整,不能出现一个类别的框大一个类别的框小,否则模型会倾向于用框的尺寸大小来区分类别,而不是用姿态特征。
我遇到过最典型的口径问题:打包者把所有“没有在游动的人”都标成了溺水,结果换到实际泳池里,站在池边休息的救生员也被误检成溺水。874 张图如果标注口径是“面部朝下漂浮”才算溺水,那训练出来的模型在“面部朝上仰漂”场景下召回会很难看。建议抽出 30 张图用 OpenCV 或任何图像查看工具人工过一遍,确认溺水样本里包含了多种姿态和多种拍摄角度,而不是同一个视频连续截帧。
3. 用 YOLOv8 训练溺水检测模型:从数据划分到参数调优
数据检查完毕后,训练环节最核心的决策有三步:划分数据集、选择模型规格、设置训练参数。874 张图属于小样本,这三点一旦选错,模型不是过拟合就是训练直接崩掉。
3.1 按“视频来源”划分数据,不是按随机比例
很多人在这一步直接train_test_split按 8:2 随机切分,这在溺水检测里是错误做法。溺水视频通常是一段连续录像切帧得到,相邻帧之间背景、人体姿态高度相似,随机划分会把同一段视频的帧同时塞进训练集和验证集,验证集的指标会虚高,到了真实场景直接原形毕露。
正确做法是把同一来源的帧全部放进同一个集合。如果压缩包内已经有 train/val 目录,先检查两个目录里是否有文件名前缀相同的帧;如果数据没分目录,按文件名里的序号段划分,例如 0001~0700 做训练、0701~0874 做验证,保证验证集完全没见过训练集的拍摄场景。这一步没有代码可抄,靠的是对数据来源的判断。
3.2 选择模型规格:别一上来就用 YOLOv8x
874 张图撑不起大模型。YOLOv8x 参数量接近 7000 万,在这种数据量下必然过拟合;反过来 YOLOv8n 只有约 300 万参数,在小样本上表现反而稳健。我的建议是首选 YOLOv8m 或 YOLOv8s。这里有一个基本逻辑可循:
| 模型规格 | 参数量级 | 对 874 张图的适配度 | 推荐场景 |
|---|---|---|---|
| YOLOv8n | 约 300 万 | 高,训练快,误检略多 | 快速跑通流程、边缘设备部署 |
| YOLOv8s | 约 1100 万 | 高,均衡 | 常规项目首选 |
| YOLOv8m | 约 2500 万 | 中,需配合数据增强 | 数据扩充后使用 |
| YOLOv8l/x | 5000 万以上 | 低,极易过拟合 | 数据量到 5000 张以上再考虑 |
考虑到溺水检测要部署在泳池、河道等固定机位场景,我一般会先拿 YOLOv8s 跑一版,看验证集 mAP 是否能过 0.5;如果验证集 mAP 都能到 0.7 以上,再尝试换 YOLOv8m 看是否有提升。
3.3 最小可复现训练命令与参数说明
使用 Ultralytics YOLOv8 训练前,先写一个数据描述 YAML 文件。注意类别名称要和你统计出来的类别 id 顺序严格对应,顺序错一位,整个训练就白跑:
# drowning_dataset.yaml # 注意: class id 必须与标签文件里的数字一一对应 path: ./swim_drowning_dataset train: images/train val: images/val names: 0: swimmer 1: drowning训练命令行如下。这里不用急着追最新版本号,ultralytics库的 API 一直保持兼容,v8 系列的命令格式没有大的破坏性变更:
# 安装 ultralytics 库之后直接开训 yolo detect train \ data=drowning_dataset.yaml \ model=yolov8s.pt \ epochs=200 \ imgsz=640 \ batch=16 \ patience=30 \ device=0 \ project=runs/drowning \ name=exp_with_aug参数含义逐个说清楚:imgsz=640是输入分辨率,874 张图的分辨率如果本身就是 1080p 以上,训练时缩放到 640 能显著加速;如果原始图里溺水者框非常小,低于 32×32 像素,建议用imgsz=1024重新训练一版。batch=16在 874 张图的情况下够用,显存不够就降到 8。patience=30表示验证集指标连续 30 个 epoch 不提升就早停,这是防过拟合的关键机制。epochs=200看起来多,但有早停机制在,实际跑到 70~90 轮就会停。
3.4 预训练权重怎么选:用还是不用
热词里“yolo预训练模型下载”经常被搜到,说明很多人想直接加载 COCO 预训练权重。我的做法是:首次训练用yolov8s.pt作为起点,原因有三。第一,COCO 里有“人”这个类别,预训练模型已经学会了人体特征,迁移到游泳场景只需要微调高层语义;第二,如果不加载预训练权重,874 张图很难在有限轮数内收敛到可用状态;第三,COCO 预训练模型转换到二分类任务时,只需要替换检测头,YOLOv8 的模型结构会自动处理类别数变化。
# 如果想对比: 不加载预训练权重, 从零开始训练 yolo detect train \ data=drowning_dataset.yaml \ model=yolov8s.yaml \ epochs=200 \ imgsz=640 \ batch=16 \ device=0差别只在model=参数是传.pt还是.yaml。.pt是加载预训练权重继续训练,.yaml是从网络结构随机初始化开始训练。小样本项目基本只跑第一种,从零训练只在验证“预训练权重是否有效”时作为对比实验出现。
4. 游泳溺水检测的避坑指南:5 个必须提前知道的坑
小样本 + 强姿态特征 + 环境千变万化,这个任务的水远比想象中深。以下五条踩坑记录全部来自实际训练中的血泪经验,每一条都按“现象 → 原因 → 解决”来写。
4.1 白天误报暴增:阳光直射水面时,水面反光被识别成人影
现象:晴天下午的泳池画面上,模型把水面波纹和池底瓷砖反光识别成溺水者,误报率从早上的 5% 飙升到 40%。
原因:YOLO 学的是纹理特征,而不是语义理解。水面波纹在阳光照射下会形成强边缘和高亮区域,这些纹理模式和标签中“穿着浅色泳衣的人”高度相似。训练数据里如果缺少强反光环境下的负样本,模型就会把这种纹理误判为人体。
解决:把晴天、阴天、室内灯光、室外自然光四种光照场景分开统计,确认训练集里包含了至少两种光照下的溺水正样本。同时,在预处理阶段增加“只检测画面下 2/3 区域”的约束,一般泳池摄像头画面中上半部分是天空或看台,不会出现溺水者,直接裁剪掉可以减少大量误报。
4.2 淹没后漏检:溺水者已经下沉,模型再也找不到目标
现象:测试视频中,溺水者从开始呛水到完全沉入水下用时约 10 秒,模型在前 5 秒能框住人,一旦人彻底没入水中,检测框消失,后续再也没能恢复。
原因:数据集里的溺水样本大多是“水面漂浮姿态”,比如仰面朝天或俯卧漂浮。但真实溺水过程是“挣扎 → 呛水 → 下沉”,下沉后人体被水折射和波纹遮挡,纹理特征完全改变,模型从未见过这种形态。
解决:调整标注策略,将“只有头部露出水面”的状态也纳入溺水类别;如果数据集中没有这类样本,从网上找公开的溺水救援视频截帧补标。还有一个工程师常用的办法:跟踪上一帧的检测框位置,如果当前帧该区域没有任何检测框,就用光流法或者背景差分判断该区域是否有运动异常,把下游判定移交给跟踪模块而不是单帧检测器。
4.3 类别混淆集中爆发:正常游泳者和溺水者都被标成同一个类别
现象:验证集混淆矩阵显示,normal swimming 类别的样本有 30% 被预测成 drowning,模型严重偏向于报警。
原因:标注数据里两类目标的姿态区分度不够。“狗刨式”游泳动作和“挣扎”动作在外观上非常相似,尤其是镜头距离远、分辨率低的时候,人眼都难分辨,模型只能学到游泳者的共性特征,导致swimmer的置信度永远低于drowning。
解决:降低误报优先级,提升阈值。把conf=0.25(默认值)调到conf=0.5,同时启用agnostic_nms=True,避免两个类别的框互相抑制。最有效的做法是给swimmer类别加权重,让模型在不确定时偏向输出正常游泳类别,把误报控制住,漏报量交给人去看。
4.4 模型训练 loss 不降反升,验证集 mAP 归零
现象:训练到第 20 个 epoch 时,box_loss 和 cls_loss 突然跳动,验证集 mAP 从 0.6 掉到 0,之后再也无法恢复。
原因:学习率设置过高,加上 label smoothing 开启后,小样本数据集对梯度波动更敏感。Ultralytics 默认使用lr0=0.01(初始学习率),对 874 张图来说这个值偏大,在训练后期容易跳过最优解。
解决:训练命令中加上lr0=0.005,把初始学习率降为默认值的一半;同时增大warmup_epochs到 10,让模型在前 10 个 epoch 用较低学习率稳定预训练权重的特征提取层。这一条能解决小样本训练 80% 的损失函数异常问题。
4.5 验证集 mAP 虚高,部署后完全不能用
现象:训练过程中 mAP50 到了 0.85,模型看起来完美。结果部署到现场摄像头后,10 分钟内在空无一人的泳池上连续误报 6 次。
原因:验证集划分是随机的,同一段视频的帧被同时分进训练集和验证集,模型相当于开卷考试。这种情况在数据量小的项目里极其常见,几乎每个拿到小数据集的人都会踩上一次。
解决:强制按视频来源划分数据。如果数据来自多个视频,保证同一个视频的所有帧只出现在训练集或验证集其中一个;如果一张图里有多个溺水者,图像级别的划分必须包含所有标注框。做完这一步之后 mAP 会明显下降,那才是模型真实水平的反映。
5. 把 874 张图“变多”:三类可靠的数据扩充手段
874 张图做一个检测任务,模型能收敛,但泛化能力有限。要提升真实场景表现,可以围绕这个数据集做扩充,三条路按性价比排序。
5.1 在线数据增强:Ultralytics 自带参数,不额外写代码
最常见的 YOLO 训练流程里,数据增强是训练时实时做的,Ultralytics 默认开启 mosaic(四张图拼接)、随机仿射变换、HSV 扰动等增强策略。对于溺水检测,这些增强并不全都适用:mosaic 能显著提升小目标的检测能力,但也会让水面纹理变得不真实,建议保留但降低强度;HSV 扰动不要开太大,泳池水的颜色稳定性是这个任务的重要线索。
在训练命令里用hsv_h=0.015(色调扰动幅度)、degrees=10(旋转角度)、scale=0.3(缩放范围)控制增强强度。数值越大增强越激进,但超过一定限度会让模型学到错误的水域颜色特征:
yolo detect train \ data=drowning_dataset.yaml \ model=yolov8s.pt \ epochs=200 \ imgsz=640 \ batch=16 \ patience=30 \ mosaic=0.8 \ hsv_h=0.015 \ hsv_s=0.3 \ hsv_v=0.2 \ degrees=10 \ scale=0.35.2 给自己的视频加标签:最务实的扩充路径
874 张图解决的是“从无到有”的问题,真实项目还是要用自己的场景视频补充数据。常见做法是:用你训练得到的第一个模型(哪怕只有 0.4 mAP)去跑一段新拍摄的泳池视频,把模型高置信度输出的帧保存下来,人工复核后直接作为训练样本追加进去。
# 导出高置信度检测帧, 供人工复核后再进训练集 from ultralytics import YOLO import cv2 model = YOLO("runs/drowning/exp_with_aug/weights/best.pt") cap = cv2.VideoCapture("pool_camera_01.mp4") frame_id = 0 saved = 0 while True: ret, frame = cap.read() if not ret: break frame_id += 1 if frame_id % 30 != 0: # 每秒取两帧, 避免连续帧冗余 continue results = model(frame, conf=0.5) if len(results[0].boxes) > 0: cv2.imwrite(f"candidate_frames/frame_{frame_id:06d}.jpg", frame) saved += 1 # 同时记录模型输出类别和坐标, 方便人工复核时比对 with open(f"candidate_frames/frame_{frame_id:06d}.txt", "w") as f: for box in results[0].boxes: cls_id = int(box.cls[0]) xywh = box.xywh[0].tolist() f.write(f"{cls_id} {xywh[0]/frame.shape[1]} {xywh[1]/frame.shape[0]} {xywh[2]/frame.shape[1]} {xywh[3]/frame.shape[0]}\n") cap.release() print(f"导出 {saved} 帧, 请人工复核标签后再加入训练集")这段代码的核心思路是“用模型找候选,用人做确认”。模型负责把可能有问题的帧挑出来,人工只需要检查这些候选帧,把误标和漏标修正掉,再追加进原数据集重训。每跑一轮,数据量就增加一次,模型在真实场景下的表现也会同步提高。注意这里输出的是归一化坐标,因为要直接追加到原数据集的 labels 目录,格式必须保持一致。
5.3 加负样本:贴一张“无人泳池”标签进去
溺水检测的误报一大半来自“空无一人的水域被识别成人”。解决办法是主动往训练数据里添加没有人的负样本,也就是全背景帧。
做法很简单:从公开的泳池视频或自己拍摄的空镜中截取 200~300 帧,放到一个新的images/background目录,对应的标签文件写一个空文本。也就是说,有图无标签即可。Ultralytics 训练时会把这张图当作没有目标的负样本,让模型学会“什么都没看到时不要输出任何框”。
# 空标签文件的内容: 没有任何行 # 对应图片: background_001.jpg (同样一张图, 标签文件就一个空txt)5.4 数据扩充后的重新训练策略
扩充完数据后不要直接从头训练,而是基于上一版权重继续训练。小样本数据集的每一轮训练,都是在上一轮学习过的特征基础上增加新知识。我一般会保持model=runs/drowning/exp_with_aug/weights/best.pt,把epochs降到 100,因为新数据量不大,模型不需要太多轮数就能收敛,轮数过多反而容易遗忘原有的水域特征。
6. 一个必须养成的习惯:训练完先跑视频再谈效果
训练指标再漂亮,不如直接拿一段真实视频看模型表现。我自己的标准流程是:训练完成后,至少用两段视频做验证——一段包含正常游泳的人,一段包含溺水模拟(让会游泳的人配合演示挣扎动作),分别统计误报率和漏检率。
视频验证的指标与图像验证完全不同。图像只看 mAP,视频要看时间连续性:一个真实的溺水过程持续几十秒,单帧检测结果必须在时间轴上保持一致,不能出现“这一秒有检测框、下一秒消失、再下一秒又出现”的情况。我用下面这段脚本统计检测框的帧间稳定性:
# 统计检测框在视频连续帧中的稳定性 # 连续N帧都检测到且框位置变化小于阈值, 才认为是可靠报警 from ultralytics import YOLO import cv2 import sys model = YOLO("runs/drowning/exp_with_aug/weights/best.pt") cap = cv2.VideoCapture("drowning_simulate.mp4") prev_box = None stable_frames = 0 alarm_frames = [] while True: ret, frame = cap.read() if not ret: break results = model(frame, conf=0.5) boxes = results[0].boxes if len(boxes) == 0: stable_frames = 0 prev_box = None continue # 取置信度最高的框做稳定性判断 best_box = boxes.xyxy[0].cpu().numpy() if prev_box is not None: # 计算中心点偏移量, 小于50像素视为同一目标持续存在 cx_diff = abs((best_box[0]+best_box[2])/2 - (prev_box[0]+prev_box[2])/2) cy_diff = abs((best_box[1]+best_box[3])/2 - (prev_box[1]+prev_box[3])/2) if cx_diff < 50 and cy_diff < 50: stable_frames += 1 else: stable_frames = 1 else: stable_frames = 1 prev_box = best_box # 连续15帧(约0.5秒)检测到同一目标, 触发一次报警 if stable_frames >= 15: alarm_frames.append(int(cap.get(cv2.CAP_PROP_POS_FRAMES))) print(f"触发警报的帧序号: {alarm_frames}") print(f"共触发 {len(alarm_frames)} 次警报")这个脚本给你的不是一个 mAP 数字,而是一个可验收的标准:如果这个溺水视频连续几十秒的溺水过程只触发了 1~2 次警报,说明检测稳定性合格;如果警报断断续续触发十几次,说明模型在场景中的置信度波动太大,部署后值班员会直接关掉警报功能。
这个习惯我一直坚持:每次训练完模型,不管训练集 mAP 多高,都必须跑一段“自己拍的真实视频”看效果。874 张图训练出来的模型,本身就是一个需要不断迭代的半成品,视频验证能告诉你模型在该场景下的真实短板在哪,是补数据还是调阈值,一目了然。希望这个流程能帮你少走一些弯路,把更多时间花在真正有效的迭代上。
本文还有配套的精品资源,点击获取