☰
游泳者溺水检测数据集:VOC+YOLO双格式4599张图片
2026/9/28 17:43:16 网站建设 项目流程

简介:这是一套面向游泳馆智能监控与溺水检测场景的目标检测数据集,适合视觉算法工程师、科研人员及算法竞赛参与者进行YOLO、Faster R-CNN等模型的训练与验证。资源覆盖drowning(溺水)与swimming(游泳)两个类别,共4599张游泳者图像,标注框总计6017个,其中drowning框2578个、swimming框3439个,类别数量较为均衡。压缩包约156.75MB,共2000个文件,以VOC格式xml标注为主,并附带使用说明txt,采用7z打包。标注由labelImg绘制,矩形框边界清晰,规则统一,提供VOC与YOLO两种格式,便于直接接入不同训练框架。标注类别名称明确,可直接作为检测模型的正负样本;xml文件命名与图像编号有对应关系,便于脚本批量切分与预处理。数据不含分割路径文件,仅保留检测所需的标注信息,降低使用门槛。目前已有521人学习/下载,适合需要标准化溺水检测数据以快速搭建训练流程的开发者和研究者使用。

1. 游泳者溺水检测数据集:VOC+YOLO双格式4599张,先看清这2类6017个框

做溺水检测的人最头疼的不是模型选型,而是找不到一张带准确框的游泳者数据。网上公开的水上数据集要么是监控截图、框得乱七八糟,要么只有分类标签没坐标,跑YOLO这类目标检测模型等于没饭吃。这份资源是4599张真实游泳场景图片,带完整的Pascal VOC xml和YOLO txt双格式标注,类别就两类:drowning(溺水)和swimming(正常游泳),总计6017个目标框。它能直接喂给YOLOv5/v8训练,省掉自己爬图、画框、转格式的三天工作量。适合正在做泳池/水域监控、安全预警、姿态异常检测的算法工程师和学生。拿到手先别急着训练,把格式、类别分布和图源特点摸清楚,后面能少踩一半坑。

2. 数据集构成与标注规则:4599张图、2类别、6017个框,先读懂文件再动手

2.1 文件清单:xml/txt/jpg一一对应,firc_swim编号规律

解压后能看到一个使用前必读.txt和一堆以firc_swim_为前缀的文件,比如firc_swim_316.xml、firc_swim_260.jpg、firc_swim_260.txt。每个图片文件都对应一个同名xml和一个同名txt,编号从两位数到四位数都有,不是连续的,比如316、50、453、260、3131、235、166、3129、3130。这种命名方式说明原始数据是分批次采集或筛选的,删掉过模糊、重复、无目标的图,所以编号有跳跃,完全正常,不影响训练。

这里要注意一个细节:标注数量是4599个xml、4599个txt、4599个jpg,三者完全对齐,没有出现“有图无标注”或“有标注无图”的情况。这是这个数据集做得比较干净的地方。用labelImg画框的时候,只要点Save,就会自动生成同名xml;而yolo格式的txt通常是由转换脚本从xml生成的,所以数量天然一致。你可以用下面这段代码快速校验文件是否一一对齐:

import os from pathlib import Path img_dir = Path('images') # 假设jpg放在images目录 xml_dir = Path('xml') # 假设xml放在xml目录 txt_dir = Path('txt') # 假设txt放在txt目录 imgs = {p.stem for p in img_dir.glob('*.jpg')} xmls = {p.stem for p in xml_dir.glob('*.xml')} txts = {p.stem for p in txt_dir.glob('*.txt')} print(f'jpg: {len(imgs)}, xml: {len(xmls)}, txt: {len(txts)}') print('图片缺少xml:', len(imgs - xmls)) print('图片缺少txt:', len(imgs - txts)) print('多余xml:', len(xmls - imgs)) print('多余txt:', len(txts - imgs))

逻辑很简单:三个集合取差集,只要差集为空就说明文件一一对应。参数说明:glob('*.jpg')只匹配当前目录下的jpg,如果数据是子目录结构,需要改成recursive遍历。我一般会把jpg、xml、txt分开放到三个子目录,方便后续用脚本统一划分train/val。

2.2 标注统计与类别平衡:drowning 2578 vs swimming 3439,注意稀疏

摘要里写得很清楚:drowning框数2578,swimming框数3439,总框数6017。也就是说平均每张图有1.3个目标,大部分图里只有一个人在水里。这跟实际监控场景一致——泳池摄像头拍到的通常就是零星几个人,不会像COCO那样一张图一堆目标。

