news 2026/10/11 1:28:27

Python高光谱遥感影像识别与分类:从数据读取到3D-CNN实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python高光谱遥感影像识别与分类:从数据读取到3D-CNN实战

简介:这份资源面向计算机、遥感与人工智能方向的本科生和研究生,适用于毕业设计、课程设计及项目开发场景,围绕高光谱遥感影像识别与分类这一课题提供完整实现方案。压缩包共24个文件,约6.01MB,以13个Python源码文件为核心,配合mat数据文件、png结果图、md说明文档及txt配置等,覆盖从数据读取、模型训练到结果可视化的完整流程。项目针对高光谱分类中的休斯现象、空谱特征利用不足与分类器泛化能力弱等问题,依次实现了基于波段组合(2D)²PCA的降维方法、双通道卷积神经网络空谱特征提取模型,以及双通道CNN与SVM相融合的分类模型,并附带项目文档与使用教程,便于理解算法思路并在此基础上延伸改进。目前已有199人学习参考,适合希望快速搭建高光谱分类实验、对照源码复现并完成论文或答辩材料的学习者。

1. 高光谱遥感影像识别与分类:从数据立方体到可复现的 Python 工程

高光谱遥感影像识别与分类,本质上是把一条几百个波段的光谱曲线,映射到一个地物类别标签上。和普通 RGB 图像不同,高光谱数据是一个三维立方体:空间两维加光谱一维,每个像素都带着连续波段的光谱信息。这带来的好处是"同色异谱"的地物能被区分开,代价是波段间高度相关、样本标注昂贵、维度灾难明显。很多做毕业设计或课程设计的同学,拿到一份高光谱数据集后卡在三个地方:数据怎么读、特征怎么降、分类器怎么选。这篇笔记就按一条能跑通的工程路径,把 Python 实现高光谱识别与分类的完整链路拆开讲,包括源码结构、关键参数、常见翻车点,以及怎么验证结果不是玄学。适合正在做遥感、模式识别方向毕业设计或项目开发的人,也适合想把高光谱当成一个具体落地任务来练手的工程师。

2. 高光谱数据怎么读、怎么切、怎么标准化

2.1 数据格式与读取:.mat、.tif、.hdr 三种常见入口

高光谱数据集常见的存储形式有三类,读取方式差别很大,选错了后面全是坑。第一类是 MATLAB 的.mat文件,比如 Indian Pines、Pavia University、Salinas 这些经典数据集,通常一个文件里同时存了数据立方体和标签矩阵。第二类是 ENVI 格式,一个.hdr头文件配一个无扩展名或.img的数据文件,元信息全在头文件里。第三类是 GeoTIFF,带地理坐标,适合做真实项目。

用 Python 读取时,.mat用scipy.io.loadmat,ENVI 用spectral库,GeoTIFF 用rasterio。下面是一段能直接跑的最小读取代码:

import scipy.io as sio import numpy as np # 读取 Indian Pines 数据集,键名以实际文件为准 data = sio.loadmat('Indian_pines_corrected.mat') gt = sio.loadmat('Indian_pines_gt.mat') # 常见键名:data 里是 'indian_pines_corrected',标签是 'indian_pines_gt' cube = data['indian_pines_corrected'].astype(np.float32) # 形状 (145,145,200) label = gt['indian_pines_gt'].astype(np.int32) # 形状 (145,145) print('数据立方体形状:', cube.shape) print('标签类别:', np.unique(label))

逻辑说明:cube是三维数组,前两维是空间,第三维是波段。label是二维整数矩阵,0 通常代表背景或未标注,1 到 N 是地物类别。参数上要注意astype(np.float32),很多.mat里存的是uint16,直接送进模型会因为数值范围差异导致梯度异常。标签转int32是为了后续做交叉熵时索引稳定。

ENVI 格式的读取稍有不同:

import spectral # 读取 ENVI 头文件,img 是数据立方体 img = spectral.open_image('data.hdr') cube = img.load().astype(np.float32) print('波段数:', img.nbands, '行数:', img.nrows, '列数:', img.ncols)

spectral.open_image会自动解析.hdr里的bands、data type、interleave等字段。如果头文件里data type写的是 12(对应 uint16),而实际数据是 float,读出来会全是噪声,这时候要手动改头文件或指定 dtype。

2.2 从立方体到样本矩阵:滑窗、逐像素与超像素

