☰
机场异物小目标检测实战:FOD-A数据集处理方法与YOLO训练避坑指南
2026/10/20 7:01:19 网站建设 项目流程

简介:面向机场跑道安全与计算机视觉研究的目标检测数据集资源。FOD-A围绕机场异物碎片识别构建,覆盖电池、螺栓、螺母、扳手、螺丝刀、胶带、木块等31类常见异物,注释同时标注明亮/昏暗/黑暗光照与干燥/潮湿天气条件,适合用于智慧机场巡检、模型鲁棒性对比及多环境泛化研究。包体共2000个XML标注文件,采用VOC与YOLO格式,压缩包约367.52MB,便于直接接入常见检测框架训练与评估。目前已有1424人学习下载,内容适用于目标检测方向学生、算法工程师及机场运维智能化项目相关人员,可作为数据预处理、类别均衡分析和环境扰动下检测性能验证的实用素材。

1. FOD-A机场异物数据集:为什么它比通用目标检测更难

做机场道面巡检的人把跑道上一颗螺丝钉、一块掉落的轮胎皮、一个混凝土碎块统称为FOD(Foreign Object Debris),这个词在民航运维里接的不是“运气”,而是“零容忍”——一颗小金属物被发动机吸进去,代价就是一个航班取消甚至更严重的事故。FOD-A机场异物数据集,就是把这些极小、极杂、极易漏检的目标单独拎出来做成检测基准,目标往往只有十几个像素,和背景几乎融为一体。

通用目标检测模型在这类数据上有个很反直觉的表现:mAP看着不低,一旦放到实地视频里,漏检率立刻拉垮。原因不复杂——通用数据集里“小目标”再小也占画面较大比例,而FOD-A里的异物占了整张图不到千分之几的面积,加上跑道水泥纹理、光照阴影、雨天反光,模型很难分清楚。这个数据集的定位,就是逼着检测方案去解决“小目标、低对比、零容忍”这三个问题。它适合的读者不是泛泛的AI爱好者,而是做工业外观检测、无人机巡检、监控安防这类“漏检一次就翻车”场景的工程师。

2. 数据先于模型:读懂FOD-A数据集的构成与标注格式

2.1 FOD-A数据集里到底装了什么

FOD-A数据集的典型构成,是一组机场道面场景的采集图像和对应的目标框标注。目标类别一般覆盖跑道巡检最关心的几类异物:金属件(螺栓、垫片、工具遗留物)、塑料件(包装带碎片、塑料片)、混凝土/道面碎块(水泥块、沥青碎屑),有些版本还会加入轮胎碎片或木片。采集视角通常有两种:一是无人机斜拍,目标更小但视角覆盖广;二是跑道巡查车或固定机位平拍,目标近一些但背景纹理更复杂。光照条件通常覆盖白天强光、阴天、雨雾天气,因为道面检测算法在晴天的表现不能代表凌晨和雨天。

拿到手第一步不是急着训练,而是先把数据文件组织看明白。这类数据集一般包含 images(原图)、annotations(标注文件)、README 或数据说明文档。标注格式常见的是 COCO JSON 或 Pascal VOC XML,少数直接给 YOLO txt。先看 README 里的类别定义和标注口径——是只要框住异物本体,还是把影子也算进去;是只标单帧还是连续帧都标,这些都直接影响后面怎么转换、怎么划分。我习惯先把所有图片尺寸统计一遍,确认是否有混合分辨率:有些数据集里航拍图和车拍图混在一起,如果直接统一 resize 训练,小目标信息会先丢一半。

2.2 把标注转成 YOLO 格式:从 JSON 到 txt 的转换脚本

我假设你拿到的标注是 COCO JSON,要转成 YOLO 训练需要的 txt。YOLO 格式每一行是一个目标:类别序号、归一化中心点 x、y、归一化宽高 w、h。下面是常见的转换脚本,我一般在初次接手FOD-A这类数据集时会先跑这个,跑完随机抽几张用可视化脚本核对:

