简介游泳者溺水检测数据集以VOC与YOLO双格式提供标注面向目标检测算法研究者和涉水安全视觉应用开发者可直接用于训练溺水、游泳等行为识别模型。压缩包共2000个文件主体为1999个xml标注文件另附1个使用说明txt整体大小257.76MB标注覆盖Drowning、Drowning-headdown、Person out of water、Swimming四个类别总计14699个标注框。该资源已有1220人学习下载。需要提醒的是受真实溺水图片获取难度影响超过80%的图片为数据增强生成分辨率普遍不高适合用来做数据扩充、算法预研与消融对比若用于高精度落地应结合真实场景数据进行补充筛选。1. 游泳者溺水检测数据集8275张VOCYOLO双格式图先想清楚怎么用做泳池或公开水域的智能监控最头疼的不是模型选型而是标注数据。溺水检测不同于通用人体检测溺水者往往半沉半浮、肢体动作异常、长时间不移动常规检测模型很难直接迁移。一个包含8275张图、4类目标、同时带VOC和YOLO两种标注格式的游泳者溺水检测数据集解决的就是这个起步问题。它既能直接喂给YOLOv5/v8训练也能用Pascal VOC系框架做迁移学习不需要你再花几周去手工标注。适合正在做泳池安全监控、水域巡检、或者研究人体姿态异常检测的团队也适合刚入门目标检测、想用一个垂直场景数据集跑通全流程的开发者。但拿到手别急着解压训练先看清楚4类目标怎么划分、VOC和YOLO格式怎么转换、8275张图里训练验证怎么分配这三件事决定你能在3天内跑出可用的模型还是浪费一周在数据格式上折腾。2. 数据集内部结构VOC和YOLO两套标注的关系与边界问题2.1 四类目标怎么划分类别定义决定模型能力边界拿到数据集后第一步不是看图片而是找类别定义文件。这个数据集标注了4个类别通常可以用data.yaml或者classes.txt看到具体类别名。按溺水检测场景的常见划分这4类往往是正常游泳者swimmer、溺水者drowning、挣扎者struggling、以及空场景或岸边人员background/person。具体以数据集自带的class定义为准但你要理解这4类划分的边界在哪里——正常游泳者是四肢有规律划水、头部多数时间在水面以上溺水者是头部下沉或只有头发露出水面、四肢动作幅度大且无规律挣扎者是介于两者之间、有明显的拍水动作。三类之间是连续状态标注本身就存在灰度地带。这个边界问题直接决定模型能力。比如一个数据集只标了“游泳者”和“溺水者”两类模型学到的就是二分类判别对岸上跳水动作、水中站立休息这些中间状态会误判如果有4类训练出来的模型能给出更细粒度的置信度分布对报警策略更友好。我一般会建议先统计每个类别的样本数如果溺水者这类正样本明显偏少就要做好后续做数据增强或类别权重调整的准备。你可以在解压后运行一行命令统计find . -name *.txt -path *yolo* | xargs cat | cut -d -f1 | sort | uniq -c这个命令把YOLO格式的标注文件里所有类别ID统计出来看每一类的目标数量分布。如果发现某一类的目标数只有几百个而其他类有几千个训练时就要考虑针对少数类别做复制粘贴增强或者调节loss函数里的类别权重否则模型会被多数类带偏。2.2 VOC格式的目录组织与XML标注一张图对应一个标注文件VOC格式的全称是Pascal VOC它用XML文件存标注信息。标准的VOC数据集目录是AnnotationsXML标注、JPEGImages原图、ImageSets/Main训练验证划分的txt文件三个核心目录。当你解压这个.7z包后大概率会看到类似结构swim_drowning_dataset/ ├── VOC/ │ ├── Annotations/ # 8275个XML文件 │ ├── JPEGImages/ # 8275张原图 │ └── ImageSets/ │ └── Main/ │ ├── train.txt │ ├── val.txt │ └── test.txt ├── YOLO/ │ ├── images/ # 原图 │ │ ├── train/ │ │ └── val/ │ └── labels/ # 每张图对应的txt │ ├── train/ │ └── val/ └── classes.txt # 4个类别名打开任意一个XML文件核心信息是object标签里的name和bndbox。一个XML里可能有多个object分别代表图上不同位置的目标。bndbox里存的是左上角xmin和ymin、右下角xmax和ymax四个坐标值单位是像素。你需要知道一个关键边界VOC坐标是绝对像素值而YOLO坐标是归一化的相对值两者转换时要除以图像的宽高。很多新手在这一步容易犯的错是直接改XML里的内容但改完不知道去更新ImageSets里对应的文件列表。其实多数情况下你不需要手动编辑XML只需要把它作为格式转换的输入源。真正需要关心的是XML文件里是否存在没有bndbox的difficult目标这类目标在转换时如果处理不当会导致YOLO训练时出现空标注文件直接报错。2.3 YOLO格式的txt与归一化坐标每一行是一个目标YOLO格式的标注是一个txt文件文件名和图片名同名训练图片IMG_1042.jpg对应的标注是IMG_1042.txt。txt文件里每一行代表一个目标格式是五个数值2 0.521484 0.366667 0.301562 0.433333 0 0.211719 0.249074 0.153906 0.229630每行的第一个数字是类别ID从0开始对应classes.txt里的顺序。后面四个数字依次是归一化后的中心点x、中心点y、宽度w、高度h所有值都在0到1之间。如果考虑图像实际尺寸反推回去就能得到像素坐标。例如第一行的类别ID是2中心点x是0.521484如果原图宽1920那一像素坐标就是0.521484乘以1920约为1001像素。这个格式的好处是坐标与图像尺寸解耦无论你训练时把图缩放到640还是1280标注都不用改。坏处是换数据集或换训练框架时需要保证类别ID的排序一致。比如这个数据集里ID为0的类别在classes.txt里可能是swimmer你训练时data.yaml里写错顺序模型就会把swimmer的框预测成drowning而且不容易发现因为loss还是正常下降、mAP看起来也不差。所以拿到数据集后第一步是打开classes.txt核对你自己的data.yaml里类别顺序和它完全一致。解压.7z文件时在Linux服务器上建议用p7zip而不是系统自带的tar因为tar不支持7z格式apt install p7zip-full 7z x swimming_dataset.7z注意7z解压后要检查一下文件是否完整尤其是如果压缩包是从网盘下载的最常见的翻车是下载不完整解压时提示数据错误但部分文件还是解出来了这时候用这部分残缺数据训练结果全是玄学。我一般会用7z t做一次完整性测试测试通过再开始下一步。3. 用Python把VOC批量转成YOLO转换脚本与数据划分3.1 为什么保留双格式而不是只留一种同时提供VOC和YOLO格式看起来冗余但这恰恰是这个数据集最实用的地方。VOC格式的可读性强XML里能看到目标的类别名、是否困难目标这些人类友好的信息适合做数据质量检查和可视化YOLO格式直接服务于训练txt文件轻量、读起来快Ultralytics YOLO、Darknet这些框架直接读txt就能跑。不同的人用的工具链不一样做数据增强时读写XML不方便用txt就简单很多。所以我的建议是别动原始文件把VOC当作master副本每次需要改标注时改XML然后跑一次转换脚本生成新的YOLO格式这样最稳妥。常见做法是拿到这种双格式数据集后直接用现成的YOLO目录跑训练VOC目录留着做二次校验和可视化。但实际场景里你可能需要修改类别——比如觉得“挣扎者”和“溺水者”边界太模糊想合并成一个类别那你需要改的是XML里的name改完再重新生成txt。这一步看似简单但批量替换时很容易写错正则表达式改错一个类别名后面模型训练就是在错误标签上学习等到上线才发现后悔药都没得吃。3.2 转换脚本的实现从XML解析到归一化计算一次跑通下面给一个我从VOCO转换到YOLO格式时经常用的脚本它的核心逻辑是读取所有XML文件解析每个object的类别名和边界框坐标把类别名映射成ID坐标做归一化最后写成txt文件。import xml.etree.ElementTree as ET import os from pathlib import Path # 类别映射表顺序必须和classes.txt保持一致 class_mapping { swimmer: 0, drowning: 1, struggling: 2, person: 3 } def convert_xml_to_yolo(xml_path, output_dir, img_width, img_height): tree ET.parse(xml_path) root tree.getroot() # 生成同名txt文件 txt_name Path(xml_path).stem .txt txt_path Path(output_dir) / txt_name lines [] for obj in root.iter(object): name obj.find(name).text # 跳过difficult目标防止空标注和错误框混入 difficult obj.find(difficult) if difficult is not None and int(difficult.text) 1: continue if name not in class_mapping: print(f[警告] 未知类别 {name} 在 {xml_path}已跳过) continue class_id class_mapping[name] bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 归一化中心点坐标和宽高都除以图像尺寸 x_center ((xmin xmax) / 2) / img_width y_center ((ymin ymax) / 2) / img_height width (xmax - xmin) / img_width height (ymax - ymin) / img_height # 边界裁剪防止数值略超1导致训练报错 x_center min(max(x_center, 0), 1) y_center min(max(y_center, 0), 1) width min(max(width, 0), 1) height min(max(height, 0), 1) lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) with open(txt_path, w) as f: f.write(\n.join(lines)) # 批量处理所有XML xml_dir Path(VOC/Annotations) output_dir Path(YOLO/labels) output_dir.mkdir(parentsTrue, exist_okTrue) image_dir Path(VOC/JPEGImages) for xml_file in xml_dir.glob(*.xml): # 获取对应的图片尺寸这里可以读图片元信息 img_name xml_file.stem .jpg img_path image_dir / img_name if not img_path.exists(): print(f[警告] 找不到图片 {img_path}跳过) continue # 这里用PIL获取宽高更准确避免手写死值 from PIL import Image with Image.open(img_path) as img: w, h img.size convert_xml_to_yolo(xml_file, output_dir, w, h) print(f转换完成输出目录{output_dir})这个脚本的关键参数说明class_mapping是字典键是类别名值是类别ID它的顺序必须和YOLO目录或者你训练时写的data.yaml一致。脚本里跳过了difficult目标是因为有些数据集把模糊目标标为difficult这类目标训练时容易产生梯度干扰但这个数据集里未必有你可以先全部转换统计一下有多少difficult目标再决定。归一化时做了数值裁剪到0到1之间如果你发现大量框都被裁剪了说明XML里有的框坐标超出了图像边界这种情况下先去修数据而不是强行裁剪。最后两个小坑图像宽高我建议用PIL读取而不是从XML的size标签读因为XML里可能漏写或者写错归一化保留6位小数精度已经足够多写并不是更好。3.3 训练集与验证集划分按场景划分而不是按文件随机划分8275张图划分训练集和验证集看起来很简单直接按比例随机分就行但实际做检测项目时这里有个大坑监控视频连续帧之间高度相似同一个游泳者在连续几十帧里只是位置稍微移动如果随机划分同一场景的相邻帧可能一部分进了训练集、一部分进了验证集验证集的精度看起来很高但模型实际部署到新场景时表现明显下降。这属于典型的数据泄漏问题。正确的做法是先按图片文件名找到场景来源把同一时段连续帧或者同一视角的图片划分到同一个集合里。常见做法是文件名里通常带摄像头编号和时间戳比如Camera01_20240510_143205.jpg可以按前几段字符分组。如果不带这个信息就只能靠聚类或人工判断。一个简单的启发式方法如果图片尺寸相同且拍摄环境相似把文件名按数字序号排序每隔10帧抽1帧做验证集这样验证集覆盖了不同时间段又能避免前后帧直接泄漏。训练集、验证集、测试集的建议比例是71.51.5。这个数据集一共8275张按这个比例大概训练集5800张、验证集1240张、测试集1230张。不需要额外再做划分脚本直接用Ultralytics YOLO训练时在data.yaml里指定train和val路径它会自动读取目录下所有图片。但如果你是先划分好再训练记得在YOLO的images目录和labels目录保持相同的子目录结构否则训练时找不到标签会输出一堆警告然后跳过这些图片。4. 用YOLOv8训练游泳者检测模型最小配置与超参数设定4.1 写data.yaml路径、类别数、类别名三个地方不能错训练前的第一关是data.yaml。Ultralytics YOLOv8会读取这个YAML文件来定位数据路径和类别信息。以下是最小可用的配置# 数据集配置文件 data.yaml path: /home/user/swim_dataset # 数据集根目录 train: YOLO/images/train # 训练图片相对路径 val: YOLO/images/val # 验证图片相对路径 test: YOLO/images/test # 测试图片可选 nc: 4 # 类别数量 names: [swimmer, drowning, struggling, person] # 类别名顺序必须和标注一致三个最容易出错的地方第一nc必须和names列表长度一致写少了一个类别训练时后续类别标签会被忽略mAP怎么训都上不去。第二names顺序必须和YOLO txt里数字ID对应ID为0的名字必须写在第一位。第三path建议写绝对路径写相对路径时YOLOv8会以当前工作目录为基准拼接你在不同目录下执行训练命令就会得到不同的路径解析结果报错说找不到图片。yaml文件里不要用Tab缩进全部空格这是YAML的老坑报错信息往往不直观。4.2 训练命令与关键超参数imgsz、epochs、batch怎么设配置好data.yaml后一条命令就可以开始训练yolo train detect data/home/user/swim_dataset/data.yaml \ modelyolov8m.pt \ epochs50 \ imgsz640 \ batch16 \ lr00.001 \ device0参数说明modelyolov8m.pt是预训练权重YOLOv8会从官方仓库下载对应的COCO预训练模型迁移学习的关键就在这预训练权重能让你用更少的epochs达到更高的精度。imgsz640是输入分辨率。溺水检测场景里人体目标占比不大如果原始图片分辨率是1080p建议先从640开始跑通流程后再尝试1280精度会有提升但显存占用会显著增大而且训练时间和推理时间都会翻几倍。batch取决于显存大小默认16比较保守但batch太小会让BN层的统计量不稳定。如果你的显卡是24G显存可以直接把batch调到32batch越大收敛越稳定但也不能盲目加大。epochs我一般不固定而是配合早停机制。Ultralytics YOLO默认开启了早停patience默认为50意味着连续50个epoch验证集没有提升就自动终止。对这个数据集50个epoch通常是基准线如果到50个epoch验证集mAP还在明显上升说明数据量足够可以继续跑。lr0初始学习率对YOLOv8来说0.001是一个比较稳的起点不建议直接上0.01因为预训练模型已经有一个较好的特征提取器学习率太大容易破坏底层权重。这里还要注意损失函数的特性。YOLOv8的损失是分类损失和回归损失两部分加权相加默认设置了cls、box相关的权重系数。溺水检测里有一个特点目标框的大小差异很大有的游泳者在画面里占了很大面积有的远处的人只有几十个像素。YOLOv8默认使用CIoU作为回归损失的度量它对小目标的梯度响应相对较弱。如果你发现验证集里远处小目标的precision很低可以调整anchor相关参数或者把imgsz加大到960甚至1280这比调loss函数权重更直接有效。4.3 训练完怎么看结果loss曲线、混淆矩阵、PR曲线是三个关键图训练结束后YOLOv8会在runs/detect/train目录下生成一批结果文件。不要只看最后的mAP数字三个图值得一读。第一个是results.png一列小图展示box_loss、cls_loss、dfl_loss三条曲线。如果box_loss收敛到平稳但偏高说明框的定位误差大常见的原因是标注框本身不精确尤其在这种游泳场景下水花和波纹会干扰标注人员对目标边界的判断。第二个是confusion_matrix.png看类别之间互相混淆的情况。溺水检测里容易混淆的是swimmer和struggling两类动作特征相似度高如果混淆矩阵里这两个类互相混淆的样本特别多说明类别定义本身需要调整建议把这两类合并或者重新审视标注标准。第三个是PR_curve.pngprecision和recall的权衡。溺水检测跟普通检测不一样漏报的代价远高于误报所以recall是第一优先级。你可以按PR曲线选择一个偏向高recall的置信度阈值比如默认0.25上线时调到0.15宁可多报几个误报也不能漏一个真溺水。如果你想快速验证训练效果可以跑一次验证yolo val detect modelruns/detect/train/weights/best.pt \ data/home/user/swim_dataset/data.yaml输出会打印每个类别的AP50、AP50-95重点关注drowning这一类的AP50。如果AP50在0.8以上说明类别检测基本可用如果在0.5以下大概率是数据问题而不是模型问题需要回去查标注。5. 游泳者溺水数据集的5个典型训练陷阱与排查方法5.1 样本不平衡溺水样本太少模型全预测成游泳者现象是训练完的模型在测试视频里几乎不检出处溺水者但对正常游泳者的检测又特别准。原因很直接4个类别里溺水相关样本可能只占少数尤其是明确的溺水正样本可能只有几百张。模型在训练时见到的大多数目标都是swimmer学到的决策边界严重偏向多数学。解决方法是先用数据增强对少数类做针对性复制尤其是mosaic增强时对少数类降低随机采样的阈值或者调整loss函数里的类别权重YOLOv8可以在data.yaml里给每个类别设置权重。另外不要只靠翻图做增强可以收集网上公开的溺水图片补充进去当然要注意标注一致性。这个坑最隐蔽因为整体mAP可能不低但类别的平均精度差距很大所以要养成看每个类别AP的习惯。5.2 小目标漏检泳池大场景里的人头只有几十个像素现象是远处的小目标人物检测不到置信度很低靠近镜头的目标一切正常。原因是这个数据集的图片分辨率高标注的目标尺寸范围跨度大在640分辨率训练时小目标被压缩得只剩十几个像素细节丢失严重。解决方法是先统计数据集里的目标框面积分布如果大量框的面积占比小于0.01直接换用yolov8s或yolov8m配合imgsz960训练或者开启YOLOv8的small_object策略。另一个实用技巧是分块训练把大图切成四块分别检测再合并但推理速度会受影响。血泪经验是别一开始就追求高分辨率先把baseline跑出来再逐级放大你会发现模型在640和960之间的mAP差距可能达到5个点以上但推理速度也相应下降了一半。5.3 标签噪声XML里面存在漏标和错标现象是训练过程中loss曲线正常下降可视化的预测结果也没问题但验证集mAP一直不稳定某些类别的精度忽高忽低。原因大概率是标注数据本身有噪声。比如某人处于游泳者和挣扎者的中间状态标注员对这个样本的判断不同有的标成swimmer有的标成struggling模型在这个边界上来回横跳。解决方法是训练前做一轮标签清洗把XML里置信度存疑的样本挑出来。我一般会写个脚本统计每张图的目标框数量、类别分布和难例目标数量目标框数量异常少的图、含有difficult目标的图先抽出来人工检查。如果某个类别的实例数比其他类别少一个数量级也建议直接合并类别而不是硬训练。5.4 水面反光与场景过拟合模型在换一个摄像头后精度骤降现象是训练集来自同一个泳池的多个摄像头模型在该场景表现很好一旦换到另一个泳池检测效果明显变差。原因是图片里大量的水池纹理、水面反光、瓷砖颜色被模型当成了关键特征换句话说模型学到了场景本身而不是人。解决方法是训练时加大mosaic和crop等数据增强强度让模型更多地关注目标物体本身的纹理而不是背景。更有效的方法是换数据集或者在这个数据集基础上加入其他泳池的图片做微调。这属于迁移学习的经典问题除非数据集本身覆盖了多个泳池环境否则跨场景部署前必须有现场采集的小批量数据做微调不然线上直接翻车。5.5 解压与路径问题7z解压不完整导致训练集缺文件现象是训练开始时报错AssertionError: train: No labels in ...或者训练过程中大量图片被跳过训练集实际参与训练的图片远少于预期。原因不一定是数据集本身很可能是.7z文件下载不完整或者解压后目录层级被改动。解决方法是解压后对比JPEGImages和Annotaions的数量两边应该是完全相同数量。然后检查一个关键点YOLO目录下images和labels的同名文件是否一一对应。在Linux下可以用一条命令检查diff (ls images/train | sort) (ls labels/train | grep -E \.txt$ | sort)没有输出说明两边文件一致有差异的输出行就是缺标签或者缺图片的文件。这类问题最费时间的是排查过程数据量一大出现几十个缺失文件训练时不会有明显报错只是精度比预期低一截。我一般会在写训练数据加载脚本时加一个校验步骤提前检查数据完整性而不是等跑了10个epoch才发现。6. 从单帧检测到时序判定把溺水报警率压下来的一步单帧检测模型只是第一步。真正部署到泳池监控系统时你会发现一个核心问题溺水不是一个静态姿态而是一个持续数秒到数十秒的动态过程。单帧检测可能在某几帧里把头部下沉的游泳者判为溺水但下一秒他又正常划水了如果直接报警误报率会高到值班人员直接关掉系统。我做的方案是引入一个轻量的时序判定层记录每个目标的检测结果和位置历史连续N帧判定为溺水且目标中心点位移小于阈值时才触发报警。具体的判断逻辑是维护一个目标跟踪列表对每个检测框做IoU匹配把同一目标的不同帧关联起来。当某个目标连续6帧被分类为drowning或struggling同时其中心点在6帧内的位移不超过目标框宽度的0.2倍则判定为溺水风险目标。前一个条件排除了瞬时误检后一个条件排除了正常游泳者——正常游泳即使头部偶尔下沉身体也是在前进的。这个思路用几行普通代码就可以实现不需要额外训练时序模型。如果你需要更强的判定能力可以在这个数据集基础上收集视频序列用slowfast或temporal attention系列的模型来做视频级别的分类但这需要更大的数据量和更长的训练周期。还有一个验证技巧值得分享训练完成后不要只看测试集指标直接把模型接到一个模拟视频流上做端到端测试。我习惯准备两段视频一段是正常游泳场景时间较长另一段是有人在水里静止不动的表演片段。正常游泳场景如果触发了报警说明误报率偏高静止不动的片段如果没触发报警说明时序判定逻辑或置信度阈值设得不对。这套验证方法成本低但能暴露单帧指标掩盖掉的真实问题。这个方向值不值得投入最终看得不是训练集上mAP多高而是实际部署时能否做到“不打扰正常游泳者又不漏掉一个真正下沉的人”——这个项目给了你一个可以讨论这个问题的起点剩下的要靠你在真实场景里一点点调希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?