高光谱分类的输入组织方式决定了模型结构。逐像素方式把每个像素的光谱向量当成一个样本,形状是(N, B),N 是像素数,B 是波段数。这种方式简单,但丢掉了空间信息,椒盐噪声严重。滑窗方式以每个像素为中心取一个k×k的空间邻域,输入变成(N, k, k, B),能同时利用空间和光谱。超像素方式先用 SLIC 等方法分割,再在超像素上聚合特征。

我一般会先用逐像素跑一个基线,确认数据读取和标签对齐没问题,再上滑窗。滑窗的代码骨架:

def extract_patches(cube, label, window=11): pad = window // 2 # 对空间维度做镜像填充,避免边缘像素取不到邻域 padded = np.pad(cube, ((pad, pad), (pad, pad), (0, 0)), mode='reflect') patches, targets = [], [] h, w = label.shape for i in range(h): for j in range(w): if label[i, j] == 0: # 跳过背景 continue patch = padded[i:i+window, j:j+window, :] patches.append(patch) targets.append(label[i, j]) return np.array(patches), np.array(targets)

参数说明:window取奇数,常见 7、11、13。太小空间信息不足,太大边缘混合严重且显存吃紧。mode='reflect'比constant好,因为零填充会在边缘引入虚假的低值光谱。这段双重循环在数据量大时很慢,实际项目里我会用numpy.lib.stride_tricks.sliding_window_view向量化,或者直接按类别采样,只取一部分像素做训练。

2.3 标准化与降维:为什么不能直接送进分类器

高光谱波段之间相关性极高,200 个波段里有效维度可能只有 20 到 30。直接送进分类器会有两个后果:一是计算量大,二是过拟合。常见做法是先做光谱维的标准化,再用 PCA 或 MNF 降维。

标准化按波段做,每个波段减均值除标准差:

def standardize(cube): mean = cube.reshape(-1, cube.shape[-1]).mean(axis=0) std = cube.reshape(-1, cube.shape[-1]).std(axis=0) + 1e-8 return (cube - mean) / std

1e-8是防止某些波段方差为零导致除零。注意标准化要在划分训练集之后,用训练集的均值和方差去变换测试集,否则就是数据泄露,这是毕业设计里最容易被答辩老师抓住的点。

PCA 降维用sklearn.decomposition.PCA,保留 95% 方差或固定 30 个主成分:

from sklearn.decomposition import PCA h, w, b = cube.shape flat = cube.reshape(-1, b) pca = PCA(n_components=30, whiten=True) flat_pca = pca.fit_transform(flat) cube_pca = flat_pca.reshape(h, w, 30) print('累计解释方差比:', pca.explained_variance_ratio_.sum())

whiten=True会让各主成分方差归一,对后续用 SVM 或 KNN 有帮助。如果做深度学习,可以不加白化,让网络自己学尺度。降维后的cube_pca再去做滑窗,输入通道从 200 降到 30,训练速度提升明显。

3. 分类器选型:从 SVM 到 3D-CNN 的取舍

3.1 传统方法:SVM 与随机森林在小样本下的表现

高光谱标注样本通常很少,Indian Pines 每类只有几十个标注像素。这种情况下 SVM 配合 RBF 核往往能打过深度网络。sklearn.svm.SVC直接可用,但要注意两个参数:C和gamma。C越大越容易过拟合,gamma控制核的宽度。

from sklearn.svm import SVC from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report X = cube_pca.reshape(-1, 30) y = label.reshape(-1) mask = y > 0 X, y = X[mask], y[mask] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.8, stratify=y, random_state=42) clf = SVC(C=100, gamma='scale', kernel='rbf') clf.fit(X_train, y_train) pred = clf.predict(X_test) print(classification_report(y_test, pred))

这里test_size=0.8是故意的,高光谱常用小训练集大测试集来模拟真实场景。stratify=y保证每类在训练测试里比例一致。gamma='scale'是 1/(n_features * X.var()),比固定值稳。如果类别极不平衡,加class_weight='balanced'。

随机森林对参数不敏感,适合快速出基线:

from sklearn.ensemble import RandomForestClassifier rf = RandomForestClassifier(n_estimators=200, max_depth=None, n_jobs=-1, random_state=42) rf.fit(X_train, y_train) print('RF 准确率:', rf.score(X_test, y_test))

n_estimators200 起步,n_jobs=-1用满 CPU。随机森林能输出特征重要性,可以反过来看哪些波段贡献大,这对写论文分析很有用。

3.2 深度方法:1D-CNN、2D-CNN 与 3D-CNN 的输入差异

