简介这是一份基于机器学习的遥感图像分类模型源码包面向计算机、人工智能、大数据等相关专业正在做课程设计、期末大作业或毕业设计的学生也可供遥感图像与机器学习方向的学习者参考。包内共十三个文件以六个Python源码脚本为主覆盖模型训练、结果绘图与数据处理等核心模块同时附带了已经训练好的SVM模型文件、结果记录文件、工作区配置文件以及说明文档整个压缩包仅十四KB体量轻巧便于快速部署启动。目前已有三百零五人学习浏览。源码经过严格调试下载后可直接运行能够帮助读者理解遥感图像分类从特征提取、模型训练到结果可视化的完整流程项目中保留的缓存文件与工作区配置也方便在代码编辑器中直接打开复现实验配套的说明文档则梳理了各模块用途与运行方式。适合具备一定编程与机器学习基础、希望高效搭建分类实验或借鉴项目模块设计思路的读者。1. 基于机器学习的遥感图像分类模型源码先看清楚它解决什么问题收到一份「基于机器学习的遥感图像分类模型源码.zip」时多数人的第一反应是解压、找 README、直接跑训练。真正做过这类项目的人知道包里最值钱的往往不是模型定义文件而是从原始 GeoTIFF 到分类图之间的一整条数据管线波段怎么读、影像怎么切块、标签怎么对齐、推理怎么拼图。这个方向回答的业务问题很直接——「这块地是什么」水体、植被、建筑、裸地每个像素一个类别。适合算法工程师做地物分类验证也适合需要拿一景影像快速出分类专题图的行业团队。先给一个反直觉结论遥感图像分类的坑大多不在模型结构而在数据口径和推理策略后面逐条拆。2. 遥感分类不能直接套用通用分类网络选型先想清三件事2.1 像素级与场景级任务决定模型结构遥感图像分类在源码包里通常有两种实现形式很多人拿到包后分不清直接跑通就以为是完成任务了。场景级分类把一景影像切成若干块每块打一个整体标签这跟普通图像分类差不多模型输出的是一个类别编号。像素级分类则要求输入一块影像、输出同尺寸的标签图每个像素一个类别本质上是语义分割。绝大多数「基于机器学习」的源码包做的是像素级因为地物分类的业务交付物是一张分类专题图而不是一段文字描述。判断一个包属于哪类最快的办法是看标签数据的形态标签是单波段掩码或 shapefile就是像素级标签是每张图对应一个 txt 里的类别名就是场景级。两种任务的评估方式完全不同像素级看逐类 IoU 和混淆矩阵场景级看 Top-1 准确率。如果你的需求只是判断某块区域整体是不是农田场景级就够不必强行上像素级反过来业务要一张完整的地物分类图那就必须走像素级管线场景级的精度再高也交付不了。2.2 随机森林还是卷积网络按样本量选基线选基线的标准不是哪个精度高而是你的标注量撑得起哪种模型。我的判断依据比较朴素每类标注像素少于 5000 时优先随机森林每类超过 2 万像素且有 GPU 可用再考虑卷积网络。随机森林的输入是每个像素的多光谱值加派生指数比如 NDVI归一化植被指数、NDWI归一化水体指数这类手工特征只用像素自身的光谱不依赖邻域训练几分钟就出结果缺点是完全没有空间上下文地物边缘会碎成锯齿。卷积网络直接吃影像块空间纹理和邻域关系都被自动建模精度上限明显更高但代价是标注需求、显存和训练时间一起涨。下表是我在做选型时常用的对照维度随机森林/SVM 基线卷积网络分割输入形式逐像素光谱值 派生指数影像块如 256×256×B最少样本量每类数千像素即可每类 2 万像素以上训练硬件CPU 可跑分钟级GPU 必须小时级起空间上下文无边缘易碎有边缘更干净跨影像泛化较差需要重新标定特征较好依赖训练分布可解释性特征重要性直接可查黑匣子只能看特征图我一般会先跑随机森林出一版整景分类图拿这张图去和业务方对齐类别定义。这一步的价值在于如果类别的语义本身定错了比如把「湿地」和「水体」混成一类你在卷积网络上调几天也救不回来而随机森林半小时内就能暴露这个问题。等类别口径确认了再用完整标注集上卷积网络。2.3 源码包的典型目录结构与数据流这类源码包的内部结构通常逃不出下面这个骨架先花五分钟对一遍能省掉大量找入口的时间root/ ├── data/ # 原始影像与标签按 train/val/test 分目录 ├── configs/ # 模型与训练参数通常是 yaml 或 json ├── src/ │ ├── data_prep.py # 切块与样本生成第一个该看的文件 │ ├── train.py # 训练入口 │ ├── inference.py # 推理入口 │ └── utils.py # 评估、可视化辅助函数 ├── models/ # 模型结构定义 └── outputs/ # 权重、日志、分类结果图整个数据流是data_prep 生成训练样本 → train 产出权重 → inference 对整景影像做滑窗推理 → utils 统计 OA 与混淆矩阵偶尔还有一张可视化对比图。你拿到包后别急着跑 train.py先把 data_prep.py 读一遍因为样本质量决定了后面所有结果的上限模型再强也补不回来。另一个值得留意的是 configs 里有没有写死影像路径很多包用的是绝对路径换机器必须改不然一运行就报文件找不到。还要看配置文件里的类别数是否和你手上的标签类别数一致不一致时训练不会报错但推理结果的类别编号和业务口径会对不上。3. 把源码跑起来从切块到第一次推理的最小流程3.1 环境准备依赖安装与连通性检查假设机器上已有 Python 3.10 和可用的 NVIDIA 驱动。常见做法是先建一个独立环境避免和已有项目互相污染依赖版本尤其是 rasterio 这类带二进制扩展的库。conda create -n rs_cls python3.10 -y conda activate rs_cls pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 pip install rasterio numpy pillow scikit-learn tqdm matplotlibrasterio 负责读写 GeoTIFF是这条管线里最核心的依赖scikit-learn 在跑随机森林基线时用得上。如果机器没有 GPU把 torch 那行的--index-url去掉直接装 CPU 版推理小影像也够用训练速度会慢几十倍但能跑通流程。装完后建议执行一次python -c import rasterio; print(rasterio.__version__)确认导入成功遥感库的二进制依赖在 Windows 上特别容易出问题这一步能提前暴露而不是等到跑训练才报错。提示CUDA 版本和 PyTorch 轮子的 cu 后缀要对上否则装完 torch 后torch.cuda.is_available()是 False训练照样能跑但慢得离谱。3.2 数据准备把一幅 GeoTIFF 切成训练样本从原始影像生成训练样本是整个项目里最不该跳过的一步。一景遥感影像可能有上万像素宽不可能整幅喂给模型必须按固定大小切成小块。最小可用的切块脚本如下import numpy as np import rasterio from pathlib import Path def make_patches(image_path, mask_path, out_dir, patch_size256, stride128, ignore_value0): out_dir Path(out_dir) (out_dir / img).mkdir(parentsTrue, exist_okTrue) (out_dir / mask).mkdir(parentsTrue, exist_okTrue) with rasterio.open(image_path) as src_img, \ rasterio.open(mask_path) as src_mask: img src_img.read() # (C, H, W) mask src_mask.read(1) # 单波段标签 height, width img.shape[1], img.shape[2] idx 0 for y in range(0, height - patch_size 1, stride): for x in range(0, width - patch_size 1, stride): p_img img[:, y:ypatch_size, x:xpatch_size] p_mask mask[y:ypatch_size, x:xpatch_size] if (p_mask ignore_value).mean() 0.5: continue # 跳过无标签区域占多数的块 np.save(out_dir / img / fp_{idx:06d}.npy, p_img) np.save(out_dir / mask / fp_{idx:06d}.npy, p_mask) idx 1 print(f生成 {idx} 个样本请检查各类别像素占比)patch_size 和 stride 是最影响训练效果的两个参数。patch_size 决定模型看到的空间范围太小看不清地物纹理太大则一块里混着多种地物、标签含义变模糊常用值是 128 到 256。stride 小于 patch_size 时相邻切块有重叠间接做了数据增强样本量成倍增加代价是训练时间变长。(p_mask ignore_value).mean() 0.5这行把无标签区域占多数的块直接丢掉避免模型在这些样本上学到无意义映射。这里有个最常见的翻车点必须确认 ignore_value 在你的标签里真的是「无标签」而不是类别编号 0。如果某类地物恰好编码为 0这个过滤条件会把该类别的有效样本全丢光训练出来的模型对该类永远预测失败。我在第一次做类似项目时就踩过症状是混淆矩阵里某一类的行和列全是零查了半天才发现是过滤条件写错了。3.3 训练模型最小可跑的训练循环与权重保存数据就绪后训练脚本的核心是数据集类、损失函数和训练循环三件事。以一个小型分割网络为例import torch import torch.nn as nn from torch.utils.data import DataLoader, Dataset from pathlib import Path import numpy as np class PatchDataset(Dataset): def __init__(self, img_dir, mask_dir): self.files sorted(Path(img_dir).glob(*.npy)) self.mask_dir mask_dir def __len__(self): return len(self.files) def __getitem__(self, i): img np.load(self.files[i]).astype(np.float32) / 4096.0 mask np.load(Path(self.mask_dir) / self.files[i].name) return torch.from_numpy(img), torch.from_numpy(mask).long() model SimpleSegNet(num_classes4) # 换成你的类别数 optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) criterion nn.CrossEntropyLoss(ignore_index0) loader DataLoader( PatchDataset(data/train/img, data/train/mask), batch_size16, shuffleTrue, num_workers4) for epoch in range(30): model.train() total_loss 0.0 for imgs, masks in loader: optimizer.zero_grad() logits model(imgs) # (B, C, H, W) loss criterion(logits, masks) loss.backward() optimizer.step() total_loss loss.item() torch.save(model.state_dict(), fcheckpoints/model_e{epoch}.pth) print(fepoch {epoch:02d} loss {total_loss / len(loader):.4f})img / 4096.0是把 12 位遥感影像的原始 DN 值归一化到 01。4096 是常见多光谱数据的最大 DN 值如果数据源位深不同这个系数必须对应调整否则模型输入分布整体偏移训练很可能不收敛、loss 卡在某个值上震荡。ignore_index0对应切块时的无标签区域让损失函数跳过这些像素避免它们干扰梯度。权重保存用state_dict()而不是整个模型对象文件小、跨机器加载方便后续做多尺度推理时也灵活。3.4 推理对整幅影像滑窗输出分类图推理阶段的任务是对一整幅大影像做滑窗预测再把小块结果拼回一张与原始影像完全对齐的分类图import numpy as np import torch import rasterio def predict_full_image(image_path, model, patch_size256, stride128, out_pathpred.tif): model.eval() with rasterio.open(image_path) as src: img src.read() profile src.profile height, width img.shape[1], img.shape[2] pred np.zeros((height, width), dtypenp.uint8) for y in range(0, height - patch_size 1, stride): for x in range(0, width - patch_size 1, stride): block torch.from_numpy( img[:, y:ypatch_size, x:xpatch_size] ).unsqueeze(0).float() / 4096.0 with torch.no_grad(): logits model(block) cls logits.argmax(dim1).squeeze(0).numpy() pred[y:ypatch_size, x:xpatch_size] cls profile.update(dtyperasterio.uint8, count1, compresslzw) with rasterio.open(out_path, w, **profile) as dst: dst.write(pred, 1)滑窗推理时重叠区域的像素会被后一次预测直接覆盖这在实践中问题不大因为模型对同一区域在不同窗口下的预测差异通常很小。真正关键的是profile.update保留原始坐标系和分辨率输出的 pred.tif 能直接在 GIS 软件里叠加原图检查空间错位问题一眼就能看出来。如果分类图出现规律的条带错位优先检查这里有没有擅自修改 profile 的 transform。4. 换自己的数据训练三处必改参数与精度验证4.1 波段顺序与标签类别对齐训练前必须核对的两个映射用自己的数据前先做两件事核对波段顺序核对标签类别编号。源码包默认的波段顺序可能是 BGR也可能是某个固定多光谱组合而你的数据可能是 RGBNIR 或只有 3 个波段直接喂进去会得到完全离谱的结果。常见做法是先写一张波段映射表在数据准备阶段就完成重排数据源原始波段顺序模型期望顺序需要做的处理4 波段多光谱B,G,R,NIRR,G,B重排为 R,G,B可去 NIR假彩色 3 波段NIR,R,GR,G,BNIR 与 R 对调全色单波段单波段R,G,B复制三份拼成 3 波段从 4 波段里要不要保留 NIR取决于模型是否支持任意通道数。如果你用卷积网络可以在第一层 conv 里把输入通道改成 4多一个红外波段对植被和水体的区分通常有明显帮助如果用随机森林NIR 直接作为一维特征加入即可不需要纠结。标签对齐比波段更隐蔽。源码包的类别 id 可能是 1水体 2植被你的掩码可能是 10、20、30 的行政区编码或者方向完全相反。不做 id 映射就训练模型学到的是编号之间的虚假关系。我一般会在训练前打印一次训练集每类的像素占比和业务口径逐类核对一遍再动手这一步花十分钟能省掉后面三天排查。顺带检查影像和掩码的坐标系是否一致如果投影不同切块时掩码和影像会错位训练出来的模型在推理时也会有系统性偏移。4.2 训练超参数一套稳妥的起点配置超参数不需要一开始就精细调先跑通再优化。下面是我常用的起点配置直接照用不会出大问题参数随机森林基线卷积网络学习率不适用1e-4batch_size不适用816epochs不适用3050patch_size不适用256滑窗 stride不适用128类别权重class_weightbalanced损失函数传入按类占比的 weight训练时最该盯的不是每轮打印的准确率而是 loss 曲线是否平滑下降。如果 loss 前几轮跳来跳去不降优先检查归一化系数和 ignore 区域占比如果 train loss 降但验证精度不动基本是过拟合需要加数据增强或早停。学习率 1e-4 是遥感分割里比较稳的起点低于 1e-5 收敛太慢高于 5e-4 容易训崩表现为 loss 突然跳高后回不来。数据集规模超过 10 万块时可以加一步 cosine 衰减效果会更平滑。4.3 精度验证OA、Kappa、IoU 怎么读才不骗自己训练和推理跑完后源码包一般会在 outputs 下生成混淆矩阵、逐类精度报告和分类 GeoTIFF。指标先看三个OA总体精度、Kappa、逐类 IoU。OA 很容易被占比高的类别主导如果一类占 80%模型全预测它也能拿到 80% 的 OA所以必须配合混淆矩阵看少数类。import numpy as np from sklearn.metrics import confusion_matrix, cohen_kappa_score y_true np.load(outputs/val_true.npy) y_pred np.load(outputs/val_pred.npy) cm confusion_matrix(y_true, y_pred) oa np.trace(cm) / cm.sum() kappa cohen_kappa_score(y_true, y_pred) iou cm.diagonal() / (cm.sum(0) cm.sum(1) - cm.diagonal()) print(fOA{oa:.3f} Kappa{kappa:.3f}) print(各类 IoU:, np.round(iou, 3))IoU 比 F1 更能暴露位置偏差因为它在分母上同时惩罚漏检和误检。如果某个类别的 IoU 是 0 或接近 0先别急着调模型回去看训练集里这类像素占比——低于 2% 属于样本不足任何模型结构都救不回来正确做法是补标注或做类别加权。可视化输出也值得做把预测图和原图半透明叠加逐类目测一遍很多指标看不出的系统性错分在图上会非常明显。5. 遥感图像分类的 5 个常见问题与排查现象、原因、解决5.1 验证集精度高但预测图全是椒盐噪声现象验证集 OA 有 90% 以上但推理输出的分类图在地物边界处布满细碎的孤立像素像撒了一层盐。原因模型本身没问题问题出在推理时每个像素都是独立预测没有利用邻域一致性。如果基线模型是随机森林这类无空间上下文的算法这种现象会更严重边缘区域尤为明显。解决在推理脚本末尾加一次多数投票滤波对每个像素取周围 3×3 或 5×5 窗口内的众数作为最终类别。更优的做法是用带重叠的滑窗推理同一像素在不同窗口下的多次预测做投票融合效果更好代价是推理时间成倍增加。先做重叠投票再看效果决定要不要加窗口滤波两件事不要反过来。5.2 验证集指标好看换一景影像就崩盘现象在训练用的那景影像上精度不错换到相邻区域或不同时相的影像分类图大面积出错水体被识别成建筑农田变成裸地。原因训练样本和验证样本来自同一景影像空间自相关性让模型学会了记忆局部光谱特征而不是真正的地物规律。相邻影像的大气条件、太阳角度、物候状态都不同光谱分布整体偏移模型没见过这种偏移自然翻车。解决样本划分从随机划分改成按影像划分同一景影像的所有样本只能进 train 或 val 之一绝不能两边都出现。想让模型具备跨影像能力数据准备阶段就要混入多个时间、多个区域的影像并在波段上做整体归一化。这一步没有后悔药必须在新数据采集阶段就规划好独立验证影像。5.3 类别不平衡少数类永远分不出来现象混淆矩阵里某一类的召回率极低大量该类的像素被预测成占比最高的类别训练日志里 loss 在降但该类的精度始终起不来。原因如果水体在场景里的占比不到 3%交叉熵损失被多数类主导模型学到的最优策略就是全部预测为多数类个别少数类样本即使预测错对 loss 的影响也微乎其微。解决两个手段叠加。损失函数侧给少数类更大的类别权重权重向量按各类像素占比的倒数设置数据侧对少数类做重复采样或对多数类做随机欠采样。处理完一定要再看逐类 IoU只看 OA 是看不出来的因为多数类的 OA 照样很漂亮。权重设得太极端会导致少数类过拟合建议用占比倒数的平方根做一个平滑。5.4 GPU 显存被大 patch 和大批次吃光现象训练刚开始就报 CUDA out of memory或者 epoch 跑到一半崩掉手动调小 batch_size 后 loss 曲线又开始震荡。原因显存需求约等于 batch_size 与 patch_size 面积的乘积两者同时拉高必然爆显存。遥感影像普遍是 16 位存储读进来转 float32 后每像素占 4 字节比普通 8 位图更吃显存。解决先固定 batch_size8patch 从 512 降到 256跑通一个 epoch 再逐项加大。也可以开混合精度训练显存大约省一半对分类任务来说精度损失通常可接受。如果显存还是不够就缩小 patch 并用重叠滑窗来补偿上下文损失这是显存紧张时比换机器更实际的路径。5.5 跨季节数据失效春季模型秋季翻车现象用春季影像训练的分类模型应用到秋季影像时落叶林大面积错分为裸地或草地模型每个月都要重新训练。原因遥感光谱的季节性很强物候变化让同类地物的光谱响应差异巨大。模型学到的是「春季的森林长什么样」换到秋季就失灵这不是模型 bug而是训练数据的时间覆盖度不够。解决训练集按时间维度混合多个季节的样本如果业务只关心单一季节干脆只在该季节采样并明确模型的适用时间范围。另一种维护成本较低的方案是应用前采少量新影像像素做主动学习校准用几十个标注点对模型输出做一次轻量纠偏把季节漂移拉回来。这种做法在业务系统里比频繁重训更实用缺点是每次应用都要人工确认采样点。6. 进阶把滑窗推理升级为多尺度投票融合6.1 多尺度推理的代码骨架单尺度滑窗遇到大尺度地物时容易碎——一栋大楼被切成四块模型在每块里看到的都是墙面和阴影分类自然出错。多尺度推理的思路是同一位置分别用 128、256、512 三种 patch 大小预测再把结果融合。大 patch 提供全局上下文小 patch 保留边缘细节。def multi_scale_predict(image, model, scales(128, 256, 512), stride_ratio0.5, num_classes4): # image 形状为 (C, H, W)已做过与训练一致的归一化 votes np.zeros((len(scales), image.shape[1], image.shape[2], num_classes)) for s_idx, scale in enumerate(scales): stride int(scale * stride_ratio) for y in range(0, image.shape[1] - scale 1, stride): for x in range(0, image.shape[2] - scale 1, stride): block torch.from_numpy( image[:, y:yscale, x:xscale] ).unsqueeze(0).float() with torch.no_grad(): prob torch.softmax(model(block), dim1) votes[s_idx, y:yscale, x:xscale] \ prob.squeeze(0).permute(1, 2, 0).numpy() final votes.sum(axis0).argmax(axis2) return final.astype(np.uint8)注意这里累加的是 softmax 概率而不是 argmax 类别概率加权的抗噪性明显更好某个尺度给出的不确定预测不会一票否决其他尺度。运行时间接近单尺度的 3 倍所以建议只在最终出图或重点区域验证时用日常调参阶段保持单尺度。6.2 概率投票融合与面积阈值去孤岛出图前的最后一道工序多尺度融合后我还会接一道后处理按连通域对分类图做面积阈值过滤小于阈值的孤立块并入邻域多数类别。这一步产出的才是业务上能直接看的专题图而不是像素级噪声点。整条收尾链是多尺度概率投票 → 面积阈值去孤岛 → 输出矢量化面状结果。回看这类源码包真正拉开效果差距的不是模型结构选得多新而是数据口径对不对、推理策略细不细。我踩过一轮后养成的习惯是拿到任何新的遥感分类代码先跑通数据准备再盯推理输出最后才调整模型结构。这个顺序几乎能避开全部低级错误。希望帮到你。本文还有配套的精品资源点击获取