但类别不平衡是真实存在的:swimming比drowning多861个框,比例约1.33:1。对于二分类检测任务,这个不平衡不算极端,YOLO默认的BCEWithLogitsLoss本身能扛。但如果你只关注溺水报警,会发现模型天然偏向预测swimming,因为负样本(正常游泳)更多,把正常游泳判成溺水的代价在损失函数里没有额外惩罚。所以后面训练时要特别看drowning类的recall,必要时候给drowning类加权重。

另外注意,框数不等于人数。一个人在一张图里可能只标一个框,但这个人在不同帧里会出现多次,所以2578个drowning框不代表有2578个溺水者,这是视频抽帧数据集的正常形态。做评估时不要按“人数”理解,要按“目标实例”理解。

2.3 标注工具与规则:labelImg矩形框,贴合目标边界

使用必读里写了标注工具是labelImg,标注规则是“对类别进行画矩形框”。这是最常规的目标检测标注方式,没有分割多边形,也没有旋转框。所以这个数据集适合YOLO系列、SSD、Faster R-CNN这类基于水平矩形框的检测器,不适合需要旋转框的文本检测或者实例分割任务(虽然有xml,但xml里只有bndbox,没有polygon或mask)。

从firc_swim_316.xml这类文件里能看到典型的VOC结构:

<annotation> <folder>firc_swim</folder> <filename>firc_swim_316.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>drowning</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>812</xmin> <ymin>455</ymin> <xmax>1023</xmax> <ymax>851</ymax> </bndbox> </object> </annotation>

注意这个field的宽高是1920x1080,属于1080p监控画面。但也有可能其他图是1280x720或者更小,训练时要统一做letterbox resize,不要直接拉伸,否则会改变目标的长宽比,影响溺水姿态的判断。

标注规则的关键点:labelImg里用的“drowning”和“swimming”是英文文件夹命名,但实际标注的语义需要你自己理解。从日常经验看,“drowning”一般指头部没入水面、手臂乱挥、身体竖直下沉这类姿态;“swimming”指自由泳、蛙泳、踩水等正常活动。但标注本身是人画的,一定存在主观性。拿到数据后最好抽样看几十张图,确认一下框是否紧贴身体,还是包住整个人连带一圈水花。如果框偏大,训练出来的预测框IoU阈值要相应放宽;如果框偏紧,NMS的阈值可以适当调高。

3. VOC转YOLO的格式对照:xml的object信息如何映射到txt的class cx cy w h

3.1 VOC xml解析关键字段:filename、size、object

VOC格式的核心是xml里嵌套的object节点,每个object代表一个标注目标。你需要关注三个信息:size(图像宽高)、object.name(类别名)、object.bndbox(xmin/ymin/xmax/ymax)。filename字段只用于校验,不能作为读取图像的绝对路径,因为xml里存的是文件名,不是路径。

这里有个容易翻车的细节:bndbox坐标是整数像素值,单位是像素,不是归一化坐标。YOLO训练时要求坐标归一化到0~1之间,而且类别索引从0开始。所以drowning和swimming在yolo txt里对应的是0和1,不是“drowning=1, swimming=2”。这个顺序取决于你写的class.txt或yaml里的names列表,而不是xml里的顺序。

3.2 YOLO txt归一化坐标计算:从xmin/ymin/xmax/ymax到cx,cy,w,h

YOLO格式的txt每一行是:class cx cy w h。cx、cy是目标中心点的归一化坐标,w、h是目标宽高的归一化值。计算公式是:

cx = (xmin + xmax) / 2 / width cy = (ymin + ymax) / 2 / height w = (xmax - xmin) / width h = (ymax - ymin) / height

用Python实现批量转换非常直接:

import xml.etree.ElementTree as ET from pathlib import Path def convert_xml_to_yolo(xml_path, classes): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') width = int(size.find('width').text) height = int(size.find('height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in classes: continue # 跳过未定义类别 cls_id = classes.index(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) # 坐标裁剪,防止越界 xmin = max(0, min(xmin, width)) xmax = max(0, min(xmax, width)) ymin = max(0, min(ymin, height)) ymax = max(0, min(ymax, height)) cx = (xmin + xmax) / 2 / width cy = (ymin + ymax) / 2 / height w = (xmax - xmin) / width h = (ymax - ymin) / height lines.append(f'{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}') return lines classes = ['drowning', 'swimming'] xml_file = Path('firc_swim_316.xml') with open('firc_swim_316.txt', 'w') as f: f.write('\n'.join(convert_xml_to_yolo(xml_file, classes)))

