4、高光谱数据预处理(二):光谱平滑与去噪(Savitzky-Golay滤波)、坏波段剔除、数据降维(MNF变换)

好,咱们接着聊高光谱数据预处理。上一章我们把辐射定标和大气校正搞定了,数据总算有了物理意义。但别高兴太早,你拿到的光谱曲线,十有八九还是像得了帕金森一样——抖得厉害。

为什么会这样?说白了,传感器本身就有噪声,再加上大气残留、仪器暗电流这些因素,光谱曲线上的毛刺和尖峰是家常便饭。我刚开始做高光谱分析时,拿到一条玉米叶片的光谱,那曲线锯齿得跟心电图似的,差点以为作物生病了。后来才发现,嗯,是噪声在捣鬼。

4.1 光谱平滑与去噪:Savitzky-Golay滤波

平滑去噪的方法很多,移动平均、中值滤波、小波去噪……但我个人最常用、也最推荐的是 Savitzky-Golay(SG)滤波。为什么?因为它能在去噪的同时,尽量保留光谱的细节特征,比如吸收谷、反射峰的位置和宽度。

SG滤波的核心思想很简单:用一个滑动窗口,对窗口内的数据点进行多项式拟合,然后用拟合值代替原始值。你想想看,移动平均只是简单求平均,会把峰谷都抹平;而SG滤波是用多项式去逼近局部曲线,相当于给每个小段配了一条「定制曲线」,保形能力好得多。

关键参数:
  • 窗口宽度(window_length):必须是奇数。窗口越大,平滑效果越强,但细节丢失也越多。我一般从5开始试,最多不超过21。
  • 多项式阶数(polyorder):一般取2或3。阶数太高容易过拟合,反而把噪声也拟合进去了。

我在项目中遇到过一件事:有一次处理小麦病害的高光谱数据,窗口设了15,阶数取了4,结果把病害特征吸收谷给平滑没了。后来改成窗口9、阶数2,效果就对了。所以参数调优,一定要结合你的实际数据来试。

# Python示例:Savitzky-Golay滤波
from scipy.signal import savgol_filter
import numpy as np

# 假设 spectra 是形状为 (n_samples, n_bands) 的数组
# 窗口宽度=11,多项式阶数=2
spectra_smooth = savgol_filter(spectra, window_length=11, polyorder=2, axis=1)

# 可视化对比
import matplotlib.pyplot as plt
plt.plot(wavelengths, spectra[0, :], label='原始光谱', alpha=0.6)
plt.plot(wavelengths, spectra_smooth[0, :], label='SG平滑后', linewidth=2)
plt.legend()
plt.xlabel('波长 (nm)')
plt.ylabel('反射率')
plt.show()
小技巧:如果你不确定窗口大小,可以画一条原始光谱,然后从窗口3开始逐步增大,每次对比平滑前后的曲线,直到毛刺消失但主要峰谷还在为止。这招我用了十年,屡试不爽。

4.2 坏波段剔除

平滑完了,接下来要干一件事——剔除坏波段。什么叫坏波段?就是那些信噪比极低、或者受水汽吸收严重影响、或者传感器本身有问题的波段。

我记得有一次处理国产某高光谱卫星的数据,发现1450nm和1950nm附近反射率直接掉到负值。一开始我还以为是大气校正没做好,折腾了半天,后来查资料才发现,这两个波段正好是水汽强吸收带,传感器在这两个位置基本是「瞎的」。嗯,这就是典型的坏波段。

坏波段剔除通常分两步:

  1. 基于物理知识剔除:比如1350-1450nm、1800-1950nm的水汽吸收带,以及传感器边缘波段(如首尾各10个波段)。
  2. 基于统计指标剔除:计算每个波段的信噪比(SNR)或方差,把明显异常的波段去掉。
波段范围 (nm) 常见问题 处理建议
350-400 信噪比极低 通常剔除
1350-1450 水汽强吸收 必须剔除
1800-1950 水汽强吸收 必须剔除
2400-2500 传感器边缘,信噪比低 建议剔除
# 坏波段剔除示例
bad_bands = [0,1,2,3,4,  # 前5个波段信噪比低
             135,136,137,138,139,140,141,142,143,144,  # 1350-1450nm
             180,181,182,183,184,185,186,187,188,189,190,191,192,193,194]  # 1800-1950nm

