diff --git a/mne/annotations.py b/mne/annotations.py index 2e3d01af628..81ee78b7294 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -1143,7 +1143,7 @@ def _write_annotations_txt(fname, annot): @fill_doc def read_annotations( - fname, sfreq="auto", uint16_codec=None, encoding="utf8" + fname, sfreq="auto", uint16_codec=None, encoding="utf8", ignore_marker_types=False ) -> Annotations: r"""Read annotations from a file. @@ -1174,6 +1174,9 @@ def read_annotations( arrays and can therefore help you solve this problem. %(encoding_edf)s Only used when reading EDF annotations. + ignore_marker_types : bool + If ``True``, ignore marker types in BrainVision files (and only use their + descriptions). Defaults to ``False``. Returns ------- @@ -1212,7 +1215,9 @@ def read_annotations( annotations = _read_annotations_txt(fname) elif name.endswith(("vmrk", "amrk")): - annotations = _read_annotations_brainvision(fname, sfreq=sfreq) + annotations = _read_annotations_brainvision( + fname, sfreq=sfreq, ignore_marker_types=ignore_marker_types + ) elif name.endswith("csv"): annotations = _read_annotations_csv(fname) diff --git a/mne/io/brainvision/brainvision.py b/mne/io/brainvision/brainvision.py index 3a95f424f3e..58eeb945c69 100644 --- a/mne/io/brainvision/brainvision.py +++ b/mne/io/brainvision/brainvision.py @@ -50,6 +50,11 @@ class RawBrainVision(BaseRaw): scale : float The scaling factor for EEG data. Unless specified otherwise by header file, units are in microvolts. Default scale factor is 1. + ignore_marker_types : bool + If ``True``, ignore marker types and only use marker descriptions. Default is + ``False``. + + .. versionadded:: 1.8 %(preload)s %(verbose)s @@ -58,6 +63,15 @@ class RawBrainVision(BaseRaw): impedances : dict A dictionary of all electrodes and their impedances. + Notes + ----- + BrainVision markers consist of a type and a description (in addition to other fields + like onset and duration). In contrast, annotations in MNE only have a description. + Therefore, a BrainVision marker of type "Stimulus" and description "S 1" will be + converted to an annotation "Stimulus/S 1" by default. If you want to ignore the + type and instead only use the description, set ``ignore_marker_types=True``, which + will convert the same marker to an annotation "S 1". + See Also -------- mne.io.Raw : Documentation of attributes and methods. @@ -72,6 +86,7 @@ def __init__( eog=("HEOGL", "HEOGR", "VEOGb"), misc="auto", scale=1.0, + ignore_marker_types=False, preload=False, verbose=None, ): # noqa: D107 @@ -129,7 +144,9 @@ def __init__( self.impedances = _parse_impedance(split_settings, self.info["meas_date"]) # Get annotations from marker file - annots = read_annotations(mrk_fname, info["sfreq"]) + annots = read_annotations( + mrk_fname, info["sfreq"], ignore_marker_types=ignore_marker_types + ) self.set_annotations(annots) # Drop the fake ahdr channel if needed @@ -207,13 +224,15 @@ def _read_segments_c(raw, data, idx, fi, start, stop, cals, mult): _mult_cal_one(data, block, idx, cals, mult) -def _read_mrk(fname): +def _read_mrk(fname, ignore_marker_types=False): """Read annotations from a vmrk/amrk file. Parameters ---------- fname : str vmrk/amrk file to be read. + ignore_marker_types : bool + If True, ignore marker types and only use marker descriptions. Default is False. Returns ------- @@ -293,36 +312,41 @@ def _read_mrk(fname): this_duration = int(this_duration) if this_duration.isdigit() else 0 duration.append(this_duration) onset.append(int(this_onset) - 1) # BV is 1-indexed, not 0-indexed - description.append(mtype + "/" + mdesc) + if not ignore_marker_types: + description.append(mtype + "/" + mdesc) + else: + description.append(mdesc) return np.array(onset), np.array(duration), np.array(description), date_str -def _read_annotations_brainvision(fname, sfreq="auto"): +def _read_annotations_brainvision(fname, sfreq="auto", ignore_marker_types=False): """Create Annotations from BrainVision vmrk/amrk. - This function reads a .vmrk or .amrk file and makes an - :class:`mne.Annotations` object. + This function reads a .vmrk or .amrk file and creates an :class:`mne.Annotations` + object. Parameters ---------- fname : str | object The path to the .vmrk/.amrk file. sfreq : float | 'auto' - The sampling frequency in the file. It's necessary - as Annotations are expressed in seconds and vmrk/amrk - files are in samples. If set to 'auto' then - the sfreq is taken from the .vhdr/.ahdr file that - has the same name (without file extension). So - data.vmrk/amrk looks for sfreq in data.vhdr or, - if it does not exist, in data.ahdr. + The sampling frequency in the file. This is necessary because Annotations are + expressed in seconds and vmrk/amrk files are in samples. If set to 'auto' then + the sfreq is taken from the .vhdr/.ahdr file with the same name (without file + extension). So data.vmrk/amrk looks for sfreq in data.vhdr or, if it does not + exist, in data.ahdr. + ignore_marker_types : bool + If True, ignore marker types and only use marker descriptions. Default is False. Returns ------- annotations : instance of Annotations The annotations present in the file. """ - onset, duration, description, date_str = _read_mrk(fname) + onset, duration, description, date_str = _read_mrk( + fname, ignore_marker_types=ignore_marker_types + ) orig_time = _str_to_meas_date(date_str) if sfreq == "auto": @@ -919,6 +943,7 @@ def read_raw_brainvision( eog=("HEOGL", "HEOGR", "VEOGb"), misc="auto", scale=1.0, + ignore_marker_types=False, preload=False, verbose=None, ) -> RawBrainVision: @@ -940,6 +965,9 @@ def read_raw_brainvision( scale : float The scaling factor for EEG data. Unless specified otherwise by header file, units are in microvolts. Default scale factor is 1. + ignore_marker_types : bool + If ``True``, ignore marker types and only use marker descriptions. Default is + ``False``. %(preload)s %(verbose)s @@ -949,6 +977,15 @@ def read_raw_brainvision( A Raw object containing BrainVision data. See :class:`mne.io.Raw` for documentation of attributes and methods. + Notes + ----- + BrainVision markers consist of a type and a description (in addition to other fields + like onset and duration). In contrast, annotations in MNE only have a description. + Therefore, a BrainVision marker of type "Stimulus" and description "S 1" will be + converted to an annotation "Stimulus/S 1" by default. If you want to ignore the + type and instead only use the description, set ``ignore_marker_types=True``, which + will convert the same marker to an annotation "S 1". + See Also -------- mne.io.Raw : Documentation of attributes and methods of RawBrainVision. @@ -958,6 +995,7 @@ def read_raw_brainvision( eog=eog, misc=misc, scale=scale, + ignore_marker_types=ignore_marker_types, preload=preload, verbose=verbose, ) diff --git a/mne/io/brainvision/tests/test_brainvision.py b/mne/io/brainvision/tests/test_brainvision.py index 51e63fa082c..f580f05b526 100644 --- a/mne/io/brainvision/tests/test_brainvision.py +++ b/mne/io/brainvision/tests/test_brainvision.py @@ -661,6 +661,49 @@ def test_read_vmrk_annotations(tmp_path): read_annotations(fname, sfreq=sfreq) +def test_ignore_marker_types(): + """Test ignore marker types.""" + # default behavior (do not ignore marker types) + raw = read_raw_brainvision(vhdr_path) + expected_descriptions = [ + "New Segment/", + "Stimulus/S253", + "Stimulus/S255", + "Event/254", + "Stimulus/S255", + "Event/254", + "Stimulus/S255", + "Stimulus/S253", + "Stimulus/S255", + "Response/R255", + "Event/254", + "Stimulus/S255", + "SyncStatus/Sync On", + "Optic/O 1", + ] + assert_array_equal(raw.annotations.description, expected_descriptions) + + # ignore marker types + raw = read_raw_brainvision(vhdr_path, ignore_marker_types=True) + expected_descriptions = [ + "", + "S253", + "S255", + "254", + "S255", + "254", + "S255", + "S253", + "S255", + "R255", + "254", + "S255", + "Sync On", + "O 1", + ] + assert_array_equal(raw.annotations.description, expected_descriptions) + + @testing.requires_testing_data def test_read_vhdr_annotations_and_events(tmp_path): """Test load brainvision annotations and parse them to events."""