☰
无人机河道垃圾检测数据集与YOLO实战:从标注到部署全解析
2026/10/20 6:44:58 网站建设 项目流程

无人机巡河这几年特别火,但真正落地时卡住的往往不是飞行平台,而是“AI到底能不能认出水面垃圾”这个核心问题。水面不像路面,目标小、背景杂、反光强,很多通用目标检测模型在河道场景直接翻车。我做这个数据集的目标很明确:把无人机视角下的河道垃圾检测做成一个可以直接训练、复现、落地的项目,数据集中包含瓶子、树枝、易拉罐、塑料等常见水面漂浮物,统一合并为1个类别——垃圾。总共2247张图像,标注文件同时提供VOC格式(XML)和YOLO格式(TXT),开箱即用,不用再折腾格式转换。

这2247张图全部来自无人机航拍视角,覆盖不同河道宽度、不同光照条件、不同飞行高度下的垃圾分布情况。之所以归为单类别,是因为在实际巡河场景里,第一步需要回答的问题是“这帧画面里有没有垃圾、在哪里”,至于具体是塑料瓶还是易拉罐,那是后续垃圾分类和处理环节才需要关心的。先解决“能不能稳定检测到”这个主要矛盾,再考虑细粒度分类,会更务实。这篇文章我打算从数据集的设计逻辑、制作流程、格式转换原理、训练实战、踩坑记录到排查技巧,完整拆一遍,不管是拿来做毕业设计、比赛,还是直接部署到无人机上做实时巡河监测,都能从中拿到可直接用的经验。

1. 项目背景与核心设计思路

1.1 为什么河道垃圾检测要做成单类别

很多人一看到“检测”两个字就直接往多类别方向做,觉得瓶子、易拉罐、树枝、塑料各分一类才完整。但实际做过无人机巡河项目的人都知道,多类别在真实水面场景下会把自己坑死。

水面漂浮物的形态差异极大,一个半浸在水里的塑料瓶和一个漂浮的白色泡沫块,在俯视视角下几乎是同一类东西。树枝更夸张,有时候横在河边的一根枯枝,和一根废弃塑料管从视觉上根本分不开。如果强行分成多个类,标注标准会变得极其主观,不同的标注人员给同一个目标打的类别标签可能都不一样,模型训练出来之后,类别之间的判别边界也是一团浆糊。与其这样,不如把所有目标统一标注成“垃圾”,先把检测框架稳定跑起来,后面有需要再在检测结果上接一个分类头或单独的分类模型。

单类别还有一个实际好处:标注成本大幅降低。标注人员不需要花大量时间讨论“这到底是瓶还是罐”,只要确认是垃圾就标一个框。2247张图总共标注了数万个目标框,如果分成多类别,同样的图至少要多花一倍的时间去核对类别准确性。

实测下来,单类别的模型训练收敛速度明显更快,mAP容易从第一天开始就稳步爬升,因为类别间不存在混淆损失。对于初期建数据集、验证检测方案是否可行,这是最稳妥的路径选择。

1.2 无人机视角与地面视角的核心差异

市面上公开的垃圾检测数据集大多是地面手持设备拍摄的,摄像头高度通常在1到2米,视角基本平行于目标。这种数据训练出来的模型,遇到无人机俯视视角时性能会断崖式下跌,原因是两种视角下目标的几何形态差异太大。

无人机在30米到80米高度拍摄时,视角接近正射投影,目标的外形轮廓会压缩变形。一个立着的塑料瓶在地面视角是一个高瘦的矩形,但从无人机看下去可能只是一个接近圆形的白点。目标的边缘信息、纹理细节、阴影方向完全变了,模型在训练时如果没见过这种视角下的样本,推理时误检漏检就在所难免。

另一个差异是尺寸分布。地面视角下物体在画面中占比大,通常占图像宽高的百分之二三十;而无人机视角下的垃圾,哪怕飞得比较低,在4K画面中也经常只占几十个像素。这导致基于通用目标检测框架训练后,小目标检测能力成为河道场景的关键瓶颈。因此这个数据集刻意保留了大量小目标样本,希望模型在训练时就适应这种目标稀疏、像素占比低的水面场景。

1.3 VOC+YOLO双格式:为什么不只给一种

我见过很多数据集只提供VOC格式或只提供YOLO格式,用的时候总会有人问“有没有转好的YOLO txt”。VOC格式是标注工具的通用产出格式,结构好读,但训练时主流检测框架普遍用的是YOLO格式的归一化坐标。反过来,YOLO的txt文件可读性差,想人工检查错误就很费劲。

