- 简称SG滤波器
一、Savitzky-Golay滤波器
-
在数据分析领域,信号处理中的噪声问题始终是一个重要议题。
无论是实验数据、金融时间序列还是其他形式的信号处理,噪声都会干扰目标模式和趋势的识别。
尽管存在多种降噪方法,但在处理短时信号时,算法的性能往往比执行效率更为重要。
在众多方法中Savitzky-Golay滤波器因其独特的特征保持能力而脱颖而出。
-
Savitzky-Golay滤波器由Abraham Savitzky和Marcel J. E. Golay于1964年提出,是一种应用广泛的数字滤波器,可用于数据平滑和微分运算。
与传统的中值滤波或均值滤波等容易造成信号特征损失的方法相比,Savitzky-Golay滤波器能够在实现信号平滑的同时保持原始信号的关键特征。
这一特性使其在信号形状和特征保持要求较高的应用场景中具有显著优势。
二、原理
-
Savitzky-Golay滤波器是一种基于局部多项式回归的数字滤波器,其核心是通过线性最小二乘法将低阶多项式拟合到相邻数据点的滑动窗口中。
该方法的主要优势在于能够在降低噪声的同时保持信号的高阶矩,这意味着信号的峰值、谷值等特征可以得到较好的保持。
-
滤波器的工作过程可以概括为:
在信号序列上滑动固定大小的窗口,对窗口内的数据点进行多项式拟合。
窗口大小和多项式阶数是该算法的两个关键参数。
算法在每个窗口位置计算多项式在中心点处的值,将其作为该点的滤波输出。
通过对每个数据点重复此过程,最终得到完整的滤波信号。
三、应用
-
对添加高斯噪声后的正弦信号。
使用scipy.signal模块中的savgol_filter函数实现滤波。y_smooth = savgol_filter(y, 11, 3)
选择窗口大小为11,多项式阶数为3:可以很好的还原正弦信号。
-
小窗口低阶配置:能够保持局部特征,但对高频噪声的抑制效果有限
小窗口高阶配置:可以捕获复杂的局部变化,但存在过拟合风险
大窗口低阶配置:具有良好的噪声抑制效果,但可能会过度平滑信号特征
大窗口高阶配置:在保持信号特征的同时提供平滑效果,但需要注意窗口大小与信号特征尺度的匹配
-
参数选择策略
Savitzky-Golay滤波器的性能很大程度上取决于窗口大小和多项式阶数的选择。这两个参数需要根据具体应用场景进行优化。
小窗口:适用于快速变化信号的处理
大窗口:适用于缓慢变化信号的处理
低阶多项式(p=2或3):适用于平滑变化的信号,具有较好的抗噪声能力,计算效率较高
高阶多项式(p=4或5):适用于具有复杂局部结构的信号,能够更好地保持信号特征,需要注意过拟合风险
四、第三方库
对于生产环境,使用经过测试的库是更高效、可靠的选择。
- Altaxo.Calc.Regression (开源免费) :
这是一个开源科学计算库的一部分,提供了完整的Savitzky-Golay实现。-
核心类 :
SavitzkyGolay类用于计算系数并应用滤波。 -
使用示例 :
// 参数:点数(必须为奇数),导数阶数,多项式阶数
var sg = new Altaxo.Calc.Regression.SavitzkyGolay(7, 0, 3);
double\[\] smoothedData = sg.Apply(originalData);
-
五、C#程序
using System;
public static class SavitzkyGolay
{
/// <summary>
/// 对输入数据进行Savitzky-Golay平滑。
/// </summary>
/// <param name="data">输入的原始数据数组。</param>
/// <param name="windowSize">窗口大小,必须是正奇数。</param>
/// <param name="polynomialOrder">多项式阶数,必须小于windowSize。</param>
/// <returns>平滑后的数据数组。</returns>
public static double[] Smooth(double[] data, int windowSize, int polynomialOrder)
{
if (data == null) throw new ArgumentNullException(nameof(data));
if (windowSize % 2 == 0) throw new ArgumentException("窗口大小必须是奇数。", nameof(windowSize));
if (polynomialOrder >= windowSize) throw new ArgumentException("多项式阶数必须小于窗口大小。");
int halfWindow = windowSize / 2;
int n = data.Length;
double[] smoothedData = new double[n];
// 1. 计算中心点的卷积系数
double[] coefficients = CalculateCoefficients(windowSize, polynomialOrder);
// 2. 对每个数据点应用卷积
for (int i = 0; i < n; i++)
{
double sum = 0;
int coeffIndex = 0;
for (int j = -halfWindow; j <= halfWindow; j++)
{
int dataIndex = i + j;
if (dataIndex >= 0 && dataIndex < n)
{
sum += coefficients[coeffIndex] * data[dataIndex];
}
coeffIndex++;
}
smoothedData[i] = sum;
}
return smoothedData;
}
/// <summary>
/// 计算Savitzky-Golay滤波器的中心点卷积系数。
/// </summary>
private static double[] CalculateCoefficients(int windowSize, int polynomialOrder)
{
int m = windowSize / 2;
// 构建设计矩阵 A (大小为 windowSize x (polynomialOrder+1))
double[,] A = new double[windowSize, polynomialOrder + 1];
for (int i = -m; i <= m; i++)
{
for (int j = 0; j <= polynomialOrder; j++)
{
A[i + m, j] = Math.Pow(i, j);
}
}
// 计算 (A^T * A) 的逆矩阵
double[,] AtA = MultiplyTranspose(A, A);
double[,] invAtA = InvertMatrix(AtA);
// 计算系数: coeff = (A^T * A)^-1 * A^T 的中心行 (即对应导数阶数为0的行)
double[] coefficients = new double[windowSize];
// 中心行索引为 polynomialOrder / 2,对于0阶导数,实际上是第一行 (索引0)
int rowIndex = 0;
for (int i = 0; i < windowSize; i++)
{
double sum = 0;
for (int j = 0; j <= polynomialOrder; j++)
{
sum += invAtA[rowIndex, j] * A[i, j];
}
coefficients[i] = sum;
}
return coefficients;
}
// --- 辅助矩阵运算函数 (为保持完整性的简化实现) ---
private static double[,] MultiplyTranspose(double[,] A, double[,] B)
{
int rows = A.GetLength(1);
int cols = B.GetLength(1);
double[,] result = new double[rows, cols];
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
{
double sum = 0;
for (int k = 0; k < A.GetLength(0); k++)
sum += A[k, i] * B[k, j];
result[i, j] = sum;
}
return result;
}
private static double[,] InvertMatrix(double[,] matrix)
{
// 这是一个简化实现,仅适用于小矩阵。
// 对于生产环境,建议使用更稳健的算法,如LU分解或SVD。
int n = matrix.GetLength(0);
double[,] augmented = new double[n, 2 * n];
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
augmented[i, j] = matrix[i, j];
for (int i = 0; i < n; i++)
augmented[i, i + n] = 1;
for (int i = 0; i < n; i++)
{
// 寻找主元
int maxRow = i;
for (int k = i + 1; k < n; k++)
if (Math.Abs(augmented[k, i]) > Math.Abs(augmented[maxRow, i]))
maxRow = k;
// 交换行
for (int k = 0; k < 2 * n; k++)
{
double tmp = augmented[i, k];
augmented[i, k] = augmented[maxRow, k];
augmented[maxRow, k] = tmp;
}
// 主元归一化
double pivot = augmented[i, i];
for (int k = i; k < 2 * n; k++)
augmented[i, k] /= pivot;
// 消去其他行
for (int k = 0; k < n; k++)
{
if (k == i) continue;
double factor = augmented[k, i];
for (int j = i; j < 2 * n; j++)
augmented[k, j] -= factor * augmented[i, j];
}
}
double[,] inv = new double[n, n];
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
inv[i, j] = augmented[i, j + n];
return inv;
}
}
-
调用
// 生成示例数据
int n = 100;
double[] x = new double[n];
double[] y = new double[n];
for (int i = 0; i < n; i++)
{
x[i] = i * 0.1;
y[i] = Math.Sin(x[i]) + new Random().NextDouble() * 0.2; // 添加噪声
}// 应用Savitzky-Golay滤波
int windowSize = 5; // 窗口大小
int polyOrder = 2; // 多项式阶数
double[] smoothedY = SavitzkyGolay.Smooth(y, windowSize, polyOrder);