6.16. 曲线与积分

数学相关的几页介绍了接受一个数组并产生一个数组(或标量)的操作——算术、归约、广播。本页则涵盖另一类操作:把数组视为采样后的函数,并就函数本身提出问题。在样本之间插值、对样本拟合曲线、对其下方的面积求积分、将其与另一个缓冲区做卷积。

所有这些都接受 ndarray 输入,并返回一个浮点标量或浮点型 ndarray

6.16.1. 插值

interp() 执行一维线性插值。xp 是一个单调递增的一维自变量数组;fp 是与之对应的因变量值;x 是需要求插值的位置:

xp = np.array([0.0, 1.0, 2.0, 3.0])
fp = np.array([10.0, 20.0, 25.0, 30.0])
x  = np.array([0.5, 1.5, 2.5])

np.interp(x, xp, fp)
# array([15.0, 22.5, 27.5])

[xp[0], xp[-1]] 范围之外,结果会分别被裁剪到 fp[0]fp[-1]left=right= 关键字可以覆盖这两个端点值。

在摄像头上用于将标定表重新映射到任意采样位置——比如热敏电阻的温度-电压表、非线性的像素响应曲线、逐通道的伽马查找表。一次库调用,对输入遍历一遍,无需 Python 循环。

6.16.2. 多项式拟合与求值

polyfit() 通过最小二乘法对数据点 (x, y) 拟合一个 deg 阶的多项式,并返回系数(最高阶在前):

x = np.array([0.0, 1.0, 2.0, 3.0, 4.0])
y = np.array([0.0, 1.1, 3.9, 9.1, 15.8])

coeffs = np.polyfit(x, y, 2)
# array([1.0, 0.0, 0.0])  (approximately)

当省略 x 时,会使用 range(len(y))——这对于给一个没有关联 x 轴、规则采样的缓冲区快速拟合多项式很有用:

np.polyfit(y, 2)

polyval()x 处对以 p 为系数的多项式求值。输入 x 可以是标量(返回浮点数)或 ndarray(返回 ndarray):

p = np.polyfit(x, y, 2)
fitted = np.polyval(p, x)

自然的搭配方式是:在标定时调用一次 polyfit,存下系数,然后每一帧调用 polyval 来对所得曲线求值。多项式求值步骤对每个样本只需少量浮点运算,即使在最小的摄像头上也很廉价。

6.16.3. 卷积

convolve() 返回两个一维数组的全长离散线性卷积。仅实现了 full 模式;输出长度为 len(a) + len(v) - 1。对结果进行切片即可获得桌面版 numpy 所提供的 samevalid 模式的相同效果:

a = np.array([1.0, 2.0, 3.0])
v = np.array([0.5, 0.5])

np.convolve(a, v)
# array([0.5, 1.5, 2.5, 1.5])

对于短的 FIR 滤波器和平滑核(矩形、三角、高斯)很有用,这些情况下搭建 SOS 链显得多余。运行时间正比于两个数组长度的乘积——对短核来说没问题,但对长核很快就会比 FFT 卷积更昂贵。

6.16.4. 梯形积分

trapz() 通过复合梯形法则对采样后的函数求积分:

x = np.linspace(0, np.pi, num=128)
y = np.sin(x)
np.trapz(y, x)             # ~ 2.0

当采样间隔均匀且只关心步长时,传入 dx=;当样本间隔不均匀时,传入 x=。这是对已采集的传感器数据求积分的正确选择,因为此时无法获得解析形式。

对于已经过带限处理的样本数据(例如经过抗混叠滤波后的音频缓冲区),梯形法则以样本数的平方收敛,这意味着将缓冲区长度加倍可将误差缩小为原来的四分之一。

当被积函数不是一个样本缓冲区,而是应用程序可以在任意点上求值的 Python 函数时,正确的选择是另一类求解器。