所以这个数据集在发布时我自己写了一个转换脚本,一次性把XML标注统一转成YOLO格式,并且做了一轮人工抽检后才发出去。VOC格式留给你做标注审查、目标分布分析,YOLO格式直接喂给模型训练,各有各的用途。

转换的核心逻辑是把VOC里的绝对像素坐标(xmin、ymin、xmax、ymax)换算成YOLO需要的相对坐标(中心点x、中心点y、宽度w、高度h),四个值全部归一化到0到1之间。计算过程不复杂,但边界情况很多,比如坐标越界、宽高为0、类名拼写不一致,这些坑我下面会详细讲。

2. 数据集的完整制作流程

2.1 数据采集阶段的场景规划

数据采集是整个流程中最耗时、最依赖现场条件的一环。无人机航拍不像爬虫下载图片那样被动地拿数据,飞行高度、云台角度、航线路径每一样都会直接影响数据分布。

我采用的方案是沿河道两岸规划航线,让无人机在河道中轴线上飞行,云台角度调整为垂直向下和略微俯视两种状态交叉采集。垂直视角适合检测河面中央的垃圾,略微俯视的视角可以看到河岸边缘、岸边草丛里的垃圾,后者的目标形态与开阔水域有差异,如果漏掉会造成模型在岸线场景的检测能力缺失。

飞行高度我分成三档:低空15到20米、中空30到40米、高空60到80米。这个高度覆盖了从大尺度瓶子到极小漂浮物的各种成像尺寸。实测中,高空图像里一个易拉罐的像素尺寸有时只有8×8像素,如果不刻意保留这些样本,整个数据集的尺度多样性会不够,训练出来的模型对远距离目标就只能靠猜。

采集时间选择上午和下午两个时段,尽量避免正午强光直射水面产生的全反射。不过即便这样,部分航段还是难免拍到反光严重的画面。这类画面我没有直接丢掉,保留了一部分作为困难样本,因为实际巡河不会因为反光就不飞。

2.2 数据清洗与筛选的取舍标准

原始视频抽帧得到的图像可能有上万张,但真正进入数据集的只有2247张,大量画面在筛选阶段被淘汰。筛选标准是按“清晰度、目标是否可见、画面是否有严重遮挡”三个维度逐帧评估。

清晰度是第一道关。无人机飞行时存在机身振动和运动模糊,尤其是低空高速飞行时快门速度不够,画面会出现拖影。这类模糊帧如果直接标注,目标边界是虚的,标注出来的框质量受到影响。我设置了一个简易判断方法:把画面放大到100%,如果目标边缘轮廓能看出清晰的像素级变化就保留,否则弃用。

目标可见性是第二道关。有些画面中垃圾目标被水花覆盖、被桥梁阴影压暗、或被岸边的树枝遮挡了一半以上,这类目标标注出来也会让模型学得混乱,统一剔除。还有一部分画面是高空飞行时整段河道都没有垃圾,这种负样本我少量保留了一些,大概几十张,用来降低误检率。但占比控制得很小,过多会影响正样本的学习。

第三道关是画面本身的可用性。比如无人机桨翼入镜、镜头上有水雾、画面严重过曝造成水面颜色完全丢失,这些都不能要。筛选工作量说起来不大,但实际操作时看几千张图还是很费眼睛的。

2.3 标注规范:单类目标一样有讲究

单类别标注看起来比多类别简单,但标注规范如果不提前定好,后期标注质量照样失控。我用的标注工具还是常规的LabelImg,标出来的就是VOC的XML文件。

标注框的贴合度是需要抠的一个细节。物体是倾斜的、部分浸在水里的,标注框要不要完全包住沉没在水下的部分?我的规范是只包住水面以上可见的区域。水下部分轮廓受水面折射影响是变形的,框进去会影响模型对目标边界的拟合。树枝这类细长目标尤其要注意,标注框的宽度必须贴合枝干的实际占位,不能为了省事把整根树枝横向拉通成一个巨大矩形,那样训练出来的预测框定位就不准确。

遮挡重叠的情况也有讲究。两个瓶子叠在一起,如果分不清前后边缘,就标成一个框;明确能看到分开边界的,则分别标注。小目标的处理上,我的经验是如果目标在画面中长边小于10个像素,就标注成一个点框(宽和高都记为目标的大致尺寸),而不是不标,这样模型在小目标区域能学到一些响应特征,比空白区域强。

标注完成后,我随机抽了100张图做二次审查,主要检查漏标和框偏移。实际情况是,即使标注很仔细,依然有接近2%的目标存在不同程度的框偏移,这类问题不人工复核很难发现。