参数说明:classes列表的顺序决定了txt里的类别索引,必须和后续训练yaml里的names保持一致。坐标裁剪那段很关键,因为有些标注框可能稍微超出图像边界,不裁剪会导致归一化坐标大于1,训练时YOLO会报错或导致loss变成nan。这里保留6位小数,精度足够。

3.3 数据划分:train/val/test比例建议与随机种子

4599张图不算大,建议按8:1:1划分train/val/test,也就是约3680张训练、460张验证、459张测试。划分时一定要按“摄像头/视频片段”分,不能纯随机打乱。因为同一段视频的连续帧高度相似,如果不做场景级别的划分,验证集会混入训练集的近邻帧,mAP会虚高。

我一般会先按文件名前缀或者采集时间分组,再把组划分到不同集合。但这份资源的文件名是firc_swim_编号,编号本身看起来是乱的,没有明显的时间或场景分组信息。稳妥的做法是先用随机种子打乱整个文件列表,再做划分,同时检查验证集里有没有和训练集重复编号的图片——这个数据集编号不连续,基本可以排除完全重复。

# 在数据集根目录执行 find . -name "*.jpg" | sort > all_images.txt shuf --random-source=<(yes 42) all_images.txt > shuffled.txt total=$(wc -l < shuffled.txt) train_n=$((total * 8 / 10)) val_n=$((total - train_n - total / 10)) head -n $train_n shuffled.txt > train.txt tail -n +$((train_n + 1)) shuffled.txt | head -n $val_n > val.txt tail -n +$((train_n + val_n + 1)) shuffled.txt > test.txt

参数说明:shuf的--random-source用固定种子42,保证可复现;train_n取整后,val和test按比例分配。注意这里只是生成了图片清单,实际训练时需要根据清单把图片和对应xml/txt拷贝到对应目录,或者写一个data.yaml用通配符。如果后续要跑YOLOv5,直接按yolo要求的images和labels目录结构组织最省事。

4. 训练配置与参数设定:用YOLOv5/v8跑通这个数据集的要点

4.1 数据集yaml文件写法:path、train、val、nc、names

拿到VOC+YOLO双格式后,最关键的一步是把数据组织成YOLO训练目录结构,然后写data.yaml。YOLOv5和YOLOv8的data.yaml格式基本一致,区别在于path字段的处理。以下是一个可直接用的配置:

# swim_data.yaml path: /your/path/firc_swim_dataset # 数据集根目录 train: images/train val: images/val test: images/test nc: 2 names: ['drowning', 'swimming']

这里的目录结构要求是根目录下images/train、images/val、images/test,以及labels/train、labels/val、labels/test。每张图的txt和jpg同名,放在对应的labels和images子目录里。参数说明:names的顺序必须和txt里类别索引一致,也就是说txt里0对应drowning,1对应swimming。如果names写反了,训练不会报错,但预测结果会完全颠倒,这是最隐蔽的坑。

如果你的训练脚本是YOLOv8,path字段可以写成绝对路径,train和val也可以直接写txt文件的路径列表,比如train: /path/train.txt,每行一个图片路径。YOLOv5也支持这种方式。

4.2 超参选择:img640、batch、epochs,以及针对小目标的建议

这个数据集原始分辨率是1920x1080,但游泳者在画面中占比不一定大,如果人是远景,目标可能只有几十乘几十像素。训练时img参数建议用640,不要盲目用320去提速——分辨率太低会把小目标直接抹掉。如果显存够,用img736或img1280会更好,但训练时长会成倍增加。

batch size根据显卡显存调整:RTX 3090 24G可以跑batch16(img640),V100 32G可以跑batch32。epochs建议从100起步,配合早停。由于数据量只有4599张,模型很容易过拟合,所以推荐在yaml里开启mosaic、mixup等增强,但训练最后20个epoch关闭mosaic,让模型适应真实分布。

具体训练命令(YOLOv8):

yolo detect train \ model=yolov8s.pt \ data=swim_data.yaml \ imgsz=640 \ batch=16 \ epochs=100 \ patience=20 \ device=0 \ save_dir=runs/swim_yolov8s

参数说明:model用yolov8s.pt作为预训练权重,比从头训练收敛快很多;patience=20表示验证集mAP连续20个epoch不提升就早停;save_dir指定输出目录,方便后面找best.pt和last.pt。如果你想更稳一点,可以用yolov5s.pt,两者的数据加载方式和增强策略略有差别,但对这个数据集的结论大体一致。

4.3 训练后验证:mAP、混淆矩阵、PR曲线解读

训练完别只看loss下降,要看验证集的mAP@0.5和mAP@0.5:0.95。对于溺水检测这个场景,mAP@0.5更实际,因为安全监控不要求框特别精确,只要报警框能锁定目标位置就行。

