如何使用Java实现低通滤波器

33

我正在尝试在Java中实现一个低通滤波器。我的要求非常简单,我需要消除特定频率(单一维度)之外的信号。似乎Butterworth滤波器很适合我的需求。

现在重要的是CPU时间应该尽可能地少。滤波器将处理接近一百万个样本,我们的用户不喜欢等待太长时间。是否有任何现成的Butterworth滤波器实现具有最优算法进行过滤?


3
Audacity是开源的,包含许多音频过滤器。它们将用C/C++编写,但将其翻译成等效的Java代码相当简单。 - Mark Peters
也许您可以展示一些代码,这样我们就知道您想要过滤什么? - Romain Hippeau
我这里有一个教程,其中包括二阶Butterworth滤波器。在Java中实现应该很容易:http://blog.bjornroche.com/2012/08/basic-audio-eqs.html - Bjorn Roche
7个回答

47

我有一个页面描述了一种非常简单、非常低CPU的低通滤波器,也能够适应帧速率的变化。我用它来平滑用户输入,并经常用于绘制帧速率图表。

http://phrogz.net/js/framerate-independent-low-pass-filter.html

简而言之,在您的更新循环中:

// If you have a fixed frame rate
smoothedValue += (newValue - smoothedValue) / smoothing

// If you have a varying frame rate
smoothedValue += timeSinceLastUpdate * (newValue - smoothedValue) / smoothing

smoothing值为1时,不会发生平滑处理,而较高的值会越来越平滑结果。

该页面有几个用JavaScript编写的函数,但公式与语言无关。


4
喜欢这个算法,但有没有办法将平滑值转换为截止频率?谢谢。 - Lorenzo

8

这里是一个使用Apache Math库中的傅里叶变换的低通滤波器。

    public double[] fourierLowPassFilter(double[] data, double lowPass, double frequency){
    //data: input data, must be spaced equally in time.
    //lowPass: The cutoff frequency at which 
    //frequency: The frequency of the input data.

    //The apache Fft (Fast Fourier Transform) accepts arrays that are powers of 2.
    int minPowerOf2 = 1;
    while(minPowerOf2 < data.length)
        minPowerOf2 = 2 * minPowerOf2;

    //pad with zeros
    double[] padded = new double[minPowerOf2];
    for(int i = 0; i < data.length; i++)
        padded[i] = data[i];


    FastFourierTransformer transformer = new FastFourierTransformer(DftNormalization.STANDARD);
    Complex[] fourierTransform = transformer.transform(padded, TransformType.FORWARD);

    //build the frequency domain array
    double[] frequencyDomain = new double[fourierTransform.length];
    for(int i = 0; i < frequencyDomain.length; i++)
        frequencyDomain[i] = frequency * i / (double)fourierTransform.length;

    //build the classifier array, 2s are kept and 0s do not pass the filter
    double[] keepPoints = new double[frequencyDomain.length];
    keepPoints[0] = 1; 
    for(int i = 1; i < frequencyDomain.length; i++){
        if(frequencyDomain[i] < lowPass)
            keepPoints[i] = 2;
        else
            keepPoints[i] = 0;
    }

    //filter the fft
    for(int i = 0; i < fourierTransform.length; i++)
        fourierTransform[i] = fourierTransform[i].multiply((double)keepPoints[i]);

    //invert back to time domain
    Complex[] reverseFourier = transformer.transform(fourierTransform, TransformType.INVERSE);

    //get the real part of the reverse 
    double[] result = new double[data.length];
    for(int i = 0; i< result.length; i++){
        result[i] = reverseFourier[i].getReal();
    }

    return result;
}

6
滤波器设计是一种权衡的艺术,要做好这个工作,你需要考虑一些细节。最大必须通过"没有多大"衰减的频率是多少,"没有多大"的最大值是多少?必须弱化的最小频率是多少,"很多"的最小值是多少?在滤波器应该通过的频率范围内,可接受多少波动(即衰减变化)?你有多种选择,它们会消耗不同数量的计算资源。像matlab或scilab这样的程序可以帮助您比较权衡。您需要熟悉将频率表示为采样率的十进制分数以及线性和对数(分贝)衰减测量之间的概念。例如,"完美"低通滤波器在频域中是矩形的。在时间域中,表达为脉冲响应,那将是一个sinc函数(sin x/x),其尾部延伸到正负无限。显然,您无法计算出这个函数,因此问题变成了如果您将sinc函数近似为您可以计算的有限持续时间,那么这会降低您的滤波器的多少?或者,如果您想要一个非常便宜易于计算的有限脉冲响应滤波器,您可以使用"box car"或矩形滤波器,其中所有系数都是1。但是在时间上是矩形的滤波器在频率上看起来像一个sinc函数 - 它具有通过带中的sin x/x下降(通常升高到某个幂次,因为您通常会有多级版本),以及一些"反弹"在阻止带中。尽管如此,在某些情况下,它仍然是有用的,无论是单独使用还是在另一种类型的滤波器之后使用。