3. 标注格式转换与质量校验

3.1 VOC转YOLO的原理与脚本实现

VOC格式和YOLO格式本质上的区别只有一点:坐标表达方式。VOC记录的是矩形框左上角和右下角的绝对像素坐标,YOLO记录的是中心点和宽高的归一化相对坐标。

转换脚本的核心逻辑是先解析XML文件里的width和height字段,读取每个object的bndbox坐标,再套用四个公式:

x_center = (xmin + xmax) / 2 / width y_center = (ymin + ymax) / 2 / height box_width = (xmax - xmin) / width box_height = (ymax - ymin) / height

我之前用的Python脚本如下,代码本身不复杂,但使用时有几个容易忽视的细节需要单独说明。

import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_path, class_list): tree = ET.parse(xml_path) root = tree.getroot() width = int(root.find("size/width").text) height = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in class_list: continue cls_id = class_list.index(name) bndbox = obj.find("bndbox") xmin = float(bndbox.find("xmin").text) ymin = float(bndbox.find("ymin").text) xmax = float(bndbox.find("xmax").text) ymax = float(bndbox.find("ymax").text) xmin = max(0.0, min(xmin, width)) xmax = max(0.0, min(xmax, width)) ymin = max(0.0, min(ymin, height)) ymax = max(0.0, min(ymax, height)) if xmax <= xmin or ymax <= ymin: continue x_center = (xmin + xmax) / 2.0 / width y_center = (ymin + ymax) / 2.0 / height box_w = (xmax - xmin) / width box_h = (ymax - ymin) / height x_center = max(0.0001, min(0.9999, x_center)) y_center = max(0.0001, min(0.9999, y_center)) box_w = max(0.0001, min(0.9999, box_w)) box_h = max(0.0001, min(0.9999, box_h)) lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") with open(out_path, "w", encoding="utf-8") as f: f.write("\n".join(lines))

这里有两个细节值得强调。一是边界钳制。无人机原始图像偶尔会出现目标框轻微越界的情况,如果不做截断,转出来的YOLO坐标可能出现大于1或小于0的异常值,训练框架读到之后要么直接报错,要么产生无法解释的loss尖峰。二是指派class_list时类名空格可能不匹配,最好在转换前统一对类名做strip处理。

3.2 格式校验的三个维度

格式转换完成后不能直接拿去训练,要先做一轮校验。我通常从三个维度检查。

第一是标签数量是否与标注目标数量一致。写个简单的脚本统计每个XML里的object数量,对比对应txt文件的行数,很容易发现漏转换或重复转换的问题。

第二是坐标合理性。把txt文件内容读出来,检查类别ID是否都在允许范围内,坐标值是否都在0到1之间,宽高是否不为0。这些看起来微不足道的异常项,实际操作中出现的概率远比你想象的高。

第三是可视化回读。我把YOLO格式的txt坐标画回原图上,叠加上边界框检查是否和VOC标注对齐。这一步最耗时,但也是最能发现问题的。我在做校验时发现过一些极端情况,比如VOC坐标本身没错,但XML里读取出来的width字段和JPG图片真实尺寸不一致,导致转换后的框整体偏移。这类问题如果只靠统计维度是发现不了的。

3.3 目录规范化与数据划分

数据集拿到手之后,第一件事是规范化目录结构,而不是急着开训练脚本。我用的是YOLO框架的通用的目录组织方式,简单直接。

river_debris/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── debris.yaml └── README.md

数据划分比例是7:2:1,train、val、test分别在三个目录里,对应的标签文件也同步放置。这里要提醒的是:划分时要保证同一个视频序列抽出的帧尽量分在同一个子集里,不能上一帧在train下一帧在val。因为相邻帧高度相似,如果被拆散到不同子集,验证集loss会显得格外低,但这只是数据泄漏的错觉,在真实新场景里模型的性能并没有你看到的那么好。

划分方法我建议按视频片段分组。先获取每张图片所属的视频文件名,以视频片段为最小单位做数据划分,而不是以单张图为单位。

4. 基于该数据集的YOLO实战训练

4.1 环境搭建与数据配置

拿到数据集之后,我用的是YOLOv8的训练流程,因为Ultralytics提供的训练接口足够简洁,导出部署也很方便。环境安装不复杂,只需要torch和ultralytics两个核心依赖,但GPU驱动和CUDA版本要提前确认匹配,这一步遇到问题的人最多。

pip install ultralytics

数据配置文件是训练的第一步。debris.yaml的内容很直接,路径用的是绝对路径,避免因为相对路径导致的找不到图片或者找不到标签的问题。

