Flatline detection using max/min

A typical EEG channel in my files (ant neuro edf) looks like this:

image

I.e., there is the magic number 32767uV which is, I suppose, the device max sensitivity.
I want to annotate this channel to exclude all the time slots with values close to this border (this is the “flatline” step).
It is relatively easy to do it in Python, but mne.preprocessing.annotate_amplitude does not seem to be able to do it because it looks at peak-to-peak signal amplitude (PTP).

I wonder if I will have to create them myself, or if there is some existing functionality I am missing.

mne.preprocessing.annotate_amplitude with flat parameter is really not able to do the job?

if not I suppose you did to do it manually at this point

Alex

This is true, but the signal does look absolutely flat there. So you may be able to use annotate_amplitude() anyway to achieve the desired result, no?

One more piece of information: 32 767 uV is probably your amplifier clipping its maximum acquisition range. It’s not supposed to happen, and I would suggest you contact your amplifier manufacturer for additional information and improvements on your recording setup.

Some amplifiers support different signal ranges, one solution can be to increase the acquisition range (at the cost of resolution).

To give you an example, we used ANT Neuro amplifiers with touch-proof connectors and intracranial electrodes. The manufacturer cap uses Silver/Silver Chloride electrodes while the intracranial electrodes were stainless steel and platinum electrodes which have a higher DC offset potential. The signal was clipping as well, but not if we increased the signal acquisition range to its maximum (1V).

1 Like

I thought so, but

mne.preprocessing.annotate_amplitude(edf, picks='EEG FC5-Ref', flat=dict(eeg=0.01))

returns

(<Annotations | 0 segments>, ['EEG FC5-Ref'])

indicating that all data in the channel is bad.

I believe by default the channel is marked as bad if the rejection criterion is met for at least 5% of the recording duration. You can adjust this setting to 100% and you should only get annotations and no rejected channels. The parameter is called bad_percent.

1 Like

alas, I get the same (<Annotations | 0 segments>, ['EEG FC5-Ref']) with bad_percent=100.

I suspect that “peak” in “PTP” is defined as a strict extremum, so my long flat lines do not count.

Please take the time to read all the parameters on the function description page. I suspect you are setting the peak argument and not the flat argument.

import numpy as np

from mne import create_info
from mne.io import RawArray
from mne.preprocessing import annotate_amplitude


data = np.random.randn(1, 4096)  # 8 seconds @ 512 Hz
# make some flat segments
data[0, 100:801] = 0.
data[0, 2000:3001] = 0.
# create a raw object
info = create_info(ch_names=["EEG 01"], sfreq=512, ch_types="eeg")
raw = RawArray(data, info)

# find flat
annotations, bads = annotate_amplitude(raw, flat=0., bad_percent=100)
raw.set_annotations(annotations)

# plot
raw.plot(scalings="auto")

Also, make sure that the data is actually exactly flat, or flat=0. will not work and you will have to set flat to a PTP value that suits your data, e.g. flat=1e-8 if the variation within the flat segments do not exceed 1e-8.

wow, I did not realize that my reputation with you is that bad ;-(

here is the copy/paste from my Emacs/ein buffer:

In [84]:
mne.preprocessing.annotate_amplitude(
    edf, picks=edf.ch_names[8], flat=dict(eeg=0.1),
    bad_percent=100, min_duration=0.01, verbose=True)

Finding segments below or above PTP threshold.

Out [84]:
(<Annotations | 0 segments>, ['EEG FC5-Ref'])
time: 42.2 ms (started: 2022-09-07 16:53:32 -04:00)

actually, flat=0 does produce (<Annotations | 505 segments: BAD_flat (505)>, [])!

why didn’t 0.1 work?!

and how do I merge those annotations? I suspect that shorter “good” segments are worthless, so I would rather merge adjacent annotations separated by less than, say, 1-5 secs of good signal.

IOW, I want, instead of min_duration to add a second to the beginning and end of each segment, because of

This is not currently supported with annotate_amplitude(). We do have the “opposite” feature (to make annotations shorter) for annotate_break().

I’m afraid that for now, you’ll have to manually post-process the annotations, changing onset and duration for each annotation.

# %%
import mne

# Create initial annotations
a = mne.Annotations(
    onset=[5, 10, 15, 20],
    duration=[1, 1, 1, 1],
    description=['1', '2', '3', '4']
)

# Alter them
a.onset -= 1     # start 1 second earlier
a.duration += 2  # end 2 - 1 = 1 second later
1 Like

Apologies if it read like that, it was not my intention. I’m glad I was wrong and you were using the correct arguments.

I don’t know, the code snippet below works. Note that I multiplied the data by 10 since randn returns values within (0, 1). Without the multiplication, additional segments annotated with BAD_flat are created where the random value respects the 0.1 threshold for more than min_duration (5 ms by default). The multiplication makes this way more unlikely and improves the example.

import numpy as np

from mne import create_info
from mne.io import RawArray
from mne.preprocessing import annotate_amplitude


data = np.random.randn(1, 4096) * 10  # 8 seconds @ 512 Hz
# make some flat segments
data[0, 100:801] = 0.
data[0, 2000:3001] = 0.
# create a raw object
info = create_info(ch_names=["EEG 01"], sfreq=512, ch_types="eeg")
raw = RawArray(data, info)

# find flat
annotations, bads = annotate_amplitude(raw, flat=0.1, bad_percent=100)
raw.set_annotations(annotations)

# plot
raw.plot(scalings="auto")

And finally, as @richard mentions, for very short good segments, you’ll have to find them yourself by looking at the returned annotations onsets and durations and figuring out which ones are closer than a threshold and should be merged.

1 Like