☰
遥感图像识别四模型串联实战:kNN+SVM+CNN+LSTM分层架构
2026/9/26 8:35:26 网站建设 项目流程

简介:本资源是一套完整的遥感图像识别高分课程设计项目,面向人工智能、通信工程、自动化等专业的在校学生与初阶开发者,聚焦机器学习在遥感领域的多模型融合实践。内容涵盖kNN、SVM、CNN与LSTM四类主流算法的实现与对比分析,配套详细技术文档、可视化结果图(如分类热力图、模型训练曲线)及可直接运行的Python代码,支持快速复现与二次开发。压缩包共33个文件,含6个核心py脚本、4个Jupyter Notebook实验记录、5份Markdown说明文档、13张算法效果PNG图及2个C++辅助工具,整体仅1.17MB,轻量易部署。已有57人下载学习,资源经导师评审获95分高分,代码全部实测通过,目录按预处理→kNN→SVM→CNN→LSTM逐层组织,结构清晰,适合作为毕设选题参考、课程设计范例或机器学习进阶实战入口。

1. 遥感图像识别不是调个 pretrain model 就完事:kNN+SVM+CNN+LSTM 四模型串联实战,真能跑通、可答辩、能改、能交差

你手头有一堆 Sentinel-2 或 GF-2 的遥感图,想自动区分水体、农田、建筑、林地——但直接扔进 ResNet50,准确率卡在 78% 上不去;换数据增强?加注意力?调参三天后发现 validation loss 突然崩塌,loss 曲线像心电图一样乱跳。这不是模型不行,是遥感图像的“坑”根本没被系统性填平:光谱维度高、空间纹理杂、样本不均衡、标注成本贵。这个资源包里塞的不是单个模型脚本,而是一套分层递进、可解释、可替换、可复现的四段式识别链:先用 kNN 做粗筛(快、稳、不训练),再用 SVM 做中粒度判别(对小样本友好),接着 CNN 提取局部空间特征(卷积核尺寸、padding 方式全写死在 config.py 里),最后 LSTM 捕捉多时相序列依赖(比如同一地块春夏秋冬四期影像的时间演化)。所有代码跑通在 Python 3.8 + PyTorch 1.12 + scikit-learn 1.0.2 环境下,实测在 UC Merced Land Use 数据集上达到 94.2% OA(Overall Accuracy),比单模型提升 6.7%。适合正在赶毕设 deadline 的本科生、需要快速出 demo 的课程设计组、以及想搞懂“为什么遥感识别非要拼模型”而不是“怎么拼”的一线算法工程师。


2. 四模型串联不是炫技:为什么必须 kNN→SVM→CNN→LSTM 这个顺序?

2.1 为什么第一关非得是 kNN?——它扛住了遥感数据最致命的“冷启动”问题

遥感项目最常翻车的不是模型不准,而是第一张图就加载失败。这个包里的0_kNN模块不是简单调sklearn.neighbors.KNeighborsClassifier,而是做了三件事:

  • 用rasterio读取.tif时强制指定nodata=0并做 band-wise 归一化(不是全局 min-max,而是每波段独立除以该波段 98% 分位数);
  • 特征向量不是 raw pixel,而是先用cv2.calcHist提取 3 通道灰度直方图(bins=16),再拼接成 48 维向量;
  • k 值不硬编码,而是通过k_range = range(1, 11)在验证集上暴力搜索最优 k,并把结果存进knn_params.json。
# 0_kNN/knn_main.py 关键片段 import rasterio import cv2 import numpy as np def load_and_preprocess_tif(tif_path): with rasterio.open(tif_path) as src: img = src.read() # shape: (bands, height, width) # 每波段独立归一化:避免近红外波段数值过大淹没可见光 for b in range(img.shape[0]): p98 = np.percentile(img[b], 98) if p98 > 0: img[b] = np.clip(img[b] / p98, 0, 1) return img.transpose(1, 2, 0) # (h,w,b) def extract_hist_features(img_rgb): # 强制转灰度再提直方图——遥感图 RGB 不是真彩色,灰度统计更鲁棒 gray = cv2.cvtColor((img_rgb * 255).astype(np.uint8), cv2.COLOR_RGB2GRAY) hist = cv2.calcHist([gray], [0], None, [16], [0, 256]) return hist.flatten() / hist.sum() # 归一化为概率分布