# 目标检测-FOD-A机场异物数据集标注转换:COCO JSON -> YOLO txt import json import os from pathlib import Path def coco_to_yolo(json_path, out_dir): with open(json_path, encoding="utf-8") as f: coco = json.load(f) # 把 COCO 的 category_id 映射成从 0 开始的连续序号 cat_id2cls = {cat["id"]: idx for idx, cat in enumerate(coco["categories"])} os.makedirs(out_dir, exist_ok=True) for img in coco["images"]: img_w, img_h = img["width"], img["height"] if img_w == 0 or img_h == 0: continue lines = [] for ann in coco["annotations"]: if ann["image_id"] != img["id"]: continue cls = cat_id2cls[ann["category_id"]] x, y, bw, bh = ann["bbox"] # COCO bbox 是左上角 + 宽高 cx = x + bw / 2.0 cy = y + bh / 2.0 # 越界保护:FOD 目标经常贴边,归一化后容易超出 [0,1],训练时直接报错 cx = min(max(cx / img_w, 0.0), 1.0) cy = min(max(cy / img_h, 0.0), 1.0) nw = min(bw / img_w, 1.0) nh = min(bh / img_h, 1.0) lines.append(f"{cls} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}") stem = Path(img["file_name"]).stem with open(Path(out_dir) / f"{stem}.txt", "w", encoding="utf-8") as f: f.write("\n".join(lines)) print(f"转换完成:{len(coco['images'])} 张图片 -> {out_dir}") if __name__ == "__main__": coco_to_yolo("fod_a/annotations/train.json", "fod_a/labels/train")

参数里 json_path 是标注文件的路径,out_dir 是输出 txt 的目录,脚本会按图片的 file_name 同名生成 txt 文件。cat_id2cls 这一步把 COCO 里可能不连续的类别编号(比如类别 id 是 1、3、5)重排成 0、1、2,YOLO 训练时类别必须从 0 开始连续编号。越界保护那两行不是多余的:FOD 目标通常在跑道边缘,标注框贴着图像边界是常态,不做 clip,训练时 loss 会出现 nan 或者框跑到画面外。转换完建议用 opencv 画一遍框,肉眼确认类别和位置对得上,这一步省不了。

2.3 划分坑:按图片随机划分会把连续帧泄漏进验证集

FOD-A 数据集如果来自视频抽帧,相邻帧之间的场景几乎一样。直接按图片随机划分,训练集和验证集里会出现大量“同一颗螺栓的两帧”,验证 mAP 虚高到 0.95,一旦部署到新视频立刻现原形。我一般会先看标注 JSON 里有没有 sequence_id 这类字段,没有就按文件名前缀分组,再按组划分:

# 按采集序列划分训练/验证集,防止连续帧数据泄漏 import json from sklearn.model_selection import GroupShuffleSplit with open("fod_a/annotations/train.json", encoding="utf-8") as f: ann = json.load(f) images = ann["images"] # 假设文件名形如 runway_01_0001.jpg,取前两段作为序列 ID,实际按数据集命名规则调整 groups = [] for img in images: parts = Path(img["file_name"]).stem.split("_") groups.append(f"{parts[0]}_{parts[1]}") splitter = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(splitter.split(images, [0] * len(images), groups))

GroupShuffleSplit 的第三个参数传的是分组 ID,同一个序列的所有帧会被整个放进训练集或验证集,不会拆散。random_state 固定下来,以后复现实验时划分不会变。这里的 groups 提取逻辑要按数据集实际命名改,比如有些数据集是按日期文件夹分层的,那就直接取文件夹路径作为分组 ID。这一步做对,后面训练出来的指标才有参考价值。

3. 模型选型与训练配置:让 YOLO 在小目标上不摆烂

3.1 为什么选 YOLO 而不是两阶段模型

FOD 检测的落地场景决定了模型选型的优先级:跑道巡检车或无人机机载设备,算力有限,需要实时推理;同时漏检零容忍,精度不能妥协。两阶段检测器比如 Faster R-CNN 在小目标上确实有一定精度优势,但推理速度在边缘设备上撑不住,而且部署链路比 YOLO 系复杂不少。我用 ultralytics 的 YOLOv8 或 YOLO11 作为主力,不是因为它在学术榜单上最强,而是它的训练、导出、部署一条龙太省心,而且对小目标场景有明确的调整空间。

