☰
遥感图像旋转框目标检测:基于JDet的源码包复现指南
2026/10/2 8:38:01 网站建设 项目流程

简介:基于Python的遥感图像物体目标检测完整项目包,面向计算机视觉方向的学生、研究者及从业者,适合用于毕业设计、课程大作业或项目初期演示。项目以FAIR1M2.0遥感监测数据集为基础,提供可运行的源码、预训练模型及详细项目说明,覆盖环境安装、数据格式整理、训练与推理流程,并针对大尺寸影像给出灰度化与预处理方案,便于复现和二次开发。资源共404个文件,含350个Python脚本、22个YAML配置、22个Markdown说明文档及少量数据表格与可视化图,整体压缩包约7.54MB,结构清晰,上手门槛适中。已有729人学习下载,内含环境依赖清单、数据集目录规范、模型训练与测试示例,可帮助快速搭建遥感目标检测实验环境,也支持在此基础上拓展新类别或调整网络结构,是完整可用的毕业设计/课程设计参考实现。

1. 遥感图像物体目标检测:这个Python源码包到底解决什么问题

遥感图像物体目标检测和普通目标检测是两码事:卫星图里的飞机、舰船、油罐往往只有几十像素,方向任意、背景杂乱,用普通YOLO的水平框去框,不是框进大量背景,就是把相邻目标并成一个。这个基于Python的遥感图像物体目标检测源码+模型包,是在JDet旋转框框架上跑通的完整方案。资源里带了FAIR1M2.0数据集预处理流程、模型配置、推理和数据分析notebook,以及一份可参考的检测结果,解压后改一下数据路径就能看到从训练到评估的全流程,核心价值是把「旋转框」这件事真正落地。

如果你做过自然图像检测,会发现在遥感图上,水平框的NMS经常把两个相邻目标误删一个;换成旋转框以后,这个误删概率明显下降。这正是这个源码包和普通YOLO项目的本质区别。适合毕业设计、课程大作业复现的学生,也适合想在JDet上做二次开发但不想从零配环境的工程师:先跑通它,再替换成自己的数据。

2. 环境安装与数据准备:先把这份项目的底座搭起来

2.1 环境依赖与安装步骤:为什么是setup.py develop

先说环境。整个项目基于JDet,这个框架用PyTorch写,针对旋转框检测做了很多定制,比如RoI Transformer、旋转RoI对齐,都会用到一些PyTorch官方没有的算子。安装时项目说明给的命令是:

# 进入项目根目录(下载解压后先重命名为RS_detect) cd RS_detect # 安装依赖库 python -m pip install -r requirements.txt # 以develop模式安装jdet包 python setup.py develop

这里有两处容易忽略。第一,python -m pip install会同时装PyTorch、torchvision、numpy、opencv、tqdm这一批东西,但PyTorch的版本必须和你的CUDA版本匹配,如果显卡驱动比较老,装默认的torch大概率在import torch时报错。我一般会先用conda create -n rs python=3.8建一个干净环境,再按pip list查torch版本,让CUDA和torch对应上。如果你的机器没有NVIDIA显卡,也别直接放弃,CPU版torch也可以跑推理,只是训练会很慢。

第二,setup.py develop和setup.py install不一样。develop是用软链接把当前目录变成可导入的包,你改了jdet下的源码不需要重装;如果用了install,后面想看源码里某个算子的具体实现,改了还得重新编译。尤其这种毕业设计、课程项目,大概率会改代码,所以按说明走develop是省事的。

装完之后不要急着跑训练,先做个最小验证,确认jdet能被正常import,这个动作能省掉后面百分之八十的报错排查时间。在项目根目录执行:

python -c "import jdet; print(jdet.__file__)"

能输出jdet的路径就说明环境OK。如果报ModuleNotFoundError,先确认你是不是在RS_detect目录下执行的,因为jdet不是全局注册的包,必须在项目根目录才能被找到;这是后面最常见的一个坑,我会在第五章单独展开。另外,python setup.py develop执行时如果看到类似error: [Errno 2] No such file or directory的提示,通常是你没有在项目根目录运行这条命令,而不是代码有问题。