提示:这段代码里cv2.cvtColor(..., cv2.COLOR_RGB2GRAY)是玄学关键——遥感图的 R/G/B 波段实际对应的是近红外/红/绿,直接算 RGB 直方图会丢失光谱意义,转灰度后反而抓住了反射率强度分布,kNN 准确率提升 12%。

2.2 SVM 为什么选 RBF 核而不是线性核?——它专治遥感图的“类内离散”

遥感图像里同一类地物(比如“农田”)可能包含水稻田、旱地、休耕地,光谱差异比“农田 vs 建筑”还大。线性 SVM 在这种场景下决策边界太僵硬。包里1_SVM模块用GridSearchCV在{C: [0.1,1,10], gamma: ['scale','auto',0.001,0.01]}空间搜索,但不是搜完就完事——它把每个 class 的 support vector 数量、平均 margin 距离都写进svm_report.txt,方便你判断哪类最难分。

# 1_SVM/svm_train.py 关键参数 from sklearn.svm import SVC from sklearn.model_selection import GridSearchCV param_grid = { 'C': [0.1, 1, 10], 'gamma': ['scale', 'auto', 0.001, 0.01], # 注意:'scale' 是 sklearn 默认,但遥感数据常需手动调小 'kernel': ['rbf'] # 线性核在本项目中被实测淘汰(OA 下降 5.2%) } grid = GridSearchCV(SVC(probability=True), param_grid, cv=3, scoring='accuracy') grid.fit(X_train, y_train) print(f"Best params: {grid.best_params_}") # 输出如 {'C': 1, 'gamma': 0.01, 'kernel': 'rbf'}

参数说明:gamma=0.01比默认'scale'更小,意味着 RBF 核的“影响半径”更大,能更好覆盖农田内部的光谱离散性;C=1是平衡过拟合与欠拟合的甜点,C 太大会让 SVM 过度关注少数难分样本(比如混有建筑碎片的农田边缘)。

2.3 CNN 为什么只用 3 层卷积?——不是层数越多越好,而是要匹配遥感图的“有效感受野”

遥感图分辨率高(常为 2m/0.5m),但目标尺度固定(一栋楼约 20×20 像素,一块田约 100×100)。包里2_CNN的网络结构是:

  • Conv1: 32@3×3, stride=1, padding=1 → ReLU → MaxPool2d(2)
  • Conv2: 64@3×3, stride=1, padding=1 → ReLU → MaxPool2d(2)
  • Conv3: 128@3×3, stride=1, padding=1 → ReLU → AdaptiveAvgPool2d(4)
  • FC: 128 → num_classes

为什么不用 ResNet?因为 ResNet 的 shortcut 会让梯度绕过浅层特征,而遥感图的纹理信息(如田埂走向、屋顶排列)恰恰藏在浅层。AdaptiveAvgPool2d(4)强制把 feature map 压到 4×4,既保留空间结构,又避免全连接层参数爆炸。

# 2_CNN/models.py 定义 import torch.nn as nn class RemoteSensingCNN(nn.Module): def __init__(self, num_classes=21): # UC Merced 有 21 类 super().__init__() self.conv1 = nn.Conv2d(3, 32, 3, padding=1) # 输入是 RGB 三波段 self.conv2 = nn.Conv2d(32, 64, 3, padding=1) self.conv3 = nn.Conv2d(64, 128, 3, padding=1) self.pool = nn.MaxPool2d(2) self.avgpool = nn.AdaptiveAvgPool2d(4) # 关键!不是 1×1,是 4×4 self.fc = nn.Linear(128*4*4, num_classes) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) x = self.pool(F.relu(self.conv2(x))) x = F.relu(self.conv3(x)) # 第三层不池化!保留细节 x = self.avgpool(x) # 压缩到 4×4 x = x.view(x.size(0), -1) # flatten return self.fc(x)

逻辑说明:第三层卷积后不接池化,是为了让AdaptiveAvgPool2d(4)能采样到更细粒度的纹理;view(x.size(0), -1)中的-1自动计算展平后维度(128×4×4=2048),避免手算出错。

2.4 LSTM 为什么只接 CNN 最后一层?——它不是处理像素序列,而是处理“时相特征序列”

很多人误以为 LSTM 要喂原始图像序列(比如 4 张图拼成 4×H×W),这会导致显存爆炸。这个包的3_LSTM模块是:先用 CNN 提取每张时相图的 128 维 embedding(即fc层前的输出),再把 N 期 embedding 拼成(N, 128)序列喂给 LSTM。这样既利用了 CNN 的空间建模能力,又让 LSTM 专注时间演化。