但默认配置直接套用会翻车。YOLO 默认输入分辨率是 640,这个尺寸下 FOD-A 里 16 像素的异物只占整图宽度的 2.5%,经过几次下采样后特征图上的目标只剩几个像素,检测头根本学不到有效信息。解决办法是把输入分辨率提到 1280 或 1536,代价是显存占用和训练时间成倍增加。另一个思路是启用 P2 小目标检测层,但 P2 层在 YOLO11 里的推理耗时增加明显,我通常先提分辨率,不够再加 P2 或切片推理。

3.2 训练配置两份文件:数据 yaml 和关键超参

数据准备完,先建一个数据集的 yaml:

# fod_a.yaml —— FOD-A 机场异物数据集配置 path: ./fod_a train: images/train val: images/val test: images/test names: 0: metal 1: plastic 2: concrete

names 的编号必须与转换脚本里 cat_id2cls 的映射一致,顺序反了模型会拿金属当塑料训练。path 是相对 ultralytics 工作目录的路径,建议用绝对路径避免在服务器上换目录后找不到数据。接着启动训练:

# 在 ultralytics 环境里启动 FOD-A 训练,小目标场景把 imgsz 提到 1280 yolo train model=yolo11s.pt data=fod_a.yaml imgsz=1280 batch=16 epochs=100

imgsz 从 640 提到 1280 后,一个小 16px 的目标在输入图上占的像素比例没变,但经过网络下采样后保留的有效特征从 2 个像素变成 4 个像素,检测头能学到的纹理信息完全不同。batch 需要根据显存调:1280 分辨率下,8G 显存的卡建议 batch 从 8 开始,16G 可以试 16;显存不够优先减 batch,别急着降 imgsz。epochs 对这类千张量级的数据集,100 轮左右足够,我一般配合早停,连续 20 轮 val mAP 50 不涨就停。

数据增强建议单独写在 yaml 里,不直接用默认值。FOD 场景特殊,过度增强反而会把小目标增强没了:

# FOD 场景数据增强配置,适合在 ultralytics 里通过 augment 参数覆盖 mosaic: 0.8 mixup: 0.2 hsv_h: 0.015 hsv_s: 0.5 hsv_v: 0.4 degrees: 10.0

mosaic 保留 0.8,它能把四张图拼在一起训练,让模型看到更多“小目标分布稀疏”的场景。mixup 开到 0.2 就行,太高会把异物和背景混合成难以辨识的目标。degrees 旋转只开 10 度,FOD 是刚体小目标,转 90 度后螺丝钉和碎塑料的形状语义会失真,模型学到的旋转不变性在真实跑道上并不需要那么强。hsv_v 亮度扰动开到 0.4 是必要的,跑道在一天内光照变化极大,中午的逆光会把金属件拍成全白,这个增强能逼模型去学物体轮廓而不是依赖亮度绝对值。

3.3 训练曲线怎么看:R 比 P 重要,APs 比 mAP 重要

训练完先看 runs/detect/ 下生成的 results.csv,别只盯着 mAP50。FOD 场景漏检的代价远大于误检,所以查全率 R 是第一指标:R@0.5 如果在验证集上低于 0.9,说明有大量异物漏检,这种模型根本没有部署价值。同时看小目标指标 APs(面积小于 32×32 像素的目标),FOD-A 里大部分框都在这个区间,APs 起不来,mAP 再高都是虚的。

再看 P(查准率):跑道场景误检的代价是清障作业白跑一趟,但比起漏检可以容忍。如果 P 低而 R 高,优先去清洗验证集标注而不是加负样本;如果 R 和 P 都低,先回第 2 章检查标注转换是不是出了问题,很多“训练效果差”其实是标签没对齐,不是模型不行。loss 曲线里 box_loss 和 cls_loss 在 100 轮后应该平滑下降,如果 val loss 在某个 epoch 后急剧反弹,就是过拟合,回 3.2 把 epochs 调短或增强里加大 mosaic 比例。

4. 把数据价值榨干:针对 FOD 场景的数据增强与难例挖掘

4.1 增强不是越多越好:跑道上最缺的是光照变化,不是形变

FOD-A 数据集的规模通常不大,少的几百张,多的两三千张,这在深度学习里属于小样本。很多新手第一反应是堆增强,旋转、翻转、裁剪全开,结果模型在验证集上不错,到实景一测全崩。我总结的经验是:FOD 目标本身是刚体,形变增强带来的收益很低,模型真正缺的是对光照、天气、背景纹理变化的鲁棒性。所以增强策略应该围绕“让模型看到同一个目标在不同光照下的样子”来配置,而不是让目标随意变形。