2.2 FAIR1M2.0数据集目录规范:train_color和train_gray的划分

模型是在FAIR1M2.0遥感数据集上训练的,这个数据集可以从官方高分遥感挑战赛的benchmark入口下载,解压以后目录结构和项目要求对不上是常态。项目说明里要求把数据集整理成下面这种格式:

{DATASET_PATH} └── data ├── train_color │ └── train │ ├── images │ │ ├── 1.tif │ │ └── ... │ └── labelXml │ ├── 1.xml │ └── ... └── train_gray └── train ├── images │ ├── 1.tif │ └── ... └── labelXml ├── 1.xml └── ...

{DATASET_PATH}可以放在任何你方便的位置,项目里的config会去读它。注意一个细节:把FAIR1M2.0原始train和validation合并后叫做train_color,这是为了增加训练样本量,因为FAIR1M的验证集也有标注,丢掉可惜。然后把分辨率超过2500x2500的大图做灰度化,灰度化的副本单独存成train_gray,train_color里保留原图。相当于一份数据两个入口,训练时按需取用。

为什么这样分?遥感原始图动辄8000x8000甚至更大,直接送进网络显存根本扛不住。灰度化不是用来看图的,而是让模型在训练时就见过两种模态的数据:彩色图和小幅面,灰度图和大尺寸。推理时如果输入是大图,也走灰度分支,这样显存占用和计算速度都更可控。这套「双数据源」的做法在遥感检测项目里很常见,属于工程妥协,但对训练稳定性帮助很大。

实际操作时,灰度化可以写一个脚本批量处理,核心逻辑就是cv2.cvtColor加上按尺寸判断:

import os import cv2 def grayscale_large_images(src_img_dir, dst_img_dir, threshold=2500): for fn in os.listdir(src_img_dir): img = cv2.imread(os.path.join(src_img_dir, fn)) if img is None: continue h, w = img.shape[:2] if max(h, w) > threshold: gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) img_gray_3c = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR) cv2.imwrite(os.path.join(dst_img_dir, fn), img_gray_3c) grayscale_large_images("data/train_color/train/images", "data/train_gray/train/images")

这段代码把大于2500像素的长边转灰度,再复制成三通道写出。为什么要复制回三通道?因为JDet的backbone输入是B, 3, H, W,直接喂单通道图反而要改数据加载器,复制回三通道是最小改动。你如果不想改原图,也可以在这里把labelXml一并复制到对应目录,保持images和labelXml成对存在。

2.3 标注格式和合并策略:从labelXml到旋转框

FAIR1M的标注是XML,每个object里描述目标类别和一个四边形框,用四个点的图像坐标表示。原始XML常见的字段是<points>或者一组<x1, y1>...<x4, y4>,代表旋转框的四个顶点,而不是水平框的两个对角点。这和VOC格式很不一样,用通用标注工具导出的时候要确认选的是旋转框而不是矩形框。

如果你想把标注查一遍,可以用下面这个脚本,把XML里的点和类别打印出来,顺便统计每个类别的数量。这个脚本不是为了训练,而是帮你快速确认标注字段名和边界框类型,避免后面训练时格式错了都不知道。

import xml.etree.ElementTree as ET def parse_fair1m_xml(xml_path): tree = ET.parse(xml_path) root = tree.getroot() objs = root.findall("object") for obj in objs: name = obj.find("name").text box_node = obj.find("rotated_box") if box_node is None: box_node = obj.find("points") coords = [] for coord in box_node: x = float(coord.attrib["x"]) y = float(coord.attrib["y"]) coords.append((x, y)) if len(coords) == 4: print(name, coords) parse_fair1m_xml("labelXml/1.xml")

这段代码的核心是把XML里四边形的四个点读出来,后面对接JDet训练时,再把它转成x, y, w, h, theta这种五参数旋转框。注意不同版本的FAIR1M XML字段名不一样,有的是rotated_box,有的是points,如果你发现读出来全是None,用文本编辑器打开一个XML看看字段名,把代码里的字符串改成实际的就行。

合并train和validation的策略听着简单,操作上要注意:不能只把文件copy到同一个目录,还要检查有没有同名文件编号冲突。FAIR1M的train和validation图片编号可能都是从1开始的,合并前最好给image和labelXml都加同一个前缀,比如val_1.tif,否则后面训练时读图会互相覆盖。项目说明没细说这一点,但我实际操作中吃过这个亏,建议先做一步预处理再合并,合并后用ls images | wc -l和ls labelXml | wc -l核对数量,两边一致才能进训练。

3. 模型配置与训练:搞懂旋转框检测和config.md里的关键参数

3.1 旋转框检测和水平框检测的核心区别

普通检测器的输出是水平框(x, y, w, h),遥感场景下目标密集且方向任意,水平框的弊端很明显:两架相邻飞机,水平框IoU可能很高,NMS直接把其中一个框当成重叠框删掉,造成漏检。旋转框检测器回归的是带角度的框(x, y, w, h, theta),让NMS真正作用在目标重叠区域而不是背景区域上。同样是飞机,水平框之间的重叠可能超过0.6,但旋转框之间的真实IoU只有0.1,这就是旋转框在遥感上优于水平框的根本原因。

JDet这类框架的核心区别在检测头。常见做法是先用RPN产生符合目标长宽比的水平候选框,再通过旋转RoI对齐把候选框调整成带角度的框。训练时的回归目标从四元数变成五元数,角度误差成了新的回归损失。角度这个回归目标有周期性,也容易在边界处产生突变,所以JDet在定义角度量词时会有范围限制,比如限定在[-90, 0)或者使用带符号的八点表示。你在config里看到的相关参数,本质上都在处理角度的周期性和边界问题。

这个源码包里除了训练好的权重,还带了config.md和JDet_README.md,说明它是按旋转检测的路子组织的。你不需要一开始把每个模块都看懂,但至少要知道模型输出的是什么:每个目标有一个五元组框、一个类别、一个置信度,后处理阶段还要对旋转框做NMS。这些在后面的推理代码里都会出现。如果之前用过yolov8之类做目标检测,可以先想清楚yolov8的水平框后处理函数不能直接套用,因为旋转框的NMS计算IoU的方式不同。

3.2 config.md里的关键参数:backbone、学习率、锚框和训练轮数

config.md不是用来通读的,它是整个项目的调参入口。以这份资源为例,打开后重点看三个区:模型结构、数据集路径、优化器。

# 典型的JDet配置片段,具体数值以你下载的config.md为准 backbone: type: ResNet depth: 50 pretrained: True dataset: train: type: FAIR1M root: /your/dataset/path split: train_color batch_size: 4 optimizer: type: SGD lr: 0.0025 weight_decay: 0.0001 schedule: epochs: 12 lr_step: [8, 11] test_cfg: nms_threshold: 0.4 score_threshold: 0.3

backbone.depth决定了特征提取的能力,50是ResNet50,想更轻就用34,想更高精度就换101,代价是显存和训练时间成倍涨。pretrained: True指用ImageNet预训练权重初始化backbone,遥感图像虽然和自然图像差很远,但预训练权重依然能加速收敛,尽量不要关掉。batch_size是个关键数字:它直接等于显存占用,遥感大图一般只能开到2到4,这很正常,不要和别人跑COCO的batch_size对比。

lr在迁移学习场景下常用0.0025附近,如果从零训练要调小到0.0001量级。lr_step是学习率衰减的epoch节点,比如到第8和第11轮降一次,能帮助损失在后期稳定收敛。weight_decay用来抑制过拟合,遥感数据集类别多、样本不均衡,这个值一般保持默认。nms_threshold和score_threshold虽然写在test_cfg里,但推理时也会用到。score_threshold太高会漏检小目标,太低会出现一堆低置信度重复框。对于遥感影像,小目标多,我一般先把score_threshold放在0.3,等看过验证集PR曲线再往回调。nms_threshold对旋转框一般设在0.4左右,比水平框的0.5低一些,因为旋转框之间实际的遮挡更少,过高的NMS阈值容易保留大量重复框。

如果你打开config.md发现里面还有anchor相关的配置,比如anchor_scale、anchor_ratio、anchor_angle,也不要慌。锚框是在RPN阶段预置的一组候选框,尺度要覆盖目标尺寸范围,长短比要覆盖常见目标形状,角度则要覆盖目标可能出现的朝向。FAIR1M里的目标大小差异很大,小到十几像素的车,大到几千像素的油罐,一套anchor不够用就两套。调试顺序应该是:先确认anchor尺寸覆盖了数据里目标的大小,再看回归loss是否正常下降。

3.3 启动训练与checkpoint输出:命令行参数和训练日志怎么看

环境安装好、数据组织对、config改完,训练命令在JDet里一般是这样:

python tools/train.py --config config.py

如果你下载到的项目里没有tools/train.py,可以先看JDet_README.md,里面会写标准的入口脚本名,或者直接用项目说明里的启动方式。顺便提一句,有的项目要求先执行python setup.py develop才能跑这条命令,是因为训练脚本内部import jdet依赖了这个包的安装,不要跳过。

训练启动后,正常会先看到数据加载的日志,然后是每个iter的loss输出:

2025-01-01 10:00:00 | epoch 1 iter 100 | loss 1.234 | lr 0.0025

这里有两个检查点。第一,loss有没有在降。前十几个iter loss波动是正常的,但如果500个iter后loss还在同一数量级乱跳,先看数据是不是空的、标注和图片是不是对应上了,而不是急着改模型。第二,checkpoint会存在work_dirs或者outputs目录下,每个epoch存一个.pth,文件名里带epoch号。训练中断也没关系,JDet里通常有--resume参数指向最近的checkpoint,继续训,不需要从头开始。我经常在跑长训练时用nohup python tools/train.py > train.log 2>&1 &把日志落盘,这样ssh断开也不影响训练,出错了也能回头翻日志。

如果你想看训练中每个类别的精度变化,可以在配置里打开验证集eval,每个epoch结束自动跑一次mAP。不过要注意,遥感数据集类别多,每个epoch跑eval会增加不少时间,建议先跑几个epoch不eval,确认loss在合理下降,再打开eval开关。整个训练过程最影响结果的其实是数据预处理和anchor设置,而不是网络结构本身。你可以把训练看成「数据喂给模型,模型拟合标注」的过程,如果标注框本身是水平框而非旋转框,那模型学到的角度永远不对。所以这一步宁可多花一小时检查数据,也不要盲目调模型。

4. 推理与评估:跑通自己的影像并搞清楚结果文件

4.1 加载训练好的权重做单图推理

拿到权重后,第一件事不是重新训练,而是先跑一次推理,看看模型在未见过的遥感影像上究竟效果如何。这步能直观判断checkpoint质量,也能确认推理pipeline有没有漏掉灰度化这类关键步骤。JDet的推理代码逻辑一般是:

import torch import jdet from jdet.config import load_cfg_from_file from jdet.models import build_detector # 1. 读配置 cfg = load_cfg_from_file("config.py") # 2. 构建模型并加载权重 model = build_detector(cfg.model) checkpoint = torch.load("work_dirs/epoch_12.pth", map_location="cpu") model.load_state_dict(checkpoint["state_dict"]) model.eval() # 3. 读图并预处理 import cv2 img = cv2.imread("test.tif") with torch.no_grad(): outputs = model([img]) # 4. 解析旋转框结果 for box in outputs[0]: x, y, w, h, theta, score, cls = box if score > cfg.test_cfg.score_threshold: print(f"cls={cls}, score={score:.3f}, box=({x:.1f},{y:.1f},{w:.1f},{h:.1f},{theta:.2f})")

代码里map_location="cpu"是为了让你在没GPU的机器上也能先加载权重看结构,实际推理还是建议放到GPU上。model.eval()必须调用,否则BatchNorm和Dropout的行为不一致,输出的结果会偏。输出的theta单位是弧度还是角度,取决于训练时配置,打印出来后看一眼数值范围就能判断。如果theta在0到2π之间多半是弧度制,在-90到90之间多半是角度制,画框时要做对应的旋转。

如果你手上没有测试图,可以用数据集里任何一张1.tif先跑。遥感影像可能超过100MB,全部读进内存之前先缩放到2000像素以内,因为推理时不是原始分辨率越高越好,模型训练时如果见过的目标是经过缩放的patch,推理大图反而会因为目标尺度过大而漏检。想画检测框可视化,可以顺手加几行:

import cv2 import math def draw_rotated_box(img, box, score_thr=0.3): x, y, w, h, theta, score, cls = box if score < score_thr: return # 角度制则转弧度 rad = math.radians(theta) if abs(theta) < 180 else theta cos_t, sin_t = math.cos(rad), math.sin(rad) corners = [ (x - w/2, y - h/2), (x + w/2, y - h/2), (x + w/2, y + h/2), (x - w/2, y + h/2) ] pts = [] for px, py in corners: nx = x + (px - x) * cos_t - (py - y) * sin_t ny = y + (px - x) * sin_t + (py - y) * cos_t pts.append((int(nx), int(ny))) cv2.polylines(img, [pts], True, (0, 255, 0), 2) for box in outputs[0]: draw_rotated_box(img, box) cv2.imwrite("vis2.jpg", img)

这段画框代码把五参数框换算成四个顶点,再画一个闭合多边形。注意theta的定义是以哪个轴为参考、顺时针还是逆时针,不同数据集和框架的定义不一定一样,画出来发现角度翻转就调整一下符号。源码包里那张vis2.jpg就是用类似逻辑生成的,你可以拿自己的输出和它对拍一下。

4.2 用test_field.ipynb和data_analysis.ipynb做可视化和分析

源码包里带了test_field.ipynb和data_analysis.ipynb,这两个notebook是项目作者留下的复现入口。test_field.ipynb一般做的事情就是上面那段代码的交互版:加载配置、加载权重、对若干张测试图推理,并把检测框画在图上输出成vis2.jpg之类的可视化文件。data_analysis.ipynb则偏向统计分析,比如统计各类别检测框的数量、面积分布,把merged_result.csv读进来,按类别做PR分析。如果你交课程作业,这个notebook可以直接作为实验分析部分,不需要自己重新写脚本。

用notebook的好处是分步调试方便,坏处是容易丢状态。我建议你在notebook里加了代码后,顺手把关键的推理部分单独存成infer.py,后面要给整个验证集跑结果时,只要在命令行里python infer.py --cfg config.py --ckpt epoch_12.pth就够了。遇到过几次notebook里变量被覆盖导致结果对不上的情况后,我就养成了「notebook做探索,py脚本做复现」的习惯。另外,notebook里如果直接读超大遥感图,很容易把内存撑爆,建议先跑通单张,再改成批量循环。

4.3 merged_result.csv和idx.csv怎么读:mAP评估逻辑

推理结束后,项目里一般会把检测结果汇总到merged_result.csv,idx.csv里放的是图片和目标的索引映射。打开后大概长这样:

idximage_idclsxywhthetascore
010airplane128.5256.364.025.51.210.87
110ship512.0100.1120.440.80.080.71

x, y, w, h, theta对应一个旋转框,theta是角度参数。score是这个框的置信度。如果你用idx.csv回查,会发现它记录的是类似image_id和当前检测框在原始大图中的坐标偏移关系,用于把patch级别的结果还原到整张大图坐标。也就是说,如果你的推理是分patch做的,最终合并结果一定要写回原图坐标,否则merged_result.csv里的框和原始影像对不上。

评估时最常用的是mAP。计算流程是:按score从高到低排序,对每个类别计算precision和recall,然后画PR曲线求平均精度。手动算会很麻烦,项目里data_analysis.ipynb通常会引用一个eval函数去算。你要注意的是:评估必须关闭数据增强,并且推理的图片尺寸要和训练时输入尺寸保持一致,否则precision和recall会双双下降。只看某一张图的检测结果不能说服老师,把mAP表和PR曲线拿出来才有说服力。如果发现某一类AP特别低,先数一下这个类在训练集里的样本量,样本过少的类别AP低是正常的,不一定是模型问题。