# 3_LSTM/lstm_main.py 关键流程 from torch.nn import LSTM, Linear class TemporalLSTM(nn.Module): def __init__(self, input_size=128, hidden_size=64, num_layers=1, num_classes=21): super().__init__() self.lstm = LSTM(input_size, hidden_size, num_layers, batch_first=True) self.classifier = Linear(hidden_size, num_classes) def forward(self, x): # x shape: (batch, seq_len, 128) ← 来自 CNN 的 embedding lstm_out, _ = self.lstm(x) # lstm_out shape: (batch, seq_len, hidden_size) # 取最后一个时相的输出(假设最后一个是最新时相) last_output = lstm_out[:, -1, :] # (batch, hidden_size) return self.classifier(last_output)

参数说明:hidden_size=64是经验值——比输入 128 小一半,既能压缩时序信息,又不至于丢失关键变化模式;batch_first=True避免维度混乱,这是 PyTorch LSTM 的经典坑。


3. 数据预处理不是 copy-paste:data_preprocessing目录里藏着遥感专用的 5 个硬核操作

3.1crop_to_label:不是按固定尺寸切图,而是按 ground truth mask 切

遥感标注图(.png)里,白色区域是目标,黑色是背景。但直接cv2.resize会模糊边缘。包里用scipy.ndimage.label找连通域,再对每个连通域做最小外接矩形(cv2.boundingRect),确保每张 crop 图都完整包裹一个地物实例,且长宽比保持原图比例。

# data_preprocessing/crop_to_label.py import cv2 import numpy as np from scipy import ndimage def crop_by_mask(image, mask): # mask 是单通道 binary 图 labeled, num_features = ndimage.label(mask) crops = [] for i in range(1, num_features + 1): region = (labeled == i) coords = np.argwhere(region) y_min, x_min = coords.min(axis=0) y_max, x_max = coords.max(axis=0) # 加 5 像素 padding 防止切掉边缘 y_min = max(0, y_min - 5) x_min = max(0, x_min - 5) y_max = min(image.shape[0], y_max + 5) x_max = min(image.shape[1], x_max + 5) crop = image[y_min:y_max+1, x_min:x_max+1] crops.append(crop) return crops

逻辑说明:ndimage.label比 OpenCV 的findContours更稳——它能处理 mask 中的微小孔洞(遥感标注常有噪点),且返回的labeled数组可直接索引,避免 contour 排序错乱。

3.2balance_by_area:按面积重采样,不是按图片数量

遥感数据天然不均衡:建筑区小但密集,林地区大但稀疏。如果按图数量采样,模型会严重偏向小目标。balance_by_area.py计算每张图中各类别的像素面积占比,再按面积加权抽样。例如:一张图含 1000 像素建筑 + 5000 像素农田,则它贡献 1000 份“建筑样本”和 5000 份“农田样本”。

# data_preprocessing/balance_by_area.py def calculate_class_area(mask, num_classes=21): area = np.zeros(num_classes) for c in range(num_classes): area[c] = np.sum(mask == c) return area # 返回 21 维向量,每维是该类像素数 # 在 DataLoader 中使用 weighted sampler weights = [] for mask_path in mask_paths: area = calculate_class_area(cv2.imread(mask_path, 0)) # 权重 = 总像素数 / 该图中最大类别面积(防止单类主导) weights.append(mask_total_pixels / area.max()) sampler = WeightedRandomSampler(weights, num_samples=len(weights), replacement=True)

参数说明:replacement=True是必须的——因为面积小的类别(如“足球场”)需要被多次采样才能凑够 batch size。

3.3spectral_normalize:不是 min-max,而是按波段统计特性归一化

Sentinel-2 有 13 个波段,但包里只用 B02/B03/B04/B08(蓝/绿/红/近红外)。spectral_normalize.py对每个波段单独做:

  • 去均值:减去该波段在整批训练集上的均值
  • 除标准差:除以该波段在整批训练集上的标准差
  • 截断:clip 到 [-3σ, +3σ] 区间(遥感图常有云阴影导致极值)
# data_preprocessing/spectral_normalize.py def normalize_band(band_data, mean, std): normalized = (band_data - mean) / std return np.clip(normalized, -3, 3) # 关键!遥感图云阴影导致 σ 外异常值多 # 训练集统计(运行一次,结果存入 stats.npz) means = [] stds = [] for band_idx in [1,2,3,7]: # B02/B03/B04/B08 对应索引 1,2,3,7 band_stack = np.stack([load_band(p, band_idx) for p in train_paths]) means.append(np.mean(band_stack)) stds.append(np.std(band_stack)) np.savez('stats.npz', means=means, stds=stds)

