Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
mscheltienne opened this issue Sep 1, 2023 · 4 comments · Fixed by #218
Closed

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

mscheltienne opened this issue Sep 1, 2023 · 4 comments · Fixed by #218
Labels
🌟 enhancement Proposed improvement or feature request. stream Issues related to the Stream API.
Milestone

Comments

@mscheltienne
Copy link
Member

mscheltienne commented Sep 1, 2023

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:

  • a ringbuffer of shape (n_samples, n_channels), already processed (re-referenced, filter, ..)
  • a new chunk of data of shape (n_samples, n_channels), raw

The API should support:

  • different filters applied to different channels -> how do we handle the delay introduced by the different filters?
  • more than one filter applied to a given channel -> does the order matter?
@mscheltienne mscheltienne added 🌟 enhancement Proposed improvement or feature request. stream Issues related to the Stream API. labels Sep 1, 2023
@mscheltienne mscheltienne added this to the 1.1 milestone Sep 1, 2023
@mscheltienne mscheltienne modified the milestones: 1.1, 1.2 Nov 20, 2023
@mscheltienne mscheltienne modified the milestones: 1.2, 1.3 Jan 24, 2024
@mscheltienne
Copy link
Member Author

@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
Copy link
Member

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
Copy link
Member Author

Great, that's similar to what I added a couple of years ago to the current viewer:

if self._zi is None:
logger.debug("Initialize ZI coefficient for BP.")
# Multiply by DC offset
self._zi = self._zi_coeff * np.mean(self._data_acquired, axis=0)
self._data_acquired, self._zi = sosfilt(
self._sos, self._data_acquired, 0, self._zi
)


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
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🌟 enhancement Proposed improvement or feature request. stream Issues related to the Stream API.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants