光谱预处理(上):暗电流扣除、背景校正、归一化方法

拿到原始光谱数据,别急着分析。我见过太多人上来就做拟合、建模型,结果模型泛化能力一塌糊涂。为什么?因为原始数据里藏着大量「脏东西」——暗电流噪声、背景干扰、样品厚度差异……这些不处理掉,后面全是白费功夫。

这一章,咱们就聊聊光谱预处理的三个基本功:暗电流扣除、背景校正、归一化。说白了,就是让光谱数据「洗干净、对齐整、可比对」。

核心观点:预处理做得好,模型差不了。预处理做得糙,结果全白搞。

光谱预处理(上)知识结构 光谱预处理 暗电流扣除 背景校正 归一化方法 消除探测器热噪声 I_corrected = I_raw - I_dark 扣除溶剂/基底信号 I_sample = I_raw - I_background 最大归一化 / 面积归一化 矢量归一化(SNV) 目标:干净、对齐、可比的光谱

一、暗电流扣除:把「零信号」找出来

暗电流是什么?说白了,就是探测器在没有光信号输入时,自己产生的电信号。热噪声、读出噪声、偏置电压……这些乱七八糟的东西叠加在一起,就成了暗电流。你想想看,如果不去掉它,你的光谱里就多了一段「假信号」。

怎么做?很简单——在完全遮光条件下采集一条光谱,这就是暗电流谱。然后用样品光谱减去它。

我的习惯:每次实验前、实验后各采一次暗电流,取平均再用。温度变化会影响暗电流,尤其是红外探测器。我曾经有一次没注意,下午的暗电流比上午高了30%,结果扣除后光谱基线都歪了。

import numpy as np

def dark_current_correction(raw_spectrum, dark_spectrum):
    """
    暗电流扣除
    raw_spectrum: 原始样品光谱 (1D array)
    dark_spectrum: 暗电流光谱 (1D array)
    返回: 扣除暗电流后的光谱
    """
    corrected = raw_spectrum - dark_spectrum
    return corrected

# 示例用法
raw = np.array([1024, 2048, 3072, 4096])  # 原始信号
dark = np.array([24, 48, 72, 96])          # 暗电流
clean = dark_current_correction(raw, dark)
print(clean)  # [1000, 2000, 3000, 4000]

注意:暗电流扣除后可能出现负值,尤其是信号弱的波段。这时候不要直接丢弃,可以检查是不是暗电流采多了,或者考虑用基线校正做二次处理。

二、背景校正:把「环境干扰」去掉

背景校正和暗电流扣除有点像,但目标不同。暗电流扣的是探测器自身的噪声,背景校正扣的是样品之外的所有信号——比如溶剂吸收、基底反射、空气水分……

我做过一个拉曼光谱项目,测的是聚合物薄膜。第一次测出来一堆峰,我兴奋得不行,结果一查——全是基底聚四氟乙烯的峰。嗯,从那以后我再也不敢跳过背景校正了。

标准流程:

  1. 在不放样品、只放基底/溶剂的条件下采集背景光谱
  2. 用样品光谱减去背景光谱
  3. 如果背景不稳定,可以多次采集取平均
def background_correction(sample_spectrum, background_spectrum):
    """
    背景校正
    sample_spectrum: 样品光谱
    background_spectrum: 背景光谱
    返回: 扣除背景后的光谱
    """
    corrected = sample_spectrum - background_spectrum
    return corrected

# 实际项目中常用:先扣暗电流,再扣背景
def full_preprocess(raw, dark, background):
    step1 = dark_current_correction(raw, dark)
    step2 = background_correction(step1, background)
    return step2

避坑指南:背景光谱和样品光谱的采集条件必须一致——同样的积分时间、同样的光路、同样的温度。我曾经为了省事,用昨天的背景数据,结果今天湿度变了,水汽吸收峰完全对不上。

三、归一化方法:让光谱「站在同一起跑线」

归一化解决什么问题?样品厚度、浓度、光源强度波动……这些因素会让光谱的绝对强度不一样,但相对形状才是我们关心的。归一化就是去掉这些「量」的差异,保留「形」的信息。

常用的有三种方法,我一个个说。

3.1 最大归一化

把光谱中最大的强度值变成1,其他点按比例缩放。简单粗暴,适合只有一个强峰的光谱。

def max_normalization(spectrum):
    """
    最大归一化:除以最大值
    """
    max_val = np.max(spectrum)
    if max_val == 0:
        return spectrum
    return spectrum / max_val

注意:如果光谱里有噪声尖峰,最大归一化会被「骗」到。我建议先做平滑,再做归一化。

3.2 面积归一化

把光谱曲线下的总面积变成1。适合比较不同样品的相对峰面积,比如定量分析。

def area_normalization(spectrum, wavenumbers=None):
    """
    面积归一化:除以曲线下面积
    如果提供波数,用梯形法积分
    """
    if wavenumbers is not None:
        area = np.trapz(spectrum, wavenumbers)
    else:
        area = np.sum(spectrum)
    if area == 0:
        return spectrum
    return spectrum / area

3.3 矢量归一化(SNV)

这是我最常用的方法。SNV(Standard Normal Variate)对每条光谱做标准化——减去均值,除以标准差。效果是:基线偏移被消除,整体强度被拉平。

def snv_normalization(spectrum):
    """
    矢量归一化 (Standard Normal Variate)
    公式: (x - mean(x)) / std(x)
    """
    mean_val = np.mean(spectrum)
    std_val = np.std(spectrum)
    if std_val == 0:
        return spectrum
    return (spectrum - mean_val) / std_val

我的推荐:如果做分类或聚类,优先用SNV。如果做定量分析,面积归一化更直观。最大归一化适合快速预览,但不建议用于建模。

三种归一化方法对比

方法 公式 适用场景 缺点
最大归一化 I' = I / I_max 单峰光谱、快速预览 受噪声尖峰影响大
面积归一化 I' = I / ∫I dλ 定量分析、峰面积比较 基线漂移会干扰面积计算
矢量归一化 (SNV) I' = (I - μ) / σ 分类、聚类、多元校正 丢失绝对强度信息

实际项目中的预处理流水线

我一般按这个顺序来:

  1. 暗电流扣除——先去掉探测器噪声
  2. 背景校正——再去掉环境干扰
  3. 平滑(下一章讲)——降低随机噪声
  4. 归一化——最后统一尺度
def preprocessing_pipeline(raw, dark, background, method='snv'):
    """
    完整预处理流水线
    """
    # 1. 暗电流扣除
    s1 = dark_current_correction(raw, dark)
    # 2. 背景校正
    s2 = background_correction(s1, background)
    # 3. 归一化
    if method == 'max':
        s3 = max_normalization(s2)
    elif method == 'area':
        s3 = area_normalization(s2)
    elif method == 'snv':
        s3 = snv_normalization(s2)
    else:
        raise ValueError("未知归一化方法")
    return s3

小经验:预处理顺序很重要。先扣暗电流再扣背景,因为暗电流是探测器本身的噪声,背景里也包含了暗电流。如果顺序反了,相当于扣了两次暗电流,反而引入误差。

好了,这一章的内容就这些。暗电流扣除、背景校正、三种归一化方法——看起来简单,但每一步都有坑。我当年刚入行时,就因为没扣暗电流,被导师骂了一顿。嗯,希望你不用经历这种尴尬。