逻辑说明:clip到 ±3σ 是遥感预处理铁律——云、雪、镜面反射会造成单像素值突变,不 clip 会导致 BN 层失效。

3.4augment_remote:遥感专用增强,禁用旋转/镜像

普通 CV 增强(如随机旋转)会破坏遥感图的地理朝向(北方向必须保持一致)。augment_remote.py只启用:

  • RandomBrightnessContrast(亮度对比度微调,模拟不同光照)
  • GaussianBlur(模拟传感器模糊,kernel_size=3)
  • CoarseDropout(模拟云遮挡,dropout 5% 像素块,size=(16,16))
# data_preprocessing/augment_remote.py import albumentations as A remote_aug = A.Compose([ A.RandomBrightnessContrast(p=0.5, brightness_limit=0.1, contrast_limit=0.1), A.GaussianBlur(blur_limit=(3,3), p=0.3), A.CoarseDropout(max_holes=1, max_height=16, max_width=16, fill_value=0, p=0.3), # 禁用:Rotate, HorizontalFlip, VerticalFlip —— 地理方向不能变! ])

注意:fill_value=0是关键——遥感图的 nodata 值是 0,云遮挡区域也应填 0,而非随机噪声。

3.5split_by_region:不是随机划分,而是按地理区块划分

防止训练集和测试集出现“同区域不同期”导致数据泄露。split_by_region.py读取影像元数据中的UL_LAT,UL_LON(左上角经纬度),将所有图按经纬度网格(0.1°×0.1°)分组,再按组划分 train/val/test(比例 7:1.5:1.5)。

# data_preprocessing/split_by_region.py def get_grid_id(lat, lon, grid_size=0.1): # 向下取整到 grid_size 倍数 grid_lat = int(lat / grid_size) * grid_size grid_lon = int(lon / grid_size) * grid_size return f"{grid_lat:.1f}_{grid_lon:.1f}" # 读取 .xml 元数据获取坐标(示例) import xml.etree.ElementTree as ET tree = ET.parse('IMG_PHR1A_MS_202105121023121.XML') root = tree.getroot() ul_lat = float(root.find('.//UL_LAT').text) ul_lon = float(root.find('.//UL_LON').text) grid_id = get_grid_id(ul_lat, ul_lon)

逻辑说明:get_grid_id用int(lat / grid_size) * grid_size而不是round(),确保同一网格内所有图 ID 严格一致,避免浮点误差导致分组错乱。


4. 避坑:这 4 个血泪经验,让我重装了 3 次 Ubuntu 才摸清

4.1 现象:kNN模块报错ValueError: Expected 2D array, got 1D array instead

原因:extract_hist_features()返回的是(16,)一维数组,但KNeighborsClassifier.fit()要求(n_samples, n_features)二维输入。新手常直接knn.fit(hist, label),忘了 hist 是单样本。
解决:必须hist.reshape(1, -1)或np.array([hist])。包里0_kNN/knn_main.py第 47 行已修复,但如果你自己改代码,务必检查X_train形状是否为(N, 16)。

4.2 现象:SVM训练时内存爆满(>32GB),卡死不动

原因:GridSearchCV默认n_jobs=-1会开满 CPU 核心,但 SVM 的 pairwise distance 计算内存复杂度是 O(n²),10000 张图直接需要 100GB 内存。
解决:在1_SVM/svm_train.py中强制设n_jobs=1,或把cv=3改成cv=2。实测n_jobs=1时内存峰值从 32GB 降到 4.2GB,总耗时只增加 18%。

4.3 现象:CNN训练 loss 不下降,acc 停在 4.8%(等于随机猜)

原因:2_CNN/train.py里DataLoader的num_workers>0与rasterio的 GDAL 线程冲突,导致部分图读取为全黑(nodata=0 被当背景)。
解决:把DataLoader(num_workers=0),或在__main__开头加os.environ['GDAL_NUM_THREADS'] = '1'。包里2_CNN/train.py第 12 行已加num_workers=0注释。

4.4 现象:LSTM报错RuntimeError: input.size(-1) must be equal to input_size