除了上一章给的超参,我通常会在训练前单独跑一遍数据筛查——统计所有图片的平均亮度和对比度,如果发现数据集里绝大多数都是白天顺光拍摄,那就必须在增强里加大亮度扰动和噪声模拟。一个实用做法是把少量真实过曝图和多雾帧单独复制一份,用 Albumentations 这类库加高斯噪声和随机亮度,再混入训练集:

# 用 Albumentations 增强 FOD-A 中的低光照样本 import albumentations as A light_aug = A.Compose([ A.RandomBrightnessContrast(brightness_limit=(-0.3, 0.3), contrast_limit=(-0.2, 0.2), p=0.8), A.GaussNoise(var_limit=(20.0, 60.0), p=0.4), A.RandomGamma(gamma_limit=(80, 120), p=0.5), ])

这里 brightness_limit 到 ±0.3 是把白天图模拟成阴天和过曝两种极端情况,var_limit 的噪声模拟雨天颗粒。Gamma 增强对暗部细节的提升很明显,跑道上阴影里的暗色塑料块靠这个才能被模型注意到。Albumentations 这份代码要放在训练循环外面,只对训练集的图片做,别动验证集,否则指标失真。

4.2 难例挖掘:让模型告诉你它漏了什么

数据增强是单方面的努力,另一个高效路径是难例挖掘——用当前版本模型去扫未标注的原始视频帧,把置信度低的检测结果捞出来,人工确认后补标进训练集。这个闭环每轮训练都能让模型在自己最薄弱的地方获得新样本,比盲目采集新数据划算得多:

# 用半成品模型扫未标注帧,捞低置信度目标做人工复核 from ultralytics import YOLO model = YOLO("runs/detect/fod/weights/best.pt") results = model.predict( source="fod_a/raw/unlabeled_frames/", conf=0.1, save_txt=True, save_conf=True, verbose=False, )

conf 阈值拉到 0.1,模型会输出大量高不确定度的框,其中一半是背景误检,一半是真正被漏掉的难例。save_conf=True 会在 txt 里写入每个框的置信度,方便后续排序:先把置信度在 0.1 到 0.3 之间的框按图抽出来,人工过一遍,确认是异物的帧补标后进训练集;确认是背景的帧可以留作负样本。这个过程的收益在 FOD-A 上非常明显——尤其是类别不平衡时(比如金属件样本多、塑料碎片极少),难例挖掘是补少数类最直接的手段。

5. FOD-A数据集避坑指南:四个翻车场景与排查路径

5.1 标注框小于 16 像素,训练 loss 降了但什么都检不出

现象:训练曲线很正常,loss 平滑下降,验证集 mAP50 也到了 0.8 以上,但打开检测结果一看,预测框全集中在画面中央的大目标上,小异物一个没框出来。

原因:FOD-A 里大量目标真实尺寸只有 10~20 像素,imgsz 用 640 时,经过网络 5 次下采样,这些目标在最终特征图上只剩 1 个像素左右,检测头无法产出有效特征。loss 下降是因为模型把容易学的大目标学好了,小目标直接躺平。

解决:第一步把 imgsz 提到 1280,先看 APs 是否明显上升。如果显存不够或 APs 还是低,做切片训练:把原图切成 640×640 带重叠的块,每个块单独训练,等价于让模型每张图只看局部放大的异物。我个人的经验顺序是:先提 imgsz,再试切片,最后才考虑换带 P2 头的模型结构。

5.2 高亮金属反光把目标洗成白色块

现象:晴天中午的测试视频里,金属螺栓和金属垫片大量漏检,但阴天和傍晚的检测效果正常。模型把高亮金属误判成跑道标线或反光。

原因:FOD-A 训练集里如果以阴天和顺光采集为主,模型没见过强光照下的金属镜面反射特征。金属目标在过曝时内部纹理完全丢失,只剩一个白色高光轮廓,和跑道上的白色标线块在特征上非常接近。