打开runs/swim_yolov8s/confusion_matrix.png,会看到一个2x2矩阵加背景类。重点关注这三项:

  • drowing被错分成swimming的比例:这代表溺水漏报,是最危险的错误。
  • swimming被错分成drowning的比例:这代表误报,会频繁触发假警报。
  • 背景被识别成目标的比例:误检,可能是水花、浮标等。

打开PR曲线,看drowning类的曲线面积。如果recall到0.8就急剧下降,说明某些溺水姿态没被充分学习。这时不要再盲目加epochs,先看是不是数据里有难例,或者类别权重不够。

from ultralytics import YOLO model = YOLO('runs/swim_yolov8s/weights/best.pt') results = model.val(data='swim_data.yaml', imgsz=640, batch=1) print(results.results_dict)

参数说明:results_dict里包含mAP相关指标。如果val时发现drowning的recall明显低于swimming,建议训练时给损失函数加类别权重。YOLOv8没有直接的class_weight参数,但可以通过修改数据集的label分布来缓解不平衡,比如对drowning类别做上下翻转、左右翻转、随机旋转等增强,增加它的样本量。

5. 避坑指南:标注一致性、格式陷阱与训练崩坏排查

5.1 现象:训练时报错 “class index out of range”

原因:txt里出现了大于等于nc的数字,或者类别名和yaml里的names对不上。这个数据集标注类别只有drowning和swimming,按理不应出错,但如果你用脚本转换时把classes顺序写错了,比如用了['swimming', 'drowning'],那么原xml里drowning的类别索引是1,但你的txt里存的可能是0,此时模型训练的语义就反了。

解决:写一个脚本扫描所有txt,打印出现的类别索引集合:

