4、光谱预处理-去噪:移动平均平滑、Savitzky-Golay滤波、小波去噪

做光谱分析这么多年,我最大的感触就是:原始光谱就像一块璞玉,不经过打磨,里面的信息根本没法用。噪声这东西,说白了就是仪器和环境的“私心杂念”,我们得想办法把它滤掉,只留下样品真实的“心声”。

今天咱们就聊聊三种最常用的去噪手段:移动平均平滑、Savitzky-Golay滤波,还有小波去噪。这三种方法我都在实际项目中摸爬滚打用过,各有各的脾气。

核心观点:去噪不是越干净越好,过度平滑会丢失关键特征。我的经验是——保留峰形,压制毛刺,两者之间找平衡。

光谱去噪三大方法 移动平均平滑 简单快速,窗口宽度是关键 适合基线噪声、高频毛刺 Savitzky-Golay滤波 保峰形好,多项式阶数+窗口 我最常用的方法,没有之一 小波去噪 多尺度分析,细节保留好 适合复杂背景、重叠峰 选择原则:噪声类型 → 峰形要求 → 计算效率

4.1 移动平均平滑:最朴素的去噪法

移动平均平滑,说白了就是“拉帮结派”。把相邻的几个点拉过来,取个平均值,代替中间那个点。你想想看,噪声是随机上下跳的,一平均,正负抵消,自然就平滑了。

公式很简单:

y_smooth[i] = (y[i-k] + y[i-k+1] + ... + y[i] + ... + y[i+k-1] + y[i+k]) / (2k+1)

这里的 2k+1 就是窗口宽度。窗口越大,平滑效果越强,但峰也会被“抹平”。

我的经验:窗口宽度一般取数据点数的1%~5%。比如一条光谱有1000个点,窗口取11~51比较合适。我曾经试过取101,结果一个尖锐的峰直接变成了小土包,那叫一个心疼。

Python实现:

import numpy as np

def moving_average(y, window_size=11):
    """移动平均平滑"""
    half = window_size // 2
    y_smooth = np.copy(y)
    for i in range(half, len(y) - half):
        y_smooth[i] = np.mean(y[i-half:i+half+1])
    return y_smooth

注意:移动平均会损失两端的数据点。窗口越大,损失越多。我一般用边界填充(比如复制端点值)来避免数据缩短。

4.2 Savitzky-Golay滤波:保峰形的利器

移动平均虽然简单,但有个硬伤——它会削峰。你想想看,峰顶本来是个尖尖,一平均,峰顶被周围的矮点拉低了,峰形就变了。

Savitzky-Golay滤波(简称SG滤波)就不一样了。它不是在窗口内简单平均,而是用多项式拟合窗口内的数据,然后用拟合值代替中心点。说白了,它假设局部数据可以用一个多项式来描述。

这样做的好处是:峰形保留得特别好。峰顶不会被削平,峰宽也不会被展宽。

参数 含义 我的建议
窗口宽度 参与拟合的数据点数(奇数) 11~31,根据噪声水平调整
多项式阶数 拟合多项式的最高次数 2~4,阶数太高容易过拟合

Python实现(用scipy):

from scipy.signal import savgol_filter

# 窗口宽度21,多项式阶数3
y_sg = savgol_filter(y, window_length=21, polyorder=3)

避坑指南:窗口宽度必须大于多项式阶数,否则没法拟合。我见过有人设窗口=5,阶数=5,结果报错还一脸懵。另外,阶数越高,计算越慢,而且容易把噪声也拟合进去——那就适得其反了。

我个人习惯用SG滤波处理拉曼光谱和红外光谱。这两种光谱的峰形比较尖锐,用移动平均会损失分辨率,但SG滤波就能很好地保留。我在做聚合物材料分析时,曾经用SG滤波成功分离了两个重叠的峰,那感觉,就像在沙子里淘出了金子。

4.3 小波去噪:多尺度分析的王者

小波去噪,听起来高大上,其实原理不复杂。它把光谱分解成不同尺度(频率)的成分,然后只保留我们想要的尺度,把噪声所在的尺度扔掉。

你可以把光谱想象成一幅画。大尺度的成分是画的轮廓(基线、宽峰),小尺度的成分是画的细节(尖锐峰、噪声)。小波去噪就是:把噪声所在的小尺度细节抹掉,再重新合成光谱

常用的步骤:

  1. 选择小波基:常用db4、db6、sym8等。我一般用db4,效果比较均衡。
  2. 确定分解层数:一般3~6层。层数越多,去噪越强,但也可能丢失有用信息。
  3. 阈值处理:对细节系数进行阈值处理,小于阈值的置零,大于阈值的保留或收缩。
  4. 重构:用处理后的系数重构光谱。

Python实现(用pywt):

import pywt

def wavelet_denoise(y, wavelet='db4', level=4, threshold=0.1):
    """小波去噪"""
    # 分解
    coeffs = pywt.wavedec(y, wavelet, level=level)
    # 对细节系数进行阈值处理
    coeffs_thresh = [coeffs[0]]  # 近似系数不动
    for i in range(1, len(coeffs)):
        coeffs_thresh.append(pywt.threshold(coeffs[i], threshold, mode='soft'))
    # 重构
    y_denoised = pywt.waverec(coeffs_thresh, wavelet)
    return y_denoised

小技巧:阈值怎么选?我一般用“通用阈值” threshold = sigma * sqrt(2 * log(N)),其中sigma是噪声标准差,N是数据长度。也可以用交叉验证来选,但计算量会大一些。

小波去噪最大的优势是:能同时处理不同频率的噪声。比如基线漂移(低频噪声)和随机毛刺(高频噪声),小波可以分层处理,各打各的板子。我在处理近红外光谱时,经常遇到基线漂移和随机噪声并存的情况,小波去噪的效果明显优于前两种方法。

注意:小波去噪的参数(小波基、层数、阈值)对结果影响很大。我建议先用一小段数据试跑,看看效果,再应用到全谱。不要一上来就全谱跑,万一参数不对,跑出来一团糟,还得重来。

4.4 三种方法怎么选?

说了这么多,到底用哪个?我总结了一个简单的选择逻辑:

  • 噪声是均匀的高频毛刺,峰形不尖锐 → 移动平均平滑。简单快速,够用就行。
  • 峰形尖锐,需要保留分辨率 → Savitzky-Golay滤波。这是最稳妥的选择,我80%的情况都用它。
  • 噪声复杂(基线漂移+高频噪声),或者峰重叠严重 → 小波去噪。虽然参数调起来麻烦,但效果确实好。

嗯,这里要注意:没有万能的方法。我建议你把三种方法都试一遍,对比结果,选效果最好的。毕竟,数据不会骗人。

最后说一句:去噪只是预处理的第一步,后面还有基线校正、归一化、特征提取等着我们。每一步都马虎不得。好了,今天就聊到这儿,下次咱们接着讲基线校正。


公众号:蓝海资料掘金营,微信deep3321