4. 信号采集与数据预处理:原始干涉信号采集、背景噪声去除、去直流项、波长到波数(k域)重采样

好,咱们进入正题。这一章讲的是OCT系统里最“脏”的活——信号采集与预处理。

很多人觉得算法才是核心,采集嘛,接上探测器就行。我刚开始也这么想。直到有一次,我花了两周调一个去噪算法,效果始终不理想。最后发现,问题出在采集卡的地线没接好。嗯,从那以后,我再也不敢小看预处理这一步了。

说白了,原始干涉信号就像一块璞玉。你得先把它表面的泥巴洗干净,才能看出它是不是块好料。这一章,我们就来聊聊怎么“洗”这块玉。

4.1 原始干涉信号采集

先说说采集。OCT系统的核心是迈克尔逊干涉仪。光源发出的光,被分束器一分为二。一束去样品,一束去参考镜。两束光反射回来,在探测器上相遇,形成干涉。

这个干涉信号,就是我们的原始数据。它长什么样?

我习惯用一个公式来描述:

I(k) = I_r + I_s + 2 * sqrt(I_r * I_s) * cos(2 * k * Δz)

其中:

  • I(k):探测器接收到的总光强,随波数k变化
  • I_r:参考臂的光强(直流项的一部分)
  • I_s:样品臂返回的光强(也是直流项)
  • Δz:样品与参考镜的光程差
  • k:波数,k = 2π/λ

你想想看,这个信号里,真正有用的信息是那个余弦项。它包含了样品的深度信息。但直流项(I_r + I_s)和噪声,把有用的信号淹没了。

核心要点:原始干涉信号 = 直流项 + 互相关项(有用信号)+ 噪声。我们的目标,就是把互相关项干净地提取出来。

4.2 背景噪声去除

背景噪声从哪来?

我遇到过最头疼的情况,是系统里的固定模式噪声。它来自光学元件的表面反射、探测器暗电流、甚至电源纹波。这些噪声不随样品变化,但会叠加在信号上,形成固定的“背景”。

怎么去除?最直接的方法——减背景。

具体做法:

  1. 采集背景信号:挡住样品臂,只采集参考臂和系统本身的噪声。这时的信号就是背景。
  2. 采集样品信号:正常采集样品干涉信号。
  3. 相减:样品信号减去背景信号。

代码实现很简单:

# 假设 data 是采集到的原始干涉信号(二维数组,行数=光谱数,列数=像素数)
# background 是采集到的背景信号

def remove_background(data, background):
    """
    去除背景噪声
    """
    # 确保背景和数据的维度一致
    if data.shape != background.shape:
        # 如果背景是单帧,则扩展到与数据相同维度
        background = np.tile(background, (data.shape[0], 1))
    
    # 相减
    cleaned_data = data - background
    return cleaned_data

我的经验:背景信号不是一成不变的。系统温度变化、光源功率漂移,都会改变背景。我建议每采集10-20帧,重新采集一次背景。或者,用滑动平均的方式动态更新背景。

4.3 去直流项

减完背景,信号里还有直流项。直流项就是公式里的 I_r + I_s。它不随光程差变化,是一个常数偏移。

为什么要去直流?

因为直流项会严重影响后续的傅里叶变换。你想想看,傅里叶变换会把直流分量变成零频处的尖峰。这个尖峰会淹没低频信号,还会产生伪影。

去直流的方法,我常用两种:

方法 原理 优缺点
均值法 计算信号的平均值,然后减去 简单快速,但无法去除缓慢变化的直流漂移
高通滤波法 用高通滤波器滤除低频直流分量 可以去除漂移,但可能影响低频信号
多项式拟合 用多项式拟合直流基线,然后减去 灵活,适合非线性漂移,但计算量大

我个人习惯用均值法。简单,有效。前提是信号里没有大的直流漂移。

def remove_dc(data):
    """
    去直流项 - 均值法
    """
    # 计算每个光谱的平均值
    dc = np.mean(data, axis=1, keepdims=True)
    # 减去直流
    data_ac = data - dc
    return data_ac