cat labels/train/*.txt | awk '{print $1}' | sort -u

正常应该只输出0和1。如果出现2或更大,说明转换时混入了没定义的类,或者txt文件是从别处拷贝错了。再用同样的方式扫描val和test。每年我都因为这个翻车,所以现在训练前必跑这条命令。

5.2 现象:训练前检查发现txt里有坐标大于1或小于0

原因:转换脚本没做裁剪,或者标注框本身越界。xml里的bndbox如果标注时手滑拖出了图像边界,就会出现xmax大于width的情况。虽然YOLOv5/v8的加载器会自动clip,但你会发现训练早期loss异常高,或者预测框明显偏移。

解决:用第3.2节的裁剪逻辑,转换时强制把坐标clip到[0, width]和[0, height]之间。写一个批处理脚本,遍历所有xml重新生成txt,覆盖不合法的行。血的教训:坐标系里的一个0.000001误差,在1920分辨率下就是2个像素,虽然不影响大局,但强迫症必须清理干净。

5.3 现象:xml和jpg文件名不匹配,训练时图片加载不到label

原因:解压后文件名编号有跳号,如果你用某些数据划分工具按序号自动重命名,就会把firc_swim_316和firc_swim_50这种编号错配。

解决:始终以jpg文件名为基准,xml和txt必须和jpg同名。批量重命名时用下面的脚本,先备份再动手:

cp -r firc_swim_dataset firc_swim_backup find firc_swim_dataset -name "*.xml" | while read f; do base="${f%.xml}" if [ ! -f "${base}.jpg" ]; then echo "Missing jpg for $base" fi done

5.4 现象:验证集mAP正常,但测试集上漏检远处的小目标

原因:分辨率与训练尺寸不匹配。1920x1080的图像缩放到640x640,相当于把整个画面压扁了,一个100x100像素的游泳者变成33x33,特征几乎消失。

解决:训练时用imgsz=1280,或者在推理时使用TTA(Test-Time Augmentation)。YOLOv8推理命令加augment=True可以多尺度投票,能提升小目标召回。如果显存不够,可以在预处理时把图像切成四块分别推理,再合并结果。这个方法对泳池监控特别有效,因为溺水者可能是远景小目标。

5.5 现象:drowning类的recall低,swimming类的precision也不稳定

原因:样本不平衡+标注主观性。两类之间的视觉差异有时不明显——一个正在游自由泳的人,手臂动作和溺水挣扎动作有相似帧;踩水姿态更接近垂直状态,容易被标成drowning或swimming。

解决:不要迷信评价指标。训练完抽样看预测结果,把模型预测错的图单独挑出来,人眼复核标注框是否合理。如果标注本身模糊,可以考虑只保留置信度高于0.5的样本用于训练,或者用投票机制:同一目标在连续帧中至少有3帧被判定为drowning,才触发报警。这个逻辑属于业务后处理,不是模型能解决的。

6. 进阶:数据增强与难例挖掘,把溺水检测的漏报率再压一截

6.1 针对小目标的马赛克与复制粘贴增强

默认的mosaic增强已经能提高泛化性,但对小目标增强有限。经典的SAHI(Slicing Aided Hyper Inference)思路是把大图切成小块推理,能直接提升小目标召回率。训练侧,我倾向在数据加载时做随机裁剪增强:随机从原图裁剪一个区域,缩放到640x640,再和原图混合训练,等效于让模型看到更近的视角。

# 伪代码:训练时动态添加裁剪增强 from PIL import Image def random_crop_and_resize(img, bboxes, crop_ratio=(0.3, 0.7)): w, h = img.size cw = int(w * random.uniform(*crop_ratio)) ch = int(h * random.uniform(*crop_ratio)) x = random.randint(0, w - cw) y = random.randint(0, h - ch) crop = img.crop((x, y, x + cw, y + ch)).resize((640, 640)) # 重新计算bbox坐标,裁剪掉完全在区域外的目标 new_boxes = [] for (cls, cx, cy, bw, bh) in bboxes: xmin = (cx - bw/2) * w ymin = (cy - bh/2) * h xmax = (cx + bw/2) * w ymax = (cy + bh/2) * h x1 = max(x, xmin) - x y1 = max(y, ymin) - y x2 = min(x + cw, xmax) - x y2 = min(y + ch, ymax) - y if x2 > x1 and y2 > y1: new_boxes.append([cls, (x1+x2)/(2*cw), (y1+y2)/(2*ch), (x2-x1)/cw, (y2-y1)/ch]) return crop, new_boxes

参数说明:crop_ratio控制裁剪区域占原图的比例,0.3~0.7让模型看到局部细节。新坐标计算必须基于裁剪区域重新归一化,否则训练会崩。这种增强对远景小目标特别有效,因为裁剪后目标在画面中的占比变大了。

6.2 用训练后的模型做难例挖掘,补充hard-mining数据

4599张图训练出的模型,漏检的往往不是普通场景,而是逆光、水花飞溅、身体半沉这些极端情况。我的做法是:先训练一个baseline,用它在所有训练图上推理,把置信度低于0.3的检测结果对应的图片挑出来,人工复核并重新标注成难例,加入训练集重训。

yolo predict model=runs/swim_yolov8s/weights/best.pt source=images/train save_txt=True conf=0.3

参数说明:conf=0.3设低阈值,是为了把更多不确定的预测暴露出来。然后去看labels目录里每个txt的置信度,筛选出所有预测框少于真实框数量的图片,以及预测类别与真实类别不一致的图片。这些就是模型的“盲区”。把盲区图片单独拷贝出来,用labelImg重新检查标注是否缺失或错误,修正后再合并到训练集里。这个过程迭代两三轮,drowning的recall通常能提升5~8个点。

6.3 一个必须养成的习惯:每次训练前都跑一遍格式校验脚本

这数据集本身质量不错,但你不能假设所有从网上下来的数据都这么规矩。我现在每次拿到新数据集,都会强制走一遍校验流程:先统计jpg/xml/txt数量是否一致,再扫描txt的类别索引是否越界,最后抽查10张图把框画出来人眼确认。

# validate_yolo.py import cv2 import random from pathlib import Path img_paths = list(Path('images/train').glob('*.jpg')) random.seed(0) samples = random.sample(img_paths, 10) for img_path in samples: txt_path = Path('labels/train') / (img_path.stem + '.txt') img = cv2.imread(str(img_path)) h, w = img.shape[:2] with open(txt_path) as f: for line in f: cls, cx, cy, bw, bh = map(float, line.split()) x1 = int((cx - bw/2) * w) y1 = int((cy - bh/2) * h) x2 = int((cx + bw/2) * w) y2 = int((cy + bh/2) * h) color = (0, 0, 255) if cls == 0 else (0, 255, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.imwrite('check_' + img_path.name, img)

参数说明:红色框是drowning,绿色框是swimming。跑完看一眼生成的check_*.jpg,如果发现框没贴目标、类别明显标反、或者框大小异常,就可以直接定位到具体文件。从那以后,我每次训练任何检测模型前都强制走一遍这10张可视化,不通过不准开训练。这套流程帮我挡掉过至少三次因为文本编码或格式错乱导致的训练翻车。

这份数据集本身的双格式对齐做得不错,但真正的功夫都在训练前的校验和训练后的难例挖掘上。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询