简介:面向高光谱数据分析与毕业设计、课程设计场景,这份Python项目整合了MSC、SNV、SG平滑、滑动平均、一阶/二阶差分、小波变换、均值中心化、标准化、最大最小归一化、矢量归一化等十余种常用预处理算法,并配有源码、说明文档与逐段代码解析,适合需要系统实现或对比高光谱预处理方法的开发者参考。压缩包共17个文件,包含Python脚本、示例光谱数据(peach_spectra_brix.csv)以及多张运行效果示意图,整体仅2.48MB,轻量且便于直接运行学习。目前已有485人学习下载,代码经过测试可放心延展使用;readme与license文件也便于快速上手和合规引用。SG平滑、小波变换等算法均有注释,可配合示例数据复现结果,也能直接替换自己的高光谱数据进行预处理流程设计与效果评估,能够帮助读者省去从零搭建和调试的额外成本。
1. 高光谱数据预处理:先想清楚再动手
高光谱数据预处理这件事,懂的人都知道它比模型选择更影响结果。同一份桃子光谱数据,直接拿去建模和先做散射校正再建模,预测糖度的R²能差出0.1以上,这在毕业设计里足够决定成绩档次。这份资源是一个基于Python的高光谱数据预处理项目,包含pretreatment.py核心代码、demo.py调用示例、readme文档、算法代码解析,以及一份真实的桃子光谱-糖度数据集peach_spectra_brix.csv。它实现了SNV、MSC、SG平滑、滑动平均、一阶/二阶差分、小波变换、均值中心化、标准化、最大最小归一化、矢量归一化共11种方法,覆盖了光谱建模前绝大多数预处理需求。适合做光谱类课题需要对比多种预处理算法、或者想把数据处理流程规范化的从业者参考。
2. 预处理方法选型:11种算法分别在解决什么问题
拿到一份高光谱数据,最常见的困惑不是不会写代码,而是不知道用哪种方法。11种算法看着多,其实按功能可以分四类:散射校正、平滑去噪、差分求导、归一化。把每类解决什么问题搞清楚,选型就自然出来了。
2.1 散射校正:MSC与SNV解决的是颗粒度与光程问题
高光谱数据里,最让人头疼的不是随机噪声,而是散射效应。拿桃子来说,果皮表面的蜡质层、果肉颗粒粗细、光照角度不一致,都会让进入光谱仪的光路发生改变。这种改变反映在光谱上是基线平移和倾斜,跟化学成分没有直接关系。如果不管它,后面模型学到的大部分是物理干扰。
多元散射校正MSC的核心思路是把全体样本的平均光谱当作参考谱,然后对每条光谱和参考谱做一元线性回归,得到斜率和截距,再用这两个系数把原始光谱校正回来。相当于给每条光谱的散射基准重新拉到同一条线上。MSC效果依赖参考谱质量,样本太少或者样本差异过大时,平均谱本身不稳定,校正效果会打折扣。
标准正态变换SNV的思路更直接:每条光谱独立操作,减去自身均值再除以自身标准差。它不依赖其他样本,单条光谱也能处理,工程里用起来更省心。在很多数据集上SNV和MSC结果非常接近,选哪个更多取决于习惯和后续模型的稳定性。两类方法都假设散射干扰以乘性和加性为主,如果你的数据里散射形态更复杂,光靠这两个还不够,可能要结合后续的差分处理。
2.2 平滑去噪:SG滤波与滑动平均的轻量级处理
光谱仪采集的信号不可避免带上高频噪声,表现为曲线上的毛刺。这类噪声不处理,后面做差分会把噪声放大到没法看。工程里用得最多的两种平滑是滑动平均(move_avg)和Savitzky-Golay卷积平滑(SG)。
滑动平均原理朴素:对每个波长点取左右N个点的平均值作为输出。N是窗口大小,窗口越大曲线越光滑,但峰形也越容易被拉平。它适合噪声大、不关心峰形细节的场景。实现用scipy的uniform_filter1d就能做,速度很快。
SG滤波聪明在保留了峰形。它不是简单取平均,而是在滑动窗口内对数据做最小二乘多项式拟合,用拟合值替代原始值。拟合阶数polyorder通常取2或3,窗口window_length必须是奇数且大于polyorder。对高光谱数据,窗口7到15是常见区间。窗口设太大比如超过31,真实吸收峰会被磨成扁平包;窗口设太小比如3,去噪效果约等于没有。我一般先看原始光谱的峰宽再定窗口,峰宽约20个波长点时窗口取9到13比较稳。
2.3 差分与导数:放大光谱细节也要放大噪声
一阶差分D1和二阶差分D2解决基线漂移和重叠峰问题。一阶差分相当于对光谱求斜率,去掉常数基线;二阶差分进一步去掉线性基线。近红外光谱里基线漂移很常见,差分后光谱形态更尖锐,重叠峰边界更清晰。
代价是信噪比下降。差分本质是高通滤波,放大的高频分量里既有有用细节也有大量噪声。正确姿势是先做SG平滑再做差分,先滤噪再求导,出来的曲线才能用。另一种思路是用小波变换wave替代差分去提取光谱局部特征,小波能按尺度分开信号和噪声,抗干扰能力更强,但对level参数敏感,需要多试几次。
2.4 归一化家族:均值中心化、标准化、最大最小归一化与矢量归一化
这一组解决量纲和分布问题。高光谱数据里不同波长点的反射率绝对值可能差几个数量级,模型会把注意力集中在量级大的变量上,而不是真正有区分度的变量上。这套归一化逻辑不只光谱建模在用,做python量化交易策略代码时处理行情数据也会用标准化和最大最小归一化,思路完全一致。
均值中心化对每个波长点减去该波长上的均值,让数据围绕零波动。不压缩方差,只是平移,适合后续接主成分分析或偏最小二乘。标准化在中心化基础上再除以标准差,每个波长变量方差变成1,量纲差异被抹平。最大最小归一化把每个波长点线性映射到0到1,保留原始分布形状,对深度学习友好。矢量归一化逐样本处理,每条光谱除以自身模长,让所有样本谱线模长统一为1,主要对付样本间整体光强差异。
选型逻辑不复杂:后续用偏最小二乘、主成分回归这类线性方法,均值中心化和标准化是首选;喂给神经网络,最大最小归一化更稳;样本间整体光强差异大,矢量归一化优先。这些方法能组合,比如先MSC去散射再做均值中心化供PLS使用,这是光谱建模里非常常规的搭配。
当然也有不需要预处理的时候。如果原始光谱信噪比很高、基线稳定,或者你用的是树模型、梯度提升这类对单调变换不敏感的模型,归一化带来的收益很小。我见过不少新手把预处理当成必经流程,也不管模型是什么,全跑一遍归一化,结果跟原始数据没差别,白费功夫还增加了代码复杂度。
3. 把pretreatment.py拆开:核心函数与参数边界
这一章直接看代码。理解了原理再看实现,参数调起来才有方向感,不然就是瞎试数字。
3.1 工程结构与数据形态
压缩包解压后目录结构很清晰:
hyperspectral_pretreatment-main/ ├── pretreatment.py # 11种预处理算法实现 ├── demo.py # 调用示例与对比图绘制 ├── data/ │ └── peach_spectra_brix.csv ├── assets/ # 示例输出图片 ├── readme.md # 使用说明与算法列表 └── LICENSEpretreatment.py放全部算法函数,demo.py负责演示调用,数据集在data目录。assets里的图片可以先翻一翻,直观看看每种处理做完曲线长什么样。
读数据这一步容易被忽视,方向搞反了后面全错。我一般会先打印shape确认行列含义。
import pandas as pd df = pd.read_csv("data/peach_spectra_brix.csv") print("原始数据形状:", df.shape) print("前3行预览:") print(df.head(3)) X = df.iloc[:, :-1].values # 波长反射率矩阵 y = df.iloc[:, -1].values # Brix糖度标签 print("光谱矩阵:", X.shape) print("糖度标签:", y.shape)代码作用是把CSV按样本矩阵读进来,波长在列方向,最后一列是标签。X的shape通常是(样本数, 波长点数),比如(124, 1300)。如果打印出来行数比列数多很多,先别急着往下走,确认CSV是不是行列反了。光谱数据最常见的翻车就是每一行代表波长而非样本,后面整批处理全错。
如果你是python入门阶段,建议先把依赖库装齐。多数人用pip一把梭就行,装最省事的命令是:
pip install numpy pandas scipy matplotlib scikit-learn装完在终端里进python解释器逐个import一遍,确认不报错再往下跑。我之前在linux系统安装python后踩过缺scipy的坑,后来习惯先一个pip命令装齐再开项目,这个习惯省了不少时间。windows下如果提示pip不是内部命令,先把python安装目录加入PATH,或者直接重装python时勾选Add to PATH。
3.2 核心函数逐个过:实现、输入与输出
pretreatment.py的核心函数设计统一:输入二维数组X,输出处理后的二维数组。先看SNV和MSC。
import numpy as np def snv(X): # 标准正态变换:逐样本去均值、除标准差 # X: (n_samples, n_wavelengths) means = X.mean(axis=1, keepdims=True) stds = X.std(axis=1, keepdims=True) return (X - means) / stds def msc(X): # 多元散射校正:以全体样本平均谱为参考,逐样本线性回归后校正 mean_spectrum = X.mean(axis=0) X_corrected = np.zeros_like(X) for i in range(X.shape[0]): # polyfit返回一次多项式系数: [斜率, 截距] slope, intercept = np.polyfit(mean_spectrum, X[i], 1) X_corrected[i] = (X[i] - intercept) / slope return X_corrected两个关键参数:axis=1表示按行处理,也就是对每个样本自己做统计,这是SNV和MSC的方向基础。keepdims=True保留二维形状,否则means会变成一维数组,减法广播会出问题。MSC里polyfit做一元回归,斜率是乘性干扰,截距是加性干扰,两者都去掉。
SG平滑和差分是另一对组合,涉及scipy和np.diff。
from scipy.signal import savgol_filter def sg_smooth(X, window_length=11, polyorder=2): # Savitzky-Golay平滑:窗口内做多项式拟合,保留峰形 # window_length必须是奇数且大于polyorder return np.apply_along_axis( lambda row: savgol_filter(row, window_length, polyorder), axis=1, arr=X ) def d1(X): # 一阶差分:沿波长方向求相邻点差值 return np.diff(X, n=1, axis=1) def d2(X): # 二阶差分:在d1基础上再做一次差分 return np.diff(X, n=2, axis=1)savgol_filter的窗口参数直接决定平滑强度。window_length=11在中等分辨率光谱里是比较稳的起点,峰形几乎不受损。polyorder=2是抛物线拟合,适合大多数光谱;取1等价局部线性拟合,更平滑但细节损失多一点;取3以上对窄峰保留更好,但对噪声也更敏感。np.diff的axis=1是波长方向,输出列数比输入少:d1少1列,d2少2列,这个边界后面专门说。
3.3 demo.py怎么跑:一条命令出对比图
demo.py的作用是把全部算法跑一遍,画出原始光谱和处理后的对比曲线图。流程大概是读取CSV、遍历预处理函数、matplotlib绘图、保存到assets目录。运行时直接:
python demo.py跑完去assets目录看输出的PNG图片。如果终端报错缺matplotlib,按前面说的pip命令补装。想单独测试某个函数,可以进python交互环境导入:
from pretreatment import snv, sg_smooth, d1 import pandas as pd df = pd.read_csv("data/peach_spectra_brix.csv") X = df.iloc[:, :-1].values X_snv = snv(X) X_sg = sg_smooth(X, window_length=11, polyorder=2) X_d1 = d1(X_sg) # 先平滑再差分 print("SNV后形状:", X_snv.shape) print("SG平滑后形状:", X_sg.shape) print("差分后形状:", X_d1.shape)逐函数调试时重点盯三件事:输出形状是否和预期一致、数值范围是否合理、画出的曲线有没有出现突变点。比如sg_smooth输出形状必须和输入一致,如果列数变了,多半是函数应用在了错误方向上。参数调试我一般写在单独脚本里,一个方法一个方法跑,把结果记录下来再对比。
4. 复现实验:从桃子光谱到糖度预测
原理和代码都过完了,接下来做一件正经事:在桃子的真实数据集上跑一遍,看预处理到底带来多少提升。这样你粘贴别人的代码时心里有底,答辩被问到也能答得出来。
4.1 数据集背景与回归任务设定
peach_spectra_brix.csv每一行是一个桃子样本,各列是不同波长处的光谱响应值,最后一列Brix是糖度。Brix也就是白利度,代表可溶性固形物含量,是水果甜度和品质的核心指标。回归目标很明确:用光谱预测糖度。
选基线和评估指标也固定,避免不同预处理之间没法比。回归用PLSR偏最小二乘回归,这是光谱建模的默认基线,它对波长共线性容忍度高,比普通多元回归稳得多。R²代表预测值能解释真实值方差的比例,越接近1越好;RMSE代表平均预测误差,单位是Brix度,越小越好。
跑数据之前先检查异常样本。有些光谱反射率可能异常高或者出现负值,这种样本要么是采集问题要么是坏点,直接剔除比留在数据集里干扰均值好得多。
import numpy as np X = df.iloc[:, :-1].values y = df.iloc[:, -1].values # 检查异常值与缺失值 print("光谱范围: {:.3f} ~ {:.3f}".format(X.min(), X.max())) print("缺失值数量:", np.isnan(X).sum()) # 剔除反射率明显异常的样本 mask = np.all((X > 0) & (X < 2.5), axis=1) X_clean, y_clean = X[mask], y[mask] print("剔除后样本数:", X_clean.shape[0])光谱反射率正常情况下在0到1之间,吸光度模式下可能在0到3之间。如果最大值超过5或者出现负数,优先检查数据采集过程,而不是强行继续跑模型。
4.2 对比脚本:全部方法在同一个协议下跑
为了让预处理方法可比,我把11种方法放进同一个协议:每种方法处理后的数据接同一个PLSR模型,用5折交叉验证打分。这样方法之间的差异就是预处理本身的差异。
from sklearn.cross_decomposition import PLSRegression from sklearn.model_selection import cross_val_score from pretreatment import ( snv, msc, sg_smooth, move_avg, d1, d2, wave, mean_centralization, standardlize, max_min_normalization, vector_normalization ) preprocessors = { "raw": lambda X: X, "snv": snv, "msc": msc, "sg": lambda X: sg_smooth(X, 11, 2), "move_avg": lambda X: move_avg(X, 5), "d1": lambda X: d1(sg_smooth(X, 9, 2)), "d2": lambda X: d2(sg_smooth(X, 9, 2)), "wave": lambda X: wave(X, level=3), "mean_centering": mean_centralization, "standardize": standardlize, "max_min": max_min_normalization, "vector_norm": vector_normalization, } for name, func in preprocessors.items(): Xp = func(X_clean) if Xp.shape[1] != X_clean.shape[1]: # 差分会让波长点数减少,实际情况按样本对齐 y_aligned = y_clean else: y_aligned = y_clean pls = PLSRegression(n_components=8) scores = cross_val_score(pls, Xp, y_aligned, cv=5, scoring="r2") print(f"{name:14s} R²: {scores.mean():.4f} ± {scores.std():.4f}")cross_val_score的cv=5意味着5折交叉验证,scoring="r2"直接用决定系数做评分。PLSRegression内部默认做均值中心化,所以raw也能跑,只是效果通常不如预处理后。跑完这段代码就能看到各方法的平均R²和标准差,哪个方法在这个数据集上靠谱一目了然。
n_components取8是临时值。主成分数选择最稳的方式是网格搜索或留一交叉验证,常见的范围是5到20之间。我习惯先跑一版n_components从3到20的循环,挑交叉验证R²最高的那个数定下来。
4.3 组合预处理的顺序策略
单种方法的效果只是第一步,真正能拉开差距的是组合。常规顺序是:先SG平滑或小波去噪,再做MSC或SNV散射校正,最后均值中心化或标准化。一句话记法是先光滑、再散射、后归一。
def compose_pipeline(X): # 组合预处理:平滑 -> 散射校正 -> 中心化 X = sg_smooth(X, window_length=11, polyorder=2) X = snv(X) X = mean_centralization(X) return X为什么是这个顺序:先平滑能让后续散射校正的参考谱更干净,避免噪声干扰回归系数;最后做中心化是因为PLS内部本来就要中心化,提前做可以统一输入口径。反过来先归一化再平滑,窗口内的数值被压缩过,多项式拟合的效果会变差。组合预处理的对比实验和单方法对比一样,直接替换preprocessors里的函数即可。
5. 避坑记录:高光谱预处理翻车现场与排查路径
预处理代码本身不长,但坑全在细节里。下面这五条是我在实际跑数据和带毕设过程中反复见到的,每一条都按现象、原因、解决展开。
5.1 矩阵方向搞反:样本行当成了波长行
现象:做SNV之后曲线形态完全不对,出来像噪声;做MSC时直接报广播维度错误。打印shape一看,X是(1300, 124)而不是(124, 1300)。
原因:CSV里有的数据格式是列代表样本、行代表波长。读进来直接用axis=1处理,等于把每个波长当成一个样本去算均值和标准差,方向全拧了。
解决:读数据后先打印shape,再画一条原始光谱确认。确认标准是每一行是一条连续光滑的光谱曲线,列数等于波长点数。如果发现行才是波长,转置回来:
X = df.iloc[:, :-1].values.T # 让样本变成行 y = df.iloc[:, -1].values5.2 SG平滑窗口设太大,把特征峰磨平了
现象:SG平滑后R²反而比原始数据低,画出曲线发现原本尖锐的吸收峰变成扁平包,峰位发生偏移。
原因:window_length取值过大,比如取31甚至51,窗口内多项式拟合把局部峰形当作噪声抹掉了。polyorder超过5也会有类似问题,模型过拟合窗口内的噪声结构。
解决:根据光谱峰宽选窗口,通常7到15。先画一条原始光谱目测最窄峰的宽度,窗口取峰宽的一半到三分之二比较稳。polyorder控制在3以内。改完参数后对比同一波长段处理前后的曲线,确认峰位没偏移再往下走。
5.3 代码里的MSC和SNV,名字和文档对不上
现象:按readme写的标准正态变换MSC去找函数,发现pretreatment.py里的msc实现的是线性回归校正,跟文档描述完全相反。新手照着文档理解代码,越看越糊涂。
原因:光谱预处理领域MSC和SNV的缩写在不同资料里确实存在混用。MSC通常指多元散射校正Multiplicative Scatter Correction,SNV指标准正态变换Standard Normal Variate,但不少项目的readme会把两个名字写反。代码本身没问题,文档与实现错位。
解决:不要凭算法名叫板,直接看函数实现。实现里用np.polyfit做线性回归的是MSC,直接减均值除标准差的是SNV。引用这个项目时尽量按代码实现来描述,必要时在论文或文档里注明所采用的数学定义。
5.4 差分后点数变少,标签长度对不上
现象:跑d1预处理后,X变成(n_samples, n_wavelengths-1),y还是(n_samples,),cross_val_score直接报数组长度不匹配。
原因:np.diff沿波长方向做差值,输出维度自然减少。代码里没有对y做对齐或边界填充,两者长度就错位了。
解决:有两个方案。一是对y做对齐,一阶差分后把y从y[:, :-1]开始用;二是在差分前对光谱边缘做镜像扩展,补一个波长点再差分,让输出长度不变。稳妥做法是把预处理放在数据加载之后,统一以样本数对齐,不要盲信函数输出shape。
# 方案一:对齐标签 X_d1 = d1(X_sg) y_aligned = y[:X_d1.shape[0]] # 仅当每行代表样本时成立5.5 归一化后建模反而变差:不是所有模型都需要归一化
现象:最大最小归一化处理后,PLS的R²比原始数据还低。换成随机森林,归一化前后几乎没差别。
原因:PLS本身自带均值中心化,对量纲变化的敏感度比树模型高。如果数据本身量纲相近,额外归一化反而压缩了部分区分信息。树模型做的是基于阈值的分裂,变量做单调变换不影响分裂点,归一化自然没有收益。
解决:看模型假设选预处理。线性模型里归一化是常规操作,但要在交叉验证里对比做与不做。树模型、梯度提升完全不需要归一化。所有预处理决策都拿交叉验证结果说话,不要凭直觉。
6. 进阶验证:用重复交叉验证与曲线形态判断预处理是否有效
前面所有实验用的是单次5折交叉验证,但单次交叉验证有不小的运气成分。换一个随机种子,R²排序可能就变了。我做预处理的最后一个习惯,是把所有候选方法放到重复交叉验证里,看均值和方差,再做最终决定。
from sklearn.model_selection import RepeatedKFold, cross_val_score from pretreatment import wave X_wave = wave(X_clean, level=3) rkf = RepeatedKFold(n_splits=5, n_repeats=3, random_state=42) pls = PLSRegression(n_components=8) scores = cross_val_score(pls, X_wave, y_clean, cv=rkf, scoring="r2") print(f"小波预处理: R²={scores.mean():.4f} ± {scores.std():.4f}")RepeatedKFold把5折交叉验证重复3次,每次重新划分数据。得到的分数分布比单次交叉验证稳定得多。选择依据很简单:均值高的方法优先,标准差大说明该方法对数据划分敏感,稳定性差。至少重复3次,推荐5次,时间换稳定性值得。
另一个便宜又好用的验证方式是肉眼检查曲线形态。SNV做完基线应该在0附近波动,SG平滑后毛刺明显减少但峰位不偏移,差分后曲线围绕0对称。这些判断不需要统计知识,一眼能看出处理是否正常。我有一回wave处理出来的数据全变成零均值噪声,交叉验证R²居然不低,我差点被分数骗了,画图一看才发现是模型把噪声当信号学了。
从那以后我每次做完预处理都强制走一遍重复交叉验证加曲线对比,先看分布再看均值,最后才定方案。这套流程看起来慢,但能挡住大部分翻车。希望这段踩坑经验能帮到你。
本文还有配套的精品资源,点击获取