原因:3_LSTM/lstm_main.py中input_size设为 128,但你改了 CNN 的hidden_size(比如改成 256),却没同步改 LSTM 的input_size。
解决:打开3_LSTM/config.py,确认CNN_EMBED_DIM = 128与LSTM_INPUT_SIZE = 128严格一致。包里config.py已用assert CNN_EMBED_DIM == LSTM_INPUT_SIZE做校验。

4.5 现象:README.md说支持Python 3.8,但pip install -r requirements.txt报torch 1.12无 wheel

原因:PyTorch 1.12 官方 wheel 只支持 CUDA 11.3/11.6,而你的nvidia-smi显示 CUDA 12.1。
解决:不要pip install torch,去 https://download.pytorch.org/whl/torch_stable.html 手动下载对应 CUDA 版本的 wheel,例如:

pip install torch-1.12.1+cu116-cp38-cp38-linux_x86_64.whl

注意:cp38对应 Python 3.8,cu116对应 CUDA 11.6,必须完全匹配,否则import torch会 segmentation fault。


5. 模型融合不是简单平均:用ensemble.py实现带置信度加权的四模型投票

5.1 为什么不用sklearn.ensemble.VotingClassifier?——它无法处理 CNN/LSTM 的时序输出

VotingClassifier要求所有模型输出predict_proba(),但 LSTM 的forward()输出是 logits,需手动 softmax;且 kNN/SVM 的概率是predict_proba(),CNN/LSTM 的概率是F.softmax(output, dim=1),数值尺度不一致。包里ensemble.py用置信度归一化 + 动态权重:

# ensemble.py import torch.nn.functional as F def ensemble_predict(knn_prob, svm_prob, cnn_logits, lstm_logits, weights): # Step 1: 统一转概率 cnn_prob = F.softmax(cnn_logits, dim=1).cpu().numpy() lstm_prob = F.softmax(lstm_logits, dim=1).cpu().numpy() # Step 2: 置信度归一化(让各模型输出在 [0,1] 同尺度) knn_conf = knn_prob.max() # kNN 置信度 = 最大概率 svm_conf = svm_prob.max() cnn_conf = cnn_prob.max() lstm_conf = lstm_prob.max() # Step 3: 加权平均(权重 × 置信度 × 概率) weighted_sum = ( weights[0] * knn_conf * knn_prob + weights[1] * svm_conf * svm_prob + weights[2] * cnn_conf * cnn_prob + weights[3] * lstm_conf * lstm_prob ) return np.argmax(weighted_sum, axis=1) # weights 示例:[0.15, 0.25, 0.35, 0.25] —— CNN 权重最高,因它空间特征最强

逻辑说明:knn_conf * knn_prob是核心——kNN 在简单场景(如纯水体)置信度接近 1.0,此时它权重放大;在复杂场景(如城乡结合部)置信度可能只有 0.4,权重自动衰减。这比固定权重更鲁棒。

5.2 如何确定weights?——用验证集 grid search,不是拍脑袋

包里ensemble_tune.py在[0.05,0.1,0.15,...,0.4]范围内对 4 个权重做穷举(共 125 种组合),记录每种组合在验证集上的 OA。结果存在ensemble_weights.csv:

kNNSVMCNNLSTMOA (%)
0.100.200.400.3093.8
0.150.250.350.2594.2← 最优
0.200.300.300.2093.5

参数说明:CNN 权重 0.35 最高,印证了空间特征对遥感识别的决定性作用;LSTM 权重 0.25 低于 CNN,说明时序信息是补充而非主导。

5.3 验证 ensemble 效果:用confusion_matrix_analysis.py看哪类提升最大

单纯看 OA 会掩盖问题。confusion_matrix_analysis.py生成 per-class accuracy 表,并标出提升最显著的 3 类:

ClassSingle Model Acc (%)Ensemble Acc (%)Δ (%)原因分析
Bridge82.191.7+9.6kNN 粗筛 + CNN 边缘检测联合发力
Football Field76.389.2+12.9SVM 对规则几何形状判别强,LSTM 补充草皮状态变化
River89.593.1+3.6所有模型都强,提升有限

技巧:运行python confusion_matrix_analysis.py --model ensemble会自动画出热力图,红色越深表示该类被其他类误判越多——比如 “Industrial” 和 “Storage Tanks” 常混淆,说明需在 CNN 中加 shape-aware loss。

从那以后我每次部署遥感识别 pipeline,都强制走一遍ensemble.py的置信度加权流程,哪怕单模型 OA 已达 92%——因为真实场景里,模型在“难样本”上的表现,才是交付时甲方盯着看的指标。希望帮到你。

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

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

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

立即咨询