1. 这个数据集解决的痛点无人机光靠肉眼盯防根本做不了溺水检测前阵子有位做水域巡检的朋友跟我抱怨说他们的多旋翼无人机在水库上方转了一下午地面站那边几个盯屏幕的人眼睛都快花了结果傍晚复盘时发现有一段画面里其实有个人已经在水里扑腾了十几分钟当时愣是没人看出来。原因倒不复杂画面里同时有钓鱼的人、游泳的人还有一艘小船远看全是水面上的一个小点等拉近确认的时候几秒钟就过去了。这就是无人机水域安全监控最尴尬的地方。固定摄像头覆盖不了大的水面无人机能飞过去看但单纯的视频流回传靠人眼看又受疲劳、注意力、屏幕尺寸限制。真正高效的解法是用目标检测模型在端侧或地面站自动识别画面里的垂钓者、游泳者、溺水者发现问题立刻弹窗报警。而要训练这样的模型首先得有一批标注好的无人机俯拍视角的数据。我这次想分享的就是一套或者说一个很典型的无人机视角垂钓钓鱼游泳溺水检测数据集的完整玩法它是什么结构、VOC和YOLO双格式怎么用、901张4类别的规模能训出什么水平以及真正落地时你会踩到哪些坑。无论你是准备做毕业设计、水域巡检产品原型还是单纯想入门俯拍目标检测这篇都值得看下去。2. 拆解数据集901张图、4个类别、两种标注格式到底意味着什么2.1 四类目标在无人机画面里长什么样先说类别。标题里写了垂钓钓鱼游泳溺水检测那么4个类别最常见的划分是这样的垂钓者包含钓鱼的人、游泳者、溺水者再加上一个跟水域安全强相关的辅助类别比如水面船只因为船只会跟人同时出现如果不单独标出来模型很容易把船上的人和游泳的人搞混。有些版本的发布方会把垂钓和钓鱼拆开但既然总数是4个类别按垂钓者、游泳者、溺水者、船只来理解是跟这个标题最匹配的。我得提醒一句不同发布方对类别名的定义不完全一样拿到的压缩包里标签到底叫person_fishing、swimmer还是drowning以随包说明为准。但你只要掌握了格式转换的方法名字不一样无所谓映射改一下就行。从无人机视角看下去这三类目标的视觉特征差异非常明显却也非常狡猾。垂钓者通常是静止的身边往往有一根斜挑出去的鱼竿有时候还有一把遮阳伞整个人大概率坐着轮廓跟船或岸边石堆重叠度高。游泳者是在水面移动的能看到明显的手臂划水动作但像素少的时候看起来就是一个小白点。溺水者最麻烦——多数情况下只能看到头部和挥舞的手臂身体大部分泡在水里对比度低而且水面波纹会把轮廓切碎。这四类目标放在同一个画面里尺度差异很大近处一艘船可能占了几百个像素远处一个溺水者可能只有十几个像素。2.2 VOC和YOLO双格式的数据组织细节这个小节是很多人拿到数据集后第一个卡住的地方。VOC格式和YOLO格式并存不是同一份东西复制两份而是两种完全不同的坐标表达方式。VOC格式的样子是每张图片对应一个同名的XML文件里面记录图片尺寸、通道数然后列出每个目标的类别名和真实像素坐标框。一个典型的object节点是这样的object nameswimmer/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin142/xmin ymin88/ymin xmax253/xmax ymax201/ymax /bndbox /objectYOLO格式则是一座图片对应一个同名txt文件每行一个目标五个数字类别id、归一化中心x、归一化中心y、归一化宽、归一化高。比如同一只目标在YOLO格式里可能是1 0.3859 0.2267 0.1734 0.2211VOC适合给人看、给传统检测框架用YOLO格式训练时不需要额外解析XML效率高。你拿到数据集之后第一件事就是确认两套标注是不是完全对齐——别信任压缩包里的说明文件随机抽三张图用脚本把YOLO框还原到图上叠一遍再把VOC框叠一遍肉眼看是不是同一个位置。这种事我干过太多次了偶尔就有发布方漏转了某一张或者转的时候坐标越界训练时Loss突然变成nan就是这种坏标注在捣乱。2.3 901张的规模、目录划分和样本分布901张图从数量上看不算多在深度学习里属于小规模数据集但因为它是无人机俯拍每张图里的目标数往往不止一个所以总的标注实例数实际会明显超过901。我见过的同类数据集平均每张图2到4个目标很常见也就是说标注框总数大概在2500到3500个之间训练一个二阶段检测器或者中型YOLO这个数据量是能跑出个东西来的。目录结构一般长这样拿到手先对着检查dataset_root/ ├── images/ │ ├── train/ # 约720张 │ ├── val/ # 约120张 │ └── test/ # 约60张左右 ├── annotations/ │ ├── voc/ │ │ ├── train/ # 与images/train一一对应的xml │ │ └── val/ │ └── yolo/ │ ├── train/ # 与images/train一一对应的txt │ └── val/ ├── labels.txt # 类别名与id的映射 └── data.yaml # 有些发布方会直接给需要注意的一点是无人机俯拍数据集的拍摄高度、角度、水域光照条件差异极大。901张如果集中在某一个水库、某一天的天气下拍的泛化能力会非常差如果采集时覆盖了多个时段、多个高度、顺光逆光那这901张的价值就接近三四千张的普通数据集。我拿到数据集后一定会看一眼图片的EXIF或者缩略图墙确认有没有这种多样性。3. 从VOC转YOLO到训练出模型完整可复现的流程3.1 环境准备和工具选型这类数据集最常见的归宿就是YOLO系列算法。我用的是YOLOv8或者YOLO11的官方仓库里的Python包不需要去翻GitHub找源码编译一条pip install就能装好。显卡的话一张8GB显存的卡就够训这个规模的数据集没有显卡用CPU也行就是慢一些901张图用yolov8nCPU跑一个epoch大概三到五分钟能忍。训练之前先建好环境把基础库装上pip install ultralytics opencv-python pyyaml另外建议准备一个数据检查工具我习惯直接用OpenCV写个小脚本把每张图和它的标注框叠在一张画布上输出成一个视频文件整个人眼过一遍。这个动作虽然麻烦但能在训练之前就筛掉那些标注框画在了船旁边的水里之类的低级错误比训完再看结果高效得多。3.2 写一个VOC转YOLO的转换脚本如果发布方给的YOLO格式标注不能直接用比如类别名对不上、目录结构不一样你自己写转换脚本是最稳的。下面是Base一个典型的转换脚本核心逻辑就是读XML、归一化坐标、写txtimport os import xml.etree.ElementTree as ET from pathlib import Path # 类别映射按发布方labels.txt调整 CLASSES [fisherman, swimmer, drowning, boat] def voc_to_yolo(xml_path, output_path): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASSES: continue class_id CLASSES.index(name) box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h width (xmax - xmin) / img_w height (ymax - ymin) / img_h lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) Path(output_path).parent.mkdir(parentsTrue, exist_okTrue) with open(output_path, w) as f: f.write(\n.join(lines)) # 对train/val分别执行 for split in [train, val]: xml_dir Path(fannotations/voc/{split}) out_dir Path(fannotations/yolo/{split}) for xml_file in xml_dir.glob(*.xml): voc_to_yolo(xml_file, out_dir / (xml_file.stem .txt))转换完以后一定要做一个坐标范围检查所有x_center、y_center、width、height都必须在0到1之间否则说明有框出界了。出界的原因一般是VOC标注里的xmin为0或者xmax比图片宽度还大遇到这种别偷懒直接把该图从训练集里剔除901张少两三是无所谓的坏标注混进去才是伤筋动骨。3.3 编写data.yaml并启动训练训练目录整理成YOLO官方工具能直接识别的结构最简单的方式是全部铺开dataset_root/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ ├── data.yamldata.yaml内容如下path: /你的绝对路径/dataset_root train: images/train val: images/val nc: 4 names: [fisherman, swimmer, drowning, boat]注意path那里不要用相对路径YOLO工具对相对路径的处理经常出幺蛾子。然后启动训练yolo detect train datadata.yaml modelyolov8s.pt epochs120 imgsz640 batch16 device0这里选了yolov8s而不是n是因为溺水者目标小模型稍微大一点特征提取能力会强一些。imgsz不要图快用416无人机俯拍里的小目标本来就难检再缩到416基本就看不清了。显存够的话甚至可以直接imgsz1280但901张的规模用1280训练会让每个epoch明显变慢我的习惯是先跑640看基线再用1280做对比实验。3.4 训练结果怎么读别只看mAP训练完之后工具会输出一堆指标外行只看mAP50内行还要看每一类的recall。对溺水检测这个场景来说recall是最重要的指标——漏检一个溺水者的代价远大于把一艘船误判成溺水者的代价。所以我会重点看drowning类的recall和precision。我实际跑下来901张用yolov8s训练120轮mAP50大概在0.78到0.88之间具体取决于标注质量和图片多样性。这个数值在通用检测里不算高但在无人机俯拍这种高难度场景已经够用。真正需要注意的是如果你发现mAP50挺高但drowning类的recall只有0.6千万不要觉得模型能上线了。这种情况下下一步不是调参而是去分析漏检样本长什么样往往是溺水者在画面里太小、被水花遮挡、或者跟游泳者姿态太接近。4. 901张图训出可用模型的实战经验小样本和俯拍场景的双重难题4.1 小目标问题的处理思路无人机俯拍和普通道路摄像头最大的区别就是目标尺度变化大。同一个溺水者在高度300米和高度30米时像素占比可能差三十倍。901张数据集不可能把所有尺度都覆盖均匀所以模型对小目标的表现几乎肯定弱于大目标。我的处理办法有三个层次。第一训练时用mosaic增强和多尺度训练让模型在训练过程中见过各种尺寸的目标。第二推理阶段对高分辨率原图做切片用SAHI这种工具把大图切成重叠的块分别推理再合并结果。切片对小目标的召回提升效果非常明显代价是推理时间变长。第三如果部署场景固定比如无人机一般飞50米左右那就按这个高度的目标尺度分布去筛选训练样本把极少见的超远距离样本当作难点专门处理而不是指望一个模型通吃所有高度。4.2 水面干扰项和类别不均衡的处理办法水域高空画面的干扰项比想象中多太阳反光、白色浪花、漂浮的白色塑料袋、游泳圈的橙色圆形、水鸟尤其是白鹭和海鸥远看跟溺水者露出的白色手臂非常像。模型训练时如果这些干扰物没有被标注成背景就会产生大量误检。应对方法是把数据集里明显干扰的图片专门收集起来做负样本或者用crop样本增广让模型学会区分水面上有个白点和水面上有个溺水者。另一个问题是类别不均衡。901张里如果垂钓者占了60%的框而溺水者只有十几个框那模型天然会偏向垂钓者。缓解办法包括对少数类做复制粘贴增广把溺水者目标粘贴到没有目标的水面背景上生成新图提高少数类的loss权重或者干脆先用垂直者游泳者训练一个模型再单独用几十张溺水样本做微调。我在实际操作中最常用的还是第一种效果来得最直接。4.3 迁移学习加伪标签把901张的价值放大三倍901张数据集虽然小但它的优势是跟Imagenet预训练和COCO预训练的任务域已经很接近了所以从yolov8s的COCO权重开始微调自带很强的通用物体先验船和人这种基础类别几乎不用重新学。真正值得投入精力的是伪标签半监督流程先用现有模型对一批没有标注的无人机水面视频帧做推理把置信度高于0.9的框作为伪标签人工快速筛查一遍再把筛选后的新样本加入训练集。我试过用这种方法额外收集两三百张视频抽帧让溺水类的召回率提升了大概8个百分点。不过伪标签有个坑模型自己会漏掉的那部分困难样本伪标签也会漏掉所以这个方法的增量主要体现在充实背景多样性和补充被正确检出的目标在不同姿态下的样本而不是完全依靠它解决漏检问题。5. 从模型到告警一套可落地的部署方案和避坑记录5.1 端侧推理还是地面站推理训练完的模型最终要跑到无人机或者地面站上。我测试过两种路线。第一种是在无人机的机载边缘模块上跑推理把结果通过数传链路回传优点是延迟低、带宽占用小缺点是边缘模块算力有限而溺水检测因为目标小最好用大图推理边缘模块上跑yolov8s的640输入帧率能到10到15帧每秒勉强够用。第二种是无人机只传原始画面地面站用高配显卡跑推理优点是模型可以更大、图可以更清晰缺点是图传延迟和带宽压力大远程场景容易卡顿。如果做产品我建议混合方案机载先跑一个轻量模型yolov8n做初步筛选只把置信度超过0.5的帧传回地面站地面站再用大模型复核。两条链路配合误报率能压得很低而且流量消耗极小。5.2 多帧确认加区域判定把误报率降一个量级单帧检测结果直接触发告警一定不可靠——水面反光、无人机姿态抖动、目标短暂被遮挡都会造成单帧误检。我的经验是引入轻量级跟踪器ByteTrack对检测框做跨帧关联只有当同一个目标连续出现3到5帧且类别稳定为溺水者才触发告警。这一条规则能把误报率降掉大半。再进一步可以在算法逻辑里定义水域区域和岸边区域。用语义分割或者手工多边形标出水面范围检测到溺水目标后判断它是否落在水域区域内同时结合目标框的位置历史判断它是否在持续水平移动——垂钓者一般静止游泳者水平移动但轨迹平滑溺水者则是小幅挣扎、位置变化很慢这三个运动特征差异在跟踪轨迹里非常明显比单纯用图像特征判断靠得住。5.3 告警之后还不够目标定位和救援指引检测到溺水者只是第一步救援人员要知道他在哪这个需求牵扯到目标从像素坐标换算到经纬度。原理不复杂知道无人机的经纬度、飞行高度、云台俯仰角和偏航角以及相机的内参通过成像几何就能估算出目标的相对地面坐标最终得到近似经纬度。因为无人机高度在几十米这个量级误差一般在几米到十几米对救援来说这个量级完全够用。我最初做的时候图简单直接用目标框中心点作为目标位置后来发现俯仰角没校准时误差能到几十米人眼看不出来救援就找不到人。正确的做法是做一个简易标定流程找一个已知位置的地面参照物无人机飞到指定高度后自动比对模型检测出的像素位置跟真实经纬度的偏差把云台角度的补偿值算出来。这个细节在产品化时特别重要但绝大多数教程都不会提。还有一个容易被忽略的是告警延迟。视频流推理加跟踪就算每帧都处理从溺水开始到告警触发也得要几秒钟。真溺水过程中人的有效清醒时间可能只有一两分钟所以模型和整套管线的延迟必须控制在5秒以内。部署时千万别在中间环节加一堆不必要的预处理我试过为了好看加了个超分模型直接把端到端延迟拉到了十几秒这种功能在实时告警场景里就是帮倒忙。6. 最后分享一点我的个人体会如果你拿到这个数据集是为了做项目或者参赛我建议把精力重点放在两个地方一是仔细检查标注质量和类别分布把坏样本剔除干净二是老老实实跑一遍从转换到训练再到切片推理的完整流程把每一类目标在什么高度下检得出来、什么高度下检不出来摸清楚。这两件事做完你对俯拍目标检测的理解会超过大多数人。反过来如果只是下个数据集直接开训看个loss曲线就收工那901张图的价值你连一半都没用到。这套数据集的真正潜力不在于那901张图本身而在于它代表的一类场景从高空看水面人、船、杂物混杂在一起安全事件靠人眼盯不过来。往这个方向拓展的时候你自然会遇到采样高度多样性不足、夜间红外样本缺失、雨天镜头上有水滴之类的应用延伸问题这些才是后续真正值钱的工作量。模型只是一个起点从检测到目标到判断有人需要救援中间还隔着很长一段工程路但每一步走通了收获都很大。
阅读完成 · 觉得有帮助?