简介:这是一份基于Python实现的高光谱遥感影像识别与分类完整项目,面向毕业设计、课程设计及项目开发场景。项目针对高光谱数据维数灾难导致的休斯现象,提出基于波段组合(2D)2PCA的高光谱降维方法,降低数据冗余并提升后续处理效率;同时构建双通道卷积神经网络空谱特征提取模型,并融合SVM分类器形成CNN-SVM识别模型,兼顾特征提取能力与泛化性能,有效提升分类精度。资源共24个文件,含13个Python源码文件、2个MAT数据文件、PNG示意图、pyc编译文件、Ubuntu环境配置及md文档与txt说明,压缩包大小约6.01MB,目录含data、model、logs等模块,方便按流程复现实验。项目源码经过严格测试,并附带项目文档与使用教程,可帮助读者快速理解算法原理、运行环境搭建及模型调优思路。目前已有199人浏览学习,适合需要完整案例参考或在此基础上拓展研究的Python与遥感方向学习者。
1. 高光谱遥感影像分类的三大障碍与HSI-SVM项目设计思路
第一次拿Indian Pines跑高光谱分类,八成你会在休斯现象上翻车:两百个波段挤在145×145像素的小图上,标记样本少则几十个,分类精度不升反降。基于Python实现的HSI-SVM项目就是针对这个痛点来的:用(2D)2PCA做波段组合降维,用双通道CNN同时提取光谱和空间特征,再把特征交给SVM做最终分类。源码按Part_1到Part_4组织,覆盖预处理、降维、训练和评估,README里有完整教程。毕业设计、课程设计可以拿它当基线,项目开发里这种CNN特征+SVM融合的思路对少样本高维数据也很有参考价值。下面我按数据准备、降维、双通道CNN、CNN-SVM融合和复现细节这条线展开。
2. Indian Pines数据集与(2D)2PCA降维:从光谱维数灾难到波段组合
2.1 Indian Pines数据格式与加载方式
Indian Pines是AVIRIS传感器采集的高光谱影像,尺寸145×145,原始波段224个,去掉噪声和吸水波段后常用200个波段。每个像素是一条光谱曲线,地面覆盖包括玉米、大豆、小麦、草地、树木共16类。由于扫描区域中部分像素没有标注,实际分类实验只使用带标签的约1万多个像素。
| 属性 | 值 |
|---|---|
| 影像尺寸 | 145 × 145 像素 |
| 原始波段数 | 224 |
| 常用波段数 | 200(去掉吸水/噪声波段) |
| 类别数 | 16(1~16,0为背景) |
| 标记像素总数 | 约10249 |
项目源码里的data目录一般放两种格式:一种是.mat文件(如Indian_pines_corrected.mat),另一种已转成numpy的.npy或CSV。加载.mat时要注意文件版本:MATLAB R7.3的HDF5格式需要h5py,旧版本用scipy.io.loadmat。
import h5py import numpy as np from scipy import io # 处理 MATLAB v7.3 格式 def load_hsi(path): with h5py.File(path, 'r') as f: img = f['paviaU'][:] # 实际键名以文件为准 return img # 处理传统 .mat 格式 data = io.loadmat('Indian_pines_corrected.mat')['indian_pines_corrected']代码里我按常见命名写了paviaU和indian_pines_corrected两个键,实际使用前要先打开mat文件看keys,避免KeyError。加载后数据维度是(M,N,B),即行、列、波段。Indian Pines的标签矩阵在同目录单独的mat文件里,形状也是145×145,像素值为0~16,其中0表示未标注。
2.2 休斯现象:为什么波段越多精度越低
休斯现象(Hughes Phenomenon)的定义:当训练样本数量固定时,分类精度随特征维数先上升、到达临界点后开始下降。原因是高维空间中样本分布变得稀疏,估计协方差矩阵需要的样本量随维度平方级增长,而高光谱的标注样本本来就稀缺。直接对所有波段做分类,SVM、KNN这些传统分类器都会遇到这个问题。
降维不是简单丢掉波段,而是要保留可分性。常见做法是PCA、MNF,但PCA把光谱向量当成一维序列,没有利用像素的二维空间结构。(2D)2PCA的思路是:先把相邻波段组合成二维图像,再对图像矩阵同时做行方向和列方向的投影,这样降维后的特征既压缩了波段冗余,又保留了空间纹理。对Indian Pines这种地物边界明显的影像,这种预处理比全局PCA更稳。
2.3 (2D)2PCA的数学表达与numpy实现
设第i个训练样本的图像块为 $A_i \in R^{h \times w}$,这里h、w是空间尺寸,每个波段作为图像的一层。(2D)2PCA先构造行方向的总离散度矩阵:
$$G_r = \frac{1}{n}\sum_{i=1}^n (A_i - \bar{A})^T(A_i - \bar{A})$$
求G_r的前d_r个最大特征值对应特征向量得到投影矩阵U,接着把图像投影为$B_i = A_i U$,再对B_i做列方向投影矩阵V,最终得到降维结果$C_i = V^T A_i U$。
下面是一个可直接运行的简化实现:
import numpy as np from numpy.linalg import eigh def twod2pca(gray_images, d_row, d_col): """ gray_images: list of 2D image matrices (h, w) d_row, d_col: 保留的行/列主成分数量 """ n = len(gray_images) h, w = gray_images[0].shape mean_img = np.zeros((h, w)) for img in gray_images: mean_img += img mean_img /= n # 行方向协方差矩阵 G_r (w x w) G_r = np.zeros((w, w)) for img in gray_images: diff = img - mean_img G_r += diff.T @ diff G_r /= n eigvals_r, eigvecs_r = eigh(G_r) idx_r = np.argsort(eigvals_r)[::-1][:d_row] U = eigvecs_r[:, idx_r] # 投影矩阵 # 第一次投影 Y = [(img - mean_img) @ U for img in gray_images] # 列方向协方差矩阵 G_c (h x h) G_c = np.zeros((h, h)) for proj in Y: G_c += proj @ proj.T G_c /= n eigvals_c, eigvecs_c = eigh(G_c) idx_c = np.argsort(eigvals_c)[::-1][:d_col] V = eigvecs_c[:, idx_c] # 最终降维特征 Z = [V.T @ proj for proj in Y] return Z, U, V实现里我做了两步投影:先沿行方向压缩到d_row维,再沿列方向压缩到d_col维。eigh返回的特征值升序,所以用argsort()[::-1]逆序取前k个。特征矩阵U的列是标准正交基,投影后保留了方差最大方向上的信息。这里要注意:输入图像要先做归一化或者至少减均值,否则投影方向会被光照强度主导。
对于高光谱数据,不会把200个波段一次性做成一个2D图像。项目里“波段组合”的做法是:先选出信息最丰富的若干波段(比如基于信息熵或PCA贡献率),把选出的波段组合成多通道图像,再对每个通道或对组合后的张量执行(2D)2PCA。常见做法是先对所有波段做一次快速PCA,保留前3~10个主成分作为图像通道,再在这个低通道图像上计算(2D)2PCA。这样既缓解了休斯现象,计算量也可控。
2.4 数据标准化与训练集划分
降维之后,特征值尺度可能差异很大。如果后面接CNN,通常对每个波段做z-score标准化;如果接SVM,标准化甚至比降维更关键。标准化参数必须在训练集上计算,不然测试集信息被泄漏。
def standardize(train_feat, test_feat): mean = train_feat.mean(axis=0) std = train_feat.std(axis=0) train_norm = (train_feat - mean) / (std + 1e-6) test_norm = (test_feat - mean) / (std + 1e-6) return train_norm, test_norm训练集划分有两种主流策略:随机采样像素划分,和按连续地块划分。前者简单,但同一块地相邻像素可能同时出现在训练和测试集里,导致分数虚高;后者更接近真实遥感分类场景,但样本少时方差大。项目代码里一般用random seed控制的可复现随机抽样。如果是毕业论文,建议两种都做,并在论文里说明差距。
3. 双通道CNN空谱特征提取:光谱序列与空间邻域并行建模
3.1 为什么需要双通道:光谱与空间互补
高光谱影像里,同一种地物的光谱曲线形状有相似性,但相邻像素的地物往往也相同,这叫空间相关性。单通道CNN只能处理一种输入:
- 1D CNN把每个像素的光谱向量视为长度为B的序列,能捕捉谱段之间的依赖关系,但完全忽略邻域信息。
- 2D CNN取像元周围的邻域块作为输入,能提取纹理和边缘特征,但把波段当通道,不强调光谱曲线的连续性。
双通道CNN让两个分支并行提取特征,最后在某个层拼接。光谱分支输入形状是(B,),空间分支输入形状是(h_window, w_window, B)。这种设计在分类精度上比单分支高,尤其对植被、建筑物这种纹理差异大的类别。
3.2 双通道模型结构设计
下面是我在HSI项目里常用的配置表:
| 分支 | 层 | 核/步长 | 输出尺寸 |
|---|---|---|---|
| 光谱 | Conv1D 1 | 3×1 / 1 | 64 |
| 光谱 | Conv1D 2 | 3×1 / 1 | 128 |
| 光谱 | MaxPool | 2 | 64 |
| 光谱 | Flatten | - | 64×(B/2?) 实际计算 |
| 空间 | Conv2D 1 | 3×3 / 1 | 32 |
| 空间 | Conv2D 2 | 3×3 / 1 | 64 |
| 空间 | MaxPool | 2×2 | 32 |
| 空间 | Flatten | - | 32×(h_pool×w_pool) |
| 融合 | Concat + FC1 | - | 256 |
| 分类 | FC2 + Softmax | - | num_classes |
这个表是示意,具体输出尺寸取决于输入窗口大小。一般空间邻域窗口取11×11或13×13,太大包含过多异类像素,太小空间上下文不足。
3.3 PyTorch实现双通道CNN
import torch import torch.nn as nn class DualChannelCNN(nn.Module): def __init__(self, n_bands, window_size, num_classes): super().__init__() # 光谱分支:输入 (B, 1, n_bands) self.spectral_branch = nn.Sequential( nn.Conv1d(1, 32, kernel_size=3, padding=1), nn.BatchNorm1d(32), nn.ReLU(), nn.Conv1d(32, 64, kernel_size=3, padding=1), nn.BatchNorm1d(64), nn.ReLU(), nn.AdaptiveAvgPool1d(16) ) # 空间分支:输入 (B, n_bands, window, window) self.spatial_branch = nn.Sequential( nn.Conv2d(n_bands, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(), nn.AdaptiveAvgPool2d((4, 4)) ) self.fusion = nn.Sequential( nn.Flatten(), nn.Linear(64*16 + 64*4*4, 256), nn.ReLU(), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x_spectral, x_spatial): f_s = self.spectral_branch(x_spectral) f_p = self.spatial_branch(x_spatial) feat = torch.cat([f_s.flatten(1), f_p.flatten(1)], dim=1) return self.fusion(feat)思路说明:光谱分支用1D卷积处理归一化后的光谱曲线,自适应池化固定输出长度;空间分支直接用整个波段立方体作为多通道图像输入,用2D卷积提取局部纹理。融合层把两个分支的特征向量拼接后送全连接。AdaptiveAvgPool的好处是输入窗口尺寸变化时不需要改全连接维度。实际训练时输入要扩展维度,光谱分支需要reshape为(B,1,B),空间分支保持(B,Bands,H,W)。
注意:
BatchNorm1d和BatchNorm2d在batch size较小时统计量不稳定。如果显存允许,batch size尽量不低于32,否则训练和推理时BN的均方差不一致会拉低精度。
3.4 从分类网络到特征提取器
CNN-SVM融合的关键是让CNN只做特征提取,把最后一层Softmax去掉。在模型里定义extract_features方法,返回全连接层之前的256维向量:
def extract_features(self, x_spectral, x_spatial): f_s = self.spectral_branch(x_spectral) f_p = self.spatial_branch(x_spatial) feat = torch.cat([f_s.flatten(1), f_p.flatten(1)], dim=1) return feat这样训练时用整个网络,测试时先用该方法抽出特征,再输入SVM。需要提醒的是,spectral_branch和spatial_branch的batch norm在eval模式下的行为与训练不同,所以提取特征前务必调用model.eval(),否则特征分布不稳定。
4. CNN-SVM融合分类:让卷积特征适配最大间隔分类器
4.1 为什么CNN后面还要接SVM
深度学习分类头通常用Softmax交叉熵,它学到的决策边界是光滑的,但在小样本、特征重叠大的场景下,Softmax边界不一定是最优的。SVM以最大化间隔为优化目标,对高维稀疏特征有更强的鲁棒性。CNN负责非线性特征提取,SVM负责分类决策,两者天然互补。
项目里“双通道CNN-SVM融合”的实现思路是:先把双通道CNN在有标签样本上训练到收敛,然后截断分类层,把训练集和测试集都映射到256维特征空间,最后用RBF核SVM作为分类器。这样既获得了CNN提取的抽象特征,又用SVM避免了Softmax在小数据集上的过拟合。
| 分类头 | 决策边界 | 小样本表现 | 实现复杂度 |
|---|---|---|---|
| Softmax | 概率平滑 | 容易过拟合特征 | 直接替换最后一层 |
| RBF SVM | 最大间隔 | 鲁棒性更好 | 需要单独训练和调参 |
4.2 训练与特征提取的标准流程
具体流程分成五步:
- 加载降维后的Indian Pines数据,构造光谱序列样本和空间邻域块样本。
- 初始化双通道CNN,用交叉熵损失训练,记录最佳验证精度。
- 加载最优权重,调用
extract_features提取全连接层前的特征向量。 - 对特征做标准化,用网格搜索或启发式方法确定SVM的C和gamma。
- 在测试集上计算混淆矩阵、总体精度、平均精度和Kappa系数。
第4步的SVM训练代码:
import numpy as np from sklearn.svm import SVC from sklearn.model_selection import GridSearchCV from sklearn.preprocessing import StandardScaler def train_svm(features, labels): # 先标准化再进SVM,避免大数值维度主导边界 scaler = StandardScaler() X_scaled = scaler.fit_transform(features) params = { 'C': [0.1, 1, 10, 100], 'gamma': ['scale', 0.01, 0.001] } grid = GridSearchCV( SVC(kernel='rbf', class_weight='balanced'), params, cv=3, scoring='accuracy', n_jobs=-1 ) grid.fit(X_scaled, labels) return grid.best_estimator_, scalerC控制误分类惩罚,越大越容易过拟合;gamma控制RBF半径,越小决策边界越平滑。class_weight='balanced'在Indian Pines这类类别不平衡数据上很重要,否则少数类(如Oats、Grass-trees)容易被吞。网格搜索的cv不要太大,3折即可,否则耗时很长。
4.3 提升SVM精度的特征处理细节
在训练SVM之前,要考虑两个细节:
- 特征是否要归一化:CNN输出特征虽然经过了BN,但不同维度的数值范围仍有差异。SVM基于距离计算,不归一化会导致变化大的维度主导边界。
- 是否要降低SVM输入的维度:256维对于几千样本的Indian Pines已经够用,但如果特征维度过高,可以先做PCA压缩到50维,再喂SVM。项目里的(2D)2PCA已经做过一次降维,所以这一步通常省略。
下面是完整的评估代码,顺便给出OA、AA、Kappa三个指标:
from sklearn.metrics import accuracy_score, cohen_kappa_score, confusion_matrix y_pred = best_svm.predict(scaler.transform(test_features)) y_true = test_labels oa = accuracy_score(y_true, y_pred) aa = np.mean([accuracy_score(y_true[y_true==c], y_pred[y_true==c]) for c in np.unique(y_true)]) kappa = cohen_kappa_score(y_true, y_pred) cm = confusion_matrix(y_true, y_pred)AA是每个类别精度的算术平均,不考虑类别样本数;OA直接看全局正确比例。Kappa排除了随机一致性的影响,遥感论文里最看重它。打印混淆矩阵时,把每类的召回率单独算出来,可用于分析哪些类别被混淆。
4.4 与纯CNN的对比实验设计
要证明CNN-SVM融合有效,需控制变量:相同通道结构、相同训练数据下,只替换分类头。常见做法是训练两个模型:一个用Softmax分类,一个把特征提取后接SVM。在Indian Pines测试集上,融合模型的OA通常会比Softmax高一些,但这种差距在大样本场景下会缩小。因此毕业论文里做这个对比时,要固定随机种子,多跑几次取均值。
项目源码里的Part_3/model保存了训练好的权重,Part_4/logs保存了每次运行日志,可以用TensorBoard或matplotlib画训练曲线。如果复现时发现SVM结果不如Softmax,优先检查数据划分是否泄漏、特征提取时是否忘记model.eval()、SVM参数搜索范围是否过窄。
5. 复现实验参数与验证技巧:划分策略、Kappa系数和三个常见坑
5.1 运行环境与目录映射
我复现这个项目时,环境是Python 3.10、PyTorch 2.0、scikit-learn 1.3。如果从零开始,先装好Python环境再按README安装依赖。注意data目录里原始的mat文件如果是HDF5格式,scipy.io无法直接读取,需要同步检查h5py是否安装。项目目录里Part_1负责数据预处理和可视化,Part_2是(2D)2PCA降维,Part_3是双通道CNN训练,Part_4是CNN-SVM评估。运行顺序不要颠倒,因为后面脚本需要读取前面脚本输出的特征。
5.2 关键超参数设置与调整
下面是几个直接影响最终精度的地方:
| 参数 | 推荐范围 | 说明 |
|---|---|---|
| 空间窗口大小 | 11×11 ~ 15×15 | 太小缺上下文,太大引入噪声 |
| 降维后主成分数量 | 3 ~ 10 | 要结合分类精度折中 |
| CNN batch size | 64 ~ 128 | 样本少时不宜过大 |
| 学习率 | 0.001 ~ 0.01 | Adam的话0.001起 |
| SVM的C | 1 ~ 100 | 与CNN特征尺度有关 |
| SVM的gamma | scale或0.001 | 用GridSearchCV搜索 |
如果训练不收敛,先检查批量归一化是否放在卷积之后、激活之前。如果分类结果里某一类全是零,检查标签对齐,Indian Pines的标签矩阵里某些类只有不到20个像素,划分时要设置stratify参数保证每类都有训练样本。
5.3 验证时最常见的三个坑
第一个坑是数据泄漏:直接对全部数据做归一化再划分,会把测试集统计信息带进训练。正确做法是先拆分,再在训练集上计算均值和标准差。第二个坑是Kappa系数计算错误:cohen_kappa_score默认权重是线性权重,而遥感论文里常用无加权Kappa,保持默认参数即可,但要在论文里写明。第三个坑是空间邻域块构建时越界:窗口大小超过5×5时,图像边缘像素没有完整邻域,常见做法是零点填充或反射填充,实际项目里多数用torch.nn.functional.pad实现反射填充。
最后给出一个快速验证模型是否有效的命令:在Part_4目录下运行python evaluate.py,脚本会后台加载Part_3里保存的模型权重,在测试集上输出分类报告,并在logs目录生成混淆矩阵图片。如果跑出来的精度和README偏差过大,优先检查数据集版本——不同来源的Indian Pines mat文件波段数和标签编号可能不同,必要时按实际形状修改加载代码。我在复现时把窗口设为11,降维保留5个主成分,分类结果与真实地块形状基本吻合;如果你换用PaviaU数据集,记得先裁剪掉标签为0的背景边界再训练。
本文还有配套的精品资源,点击获取