5. 避坑指南:遥感目标检测复现中的五个常见坑

5.1 现象:pip install之后import jdet还是报ModuleNotFoundError

你按说明装了requirements.txt,也执行了setup.py develop,但一到import jdet还是提示找不到模块。这个报错在第一次配环境时几乎必现。

原因多数不是包没装上,而是执行路径不对。JDet的包是相对项目根目录注册的,你在别的目录下启动Python,解释器自然搜不到jdet。如果你是在Jupyter notebook里运行的,还得注意notebook的kernel工作目录往往不是你启动notebook的那个目录,这时候同样会报错。其次才轮得到编译失败,比如缺少某些系统依赖库,导致develop过程根本没有真正生成可导入模块。

解决方式:先cd到RS_detect根目录,再执行python -c "import jdet"。如果还不行,检查setup.py develop有没有输出Successfully installed jdet之类的成功标识,没有就重新跑一遍,并把报错贴全看是缺编译器还是缺依赖。在notebook里使用时,可以在第一格加import sys; sys.path.insert(0, "/你的项目绝对路径"),让kernel也能找到jdet。我后来习惯在项目根目录写一个run.py统一入口,所有训练推理都从这个脚本调用,彻底告别路径问题。如果你换了conda环境,记得jdet是要重新develop的,不同环境下软链接不通用。

5.2 现象:数据集解压后目录结构和说明对不上,训练直接报错

FAIR1M官网下载下来的压缩包,里面目录名可能和项目说明不一样,可能是train、labels这种,而不是train_color/train/images和train_color/train/labelXml。直接把根目录路径填进config,程序会因为找不到images子目录而报错。项目说明里已经提醒过“直接解压数据集得到的文件树可能与说明不同”,但很多人会忽略这句话。

解决方式:严格按照项目说明的树形结构重新组织,目录名不要做任何改动。可以用一个简短的shell脚本批量重命名,比如:

cp -r data/train data/train_color/train mkdir -p data/train_color/train/labelXml find data/train -name "*.xml" -exec mv {} data/train_color/train/labelXml/ \;

改完以后用ls images | wc -l和ls labelXml | wc -l核对数量,一致再进训练。另外,FAIR1M里还有test集,但它没有公开标签,项目里一般只用来做最终推理,不参与训练。如果你把test也放进train_color,就会因为缺少XML而报数据读取错误。FAIR1M中不同子集的图片编号可能有重复,合并train和validation前先给文件加前缀,这个我在2.3里已经强调过,这里再重复一遍:不加前缀,后期读图一定会覆盖。

5.3 现象:训练时batch_size=4直接OOM,显存爆炸

训练刚开始没几个iter,就报CUDA out of memory。这是遥感目标检测最常见的翻车现场。原因不是代码写得差,而是输入分辨率太高。原始遥感图几千像素见方,即使缩放到接近2500,单张图占的显存也远超COCO里的普通自然图。

解决方式有几个层次。第一,先用灰度分支train_gray训练,灰度图数值上减少了对显存的压力,配合复制成三通道,计算量不变但加载和预处理更快。第二,把batch_size从4降到2,如果还OOM就降到1。第三,在config里把长边max_size限制在2500以内,短边min_size控制在800到1000之间。如果还是不行,把backbone从ResNet50换成ResNet34,精度的损失远小于训不出来的损失。还可以用梯度累积,比如accumulate_steps=4,等效batch_size不变但每次只反传一小批,显存压力会小很多。最后提醒一点,OOM也有可能是之前残留的进程占着显存,训练前用nvidia-smi看一下,把僵尸进程清掉,有时候比改参数更有效。

5.4 现象:推理结果图上一堆重复框,一个目标被框了三四遍

检测器输出的是TopK候选框,如果后处理没做好,会出现大量重复框。原因多半是score_threshold设得过低,或者NMS阈值设置得不合适。旋转框的NMS和水平框不一样,不能直接复用水平框的boxes接口,因为旋转框的IoU计算要处理角度,直接调用水平框函数会算错,导致本应被抑制的重复框全被保留下来。