5
我最近设计了一个简单的Butterworth函数(http://baumdevblog.blogspot.com/2010/11/butterworth-lowpass-filter-coefficients.html)。它们在Java中很容易编码,如果你问我,应该足够快(你只需要将filter(double * samples,int count)更改为filter(double [] samples,int count),我猜测)。
JNI的问题在于它会导致平台不兼容性,可能会混淆热点编译器,并且代码中的JNI方法调用可能仍然会减慢速度。因此,我建议尝试Java并查看它是否足够快。
在某些情况下,先使用快速傅里叶变换,然后在频域中应用滤波可能会有益,但我怀疑对于简单的低通滤波器,这不会比每个样本约6次乘法和几次加法更快。

2
我不会尝试使用傅里叶方法(正如OP所建议的那样)来过滤一百万个数据点:http://blog.bjornroche.com/2012/08/why-eq-is-done-in-time-domain.html - Bjorn Roche

2

正如Mark Peters在评论中所说:需要过滤大量数据的过滤器应该用C或C++编写。但是你仍然可以利用Java。只需查看Java Native Interface (JNI)即可。由于C/C++编译为本机机器代码,它将比在Java虚拟机(JVM)中运行字节码要快得多,JVM实际上是一个虚拟处理器,将字节码转换为本地机器的本机代码(取决于CPU指令集,如x86、x64、ARM等)。


23
在你重写基准测试之前,你可能会惊讶于两者之间的差异并没有你想象得那么大。在许多情况下,Java 实际上比 C/C++ 更快。 - Romain Hippeau

2

我知道这是一个老问题,但我想补充一些之前似乎没有提到的内容。

首先,你应该意识到:

  1. 有几种不同类型的(数字)滤波器。
  2. 有几种不同的设计滤波器的方法。
  3. 有几种不同的滤波器实现。

当你需要在应用程序中使用滤波器时,你需要选择某种类型的滤波器,选择特定的滤波器设计方法,应用该方法找到满足你约束条件的滤波器系数,最后将这些系数复制到你的滤波器实现中。

选择滤波器类型和应用设计方法是你一次性完成的,使用适当的软件,如Matlab、Octave或Scilab。这可能涉及一些试验和观察滤波器获得的特性,如频率响应(幅度和相位)和/或冲激响应,以查看它们是否符合你的规格。一旦你确定了解决方案,你将有一组常数系数。这些数字,或这些数字的某些线性组合,是你需要复制到你的程序(Java或其他)中作为常数表的全部内容。

在您的程序中,您只需要一个应用某些滤波器实现的函数,该函数使用这些系数对输入流样本(以及可能的先前输出样本)进行线性组合,以在每个时间点产生新的输出样本。
我假设您可能会对有限冲激响应(FIR)或无限冲激响应(IIR)的线性时不变滤波器感兴趣。这两个子类之间的设计方法不同。Butterworth、Chebyshev、椭圆滤波器仅是为设计IIR滤波器而使用的不同技术(或优化)的结果。它们可以通过小阶数实现非常好的频率响应,这意味着您需要很少的系数,因此在实现中每个样本需要较少的乘法/加法运算。
但是,如果您真的想要线性相位响应,则需要FIR滤波器。这些具有不同的设计技术,并且通常需要更高的阶数以获得类似的频率特性。使用快速傅里叶变换有有效的实现方法,但我怀疑您是否需要这样的东西。
可能的不同滤波器实现主要在于数值稳定性。除非您使用非常低精度的算术和/或相当奇特的系数,否则您可能不会注意到差异。我相信Matlab/Octave滤波器函数使用“直接型II”实现,这非常简单。您肯定可以在DSP书籍或网络上找到描述。
João Manuel Rodrigues

2

我从http://www.dspguide.com/采用了这个。我对Java还很陌生,所以代码可能不太好看,但它可以工作。

/*
* To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package SoundCruncher;

import java.util.ArrayList;

/**
 *
 * @author 2sloth
 * filter routine from "The scientist and engineer's guide to DSP" Chapter 20
 * filterOrder can be any even number between 2 & 20


* cutoffFreq must be smaller than half the samplerate
 * filterType: 0=lowPass   1=highPass
 * ripplePercent is amount of ripple in Chebyshev filter (0-29) (0=butterworth)
 */
public class Filtering {
    double[] filterSignal(ArrayList<Float> signal, double sampleRate ,double cutoffFreq, double filterOrder, int filterType, double ripplePercent) {
        double[][] recursionCoefficients =   new double[22][2];
        // Generate double array for ease of coding
        double[] unfilteredSignal =   new double[signal.size()];
        for (int i=0; i<signal.size(); i++) {
            unfilteredSignal[i] =   signal.get(i);
        }

        double cutoffFraction   =   cutoffFreq/sampleRate;  // convert cut-off frequency to fraction of sample rate
        System.out.println("Filtering: cutoffFraction: " + cutoffFraction);
        //ButterworthFilter(0.4,6,ButterworthFilter.Type highPass);
        double[] coeffA =   new double[22]; //a coeffs
        double[] coeffB =   new double[22]; //b coeffs
        double[] tA =   new double[22];
        double[] tB =   new double[22];

        coeffA[2]   =   1;
        coeffB[2]   =   1;

        // calling subroutine
        for (int i=1; i<filterOrder/2; i++) {
            double[] filterParameters   =   MakeFilterParameters(cutoffFraction, filterType, ripplePercent, filterOrder, i);

            for (int j=0; j<coeffA.length; j++){
                tA[j]   =   coeffA[j];
                tB[j]   =   coeffB[j];
            }
            for (int j=2; j<coeffA.length; j++){
                coeffA[j]   =   filterParameters[0]*tA[j]+filterParameters[1]*tA[j-1]+filterParameters[2]*tA[j-2];
                coeffB[j]   =   tB[j]-filterParameters[3]*tB[j-1]-filterParameters[4]*tB[j-2];
            }
        }
        coeffB[2]   =   0;
        for (int i=0; i<20; i++){
            coeffA[i]   =   coeffA[i+2];
            coeffB[i]   =   -coeffB[i+2];
        }

        // adjusting coeffA and coeffB for high/low pass filter
        double sA   =   0;
        double sB   =   0;
        for (int i=0; i<20; i++){
            if (filterType==0) sA   =   sA+coeffA[i];
            if (filterType==0) sB   =   sB+coeffB[i];
            if (filterType==1) sA   =   sA+coeffA[i]*Math.pow(-1,i);
            if (filterType==1) sB   =   sB+coeffA[i]*Math.pow(-1,i);
        }

        // applying gain
        double gain =   sA/(1-sB);
        for (int i=0; i<20; i++){
            coeffA[i]   =   coeffA[i]/gain;
        }
        for (int i=0; i<22; i++){
            recursionCoefficients[i][0] =   coeffA[i];
            recursionCoefficients[i][1] =   coeffB[i];
        }
        double[] filteredSignal =   new double[signal.size()];
        double filterSampleA    =   0;
        double filterSampleB    =   0;

        // loop for applying recursive filter 
        for (int i= (int) Math.round(filterOrder); i<signal.size(); i++){
            for(int j=0; j<filterOrder+1; j++) {
                filterSampleA    =   filterSampleA+coeffA[j]*unfilteredSignal[i-j];
            }
            for(int j=1; j<filterOrder+1; j++) {
                filterSampleB    =   filterSampleB+coeffB[j]*filteredSignal[i-j];
            }
            filteredSignal[i]   =   filterSampleA+filterSampleB;
            filterSampleA   =   0;
            filterSampleB   =   0;
        }


        return filteredSignal;

    }
    /*  pi=3.14... 
        cutoffFreq=fraction of samplerate, default 0.4  FC
        filterType: 0=LowPass   1=HighPass              LH
        rippleP=ripple procent 0-29                     PR
        iterateOver=1 to poles/2                        P%
    */
    // subroutine called from "filterSignal" method
    double[] MakeFilterParameters(double cutoffFraction, int filterType, double rippleP, double numberOfPoles, int iteration) {
        double rp   =   -Math.cos(Math.PI/(numberOfPoles*2)+(iteration-1)*(Math.PI/numberOfPoles));
        double ip   =   Math.sin(Math.PI/(numberOfPoles*2)+(iteration-1)*Math.PI/numberOfPoles);
        System.out.println("MakeFilterParameters: ripplP:");
            System.out.println("cutoffFraction  filterType  rippleP  numberOfPoles  iteration");
            System.out.println(cutoffFraction + "   " + filterType + "   " + rippleP + "   " + numberOfPoles + "   " + iteration);
        if (rippleP != 0){
            double es   =   Math.sqrt(Math.pow(100/(100-rippleP),2)-1);
//            double vx1  =   1/numberOfPoles;
//            double vx2  =   1/Math.pow(es,2)+1;
//            double vx3  =   (1/es)+Math.sqrt(vx2);
//            System.out.println("VX's: ");
//            System.out.println(vx1 + "   " + vx2 + "   " + vx3);
//            double vx   =   vx1*Math.log(vx3);
            double vx   =   (1/numberOfPoles)*Math.log((1/es)+Math.sqrt((1/Math.pow(es,2))+1));
            double kx   =   (1/numberOfPoles)*Math.log((1/es)+Math.sqrt((1/Math.pow(es,2))-1));
            kx  =   (Math.exp(kx)+Math.exp(-kx))/2;
            rp  =   rp*((Math.exp(vx)-Math.exp(-vx))/2)/kx;
            ip  =   ip*((Math.exp(vx)+Math.exp(-vx))/2)/kx;
            System.out.println("MakeFilterParameters (rippleP!=0):");
            System.out.println("es  vx  kx  rp  ip");
            System.out.println(es + "   " + vx*100 + "   " + kx + "   " + rp + "   " + ip);
        }

        double t    =   2*Math.tan(0.5);
        double w    =   2*Math.PI*cutoffFraction;
        double m    =   Math.pow(rp, 2)+Math.pow(ip,2);
        double d    =   4-4*rp*t+m*Math.pow(t,2);
        double x0   =   Math.pow(t,2)/d;
        double x1   =   2*Math.pow(t,2)/d;
        double x2   =   Math.pow(t,2)/d;
        double y1   =   (8-2*m*Math.pow(t,2))/d;
        double y2   =   (-4-4*rp*t-m*Math.pow(t,2))/d;
        double k    =   0;
        if (filterType==1) {
            k =   -Math.cos(w/2+0.5)/Math.cos(w/2-0.5);
        }
        if (filterType==0) {
            k =   -Math.sin(0.5-w/2)/Math.sin(w/2+0.5);
        }
        d   =   1+y1*k-y2*Math.pow(k,2);
        double[] filterParameters   =   new double[5];
        filterParameters[0] =   (x0-x1*k+x2*Math.pow(k,2))/d;           //a0
        filterParameters[1] =   (-2*x0*k+x1+x1*Math.pow(k,2)-2*x2*k)/d; //a1
        filterParameters[2] =   (x0*Math.pow(k,2)-x1*k+x2)/d;           //a2
        filterParameters[3] =   (2*k+y1+y1*Math.pow(k,2)-2*y2*k)/d;     //b1
        filterParameters[4] =   (-(Math.pow(k,2))-y1*k+y2)/d;           //b2
        if (filterType==1) {
            filterParameters[1] =   -filterParameters[1];
            filterParameters[3] =   -filterParameters[3];
        }
//        for (double number: filterParameters){
//            System.out.println("MakeFilterParameters: " + number);
//        }


        return filterParameters;
    }


}

过滤器的顺序是什么?@2sloth - Abdul Rab Memon
过滤器顺序是该方法的参数之一,因此您可以选择任何顺序 :) - Rasmus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接