mne-tools / mne-lsl

A framework for real-time brain signal streaming with MNE-Python.
https://mne.tools/mne-lsl
BSD 3-Clause "New" or "Revised" License
58 stars 27 forks source link

Add Stream.filter to filter signals from the stream #105

Closed mscheltienne closed 7 months ago

mscheltienne commented 1 year ago

Stream filter API should allow:

stream = StreamLSL(...).connect()
stream.filter(1., 100., picks="eeg")
stream.filter(1., 15., picks="ecg")
stream.notch_filter(np.arange(50, 150, 50), picks="all")

In practice, we have:

The API should support:

mscheltienne commented 8 months ago

@larsoner I would like to start looking into this one, I remember you mentioned scipy functions to efficiently work on ringbuffer. Would you have some references or hints I could look into?

larsoner commented 8 months ago

For realtime applications I would expect an IIR filter with scipy.signal.sosfilt to work reasonably well. Filter a given buffer, save the end filter states, next buffer you filter pass those to sosfilt, etc. and it's the same as filtering one long continuous signal instead of individual buffers. (And sosfilt is more stable than lfilter for higher filter orders.) Here's a tiny snippet I used once:

        if self._zi is None:
            self._zi = sosfilt_zi(sos)[:, :, np.newaxis] * data[:, :1].T
        data[:], self._zi = sosfilt(sos, data, zi=self._zi)
mscheltienne commented 8 months ago

Great, that's similar to what I added a couple of years ago to the current viewer: https://github.com/mne-tools/mne-lsl/blob/95151593579e52f7c5018b4e726939cad9f83718/mne_lsl/stream_viewer/scope/scope_eeg.py#L122-L128


I just re-read the background on filtering tutorial, and IMO, if someone wants to apply 2 different filters to 2 different channels within the Stream, it falls upon him to handle the difference in filter response. Do you agree or do you think we should prevent some combinations/parameters (order, transition bandwidth, ..)?

In the case of 2 different filters applied to 2 different channels, is it possible to combined the sos and zi to then run a single call through sosfilt or should we split those?


Same questions in the case of 2 different filters applied to the same channels, e.g. bandpass (1, 100) Hz + notch (50, 100) Hz, can we combine the coefficients and call sosfilt only once?

larsoner commented 8 months ago

Do people want different filtering on different channels? If they do in principle then I would from the start plan on having N different filters each with a set of picks, coefficients, and filter states. Iterating over the N shouldn't be too painful (N will probably never be greater than 10 or so, and only in the thousands would I expect Python call overhead to start to matter.)

Same questions in the case of 2 different filters applied to the same channels, e.g. bandpass (1, 100) Hz + notch (50, 100) Hz, can we combine the coefficients and call sosfilt only once?

For this you can, yeah. SOS is really a way of turning say a 10th order filter into 5 2nd order filters, then (by linearity) equivalently filtering sequentially rather than all at once, which is more numerically stable. So if you have multiple SOS filters you can just concatenate the coefficients and it should work.