diff --git a/doc/_includes/dig_formats.rst b/doc/_includes/dig_formats.rst index 9354b873ade..8a6074a1a8f 100644 --- a/doc/_includes/dig_formats.rst +++ b/doc/_includes/dig_formats.rst @@ -1,4 +1,5 @@ :orphan: +.. _dig-formats: Supported formats for digitized 3D locations ============================================ @@ -32,6 +33,8 @@ EGI .xml :func:`mne.channels.read_dig_egi` MNE-C .hpts :func:`mne.channels.read_dig_hpts` Brain Products .bvct :func:`mne.channels.read_dig_captrack` + +Compumedics .dat :func:`mne.channels.read_dig_dat` ================= ================ ============================================== To load Polhemus FastSCAN files you can use diff --git a/doc/changes/latest.inc b/doc/changes/latest.inc index 7f31196711d..ebb19fa58bb 100644 --- a/doc/changes/latest.inc +++ b/doc/changes/latest.inc @@ -32,6 +32,8 @@ Changelog - Add reader for NIRx data in :func:`mne.io.read_raw_nirx` by `Robert Luke`_ +- Add reader for ``*.dat`` electrode position files :func:`mne.channels.read_dig_dat` by `Christian Brodbeck`_ + - For KIT systems without built-in layout, :func:`mne.channels.find_layout` now falls back on an automatically generated layout, by `Christian Brodbeck`_ Bug diff --git a/doc/python_reference.rst b/doc/python_reference.rst index e0a6566365e..bdbf1c93a16 100644 --- a/doc/python_reference.rst +++ b/doc/python_reference.rst @@ -312,6 +312,7 @@ Projections: make_dig_montage read_dig_polhemus_isotrak read_dig_captrack + read_dig_dat read_dig_egi read_dig_fif read_dig_hpts diff --git a/mne/channels/__init__.py b/mne/channels/__init__.py index 20f4894773c..d82cf523a8a 100644 --- a/mne/channels/__init__.py +++ b/mne/channels/__init__.py @@ -6,7 +6,7 @@ from .layout import (Layout, make_eeg_layout, make_grid_layout, read_layout, find_layout, generate_2d_layout) from .montage import (DigMontage, - get_builtin_montages, make_dig_montage, + get_builtin_montages, make_dig_montage, read_dig_dat, read_dig_egi, read_dig_captrack, read_dig_fif, read_dig_polhemus_isotrak, read_polhemus_fastscan, compute_dev_head_t, make_standard_montage, @@ -25,10 +25,10 @@ 'make_standard_montage', # Readers - 'read_ch_connectivity', 'read_dig_captrack', 'read_dig_egi', - 'read_dig_fif', 'read_dig_montage', 'read_dig_polhemus_isotrak', - 'read_layout', 'read_montage', 'read_polhemus_fastscan', - 'read_custom_montage', 'read_dig_hpts', + 'read_ch_connectivity', 'read_dig_captrack', 'read_dig_dat', + 'read_dig_egi', 'read_dig_fif', 'read_dig_montage', + 'read_dig_polhemus_isotrak', 'read_layout', 'read_montage', + 'read_polhemus_fastscan', 'read_custom_montage', 'read_dig_hpts', # Helpers 'rename_channels', 'make_1020_channel_selections', diff --git a/mne/channels/montage.py b/mne/channels/montage.py index 6d4eda8000a..c63e13de7f2 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -178,6 +178,7 @@ class DigMontage(object): See Also -------- read_dig_captrack + read_dig_dat read_dig_egi read_dig_fif read_dig_hpts @@ -359,6 +360,71 @@ def transform_to_head(montage): return montage +def read_dig_dat(fname): + r"""Read electrode positions from a ``*.dat`` file. + + .. Warning:: + This function was implemented based on ``*.dat`` files available from + `Compumedics `_ and might not work as expected with novel + files. If it does not read your files correctly please contact the + mne-python developers. + + Parameters + ---------- + fname : path-like + File from which to read electrode locations. + + Returns + ------- + montage : DigMontage + The montage. + + See Also + -------- + read_dig_captrack + read_dig_dat + read_dig_egi + read_dig_fif + read_dig_hpts + read_dig_polhemus_isotrak + make_dig_montage + + Notes + ----- + ``*.dat`` files are plain text files and can be inspected and amended with + a plain text editor. + """ + fname = _check_fname(fname, overwrite='read', must_exist=True) + + with open(fname, 'r') as fid: + lines = fid.readlines() + + electrodes = {} + nasion = lpa = rpa = None + for i, line in enumerate(lines): + items = line.split() + if not items: + continue + elif len(items) != 5: + raise ValueError( + "Error reading %s, line %s has unexpected number of entries:\n" + "%s" % (fname, i, line.rstrip())) + num = items[1] + if num == '67': + continue # centroid + pos = np.array([float(item) for item in items[2:]]) + if num == '78': + nasion = pos + elif num == '76': + lpa = pos + elif num == '82': + rpa = pos + else: + electrodes[items[0]] = pos + return make_dig_montage(electrodes, nasion, lpa, rpa) + + def read_dig_fif(fname): r"""Read digitized points from a .fif file. @@ -379,6 +445,7 @@ def read_dig_fif(fname): See Also -------- DigMontage + read_dig_dat read_dig_egi read_dig_captrack read_dig_polhemus_isotrak @@ -419,6 +486,7 @@ def read_dig_hpts(fname, unit='mm'): -------- DigMontage read_dig_captrack + read_dig_dat read_dig_egi read_dig_fif read_dig_polhemus_isotrak @@ -508,6 +576,7 @@ def read_dig_egi(fname): -------- DigMontage read_dig_captrack + read_dig_dat read_dig_fif read_dig_hpts read_dig_polhemus_isotrak @@ -546,6 +615,7 @@ def read_dig_captrack(fname): See Also -------- DigMontage + read_dig_dat read_dig_egi read_dig_fif read_dig_hpts @@ -792,6 +862,7 @@ def read_dig_polhemus_isotrak(fname, ch_names=None, unit='m'): make_dig_montage read_polhemus_fastscan read_dig_captrack + read_dig_dat read_dig_egi read_dig_fif """ @@ -1045,9 +1116,9 @@ def make_standard_montage(kind, head_size=HEAD_SIZE_DEFAULT): Notes ----- Individualized (digitized) electrode positions should be read in using - :func:`read_dig_captrack`, :func:`read_dig_egi`, :func:`read_dig_fif`, - :func:`read_dig_polhemus_isotrak`, :func:`read_dig_hpts` or made with - :func:`make_dig_montage`. + :func:`read_dig_captrack`, :func:`read_dig_dat`, :func:`read_dig_egi`, + :func:`read_dig_fif`, :func:`read_dig_polhemus_isotrak`, + :func:`read_dig_hpts` or made with :func:`make_dig_montage`. Valid ``kind`` arguments are: diff --git a/mne/channels/tests/test_montage.py b/mne/channels/tests/test_montage.py index 7ead6d8b1bc..bce8588cfb9 100644 --- a/mne/channels/tests/test_montage.py +++ b/mne/channels/tests/test_montage.py @@ -3,6 +3,7 @@ # # License: BSD (3-clause) +from itertools import chain import os import os.path as op @@ -17,7 +18,7 @@ from mne import __file__ as _mne_file, create_info, read_evokeds from mne.utils._testing import _dig_sort_key -from mne.channels import (get_builtin_montages, DigMontage, +from mne.channels import (get_builtin_montages, DigMontage, read_dig_dat, read_dig_egi, read_dig_captrack, read_dig_fif, make_standard_montage, read_custom_montage, compute_dev_head_t, make_dig_montage, @@ -346,6 +347,47 @@ def test_read_locs(): ) +def test_read_dig_dat(): + """Test reading *.dat electrode locations.""" + rows = [ + ['Nasion', 78, 0.00, 1.00, 0.00], + ['Left', 76, -1.00, 0.00, 0.00], + ['Right', 82, 1.00, -0.00, 0.00], + ['O2', 69, -0.50, -0.90, 0.05], + ['Centroid', 67, 0.00, 0.00, 0.00], + ] + # write mock test.dat file + temp_dir = _TempDir() + fname_temp = op.join(temp_dir, 'test.dat') + with open(fname_temp, 'w') as fid: + for row in rows: + name = row[0].rjust(10) + data = '\t'.join(map(str, row[1:])) + fid.write("%s\t%s\n" % (name, data)) + # construct expected value + idents = { + 78: FIFF.FIFFV_POINT_NASION, + 76: FIFF.FIFFV_POINT_LPA, + 82: FIFF.FIFFV_POINT_RPA, + 69: 1, + } + kinds = { + 78: FIFF.FIFFV_POINT_CARDINAL, + 76: FIFF.FIFFV_POINT_CARDINAL, + 82: FIFF.FIFFV_POINT_CARDINAL, + 69: FIFF.FIFFV_POINT_EEG, + } + target = {row[0]: {'r': row[2:], 'ident': idents[row[1]], + 'kind': kinds[row[1]], 'coord_frame': 0} + for row in rows[:-1]} + # read it + dig = read_dig_dat(fname_temp) + assert set(dig.ch_names) == {'O2'} + keys = chain(['Left', 'Nasion', 'Right'], dig.ch_names) + target = [target[k] for k in keys] + assert dig.dig == target + + def test_read_dig_montage_using_polhemus_fastscan(): """Test FastScan.""" N_EEG_CH = 10 diff --git a/mne/io/cnt/cnt.py b/mne/io/cnt/cnt.py index b033ba8ab62..131cdfc40ef 100644 --- a/mne/io/cnt/cnt.py +++ b/mne/io/cnt/cnt.py @@ -98,16 +98,23 @@ def read_raw_cnt(input_fname, eog=(), misc=(), ecg=(), """Read CNT data as raw object. .. Note:: - If montage is not provided, the x and y coordinates are read from the - file header. Channels that are not assigned with keywords ``eog``, - ``ecg``, ``emg`` and ``misc`` are assigned as eeg channels. All the eeg - channel locations are fit to a sphere when computing the z-coordinates - for the channels. If channels assigned as eeg channels have locations + 2d spatial coordinates (x, y) for EEG channels are read from the file + header and fit to a sphere to compute corresponding z-coordinates. + If channels assigned as EEG channels have locations far away from the head (i.e. x and y coordinates don't fit to a - sphere), all the channel locations will be distorted. If you are not + sphere), all the channel locations will be distorted + (all channels that are not assigned with keywords ``eog``, ``ecg``, + ``emg`` and ``misc`` are assigned as EEG channels). If you are not sure that the channel locations in the header are correct, it is - probably safer to use a (standard) montage. See - :func:`mne.channels.make_standard_montage` + probably safer to replace them with :meth:`mne.io.Raw.set_montage`. + Montages can be created/imported with: + + - Standard montages with :func:`mne.channels.make_standard_montage` + - Montages for `Compumedics systems `_ with + :func:`mne.channels.read_dig_dat` + - Other reader functions are listed under *See Also* at + :class:`mne.channels.DigMontage` Parameters ---------- diff --git a/tutorials/intro/plot_40_sensor_locations.py b/tutorials/intro/plot_40_sensor_locations.py index 992ef44686a..34ba7bc8737 100644 --- a/tutorials/intro/plot_40_sensor_locations.py +++ b/tutorials/intro/plot_40_sensor_locations.py @@ -173,12 +173,8 @@ # ` — this is because the sensor positions in that dataset are # digitizations of the sensor positions on an actual subject's head. Depending # on what system was used to scan the positions one can use different -# reading functions (:func:`mne.channels.read_dig_captrack` for -# a CapTrak Brain Products system, :func:`mne.channels.read_dig_egi` -# for an EGI system, :func:`mne.channels.read_dig_polhemus_isotrak` for -# Polhemus ISOTRAK, :func:`mne.channels.read_dig_fif` to read from -# a `.fif` file or :func:`mne.channels.read_dig_hpts` to read MNE `.hpts` -# files. The read :class:`montage ` can then be added +# reading functions (see :ref:`dig-formats`). +# The read :class:`montage ` can then be added # to :class:`~mne.io.Raw` objects with the :meth:`~mne.io.Raw.set_montage` # method; in the sample data this was done prior to saving the # :class:`~mne.io.Raw` object to disk, so the sensor positions are already