遥感图像目标检测工程实战:基于JDet的旋转框检测全流程解析
发布时间:2026/9/28 14:02:30来源:尧图网络
简介围绕国际算法算例大赛中的遥感图像物体目标检测任务资料包含可直接运行的Python源代码、配套文档说明以及实验数据集主要面向计算机相关专业的在校学生、教师或企业开发者可用于毕设项目、课程设计、作业布置或竞赛初期的方案演示。代码模块已经测试通过运行稳定能够作为二次开发的基础若遇到运行问题还可私聊咨询远程教学。压缩包整体约7.55MB共405个文件其中以350个py脚本为主体辅以22个yaml配置文件和22个md说明文档另有csv结果表、ipynb数据分析与测试脚本、样例图片等目录划分明确方便按功能阅读、修改和排查问题。除了目标检测主流程外资源还提供数据预处理、模型配置、结果可视化与分析等环节的相关材料可帮助学习者从数据到算法完整走通一个遥感检测项目。目前已有272人学习下载对于希望获得可复现项目参考或准备相关算法竞赛的人群较有实用价值。1. 遥感图像物体目标检测资源一份能直接跑通的算法大赛基线工程遥感图像物体目标检测和自然场景检测最大的不同在于目标方向任意、排列密集、小目标成片出现直接把 YOLO 那套水平框迁移过来一个框里经常框住好几个目标NMS 之后漏检严重。这份国际算法算例大赛的遥感图像物体目标检测资源拿到手是一个包含 Python 源代码、文档说明、数据集三个部分的完整工程基于 JDet 旋转框检测框架把从数据分析、模型推理到结果合并输出的整条链路都跑通了。它适合两类人一类是备赛或做毕设的学生需要一条能直接运行的基线和一份参赛提交格式的参考另一类是刚接触遥感目标检测的工程师想看看 DOTA 格式标注、旋转框预测、CSV 结果合并这些环节在一个真实项目里是怎么串联的。我把它拆开过了一遍下面按「框架选型 → 环境与数据 → 推理输出 → 踩坑 → 进阶」把可复现的部分逐一讲清楚。2. 旋转框检测与 JDet为什么遥感目标不能用水平框硬套2.1 旋转框与水平框的核心差别自然场景的目标检测大多假设目标可以近似用轴对齐的矩形框表达模型输出的是cx, cy, w, h四个值或者x1, y1, x2, y2两个对角点。遥感影像里飞机、舰船、储油罐、棒球场这些目标在俯视视角下朝向完全随机水平框有两个致命问题一是框内会混入大量背景和相邻目标特征被污染二是两个朝向不同的目标挨在一起时水平框之间的 IoU 非常高NMS 一压就误删了其中一个。所以遥感检测竞赛里主流做法是用旋转框OBBOriented Bounding BoxDOTA 数据集的标注格式就是每个目标用四个角点共 8 个坐标描述x1, y1, x2, y2, x3, y3, x4, y4再加类别和 difficult 标志。实际训练时模型一般会把四点转成cx, cy, w, h, theta的形式theta 表示框的朝向角。JDet 这个工程库做的事情就是把 DOTA 这种四点标注的数据读进来、转成旋转框表示再交给旋转框检测头去回归。# 一个典型的 DOTA 标注行 # imagesource: Google Earth # gsd: 0.3 327,186,393,174,441,247,375,259,plane,0上面这行标注里前 8 个数字是目标四个角点的像素坐标第 9 个是类别第 10 个是 difficult 标志。我在处理数据时习惯先写一个小函数把这个 txt 读成 numpy 数组方便后续做统计分析和可视化这也是 JDet_README 里提到的数据接入方式。2.2 工程文件结构说明了什么项目压缩包解开后的文件不算多但每个文件的定位非常清晰。README.md和JDet_README.md是两份文档前者偏项目使用说明后者是 JDet 框架自带的说明包括环境依赖、数据格式和训练命令。两个 notebook 文件分工明确data_analysis.ipynb负责训练前的数据分析test_field.ipynb负责测试阶段推理merged_result.csv是预测结果合并后的提交文件idx.csv是测试集的图片索引vis2.jpg是检测结果的可视化样例。这套文件组织在算法大赛参赛项目里是高分的常见结构先分析数据分布再训练或加载模型做推理最后把结果合并导出。很多人在毕设里只交一个训练好的权重和一堆测试图片没有中间的坐标还原、CSV 合并环节评审一看流程就不完整。这份资源的参考价值恰恰在于它把「预测结果 → 提交文件」这最后一段补全了这也是我建议下载后重点读merged_result.csv原因格式本身就是一个答案模板。2.3 JDet 的核心逻辑与配置入口JDet 属于旋转目标检测框架里比较成熟的一支底层依赖 PyTorch 和 MMDetection 的组件支持 Rotated RetinaNet、RoI Transformer、S2ANet 这一票旋转框模型。选择它而不是直接用 YOLO 改是因为旋转框模型的回归分支比水平框多一个角度参数数据加载、anchor 分配、loss 计算、NMS 都要针对角度做特殊处理自己从零写不现实站在 JDet 的肩膀上能把精力放在调参和数据处理上。# JDet 训练一个旋转框模型的典型命令 python tools/train.py configs/rotated_retinanet/rotated_retinanet_r50_fpn_1x_dota.py \ --work-dir work_dirs/rotated_retinanetconfigs/rotated_retinanet/...是模型配置文件的路径--work-dir指定输出目录checkpoint 和日志都会写进去。实际使用中我会先确认配置文件里的num_classes是不是自己的类别数再确认data_root指向的数据集目录结构这两处是新手最容易忽略的。评估时用python tools/test.py加--eval mAP它会按 DOTA 比赛的标准评估逻辑计算 mAP而不是普通 VOC 那套指标。3. 环境搭建与数据准备把 DOTA 流程从零跑起来3.1 Python 环境与依赖版本匹配这份资源要在本地跑起来第一步是 Python 环境。JDet 这类基于 MMDetection 的工程对版本匹配比较敏感不是装个最新版 torch 就能通的。常见做法是 Python 3.7 到 3.9PyTorch 1.6 到 1.10 之间MMCV 版本要和 PyTorch 版本对应上否则 import 阶段就会报错。conda create -n remote_det python3.8 -y conda activate remote_det pip install torch1.8.0 torchvision0.9.0 pip install mmcv-full1.4.0 -f https://download.openmmlab.com/mmcv/dist/cu111/torch1.8/index.html pip install mmdet2.25.0这段命令里conda 创建虚拟环境是隔离依赖的第一步我一般不用裸的 pip 装 torch而是用 conda 管理 Python 版本。mmcv-full 的下载地址里cu111表示 CUDA 11.1torch1.8表示对应 PyTorch 1.8如果你的 GPU 驱动版本不同需要改成对应的 cu102、cu116 这些标识。装完这几层再装项目自身依赖最后能正常import jdet就说明环境通了。如果跑 notebook 时 kernel 选错环境也会出现 ModuleNotFoundError记得在 Jupyter 里把 kernel 切到remote_det。3.2 DOTA 数据集格式与目录组织JDet 默认读取的是 DOTA 格式的数据集目录结构一般长这样images目录放原始遥感影像labelTxt目录放同名 txt 标注文件还需要一个trainval.txt或test.txt列出参与训练或测试的图片名称。这份资源自带的数据集如果也是这个结构直接替换图片和标注就能复现训练流程。我在拿到一套新标注数据时会先跑一次快速的格式检查脚本把所有 txt 文件读出来统计每个类别的目标数量和目标尺寸分布。这个脚本很重要因为标注里常见的问题是坐标越界、类别名拼写不一致、某些 txt 文件为空而这些在一开始的统计分析里就能暴露。import os import numpy as np label_dir data/dota/labelTxt cls_counter {} size_list [] for fname in os.listdir(label_dir): with open(os.path.join(label_dir, fname)) as f: for line in f: parts line.strip().split(,) if len(parts) 9: continue coords np.array(parts[:8], dtypenp.float32) cls parts[8] cls_counter[cls] cls_counter.get(cls, 0) 1 xs coords[0::2] ys coords[1::2] w max(xs) - min(xs) h max(ys) - min(ys) size_list.append((w h) / 2) for k, v in sorted(cls_counter.items(), keylambda x: -x[1]): print(k, v)这段代码的作用是把每个类别出现的次数统计出来同时估算每个目标的平均边长。coords[0::2]取出四个角点的 x 坐标coords[1::2]取出 y 坐标然后计算外接矩形的宽高。参数上唯一要留意的是len(parts) 9这个判断因为 DOTA 的 difficult 字段有时缺失我用它把不完整的行过滤掉避免后面训练时崩在数据解析上。3.3 切图窗口大小与重叠率怎么定遥感影像通常是大图比如几千乘几千像素甚至更大直接整图丢进网络显存不够而且目标太小。DOTA 比赛的标准做法是把大图切成 1024×1024 的小块相邻窗口之间留重叠区域防止目标正好被切在边界上。切图的同时标注里的角点坐标也要减去窗口起点换到子图的坐标系里。def crop_image_and_boxes(img, boxes, crop_size1024, overlap200): h, w img.shape[:2] patches [] new_boxes_list [] step crop_size - overlap for y in range(0, max(h - crop_size, 1), step): for x in range(0, max(w - crop_size, 1), step): x2, y2 min(x crop_size, w), min(y crop_size, h) sub_img img[y:y2, x:x2] sub_boxes [] for box in boxes: pts box[:8].copy() xs pts[0::2] - x ys pts[1::2] - y in_flag (xs 0) (xs crop_size) (ys 0) (ys crop_size) if in_flag.sum() 4: new_pts np.stack([xs, ys], axis-1).reshape(-1) sub_boxes.append(np.concatenate([new_pts, box[8:]])) keep_areas [] for b in sub_boxes: area (max(b[0::2]) - min(b[0::2])) * (max(b[1::2]) - min(b[1::2])) keep_areas.append(area 100) sub_boxes [b for b, k in zip(sub_boxes, keep_areas) if k] if len(sub_boxes) 0: patches.append(sub_img) new_boxes_list.append(sub_boxes) return patches, new_boxes_list这里crop_size1024是窗口边长overlap200是窗口重叠宽度step crop_size - overlap等于 824也就是窗口每次平移 824 像素。四个角点必须全部落在窗口内才保留这是 DOTA 切图的常见约束。area 100是为了过滤掉切图后被截得太小的碎片这类碎片不仅没有标注价值还会给训练带来大量难负样本。实际调参时如果图像里小目标特别密集我会把crop_size降到 800、overlap提到 300代价是生成的小图数量翻倍训练时间变长。4. 推理与结果合并从 notebook 到 merged_result.csv4.1 test_field.ipynb 的推理主流程test_field.ipynb在测试阶段做的事简单说就是加载训练好的 checkpoint对切好的测试子图逐张推理输出旋转框、置信度和类别。notebook 的好处是把每一步都留在单元格里跑完能看到中间结果比纯脚本更适合调试。推理时我会先手动指定权重路径和配置路径再调inference_detector那类接口。JDet 的配置里model.test_cfg决定了 NMS 的阈值旋转框 NMS 和普通 NMS 不一样它对角度敏感阈值一般设在 0.4 到 0.5 之间。如果你发现同一目标被输出两个重叠框大概率是 NMS 阈值太高或者没有按角度对齐做旋转 IoU 计算。from jdet.model import build_detector from jdet.utils.registry import build_from_cfg # 加载训练好的权重 checkpoint work_dirs/rotated_retinanet/epoch_12.pth import torch state_dict torch.load(checkpoint, map_locationcpu)[state_dict] model build_detector(cfg.model, test_cfgcfg.model.test_cfg) model.load_state_dict(state_dict) model.cuda().eval()这段代码的核心是load_state_dict与eval()的配合。加载权重时我习惯加map_locationcpu避免模型在 CPU 机器上加载到一半报 CUDA 错误eval()必须显式调用它会关闭 dropout 和 BatchNorm 的统计更新这和训练阶段的 forward 行为完全不同。新手上手最容易翻车的就是忘了eval()导致每次推理结果抖动。4.2 从子图坐标还原到原图坐标切图推理的最大坑在坐标还原。模型对 1024×1024 子图输出的框都是子图坐标系要合并成最终结果必须把x win_x、y win_y加回去。我在项目里见过不少人训练 mAP 很高一提交分数就崩绝大多数是这一步坐标映射写错了。def local_to_global(boxes, win_x, win_y): result [] for b in boxes: x1, y1, x2, y2, x3, y3, x4, y4 b[:8] result.append([ x1 win_x, y1 win_y, x2 win_x, y2 win_y, x3 win_x, y3 win_y, x4 win_x, y4 win_y, b[8], b[9] ]) return result这个函数做的事情很朴素对每个旋转框的 8 个坐标分量分别加上窗口左上角在原图中的位置。参数win_x、win_y是切图时窗口在原图中的偏移量b[8]是类别b[9]是置信度。合并到全图之后还需要再做一次全局旋转 NMS因为同一目标可能出现在相邻两个窗口中两次各自都有高置信度预测不 NMS 就会产生重复框。JDet 的旋转 NMS 实现在jdet.ops里可以直接调也可以用shapely先做多边形 IoU 再自己过滤。4.3 merged_result.csv 的格式与合并技巧打开merged_result.csv会看到每一行对应一个预测目标列结构一般是图片名加 8 个坐标、类别、置信度。这是 DOTA 评测任务的标准提交格式评测脚本拿到这个 CSV会和 ground truth 的旋转框做匹配按不同 IoU 阈值计算 mAP。idx.csv则记录测试集图片的顺序合并时用它来对齐每个窗口的结果属于哪张原图。import pandas as pd df pd.read_csv(merged_result.csv, names[image_id, x1, y1, x2, y2, x3, y3, x4, y4, class, score]) df df[df[score] 0.05] df df.sort_values(score, ascendingFalse) df.to_csv(submit.csv, indexFalse, headerFalse)这里的操作逻辑是读取合并结果按置信度阈值0.05过滤掉低分框再按分数降序排列。DOTA 评测只关心每张图预测框的相对顺序吗不是分数降序排列是为了让评测脚本在计算 PR 曲线时能有正确的置信度排序这一步影响 ap 曲线面积不能省。score 0.05是一个经验值如果目标特别小或者特别难可以放宽到 0.02但随之而来的是结果文件变大、误检增多。4.4 用 data_analysis.ipynb 反推训练参数data_analysis.ipynb的作用经常被低估其实它应该是最先打开的文件。它能告诉你数据集的类别分布和目标的尺度分布这两个信息直接决定训练参数如果目标平均边长在 20 像素到 60 像素anchor 的基础尺寸就不应该按自然场景的[32, 64, 128]来设而要整体调小如果某些类别样本数只有几十个训练时的类别权重就得调整。vis2.jpg是推理结果的可视化我一般会拿它和 ground truth 图对比看漏检集中在哪些类别是角度预测偏了还是小目标根本没检出来这样调参才有方向而不是盲试。5. 五个避坑记录切图、标签、显存与坐标还原5.1 切图后目标被切断小目标漏检严重现象训练时 loss 能下降但验证集 mAP 特别低尤其小目标类别几乎为 0可视化发现很多目标被窗口边界切成了一半。原因切图窗口是 1024×1024、重叠 200目标平均尺寸 60 像素时理论上不易被切但如果锚框面积阈值设得过高被切掉一半的目标在过滤阶段直接丢弃等效于训练样本变少。还有一类是切图时没有做全图铺满图像最右和最下边缘的窗口起点超过w - crop_size后循环直接跳过了边缘区域边缘目标永远进不了训练。解决切图循环的终点改成range(0, max(h - crop_size, 1), step)保证最后一个窗口能覆盖到图像边缘过滤小目标的条件从「四个角点都在窗口内」放宽为「目标与窗口的交集面积占原目标面积的 50% 以上」再配合overlap300减少被切概率。把那 100 像素的面积阈值去掉改用保留完整目标加截断目标各自的权重。5.2 推理时报 KeyError 或维度不匹配输入图片是 4 通道现象test_field.ipynb跑某一张图报KeyError: img_metas或者模型 forward 时维度对不上检查图片才发现有的影像保存成了 RGBA 四通道甚至 16 位深度的 TIFF。原因遥感影像来源杂卫星图、航拍图有时带近红外波段或 Alpha 通道JDet 的数据 pipeline 默认按三通道 8 位图处理多一个通道直接打乱预处理逻辑。解决测试阶段加一个统一读取函数把图片强制转成 RGB。我用的是cv2.imread(path, cv2.IMREAD_COLOR)它会自动丢掉 Alpha 通道并转为三通道 8 位图16 位图需要先除以 65535 再乘 255 做归一化。这一步放在推理脚本的最前面比放在模型里修更省事。import cv2 img cv2.imread(test/images/P0001.png, cv2.IMREAD_COLOR) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB)这段代码重点在于IMREAD_COLOR强制三通道读取以及对 BGR 到 RGB 的转换。JDet 训练时的数据 pipeline 默认按 RGB 顺序读图如果直接喂 BGR模型输出的结果会整体漂移。我一般在写推理脚本时都会随手加这一句几乎成了肌肉记忆。5.3 单张测试图显存溢出batch size 调小还是炸现象单卡 11GB 显存推理一张 1024 的图就 OOM报CUDA out of memory。原因旋转框模型里的 RoI Align Rotated 或角度回归分支比普通检测模型更吃显存而且很多人是在训练参数下直接跑推理num_gpus2、batch_size2这些训练配置没改。另一个隐藏原因是开启了 TTA多尺度翻转每张测试图被放大成 6 个输入显存瞬时翻几倍。解决推理时把 batch size 强制设为 1torch.cuda.empty_cache()在每张图之后调用TTA 在测试阶段先关掉跑通后再按需开启。我一般还把输入尺寸从 1024 缩到 800 做快速验证确认流程无误后再用完整尺寸。如果还爆显存检查有没有其他进程占用 GPUnvidia-smi看一眼最直接。5.4 提交 CSV 坐标错位可视化没问题但线上分数为 0现象本地可视化图框位置准确但提交到评测系统分数为 0或者 mAP 只有个位数检查 CSV 发现坐标列和原图对不上。原因切图推理后坐标还原漏了窗口偏移或者把x, y顺序写反了x4, y4变成y4, x4还有一种是把归一化坐标当成了像素坐标模型输出 0 到 1 的相对值未乘回原图尺寸。解决写一个回读脚本把 CSV 里的框画回原图和预测可视化做逐张对比。这个自查脚本一定要在提交前跑我后来把坐标还原做成了独立函数并在脚本里断言x2 x1 and y2 y1才继续简单粗暴但有效。画框代码用cv2.polylines把四点连起来即可这类问题肉眼一眼就能看出来。5.5 合并多个窗口结果后大量重复框现象同一目标出现在 3 个窗口里合并后输出 3 个几乎重合的旋转框最终 mAP 因为重复预测被严重扣分。原因窗口重叠区域内的目标天然会被多个窗口各自检出合并时只拼接坐标不做去重评测的匹配逻辑会把这些多余的框判定为 false positive。解决全局做一次旋转框 NMS。先用shapely.geometry.Polygon计算两个框的交并比再按置信度从高到低逐个淘汰与高置信框 IoU 超过 0.4 的低置信框。这样一个目标只保留一个最高分预测评测分数能肉眼可见地回升。6. 进阶用法用自己的遥感数据集复现整套流程如果你手上没有 DOTA 官方数据而是自己标了一批遥感影像也能用这套资源复现完整流程。关键是把你自己的标注转成 DOTA 的四点格式意思就是每张图配一个同名 txt每行写目标的四个角点坐标加类别。我用 LabelMe 标过一批港口影像LabelMe 导出的是多边形 JSON写个十几行脚本就能完成转换。import json, glob, os for json_path in glob.glob(labels/*.json): with open(json_path) as f: data json.load(f) image_name os.path.basename(json_path).replace(.json, ) out_lines [] for shape in data[shapes]: label shape[label] pts shape[points] if len(pts) 4: pts pts [pts[0]] * (4 - len(pts)) flat [str(round(v, 1)) for p in pts for v in p] out_lines.append(,.join(flat [label, 0])) with open(fdota/labelTxt/{image_name}.txt, w) as f: f.write(\n.join(out_lines))这段转换脚本里json_path遍历标注文件pts是多边形顶点不足四个点就补首点凑成四边形最后写成x1,y1,...,x4,y4,class,0的行。round(v, 1)是保留一位小数遥感标注没必要精确到亚像素反而能减少文件体积。转换完成后用第 3.2 节的统计脚本检查一遍类别名称和坐标范围再进入切图训练流程。自己数据集上最值得调的参数有三个窗口大小、anchor 尺寸、NMS 阈值。我用data_analysis.ipynb统计目标外接矩形边长的中位数如果中位数是 30 像素切图窗口用 800anchor 基础尺寸从[8, 16, 32, 64]起步如果中位数是 100 像素窗口回到 1024anchor 也要等比放大。NMS 阈值在密集场景先设 0.35稀疏场景可以放到 0.5每次改完都重新跑一遍测试集观察重复框变化。我自己的习惯是每拿到一批新遥感数据先强制走一遍「格式检查 → 尺寸统计 → 小目标过滤 → 局部可视化」这个前置流程再碰训练参数。这样做的直接收获是后面所有训练和推理环节都不会因为数据问题瞎折腾定位问题永远比盲目调参快。这份资源真正的价值不在于某个模型有多强而在于它把遥感检测项目里最容易被忽略的工程环节都摆出来了照着它的套路把数据、代码、结果串起来你会有一种「原来这个流程是这样闭合的」的实感。希望帮到你动手复现时记得先跑通推理再谈调参。本文还有配套的精品资源点击获取
网站建设高端定制企业官网