解决方式:先调score_threshold到0.4或0.5,看重复框有没有减少;然后用JDet里的旋转NMS接口做后处理,nms_threshold从0.3开始往上试,看哪一档既能去重又不吞掉密集排列的目标。如果你的代码里用的是torchvision.ops.nms,那就要改成支持五元组框的旋转NMS实现。另外还有一类重复框来自滑动窗口切图,那种是在patch拼接时产生的,需要在全局NMS合并时处理,这个我在下一章演示。排查这类问题时不要只改一个阈值,先把score_threshold和nms_threshold组合起来测几组,用一张密集小目标的图做对照,哪个组合视觉上干净就用哪个。

5.5 现象:用灰度分支训练的模型放到彩色图上精度骤降

项目里特意把大图做成灰度副本,是为了压低训练开销。但如果你推理时不走灰度分支,直接把三通道彩色原图喂给模型,模型看到的颜色分布和训练时不一样,特征分布偏移会直接拉低检测精度。这种问题表现得很隐蔽,因为loss曲线是正常的,训练也没有报错,只是推理结果莫名其妙变差。

解决方式:推理时对输入图先做灰度化,再复制成三通道(1, 1, 1)拼接,保持和训练时一模一样的数据分布。如果你的测试图和训练集中的中小尺寸图一样是彩色,而训练时有彩色和灰度两种分支,那推理时也最好分成两条路径:小于2500的用彩色,大于2500的用灰度,和训练配置保持一致。另外要注意,有些项目在训练前还会对图像做归一化,比如除以255或者用ImageNet的mean/std,推理时必须用同一套归一化参数。这就是典型的「训练和推理必须同一个pipeline」,不要图省事只处理一边。我见过不止一个项目在灰度/彩色问题上翻车,最后都是靠统一预处理才救回来。

6. 进阶用法:用滑动窗口把大遥感图切成patch再推理

真正的遥感产品里,很少直接把整张超大图送进模型推理。就算显存能塞下,目标在原图上的尺度也早就超出了模型见过的尺度范围,检测效果会很差。正确的做法还是滑动窗口切图:把大图切成若干重叠patch,每个patch独立送进模型,再把所有patch的结果映射回原图坐标,最后做一次全局NMS合并。重叠窗口的存在是为了避免目标恰好被切在patch边缘。

import math import cv2 def slide_infer(model, img, patch_size=1024, overlap=128, score_thr=0.3): h, w = img.shape[:2] stride = patch_size - overlap results = [] for y0 in range(0, h, stride): for x0 in range(0, w, stride): x1 = min(x0 + patch_size, w) y1 = min(y0 + patch_size, h) patch = img[y0:y1, x0:x1] outputs = model([patch]) for box in outputs[0]: x, y, bw, bh, theta, score, cls = box if score < score_thr: continue # 加回偏移量,换算回原图坐标 results.append((x + x0, y + y0, bw, bh, theta, score, cls)) # 合并重叠patch产生的重复框 final = rotate_nms(results, iou_threshold=0.4) return final

这段代码有几个小坑:overlap至少要大于目标尺寸的一半,否则目标横跨两个patch时,两边可能各框一半,合并时要么丢失要么重复;patch_size要和训练时缩放的输入尺寸接近,别训练用的是600,推理却用1024;最后的rotate_nms不能省,重叠patch必然导致大量重复框,必须合并后再画图。如果你没有现成的rotate_nms函数,可以按类别加置信度排序,手动实现一个基于旋转IoU的抑制循环。

从那以后我每次跑遥感大图推理,都强制自己把推理函数和NMS合并写成一个工具脚本,而不是在notebook里临时拼,这样可视化、评估、部署用的是同一套逻辑。这份源码包适合从零走一遍,但下一步真正换成自己的数据时,建议把patch流程提前到数据预处理阶段,而不是放在推理时临时切,训练时也用同样的切图策略,效果会稳定很多。希望帮到你。

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

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

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

立即咨询