ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

游泳者溺水数据集VOC+YOLO格式2类别895张:从数据校验到YOLOv8训练全流程

游泳者溺水数据集VOC+YOLO格式2类别895张:从数据校验到YOLOv8训练全流程 简介这是一份面向计算机视觉开发者与溺水检测研究者的双类别目标检测数据集聚焦游泳者与溺水状态的识别任务可用于训练YOLO或VOC格式的检测模型适合具备一定深度学习基础、正在做安全监控或水域救援相关课题的读者。资源包共约2000个文件包含895张jpg图片、895个VOC格式xml标注文件与895个YOLO格式txt标注文件另有少量说明文本压缩包整体约315.71MB采用7z格式打包图片与标注一一对应可直接投入训练流程。数据集共标注swimmer与drowning两类其中swimmer框数1433、drowning框数97总框数1530均使用labelImg以矩形框方式标注图片截取自30段视频。目前已有1886人学习下载。需要提醒的是溺水状态本身较难界定作者建议下载后按自己的判定标准重新校正标注若仅需原始视频素材也可另行获取配套视频文件便于自行截帧扩充样本。1. 游泳者溺水数据集 VOCYOLO 格式 2 类别 895 张从拿到压缩包到跑通第一轮训练溺水检测这件事真正卡住大多数团队的从来不是模型结构而是数据。公开的溺水视频要么是影视片段要么是泳池监控里几秒钟的远景标注质量参差不齐类别定义更是各说各话。手上这份「游泳者溺水数据集 VOCYOLO 格式 2 类别 895 张」的价值就在这它把「游泳者」和「溺水者」拆成两个明确的目标类别同时给了 VOC 的 XML 和 YOLO 的 TXT 两套标注895 张图虽然不算大但足够把一条从数据校验、格式转换、训练到推理的链路完整跑通。适合谁做泳池安全监控、水域救援辅助、边缘端溺水预警的算法同学以及想拿一个真实二分类检测任务练手 YOLO 全流程的人。这一章先把这份数据集的边界讲清楚后面几章再一步步落到命令和参数上。2. 先看清这份数据集2 类别 895 张的构成与选型判断拿到一个数据集第一件事不是急着train.py而是把它拆开看。895 张图、2 个类别这个规模决定了它不可能直接训出一个泛化到所有水域的模型但它的定位很清晰——做一个特定场景泳池、静水区的溺水行为检测基线或者作为更大数据集的一个子集来用。理解它的构成才能判断要不要扩、怎么扩。2.1 两个类别到底怎么定义为什么这个定义会直接影响召回「游泳者」和「溺水者」这两个类别看起来简单实际标注时最容易出问题的是边界。一个正在挣扎、还没完全下沉的人算游泳者还是溺水者如果标注规范里没有明确「溺水」的判定标准比如头部持续低于水面、无规律扑腾、身体垂直下沉那 895 张里就会混进大量模棱两可的样本模型学到的决策边界是糊的。常见的做法是溺水者定义为「头部没入水面以下或反复没入、肢体动作失去推进规律」的帧游泳者则是「头部稳定在水面以上、有明确划水或漂浮姿态」。这份数据集既然给了 2 类别说明标注时已经做了这个切分。你要做的是抽样验证——随机抽 30 到 50 张把 XML 里的name字段和图像对照看标注框是否真的框住了人而不是框住了水花或泳道线。提示如果发现大量标注框把整个泳池区域都框进去或者框在了水面反光上这份数据集的可用性就要打问号先清洗再谈训练。2.2 VOC 与 YOLO 双格式并存先搞清目录结构和字段映射VOC 格式的核心是Annotations/*.xml每个 XML 里有filename、size、若干object每个object包含name、bndboxxmin/ymin/xmax/ymax。YOLO 格式则是labels/*.txt每行class_id x_center y_center width height全部归一化到 0~1。两套格式同时给好处是你不用自己写转换坏处是如果两套标注不一致你根本不知道以哪个为准。先做一次一致性抽查取同一张图读它的 XML 和 TXT把 VOC 的绝对坐标按图像宽高归一化和 TXT 里的值对比。误差超过 0.01 就说明两套标注不是同一次生成的得查来源。import xml.etree.ElementTree as ET import os def voc_to_yolo_check(xml_path, txt_path, img_w, img_h): tree ET.parse(xml_path) root tree.getroot() voc_boxes [] for obj in root.findall(object): name obj.find(name).text 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) # 转成 YOLO 归一化中心点格式 xc (xmin xmax) / 2.0 / img_w yc (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h voc_boxes.append((name, round(xc,4), round(yc,4), round(w,4), round(h,4))) with open(txt_path) as f: yolo_lines [l.strip().split() for l in f if l.strip()] return voc_boxes, yolo_lines # 调用示例传入一张图的 xml、txt 和图像尺寸 # voc_boxes, yolo_lines voc_to_yolo_check(Annotations/001.xml, labels/001.txt, 1920, 1080) # print(voc_boxes); print(yolo_lines)这段代码做的是「反向校验」把 VOC 的绝对坐标按图像宽高转成 YOLO 的归一化格式再和 TXT 里的行对比。参数上img_w和img_h必须从对应图像读取不能硬编码因为 895 张图可能来自不同分辨率的摄像头。如果发现 VOC 转出来的框和 TXT 对不上优先信 VOC因为 XML 里保留了原始像素坐标信息更完整。2.3 895 张的规模能训出什么什么时候必须做数据增强895 张、2 类别按 8:1:1 划分训练集大约 716 张。这个量级用 YOLOv8n 或 YOLOv5s 这种轻量模型从预训练权重微调是能收敛的。但要注意如果溺水者样本只有一两百张类别不平衡会直接导致模型偏向游泳者溺水召回率上不去。判断标准很简单统计两个类别的框数量。如果溺水者框数不到游泳者的三分之一就必须做增强——不是随便翻转而是针对溺水场景做亮度扰动、水面波纹模拟、局部遮挡。常见做法是用 Albumentations 加RandomBrightnessContrast、MotionBlur、CoarseDropout模拟水面反光和部分遮挡。增强只加在训练集验证集和测试集保持原样否则评估结果会虚高。3. 从 VOC 到 YOLO转换脚本、目录规范与三个必查项虽然数据集号称双格式但实际训练时你大概率还是要自己重新组织目录因为 YOLO 训练要求images/和labels/严格对应且类别 ID 从 0 开始连续。这一章把转换、划分、校验的完整流程写清楚照着做能避开大部分「训练时找不到标签」的坑。3.1 写一个可复用的 VOC 转 YOLO 脚本处理类别映射和边界裁剪转换的核心逻辑不复杂但有两个细节容易翻车一是类别名到 ID 的映射必须固定不能按文件顺序动态生成二是坐标要裁剪到 [0,1]因为标注时可能有人把框画到了图像外面。import xml.etree.ElementTree as ET import os import shutil # 固定类别映射顺序一旦确定不要改 CLASS_MAP {swimmer: 0, drowning: 1} def convert_voc_to_yolo(xml_dir, img_dir, out_img_dir, out_lbl_dir): os.makedirs(out_img_dir, exist_okTrue) os.makedirs(out_lbl_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, xml_file)) root tree.getroot() filename root.find(filename).text size root.find(size) img_w float(size.find(width).text) img_h float(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in CLASS_MAP: continue # 跳过未定义类别避免 ID 错乱 cls_id CLASS_MAP[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, img_w)) xmax max(0, min(xmax, img_w)) ymin max(0, min(ymin, img_h)) ymax max(0, min(ymax, img_h)) if xmax xmin or ymax ymin: continue # 无效框直接丢弃 xc (xmin xmax) / 2.0 / img_w yc (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}) # 写 label txt_name os.path.splitext(xml_file)[0] .txt with open(os.path.join(out_lbl_dir, txt_name), w) as f: f.write(\n.join(lines)) # 拷贝图像 src_img os.path.join(img_dir, filename) if os.path.exists(src_img): shutil.copy(src_img, os.path.join(out_img_dir, filename)) # convert_voc_to_yolo(Annotations, JPEGImages, dataset/images, dataset/labels)参数说明CLASS_MAP必须和后续训练配置里的names顺序完全一致否则模型学到的类别会错位。坐标裁剪那几行是血泪经验——有些标注框的 xmax 会超过图像宽度不裁剪的话 YOLO 训练时虽然不报错但归一化值大于 1会导致损失异常。if xmax xmin的判断是丢弃退化框防止出现宽高为 0 的无效样本。3.2 按 8:1:1 划分并生成 train/val/test 列表注意同名文件冲突划分不能随便random.shuffle就完事要保证同一段视频的帧不会同时出现在训练集和验证集否则验证指标会虚高。如果数据集里图像文件名带视频编号比如video01_frame023.jpg就按视频编号分组划分如果没有至少用固定随机种子保证可复现。# 假设所有图像在 dataset/images标签在 dataset/labels ls dataset/images | shuf --random-source(yes 42) | head -n 716 train.txt ls dataset/images | shuf --random-source(yes 42) | tail -n 717 | head -n 90 val.txt ls dataset/images | shuf --random-source(yes 42) | tail -n 807 test.txt这里用--random-source固定随机源保证每次划分结果一致。train.txt里写的是图像路径YOLO 训练时会自动把images替换成labels去找同名 TXT。注意如果图像和标签不在同一级目录要在配置里显式指定labels路径否则会报「No labels found」。3.3 训练前必查的三项类别分布、标注越界、图像可读性在启动训练之前花十分钟跑一个校验脚本能省掉后面几小时的无效等待。三项检查第一统计每个类别的框数量确认没有某个类别为 0第二检查所有 TXT 里的归一化值是否都在 [0,1] 区间第三用 OpenCV 逐张imread确认没有损坏图像。import os, cv2 def validate_dataset(img_dir, lbl_dir): cls_count {} bad_coord [] bad_img [] for txt in os.listdir(lbl_dir): with open(os.path.join(lbl_dir, txt)) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cid int(parts[0]) cls_count[cid] cls_count.get(cid, 0) 1 vals [float(v) for v in parts[1:]] if any(v 0 or v 1 for v in vals): bad_coord.append(txt) break for img in os.listdir(img_dir): if cv2.imread(os.path.join(img_dir, img)) is None: bad_img.append(img) print(类别分布:, cls_count) print(坐标越界文件:, bad_coord[:10]) print(损坏图像:, bad_img[:10]) # validate_dataset(dataset/images, dataset/labels)输出里如果cls_count只有{0: xxx}说明溺水者类别没转进来回头查CLASS_MAP的键名是否和 XML 里的name完全一致大小写、空格都算。坐标越界和损坏图像的数量如果超过总数的 2%建议先清洗再训练。4. 用 YOLOv8 跑通第一轮训练配置、参数与显存控制数据准备好了接下来是训练。这一章以 YOLOv8 为例因为它的 CLI 和 Python API 都比较直接适合快速验证数据集是否可用。如果你用 YOLOv5流程类似只是配置文件格式略有差异。4.1 写一份最小 data.yaml把路径和类别名对齐YOLOv8 训练依赖一个 YAML 文件描述数据路径和类别。路径可以是绝对路径也可以是相对路径但相对路径的基准是「运行命令时的当前目录」不是 YAML 文件所在目录这点很容易搞混。# data.yaml path: /home/user/drowning_dataset # 数据集根目录 train: train.txt # 相对于 path 的训练列表 val: val.txt test: test.txt names: 0: swimmer 1: drowningnames的顺序必须和转换脚本里的CLASS_MAP完全一致。如果这里写反了模型会把溺水者学成游泳者推理时类别标签全错。path建议用绝对路径避免因为工作目录变化导致找不到文件。4.2 启动训练batch、imgsz、epochs 三个参数怎么定第一轮训练的目标不是刷指标而是确认整条链路能跑通、损失能下降。所以参数可以保守一点。yolo detect train \ datadata.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ device0 \ projectruns/drowning \ nameexp1 \ patience20 \ seed42参数逐个说modelyolov8n.pt用预训练权重895 张图从零训几乎不可能收敛imgsz640是 YOLO 的默认输入尺寸如果你的图像分辨率很高比如 1920×1080可以试 960但显存占用会明显上升batch16在 8GB 显存上跑 640 尺寸基本安全如果 OOM 就降到 8patience20表示 20 轮验证指标不提升就早停避免过拟合seed42固定随机种子保证可复现。注意如果训练一开始 loss 就是 nan先检查 data.yaml 里的路径是否正确再检查 labels 里是否有空文件或格式错误的行。空 TXT 文件会被当成「无目标」样本少量可以大量会导致模型学不到东西。4.3 看训练日志怎么判断是正常收敛还是已经翻车训练启动后重点看几个指标box_loss和cls_loss应该在前 10 个 epoch 内明显下降如果一直平着不动说明学习率不对或者数据有问题mAP50在 50 个 epoch 左右应该有一个可见的上升如果始终低于 0.1大概率是类别映射错了或者标注框质量太差。另一个容易忽略的是val/box_loss和train/box_loss的差距。如果训练 loss 持续下降但验证 loss 从某个 epoch 开始上升就是过拟合这时候要么加增强要么减模型容量要么早停。895 张图训 YOLOv8n正常情况 mAP50 能到 0.6~0.8 就算不错如果只有 0.2 以下先别调模型回头查数据。5. 避坑与排查溺水数据集训练中最容易翻车的 5 个点这一章列的每一条都是实际做溺水检测时踩过的按「现象 → 原因 → 解决」写方便你对号入座。现象一训练时提示「No labels found」但 labels 目录里明明有 TXT。原因YOLO 默认按images替换成labels去找标签如果你的目录结构不是images/和labels/平级或者 TXT 文件名和图像名不完全一致比如图像是.jpgTXT 是.JPG.txt就会找不到。 解决统一用「图像文件名去扩展名 .txt」命名目录结构保持dataset/images/和dataset/labels/平级。如果必须用自定义结构在 data.yaml 里显式写labels: path/to/labels。现象二训练 loss 正常下降但推理时所有框都标成 swimmerdrowning 一个都检不出来。原因类别不平衡。如果 drowning 的框数远少于 swimmer模型会倾向于把所有目标预测成多数类。 解决先统计两类框数如果比例超过 3:1对 drowning 做过采样或加增强也可以在损失里给 drowning 更高的权重YOLOv8 支持通过cls_pw调整但更稳妥的还是补数据。现象三验证集 mAP 很高但拿真实泳池监控视频一跑全是误检。原因训练集和真实场景的域差异。895 张图可能来自特定角度、特定光照而真实监控是另一个视角。 解决在验证集里加入至少 20% 来自目标场景的图哪怕不训练也要用来评估。如果差异太大先做一轮无监督的域适应或者直接补标目标场景的数据。现象四训练到一半突然 OOM之前都正常。原因YOLOv8 在训练过程中会做多尺度验证某些 batch 的图像尺寸变大显存峰值上升。 解决把batch降一档或者设置rectTrue开启矩形训练减少 padding 带来的显存浪费。如果还不行把imgsz从 640 降到 512。现象五转换后的 TXT 里出现0 0.5 0.5 0.0 0.0这种宽高为 0 的行。原因VOC 标注里存在退化框xmin 等于 xmax 或 ymin 等于 ymax。 解决在转换脚本里加判断宽或高小于 1 像素的框直接丢弃。这种框不丢训练时虽然不报错但会拉低定位精度。6. 把 895 张用出更大价值小数据集的验证技巧与迭代习惯895 张图训一个二分类检测器指标上限是肉眼可见的。但这份数据集真正的价值不在「训一个能上线的模型」而在于它是一个干净的、双格式的、类别定义明确的起点。我一般会用它做三件事第一验证新的数据增强策略是否有效因为数据量小增强带来的差异很容易观察到第二作为预训练的一个中间阶段先在溺水数据上微调再迁移到更大的水域检测任务第三用来测试推理管线的鲁棒性比如把模型导出 ONNX 后用 OpenCV DNN 跑看前后处理是否对齐。一个具体的技巧用yolo detect val生成混淆矩阵重点看 drowning 被误判成 swimmer 的比例。如果这个比例超过 20%说明两个类别的特征在模型眼里太接近这时候不要急着加数据先检查标注——大概率是部分 drowning 样本标得不够「溺水」模型学到的边界是模糊的。我自己的习惯是每轮训练后都把误检最高的 10 张图挑出来逐张看标注往往能发现一批需要重标的样本。这个回头看的动作比调参有用得多。另一个习惯永远保留一份「从未参与训练」的测试集哪怕只有 50 张。895 张里划出 50 张做测试剩下的再分训练和验证。测试集只在最终评估时用一次平时不碰。这样你得到的指标才是可信的不会因为反复在验证集上调参而虚高。溺水检测这件事漏检的代价远大于误检所以评估时优先看 drowning 的召回率而不是整体 mAP。希望帮到你。本文还有配套的精品资源点击获取
返回列表