简介:面向无人机目标检测与频射信号识别任务的图像数据集,适合从事无人机监测、反制或视觉识别研究的开发者、算法工程师及相关专业学生使用。数据集共728个文件,包括364张JPG原始图片和364个配套的Pascal VOC XML格式标注文件,压缩包总大小约136.89MB。图片内容涵盖多类无人机拍摄或地面视角样本,并包含无目标背景图像(如文件名中的kong系列)以及具体机型样本(如Mini3Pro),可用于训练和验证无人机检测模型,平均正确识别率达94.3%。当前已有约700人学习下载。借助标准XML标注,用户可直接适配YOLO、Faster R-CNN等主流目标检测框架,省去手动标注成本,也便于进行数据划分、增广与模型效果对比实验,是快速搭建无人机识别方案的实用数据集。
1. 为什么用图像检测做射频信号识别,而不是直接分类IQ序列
无线电监测机房每天会导出一批IQ记录,扫完就会有几百GB;可真正要判断“这里有没有无人机”,不是把原始IQ直接喂给网络就完事。无人机频射信号检测数据集给了另一条路——把2.4GHz和5.8GHz频段上的能量随时间的变化画成二维频谱瀑布图,再用目标检测框出信号出现的区域。数据集里364张原始图片大多来自kong系列和DJI Mini3Pro的采集样本,标注文件是Pascal VOC XML,前人在图上测得的平均识别率有94.3%。
这类任务的价值在于把射频信号识别转化为视觉问题,做过目标检测的人能很快上手。适合的读者有两类:一类是正在搭无人机管控平台,需要一份干净的信号检测基线;另一类是研究射频指纹识别,想先把“检出目标”这个前置步骤做扎实。接下来的章节不只看数据本身,我会把XML解析、YOLOv8训练、评估和落地部署的完整链路拆开讲,参数和坑都会提到。
2. 364张图片的标注结构:Pascal VOC XML解析与YOLO标签转换
2.1 先认清单张频谱图里到底放了什么信息
对无线电扫描设备来说,常见的存储形式是IQ样本,但直接看IQ数据并不能直观识别无人机信号。惯用的做法是把一段时间的IQ数据做短时傅里叶变换(STFT),得到以时间为横轴、频率为纵轴、像素亮度表示功率密度的时频谱图。数据集里的jpg图片就是这种渲染结果,它们与自然照片最大的区别是:图中任何一个能看出来的几何形状,对应的都是无线电发射行为。
标注时bndbox框住的不是一个“物体”,而是一段信号能量包络。比如跳频遥控信号会在图上形成一条斜向或阶梯状的亮带;图传信标则是相对稳定的宽带条。正因如此,数据处理环节不能照搬COCO的类别体系,通常只分两类:无人机信号(uav)和背景(background)。如果误把Wi-Fi干扰当成无人机目标框进去,模型学到的就不是信号形态,而是某个频段位置。
数据集的xml文件名如kong-29-_jpg.rf.9cd17aec4f296710e6d97856d96ce807.jpg,其中的kong-29是采集样本编号,rf.后跟着的哈希是Roboflow等标注平台重命名时留下的痕迹。这类重命名不会影响训练,只要annotation里的filename字段和实际文件对应上就行。
2.2 Pascal VOC XML的结构:字段逐个说清
打开一个典型的标注文件,内容如下:
<annotation> <folder>kong-29</folder> <filename>kong-29-_jpg.rf.9cd17aec4f296710e6d97856d96ce807.jpg</filename> <source> <database>Unknown</database> </source> <size> <width>1280</width> <height>512</height> <depth>3</depth> </size> <object> <name>kong</name> <bndbox> <xmin>361</xmin> <ymin>129</ymin> <xmax>522</xmax> <ymax>196</ymax> </bndbox> </object> </annotation>这里size里的width和height是整张图的像素尺寸,YOLO训练时会用它把像素坐标归一化。object可以重复多次,表示一张图里有多个无人机信号目标。name字段当前实际值是型号名(kong、Mini3Pro),转换标签时可以统一映射成uav,也可以按型号保留下游做型号识别。bndbox的四个值都是整数,分别表示左上角(xmin,ymin)和右下角(xmax,ymax)的坐标。需要留意difficult和truncated字段在某些xml里可能缺失,解析时要给缺省值。
下表是我会重点关注的字段,其他字段在转换时丢掉即可。
| 字段 | 类型 | 用途 | 缺省处理 |
|---|---|---|---|
| filename | string | 与图像文件对应 | 缺失时跳过 |
| size.width/height | int | 坐标归一化分母 | 必填,缺失报错 |
| object.name | string | 类别名 | 空则忽略该框 |
| object.bndbox | 四角坐标 | 像素框 | 坐标为空时作废该object |
| object.difficult | int | 难例标记 | 不存在时按0处理 |
2.3 用Python把VOC转成YOLO可用的标签
YOLOv8的标签格式是:class_id center_x center_y width height,且所有坐标相对图像尺寸归一化。下面的脚本能批量处理数据集里的所有xml:
import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_dir, class_zoo={"kong": 0, "Mini3Pro": 0}): os.makedirs(out_dir, exist_ok=True) tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.findtext("size/width")) img_h = int(root.findtext("size/height")) lines = [] for obj in root.iter("object"): cls = obj.findtext("name") cls_id = class_zoo.get(cls, -1) if cls_id < 0: continue box = obj.find("bndbox") xmin = int(box.findtext("xmin")) ymin = int(box.findtext("ymin")) xmax = int(box.findtext("xmax")) ymax = int(box.findtext("ymax")) center_x = ((xmin + xmax) / 2) / img_w center_y = ((ymin + ymax) / 2) / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h # 坐标越界裁剪,避免训练时崩框 center_x = min(max(center_x, 0.0), 1.0) center_y = min(max(center_y, 0.0), 1.0) box_w = min(box_w, 1.0) box_h = min(box_h, 1.0) lines.append(f"{cls_id} {center_x:.6f} {center_y:.6f} {box_w:.6f} {box_h:.6f}") txt_path = os.path.join(out_dir, os.path.splitext(os.path.basename(xml_path))[0] + ".txt") with open(txt_path, "w") as f: f.write("\n".join(lines)) # 遍历xml目录生成yolo标签 xml_dir = "annotations" out_dir = "labels" for f in os.listdir(xml_dir): if f.endswith(".xml"): voc_to_yolo(os.path.join(xml_dir, f), out_dir)这个脚本的核心是坐标归一化公式。把绝对像素除以图片宽高之后,框位置不再依赖当前训练的resize尺寸,就算后来改imgsz也不用重新标标签。class_zoo用字典而不是set,是给未来扩展预留的:如果想让Mini3Pro单独成为一个类别,把映射值改成1或2即可。注释里的“越界裁剪”严格说不是所有情况都需要,标注框本来就不应超过画布,但Roboflow导出的xml偶尔会有1-2像素越界,clip一下能让训练不中断。
2.4 转换完先可视化一遍,不要急着训练
标签转换完成后必须人眼确认一遍。加载jpg和对应的txt,用OpenCV画框:
import cv2 from pathlib import Path def show_yolo_label(image_path, label_path, class_names=["uav"]): img = cv2.imread(str(image_path)) h, w = img.shape[:2] with open(label_path) as f: for line in f: cls, cx, cy, bw, bh = line.split() cx, cy, bw, bh = map(float, (cx, cy, bw, bh)) x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[int(cls)], (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) return img img = show_yolo_label("images/kong-29.jpg", "labels/kong-29.txt") cv2.imwrite("check.jpg", img)这段代码有个容易看漏的点:txt里的中心坐标是相对值,画框时必须乘回原图宽高。很多初学的人忘记乘回,导致框画到左上角一小块。画完检查时重点看三类情况:框是否框住了完整信号带;是否把底噪误标成信号;同型号多个样本框尺寸是不是稳定。出现偏差就回到XML手动修正,364张的工作量并不大。
3. 把识别率冲到94.3%:YOLOv8训练数据加载与关键参数
3.1 为什么选YOLOv8而不是Faster R-CNN
在射频信号检测任务上,Faster R-CNN的两阶段结构能减少漏检,但一次推理要跑两遍网络,在加固笔记本上不容易到实时帧率。YOLOv8的C2f结构对中低分辨率信号块敏感度不错,且能用ONNX或TensorRT压进边缘设备。对364张图片的数据量,YOLOv8s比nano更稳,比medium更容易收敛。
选YOLOv8还有一个实际原因:Ultralytics框架把数据增强、训练、验证打包好了,不用自己写Dataloader。下面我按数据集的实际排列,逐步配置训练流程。
3.2 目录与data.yaml
先把图片和标签按images/train、images/val、images/test和labels/train、labels/val、labels/test分好。划分时不要随机切,而是按文件前缀里的型号分组:把kong系列的一部分放进验证集,Mini3Pro样本也要留出验证集,否则模型见过某型号的相似频谱,验证分数会虚高。目录结构如下:
datasets/rf_uav/ ├── images/ │ ├── train/ │ │ ├── kong-29-xxx.jpg │ │ └── Mini3Pro_5-8GHZ-6-xxx.jpg │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/对应的data.yaml:
path: /data/rf_uav train: images/train val: images/val test: images/test nc: 1 names: ["uav"]这里的path用绝对路径,train和val是相对于path的目录。如果改用相对路径,YOLOv8会以当前工作目录去拼,容易出现找不到图片的报错。nc是类别数,因为把kong和Mini3Pro都映射成uav,所以是1。如果想在同一模型里区分遥控和图传,就要回到第2章,把两个型号映射成不同类别id。
3.3 训练命令与逐参数解释
安装依赖用pip install ultralytics即可。下面是一条常见配置:
yolo detect train model=yolov8s.pt \ data=datasets/rf_uav/data.yaml \ epochs=200 imgsz=960 batch=16 \ lr0=0.005 lrf=0.01 \ mosaic=0.6 close_mosaic=10 \ patience=30 project=runs/detect exp_name=rf_uav_v8s逐个看参数:imgsz=960是因为原始图是横长频谱图,频率轴分辨率不能让出来,用640可能把5.8GHz的窄信号带压掉。如果显存不够可以降到768,但不要低于640。mosaic=0.6让60%的batch做拼接,合成更复杂的底噪背景;最后10个epoch关闭mosaic,让模型在真实数据分布上微调,否则收敛时会一直看到拼接边界。patience=30代表验证指标30个epoch不涨就提前停,数据量小时很多模型100个epoch内就稳定。
lr0=0.005而不是默认的0.01。数据量小且信号特征稀疏时,学习率偏大容易在mAP 0.7附近震荡。lrf=0.01表示最后的lr是初始的1%,给损失一个平滑阶段。如果你想复现94.3%附近的指标,这组参数可以当起点;如果显存小,把batch减半并同步把lr0降到0.003,不要只减batch不动学习率。
3.4 数据增强:哪些能用,哪些是坑
射频信号图的增强不能直接套自然图像。下表是我踩过一轮之后的推荐配置:
| 增强操作 | 是否建议 | 说明 |
|---|---|---|
| fliplr 水平翻转 | 建议 | 时间方向反演不影响信号结构 |
| flipud 垂直翻转 | 不要开 | 上下翻转等同于频率轴颠倒,模型学到频段位置会失效 |
| hsv_h / hsv_s | 不建议 | 频谱图颜色表示功率强度,不能随意调色相 |
| brightness / contrast | 可用 | 控制在0.1内,模拟监测设备增益差异 |
| rotation | 限制±5° | 角度过大会把斜条纹变成竖条,破坏物理特征 |
| scale 0.4 | 建议 | 模拟不同距离下目标信号的尺度变化 |
Ultralytics命令里通过hsv_h=0.0 hsv_s=0.0关闭色相与饱和度增强,再显式设置fliplr=0.5、flipud=0.0。rotation和scale直接加在参数里即可。如果你的目标是平均正确识别率94.3%,单纯加翻转基本不掉点,但垂直翻转通常会掉5个百分点以上,这是时频数据集最典型的一个坑。
3.5 训练监控:关注map50而不是val_loss
开始训练后,控制台会打印每个epoch的metrics。不要只看loss曲线下降就以为模型好了,目标检测的loss包含多个分支,有些下降只代表分类头在拟合背景。用Python脚本复现训练并打印关键指标:
from ultralytics import YOLO model = YOLO("yolov8s.pt") model.train( data="datasets/rf_uav/data.yaml", epochs=200, imgsz=960, batch=16, lr0=0.005, lrf=0.01, mosaic=0.6, close_mosaic=10, patience=30, plots=True, project="runs/detect", name="rf_uav_v8s", ) metrics = model.val() print("mAP@0.5:", metrics.box.map50) print("mAP@0.5:0.95:", metrics.box.map)上面的metrics.box.map50才是通常说的平均识别率,也就是验证集IoU阈值0.5下的平均精度。如果跑完map50在0.93~0.95附近,说明特征提取是有效的。如果map50超过了0.98但map50-95不到0.6,说明模型只会用高置信度输出,目标一变小或信噪比降低就漏检,这时要回头查增强和标签框是否完整框住了信号能量。
4. 识别率与抗干扰的边界:mAP评估、误检分析与信号级增强
4.1 0.943到底是哪个指标
数据集介绍里“平均正确识别率94.3%”在目标检测语境下通常被记为mAP@0.5。下面是一份典型的验证报告输出片段:
Class Images Instances Box(P) Box(R) mAP@0.5 mAP@0.5:0.95 all 45 109 0.931 0.917 0.943 0.7920.943表示在IoU=0.5的判断标准下,综合precision-recall曲线得到的平均精度。如果只是做“有没有无人机”的二分类决策,这个指标够用。但真要在城市环境里抓黑飞,还要看Box(R)召回率。上表的召回率0.917意味着大约8%的目标没有被框到,通常是信号太弱或与底噪对比度太低。召回不足时不要无脑调低置信度阈值,误检会增加,要结合后面的增强和时序后处理一起解决。
4.2 把误检样本挑出来重审
把模型验证时的plots=True打开后,YOLO会在runs/detect/val下保存带预测框的图,同时标出误检和漏检文件。我一般用下面脚本快速统计:
from pathlib import Path val_dir = Path("runs/detect/val") fp_fn_samples = [] for img in val_dir.glob("*.jpg"): # _fp 是误检,_fn 是漏检,Ultralytics会在文件名里标注 if "_fp" in img.name or "_fn" in img.name: fp_fn_samples.append(img.name) print("误检/漏检样本数量:", len(fp_fn_samples)) print("\n".join(fp_fn_samples[:20]))_fp和_fn是验证可视化时的标记文件。实际做法是打开这些图片逐张看,重点找两类误检源:手机热点在2.4G形成的持续宽带亮带,和无人机图传在瀑布图上的形态接近;蓝牙跳频形成的稀疏点簇,被模型当成无人机跳频细节。这类干扰在频谱图上没有稳定结构,单靠网络很难压下去,需要专门的抗干扰数据。
4.3 用信号级增强压住干扰
解决误检不要只靠堆epoch,要对真实物理干扰做合成。我常用的一个函数是给频谱图叠加高斯底噪,模拟低信噪比接收环境:
import cv2 import numpy as np def simulate_low_snr(img, noise_sigma=15): """对灰度频谱图叠加高斯噪声,模拟接收机底噪抬升。""" if len(img.shape) == 3: img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) else: img_gray = img noise = np.random.normal(0, noise_sigma, img_gray.shape) low_snr = np.clip(img_gray.astype(np.float32) + noise, 0, 255) return low_snr.astype(np.uint8)noise_sigma建议放在10~20之间,相当于在原始功率图上叠加标准差为10~20的像素噪声。叠加后信号区域的峰值不会被完全淹没,但背景会变粗糙,模型被迫去学信号形态而不是依赖干净底噪。把这类图按30%的比例混进训练集,能明显降低验证阶段对“干净背景”的依赖。注意不要在RGB三个通道上分别加不同噪声,那会产生颜色伪影,让模型学到错误的通道关系。
4.4 小数据集的正确评估方式:分组5折交叉验证
364张图太少,只做一次train/val划分容易因为随机性高估或低估。常见做法是按型号分组做5折,保证同一型号的所有样本只在同一个折里出现。
from sklearn.model_selection import GroupKFold # image_files 是图像路径列表,groups 按文件名首段区分采集批次 groups = [p.parts[-1].split("_")[0] for p in image_files] gkf = GroupKFold(n_splits=5) for fold, (train_idx, val_idx) in enumerate(gkf.split(image_files, groups=groups)): print(f"fold {fold}: train={len(train_idx)} val={len(val_idx)}")把每一折生成的train和val样本写进data.yaml后单独训练,最后取5个模型的map50均值和标准差。如果均值仍在0.94附近,说明94.3%不是靠某一张图运气得到的。如果某一折特别低,去检查那一折的信号形态是否与其他折差异过大,这通常意味着数据集里某个型号的样本量不足,也是后续要补数据的直接依据。
5. 把模型搬到手持侦测设备:TTA、INT8量化与时序NMS
5.1 TTA测试时增强,牺牲速度换召回
yolo detect predict model=runs/detect/rf_uav_v8s/weights/best.pt \ source=runs/detect/rf_uav_v8s/test \ imgsz=960 augment=True save_txt=Trueaugment=True会启用水平翻转和多尺度预测,对时间反向不敏感的射频信号图是安全的。它能把召回率稳定提升1-2个百分点,但推理耗时放大3~5倍,所以更适合离线录制数据复核,不作为最终部署配置。
5.2 导出ONNX+INT8量化
yolo export model=runs/detect/rf_uav_v8s/weights/best.pt format=onnx dynamic=True \ opset=13 half=True simplify=Truedynamic=True让输入尺寸可随实际图像变化,但TensorRT推理会稍慢;如果统一输入960x544,建议不开启。half=True让ONNX层直接转FP16,随后再做INT8量化,模型体积能压到十几MB。量化需要校准集,而且校准集不能只放已标注的正样本,要加入城市底噪和Wi-Fi干扰图,否则量化后的模型会对干净背景过于自信,到现场一跑就误报。
5.3 用时序NMS压掉重复框
手持设备扫同一目标时,相邻几帧输出的框会左右抖动,一个信号经常被当成两个目标。我习惯在模型输出后加一个轻量级时序过滤:
def temporal_nms(prev_boxes, cur_boxes, thr=0.6): keep = [] for box in cur_boxes: # compute_iou 为两框交并比,可以用cv2或手写实现 iou_scores = [compute_iou(box, p) for p in prev_boxes] if max(iou_scores, default=0.0) >= thr: keep.append(True) # 与上一帧同一目标,保留 else: keep.append(False) # 新目标,同样保留用于后续判断 return [b for b, ok in zip(cur_boxes, keep) if ok]thr控制多少IoU算同一目标。固定监测位时可以把thr提到0.8,让抖动框合并;手持设备晃动大则降到0.5,否则新出现的真实目标会被过滤掉。这个后处理不依赖硬件,十行代码就能解决大量“重复计数”的投诉。
5.4 现场环境快速验证
最后用导出后的推理引擎跑一段实际射频采集流:
python detect.py --source rtsp://192.168.1.20/live \ --weights rf_uav_int8.engine \ --conf 0.35 --iou 0.45 --imgsz 960conf=0.35比验证阶段的0.25高,比常规0.5低,用来平衡远距离弱信号的召回和城市环境干扰。iou=0.45设置NMS阈值,避免同一跳频链路的多个分框挤在一起。现场建议把conf先定在0.35,误报多就调成0.4,同时把时序NMS的thr从0.6提高到0.8,这样同一条跳频信号不会被反复计数。
本文还有配套的精品资源,点击获取