深度方法在高光谱里主要三种结构。1D-CNN 只吃光谱向量,输入(N, B, 1),等价于逐像素但用卷积提特征。2D-CNN 把降维后的主成分当成通道,输入(N, H, W, C),用二维卷积提空间特征。3D-CNN 同时卷空间和光谱,输入(N, H, W, B, 1),参数量最大。

用 PyTorch 搭一个 1D-CNN 骨架:

import torch import torch.nn as nn class SpectralCNN(nn.Module): def __init__(self, bands, n_classes): super().__init__() self.net = nn.Sequential( nn.Conv1d(1, 32, kernel_size=7, padding=3), nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(32, 64, kernel_size=5, padding=2), nn.BatchNorm1d(64), nn.ReLU(), nn.AdaptiveAvgPool1d(1), ) self.fc = nn.Linear(64, n_classes) def forward(self, x): # x: (batch, 1, bands) feat = self.net(x).squeeze(-1) return self.fc(feat)

参数说明:kernel_size=7对应光谱上的局部吸收峰,padding保持长度。BatchNorm1d在光谱维上做归一,能加速收敛。AdaptiveAvgPool1d(1)把任意波段长度压成 1,避免全连接层参数爆炸。训练时bands就是降维后的维度,比如 30。

3D-CNN 的输入构造要注意显存。(N, 11, 11, 30, 1)这种形状,batch size 只能开到 32 左右。我一般先用 1D-CNN 跑通流程,再换 3D 看提升,如果提升不到 2 个点,说明数据本身空间信息有限,不值得上大模型。

3.3 训练策略:小样本下的数据增强与早停

高光谱样本少,数据增强很关键。常见做法有光谱加噪、随机波段丢弃、空间翻转旋转。光谱加噪最简单:

def add_noise(x, sigma=0.01): noise = torch.randn_like(x) * sigma return x + noise

sigma取 0.01 到 0.05,太大就破坏光谱形状。空间翻转只对滑窗输入有效,torch.flip即可。

早停用验证集损失监控:

best_loss = float('inf') patience, counter = 10, 0 for epoch in range(200): train_one_epoch(model, train_loader, optimizer) val_loss = evaluate(model, val_loader) if val_loss < best_loss: best_loss = val_loss torch.save(model.state_dict(), 'best.pth') counter = 0 else: counter += 1 if counter >= patience: print('早停于 epoch', epoch) break

patience=10是经验值,数据量小可以设 5。保存best.pth而不是最后一个 epoch,这是后悔药,防止过拟合后拿错模型。

4. 避坑与排查:高光谱分类里最容易翻车的 5 个地方

4.1 现象:准确率 99%,但预测图全是噪点

原因:训练集和测试集用了同一批像素,或者标准化时用了全量数据。高光谱里相邻像素高度相关,随机划分会让训练测试样本几乎一样,准确率虚高。解决:按空间块划分,或者用固定划分文件。标准化统计量只用训练集计算。

4.2 现象:损失不下降,准确率停在背景类比例

原因:标签里 0 是背景,没被过滤,模型学会了全预测背景。解决:训练前mask = y > 0,并且检查np.bincount(y)看类别分布。如果某类样本少于 10 个,考虑合并或过采样。

4.3 现象:PCA 后分类效果反而变差

原因:PCA 是无监督的,保留方差大的方向不一定是判别性强的方向。某些小方差波段反而对区分地物关键。解决:改用 MNF 或监督降维如 LDA,或者直接不降维用 SVM 的核方法。也可以保留更多主成分,比如 50 个,再看效果。

4.4 现象:3D-CNN 训练时显存溢出

原因:滑窗输入(N, 11, 11, 200, 1)太大,200 个波段没降维。解决:先 PCA 到 30 以下,或者用 2D-CNN 把波段当通道。batch size 从 16 开始试,用torch.cuda.amp混合精度。

4.5 现象:换数据集后代码报维度错误

原因:不同数据集波段数、类别数、标签编码不一样。Indian Pines 是 200 波段 16 类,Pavia University 是 103 波段 9 类。解决:把波段数、类别数写成配置参数,读取后打印cube.shape和np.unique(label)确认,不要硬编码。

5. 验证与进阶:怎么确认结果可信,以及一个提点技巧

5.1 用混淆矩阵和 Kappa 系数代替单一准确率

总体准确率会被多数类主导。看混淆矩阵能发现哪两类在互相混。Kappa 系数剔除了随机一致的概率,更可靠。

from sklearn.metrics import confusion_matrix, cohen_kappa_score cm = confusion_matrix(y_test, pred) kappa = cohen_kappa_score(y_test, pred) print('Kappa:', kappa) print(cm)

