第三章 光谱预处理实战:去宇宙射线、平滑滤波、基线校正与归一化

说实话,我刚入行那会儿,拿到一条拉曼光谱就急着找峰、对峰、鉴定材料。结果呢?被一条满是噪声、基线漂移得像过山车一样的光谱坑了好几次。后来我才明白——光谱预处理做不好,后面的分析全是白搭

这一章,咱们就来聊聊光谱预处理的四个核心步骤。我管它叫「光谱四件套」:去宇宙射线、平滑滤波、基线校正、归一化。每一步都有坑,每一步都有技巧。来,咱们一个一个过。

核心观点:预处理不是「美化」光谱,而是还原光谱的真实面貌。你做的每一步操作,都要问自己一句——我是在去除噪声,还是在引入假峰?

光谱预处理四步流程 原始光谱 步骤1 去宇宙射线 步骤2 SG平滑滤波 步骤3 基线校正 (airPLS) 步骤4 归一化 干净光谱 顺序很重要:先除噪 → 再平滑 → 再基线 → 最后归一化

3.1 去宇宙射线:把那些「鬼峰」揪出来

什么是宇宙射线?说白了,就是高能粒子撞到探测器上,产生一个极窄、极高的尖峰。你想想看,一条平滑的光谱上突然冒出一根「针」,这玩意儿要是被当成拉曼峰,那鉴定结果就全错了。

我在实验室里遇到过最离谱的一次——一条聚苯乙烯的光谱,在1000 cm⁻¹附近莫名其妙多了一个峰。我对着数据库查了半天,差点以为样品被污染了。后来才发现,是宇宙射线捣的鬼。

常用的去宇宙射线方法

  • 阈值法:设定一个强度阈值,超过阈值的点直接剔除或替换为邻域均值。简单粗暴,但容易误伤真实峰。
  • 中值滤波法:用滑动窗口的中值替代中心点。对孤立尖峰效果不错,但窗口大小要调好。
  • 多次采集对比法:同一位置采集多次光谱,取中位数或均值。这是最稳妥的方法,但耗时。

我的小技巧:如果你只有单条光谱,我建议先用阈值法粗筛,再用中值滤波精修。阈值可以设为「局部均值的3倍标准差」,这样不容易误伤。

import numpy as np
from scipy.signal import medfilt

def remove_cosmic_rays(spectrum, threshold=5, window=5):
    """
    去宇宙射线:阈值检测 + 中值替换
    spectrum: 原始光谱强度数组
    threshold: 阈值倍数(相对于局部标准差)
    window: 中值滤波窗口大小
    """
    # 计算局部均值和标准差
    local_mean = np.convolve(spectrum, np.ones(window)/window, mode='same')
    local_std = np.sqrt(np.convolve((spectrum - local_mean)**2, np.ones(window)/window, mode='same'))
    
    # 标记异常点
    mask = np.abs(spectrum - local_mean) > threshold * local_std
    
    # 用中值替换异常点
    cleaned = spectrum.copy()
    cleaned[mask] = medfilt(spectrum, kernel_size=window)[mask]
    
    return cleaned

注意:阈值设得太低,会把真实峰当噪声去掉;设得太高,又漏掉宇宙射线。我一般从5倍标准差开始试,不行再调。另外,窗口大小建议用奇数,3、5、7都行。

3.2 Savitzky-Golay 平滑滤波:去噪不丢峰

平滑滤波的方法很多,移动平均、高斯滤波、小波去噪……但我个人最常用的是 Savitzky-Golay(SG)滤波。为什么?因为它能在去噪的同时,尽量保留峰的宽度和高度。

SG滤波的原理,说白了就是在滑动窗口内做多项式拟合,用拟合值代替原始值。你想想看,移动平均是「平均」,SG是「拟合」——拟合能更好地保留曲线的形状。

SG滤波的两个关键参数

参数 含义 我的建议
窗口长度 每次拟合用的数据点数 奇数,一般取 5~15。窗口越大,平滑越强,但峰形损失也越大
多项式阶数 拟合多项式的次数 2 或 3 最常见。阶数越高,保留细节越好,但去噪能力下降

经验之谈:对于尖锐的拉曼峰,我建议用「小窗口 + 低阶数」,比如窗口=7、阶数=2。对于宽峰,可以适当放大窗口。记住一个原则——宁可平滑不足,也不要过度平滑把峰给抹平了。

from scipy.signal import savgol_filter

def sg_smooth(spectrum, window_length=7, polyorder=2):
    """
    Savitzky-Golay 平滑滤波
    window_length: 窗口长度(必须为奇数)
    polyorder: 多项式阶数(必须小于 window_length)
    """
    return savgol_filter(spectrum, window_length, polyorder)

3.3 基线校正:airPLS 算法实战