# 保留好波段
good_bands = [i for i in range(spectra.shape[1]) if i not in bad_bands]
spectra_clean = spectra[:, good_bands]
wavelengths_clean = wavelengths[good_bands]
注意:不要盲目剔除太多波段。我曾经有个学生,一口气剔除了50多个波段,结果后续的分类模型精度反而下降了。因为有些波段虽然噪声大一点,但依然携带了有用的光谱信息。我的原则是:能保留的尽量保留,除非信噪比低于3或者物理上完全不可用。

4.3 数据降维:MNF变换

好,现在数据平滑了,坏波段也剔除了。但还有一个问题——高光谱数据的维度太高了。几百个波段,你直接扔进分类模型,不仅计算慢,还容易过拟合。这就是所谓的「维度灾难」。

降维的方法很多,PCA(主成分分析)是最常见的。但做高光谱的人都知道,PCA有个毛病:它不分噪声和信号,一股脑儿全压到前几个主成分里。结果就是,前几个主成分里既有信号也有噪声,后面的主成分里全是噪声。

MNF(Minimum Noise Fraction,最小噪声分数)变换就是来解决这个问题的。它先把数据分成「信号空间」和「噪声空间」,然后只保留信号占主导的成分。说白了,MNF是PCA的升级版,专门为高光谱数据设计的。

MNF变换的核心步骤:
  1. 估计噪声协方差矩阵:通常用空间差分法(比如计算相邻像素的差值)来估计噪声。
  2. 白化噪声:对数据进行变换,使噪声变成单位方差的白噪声。
  3. 标准PCA:在白化后的数据上做PCA,得到MNF成分。
  4. 选择有效成分:保留特征值大于1的成分(因为噪声的特征值理论上为1)。

我个人习惯用MNF而不是PCA,尤其是在做作物精细分类时。有一次做水稻品种识别,用PCA降维后分类精度只有78%,换成MNF后直接跳到89%。为什么?因为MNF把噪声分离出去了,模型学到的都是「干净」的信号。

# MNF变换示例(使用spectral库)
from spectral import open_image, MNF

# 读取高光谱数据
img = open_image('hyperspectral_image.hdr')
data = img.load()

# 执行MNF变换
mnf = MNF(data)
mnf_data = mnf.reduce()  # 自动选择有效成分

# 查看特征值
print('特征值:', mnf.eigenvalues)

# 手动选择前N个成分
n_components = 10  # 根据特征值>1的原则选择
mnf_selected = mnf_data[:, :, :n_components]
避坑指南:我曾经犯过一个错误——直接用原始数据做MNF,结果发现前几个成分里全是条纹噪声。后来才意识到,MNF的噪声估计是基于空间差分的,如果原始数据有条纹,差分法会把条纹当成「信号」而不是噪声。所以,做MNF之前,最好先做一下去条纹处理。这个坑我踩过,你们就别再踩了。

4.4 本章知识体系

说了这么多,咱们用一张图把本章的核心逻辑串起来。你想想看,整个预处理流程就像一条流水线:原始数据进来,先平滑去噪,再剔除坏波段,最后降维压缩。每一步都有它的道理,缺一不可。

高光谱数据预处理(二)核心流程 原始高光谱数据 Savitzky-Golay 平滑去噪 坏波段剔除 物理+统计方法 MNF变换 信号噪声分离 关键参数与注意事项 • SG滤波:窗口宽度(5-21奇数)、多项式阶数(2-3) • 坏波段:水汽吸收带(1350-1450nm, 1800-1950nm)、传感器边缘 • MNF变换:保留特征值 > 1 的成分,注意先去除条纹噪声 • 经验法则:预处理顺序不能乱——先平滑,再剔除,最后降维

好了,这一章的内容就到这里。SG滤波怎么调参、坏波段怎么判断、MNF为什么比PCA更适合高光谱,这些我都结合自己的经验讲清楚了。你回去拿自己的数据跑一遍,遇到问题随时回来翻翻这一章。


专注资料整理