Skip to content

Commit fd08b52

Browse files
paulroujanskyPaul ROUJANSKY
andauthored
Do not set annotation channel when missing from input data when reading EDF (#12044)
Co-authored-by: Paul ROUJANSKY <paul.roujansky@bioserenity.com>
1 parent 578f2a9 commit fd08b52

File tree

3 files changed

+93
-19
lines changed

3 files changed

+93
-19
lines changed

doc/changes/devel.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Bugs
5656
- Fix bug with axis clip box boundaries in :func:`mne.viz.plot_evoked_topo` and related functions (:gh:`11999` by `Eric Larson`_)
5757
- Fix bug with ``subject_info`` when loading data from and exporting to EDF file (:gh:`11952` by `Paul Roujansky`_)
5858
- Fix bug with delayed checking of :class:`info["bads"] <mne.Info>` (:gh:`12038` by `Eric Larson`_)
59-
- Fix handling of channel information in annotations when loading data from and exporting to EDF file (:gh:`11960` :gh:`12017` by `Paul Roujansky`_)
59+
- Fix handling of channel information in annotations when loading data from and exporting to EDF file (:gh:`11960` :gh:`12017` :gh:`12044` by `Paul Roujansky`_)
6060
- Add missing ``overwrite`` and ``verbose`` parameters to :meth:`Transform.save() <mne.transforms.Transform.save>` (:gh:`12004` by `Marijn van Vliet`_)
6161
- Fix parsing of eye-link :class:`~mne.Annotations` when ``apply_offsets=False`` is provided to :func:`~mne.io.read_raw_eyelink` (:gh:`12003` by `Mathieu Scheltienne`_)
6262
- Correctly prune channel-specific :class:`~mne.Annotations` when creating :class:`~mne.Epochs` without the channel(s) included in the channel specific annotations (:gh:`12010` by `Mathieu Scheltienne`_)

mne/io/edf/edf.py

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ def __init__(
206206
)
207207
annotations = _read_annotations_edf(
208208
tal_data[0],
209+
ch_names=info["ch_names"],
209210
encoding=encoding,
210211
)
211212
self.set_annotations(annotations, on_missing="warn")
@@ -1892,25 +1893,21 @@ def read_raw_gdf(
18921893

18931894

18941895
@fill_doc
1895-
def _read_annotations_edf(annotations, encoding="utf8"):
1896+
def _read_annotations_edf(annotations, ch_names=None, encoding="utf8"):
18961897
"""Annotation File Reader.
18971898
18981899
Parameters
18991900
----------
19001901
annotations : ndarray (n_chans, n_samples) | str
19011902
Channel data in EDF+ TAL format or path to annotation file.
1903+
ch_names : list of string
1904+
List of channels' names.
19021905
%(encoding_edf)s
19031906
19041907
Returns
19051908
-------
1906-
onset : array of float, shape (n_annotations,)
1907-
The starting time of annotations in seconds after ``orig_time``.
1908-
duration : array of float, shape (n_annotations,)
1909-
Durations of the annotations in seconds.
1910-
description : array of str, shape (n_annotations,)
1911-
Array of strings containing description for each annotation. If a
1912-
string, all the annotations are given the same description. To reject
1913-
epochs, use description starting with keyword 'bad'. See example above.
1909+
annot : instance of Annotations
1910+
The annotations.
19141911
"""
19151912
pat = "([+-]\\d+\\.?\\d*)(\x15(\\d+\\.?\\d*))?(\x14.*?)\x14\x00"
19161913
if isinstance(annotations, str):
@@ -1949,7 +1946,11 @@ def _read_annotations_edf(annotations, encoding="utf8"):
19491946
duration = float(ev[2]) if ev[2] else 0
19501947
for description in ev[3].split("\x14")[1:]:
19511948
if description:
1952-
if "@@" in description:
1949+
if (
1950+
"@@" in description
1951+
and ch_names is not None
1952+
and description.split("@@")[1] in ch_names
1953+
):
19531954
description, ch_name = description.split("@@")
19541955
key = f"{onset}_{duration}_{description}"
19551956
else:
@@ -1979,22 +1980,20 @@ def _read_annotations_edf(annotations, encoding="utf8"):
19791980
offset = -onset
19801981

19811982
if events:
1982-
onset, duration, description, ch_names = zip(*events.values())
1983+
onset, duration, description, annot_ch_names = zip(*events.values())
19831984
else:
1984-
onset, duration, description, ch_names = list(), list(), list(), list()
1985+
onset, duration, description, annot_ch_names = list(), list(), list(), list()
19851986

1986-
assert len(onset) == len(duration) == len(description) == len(ch_names)
1987+
assert len(onset) == len(duration) == len(description) == len(annot_ch_names)
19871988

1988-
annotations = Annotations(
1989+
return Annotations(
19891990
onset=onset,
19901991
duration=duration,
19911992
description=description,
19921993
orig_time=None,
1993-
ch_names=ch_names,
1994+
ch_names=annot_ch_names,
19941995
)
19951996

1996-
return annotations
1997-
19981997

19991998
def _get_annotations_gdf(edf_info, sfreq):
20001999
onset, duration, desc = list(), list(), list()

mne/io/edf/tests/test_edf.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import pytest
2525

2626
from mne import pick_types, Annotations
27-
from mne.annotations import events_from_annotations, read_annotations
27+
from mne.annotations import _ndarray_ch_names, events_from_annotations, read_annotations
2828
from mne.datasets import testing
2929
from mne.io import read_raw_edf, read_raw_bdf, read_raw_fif, edf, read_raw_gdf
3030
from mne.io.tests.test_raw import _test_raw_reader
@@ -504,6 +504,81 @@ def test_read_utf8_annotations():
504504
assert raw.annotations[1]["description"] == "仰卧"
505505

506506

507+
def test_read_annotations_edf(tmp_path):
508+
"""Test reading annotations from EDF file."""
509+
annot = (
510+
b"+1.1\x14Event A@@CH1\x14\x00\x00"
511+
b"+1.2\x14Event A\x14\x00\x00"
512+
b"+1.3\x14Event B@@CH1\x14\x00\x00"
513+
b"+1.3\x14Event B@@CH2\x14\x00\x00"
514+
b"+1.4\x14Event A@@CH3\x14\x00\x00"
515+
b"+1.5\x14Event B\x14\x00\x00"
516+
)
517+
annot_file = tmp_path / "annotations.edf"
518+
with open(annot_file, "wb") as f:
519+
f.write(annot)
520+
521+
# Test reading annotations from channel data
522+
with open(annot_file, "rb") as f:
523+
tal_channel = _read_ch(
524+
f,
525+
subtype="EDF",
526+
dtype="<i2",
527+
samp=-1,
528+
dtype_byte=None,
529+
)
530+
531+
# Read annotations without input channel names: annotations are left untouched and
532+
# assigned as global
533+
annotations = _read_annotations_edf(tal_channel, ch_names=None, encoding="latin1")
534+
assert_allclose(annotations.onset, [1.1, 1.2, 1.3, 1.3, 1.4, 1.5])
535+
assert not any(annotations.duration) # all durations are 0
536+
assert_array_equal(
537+
annotations.description,
538+
[
539+
"Event A@@CH1",
540+
"Event A",
541+
"Event B@@CH1",
542+
"Event B@@CH2",
543+
"Event A@@CH3",
544+
"Event B",
545+
],
546+
)
547+
assert_array_equal(
548+
annotations.ch_names, _ndarray_ch_names([(), (), (), (), (), ()])
549+
)
550+
551+
# Read annotations with complete input channel names: each annotation is parsed and
552+
# associated to a channel
553+
annotations = _read_annotations_edf(
554+
tal_channel, ch_names=["CH1", "CH2", "CH3"], encoding="latin1"
555+
)
556+
assert_allclose(annotations.onset, [1.1, 1.2, 1.3, 1.4, 1.5])
557+
assert not any(annotations.duration) # all durations are 0
558+
assert_array_equal(
559+
annotations.description, ["Event A", "Event A", "Event B", "Event A", "Event B"]
560+
)
561+
assert_array_equal(
562+
annotations.ch_names,
563+
_ndarray_ch_names([("CH1",), (), ("CH1", "CH2"), ("CH3",), ()]),
564+
)
565+
566+
# Read annotations with incomplete input channel names: "CH3" is missing from input
567+
# channels, turning the related annotation into a global one
568+
annotations = _read_annotations_edf(
569+
tal_channel, ch_names=["CH1", "CH2"], encoding="latin1"
570+
)
571+
assert_allclose(annotations.onset, [1.1, 1.2, 1.3, 1.4, 1.5])
572+
assert not any(annotations.duration) # all durations are 0
573+
assert_array_equal(
574+
annotations.description,
575+
["Event A", "Event A", "Event B", "Event A@@CH3", "Event B"],
576+
)
577+
assert_array_equal(
578+
annotations.ch_names, _ndarray_ch_names([("CH1",), (), ("CH1", "CH2"), (), ()])
579+
)
580+
581+
507582
def test_read_latin1_annotations(tmp_path):
508583
"""Test if annotations encoded as Latin-1 can be read.
509584

0 commit comments

Comments
 (0)