拉曼光谱的基线漂移,是我在项目中遇到最多的问题。荧光背景、样品表面不平、仪器状态变化……都会导致基线歪歪扭扭。如果不校正,峰高、峰面积全都不准。

基线校正的算法很多,但我最推荐 airPLS(自适应迭代惩罚最小二乘)。这玩意儿的好处是——不需要手动选点,算法自动拟合基线。

airPLS 的核心思想

说白了,就是迭代地给每个数据点分配一个权重。落在基线上的点,权重高;落在峰上的点,权重低。然后做加权最小二乘拟合,得到基线。迭代几次,基线就收敛了。

def airPLS(spectrum, lambda_=100, porder=1, itermax=20):
    """
    airPLS 基线校正
    lambda_: 平滑参数(越大基线越平滑)
    porder: 惩罚阶数(1 或 2)
    itermax: 最大迭代次数
    """
    from scipy import sparse
    from scipy.sparse.linalg import spsolve
    
    m = len(spectrum)
    w = np.ones(m)
    
    # 构建差分矩阵
    e = np.ones(m)
    D = sparse.diags([e, -2*e, e], [0, 1, 2], shape=(m-2, m))
    
    for i in range(itermax):
        W = sparse.diags(w, 0)
        Z = W + lambda_ * (D.T @ D)
        z = spsolve(Z, w * spectrum)
        
        # 更新权重:高于基线的点权重设为0
        d = spectrum - z
        w_new = np.where(d > 0, 0, np.exp(i * np.abs(d) / np.abs(d).sum()))
        w = w_new
        
        # 收敛判断
        if np.abs(d).sum() < 1e-6:
            break
    
    return z

参数调优:lambda_ 是 airPLS 最重要的参数。我一般从 100 开始试。如果基线太「硬」(跟着峰走),就增大 lambda_;如果基线太「软」(过度弯曲),就减小 lambda_。porder 用 2 通常效果更好。

我曾经踩过的坑:有一次处理一批聚合物样品,基线校正后峰形变得很奇怪。后来发现是 lambda_ 设得太小,基线把部分峰当成了基线的一部分。记住——基线校正不是「削峰」,而是「找底」。

3.4 归一化:让不同光谱站在同一起跑线

归一化的目的很简单——消除测量条件不同带来的强度差异。比如激光功率波动、积分时间不同、样品厚度不一……这些都会导致光谱强度不在一个量级上。

常见的归一化方法

  • 最大峰归一化:除以光谱中的最大强度值。简单,但受噪声影响大。
  • 面积归一化:除以光谱的总面积(积分)。适合比较峰形。
  • 向量归一化:除以光谱的 L2 范数。常用于多元分析。
  • 内标归一化:用已知峰(如硅的 520 cm⁻¹)做参考。最准确,但需要内标。
def normalize_spectrum(spectrum, method='max'):
    """
    光谱归一化
    method: 'max' | 'area' | 'vector'
    """
    if method == 'max':
        return spectrum / np.max(spectrum)
    elif method == 'area':
        return spectrum / np.trapz(spectrum)
    elif method == 'vector':
        return spectrum / np.linalg.norm(spectrum)
    else:
        raise ValueError("不支持的方法")

我的建议:如果是做定性分析(找峰、对峰),用最大峰归一化就够了。如果是做定量分析(比较峰面积),用面积归一化更合理。如果是做 PCA 或聚类分析,向量归一化是标配。

3.5 完整的预处理流水线

好了,四个步骤都讲完了。咱们把它们串起来,形成一个完整的预处理流水线。顺序很重要——先除噪、再平滑、再基线、最后归一化。你想想看,如果先归一化再基线校正,那基线漂移会被放大,校正效果就差了。

def preprocess_pipeline(spectrum, 
                        cosmic_threshold=5, cosmic_window=5,
                        sg_window=7, sg_poly=2,
                        airpls_lambda=100, airpls_order=2,
                        norm_method='max'):
    """
    完整的光谱预处理流水线
    """
    # 步骤1:去宇宙射线
    s1 = remove_cosmic_rays(spectrum, cosmic_threshold, cosmic_window)
    
    # 步骤2:SG平滑
    s2 = sg_smooth(s1, sg_window, sg_poly)
    
    # 步骤3:airPLS基线校正
    baseline = airPLS(s2, airpls_lambda, airpls_order)
    s3 = s2 - baseline
    
    # 步骤4:归一化
    s4 = normalize_spectrum(s3, norm_method)
    
    return s4

调试建议:每次做完一步,都把结果画出来看看。我习惯用 matplotlib 把原始光谱和处理后的光谱叠在一起对比。如果发现处理后的光谱出现了「假峰」或「畸变」,赶紧回头调参数。

嗯,这一章的内容就到这儿。预处理是拉曼光谱分析的地基,地基打不牢,后面盖什么楼都得塌。下一章咱们就要进入真正的「找峰」环节了——到时候你会发现,预处理做得好,找峰就是水到渠成的事。


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