path: /data/river_debris train: images/train val: images/val test: images/test nc: 1 names: 0: debris

有个容易被忽略的点是:这里指定的train和val是相对于path的路径,Ultralytics会自动去拼接。如果你把path写成相对路径,之后换工作目录再跑训练脚本,路径就失效了。写绝对路径虽然不利于项目移植,但在单机训练场景下是最稳妥的。

4.2 训练参数的选取思路

训练参数我调过几轮,最终选用的是一组兼顾精度和速度的配置。

from ultralytics import YOLO model = YOLO("yolov8n.pt") results = model.train( data="debris.yaml", epochs=300, imgsz=640, batch=16, patience=30, lr0=0.01, optimizer="SGD", project="river_debris", name="train_exp" )

模型权重选择的是yolov8n,也就是nano版本。河道垃圾检测这个任务对算力约束比较大,很多实际部署环境用的是机载计算平台或者边缘设备,nano版本参数量小,在边缘平台可以跑到实时。如果算力够,换成yolov8s或yolov8m会在mAP上有些提升,但幅度不会特别大,因为单类别任务相对简单,模型容量不是瓶颈。

imgsz选择640是最均衡的值。无人机原始图像分辨率非常高,直接在原始分辨率上训练,显存会爆炸。640×640的输入尺寸下,原本在高清原图里只有几十个像素的垃圾目标,在缩放后会变得更小。这也是为什么这个数据集的mAP值在普通目标检测数据集面前不会特别亮眼,因为大部分人拿过来的模型对小目标天生不敏感。

epochs开到300配合patience=30做早停。我从实际训练曲线来看,模型在180到220个epoch之间基本收敛,早停机制会在这个区间自动截断,不需要人工盯着看。

4.3 推理部署与格式转换

模型训练完成后,最关键的一步是把训练好的pt权重导出成部署格式。我同时导出了ONNX和TensorRT的engine文件。TensorRT在边缘设备上的推理速度比原始PyTorch模型快3倍以上,是实际部署时的首选。

yolo export model=best.pt format=onnx imgsz=640 yolo export model=best.pt format=engine device=0

导出Engine时要注意输入尺寸必须与训练时一致。如果训练用的是640,导出时也强制用640,否则TensorRT的输入绑定会不匹配。实测下来,TensorRT在浮点推理下检测整段河道视频实时性足够用。

如果要做更高分辨率的推理,比如想直接在1920×1080帧面上检测而不损失小目标细节,不能简单地把imgsz改成1920,那样显存直接爆炸。正确做法是保持640推理,对高分辨率大图做切片推理:把大图切分成若干640×640的重叠切片,分别检测,再用NMS合并重叠区域的检测结果。这个方法在河道巡河场景里非常实用,代价是推理时间线性增加,但换来了小目标召回率的显著提升。

5. 无人机河道垃圾检测的难点与踩坑记录

5.1 小目标漏检:易拉罐与塑料瓶的区别对待

模型训练完成之后,在测试集上跑了一轮,发现漏检集中在两类目标上,一类是远处小尺寸的易拉罐,一类是颜色和水面接近的半透明塑料瓶。

易拉罐的问题是典型的小目标问题。在80米飞行高度下,易拉罐在画面中不到20个像素,YOLO的检测头在640×640输入下对其中的小目标响应本来就弱。解决这个问题的思路不是换模型,而是改变推理策略——切片推理、多尺度推理、加高分辨率输入,这些组合起来效果明显。

塑料瓶的问题则出在颜色和对比度上。半透明的矿泉水瓶在平静水面背景下几乎没有边缘梯度,人眼都要仔细看,模型更难分。我在后续版本的数据采集中有意识地增加了这类低对比度样本的比例,模型对它们的召回率才慢慢上来。

5.2 形态相近的垃圾类目相互干扰

虽然是单类别,但内部目标形态差异依然巨大。树枝和塑料管在外观上具有相似的细长形状,模型在初始训练阶段会把它们视为同一特征群,这原本没有问题。但当树枝处于岸边树影区域时,特征提取容易受到干扰,出现误检。解决的办法主要有两条:一是增加目标形态多样性的样本,让模型见过的细长目标足够多;二是调整损失函数的正负样本分配策略,低置信度预测框的阈值不要设置太高。

5.3 水面反光与阴影下的标注训练策略

水面反光是对检测性能影响最大的干扰项。阳光直射下,水面会出现大片高亮区域,有时亮度接近纯白,垃圾目标在这个区域里的信号被完全淹没。我保留了大约5%这样的反光样本作为训练数据,模型在高亮区域的漏检有一点改善,但最终真正起作用的是图像预处理阶段的对比度增强和亮度平衡。

