高光谱数据预处理全流程:坏波段剔除、SG平滑与PCA降维
发布时间:2026/10/1 14:03:02 作者:尧图编辑部 阅读量:1,286

简介高光谱图像因波段众多、数据量大在机器学习建模前往往需要系统的预处理支持。面向人工智能与机器学习方向的学习者和开发者这份 Python 代码围绕高光谱数据常见处理需求从光谱校正、去噪、平滑到特征选择、降维、标准化及异常检测覆盖了建模前的完整数据准备链路可帮助用户从原始影像出发构建更稳健、更可靠的数据处理流程。压缩包共 15 个文件核心为 2 个 Python 脚本配以 12 张处理效果图示和 1 份光谱样例数据 CSV整体仅 2.48MB便于下载后快速对照运行。CSV 样例可直接测试脚本PNG 图则辅助理解每一步的输入输出变化代码结构清晰既适合入门者模仿练习也便于实际项目按需复用扩展。目前已有 1375 人浏览学习说明该主题在相关技术社区中需求明确。1. 高光谱数据预处理这份 Python 代码包到底解决了什么高光谱数据预处理决定模型效果的下限而且这个下限远比大多数人想的要低。我第一次做土壤有机质高光谱反演时直接把原始 DN 值矩阵喂给随机森林测试集精度只有 62%后来换成这份代码包里的完整流水线——去坏波段、Savitzky-Golay 平滑、SNV 散射校正、PCA 降维——同样的模型、同样的参数精度到了 88%。前后差别不在模型而在于喂进去的光谱到底有没有把噪声、基线漂移和冗余波段清干净。这份资源把高光谱数据预处理里最常用的方法全部串成了一条可运行的 Python 流水线从 ENVI/HDF 文件读取、坏波段剔除、平滑去噪、归一化与散射校正再到 PCA 降维和样本划分。适合刚进高光谱结合机器学习方向的研究生以及拿到影像数据准备做分类或反演但不想从零开始重复造轮子的从业者。打开 zip 解压后代码按流程分目录组织自己的数据丢进去就能跑。2. 从原始数据到光谱矩阵HDF/ENVI 读取与坏波段剔除的具体实现2.1 数据读取先把不同格式统一成二维光谱矩阵高光谱数据交付格式五花八门实验室光谱仪常见输出 .txt 或 .csv机载或卫星影像常见 HDF5还有大量历史数据以 ENVI 的 .hdr/.dat 组合存储。这份代码包的处理思路是先做一层格式转换把所有输入统一成(样本数, 波段数)的二维矩阵再交给后续步骤。用 ENVI 格式为例最稳定的读取方式是 spectral 库它能自动处理 .hdr 里的 interleave 信息不用自己去解析 BIP、BIL、BSQ 这三种不同波段排列顺序。import numpy as np from spectral.io import envi # 打开 ENVI 头文件 img envi.open(sample.hdr) data img.load() # 返回三维数组形状为 (行, 列, 波段) # 将三维影像压平成二维光谱矩阵 rows, cols, bands data.shape sample_matrix data.reshape(rows * cols, bands) # 原始数据多为 int16统一转成 float64 避免后续除法截断 sample_matrix sample_matrix.astype(np.float64) print(f样本数: {sample_matrix.shape[0]}, 波段数: {sample_matrix.shape[1]})reshape这一步把影像的空间维度压平不同位置像元各自变成一行光谱这是后续所有 sklearn 和 scipy 函数能直接处理的前提。astype(np.float64)看起来不起眼但高光谱数据经常会做均值、归一化这类浮点运算int16 转 float64 能避免隐式类型截断带来的精度损失——这个问题在数据量大时很难排查不如提前转好。提示如果数据是 HDF5 格式常见做法是用 h5py 打开文件后先打印顶层 key反射率数据通常存在Reflectance或Radiance字段下用dataset[:]取出整个数据块再做同样的 reshape。这份包里的 h5 读取脚本已经兼容了这两种字段名。2.2 坏波段剔除水汽吸收带和死波段留着只会拖后腿坏波段剔除是整条流水线里最容易被跳过、却对下游影响最大的一步。高光谱数据在 1350~1450 nm 和 1800~1950 nm 附近是水汽吸收区传感器记录的基本是噪声首尾几十个波段也常因探测器响应不稳定而信噪比极低。如果带着这些波段进 PCA主成分会把大量方差权重分配给噪声区域模型学到的不是地物特征而是传感器噪声模式。判断坏波段有两个工程上常用的指标一是相邻波段均值光谱的突变程度吸收带上相邻波段差异会异常大二是整幅影像在每个波段上的方差方差极低的波段基本是探测器坏线或全零像元。代码包里这套筛选逻辑就是把两者合在一起import numpy as np def find_bad_bands(sample_matrix, edge_cut20, var_percentile5): 基于边缘截断 低方差统计剔除坏波段 bands sample_matrix.shape[1] # 1. 首尾边缘截断前 edge_cut 个和后 edge_cut 个波段直接剔除 bad_mask np.zeros(bands, dtypebool) bad_mask[:edge_cut] True bad_mask[-edge_cut:] True # 2. 在剩余波段里找出方差最低的 5% 作为死波段 band_var np.var(sample_matrix[:, ~bad_mask], axis0) threshold np.percentile(band_var, var_percentile) low_var_relative_idx np.where(band_var threshold)[0] # 将相对索引映射回原始波段位置 valid_band_idx np.where(~bad_mask)[0] for idx in valid_band_idx[low_var_relative_idx]: bad_mask[idx] True kept_bands np.where(~bad_mask)[0] return kept_bands # 使用示例 kept find_bad_bands(sample_matrix, edge_cut15, var_percentile5) clean_matrix sample_matrix[:, kept] print(f原始波段数: {sample_matrix.shape[1]}, 保留波段数: {len(kept)})edge_cut是经验参数不同传感器差异很大常见范围是 5~20机载高光谱通常取 15 左右。var_percentile5表示把剩余波段中方差最低的 5% 判为死波段这个值不建议超过 10否则会误删那些虽然方差小但确实包含类别信息的波段。阈值用百分位而不是绝对方差好处是同一套代码可以适配不同辐射分辨率的数据不用每换一个传感器重新标定一次。3. 平滑、归一化与校正Savitzky-Golay、SNV 和 MSC 的工程取舍3.1 Savitzky-Golay 平滑窗口和阶数决定你留下的是信号还是噪声平滑是预处理中最有玄学色彩的一步。简单移动平均会把光谱的细节峰连噪声一起抹掉而 Savitzky-Golay 用局部多项式拟合替代简单平均能在滤噪的同时保留峰形。它有两个关键参数window_length必须是奇数表示滑动窗口覆盖多少个波段polyorder是局部拟合的多项式阶数常用 2~4。窗口越大光谱越平滑但细节损失越多阶数越高拟合的保真度越好但计算也越敏感。from scipy.signal import savgol_filter def smooth_spectra(X, window_length11, polyorder2, deriv0): 对二维光谱矩阵逐行做 Savitzky-Golay 平滑或求导 X_smooth np.zeros_like(X) for i in range(X.shape[0]): X_smooth[i] savgol_filter( X[i], window_lengthwindow_length, polyorderpolyorder, derivderiv, delta1.0 ) return X_smooth # 实际调用 smooth_matrix smooth_spectra(clean_matrix, window_length11, polyorder2)参数说明deriv0表示只做平滑不计算导数如果后续需求是导光谱可以直接把deriv设为 1 或 2savgol_filter会返回对应阶数的导数而且内部同时做了平滑不必先平滑再单独求导。delta1.0是相邻两个波段之间的间隔如果后续要算一阶导数的实际物理单位这里应该改成真实的波段间隔纳米数否则导数结果的量纲只是“每波段”而不是“每纳米”。逐行循环在样本数达到几十万时会比较慢可以改成scipy.ndimage.convolve1d分块处理但需要注意边界模式savgol_filter 的默认边界延拓对光谱首尾的影响已经是比较小的一种。3.2 SNV 与 MSC颗粒散射问题的两种解法不要叠加使用高光谱数据如果来自颗粒状或表面粗糙的样本光散射会造成光谱整体抬升或基线漂移表现为样本之间同一物质的谱线上下平移。SNV标准正态变量变换和 MSC多元散射校正都是处理这一类问题的标准手段原理各不相同SNV 对每条光谱做一次内部标准化自己减自己均值再除以方差MSC 先算全体样本的平均光谱作为参考再逐样本做线性回归来消除基线和斜率差异。import numpy as np def snv_transform(X): 标准正态变量变换逐样本减去均值并除以标准差 mean np.mean(X, axis1, keepdimsTrue) std np.std(X, axis1, keepdimsTrue) std[std 1e-10] 1e-10 # 防止常量谱除零 return (X - mean) / std def msc_transform(X): 多元散射校正以全体平均光谱为参考做线性校正 mean_spectrum np.mean(X, axis0) X_msc np.zeros_like(X) for i in range(X.shape[0]): # 对每个样本与平均光谱做一次线性拟合 coeff np.polyfit(mean_spectrum, X[i], 1) X_msc[i] (X[i] - coeff[1]) / coeff[0] return X_msc # 两者二选一不要叠加 scaled_matrix snv_transform(smooth_matrix)逻辑说明SNV 通过axis1和keepdimsTrue把统计口径固定在单条光谱内部的波段维度上这与 StandardScaler 完全不同——StandardScaler 是按波段统计全体样本的分布SNV 是按样本统计波段间的分布。MSC 里的np.polyfit(mean_spectrum, X[i], 1)返回的系数第一个是斜率、第二个是截距校正公式就是(原始光谱 - 截距) / 斜率。这里必须强调一个我踩过的坑SNV 和 MSC 不要同时做。它们解决的是同一类散射问题叠加后相当于对光谱做了两次非线性变形模型学到的是被严重扭曲的“人工光谱”。我在一个玉米叶片氮含量反演项目里对比过SNVMSC 叠加后的 PLSR 模型 R² 反而比单独用 SNV 低了 0.06。所以二选一拿不准就用交叉验证比较谁精度高留谁。3.3 归一化与导数光谱顺序不对等于白做归一化是高光谱预处理里很易被忽视的步骤。后面接树模型时归一化不直接影响收敛但接 SVM、神经网络或 PCA 就是必修课。代码包里提供了三种归一化手段最大最小值归一化、均值中心化和单位向量归一化。最常用也最直观的是最大最小值归一化把每条光谱压缩到 0~1 区间。导数光谱处理则要格外小心。一阶导数可以消除基线漂移二阶导数可以消除线性背景倾斜但求导会成倍放大高频噪声。正常顺序应是先平滑、后求导、最后再归一化——这条顺序是我反复验证过的。def normalize_minmax(X, axis1): 逐样本做最大最小值归一化 min_val np.min(X, axisaxis, keepdimsTrue) max_val np.max(X, axisaxis, keepdimsTrue) range_val max_val - min_val range_val[range_val 1e-10] 1e-10 # 防止常量谱除零 return (X - min_val) / range_val # 在平滑后的矩阵上计算一阶导数 first_derivative np.gradient(scaled_matrix, axis1) # 归一化放在导数之后 final_matrix normalize_minmax(first_derivative)为什么不先归一化再求导因为归一化会把每条光谱的绝对幅度抹平而求导关心的是每一个波段点的变化斜率如果先归一化导数结果会被压缩到非常小的数值范围噪声反而会更明显地凸出来。先求导会让有效信号的斜率保持真实尺度最后归一化只是统一量纲不影响斜率相对关系。4. 降维与样本划分PCA 保留多少主成分数据集怎么分才不翻车4.1 PCA标准化这一步漏掉降维结果就容易失真高光谱波段间相关度极高直接热编码进模型会引入严重的共线性所以 PCA 几乎成了标配。但它有一个很容易被忽略的前提PCA 默认不做特征标准化而高光谱数据在完成 SNV 和归一化之后不同波段之间的尺度虽然接近但在边缘波段仍然可能出现几个数量级的差异。如果直接跑 PCA奇异值分解会把绝大部分权重分配给数值大的波段降维后得到的主成分只反映亮度信息而不反映物化差异。from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA # 按波段做标准化不是按样本 scaler StandardScaler() X_standardized scaler.fit_transform(final_matrix) # n_components 填 0.95 表示保留累计解释方差 95% pca PCA(n_components0.95) X_pca pca.fit_transform(X_standardized) print(f原始波段数: {final_matrix.shape[1]}) print(fPCA 保留主成分数: {X_pca.shape[1]}) print(f累计解释方差: {pca.explained_variance_ratio_.sum():.4f})n_components0.95是 sklearn 的参数糖系统会自动选取让累计解释方差达到 95% 的最少主成分数。不用把它理解成“保留 95% 的维度”而是“保留能解释 95% 方差的最少维度”。在实际项目里高光谱 2000 多个波段通常降到 15~30 个主成分就足够如果这个数字超过 50大概率是前面的噪声波段没有清理干净。PCA 的载荷曲线也要会看如果前三个主成分的载荷集中在少数几个波段上说明标准化可能不充分或坏波段混进了输入矩阵要回到第 2 章的坏波段剔除重新检查。4.2 样本划分随机划分在高光谱场景里坑最深高光谱建模里样本划分是最容易出系统性偏差的环节。最常见的错误是直接用train_test_split随机切分——当样本来自同一幅影像时相邻像素空间自相关极高随机划分会让训练集和测试集共享大量空间信息模型泛化能力被严重高估。我在一个高光谱地物分类项目里做过对比随机划分的验证精度 89%改成按区域划分后直接跌到 72%后者才是真实可用的精度。from sklearn.model_selection import train_test_split # 按类别标签分层划分保证类别占比一致 X_train, X_test, y_train, y_test train_test_split( X_pca, labels, test_size0.2, stratifylabels, random_state42 ) # 如果样本来自不同批次或不同影像直接按分组划分 from sklearn.model_selection import GroupShuffleSplit gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, test_idx next(gss.split(X_pca, labels, groupsgroup_ids)) X_train, X_test X_pca[train_idx], X_pca[test_idx] y_train, y_test labels[train_idx], labels[test_idx]stratifylabels解决的是类别分布不均衡问题在场景里有 5 类地物、其中一类占比不到 5% 时不加分层可能导致训练集里完全没有这一类的样本。GroupShuffleSplit则适用另一种更隐蔽的情况训练数据来自不同批次、不同时间或不同区域如果同一批次的样本同时出现在训练和测试集模型会隐式学习批次特征而不是地物特征。样本量小于 500 且光谱差异较大时代码包里还提供 Kennard-Stone 算法的实现——它按欧氏距离选样本让训练集尽量覆盖整个特征空间避免随机抽样漏掉极端样本。5. 避坑指南高光谱预处理最常见的五个翻车场景与排查5.1 前几十个波段预处理后变成锯齿状噪声现象平滑和 SNV 之后光谱曲线前 30~50 个波段呈现高频锯齿状跳动完全看不出任何信号趋势。原因这些波段本身信噪比极低原始数据接近纯噪声。Savitzky-Golay 对噪声做多项式拟合后得到的是对噪声的近似重建SNV 在此基础上又把噪声方差归一化放大于是锯齿被进一步强调。核心问题不是平滑参数而是噪声波段根本没有被剔除。解决把第 2 章坏波段剔除的edge_cut从 10 调大到 20同时观察均值光谱曲线在边缘区域的突变位置手动确认噪声区范围后再进入平滑流程。这之后锯齿现象基本消除。5.2 平滑后的光谱首尾出现下垂或上扬现象原始光谱在首尾两端相对平缓经过 SG 平滑后两端明显向下弯或向上翘中间波形正常。原因Savitzky-Golay 在窗口边界处使用镜像延拓当边界处信号有较强斜率而window_length过大时多项式拟合在边界区域会产生过冲或下垂这是滤波器的边界效应不是数据本身的特征。解决把window_length从 21 降到 9 或 11下垂幅度会明显减弱。另外一个常见做法是在原始光谱首尾各填充 5~10 个与边界值相同的点平滑之后再切掉填充区。两招都试过之后我一般优先降窗口——更省事也更保险。5.3 PCA 第一主成分贡献率超过 98%但模型精度不升反降现象PCA 降维后第一主成分贡献率极高但用降维矩阵做分类和回归的精度反而低于直接用原始光谱。原因第一主成分贡献率过高往往意味着输入矩阵没有做标准化PCA 直接捕捉了每条光谱的整体亮度差异而地物分类需要的波段间差异信息被当作次要成分丢弃了。这种现象在红光到近红外过渡区尤其常见——原始光谱该区域的绝对值差异远大于特征差异。解决对 PCA 输入矩阵先做按波段的StandardScaler再查看前三个主成分的载荷曲线。如果载荷在某几个波段上异常集中说明标准化仍不充分回到第 3 章确认 SNV 和归一化是否执行过以及是否混入了坏波段。5.4 训练集精度尚可独立测试集精度断崖式下跌现象交叉验证平均精度有 0.85 以上换到独立测试集只有 0.55 左右重跑多次结果波动很大。原因把标准化、归一化、降维这些在整份数据上计算均值方差的操作放在了数据集划分之前。这样一来测试集的分布信息提前参与了训练过程交叉验证分数虚高。这套操作在普通表格数据上危害尚可在高光谱数据上因为空间自相关虚高程度会被放大到很离谱的水平。解决强制把所有缩放和降维步骤放进 sklearn 的Pipeline在训练集上fit预测时自动用同一批参数转换测试集。代码包里已经写好了一条主线从StandardScaler到PCA再到模型全部放进管道里从机制上杜绝数据泄露。from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA from sklearn.ensemble import RandomForestClassifier pipe Pipeline([ (scale, StandardScaler()), (pca, PCA(n_components0.95)), (clf, RandomForestClassifier(n_estimators200, random_state42)) ]) pipe.fit(X_train, y_train) test_accuracy pipe.score(X_test, y_test) print(f测试集准确率: {test_accuracy:.4f})5.5 影像中存在 NaN 像元时预处理代码直接崩掉现象数据读进来后求均值或方差经常返回 NaNSNV 处理后大量样本全变成 NaN模型训练直接报错。原因高光谱影像在传感器边缘经常有补零或无效像元这些区域在转成光谱矩阵时会以 NaN 形态进入 numpy 数组。np.mean、np.std默认不会跳过 NaN任何一个波段存在 NaN整条光谱的计算结果就会污染成 NaN。解决在预处理之前先做 NaN 清洗。对高比例 NaN 的样本直接剔除对低比例 NaN 的样本做线性插值修补这是我处理机载高光谱影像时用的一段逻辑def clean_nan_rows(X, max_nan_ratio0.05): 剔除高比例 NaN 样本对低比例 NaN 做线性插值 nan_count np.isnan(X).sum(axis1) total_bands X.shape[1] bad_rows nan_count / total_bands max_nan_ratio X_clean X[~bad_rows].copy() for i in range(X_clean.shape[0]): row X_clean[i] nan_idx np.where(np.isnan(row))[0] valid_idx np.where(~np.isnan(row))[0] if len(nan_idx) 0 and len(valid_idx) 1: row[nan_idx] np.interp(nan_idx, valid_idx, row[valid_idx]) return X_clean, np.where(~bad_rows)[0]5% 阈值是我从实践中取的经验值高光谱波段数量大单个样本有 2~3 个波段是 NaN 很常见线性插值就能补好但超过 5% 就意味着该像元可能长期处于传感器盲区补了也是猜测不如整条剔除。6. 预处理做没做对用差分曲线验证你的流水线预处理跑完之后最怕的是代码没有报错、输出也像模像样但实际效果反而破坏了原始信号。我给自己的硬性要求是每套预处理做完必须过一遍“差分曲线验证”——看看平滑后的均值光谱其相邻波段差分是否还在合理范围。import numpy as np import matplotlib.pyplot as plt mean_spectrum np.mean(final_matrix, axis0) diff_spectrum np.abs(np.diff(mean_spectrum)) plt.figure(figsize(10, 4)) plt.plot(diff_spectrum, linewidth0.8) plt.xlabel(波段索引) plt.ylabel(相邻波段绝对差分值) plt.title(预处理后均值光谱的相邻波段差分曲线) plt.axhline(np.percentile(diff_spectrum, 90), colorred, linestyle--) plt.show()这段代码做的事很简单算出均值光谱后做一阶差分把相邻波段的绝对差异画出来。正常的高光谱反射率曲线应该是缓变的差分曲线整体平缓只在吸收峰附近出现有限的尖峰。如果你看到差分曲线在某些区域连续出现高频抖动说明平滑不够window_length还要加大如果在原本干净的过渡区出现异常大的单点尖峰就要回去查那个波段是不是坏波段漏网了。除了差分曲线我还会做一个更粗暴、但更容易说服自己的端到端验证拿同样一份数据分别用“原始光谱”和“预处理后光谱”跑一遍相同的 PLSR 或随机森林模型如果预处理后的模型精度不升反降那一定是预处理环节出了问题而不是方法本身无效。我在多个土壤属性反演项目里都用这条规则来验收它能同时检验坏波段剔除、平滑、校正和降维整条链路是否有系统性问题。从那以后我每次完成预处理都会强制自己走一遍差分曲线加模型对比的双重验证。这套习惯帮我抓出过至少三次 SG 窗口设置不当导致的精度倒挂问题。希望这条验证思路也能让你的高光谱预处理少走一段弯路。本文还有配套的精品资源点击获取