注意:去直流和去背景是两个步骤,不要搞混。去背景是去除系统固定噪声,去直流是去除信号本身的常数偏移。顺序上,我建议先做去背景,再做去直流。

4.4 波长到波数(k域)重采样

这一步,是预处理里最关键的。也是很多人容易忽略的。

为什么需要重采样?

因为我们的探测器采集到的信号,是在波长域(λ域)上均匀采样的。但傅里叶变换要求信号在波数域(k域)上均匀采样。k和λ的关系是:k = 2π/λ。这不是线性关系。

说白了,你在λ域上均匀采样,到了k域就变成不均匀了。直接做傅里叶变换,结果会失真,分辨率下降。

我见过有人直接拿λ域数据做FFT,结果图像模糊得一塌糊涂。他还以为是系统分辨率不够。其实,只是没做重采样。

重采样的核心步骤:

  1. 计算k向量:根据波长范围,计算出对应的k值。
  2. 插值:在k域上建立均匀的网格,然后用插值方法(线性插值、样条插值等)从原始λ域数据中获取k域均匀网格上的值。

代码示例:

def resample_k_domain(wavelength, signal, num_points=None):
    """
    波长到波数域重采样
    
    参数:
        wavelength: 原始波长数组 (1D)
        signal: 原始干涉信号 (2D, 行数=光谱数, 列数=波长点数)
        num_points: 重采样后的点数,默认与原始点数相同
    
    返回:
        k_uniform: 均匀的k域坐标
        signal_resampled: 重采样后的信号
    """
    # 1. 计算原始k域
    k = 2 * np.pi / wavelength
    
    # 2. 建立均匀的k域网格
    if num_points is None:
        num_points = len(wavelength)
    
    k_uniform = np.linspace(k.min(), k.max(), num_points)
    
    # 3. 插值(对每个光谱进行插值)
    signal_resampled = np.zeros((signal.shape[0], num_points))
    for i in range(signal.shape[0]):
        # 使用线性插值
        signal_resampled[i, :] = np.interp(k_uniform, k, signal[i, :])
    
    return k_uniform, signal_resampled

避坑指南:我曾经犯过一个错误——直接用波长数据做插值,然后做FFT。结果图像出现了严重的“鬼影”。后来发现,插值必须在k域做,而不是在λ域。因为FFT要求的是k域均匀采样,不是λ域均匀采样。这个顺序不能搞反。

另外,插值方法的选择也有讲究。线性插值速度快,但精度一般。样条插值精度高,但计算量大。我一般用线性插值,够用了。如果追求极致分辨率,可以用三次样条。

4.5 本章小结

好,我们来捋一捋。信号采集与预处理,一共四步:

  • 采集:拿到原始干涉信号,理解它的组成。
  • 去背景:减去系统固定噪声。
  • 去直流:去掉常数偏移。
  • k域重采样:把信号从λ域映射到k域,为FFT做准备。

这四步做完,你的信号才算“干净”了。可以进入下一步——傅里叶变换和图像重建。

嗯,预处理这一步,看似简单,但细节很多。我见过太多人在这上面栽跟头。希望你能重视起来。

下面这张图,是我自己画的预处理流程图。你可以对照着看,每一步的位置和作用。

OCT信号预处理流程图 原始干涉信号采集 背景噪声去除 去直流项 k域重采样 各步骤说明: • 原始干涉信号采集:探测器获取干涉光谱,包含直流项、互相关项和噪声。 • 背景噪声去除:减去系统固定噪声(如暗电流、光学元件反射等)。 • 去直流项:去除信号中的常数偏移(I_r + I_s),避免FFT后零频尖峰。 • k域重采样:将λ域均匀采样转换为k域均匀采样,保证FFT结果准确。 顺序不可颠倒!去背景 → 去直流 → k域重采样

好了,这一章就到这里。预处理是OCT信号处理的地基。地基打不牢,后面盖再高的楼也是白搭。希望你能把每一步都吃透。


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