阴影则相反。河道两侧的山体、桥梁、树木在水面形成深色阴影区域,深色垃圾(比如黑色塑料、暗色树枝)在阴影里几乎不可见。这类目标在标注时就需要很大的耐心,因为肉眼识别都困难。我的建议是:这类样本宁可少标,也不要乱标。标注一个错误的目标框,对模型训练的负面影响远大于漏标一个真实目标。因为漏标只是少一个正样本,错标却会引入一个虚假的监督信号,模型会去拟合一个本来不存在的目标。

6. 常见问题排查与效率提升技巧

6.1 数据集使用中的典型问题速查

训练过程中,我汇总了一套高频问题的排查表,整理成表格方便对照使用。

问题现象可能原因排查与解法
训练启动时报label shape不匹配有txt文件为空或行数据格式错误检查对应txt的坐标值和类别ID
部分图片没有被训练使用图片与标签文件名不一致确认jpg与txt在同一级目录且文件名严格一致
验证集loss偏低但实际效果差数据划分按单帧切分导致数据泄漏按视频片段分组重新划分
训练过程中loss出现NaN标注框异常、精度溢出检查PMC建议的坐标钳制逻辑是否有遗漏
模型对特定尺寸目标不敏感数据增强过于激进去除了小目标关闭moscia等强增强项或用小目标增强策略
同一目标被输出多个检测框NMS阈值设置不适合场景调低IoU阈值或调整置信度阈值再NMS

这里面最常遇到的是文件不匹配问题。2247张图对应2247个标签文件,但有一张图的标签文件名后缀是.txt却写成了.jpg.png这样的错误后缀,导致那一个文件的标签一直没被读取。这类问题靠人工肉眼翻几百个文件很不现实,写脚本批量校验才是正解。

6.2 提升标注效率的实用方法

如果要从头做一个类似的数据集,或者在现有数据集上补充数据,标注效率直接决定项目能不能按时完成。我用过几种提升效率的组合策略,整体效果不错。

一个是用预标注加人工修订的工作流。先拿已经训练好的模型对新增视频做自动推理生成预标注框,再让标注人员基于这些预标注框做调整。这个流程比从零标注快,但有前提:预标注模型在当前场景上不能太差,如果大部分目标都漏检了,那标注人员还是得自己找目标,提升有限。

另一个是抽帧间隔的调整。原始视频不需要逐帧标注,我一般按2到3帧选1帧的比例抽取。无人机飞行速度不快时目标在连续帧中的位置变化很小,全标会导致数据高度冗余。

标注工具的选择也有讲究。LabelImg体积小、使用简单,但如果要在云上协同标注,建议用基于浏览器的标注工具,团队协作时效率更高。

6.3 模型落地的扩展方向

这个数据集训练出来的检测模型,实际上只是整套河道垃圾监测系统的前端感知模块。做了几轮迭代后,我还加了两个扩展方向。

一是把单帧检测结果接入到视频时序逻辑里,检测到垃圾后做轨迹跟踪。因为无人机一直在移动,同一堆垃圾会出现在连续多帧画面里,如果不做跟踪,一秒钟内同一个目标会被重复上报好多次,后端平台的统计信息就完全不可信。接入跟踪器后,每个目标只在首次出现时上报一次,再辅以简单的规则判断目标是否一直停留在某个位置,可以自动生成垃圾聚集区域的告警。

二是把检测结果与地理坐标关联起来。通过无人机的GPS信息和云台姿态,检测框坐标可以反算出目标的大致经纬度位置,这样巡河人员拿到的不是“这段河道有垃圾”,而是“东经某度某分、北纬某度某分附近有垃圾聚集区”。这个方向的做法成熟度已经不低,我在模拟项目中验证过,定位误差在十米量级,足够让清理人员快速找到目标。

顺带提一句,如果想把已有的YOLO格式数据扩充成更复杂的任务,比如同时输出垃圾类别和大小等级,可以在现有检测框基础上做二次分类标注,不用重新检测目标,只需要对已有的框打标签,工作量相对小很多。

我在实际迭代中还有一个体会:做这类数据集,最难的不是训练模型那几步,而是愿意花时间去抠数据质量。前期多花一小时检查标注、检查坐标转换、检查数据划分,后期训练时就能少踩上百个莫名其妙的坑。很多人手里的模型效果一直上不去,回头一看根本原因都是数据里的低级错误。先花心思把数据整扎实,再谈算法调优,这才是做检测项目真正靠谱的路径。

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

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

立即咨询