解决:在数据增强里加强亮度扰动和锐化模拟,把部分训练图通过非线性变换模拟过曝效果;同时建议在采集端增加偏振镜,从源头压制金属反光。如果这两条都来不及,可以单独筛出所有高亮金属样本复制一份,加上高斯模糊和亮度提升后混入训练集,让模型至少见过这种形态。

5.3 验证集与训练集同来源帧,指标虚高到打假

现象:验证集 mAP50 高达 0.97,模型一部署到新采集的视频上,mAP 直接跌到 0.6,漏检一堆。反复调参都救不回来。

原因:数据划分时直接对图片随机划分,没有考虑连续帧之间的内容重复。FOD-A 如果从视频抽帧,相邻两帧里同一颗螺栓只是偏移了几个像素,模型在训练时已经“记住”了验证集里的目标,相当于开卷考试。

解决:按第 2.3 节的方法,用 GroupShuffleSplit 按采集序列 ID 划分数据集。判断特征是看验证集里是否大量出现与训练集目标几乎完全相同的场景;如果是,立即重建划分。这件事做在训练之前,就能避免后面所有指标分析失去意义。

5.4 类别不平衡:塑料碎片样本少,复制增强反而越学越偏

现象:金属件检测效果很好,塑料碎片和混凝土碎块几乎检不出,或者说框出来了但类别总是猜成金属。查看训练集分布,塑料类样本数不到金属类的十分之一。

原因:直接用复制粘贴的方式给少数类加样本,会让模型反复看到同一批塑料碎片的相同姿态,过拟合到这些特定像素上,无法泛化到新的塑料异物。数量不平衡背后其实是形态多样性不足。

解决:用难例挖掘从原始未标注帧里找更多的塑料碎片真实样本;再配合适度复制增强但控制比例,复制增强的部分不超过该类原始样本的 1 倍。另一种做法是用 mosaic 增强强制少数类目标与不同背景拼接,增加背景多样性而非目标多样性。这一步做完,再回 3.3 看 APs 里塑料类的单独指标。

6. 把推理做稳:切片推理与最终验收

训练完成只算走了一半路。FOD-A 这类小目标数据,最终推理环节我强烈建议做切片推理,而不是把整张大图直接塞给模型。切片推理的本质是把大图切成有重叠的小块,分别推理再合并结果,目标在整图里只有十几像素,但在 512×512 的切块里可能占到几十像素,检测难度完全不同。以下是一个简洁的实现思路:

# 大图切片推理示例:重叠 64px,解决目标恰好落在切缝上的问题 def slice_infer(img, model, slice_h=512, slice_w=512, overlap=64): h, w = img.shape[:2] detections = [] for y in range(0, h, slice_h - overlap): for x in range(0, w, slice_w - overlap): patch = img[y:y + slice_h, x:x + slice_w] res = model.predict(patch, conf=0.25, verbose=False)[0] for box in res.boxes: bx1, by1, bx2, by2 = box.xyxy[0].tolist() conf = float(box.conf[0]) cls = int(box.cls[0]) # 把局部坐标偏移回原图坐标 detections.append([x + bx1, y + by1, x + bx2, y + by2, conf, cls]) return detections

这里的循环步长是 slice_h - overlap,也就是 512 - 64 = 448,保证每张图在边缘处有 64 像素的冗余覆盖,目标即使正好被切缝切开,也会在相邻块里被完整检测到。合并后如果同一个目标被多个块检出,用最简单的 NMS 按 IoU 0.5 去重就行。切片尺寸的选择有一个经验值:让切片后目标尺寸占切片宽度的 3%~10%最佳,对应 FOD-A 里 15 像素左右的目标,用 512 切片正好。

最终验收不要只看验证集表格。我会做一件事:用切片推理跑一段完整的跑道巡检视频,把输出的所有漏检帧集中抽出来,逐帧看漏的是什么、漏在什么光照和背景下。我第一次做这套方案时,自认为指标已经合格,结果一跑视频发现所有漏检都发生在逆光段——训练集里的逆光样本太少,增强也没模拟到位,回头洗了一批逆光帧重新训练,漏检率才真正降下来。这段经历让我养成一个习惯:凡是小目标数据集,训练结束后的第一件事不是发报告,而是拿一段和训练集不同来源的真实视频跑一遍,眼见为实。希望帮到你。

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

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

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

立即咨询