简介:本资源面向高校学生与开发者,提供一套基于Python的高光谱遥感影像识别与分类完整项目,适用于毕业设计、课程设计及项目开发等场景。项目围绕高光谱影像分类中的特征冗余与泛化能力不足等问题展开,涵盖基于波段组合(2D)²PCA的降维方法、双通道卷积神经网络空谱特征提取模型,以及双通道CNN-SVM融合分类模型,兼顾光谱与空间特征,可有效提升分类精度。压缩包共24个文件,约6.01MB,以13个Python源码文件为核心,辅以mat数据、png结果图、md说明文档及txt配置等,结构清晰,便于按模块阅读与复现。目前已有199人学习下载。读者可获得经过测试的完整源码、项目文档与使用教程,理解从降维、特征提取到分类器融合的完整流程,并在此基础上进行二次开发或论文写作参考。
1. 从一份能跑通的 HSI 分类工程说起:它到底解决了什么
高光谱遥感影像分类这个方向,很多同学卡在同一个地方:论文里的公式看得懂,但真拿到 Indian Pines 这种数据集,从读数据、降维、搭网络到出精度图,中间每一步都能翻车。这份基于 Python 实现的高光谱遥感影像识别与分类工程,就是冲着这个断层来的——它把波段组合 (2D)²PCA 降维、双通道 CNN 空谱特征提取、CNN-SVM 融合分类这三块串成了一条能跑通的链路,附带源码、项目文档和使用教程,目录里能看到HSI-SVM-master、Part_1到Part_4、data、model、logs这些结构。它适合正在做毕业设计、课程设计,或者需要一个高光谱分类基线来二次开发的人。你不需要从零推导休斯现象,但需要知道每一步在干什么、参数怎么调、哪里最容易出问题。
2. 工程结构与数据流:先搞清楚 Part_1 到 Part_4 各自在干什么
拿到一个压缩包,最忌讳的就是直接python main.py。这个工程按Part_1到Part_4切分,本质上是把「数据准备 → 降维 → 特征提取 → 分类评估」拆成了四个可独立调试的阶段。先把这个数据流理顺,后面调参和排错才有坐标系。
2.1 目录映射与各模块职责
从工程命名和常见的高光谱分类实现习惯来看,各部分的职责大致如下:
| 目录/文件 | 职责 | 典型输入 | 典型输出 |
|---|---|---|---|
data | 存放 Indian Pines 等数据集及标签 | .mat原始数据 | 归一化后的数组 |
Part_1 | 数据加载、预处理、样本划分 | 原始 HSI 立方体 | 训练/测试样本 |
Part_2 | (2D)²PCA 波段组合降维 | 高维光谱向量 | 降维后特征 |
Part_3 | 双通道 CNN 空谱特征提取 | 降维特征 + 空间邻域 | 特征向量 |
Part_4 | SVM 分类与精度评估 | CNN 特征 | 分类图、OA/AA/Kappa |
model | 保存训练好的权重 | — | .h5/.pkl |
logs | 训练日志、精度曲线 | — | 文本/图片 |
这个划分的好处是:你可以只跑Part_2验证降维效果,也可以冻结 CNN 只调 SVM 的C和gamma。很多同学一上来就端到端跑,结果精度不对根本不知道是哪一层的问题。
2.2 环境依赖与数据准备
高光谱工程的依赖不算多,但版本敏感。常见做法是建一个干净的虚拟环境,把下面这些装齐:
# 建议 Python 3.8,高光谱常用的 scipy/spectral 在这个版本上最稳 python -m venv hsi_env source hsi_env/bin/activate # Windows 用 hsi_env\Scripts\activate pip install numpy scipy scikit-learn pip install tensorflow==2.4.0 # 或 pytorch,看工程用的是哪个后端 pip install matplotlib spectral # spectral 用于高光谱数据读写这里有几个参数值得说清楚。tensorflow==2.4.0不是随便写的,双通道 CNN 里如果用了Conv2D加自定义层,2.5 之后的 API 变动会让老代码报tf.compat相关的错。spectral这个库专门处理.mat格式的高光谱数据,Indian Pines 的corrected版本就是标准.mat。数据放进data目录后,先确认文件名和代码里loadmat的路径一致,否则会直接抛FileNotFoundError。
import scipy.io as sio import numpy as np # 加载 Indian Pines,常见字段名是 'indian_pines_corrected' 和 'indian_pines_gt' data = sio.loadmat('data/Indian_pines_corrected.mat') gt = sio.loadmat('data/Indian_pines_gt.mat') hsi = data['indian_pines_corrected'].astype(np.float32) # 形状 (145,145,200) label = gt['indian_pines_gt'].astype(np.int32) # 形状 (145,145) # 按波段做归一化,避免量纲差异影响 PCA hsi = (hsi - hsi.min()) / (hsi.max() - hsi.min()) print(hsi.shape, label.shape, np.unique(label))这段代码的逻辑是:先把数据读成float32,再做全局 min-max 归一化。注意np.unique(label)会告诉你标签里有几类、有没有 0(0 通常是背景类,训练时要剔除)。Indian Pines 标准是 16 类,如果你打印出来是 17 个值,那个多出来的 0 就是背景,样本划分时必须过滤掉,否则分类器会把背景也当成一类学。
3. (2D)²PCA 降维:休斯现象到底怎么被压下去的
高光谱最反直觉的一点是:波段不是越多越好。当特征维数超过某个临界点,分类器性能反而下降,这就是休斯现象。原因在于有限训练样本下,维数越高,类间可分性被噪声稀释得越厉害。这一章讲清楚 (2D)²PCA 为什么适合高光谱,以及怎么在工程里落地。
3.1 为什么是 (2D)²PCA 而不是普通 PCA
普通 PCA 把每个像素的 200 维光谱当成一个向量做降维,只利用了光谱信息,丢掉了空间结构。而 (2D)²PCA 是双向二维主成分分析:它在行方向和列方向分别做 PCA,同时压缩空间维和光谱维。对高光谱来说,一个像素邻域本身就是一个小二维矩阵,双向 PCA 能在保留空间纹理的同时降低波段冗余。
工程里Part_2的波段组合逻辑,常见做法是先对波段分组,再对每组做 (2D)²PCA,最后把各组的主成分拼接。这样做的原因是:直接对全部 200 个波段做一次双向 PCA,计算量大且主成分解释性差;分组后每组内部相关性高,降维更干净。
from sklearn.decomposition import PCA def band_group_pca(hsi, group_size=20, n_components=5): """按波段分组做 PCA,返回拼接后的降维特征""" h, w, b = hsi.shape groups = [] for start in range(0, b, group_size): block = hsi[:, :, start:start+group_size] # 展平成 (h*w, group_size) 做 PCA flat = block.reshape(-1, block.shape[-1]) pca = PCA(n_components=n_components) reduced = pca.fit_transform(flat) groups.append(reduced) # 沿特征维拼接 return np.concatenate(groups, axis=1) features = band_group_pca(hsi, group_size=20, n_components=5) print(features.shape) # (145*145, 50) 左右参数说明:group_size=20表示每 20 个波段一组,Indian Pines 有 200 个波段,正好分 10 组;n_components=5是每组保留 5 个主成分,最终特征维数是 50。这两个参数是精度和计算量的平衡点——组太大,组内相关性下降,降维效果变差;组太小,主成分拼接后维数又上去了。我一般会先跑n_components从 3 到 10 的网格,看验证集 OA 的拐点在哪。
3.2 降维后的验证:怎么确认休斯现象被压住了
降维不是跑完就完事,得验证。最直接的办法是画一条「特征维数 vs 分类精度」的曲线:如果曲线在某个维数后开始下降,说明休斯现象还在;如果曲线趋于平稳或缓慢上升,说明降维有效。
from sklearn.svm import SVC from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score def evaluate_dimension(features, labels, dims=[10, 30, 50, 80, 120]): scores = [] for d in dims: X = features[:, :d] X_tr, X_te, y_tr, y_te = train_test_split( X, labels, test_size=0.3, random_state=42, stratify=labels) clf = SVC(kernel='rbf', C=100, gamma='scale') clf.fit(X_tr, y_tr) scores.append(accuracy_score(y_te, clf.predict(X_te))) return dict(zip(dims, scores)) # 注意:labels 要先用 gt 展平并剔除背景 0这段代码用 SVM 做快速探针,不涉及 CNN,几分钟就能跑完。如果 50 维之后精度不再明显上升甚至下降,说明n_components=5是合理的;如果 80 维还在涨,就得把n_components调大。这一步是很多同学忽略的——直接上 CNN,训练半天,精度不对再回头查降维,时间全浪费了。
4. 双通道 CNN 与 CNN-SVM 融合:空谱特征怎么进分类器
降维解决的是冗余问题,接下来要解决的是「光谱特征和空间特征怎么一起用」。双通道 CNN 的思路是:一个通道吃光谱向量,一个通道吃空间邻域块,两路特征在高层融合后再分类。而 CNN-SVM 融合则是把 CNN 当特征提取器,把提取出的特征喂给 SVM,用 SVM 的泛化能力兜底。
4.1 双通道 CNN 的输入构造与网络搭建
双通道的关键在于输入不是单一形式。光谱通道输入的是降维后的光谱向量(比如 50 维),空间通道输入的是以该像素为中心的p×p邻域块(比如 9×9)。两者形状不同,所以网络前几层要分开写。
import tensorflow as tf from tensorflow.keras import layers, Model def build_dual_channel_cnn(spectral_dim=50, patch_size=9, n_classes=16): # 光谱通道:一维卷积提取光谱特征 spec_in = layers.Input(shape=(spectral_dim,), name='spectral_input') x1 = layers.Reshape((spectral_dim, 1))(spec_in) x1 = layers.Conv1D(32, 3, activation='relu', padding='same')(x1) x1 = layers.Conv1D(64, 3, activation='relu', padding='same')(x1) x1 = layers.GlobalAveragePooling1D()(x1) # 空间通道:二维卷积提取邻域纹理 spa_in = layers.Input(shape=(patch_size, patch_size, spectral_dim), name='spatial_input') x2 = layers.Conv2D(32, 3, activation='relu', padding='same')(spa_in) x2 = layers.Conv2D(64, 3, activation='relu', padding='same')(x2) x2 = layers.GlobalAveragePooling2D()(x2) # 特征融合 merged = layers.Concatenate()([x1, x2]) merged = layers.Dense(128, activation='relu')(merged) merged = layers.Dropout(0.4)(merged) out = layers.Dense(n_classes, activation='softmax')(merged) return Model(inputs=[spec_in, spa_in], outputs=out)参数说明:spectral_dim=50对应上一章降维后的维数;patch_size=9是空间邻域大小,奇数保证中心像素对齐;Dropout(0.4)是防过拟合的关键,高光谱样本少,不加 dropout 训练集精度能到 99% 但测试集惨不忍睹。两个通道各自用GlobalAveragePooling压成向量再拼接,比直接 flatten 参数量小很多,也更不容易过拟合。
4.2 CNN-SVM 融合:把 softmax 换成 SVM
CNN 直接输出 softmax 在样本少的时候泛化能力弱,这就是原文提到的「分类器泛化能力较弱」的问题。常见做法是把 CNN 最后一层Dense(128)的输出当特征,训练一个 SVM 做最终分类。
from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler # 假设 model 是上面搭好的双通道 CNN,先训练到收敛 # 然后取融合层输出作为特征 feature_extractor = Model(inputs=model.input, outputs=model.get_layer(index=-3).output) X_train_feat = feature_extractor.predict([X_spec_train, X_spa_train]) X_test_feat = feature_extractor.predict([X_spec_test, X_spa_test]) # 特征标准化后再喂 SVM scaler = StandardScaler() X_train_feat = scaler.fit_transform(X_train_feat) X_test_feat = scaler.transform(X_test_feat) svm = SVC(kernel='rbf', C=1000, gamma='scale', probability=True) svm.fit(X_train_feat, y_train) pred = svm.predict(X_test_feat)逻辑说明:get_layer(index=-3)取的是融合层后面的 Dense 层输出,具体索引要看你的网络结构,用model.summary()确认。SVM 的C=1000比普通场景大,是因为 CNN 特征已经比较紧凑,需要更大的惩罚系数来分开边界样本。gamma='scale'是安全默认值,如果精度不理想,可以试gamma=0.01到0.1之间。这一步的坑在于:CNN 特征提取器和 SVM 要分开调,先固定 CNN 调 SVM,再回头微调 CNN,同时调两边会互相干扰。
5. 避坑与排查:这份工程最容易翻车的五个地方
高光谱工程的坑大多不在算法本身,而在数据对齐、维度匹配和版本兼容上。下面这五条是我实际跑这类工程时踩过的,按「现象 → 原因 → 解决」写清楚。
5.1 精度异常高但分类图全黑
现象:训练时验证集 OA 到 99%,但输出的分类图大片是背景色。原因:样本划分时没剔除标签为 0 的背景像素,分类器把背景也当成一类学了,测试集里背景占多数,精度虚高。解决:在生成样本前加mask = label > 0,只对前景像素做训练和评估,分类图输出时再把背景填回去。
5.2 空间通道输入形状报错
现象:ValueError: Input 0 is incompatible with layer conv2d。原因:空间邻域块的形状是(patch_size, patch_size, spectral_dim),但构造时用了降维前的波段数,和网络定义的spectral_dim对不上。解决:确认空间块是在降维后的特征上取的,或者网络输入层改成降维前的波段数,两者必须一致。用print(X_spa.shape)在喂数据前检查一次。
5.3 PCA 拟合时内存溢出
现象:对整幅影像做 PCA 时MemoryError。原因:把(145, 145, 200)直接 reshape 成(21025, 200)没问题,但如果数据是更大的场景,或者用了float64,内存会翻倍。解决:读数据时就转float32,PCA 用sklearn的IncrementalPCA分批拟合,或者先按波段分组再降维,别一次性处理全部波段。
5.4 SVM 训练时间过长
现象:CNN 特征提取很快,但 SVM 拟合卡住。原因:样本数上万、特征维数 128 时,RBF 核 SVM 的复杂度是 O(n²) 到 O(n³)。解决:先用LinearSVC快速验证特征可分性,再用 RBF;或者对训练样本做分层抽样,每类取固定数量(比如每类 200 个)训练 SVM,测试集保持全量。
5.5 日志目录为空、模型没保存
现象:跑完Part_4,logs和model目录里什么都没有。原因:代码里的保存路径是相对路径,而你在别的目录下执行脚本,文件写到了别处;或者保存逻辑被注释掉了。解决:在脚本开头用os.chdir(os.path.dirname(os.path.abspath(__file__)))切到脚本所在目录,保存模型时用model.save('model/cnn_svm.h5')并确认目录存在。
6. 进阶技巧:把这份工程变成你自己的实验平台
跑通只是起点,真正有价值的是把它改成能验证你自己想法的平台。我一般会做三件事:固定随机种子、加早停、把精度指标写进日志。
import random, numpy as np, tensorflow as tf def set_seed(seed=42): random.seed(seed) np.random.seed(seed) tf.random.set_seed(seed) set_seed(42) # 早停 + 模型检查点 callbacks = [ tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=15, restore_best_weights=True), tf.keras.callbacks.ModelCheckpoint('model/best.h5', monitor='val_loss', save_best_only=True) ]固定种子是为了让每次实验可比,不然你改了参数精度涨了 2%,可能只是随机初始化运气好。patience=15表示验证损失 15 轮不降就停,高光谱训练通常 100 轮以内收敛,设太大浪费时间。restore_best_weights=True保证最后用的是验证集最好的权重,而不是最后一轮的。
再进一步,可以把Part_2的降维方法换成 LDA、LE 或者自己写的流形学习,把Part_3的双通道换成注意力机制,只要保持输入输出接口一致,就能快速做消融实验。我习惯在logs里每次实验建一个带时间戳的子目录,把参数配置和 OA/AA/Kappa 一起写进去,回头对比不用翻代码。
从那以后我每次拿到这类高光谱工程,都强制先跑一遍降维探针、确认样本划分剔了背景、再端到端训练,这三步走完基本不会出玄学问题。希望帮到你。
本文还有配套的精品资源,点击获取