
简介面向游泳监控与溺水预警场景这份数据集提供4599张真实场景图片标注框总数为6017个覆盖drowning与swimming两个目标类别可直接用于训练YOLO、SSD、Faster R-CNN等目标检测模型。数据格式兼顾Pascal VOC与YOLO两种主流标准图片与XML、TXT标注一一对应采用labelImg工具以矩形框逐张人工标注类别框数分别为drowning 2578个、swimming 3439个便于进行类别均衡分析和数据增强。资源包共2000个文件主要由1999个XML标注文件和1个使用说明TXT文件构成压缩包整体约156.75MB结构清晰无需复杂预处理即可接入常用深度学习框架。目前已有521人学习下载适合计算机视觉初学者、安防算法工程师以及水上安全监测项目开发者作为算法验证和模型迭代的基础数据。拿到后可直接划分训练与验证集快速启动训练实验内含标注统计与使用说明能够帮助快速了解数据分布减少数据清洗时间聚焦模型本身优化。1. 游泳者溺水检测数据集先别急着找模型先弄懂这份VOCYOLO双格式的4599张图做泳池安防或者水域监控的人大概率都遇到过同一个尴尬模型结构选好了训练脚本写好了结果卡在“数据不够”或者“标注格式对不上”上。这份“游泳者溺水检测数据集VOCYOLO格式4599张2类别.7z”解决的正是这个卡点——它不是给你一个现成的端到端推理系统而是给你一份可以直接喂进YOLO训练流程的标注数据同时保留了VOC格式作为中间交换。4599张图、2个类别这个规模对从零训练来说不算大但对迁移学习、微调预训练权重、验证检测pipeline来说是恰到好处的量级。适合谁想用yolov8训练自己的数据集的新手以及需要快速验证“溺水检测到底能不能做”的从业者。这份数据集的真正价值不在图多而在它同时提供了两种主流标注格式可以少踩很多格式转换的坑。2. VOC与YOLO双格式拆解4599张图里的目录结构和标注差异2.1 为什么是2个类别游泳者与溺水者的分类边界这个数据集只有2个类别常见的命名是“swimmer”游泳者和“drowning”溺水者。这里有个容易忽略的点这两个类别不是“有人”和“没人”的区别而是同一个目标在不同状态下的区分。换句话说模型要学习的不是“目标有没有出现”而是“这个人的姿态、位置、运动轨迹是否符合溺水特征”。这就带来一个现实问题类别的语义边界不像COCO数据集里“人”和“自行车”那么清晰。某个泳姿可能就是“看起来像在划水但实际在挣扎”某个人仰面漂着可能是在休息而不是溺水。数据集的标注质量直接决定模型的精度上限算法反而是次要的。所以拿到这份数据后的第一件事不是改网络结构而是把图片和标注过一遍搞清楚标注者定义的“溺水”到底是什么样的标准。2.2 VOC版目录结构JPEGImages与Annotations的字段对照VOC格式Pascal VOC是目标检测领域最通用的标注交换格式。这份数据集如果用VOC版组织典型目录结构如下dataset_root/ ├── JPEGImages/ # 所有jpg图片文件名与标注文件一一对应 │ ├── swimmer_001.jpg │ ├── swimmer_002.jpg │ └── ... ├── Annotations/ # 每张图对应的xml标注文件 │ ├── swimmer_001.xml │ └── ... └── ImageSets/ └── Main/ ├── train.txt └── val.txt单个XML标注文件的核心结构如下annotation filenameswimmer_001.jpg/filename size width1920/width height1080/height depth3/depth /size object namedrowning/name bndbox xmin312/xmin ymin287/ymin xmax658/xmax ymax712/ymax /bndbox /object /annotation注意这里几个关键字段filename对应JPEGImages里的实际文件名size里的宽高必须跟图片真实尺寸一致否则后面转YOLO格式时归一化坐标会错位object里的name就是类别名称这份数据应该是“swimmer”和“drowning”二选一。VOC格式的优点是人眼可读、字段规范、几乎所有检测框架都提供解析工具缺点是每张图一个XML文件文件数量多而且标注信息有冗余处理起来比YOLO的txt慢。2.3 YOLO版每行标注的含义中心点、宽高和归一化YOLO格式的标注是每个图片对应一个txt文件放在labels目录下每一行代表一个目标框class_id x_center y_center width height具体来说x_center和y_center是目标框中心点的归一化坐标width和height是归一化的宽和高。所有数值都在0到1之间计算方式是x_center (xmin xmax) / 2 / image_width y_center (ymin ymax) / 2 / image_height width (xmax - xmin) / image_width height (ymax - ymin) / image_height举个例子前面XML里的标注框在1920x1080的图上转成YOLO格式就是1 0.2526 0.4625 0.1802 0.3935其中class_id为1表示drowning0表示swimmer具体编号要看数据集自带的类别映射。YOLO格式的优势是每行一个目标读取快、占用空间小训练时几乎不解析XML。但缺点也很明显txt里不包含图片尺寸信息一旦用错尺寸做归一化坐标就全乱了。另外txt文件里没有类别名称只有编号编号顺序错了模型就学反了。所以这份数据集同时提供VOC和YOLO格式本质上是给了你一条退路VOC格式用来核对语义YOLO格式直接训练。2.4 4599张图算什么规模迁移学习的题材4599张图、2个类别如果按类别平均分每个类别约2300张图。这个规模在目标检测领域属于“中小型数据集”。直接随机初始化权重从零训练效果大概率不理想因为检测模型动辄上千万参数2300张图远不够拟合。但有两个有利条件一是泳池场景的视觉变化相对可控不像自动驾驶那样有极端多样的光照和背景二是目标类别只有2个模型需要学习的特征区分度比80类COCO简单得多。常见做法是采用迁移学习用COCO预训练的YOLO权重做初始化冻结骨干网络训练前几十个epoch然后解冻全部层微调。这样4599张图是够用的。另外要注意类别不平衡问题如果swimmer和drowning的数量悬殊训练时需要调整类别损失权重或者做采样策略后面会讲具体参数。3. 从.7z到可训练目录解压、校验、格式归一化三件事3.1 解压和环境准备先看目录再写脚本拿到“游泳者溺水检测数据集VOCYOLO格式4599张2类别.7z”这样的压缩包第一步不是急着解压而是确认压缩包内部结构。.7z格式用7-Zip或Linux下的p7zip解压命令如下# Ubuntu/Debian系安装p7zip sudo apt install p7zip-full # 解压到指定目录保持原有目录结构 7z x swimming_drowning.7z -o/path/to/dataset参数说明x表示解压并保留完整路径-o指定输出目录注意-o和路径之间没有空格。解压完成后先进入目录看一下层级ls -la /path/to/dataset这里提醒一个常见的坑很多打包者会把数据集再套一层上级目录比如解压后出现dataset/然后里面才是JPEGImages/。后面的训练脚本路径全都要基于实际解压层级来写建议解压后直接以JPEGImages所在的目录作为DATASET_ROOT避免后续脚本里出现dataset/dataset/JPEGImages这种路径错误。3.2 校验图片和标注统计类别分布和无效框训练前必须做一轮数据体检检查三件事图片数量是否等于标注数量、每个类别的目标框数量、有没有宽高为0或超出图片边界的非法框。下面这个Python脚本可以一次完成统计import os from collections import Counter from PIL import Image DATASET_ROOT /path/to/dataset IMAGES_DIR os.path.join(DATASET_ROOT, JPEGImages) LABELS_DIR os.path.join(DATASET_ROOT, labels) # 如果数据集已含YOLO格式 # 类别映射这里假设编号0swimmer, 1drowning CLASS_NAMES {0: swimmer, 1: drowning} def check_yolo_labels(): total_boxes Counter() invalid_files [] for label_file in os.listdir(LABELS_DIR): if not label_file.endswith(.txt): continue img_file label_file.replace(.txt, .jpg) img_path os.path.join(IMAGES_DIR, img_file) if not os.path.exists(img_path): invalid_files.append(f{label_file}: 对应图片不存在) continue with Image.open(img_path) as img: img_w, img_h img.size with open(os.path.join(LABELS_DIR, label_file), r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: invalid_files.append(f{label_file}: 字段数不对) continue cls, x_center, y_center, w, h parts x_center, y_center, w, h map(float, (x_center, y_center, w, h)) cls int(cls) # 检查坐标是否在[0,1]范围内以及宽高是否为正 if w 0 or h 0 or not (0 x_center 1 and 0 y_center 1): invalid_files.append(f{label_file}: 坐标异常 [{cls} {x_center} {y_center} {w} {h}]) continue total_boxes[CLASS_NAMES.get(cls, f未知类{cls})] 1 print(各类别目标框数量:, dict(total_boxes)) print(异常文件数:, len(invalid_files)) for item in invalid_files[:10]: print( -, item) if __name__ __main__: check_yolo_labels()脚本的核心逻辑是逐行解析txt标注同时打开对应图片做尺寸校验。total_boxes用Counter统计每个类别的框数这能直接暴露类别不平衡问题。invalid_files收集三类异常图片缺失、字段数不对、坐标越界或宽高非正。只要出现未知类的计数说明类别编号跟预期不一致需要查看数据集自带的类别说明文件。3.3 VOC转YOLO自己动手写转换脚本比找现成工具更靠谱虽然这份数据集已经提供了YOLO格式但实际使用中你很可能需要把VOC转成YOLO原因有两个一是官方给的划分可能不合你的场景你要重新划分train/val二是你想增加自己的图片新标注的xml要转成yolo才能跟这份数据合并训练。网上有很多转换脚本但多数没做异常处理。我一般会自己写一个逻辑完全可控import os import xml.etree.ElementTree as ET from collections import defaultdict VOC_ROOT /path/to/dataset/voc YOLO_ROOT /path/to/dataset/yolo CLASS_MAPPING {swimmer: 0, drowning: 1} # 按数据集的类别定义调整 def voc_to_yolo(xml_path, output_txt_path): tree ET.parse(xml_path) root tree.getroot() img_width int(root.find(size/width).text) img_height int(root.find(size/height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASS_MAPPING: print(f跳过未知类别 {name} in {xml_path}) continue cls_id CLASS_MAPPING[name] bbox obj.find(bndbox) xmin int(bbox.find(xmin).text) ymin int(bbox.find(ymin).text) xmax int(bbox.find(xmax).text) ymax int(bbox.find(ymax).text) # 归一化计算 x_center (xmin xmax) / 2.0 / img_width y_center (ymin ymax) / 2.0 / img_height w (xmax - xmin) / img_width h (ymax - ymin) / img_height # 夹紧到[0,1]防止边界标注越界 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) w min(max(w, 0.0), 1.0) h min(max(h, 0.0), 1.0) lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(output_txt_path, w) as f: f.write(\n.join(lines)) os.makedirs(os.path.join(YOLO_ROOT, labels), exist_okTrue) for xml_file in os.listdir(os.path.join(VOC_ROOT, Annotations)): if not xml_file.endswith(.xml): continue base xml_file.replace(.xml, ) xml_path os.path.join(VOC_ROOT, Annotations, xml_file) txt_path os.path.join(YOLO_ROOT, labels, base .txt) voc_to_yolo(xml_path, txt_path) print(转换完成输出目录:, YOLO_ROOT)这个转换脚本有四个关键设计。第一CLASS_MAPPING由手工定义不依赖数据集的任何配置文件避免类别编号错位。第二归一化时把坐标结果夹紧到[0,1]VOC标注偶尔会出现坐标比图片尺寸大一点点的边界框如果不过滤训练时YOLO算损失会出现NaN。第三未知类别直接跳过并打印不会让整个转换崩掉。第四输出的txt中坐标保留6位小数精度足够且文件体积小。你需要注意如果转换后某个图片对应的txt是空文件说明这张图没有有效标注训练时要在数据加载阶段过滤掉。3.4 重新划分train/val固定随机种子保证可复现数据集自带的划分不一定适合你特别是当你只想要“drowning”样本比较多的一组做验证时。重新划分的规则很简单按图片名随机打乱保证同一张图不会同时出现在训练集和验证集。下面这段脚本可以同时生成VOC训练所需的train.txt/val.txt和YOLO训练所需的images/train软链或文件列表import os import random IMAGES_DIR /path/to/dataset/JPEGImages OUTPUT_DIR /path/to/dataset/split TRAIN_RATIO 0.8 # 80%训练、20%验证 random.seed(42) # 固定随机种子多次运行结果一致 all_images [f for f in os.listdir(IMAGES_DIR) if f.endswith(.jpg)] random.shuffle(all_images) split_idx int(len(all_images) * TRAIN_RATIO) train_images all_images[:split_idx] val_images all_images[split_idx:] os.makedirs(OUTPUT_DIR, exist_okTrue) with open(os.path.join(OUTPUT_DIR, train.txt), w) as f: f.write(\n.join(os.path.splitext(img)[0] for img in train_images)) with open(os.path.join(OUTPUT_DIR, val.txt), w) as f: f.write(\n.join(os.path.splitext(img)[0] for img in val_images)) print(f训练集: {len(train_images)} 张, 验证集: {len(val_images)} 张)这里的random.seed(42)是关键参数。换一个seed划分结果就变了训练结果不具备可比性。另外TRAIN_RATIO取0.8对4599张图来说训练集约3680张、验证集约920张验证集规模充足。如果你的目标是做溺水检测的专项测试可以把所有含drowning的目标框单独抽出来做一次分层统计确保验证集里两个类别的比例跟训练集接近避免出现验证集里没有溺水样本的尴尬。4. 用YOLOv8训练这份数据data.yaml、超参和迁移学习的具体配置4.1 data.yaml的正确写法路径、类别数和类别名YOLOv8是目前训练自定义数据集最顺手的框架但它对data.yaml的要求有一些细节。基于这份数据集的目录结构配置文件这样写# dataset.yaml path: /path/to/dataset # 数据集根目录 train: images/train # 训练图片相对路径 val: images/val # 验证图片相对路径 nc: 2 # 类别数量 names: [swimmer, drowning] # 类别名称顺序必须和标签txt里的编号一致注意三个容易踩坑的点。第一path建议写绝对路径相对路径在调试时容易出幺蛾子。第二train和val指向的是图片目录YOLOv8会自行去同级labels目录找对应的txt文件不要用逗号分隔多个目录除非你要做多数据集合并。第三names里的顺序要和前文CLASS_MAPPING保持完全一致——如果标签txt里class_id0表示drowning而你这里names[0]swimmer训练不会报错但验证时的混淆矩阵和最终检测结果是完全错位的。4.2 训练命令和关键参数从预训练权重出发而不是从零开始训练命令建议用yolo命令行工具加参数覆盖而不是直接改cfg文件yolo detect train \ data/path/to/dataset.yaml \ modelyolov8n.pt \ epochs80 \ batch16 \ imgsz640 \ patience10 \ lr00.001 \ device0 \ project./runs \ nameswimmer_det参数说明如下modelyolov8n.pt表示加载COCO预训练权重继续训练这是4599张图规模下最好的起点。batch16是根据显存调整的如果你的显卡是8GB显存16可能要降到8如果是24GB显存可以试着升到32。imgsz640是YOLOv8默认输入尺寸泳池场景目标比较大不需要用1280。patience10表示验证集mAP连续10个epoch不提升就提前停止防止过拟合。lr00.001是初始学习率从预训练权重微调时这个值不宜太大否则会破坏已学好的特征。这里有个细节加载预训练权重时YOLOv8会自动适配类别数。COCO预训练模型是80类你这份数据是2类框架会截断最后一层分类头重新初始化。不需要手动修改权重文件但要注意如果直接指定modelyolov8n.yaml而不是.pt就是从零训练效果会差很多。第一次先用nano规模跑通确认loss下降趋势正常再换yolov8s.pt或yolov8m.pt提精度。4.3 类别不平衡怎么处理先看统计再决定要不要改loss如果校验脚本统计出swimmer有3500个框、drowning只有1500个框这个比例不算太失衡还不至于崩。但如果出现10:1的悬殊比例就需要干预。常见做法有几种一是用class_weights参数给少数类更高的损失权重YOLOv8里开启方式如下yolo detect train \ data... \ modelyolov8n.pt \ epochs80 \ batch16 \ imgsz640 \ loss_weights0.8,1.2loss_weights里第一个数对应swimmer的box/class损失权重第二个数对应drowning。把“溺水者”的权重调高让模型更重视这个类别。但这个做法要小心过拟合权重太高会让模型倾向于把所有目标都预测成drowning。另一个做法是数据增强里加mosaic0.5、mixup0.2增加样本多样性。不过游泳者检测场景里Mosaic增强会让多个泳池的画面拼在一起泳池边的颜色纹理差异反而可能误导模型建议mosaic不要超过0.8。我看到不少人在这个数据集上把mosaic开到1.0训练集loss虽然降得快但验证集mAP忽高忽低就是增强强度过大的典型表现。5. 泳池场景训练最常见的5个坑现象、原因和解决办法5.1 解压后图片和标注文件名对不上训练时报错Image not found现象用YOLOv8训练时日志里出现大量“image not found”警告训练直接中断。原因.7z压缩包内部可能把图片和标注分成了两个子压缩包解压时只解压了其中一部分或者文件名包含特殊字符比如中文命名的人名编号Linux下编码不同导致匹配失败。解决解压后执行一次完整校验不要把“能打开”当作“能用”。用md5sum对图片和标注文件做一一比对或者简单点看两边文件数是否一致——图片4599张XML和txt也该是4599个。ls /path/to/dataset/JPEGImages | wc -l ls /path/to/dataset/labels | wc -l如果数量不一致大概率是压缩包里的标注有缺失或者解压时有文件损坏。这时不要硬训练先重新解压并检查压缩包完整性。5.2 类别编号对不上模型训练正常但检测结果张冠李戴现象训练跑完loss正常下降但把模型部署到测试视频上时把正常的游泳者标成drowning。原因这是最容易踩的坑。数据集的YOLO格式txt里class_id0可能是“drowning”而你data.yaml里names列表顺序写的是[swimmer,drowning]编号就错位了。这类数据集的命名规范不统一有的把drowning作为正类设为0有的作为第二类设为1。解决不要在data.yaml里猜直接统计原始txt里每行的第一个数字看有几个类别编号再抽样打开几张图片用肉眼核对实际目标。确认编号后再写names列表并固定下来不再改动。这个坑不报错、不崩训练发现的窗口只在最终效果验证阶段越早检查越好。5.3 标签框坐标出现0.0或1.0loss直接变成NaN现象训练到第十几个epoch时box_loss突然变成nan然后整体loss全部崩掉。原因某些YOLO标签txt里出现0.000000 0.000000 1.000000之类的整行边界值这通常是VOC转YOLO时坐标计算错误把整个图片误标成了一个目标框。YOLO训练时算anchor匹配这种极端框容易让梯度冲过头直接爆掉。解决训练前用前面写的校验脚本检查坐标。如果出现0 x_center 1范围内的正常框但宽度恰好等于1.0或高度恰好等于1.0删除这一行标注因为这明显是转换时的异常值不是真实的目标框。训练时加上cacheTrue参数让YOLOv8把数据缓存到内存这样如果哪个样本有问题报错信息能精确定位到文件名。5.4 整体mAP漂亮但drowning类别的AP极低数据标注含大量噪点现象训练20个epoch后验证集整体mAP到了0.75但看每个类别的APswimmer是0.91drowning只有0.42。原因溺水样本本身的标注一致性差——不同标注者对“正在溺水”的判定标准不同有的把“仰面漂浮”也算作溺水有的只把“挣扎姿态”算溺水。模型学到了一个折中的特征边界两头都不讨好。解决把训练集里所有drowning类别的图片抽出来做一次人工复检重点看标注框是不是把目标框全了以及类别标签是否跟自己的业务定义一致。4599张图里drowning类别的样本很少一次人工检查一个小时内能过完。这一步比换任何模型结构都管用。检查后按照业务重新标注不一致的样本再微调训练。做这一步时可以用python脚本把所有drowning样本复制到一个单独文件夹逐张快速查看。5.5 预训练权重加载报错类别数不匹配导致的KeyError现象执行训练命令时报错提示Error in loading state_dict或者size mismatch for model.head.cv2...。原因虽然YOLOv8会自动适配类别数但如果你用了某些修改版的网络结构或者直接加载了一个在别的数据集上训好的best.pt而非COCO官方权重最后一层卷积输出的通道数跟当前nc2对不上。解决如果是YOLOv8官方预训练模型检查下载的.pt文件是否属于同一个大版本比如不要用YOLOv5的.pt去初始化YOLOv8的模型虽然它们同出ultralytics但网络结构定义不一样。如果是自己训过的权重加载时指定strictFalse跳过不匹配层# 加载自己的权重时忽略不匹配层 from ultralytics import YOLO model YOLO(path/to/best.pt) model.load(path/to/previous_weights.pt, strictFalse)strictFalse的意思是结构不匹配的层比如分类头保留当前模型的初始化值匹配的层比如骨干网络加载旧权重。这在做数据集增量更新时是常用技巧。6. 生产环境验证用批量推理、混淆矩阵和分类别AP判断模型是否真的能用训练完不等于能用尤其在溺水检测这种场景下模型在验证集上的mAP高不代表在真实泳池视频里不误报。我的习惯是训练结束后做一轮“生产环境模拟验证”取一段泳池监控视频不是训练集和验证集里出现过的画面用训练好的权重做批量推理保存每一帧的检测框和置信度再统计两个类别的误报分布。下面这段脚本可以把YOLOv8的输出整理成结构化数据from ultralytics import YOLO import cv2 import csv model YOLO(runs/swimmer_det/weights/best.pt) video_path test_pool.mp4 cap cv2.VideoCapture(video_path) results [] frame_id 0 while cap.isOpened(): ret, frame cap.read() if not ret: break # conf0.4是泳池场景下的平衡点生产环境建议0.35-0.5之间 det model.predict(frame, conf0.4, imgsz640, verboseFalse) for box in det[0].boxes: x1, y1, x2, y2 box.xyxy[0].tolist() cls_id int(box.cls[0].item()) conf float(box.conf[0].item()) results.append([frame_id, cls_id, conf, x1, y1, x2, y2]) frame_id 1 cap.release() with open(detection_results.csv, w, newline) as f: writer csv.writer(f) writer.writerow([frame_id, class_id, confidence, x1, y1, x2, y2]) writer.writerows(results) print(f推理完成共{frame_id}帧输出{len(results)}个检测框)这段脚本的核心是conf0.4这个参数。对溺水检测来说漏报比误报严重得多——漏报一个正在溺水的人可能出人命所以置信度阈值应该往低调。我看到不少团队在部署时为了展示效果把阈值调高到0.7结果溺水时目标姿态模糊、置信度上不来直接漏检。调低到0.35-0.4配合帧间跟踪或人工二次确认才是可靠的工作模式。跑完批量推理后把检测结果里的drowning框对应的帧抽出来做成一组缩略图快速浏览一遍重点看两个问题一是误报集中在泳池边的人、穿红衣服的人还是水花飞溅的瞬间二是漏报的帧里溺水者的姿态跟训练集里的差异在哪里。这一步完成后你会得到一个明确的结论当前模型是直接能用、需要补数据还是需要调阈值。我的个人教训是第一次在这个数据集上训练时只看了整体mAP就上线测试结果对着一池子游泳教学班的视频连续误报把教练的正常教学动作识别成了溺水。后来把验证重心从“总量mAP”挪到“分类别AP人工抽帧”之后才真正把模型调到能在真实场景里干活的程度。这个顺序希望帮到你节省几天调参的弯路。本文还有配套的精品资源点击获取