第三章 光谱数据分析:光谱特征提取与匹配
光谱数据拿到手,第一件事是什么?
我个人习惯,不是急着做分类或者定量分析。而是先看看——这条光谱长什么样?它的峰在哪?有多宽?跟数据库里的标准谱像不像?
说白了,光谱分析的核心就两件事:特征提取和光谱匹配。今天咱们就把这两块掰开揉碎了讲。
3.1 峰值检测——找到光谱的“指纹”
峰值检测,听起来简单吧?不就是找最大值吗?
嗯,我在项目里踩过这个坑。有一次做拉曼光谱分析,直接取最大值,结果噪声尖峰被当成了特征峰……后来我学乖了,峰值检测必须考虑三个要素:信噪比、峰宽、局部极大值。
峰值检测的黄金法则:
- 信号强度必须高于噪声阈值(通常取3倍标准差)
- 峰宽不能太窄(排除单点噪声)
- 必须是局部极大值(左右相邻点都小于它)
来看一段我常用的Python代码,用的是scipy的find_peaks函数:
import numpy as np
from scipy.signal import find_peaks
# 假设 wavelength 是波长数组,intensity 是光强数组
peaks, properties = find_peaks(
intensity,
height=0.1, # 峰高阈值
distance=10, # 峰间最小距离(像素)
prominence=0.05, # 峰显著度
width=3 # 峰宽下限
)
print(f"检测到 {len(peaks)} 个特征峰")
for i, p in enumerate(peaks):
print(f"峰 {i+1}: 波长 {wavelength[p]:.2f} nm, 强度 {intensity[p]:.4f}")
这里我特别想强调prominence(显著度)这个参数。它衡量的是一个峰相对于周围谷底的突出程度。我曾经处理过一组荧光光谱,背景漂移很严重,光设高度阈值根本没用。后来加了prominence限制,才把真正的荧光峰从斜坡背景里揪出来。
小技巧:如果数据噪声大,可以先做平滑处理。我常用Savitzky-Golay滤波器,窗口大小选奇数,比如5或7。窗口太小去噪效果差,太大又会把细小的峰抹平——这个度需要根据你的采样率来调。
3.2 半高宽(FWHM)计算——峰的“胖瘦”藏着信息
峰值位置告诉你“是什么”,半高宽告诉你“状态怎么样”。
举个例子,我在做量子点荧光检测时,同一个材料的量子点,尺寸分布越均匀,半高宽就越窄。如果半高宽突然变宽了,说明样品可能发生了团聚或者降解。
半高宽的计算逻辑不复杂:
- 找到峰顶位置和峰高
- 计算半峰高 = 峰高 / 2
- 在峰的两侧找到强度等于半峰高的两个点
- 两个点对应的波长差就是半高宽
但实际做的时候,有个坑——如果峰不对称怎么办?
我建议用高斯拟合的方法。把峰附近的点用高斯函数拟合,然后从拟合曲线上读取半高宽。这样即使数据有噪声或者峰形不对称,结果也更稳定。
from scipy.optimize import curve_fit
import numpy as np
def gaussian(x, amp, cen, sigma):
return amp * np.exp(-(x - cen)**2 / (2 * sigma**2))
# 取峰附近的数据点
x_data = wavelength[peaks[0]-5:peaks[0]+6]
y_data = intensity[peaks[0]-5:peaks[0]+6]
# 拟合
popt, _ = curve_fit(gaussian, x_data, y_data,
p0=[y_data.max(), x_data.mean(), 1])
fwhm = 2 * np.sqrt(2 * np.log(2)) * abs(popt[2])
print(f"半高宽: {fwhm:.2f} nm")
注意:拟合前一定要确保数据点覆盖了峰的完整轮廓。如果只取了峰顶附近几个点,拟合结果会严重偏离真实值。我一般取峰两侧各延伸2-3个半高宽范围的数据点。
3.3 光谱匹配——拿你的谱跟数据库“对对碰”
特征提取完了,接下来就是匹配。说白了,就是算一算你的光谱跟数据库里的标准谱有多像。
常用的方法有两种:欧氏距离和相关系数。它们各有各的脾气。
3.3.1 欧氏距离——简单粗暴,但有前提
欧氏距离就是逐点计算强度差的平方和再开方。数值越小,说明两条光谱越接近。
def euclidean_distance(spectrum1, spectrum2):
return np.sqrt(np.sum((spectrum1 - spectrum2)**2))
# 假设 unknown 是待测光谱,database 是标准谱列表
distances = [euclidean_distance(unknown, db_spec) for db_spec in database]
best_match = np.argmin(distances)
print(f"最佳匹配: 数据库第 {best_match+1} 条, 距离 = {distances[best_match]:.4f}")
但这里有个大坑——欧氏距离对强度变化非常敏感。我在做荧光光谱匹配时遇到过,同样的样品,只是激发光功率不同,强度差了一倍,欧氏距离就变得很大,导致匹配失败。
怎么办?归一化。把光谱缩放到0-1之间,或者做面积归一化。这样就能消除绝对强度的影响,只比较形状。
3.3.2 相关系数——只看形状,不看强度
相关系数(Pearson相关系数)衡量的是两条光谱的线性相关程度。值越接近1,说明形状越相似。它不受强度缩放的影响,这一点在实际应用中非常实用。
from scipy.stats import pearsonr
def spectral_correlation(spectrum1, spectrum2):
r, _ = pearsonr(spectrum1, spectrum2)
return r
correlations = [spectral_correlation(unknown, db_spec) for db_spec in database]
best_match = np.argmax(correlations)
print(f"最佳匹配: 数据库第 {best_match+1} 条, 相关系数 = {correlations[best_match]:.4f}")
我的经验:在实际项目中,我通常把两种方法结合起来用。先用相关系数筛选出形状相似的候选谱(比如r > 0.9),再用欧氏距离从候选里挑出最接近的。这样既避免了强度干扰,又能区分细微差异。
3.4 知识体系总览
下面这张图是我自己整理的,把本章的核心逻辑串起来了:
3.5 实战中的避坑指南
最后,分享几个我踩过的坑,希望能帮你少走弯路:
- 峰值检测时,别忘了基线校正。我曾经处理过一组红外光谱,基线漂移导致低波数段的峰被淹没。后来用非对称最小二乘法做了基线校正,才把隐藏的峰找出来。
- 半高宽计算,别用原始数据直接量。噪声会让半高宽偏大。先拟合再计算,结果稳定得多。
- 光谱匹配前,一定要对齐波长轴。不同仪器采集的光谱,波长间隔可能不一样。我习惯用线性插值把待测谱和数据库谱统一到相同的波长网格上。
- 相关系数不是万能的。如果两条光谱形状完全一样但强度相反(比如吸收谱和透射谱),相关系数会是-1。这时候要结合物理意义判断,别被数字骗了。
我的工具箱:日常做光谱分析,我常用的库是scipy.signal(峰值检测、滤波)、numpy(数值计算)、scipy.stats(相关系数)、scipy.optimize(曲线拟合)。这几个组合起来,基本能覆盖90%的场景。
好了,光谱特征提取和匹配这块就讲到这里。记住,工具是死的,数据是活的。多动手试试不同的参数组合,慢慢你就能找到感觉。