ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

战车卫星图目标检测数据集全流程:标注转换、切图与训练避坑

战车卫星图目标检测数据集全流程:标注转换、切图与训练避坑 简介面向目标检测研究与开发者的战车卫星图数据集专为处理军事侦察、安防监控与自动驾驶等场景中的装甲车辆识别任务而构建。资源内含1000张1024×1024像素的高清彩色卫星图目标涵盖坦克、步兵战车等多种装甲车型可支撑YOLO、Faster R-CNN、Mask R-CNN等主流目标检测框架的训练与验证。压缩包除图像外还附有批量重命名脚本、信息说明文档及对应标注文件标注以边界框形式给出目标位置为模型提供清晰监督信号卫星图像特有的光照变化、遮挡和远距离目标等挑战可用于评测算法在复杂环境下的鲁棒性。整包约343.63MB目前已有2902人学习下载适合具备计算机视觉基础的研究者、算法工程师及军事相关领域开发者用于模型训练、超参数调优与检测性能对比分析亦可直接作为遥感目标检测课题的基准数据集使用。1. 战车卫星图目标检测这份数据集到底能干什么第一次拿战车卫星图数据集跑目标检测验证集精度看着不错换到一张完整的大图上却基本漏检当时心里只有一个词翻车。问题不在网络结构而在卫星视角本身——装甲车、战车这类目标在遥感图里常常只有几十个像素道路、建筑、树影全是干扰。这套人工智能目标检测数据集战车卫星图2要解决的正是这个场景把高空俯拍影像中的战车与装甲车标注出来喂给主流目标检测框架做训练帮你在小目标检测方向少走弯路。适合两类人一是刚接触遥感目标检测、想做点实际训练的新手二是从自然图像模型迁移到卫星数据后精度骤降、正在排查原因的从业者。拿到手先别急着训练把数据分布看清后面的脚本能省你好几天。2. 数据集结构与标注格式拿到手先做这三件事这类战车卫星图数据下载下来通常是一个压缩包解压后目录结构大同小异。我的习惯是不急着打开训练脚本先花十分钟把三件事做掉核实文件目录、搞清标注格式、跑一次数据质量体检。这三件事决定后面所有操作是顺利还是反复返工。很多新手一上来就把数据集路径填进训练配置结果不是标签读不出来就是训练到一半发现坐标全错回头再查时间全搭进去了。2.1 目录组织与文件清单先确认这套数据是哪一代典型的目录结构一般是这样的JPEGImages/原始卫星影像JPG或PNG一张图对应一个文件Annotations/VOC格式的XML标注文件名和JPEGImages里的图片名一一对应ImageSets/Main/train.txt、val.txt、test.txt每行一个不带扩展名的图片ID部分版本会额外提供labels/已转好的YOLO txt或者annotations.jsonCOCO格式。有些打包版本为了适配不同框架会放两份标注内容一致但格式不同。我一般先打开ImageSets/Main里的txt确认作者给的划分方式如果这个目录是空的说明划分要靠自己完成。这里有个容易踩的点labels/里的txt不一定是和当前JPEGImages/完全对应的有的发布者会漏掉一部分图片的标签只靠肉眼根本看不出来必须用脚本统计一下文件数量和ID匹配情况。路径内容用途JPEGImages/原始影像JPG/PNG模型输入Annotations/VOC XML标注目标框坐标、类别名ImageSets/Main/train/val/test ID列表数据集划分依据labels/ 或 annotations.jsonYOLO或COCO备用标注跨框架迁移真正需要仔细检查的是XML里object节点的name字段——同一类目标叫法是否统一。我见过label里tank、armor、armored_vehicle混着来的版本可能只是不同批次标注的人叫法不同但在转格式和计算类别数时会直接让脚本崩掉。检查方法是抽十来个XML看一下或者用下一章的统计脚本把全部标签列出来。另外如果目录里存在labels/建议还是自己用XML重新转一份而不是直接用打包的txt你不确定它和原图尺寸是否匹配、坐标是否被压缩过。2.2 标注格式映射VOC、COCO与YOLO三种读法先花两分钟把三种主流标注格式对齐后面就不会被坐标搞晕。VOC的XML把目标框记录成xmin、ymin、xmax、ymax四个像素值含义简单直接左上角点和右下角点。COCO的annotations.json里每条标注用[x, y, width, height]表示也是像素坐标但注意它和VOC的顺序不一样x和y是框左上角后面是宽高。YOLO格式则完全不同它把坐标归一化到0到1之间存储中心点横坐标、中心点纵坐标、框宽、框高全部除以原图尺寸所以不管原始分辨率是多大txt里的值都在0到1之间。这个换算关系是卫星图数据集中最容易被忽略的地方x_center (xmin xmax) / 2 / image_widthy_center (ymin ymax) / 2 / image_heightbox_width (xmax - xmin) / image_widthbox_height (ymax - ymin) / image_height这里image_width和image_height必须是该图原始尺寸不是某次resize后的尺寸。很多训练框架内部读图片时会把原图直接缩放同时会把label按原尺寸归一化如果你在外部先缩放就会出现框和内容对不上训练半天模型学了个寂寞。再看旋转框的问题。部分卫星图数据集为了提高标注精度用的是四点多边形或者带旋转角度的矩形框这类标注在XML里可能写成rotated_bndbox或八个点的polygon。如果遇到这种结构不要硬塞给按水平框设计的YOLO训练管线。常见做法是选用支持旋转框检测的模型或者用旋转框的外接矩形生成水平框同时接受一定程度的背景误差。转之前先看清楚别等训练报错才回头。2.3 数据质量快速体检别让标注问题拖累训练体检不需要精确到每张图但要有一个整体感觉。我一般检查四项一是空标注图没有目标的背景图占比多少。如果占比低说明作者已经做了初步筛选如果有一半图没有目标就要考虑保留部分作为负样本。二是图像尺寸是否一致如果原始影像尺寸横跨几百像素到几千像素训练时统一resize会对小目标极不友好后面大概率需要切图。三是目标框尺寸是否离谱有的目标明显只有20像素却标着50像素的框导致训练时模型学到的是“把框扩大一点也行”mAP很难提升。四是类别是否出现异常比如某个类只出现在几张重复背景里模型容易过拟合到特定纹理。体检脚本不另写用第3章的统计脚本直接跑一遍就能覆盖大部分检查项。跑完之后建议抽50张图把标注框画出来用OpenCV把矩形画到原图上肉眼扫一遍。这个操作能看到框和车厢边缘是否贴合、有没有框住两辆车、有没有漏标。虽然只花十几分钟但能避免后面三天莫名其妙的调参。从经验看数据质量有问题时调什么网络结构都很难救回来。3. 用脚本摸清数据分布目标太小是卫星图的头号问题在把数据送进模型之前先回答三个问题目标在图像里占多大面积、每张图平均有多少目标、各类别数量是否均衡。卫星图目标检测和自然图像目标检测的本质差别就在这——同样的检测模型在COCO上表现不错换到卫星图上精度掉一半多半是数据分布变了模型没见过这么小的目标。3.1 目标尺寸与纵横比分布统计先写一个脚本把全部XML里的目标框尺寸统计出来这段代码很短但对后续所有决策都有用。# stat_annotations.py统计全部XML标注的目标尺寸与数量 import os import glob import xml.etree.ElementTree as ET from collections import defaultdict import statistics xml_dir Annotations # 改成你的标注目录 widths, heights [], [] per_image_counts [] class_counter defaultdict(int) for xml_path in glob.glob(os.path.join(xml_dir, *.xml)): tree ET.parse(xml_path) root tree.getroot() # 读原始图尺寸 size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) count 0 for obj in root.findall(object): name obj.find(name).text class_counter[name] 1 bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) widths.append(xmax - xmin) heights.append(ymax - ymin) count 1 per_image_counts.append(count) print(图片数:, len(per_image_counts)) print(目标总数:, len(widths)) print(每图目标数 中位数:, statistics.median(per_image_counts)) # 目标宽度中位数和十分位 sorted_w sorted(widths) print(目标宽度 中位数:, statistics.median(widths), p10:, sorted_w[len(sorted_w)//10]) print(目标高度 中位数:, statistics.median(heights)) print(类别统计:, dict(class_counter))逻辑说明逐个解析XML把size节点里的图宽图高取出来再遍历object节点读取目标框把每个目标宽高记录到列表里。用中位数而不是平均值是因为个别超大的标注会拉高平均值掩盖小目标问题。class_counter用defaultdict统计每个类目标数方便看类别间的数量差距。参数说明xml_dir改成自己的Annotations路径即可。所有坐标都用float()做转换避免有的XML标了带小数坐标而报错。p10是排序后第10%位置的值它比中位数更能反映“大量目标到底有多小”。拿到结果后怎么判断如果目标宽度中位数低于40像素说明这批数据普遍是小目标。常规backbone下采样5次后一个40像素的目标在特征图上只剩一个点信息几乎丢光后面训练要么提高输入分辨率要么换支持小目标的检测头。如果宽高比大部分集中在1附近说明目标是接近方形的战车如果出现大量2.0以上的长条框说明数据集里可能有坦克或长车厢车辆anchor设计要考虑长宽比。3.2 切图策略大图切成可训练的小图几千像素的卫星原图直接进模型显存装不下硬缩放到640会让30像素的目标缩到几乎不可见。常见做法是滑窗切图把大图切成固定尺寸的小块同时把标注框同步裁剪。切图的核心是保证目标完整落在某个窗口里并且裁剪后的坐标正确。# crop_sliding.py按滑窗切图并同步裁剪、过滤标注 import os import cv2 import glob import xml.etree.ElementTree as ET WIN_SIZE 1024 # 切图窗口尺寸 OVERLAP 256 # 窗口重叠像素 STEP WIN_SIZE - OVERLAP # 滑动步长 MIN_KEEP 0.7 # 目标最小保留面积比例 SRC_IMG JPEGImages SRC_XML Annotations OUT_IMG cropped_images OUT_LABEL cropped_labels def load_boxes(xml_path): tree ET.parse(xml_path) root tree.getroot() boxes [] for obj in root.findall(object): bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) boxes.append([xmin, ymin, xmax, ymax]) return boxes def crop_and_save(img, boxes, ox, oy, save_id): patch img[oy:oyWIN_SIZE, ox:oxWIN_SIZE] new_boxes [] for xmin, ymin, xmax, ymax in boxes: # 计算目标在当前窗口内的可见区域 nxmin max(xmin, ox) nymin max(ymin, oy) nxmax min(xmax, ox WIN_SIZE) nymax min(ymax, oy WIN_SIZE) if nxmax nxmin or nymax nymin: continue inter_area (nxmax - nxmin) * (nymax - nymin) full_area (xmax - xmin) * (ymax - ymin) # 只保留完整度足够高的目标框 if inter_area / full_area MIN_KEEP: continue new_boxes.append((nxmin - ox, nymin - oy, nxmax - ox, nymax - oy)) if new_boxes: cv2.imwrite(f{save_id}.jpg, patch) with open(f{save_id}.txt, w) as f: for b in new_boxes: f.write( .join(str(int(v)) for v in b) \n) os.makedirs(OUT_IMG, exist_okTrue) os.makedirs(OUT_LABEL, exist_okTrue) for img_path in glob.glob(os.path.join(SRC_IMG, *.jpg)): base os.path.splitext(os.path.basename(img_path))[0] xml_path os.path.join(SRC_XML, base .xml) if not os.path.exists(xml_path): continue img cv2.imread(img_path) h, w img.shape[:2] boxes load_boxes(xml_path) for oy in range(0, h - WIN_SIZE 1, STEP): for ox in range(0, w - WIN_SIZE 1, STEP): crop_and_save(img, boxes, ox, oy, f{OUT_IMG}/{base}_{ox}_{oy})逻辑说明先用load_boxes把XML里的水平框读出来然后按滑窗逐步移动窗口。每到一个窗口位置计算每个目标框落在窗口内的可见面积如果可见面积占原框面积的比例小于MIN_KEEP就丢掉这个框避免模型学到半截目标。裁剪后的坐标要减去窗口的左上角坐标ox, oy因为在新图片里坐标系原点变成了窗口左上角。参数说明WIN_SIZE主要取决于显卡显存1024是中等显存的稳妥值显存充足可以设1280目标更小时甚至可以用1536。OVERLAP设256能保证小尺寸目标至少完整落在一个窗口里同时不会产生太多重复样本。MIN_KEEP设0.7是个经验值设太高会把大量目标滤掉设太低则给训练增加低质量框。切完图建议抽50个窗口看一眼确认目标没有被切得只剩一个角。还有一点容易被忽略如果你的目标本身大于窗口尺寸那无论如何都不可能完整保留这时要么把该目标从样本中剔除要么改用更大窗口。卫星图里极少数目标可能出现这种情况不用强行处理统计一下占比低于1%直接丢弃不影响效果。3.3 类别不平衡与背景样本的处理数据分布统计往往还会暴露类别失衡。战车卫星图数据里坦克和装甲车两类数量差距可能达到五倍甚至更高。两个实用方向一是类别权重在损失函数里给少数类更高的权重降低多数类主导梯度。很多框架在训练配置里直接支持class_weights参数不需要自己实现损失函数。二是样本增强对少数类做复制粘贴增强或过采样。与自然图像不同的是卫星影像里目标大多是俯视视角可以自由旋转粘贴只要背景不穿帮就行。不推荐简单重复复制同一张图模型容易记住样本本身而不是学到泛化特征。背景样本的处理如果统计时发现大量图没有目标这部分背景图不要丢。把它们切成固定尺寸的背景块作为负样本放进训练集模型能学到“道路、建筑、树影不是装甲车”对全图推理时的虚警压制非常明显。负样本也可以来自有目标图片中那些没有目标的切块这类样本来源多且天然贴近真实场景分布。我一般在分类统计里单独建一个background目录训练时把背景块路径写进训练列表让模型在前景和背景之间学会平衡。4. 转成YOLO格式并跑通一次训练完整链路数据分布看清楚了切图也做了这一步把标注从VOC转成YOLO格式然后划分数据集跑通一次完整训练。YOLO格式之所以常用是因为脚本读取效率高、一行一个目标而且和框架自带的增强、损失函数无缝对接。就算资源包本身带了labels/我也建议用XML重新转一遍保证版本一致。4.1 VOC转YOLO目录与归一化坐标转换的核心是坐标归一化。前面2.2讲过换算公式这里直接落到脚本# voc2yolo.pyVOC XML 转 YOLO txt import os import glob import xml.etree.ElementTree as ET # 按实际数据集标签调整顺序决定类别ID CLASSES [tank, armored_vehicle] def convert(xml_path, label_dir): 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: print(f未知类别: {name} in {xml_path}) continue cls_id CLASSES.index(name) bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) base os.path.splitext(os.path.basename(xml_path))[0] out_path os.path.join(label_dir, base .txt) with open(out_path, w) as f: f.write(\n.join(lines)) os.makedirs(labels, exist_okTrue) for xml_path in glob.glob(Annotations/*.xml): convert(xml_path, labels)逻辑说明先把XML里size节点的宽高读出来当作归一化分母然后遍历所有object把类别名映射为类别ID再读bndbox四个坐标转换成YOLO五元组。坐标保留6位小数既能满足精度也不会让文件变大。参数说明CLASSES的顺序决定类别ID第0类是tank第1类是armored_vehicle后续训练yaml里names的顺序必须一致否则会出现标签错位。转换时遇到未知类别我选择打印并跳过而不是直接崩溃因为卫星图数据里“多写一个空格”“大小写不一致”太常见了先让转换跑完再回头统一处理这些异常。转换完检查一下输出文件数量和XML数量对比。如果少了文件多半是XML解析出错比如标注里出现负数坐标。这时候回到第2章说的体检步骤把那些异常的XML单独挑出来修掉。4.2 划分训练集与验证集按整图划分而不是按切块划分很多人在这一步偷懒直接对所有切块图随机按比例划分。这么做验证集精度会虚高得离谱——同一张原始大图切出来的块可能一半在训练集一半在验证集模型等于见过“同一片地”了。正确做法是先以原始图片ID为粒度划集合再对切块分批。# split_dataset.py按原图ID划分训练/验证/测试集 import os import random IMG_DIR JPEGImages # 原图目录 TRAIN_RATIO 0.8 VAL_RATIO 0.1 ids [] for f in os.listdir(IMG_DIR): if f.lower().endswith((.jpg, .jpeg, .png)): ids.append(os.path.splitext(f)[0]) random.Random(42).shuffle(ids) n_train int(len(ids) * TRAIN_RATIO) n_val int(len(ids) * VAL_RATIO) train_ids ids[:n_train] val_ids ids[n_train:n_train n_val] test_ids ids[n_train n_val:] with open(train.txt, w) as f: f.write(\n.join(train_ids)) with open(val.txt, w) as f: f.write(\n.join(val_ids)) with open(test.txt, w) as f: f.write(\n.join(test_ids))逻辑说明先收集原图IDshuffle后按比例切成三段每个txt每行一个ID。参数说明seed固定42保证后续实验可复现TRAIN_RATIO和VAL_RATIO可以按自己数据量调整验证集我一般保持不低于总图片数的10%。注意这里划分的是原图ID如果你已经做了切图需要用原图ID做前缀匹配让同一原图的切块全部进入同一个集合。另外一个细节如果官方给的ImageSets/Main存在可以直接用那是作者自己的划分更容易复现他们的报告。只有在自己需要重新划分或交叉验证时才重写。4.3 训练参数与验证指标从mAP看数据集质量训练配置先给一个数据集描述文件# armor_sat.yaml放在训练的 datasets 目录下 path: ./armor_sat train: images/train val: images/val nc: 2 names: 0: tank 1: armored_vehicle训练命令以YOLOv5/v8系列为例python train.py --data armor_sat.yaml --weights yolov5s.pt --img 1280 --batch 16 --epochs 100 --device 0命令参数说明img从默认640调到1280是因为目标框中位数小分辨率翻倍等于给模型更多可用的目标像素。batch 16是中等显存的常见配置显存不够降到8够用就往上加。epochs先跑100轮看val曲线是否在最后20轮还在涨如果还在涨就续跑。验证注意点不要只用切块的验证集算mAP还要抽几张大原图做滑窗推理全图检测效果才是这个数据集的真实水平。指标上重点看每类的AP以及mAP0.5和mAP0.5:0.95的差距——这两个数字差得多基本可以断定定位精度不足。下一章的坑2会详细说这个现象。5. 避坑卫星图目标检测常见的五个坑与对应解法以下五个问题是我在类似卫星数据集上反复踩过的。每条按现象、原因、解决三步写对照自己的实验记录排查比盲调参数有效得多。5.1 验证集mAP高换大图漏成筛子现象在切块验证集上mAP0.5能到0.85换到真实大图上目测只能检出四分之一。原因切图后随机划分数据集同一原始大图的切块同时出现在训练集和验证集模型见过了绝大部分纹理上下文验证集数字是“背题”的结果。另外全图推理和切块推理的尺度不一致模型没见过小目标在全景里的上下文特征漏检自然发生。解决按原图ID划分集合已经切好的块先记录归属原图ID再做分层划分。推理时不要一次性resize全图而是滑窗推理加NMS合并窗口尺寸和训练时保持一致。这一步做完验证集数字会“变难看”但真实场景表现会反过来变好。5.2 mAP0.5高但mAP0.5:0.95特别低现象mAP0.5能到0.8mAP0.5:0.95只有0.2左右目标能框住但框总是不够准。原因卫星目标像素少几个像素的偏移在IoU上被放大。IoU阈值从0.5提到0.75时小目标框稍微歪一点就低于阈值定位精度不足直接反映在0.95曲线上。解决输入分辨率提到1280或更大backbone和neck保持不变时锚框尺寸整体缩小边框回归的损失权重适当调高。如果框架支持多尺度训练把尺度范围从0.5到1.5拉大也能改善不同尺寸目标的定位能力。5.3 切图把目标切成两半标签被过滤掉现象训练集构建完成后目标总数比原始标注减少了三成验证时模型对小目标几乎无感。原因滑窗重叠设的是0或者MIN_KEEP设到0.9把大量边框被窗口边缘切断的目标过滤掉了。解决重叠设到128以上让同一目标至少完整出现在某一个窗口里。过滤阈值用0.7而不是0.9。切完图抽样看50个切块确认标注框和目标的贴合度再进训练。5.4 标签叫法不统一转换脚本直接崩现象voc2yolo脚本跑到一半报KeyError打开XML一看同一类目标三种名字。原因标注是分批次做的不同标注的人用了不同缩写比如tank、armor、armoured_vehicle、Tank都有。解决先用统计脚本把所有类别名列出来然后建一个映射表统一成一套标准名称再转格式。不要直接在转换脚本里一个个加elif那样标签会越补越乱。正确的做法是准备一个{原始标签: 标准标签}字典转换时先映射再写入YOLO文件。5.5 虚警一片模型把道路和屋顶当战车现象漏检不多但全图推理时框满天飞道路、停车场、屋顶全是误检。原因训练集里负样本太少模型没有足够多“这里没有目标”的样本去压制背景响应。小尺寸目标天然容易激活高分辨率特征图误报更多。解决加入背景切块组成的负样本。一张无目标大图可以切出几十个负样本块合成一个负样本目录。训练时正负样本比例控制在1:2到1:3左右对虚警有明显的抑制作用。如果虚警还是压不住再配合第6章的难例挖掘。6. 进阶用旋转增强与硬负样本把精度再拉一截6.1 旋转增强与标注同步变换卫星图和自然图像最大的区别是没有“正立”概念装甲车头朝哪个方向都可能。自然场景里常用的水平翻转能提供的角度变化非常有限建议在数据增强里加入90度、180度、270度旋转配合镜像翻转相当于把每个标注样本的有效朝向扩到八种。实现时需要注意标注同步变换。如果直接用训练框架内置的几何增强框架会自动转换标注框这是最省事的方式但如果自己写预处理一定要把中心和宽高坐标同步旋转旋转后框的宽高会互换这一点容易忽略。我常用的做法是先放弃任意角度的旋转增强改用90度、180度、270度加镜像用框架内置参数直接开启简单且不存在角度旋转后的坐标漂移问题。对卫星图来说离散角度带来的收益已经很大而且省心。6.2 背景负样本与难例挖掘把无目标切块直接合成负样本目录第3章说了做法。这里说难例挖掘第一轮训练结束后用当前模型对训练集里的背景图和大图上无目标窗口做推理把预测置信度高于0.3的样本挑出来加入训练集。这些是模型自己“认错”的样本针对性特别强对压低虚警非常有效。难例挖掘的节奏要控制好。第二轮加入的负样本比第一轮的更难模型会变得更保守如果发现mAP下滑说明负样本比例过大回调正负比例。这个平衡点每个数据集都不一样需要自己试两三次感觉它就相当于模型在这个数据集上的“脾气”。从那以后我每次拿到新数据集都会先花半小时跑分布统计再决定要不要切图、要不要加负样本、用多大输入分辨率切完图先抽50张人工看一眼再进训练。这套流程虽然不起眼却帮我避免过好几次“验证集好看、上线翻车”的尴尬。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进