Kappa 低于 0.6 说明分类器只是比瞎猜好一点,别急着写论文。混淆矩阵里如果两类互相大量误分,去看它们的光谱曲线是不是本来就重叠,重叠的话换分类器也没用,得加空间特征或换波段。

5.2 可视化预测图与标签图对比

把pred填回label形状,用matplotlib画出来,和真值图并排看。预测图如果出现大片同色斑块,说明空间平滑过度;如果碎点很多,说明空间信息没用上。

import matplotlib.pyplot as plt pred_map = np.zeros_like(label) pred_map[mask.reshape(label.shape)] = pred fig, axes = plt.subplots(1, 2, figsize=(10, 5)) axes[0].imshow(label, cmap='jet') axes[0].set_title('Ground Truth') axes[1].imshow(pred_map, cmap='jet') axes[1].set_title('Prediction') plt.show()

5.3 一个提点技巧:测试时增强

训练时增强常见,测试时也能用。对同一个测试像素做多次轻微扰动(加噪、翻转),把多次预测概率平均,能稳定提升 1 到 2 个点。代价是推理时间翻倍,但毕业设计里这点时间换准确率很划算。

def tta_predict(model, x, n=8): model.eval() probs = [] with torch.no_grad(): for _ in range(n): x_aug = add_noise(x, sigma=0.01) probs.append(torch.softmax(model(x_aug), dim=1)) return torch.stack(probs).mean(0)

n=8是扰动次数,太多收益递减。注意扰动幅度要和训练时一致,否则分布偏移反而掉点。

我自己的习惯是,每换一个数据集,先跑逐像素 SVM 拿到基线,再上滑窗加 CNN,最后用 Kappa 和预测图双重确认。如果 CNN 比 SVM 只高一个点以内,我会在论文里老实写 SVM 结果,因为可复现性更好,答辩时也少被追问。高光谱这个方向,数据质量比模型花哨程度重要得多,把读取、划分、标准化这三步做扎实,后面都是水到渠成。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/11 1:25:31

rea:基于Rust的命令行实时事件流分析工具实战

第一次把 rea 的二进制丢到测试服务器上跑通的时候&#xff0c;我脑子里就一个念头&#xff1a;这东西早该做了。不卖关子&#xff0c;rea 是我最近在团队内部打磨的一个事件流分析工具&#xff0c;全称是 Real-time Event Analyzer&#xff0c;解决的问题特别直白&#xff1a;…

作者头像 李华
网站建设 2026/10/11 1:25:29

Visual C++运行库缺失导致老游戏无法启动?教你一次搞定dll报错

简介&#xff1a;这是一份基于Visual C与MFC框架编写的连连看游戏完整工程源码&#xff0c;面向Windows平台上的C初学者和休闲游戏编程爱好者&#xff0c;可用来学习图形界面搭建、图像匹配消除、用户点击交互等游戏开发核心技巧。压缩包共收录26个文件&#xff0c;总体积183KB…

作者头像 李华
网站建设 2026/10/11 1:24:50

FreeCAD 新手入门:5 步从草图到 3D 参数化模型

FreeCAD 新手入门&#xff1a;5 步从草图到 3D 参数化模型 【免费下载链接】FreeCAD Official source code of FreeCAD, a free and opensource multiplatform 3D parametric modeler. 项目地址: https://gitcode.com/GitHub_Trending/fr/FreeCAD 你想给桌上的设备加一个…

作者头像 李华
网站建设 2026/10/11 1:24:33

Claude Code Mods 实战:用配置、提示词与钩子打造高效 AI 编程工作流

1. 从“闪身步”说起&#xff1a;这个项目到底在折腾什么第一次看到“闪身步”这个词&#xff0c;我脑子里蹦出来的不是武侠&#xff0c;而是游戏里那种贴脸走位、瞬间拉开身位再反打的操作。后来把 Claude Code Mods 这套东西摸了一遍&#xff0c;发现用“闪身步”来形容它&am…

作者头像 李华
网站建设 2026/10/11 1:24:22

ArchLinux(一):基础安装(手动版)

本文档使用纯手工的方式安装进行安装ArchLinux&#xff0c;只进行完成命令行的安装&#xff0c;后面有继续操作 一、U盘启动盘制作 ArchLinux 系统下载&#xff1a;https://archlinux.org/ 烧录工具下载&#xff1a;这里使用 BalenaEtcher进行烧录&#xff1b;https://etcher…

作者头像 李华