/mne/channels/montage.py

http://github.com/mne-tools/mne-python · Python · 1573 lines · 1262 code · 67 blank · 244 comment · 68 complexity · 20374d123779c161d54064af888537a4 MD5 · raw file

Large files are truncated click here to view the full file

  1. # Authors: Alexandre Gramfort <alexandre.gramfort@inria.fr>
  2. # Denis Engemann <denis.engemann@gmail.com>
  3. # Martin Luessi <mluessi@nmr.mgh.harvard.edu>
  4. # Eric Larson <larson.eric.d@gmail.com>
  5. # Marijn van Vliet <w.m.vanvliet@gmail.com>
  6. # Jona Sassenhagen <jona.sassenhagen@gmail.com>
  7. # Teon Brooks <teon.brooks@gmail.com>
  8. # Christian Brodbeck <christianbrodbeck@nyu.edu>
  9. # Stefan Appelhoff <stefan.appelhoff@mailbox.org>
  10. # Joan Massich <mailsik@gmail.com>
  11. #
  12. # License: Simplified BSD
  13. from collections import OrderedDict
  14. from copy import deepcopy
  15. import os.path as op
  16. import re
  17. import numpy as np
  18. from ..defaults import HEAD_SIZE_DEFAULT
  19. from .._freesurfer import get_mni_fiducials
  20. from ..viz import plot_montage
  21. from ..transforms import (apply_trans, get_ras_to_neuromag_trans, _sph_to_cart,
  22. _topo_to_sph, _frame_to_str, Transform,
  23. _verbose_frames, _fit_matched_points,
  24. _quat_to_affine, _ensure_trans)
  25. from ..io._digitization import (_count_points_by_type,
  26. _get_dig_eeg, _make_dig_points, write_dig,
  27. _read_dig_fif, _format_dig_points,
  28. _get_fid_coords, _coord_frame_const,
  29. _get_data_as_dict_from_dig)
  30. from ..io.meas_info import create_info
  31. from ..io.open import fiff_open
  32. from ..io.pick import pick_types, _picks_to_idx, channel_type
  33. from ..io.constants import FIFF, CHANNEL_LOC_ALIASES
  34. from ..utils import (warn, copy_function_doc_to_method_doc, _pl, verbose,
  35. _check_option, _validate_type, _check_fname, _on_missing,
  36. fill_doc, _docdict)
  37. from ._dig_montage_utils import _read_dig_montage_egi
  38. from ._dig_montage_utils import _parse_brainvision_dig_montage
  39. _BUILT_IN_MONTAGES = [
  40. 'EGI_256',
  41. 'GSN-HydroCel-128', 'GSN-HydroCel-129', 'GSN-HydroCel-256',
  42. 'GSN-HydroCel-257', 'GSN-HydroCel-32', 'GSN-HydroCel-64_1.0',
  43. 'GSN-HydroCel-65_1.0',
  44. 'biosemi128', 'biosemi16', 'biosemi160', 'biosemi256',
  45. 'biosemi32', 'biosemi64',
  46. 'easycap-M1', 'easycap-M10',
  47. 'mgh60', 'mgh70',
  48. 'standard_1005', 'standard_1020', 'standard_alphabetic',
  49. 'standard_postfixed', 'standard_prefixed', 'standard_primed',
  50. 'artinis-octamon', 'artinis-brite23'
  51. ]
  52. def _check_get_coord_frame(dig):
  53. dig_coord_frames = sorted(set(d['coord_frame'] for d in dig))
  54. if len(dig_coord_frames) != 1:
  55. raise RuntimeError(
  56. 'Only a single coordinate frame in dig is supported, got '
  57. f'{dig_coord_frames}')
  58. return _frame_to_str[dig_coord_frames.pop()] if dig_coord_frames else None
  59. def get_builtin_montages():
  60. """Get a list of all builtin montages.
  61. Returns
  62. -------
  63. montages : list
  64. Names of all builtin montages that can be used by
  65. :func:`make_standard_montage`.
  66. """
  67. return _BUILT_IN_MONTAGES
  68. def make_dig_montage(ch_pos=None, nasion=None, lpa=None, rpa=None,
  69. hsp=None, hpi=None, coord_frame='unknown'):
  70. r"""Make montage from arrays.
  71. Parameters
  72. ----------
  73. ch_pos : dict | None
  74. Dictionary of channel positions. Keys are channel names and values
  75. are 3D coordinates - array of shape (3,) - in native digitizer space
  76. in m.
  77. nasion : None | array, shape (3,)
  78. The position of the nasion fiducial point.
  79. This point is assumed to be in the native digitizer space in m.
  80. lpa : None | array, shape (3,)
  81. The position of the left periauricular fiducial point.
  82. This point is assumed to be in the native digitizer space in m.
  83. rpa : None | array, shape (3,)
  84. The position of the right periauricular fiducial point.
  85. This point is assumed to be in the native digitizer space in m.
  86. hsp : None | array, shape (n_points, 3)
  87. This corresponds to an array of positions of the headshape points in
  88. 3d. These points are assumed to be in the native digitizer space in m.
  89. hpi : None | array, shape (n_hpi, 3)
  90. This corresponds to an array of HPI points in the native digitizer
  91. space. They only necessary if computation of a ``compute_dev_head_t``
  92. is True.
  93. coord_frame : str
  94. The coordinate frame of the points. Usually this is "unknown"
  95. for native digitizer space.
  96. Returns
  97. -------
  98. montage : instance of DigMontage
  99. The montage.
  100. See Also
  101. --------
  102. DigMontage
  103. read_dig_captrak
  104. read_dig_egi
  105. read_dig_fif
  106. read_dig_localite
  107. read_dig_polhemus_isotrak
  108. Notes
  109. -----
  110. Valid ``coord_frame`` arguments are 'meg', 'mri', 'mri_voxel', 'head',
  111. 'mri_tal', 'ras', 'fs_tal', 'ctf_head', 'ctf_meg', 'unknown'. For custom
  112. montages without fiducials this parameter has to be set to 'head'.
  113. """
  114. _validate_type(ch_pos, (dict, None), 'ch_pos')
  115. if ch_pos is None:
  116. ch_names = None
  117. else:
  118. ch_names = list(ch_pos)
  119. dig = _make_dig_points(
  120. nasion=nasion, lpa=lpa, rpa=rpa, hpi=hpi, extra_points=hsp,
  121. dig_ch_pos=ch_pos, coord_frame=coord_frame
  122. )
  123. return DigMontage(dig=dig, ch_names=ch_names)
  124. class DigMontage(object):
  125. """Montage for digitized electrode and headshape position data.
  126. .. warning:: Montages are typically created using one of the helper
  127. functions in the ``See Also`` section below instead of
  128. instantiating this class directly.
  129. Parameters
  130. ----------
  131. dig : list of dict
  132. The object containing all the dig points.
  133. ch_names : list of str
  134. The names of the EEG channels.
  135. See Also
  136. --------
  137. read_dig_captrak
  138. read_dig_dat
  139. read_dig_egi
  140. read_dig_fif
  141. read_dig_hpts
  142. read_dig_localite
  143. read_dig_polhemus_isotrak
  144. make_dig_montage
  145. Notes
  146. -----
  147. .. versionadded:: 0.9.0
  148. """
  149. def __init__(self, *, dig=None, ch_names=None):
  150. dig = list() if dig is None else dig
  151. _validate_type(item=dig, types=list, item_name='dig')
  152. ch_names = list() if ch_names is None else ch_names
  153. n_eeg = sum([1 for d in dig if d['kind'] == FIFF.FIFFV_POINT_EEG])
  154. if n_eeg != len(ch_names):
  155. raise ValueError(
  156. 'The number of EEG channels (%d) does not match the number'
  157. ' of channel names provided (%d)' % (n_eeg, len(ch_names))
  158. )
  159. self.dig = dig
  160. self.ch_names = ch_names
  161. def __repr__(self):
  162. """Return string representation."""
  163. n_points = _count_points_by_type(self.dig)
  164. return ('<DigMontage | {extra:d} extras (headshape), {hpi:d} HPIs,'
  165. ' {fid:d} fiducials, {eeg:d} channels>').format(**n_points)
  166. @copy_function_doc_to_method_doc(plot_montage)
  167. def plot(self, scale_factor=20, show_names=True, kind='topomap', show=True,
  168. sphere=None, verbose=None):
  169. return plot_montage(self, scale_factor=scale_factor,
  170. show_names=show_names, kind=kind, show=show,
  171. sphere=sphere)
  172. @fill_doc
  173. def rename_channels(self, mapping, allow_duplicates=False):
  174. """Rename the channels.
  175. Parameters
  176. ----------
  177. %(rename_channels_mapping_duplicates)s
  178. Returns
  179. -------
  180. inst : instance of DigMontage
  181. The instance. Operates in-place.
  182. """
  183. from .channels import rename_channels
  184. temp_info = create_info(list(self._get_ch_pos()), 1000., 'eeg')
  185. rename_channels(temp_info, mapping, allow_duplicates)
  186. self.ch_names = temp_info['ch_names']
  187. def save(self, fname):
  188. """Save digitization points to FIF.
  189. Parameters
  190. ----------
  191. fname : str
  192. The filename to use. Should end in .fif or .fif.gz.
  193. """
  194. coord_frame = _check_get_coord_frame(self.dig)
  195. write_dig(fname, self.dig, coord_frame)
  196. def __iadd__(self, other):
  197. """Add two DigMontages in place.
  198. Notes
  199. -----
  200. Two DigMontages can only be added if there are no duplicated ch_names
  201. and if fiducials are present they should share the same coordinate
  202. system and location values.
  203. """
  204. def is_fid_defined(fid):
  205. return not(
  206. fid.nasion is None and fid.lpa is None and fid.rpa is None
  207. )
  208. # Check for none duplicated ch_names
  209. ch_names_intersection = set(self.ch_names).intersection(other.ch_names)
  210. if ch_names_intersection:
  211. raise RuntimeError((
  212. "Cannot add two DigMontage objects if they contain duplicated"
  213. " channel names. Duplicated channel(s) found: {}."
  214. ).format(
  215. ', '.join(['%r' % v for v in sorted(ch_names_intersection)])
  216. ))
  217. # Check for unique matching fiducials
  218. self_fid, self_coord = _get_fid_coords(self.dig)
  219. other_fid, other_coord = _get_fid_coords(other.dig)
  220. if is_fid_defined(self_fid) and is_fid_defined(other_fid):
  221. if self_coord != other_coord:
  222. raise RuntimeError('Cannot add two DigMontage objects if '
  223. 'fiducial locations are not in the same '
  224. 'coordinate system.')
  225. for kk in self_fid:
  226. if not np.array_equal(self_fid[kk], other_fid[kk]):
  227. raise RuntimeError('Cannot add two DigMontage objects if '
  228. 'fiducial locations do not match '
  229. '(%s)' % kk)
  230. # keep self
  231. self.dig = _format_dig_points(
  232. self.dig + [d for d in other.dig
  233. if d['kind'] != FIFF.FIFFV_POINT_CARDINAL]
  234. )
  235. else:
  236. self.dig = _format_dig_points(self.dig + other.dig)
  237. self.ch_names += other.ch_names
  238. return self
  239. def copy(self):
  240. """Copy the DigMontage object.
  241. Returns
  242. -------
  243. dig : instance of DigMontage
  244. The copied DigMontage instance.
  245. """
  246. return deepcopy(self)
  247. def __add__(self, other):
  248. """Add two DigMontages."""
  249. out = self.copy()
  250. out += other
  251. return out
  252. def _get_ch_pos(self):
  253. pos = [d['r'] for d in _get_dig_eeg(self.dig)]
  254. assert len(self.ch_names) == len(pos)
  255. return OrderedDict(zip(self.ch_names, pos))
  256. def _get_dig_names(self):
  257. NAMED_KIND = (FIFF.FIFFV_POINT_EEG,)
  258. is_eeg = np.array([d['kind'] in NAMED_KIND for d in self.dig])
  259. assert len(self.ch_names) == is_eeg.sum()
  260. dig_names = [None] * len(self.dig)
  261. for ch_name_idx, dig_idx in enumerate(np.where(is_eeg)[0]):
  262. dig_names[dig_idx] = self.ch_names[ch_name_idx]
  263. return dig_names
  264. def get_positions(self):
  265. """Get all channel and fiducial positions.
  266. Returns
  267. -------
  268. positions : dict
  269. A dictionary of the positions for channels (``ch_pos``),
  270. coordinate frame (``coord_frame``), nasion (``nasion``),
  271. left preauricular point (``lpa``),
  272. right preauricular point (``rpa``),
  273. Head Shape Polhemus (``hsp``), and
  274. Head Position Indicator(``hpi``).
  275. E.g.::
  276. {
  277. 'ch_pos': {'EEG061': [0, 0, 0]},
  278. 'nasion': [0, 0, 1],
  279. 'coord_frame': 'mni_tal',
  280. 'lpa': [0, 1, 0],
  281. 'rpa': [1, 0, 0],
  282. 'hsp': None,
  283. 'hpi': None
  284. }
  285. """
  286. # get channel positions as dict
  287. ch_pos = self._get_ch_pos()
  288. # get coordframe and fiducial coordinates
  289. montage_bunch = _get_data_as_dict_from_dig(self.dig)
  290. coord_frame = _frame_to_str.get(montage_bunch.coord_frame)
  291. # return dictionary
  292. positions = dict(
  293. ch_pos=ch_pos,
  294. coord_frame=coord_frame,
  295. nasion=montage_bunch.nasion,
  296. lpa=montage_bunch.lpa,
  297. rpa=montage_bunch.rpa,
  298. hsp=montage_bunch.hsp,
  299. hpi=montage_bunch.hpi,
  300. )
  301. return positions
  302. @verbose
  303. def apply_trans(self, trans, verbose=None):
  304. """Apply a transformation matrix to the montage.
  305. Parameters
  306. ----------
  307. trans : instance of mne.transforms.Transform
  308. The transformation matrix to be applied.
  309. %(verbose)s
  310. """
  311. _validate_type(trans, Transform, 'trans')
  312. coord_frame = self.get_positions()['coord_frame']
  313. trans = _ensure_trans(trans, fro=coord_frame, to=trans['to'])
  314. for d in self.dig:
  315. d['r'] = apply_trans(trans, d['r'])
  316. d['coord_frame'] = trans['to']
  317. @verbose
  318. def add_estimated_fiducials(self, subject, subjects_dir=None,
  319. verbose=None):
  320. """Estimate fiducials based on FreeSurfer ``fsaverage`` subject.
  321. This takes a montage with the ``mri`` coordinate frame,
  322. corresponding to the FreeSurfer RAS (xyz in the volume) T1w
  323. image of the specific subject. It will call
  324. :func:`mne.coreg.get_mni_fiducials` to estimate LPA, RPA and
  325. Nasion fiducial points.
  326. Parameters
  327. ----------
  328. %(subject)s
  329. %(subjects_dir)s
  330. %(verbose)s
  331. Returns
  332. -------
  333. inst : instance of DigMontage
  334. The instance, modified in-place.
  335. See Also
  336. --------
  337. :ref:`tut-source-alignment`
  338. Notes
  339. -----
  340. Since MNE uses the FIF data structure, it relies on the ``head``
  341. coordinate frame. Any coordinate frame can be transformed
  342. to ``head`` if the fiducials (i.e. LPA, RPA and Nasion) are
  343. defined. One can use this function to estimate those fiducials
  344. and then use ``montage.get_native_head_t()`` to get the
  345. head <-> MRI transform.
  346. """
  347. # get coordframe and fiducial coordinates
  348. montage_bunch = _get_data_as_dict_from_dig(self.dig)
  349. # get the coordinate frame and check that it's MRI
  350. if montage_bunch.coord_frame != FIFF.FIFFV_COORD_MRI:
  351. raise RuntimeError(
  352. f'Montage should be in the "mri" coordinate frame '
  353. f'to use `add_estimated_fiducials`. The current coordinate '
  354. f'frame is {montage_bunch.coord_frame}')
  355. # estimate LPA, nasion, RPA from FreeSurfer fsaverage
  356. fids_mri = list(get_mni_fiducials(subject, subjects_dir))
  357. # add those digpoints to front of montage
  358. self.dig = fids_mri + self.dig
  359. return self
  360. @verbose
  361. def add_mni_fiducials(self, subjects_dir=None, verbose=None):
  362. """Add fiducials to a montage in MNI space.
  363. Parameters
  364. ----------
  365. %(subjects_dir)s
  366. %(verbose)s
  367. Returns
  368. -------
  369. inst : instance of DigMontage
  370. The instance, modified in-place.
  371. Notes
  372. -----
  373. ``fsaverage`` is in MNI space and so its fiducials can be
  374. added to a montage in "mni_tal". MNI is an ACPC-aligned
  375. coordinate system (the posterior commissure is the origin)
  376. so since BIDS requires channel locations for ECoG, sEEG and
  377. DBS to be in ACPC space, this function can be used to allow
  378. those coordinate to be transformed to "head" space (origin
  379. between LPA and RPA).
  380. """
  381. montage_bunch = _get_data_as_dict_from_dig(self.dig)
  382. # get the coordinate frame and check that it's MNI TAL
  383. if montage_bunch.coord_frame != FIFF.FIFFV_MNE_COORD_MNI_TAL:
  384. raise RuntimeError(
  385. f'Montage should be in the "mni_tal" coordinate frame '
  386. f'to use `add_estimated_fiducials`. The current coordinate '
  387. f'frame is {montage_bunch.coord_frame}')
  388. fids_mni = get_mni_fiducials('fsaverage', subjects_dir)
  389. for fid in fids_mni:
  390. # "mri" and "mni_tal" are equivalent for fsaverage
  391. assert fid['coord_frame'] == FIFF.FIFFV_COORD_MRI
  392. fid['coord_frame'] = FIFF.FIFFV_MNE_COORD_MNI_TAL
  393. self.dig = fids_mni + self.dig
  394. return self
  395. @verbose
  396. def remove_fiducials(self, verbose=None):
  397. """Remove the fiducial points from a montage.
  398. Parameters
  399. ----------
  400. %(verbose)s
  401. Returns
  402. -------
  403. inst : instance of DigMontage
  404. The instance, modified in-place.
  405. Notes
  406. -----
  407. MNE will transform a montage to the internal "head" coordinate
  408. frame if the fiducials are present. Under most circumstances, this
  409. is ideal as it standardizes the coordinate frame for things like
  410. plotting. However, in some circumstances, such as saving a ``raw``
  411. with intracranial data to BIDS format, the coordinate frame
  412. should not be changed by removing fiducials.
  413. """
  414. for d in self.dig.copy():
  415. if d['kind'] == FIFF.FIFFV_POINT_CARDINAL:
  416. self.dig.remove(d)
  417. return self
  418. VALID_SCALES = dict(mm=1e-3, cm=1e-2, m=1)
  419. def _check_unit_and_get_scaling(unit):
  420. _check_option('unit', unit, sorted(VALID_SCALES.keys()))
  421. return VALID_SCALES[unit]
  422. def transform_to_head(montage):
  423. """Transform a DigMontage object into head coordinate.
  424. It requires that the LPA, RPA and Nasion fiducial
  425. point are available. It requires that all fiducial
  426. points are in the same coordinate e.g. 'unknown'
  427. and it will convert all the point in this coordinate
  428. system to Neuromag head coordinate system.
  429. Parameters
  430. ----------
  431. montage : instance of DigMontage
  432. The montage.
  433. Returns
  434. -------
  435. montage : instance of DigMontage
  436. The montage after transforming the points to head
  437. coordinate system.
  438. """
  439. # Get fiducial points and their coord_frame
  440. native_head_t = compute_native_head_t(montage)
  441. montage = montage.copy() # to avoid inplace modification
  442. if native_head_t['from'] != FIFF.FIFFV_COORD_HEAD:
  443. for d in montage.dig:
  444. if d['coord_frame'] == native_head_t['from']:
  445. d['r'] = apply_trans(native_head_t, d['r'])
  446. d['coord_frame'] = FIFF.FIFFV_COORD_HEAD
  447. return montage
  448. def read_dig_dat(fname):
  449. r"""Read electrode positions from a ``*.dat`` file.
  450. .. Warning::
  451. This function was implemented based on ``*.dat`` files available from
  452. `Compumedics <https://compumedicsneuroscan.com/scan-acquire-
  453. configuration-files/>`__ and might not work as expected with novel
  454. files. If it does not read your files correctly please contact the
  455. mne-python developers.
  456. Parameters
  457. ----------
  458. fname : path-like
  459. File from which to read electrode locations.
  460. Returns
  461. -------
  462. montage : DigMontage
  463. The montage.
  464. See Also
  465. --------
  466. read_dig_captrak
  467. read_dig_dat
  468. read_dig_egi
  469. read_dig_fif
  470. read_dig_hpts
  471. read_dig_localite
  472. read_dig_polhemus_isotrak
  473. make_dig_montage
  474. Notes
  475. -----
  476. ``*.dat`` files are plain text files and can be inspected and amended with
  477. a plain text editor.
  478. """
  479. from ._standard_montage_utils import _check_dupes_odict
  480. fname = _check_fname(fname, overwrite='read', must_exist=True)
  481. with open(fname, 'r') as fid:
  482. lines = fid.readlines()
  483. ch_names, poss = list(), list()
  484. nasion = lpa = rpa = None
  485. for i, line in enumerate(lines):
  486. items = line.split()
  487. if not items:
  488. continue
  489. elif len(items) != 5:
  490. raise ValueError(
  491. "Error reading %s, line %s has unexpected number of entries:\n"
  492. "%s" % (fname, i, line.rstrip()))
  493. num = items[1]
  494. if num == '67':
  495. continue # centroid
  496. pos = np.array([float(item) for item in items[2:]])
  497. if num == '78':
  498. nasion = pos
  499. elif num == '76':
  500. lpa = pos
  501. elif num == '82':
  502. rpa = pos
  503. else:
  504. ch_names.append(items[0])
  505. poss.append(pos)
  506. electrodes = _check_dupes_odict(ch_names, poss)
  507. return make_dig_montage(electrodes, nasion, lpa, rpa)
  508. def read_dig_fif(fname):
  509. r"""Read digitized points from a .fif file.
  510. Note that electrode names are not present in the .fif file so
  511. they are here defined with the convention from VectorView
  512. systems (EEG001, EEG002, etc.)
  513. Parameters
  514. ----------
  515. fname : path-like
  516. FIF file from which to read digitization locations.
  517. Returns
  518. -------
  519. montage : instance of DigMontage
  520. The montage.
  521. See Also
  522. --------
  523. DigMontage
  524. read_dig_dat
  525. read_dig_egi
  526. read_dig_captrak
  527. read_dig_polhemus_isotrak
  528. read_dig_hpts
  529. read_dig_localite
  530. make_dig_montage
  531. """
  532. _check_fname(fname, overwrite='read', must_exist=True)
  533. # Load the dig data
  534. f, tree = fiff_open(fname)[:2]
  535. with f as fid:
  536. dig = _read_dig_fif(fid, tree)
  537. ch_names = []
  538. for d in dig:
  539. if d['kind'] == FIFF.FIFFV_POINT_EEG:
  540. ch_names.append('EEG%03d' % d['ident'])
  541. montage = DigMontage(dig=dig, ch_names=ch_names)
  542. return montage
  543. def read_dig_hpts(fname, unit='mm'):
  544. """Read historical .hpts mne-c files.
  545. Parameters
  546. ----------
  547. fname : str
  548. The filepath of .hpts file.
  549. unit : 'm' | 'cm' | 'mm'
  550. Unit of the positions. Defaults to 'mm'.
  551. Returns
  552. -------
  553. montage : instance of DigMontage
  554. The montage.
  555. See Also
  556. --------
  557. DigMontage
  558. read_dig_captrak
  559. read_dig_dat
  560. read_dig_egi
  561. read_dig_fif
  562. read_dig_localite
  563. read_dig_polhemus_isotrak
  564. make_dig_montage
  565. Notes
  566. -----
  567. The hpts format digitzer data file may contain comment lines starting
  568. with the pound sign (#) and data lines of the form::
  569. <*category*> <*identifier*> <*x/mm*> <*y/mm*> <*z/mm*>
  570. where:
  571. ``<*category*>``
  572. defines the type of points. Allowed categories are: ``hpi``,
  573. ``cardinal`` (fiducial), ``eeg``, and ``extra`` corresponding to
  574. head-position indicator coil locations, cardinal landmarks, EEG
  575. electrode locations, and additional head surface points,
  576. respectively.
  577. ``<*identifier*>``
  578. identifies the point. The identifiers are usually sequential
  579. numbers. For cardinal landmarks, 1 = left auricular point,
  580. 2 = nasion, and 3 = right auricular point. For EEG electrodes,
  581. identifier = 0 signifies the reference electrode.
  582. ``<*x/mm*> , <*y/mm*> , <*z/mm*>``
  583. Location of the point, usually in the head coordinate system
  584. in millimeters. If your points are in [m] then unit parameter can
  585. be changed.
  586. For example::
  587. cardinal 2 -5.6729 -12.3873 -30.3671
  588. cardinal 1 -37.6782 -10.4957 91.5228
  589. cardinal 3 -131.3127 9.3976 -22.2363
  590. hpi 1 -30.4493 -11.8450 83.3601
  591. hpi 2 -122.5353 9.2232 -28.6828
  592. hpi 3 -6.8518 -47.0697 -37.0829
  593. hpi 4 7.3744 -50.6297 -12.1376
  594. hpi 5 -33.4264 -43.7352 -57.7756
  595. eeg FP1 3.8676 -77.0439 -13.0212
  596. eeg FP2 -31.9297 -70.6852 -57.4881
  597. eeg F7 -6.1042 -68.2969 45.4939
  598. ...
  599. """
  600. from ._standard_montage_utils import _str_names, _str
  601. _scale = _check_unit_and_get_scaling(unit)
  602. out = np.genfromtxt(fname, comments='#',
  603. dtype=(_str, _str, 'f8', 'f8', 'f8'))
  604. kind, label = _str_names(out['f0']), _str_names(out['f1'])
  605. kind = [k.lower() for k in kind]
  606. xyz = np.array([out['f%d' % ii] for ii in range(2, 5)]).T
  607. xyz *= _scale
  608. del _scale
  609. fid_idx_to_label = {'1': 'lpa', '2': 'nasion', '3': 'rpa'}
  610. fid = {fid_idx_to_label[label[ii]]: this_xyz
  611. for ii, this_xyz in enumerate(xyz) if kind[ii] == 'cardinal'}
  612. ch_pos = {label[ii]: this_xyz
  613. for ii, this_xyz in enumerate(xyz) if kind[ii] == 'eeg'}
  614. hpi = np.array([this_xyz for ii, this_xyz in enumerate(xyz)
  615. if kind[ii] == 'hpi'])
  616. hpi.shape = (-1, 3) # in case it's empty
  617. hsp = np.array([this_xyz for ii, this_xyz in enumerate(xyz)
  618. if kind[ii] == 'extra'])
  619. hsp.shape = (-1, 3) # in case it's empty
  620. return make_dig_montage(ch_pos=ch_pos, **fid, hpi=hpi, hsp=hsp)
  621. def read_dig_egi(fname):
  622. """Read electrode locations from EGI system.
  623. Parameters
  624. ----------
  625. fname : path-like
  626. EGI MFF XML coordinates file from which to read digitization locations.
  627. Returns
  628. -------
  629. montage : instance of DigMontage
  630. The montage.
  631. See Also
  632. --------
  633. DigMontage
  634. read_dig_captrak
  635. read_dig_dat
  636. read_dig_fif
  637. read_dig_hpts
  638. read_dig_localite
  639. read_dig_polhemus_isotrak
  640. make_dig_montage
  641. """
  642. _check_fname(fname, overwrite='read', must_exist=True)
  643. data = _read_dig_montage_egi(
  644. fname=fname,
  645. _scaling=1.,
  646. _all_data_kwargs_are_none=True
  647. )
  648. return make_dig_montage(**data)
  649. def read_dig_captrak(fname):
  650. """Read electrode locations from CapTrak Brain Products system.
  651. Parameters
  652. ----------
  653. fname : path-like
  654. BrainVision CapTrak coordinates file from which to read EEG electrode
  655. locations. This is typically in XML format with the .bvct extension.
  656. Returns
  657. -------
  658. montage : instance of DigMontage
  659. The montage.
  660. See Also
  661. --------
  662. DigMontage
  663. read_dig_dat
  664. read_dig_egi
  665. read_dig_fif
  666. read_dig_hpts
  667. read_dig_localite
  668. read_dig_polhemus_isotrak
  669. make_dig_montage
  670. """
  671. _check_fname(fname, overwrite='read', must_exist=True)
  672. data = _parse_brainvision_dig_montage(fname, scale=1e-3)
  673. return make_dig_montage(**data)
  674. def read_dig_localite(fname, nasion=None, lpa=None, rpa=None):
  675. """Read Localite .csv file.
  676. Parameters
  677. ----------
  678. fname : path-like
  679. File name.
  680. nasion : str | None
  681. Name of nasion fiducial point.
  682. lpa : str | None
  683. Name of left preauricular fiducial point.
  684. rpa : str | None
  685. Name of right preauricular fiducial point.
  686. Returns
  687. -------
  688. montage : instance of DigMontage
  689. The montage.
  690. See Also
  691. --------
  692. DigMontage
  693. read_dig_captrak
  694. read_dig_dat
  695. read_dig_egi
  696. read_dig_fif
  697. read_dig_hpts
  698. read_dig_polhemus_isotrak
  699. make_dig_montage
  700. """
  701. ch_pos = {}
  702. with open(fname) as f:
  703. f.readline() # skip first row
  704. for row in f:
  705. _, name, x, y, z = row.split(",")
  706. ch_pos[name] = np.array((float(x), float(y), float(z))) / 1000
  707. if nasion is not None:
  708. nasion = ch_pos.pop(nasion)
  709. if lpa is not None:
  710. lpa = ch_pos.pop(lpa)
  711. if rpa is not None:
  712. rpa = ch_pos.pop(rpa)
  713. return make_dig_montage(ch_pos, nasion, lpa, rpa)
  714. def _get_montage_in_head(montage):
  715. coords = set([d['coord_frame'] for d in montage.dig])
  716. if len(coords) == 1 and coords.pop() == FIFF.FIFFV_COORD_HEAD:
  717. return montage
  718. else:
  719. return transform_to_head(montage.copy())
  720. def _set_montage_fnirs(info, montage):
  721. """Set the montage for fNIRS data.
  722. This needs to be different to electrodes as each channel has three
  723. coordinates that need to be set. For each channel there is a source optode
  724. location, a detector optode location, and a channel midpoint that must be
  725. stored. This function modifies info['chs'][#]['loc'] and info['dig'] in
  726. place.
  727. """
  728. from ..preprocessing.nirs import _validate_nirs_info
  729. # Validate that the fNIRS info is correctly formatted
  730. picks = _validate_nirs_info(info)
  731. # Modify info['chs'][#]['loc'] in place
  732. num_ficiduals = len(montage.dig) - len(montage.ch_names)
  733. for ch_idx in picks:
  734. ch = info['chs'][ch_idx]['ch_name']
  735. source, detector = ch.split(' ')[0].split('_')
  736. source_pos = montage.dig[montage.ch_names.index(source)
  737. + num_ficiduals]['r']
  738. detector_pos = montage.dig[montage.ch_names.index(detector)
  739. + num_ficiduals]['r']
  740. info['chs'][ch_idx]['loc'][3:6] = source_pos
  741. info['chs'][ch_idx]['loc'][6:9] = detector_pos
  742. midpoint = (source_pos + detector_pos) / 2
  743. info['chs'][ch_idx]['loc'][:3] = midpoint
  744. info['chs'][ch_idx]['coord_frame'] = FIFF.FIFFV_COORD_HEAD
  745. # Modify info['dig'] in place
  746. with info._unlock():
  747. info['dig'] = montage.dig
  748. @fill_doc
  749. def _set_montage(info, montage, match_case=True, match_alias=False,
  750. on_missing='raise'):
  751. """Apply montage to data.
  752. With a DigMontage, this function will replace the digitizer info with
  753. the values specified for the particular montage.
  754. Usually, a montage is expected to contain the positions of all EEG
  755. electrodes and a warning is raised when this is not the case.
  756. Parameters
  757. ----------
  758. %(info_not_none)s
  759. %(montage)s
  760. %(match_case)s
  761. %(match_alias)s
  762. %(on_missing_montage)s
  763. Notes
  764. -----
  765. This function will change the info variable in place.
  766. """
  767. _validate_type(montage, (DigMontage, None, str), 'montage')
  768. if montage is None:
  769. # Next line modifies info['dig'] in place
  770. with info._unlock():
  771. info['dig'] = None
  772. for ch in info['chs']:
  773. # Next line modifies info['chs'][#]['loc'] in place
  774. ch['loc'] = np.full(12, np.nan)
  775. return
  776. if isinstance(montage, str): # load builtin montage
  777. _check_option('montage', montage, _BUILT_IN_MONTAGES)
  778. montage = make_standard_montage(montage)
  779. mnt_head = _get_montage_in_head(montage)
  780. del montage
  781. def _backcompat_value(pos, ref_pos):
  782. if any(np.isnan(pos)):
  783. return np.full(6, np.nan)
  784. else:
  785. return np.concatenate((pos, ref_pos))
  786. # get the channels in the montage in head
  787. ch_pos = mnt_head._get_ch_pos()
  788. # only get the eeg, seeg, dbs, ecog channels
  789. picks = pick_types(
  790. info, meg=False, eeg=True, seeg=True, dbs=True, ecog=True,
  791. exclude=())
  792. non_picks = np.setdiff1d(np.arange(info['nchan']), picks)
  793. # get the reference position from the loc[3:6]
  794. chs = [info['chs'][ii] for ii in picks]
  795. non_names = [info['chs'][ii]['ch_name'] for ii in non_picks]
  796. del picks
  797. ref_pos = [ch['loc'][3:6] for ch in chs]
  798. # keep reference location from EEG-like channels if they
  799. # already exist and are all the same.
  800. custom_eeg_ref_dig = False
  801. # Note: ref position is an empty list for fieldtrip data
  802. if ref_pos:
  803. if all([np.equal(ref_pos[0], pos).all() for pos in ref_pos]) \
  804. and not np.equal(ref_pos[0], [0, 0, 0]).all():
  805. eeg_ref_pos = ref_pos[0]
  806. # since we have an EEG reference position, we have
  807. # to add it into the info['dig'] as EEG000
  808. custom_eeg_ref_dig = True
  809. if not custom_eeg_ref_dig:
  810. refs = set(ch_pos) & {'EEG000', 'REF'}
  811. assert len(refs) <= 1
  812. eeg_ref_pos = np.zeros(3) if not(refs) else ch_pos.pop(refs.pop())
  813. # This raises based on info being subset/superset of montage
  814. info_names = [ch['ch_name'] for ch in chs]
  815. dig_names = mnt_head._get_dig_names()
  816. ref_names = [None, 'EEG000', 'REF']
  817. if match_case:
  818. info_names_use = info_names
  819. dig_names_use = dig_names
  820. non_names_use = non_names
  821. else:
  822. ch_pos_use = OrderedDict(
  823. (name.lower(), pos) for name, pos in ch_pos.items())
  824. info_names_use = [name.lower() for name in info_names]
  825. dig_names_use = [name.lower() if name is not None else name
  826. for name in dig_names]
  827. non_names_use = [name.lower() for name in non_names]
  828. ref_names = [name.lower() if name is not None else name
  829. for name in ref_names]
  830. n_dup = len(ch_pos) - len(ch_pos_use)
  831. if n_dup:
  832. raise ValueError('Cannot use match_case=False as %s montage '
  833. 'name(s) require case sensitivity' % n_dup)
  834. n_dup = len(info_names_use) - len(set(info_names_use))
  835. if n_dup:
  836. raise ValueError('Cannot use match_case=False as %s channel '
  837. 'name(s) require case sensitivity' % n_dup)
  838. ch_pos = ch_pos_use
  839. del ch_pos_use
  840. del dig_names
  841. # use lookup table to match unrecognized channel names to known aliases
  842. if match_alias:
  843. alias_dict = (match_alias if isinstance(match_alias, dict) else
  844. CHANNEL_LOC_ALIASES)
  845. if not match_case:
  846. alias_dict = {
  847. ch_name.lower(): ch_alias.lower()
  848. for ch_name, ch_alias in alias_dict.items()
  849. }
  850. # excluded ch_alias not in info, to prevent unnecessary mapping and
  851. # warning messages based on aliases.
  852. alias_dict = {
  853. ch_name: ch_alias
  854. for ch_name, ch_alias in alias_dict.items()
  855. }
  856. info_names_use = [
  857. alias_dict.get(ch_name, ch_name) for ch_name in info_names_use
  858. ]
  859. non_names_use = [
  860. alias_dict.get(ch_name, ch_name) for ch_name in non_names_use
  861. ]
  862. # warn user if there is not a full overlap of montage with info_chs
  863. missing = np.where([use not in ch_pos for use in info_names_use])[0]
  864. if len(missing): # DigMontage is subset of info
  865. missing_names = [info_names[ii] for ii in missing]
  866. missing_coord_msg = (
  867. 'DigMontage is only a subset of info. There are '
  868. f'{len(missing)} channel position{_pl(missing)} '
  869. 'not present in the DigMontage. The required channels are:\n\n'
  870. f'{missing_names}.\n\nConsider using inst.set_channel_types '
  871. 'if these are not EEG channels, or use the on_missing '
  872. 'parameter if the channel positions are allowed to be unknown '
  873. 'in your analyses.'
  874. )
  875. _on_missing(on_missing, missing_coord_msg)
  876. # set ch coordinates and names from digmontage or nan coords
  877. for ii in missing:
  878. ch_pos[info_names_use[ii]] = [np.nan] * 3
  879. del info_names
  880. assert len(non_names_use) == len(non_names)
  881. # There are no issues here with fNIRS being in non_names_use because
  882. # these names are like "D1_S1_760" and the ch_pos for a fNIRS montage
  883. # will have entries "D1" and "S1".
  884. extra = np.where([non in ch_pos for non in non_names_use])[0]
  885. if len(extra):
  886. types = '/'.join(sorted(set(
  887. channel_type(info, non_picks[ii]) for ii in extra)))
  888. names = [non_names[ii] for ii in extra]
  889. warn(f'Not setting position{_pl(extra)} of {len(extra)} {types} '
  890. f'channel{_pl(extra)} found in montage:\n{names}\n'
  891. 'Consider setting the channel types to be of '
  892. f'{_docdict["montage_types"]} '
  893. 'using inst.set_channel_types before calling inst.set_montage, '
  894. 'or omit these channels when creating your montage.')
  895. for ch, use in zip(chs, info_names_use):
  896. # Next line modifies info['chs'][#]['loc'] in place
  897. if use in ch_pos:
  898. ch['loc'][:6] = _backcompat_value(ch_pos[use], eeg_ref_pos)
  899. ch['coord_frame'] = FIFF.FIFFV_COORD_HEAD
  900. del ch_pos
  901. # XXX this is probably wrong as it uses the order from the montage
  902. # rather than the order of our info['ch_names'] ...
  903. digpoints = [
  904. mnt_head.dig[ii] for ii, name in enumerate(dig_names_use)
  905. if name in (info_names_use + ref_names)]
  906. # get a copy of the old dig
  907. if info['dig'] is not None:
  908. old_dig = info['dig'].copy()
  909. else:
  910. old_dig = []
  911. # determine if needed to add an extra EEG REF DigPoint
  912. if custom_eeg_ref_dig:
  913. # ref_name = 'EEG000' if match_case else 'eeg000'
  914. ref_dig_dict = {'kind': FIFF.FIFFV_POINT_EEG,
  915. 'r': eeg_ref_pos,
  916. 'ident': 0,
  917. 'coord_frame': info['dig'].pop()['coord_frame']}
  918. ref_dig_point = _format_dig_points([ref_dig_dict])[0]
  919. # only append the reference dig point if it was already
  920. # in the old dig
  921. if ref_dig_point in old_dig:
  922. digpoints.append(ref_dig_point)
  923. # Next line modifies info['dig'] in place
  924. with info._unlock():
  925. info['dig'] = _format_dig_points(digpoints, enforce_order=True)
  926. # Handle fNIRS with source, detector and channel
  927. fnirs_picks = _picks_to_idx(info, 'fnirs', allow_empty=True)
  928. if len(fnirs_picks) > 0:
  929. _set_montage_fnirs(info, mnt_head)
  930. def _read_isotrak_elp_points(fname):
  931. """Read Polhemus Isotrak digitizer data from a ``.elp`` file.
  932. Parameters
  933. ----------
  934. fname : str
  935. The filepath of .elp Polhemus Isotrak file.
  936. Returns
  937. -------
  938. out : dict of arrays
  939. The dictionary containing locations for 'nasion', 'lpa', 'rpa'
  940. and 'points'.
  941. """
  942. value_pattern = r"\-?\d+\.?\d*e?\-?\d*"
  943. coord_pattern = r"({0})\s+({0})\s+({0})\s*$".format(value_pattern)
  944. with open(fname) as fid:
  945. file_str = fid.read()
  946. points_str = [m.groups() for m in re.finditer(coord_pattern, file_str,
  947. re.MULTILINE)]
  948. points = np.array(points_str, dtype=float)
  949. return {
  950. 'nasion': points[0], 'lpa': points[1], 'rpa': points[2],
  951. 'points': points[3:]
  952. }
  953. def _read_isotrak_hsp_points(fname):
  954. """Read Polhemus Isotrak digitizer data from a ``.hsp`` file.
  955. Parameters
  956. ----------
  957. fname : str
  958. The filepath of .hsp Polhemus Isotrak file.
  959. Returns
  960. -------
  961. out : dict of arrays
  962. The dictionary containing locations for 'nasion', 'lpa', 'rpa'
  963. and 'points'.
  964. """
  965. def get_hsp_fiducial(line):
  966. return np.fromstring(line.replace('%F', ''), dtype=float, sep='\t')
  967. with open(fname) as ff:
  968. for line in ff:
  969. if 'position of fiducials' in line.lower():
  970. break
  971. nasion = get_hsp_fiducial(ff.readline())
  972. lpa = get_hsp_fiducial(ff.readline())
  973. rpa = get_hsp_fiducial(ff.readline())
  974. _ = ff.readline()
  975. line = ff.readline()
  976. if line:
  977. n_points, n_cols = np.fromstring(line, dtype=int, sep='\t')
  978. points = np.fromstring(
  979. string=ff.read(), dtype=float, sep='\t',
  980. ).reshape(-1, n_cols)
  981. assert points.shape[0] == n_points
  982. else:
  983. points = np.empty((0, 3))
  984. return {
  985. 'nasion': nasion, 'lpa': lpa, 'rpa': rpa, 'points': points
  986. }
  987. def read_dig_polhemus_isotrak(fname, ch_names=None, unit='m'):
  988. """Read Polhemus digitizer data from a file.
  989. Parameters
  990. ----------
  991. fname : str
  992. The filepath of Polhemus ISOTrak formatted file.
  993. File extension is expected to be '.hsp', '.elp' or '.eeg'.
  994. ch_names : None | list of str
  995. The names of the points. This will make the points
  996. considered as EEG channels. If None, channels will be assumed
  997. to be HPI if the extension is ``'.elp'``, and extra headshape
  998. points otherwise.
  999. unit : 'm' | 'cm' | 'mm'
  1000. Unit of the digitizer file. Polhemus ISOTrak systems data is usually
  1001. exported in meters. Defaults to 'm'.
  1002. Returns
  1003. -------
  1004. montage : instance of DigMontage
  1005. The montage.
  1006. See Also
  1007. --------
  1008. DigMontage
  1009. make_dig_montage
  1010. read_polhemus_fastscan
  1011. read_dig_captrak
  1012. read_dig_dat
  1013. read_dig_egi
  1014. read_dig_fif
  1015. read_dig_localite
  1016. """
  1017. VALID_FILE_EXT = ('.hsp', '.elp', '.eeg')
  1018. _scale = _check_unit_and_get_scaling(unit)
  1019. _, ext = op.splitext(fname)
  1020. _check_option('fname', ext, VALID_FILE_EXT)
  1021. if ext == '.elp':
  1022. data = _read_isotrak_elp_points(fname)
  1023. else:
  1024. # Default case we read points as hsp since is the most likely scenario
  1025. data = _read_isotrak_hsp_points(fname)
  1026. if _scale != 1:
  1027. data = {key: val * _scale for key, val in data.items()}
  1028. else:
  1029. pass # noqa
  1030. if ch_names is None:
  1031. keyword = 'hpi' if ext == '.elp' else 'hsp'
  1032. data[keyword] = data.pop('points')
  1033. else:
  1034. points = data.pop('points')
  1035. if points.shape[0] == len(ch_names):
  1036. data['ch_pos'] = OrderedDict(zip(ch_names, points))
  1037. else:
  1038. raise ValueError((
  1039. "Length of ``ch_names`` does not match the number of points"
  1040. " in {fname}. Expected ``ch_names`` length {n_points:d},"
  1041. " given {n_chnames:d}"
  1042. ).format(
  1043. fname=fname, n_points=points.shape[0], n_chnames=len(ch_names)
  1044. ))
  1045. return make_dig_montage(**data)
  1046. def _is_polhemus_fastscan(fname):
  1047. header = ''
  1048. with open(fname, 'r') as fid:
  1049. for line in fid:
  1050. if not line.startswith('%'):
  1051. break
  1052. header += line
  1053. return 'FastSCAN' in header
  1054. @verbose
  1055. def read_polhemus_fastscan(fname, unit='mm', on_header_missing='raise', *,
  1056. verbose=None):
  1057. """Read Polhemus FastSCAN digitizer data from a ``.txt`` file.
  1058. Parameters
  1059. ----------
  1060. fname : str
  1061. The filepath of .txt Polhemus FastSCAN file.
  1062. unit : 'm' | 'cm' | 'mm'
  1063. Unit of the digitizer file. Polhemus FastSCAN systems data is usually
  1064. exported in millimeters. Defaults to 'mm'.
  1065. %(on_header_missing)s
  1066. %(verbose)s
  1067. Returns
  1068. -------
  1069. points : array, shape (n_points, 3)
  1070. The digitization points in digitizer coordinates.
  1071. See Also
  1072. --------
  1073. read_dig_polhemus_isotrak
  1074. make_dig_montage
  1075. """
  1076. VALID_FILE_EXT = ['.txt']
  1077. _scale = _check_unit_and_get_scaling(unit)
  1078. _, ext = op.splitext(fname)
  1079. _check_option('fname', ext, VALID_FILE_EXT)
  1080. if not _is_polhemus_fastscan(fname):
  1081. msg = "%s does not contain a valid Polhemus FastSCAN header" % fname
  1082. _on_missing(on_header_missing, msg)
  1083. points = _scale * np.loadtxt(fname, comments='%', ndmin=2)
  1084. _check_dig_shape(points)
  1085. return points
  1086. def _read_eeglab_locations(fname):
  1087. ch_names = np.genfromtxt(fname, dtype=str, usecols=3).tolist()
  1088. topo = np.loadtxt(fname, dtype=float, usecols=[1, 2])
  1089. sph = _topo_to_sph(topo)
  1090. pos = _sph_to_cart(sph)
  1091. pos[:, [0, 1]] = pos[:, [1, 0]] * [-1, 1]
  1092. return ch_names, pos
  1093. def read_custom_montage(fname, head_size=HEAD_SIZE_DEFAULT, coord_frame=None):
  1094. """Read a montage from a file.
  1095. Parameters
  1096. ----------
  1097. fname : str
  1098. File extension is expected to be:
  1099. '.loc' or '.locs' or '.eloc' (for EEGLAB files),
  1100. '.sfp' (BESA/EGI files), '.csd',
  1101. '.elc', '.txt', '.csd', '.elp' (BESA spherical),
  1102. '.bvef' (BrainVision files),
  1103. '.csv', '.tsv', '.xyz' (XYZ coordinates).
  1104. head_size : float | None
  1105. The size of the head (radius, in [m]). If ``None``, returns the values
  1106. read from the montage file with no modification. Defaults to 0.095m.
  1107. coord_frame : str | None
  1108. The coordinate frame of the points. Usually this is "unknown"
  1109. for native digitizer space. Defaults to None, which is "unknown" for
  1110. most readers but "head" for EEGLAB.
  1111. .. versionadded:: 0.20
  1112. Returns
  1113. -------
  1114. montage : instance of DigMontage
  1115. The montage.
  1116. See Also
  1117. --------
  1118. make_dig_montage
  1119. make_standard_montage
  1120. Notes
  1121. -----
  1122. The function is a helper to read electrode positions you may have
  1123. in various formats. Most of these format are weakly specified
  1124. in terms of units, coordinate systems. It implies that setting
  1125. a montage using a DigMontage produced by this function may
  1126. be problematic. If you use a standard/template (eg. 10/20,
  1127. 10/10 or 10/05) we recommend you use :func:`make_standard_montage`.
  1128. If you can have positions in memory you can also use
  1129. :func:`make_dig_montage` that takes arrays as input.
  1130. """
  1131. from ._standard_montage_utils import (
  1132. _read_theta_phi_in_degrees, _read_sfp, _read_csd, _read_elc,
  1133. _read_elp_besa, _read_brainvision, _read_xyz
  1134. )
  1135. SUPPORTED_FILE_EXT = {
  1136. 'eeglab': ('.loc', '.locs', '.eloc', ),
  1137. 'hydrocel': ('.sfp', ),
  1138. 'matlab': ('.csd', ),
  1139. 'asa electrode': ('.elc', ),
  1140. 'generic (Theta-phi in degrees)': ('.txt', ),
  1141. 'standard BESA spherical': ('.elp', ), # NB: not same as polhemus elp
  1142. 'brainvision': ('.bvef', ),
  1143. 'xyz': ('.csv', '.tsv', '.xyz'),
  1144. }
  1145. _, ext = op.splitext(fname)
  1146. _check_option('fname', ext, list(sum(SUPPORTED_FILE_EXT.values(), ())))
  1147. if ext in SUPPORTED_FILE_EXT['eeglab']:
  1148. if head_size is None:
  1149. raise ValueError(
  1150. "``head_size`` cannot be None for '{}'".format(ext))
  1151. ch_names, pos = _read_eeglab_locations(fname)
  1152. scale = head_size / np.median(np.linalg.norm(pos, axis=-1))
  1153. pos *= scale
  1154. montage = make_dig_montage(
  1155. ch_pos=OrderedDict(zip(ch_names, pos)),
  1156. coord_frame='head',
  1157. )
  1158. elif ext in SUPPORTED_FILE_EXT['hydrocel']:
  1159. montage = _read_sfp(fname, head_size=head_size)
  1160. elif ext in SUPPORTED_FILE_EXT['matlab']:
  1161. montage = _read_csd(fname, head_size=head_size)
  1162. elif ext in SUPPORTED_FILE_EXT['asa electrode']:
  1163. montage = _read_elc(fname, head_size=head_size)
  1164. elif ext in SUPPORTED_FILE_EXT['generic (Theta-phi in degrees)']:
  1165. if head_size is None:
  1166. raise ValueError(
  1167. "``head_size`` cannot be None for '{}'".format(ext))
  1168. montage = _read_theta_phi_in_degrees(fname, head_size=head_size,
  1169. fid_names=('Nz', 'LPA', 'RPA'))
  1170. elif ext in SUPPORTED_FILE_EXT['standard BESA spherical']:
  1171. montage = _read_elp_besa(fname, head_size)
  1172. elif ext in SUPPORTED_FILE_EXT['brainvision']:
  1173. montage = _read_brainvision(fname, head_size)
  1174. elif ext in SUPPORTED_FILE_EXT['xyz']:
  1175. montage = _read_xyz(fname)
  1176. if coord_frame is not None:
  1177. coord_frame = _coord_frame_const(coord_frame)
  1178. for d in montage.dig:
  1179. d['coord_frame'] = coord_frame
  1180. return montage
  1181. def compute_dev_head_t(montage):
  1182. """Compute device to head transform from a DigMontage.
  1183. Parameters
  1184. ----------
  1185. montage : instance of DigMontage
  1186. The DigMontage must contain the fiducials in head
  1187. coordinate system and hpi points in both head and
  1188. meg device coordinate system.
  1189. Returns
  1190. -------
  1191. dev_head_t : instance of Transform
  1192. A Device-to-Head transformation matrix.
  1193. """
  1194. _, coord_frame = _get_fid_coords(montage.dig)
  1195. if coord_frame != FIFF.FIFFV_COORD_HEAD:
  1196. raise ValueError('montage should have been set to head coordinate '
  1197. 'system with transform_to_head function.')
  1198. hpi_head = np.array(
  1199. [d['r'] for d in montage.dig
  1200. if (d['kind'] == FIFF.FIFFV_POINT_HPI and
  1201. d['coord_frame'] == FIFF.FIFFV_COORD_HEAD)], float)
  1202. hpi_dev = np.array(
  1203. [d['r'] for d in montage.dig
  1204. if (d['kind'] == FIFF.FIFFV_POINT_HPI and
  1205. d['coord_frame'] == FIFF.FIFFV_COORD_DEVICE)], float)
  1206. if not (len(hpi_head) == len(hpi_dev) and len(hpi_dev) > 0):
  1207. raise ValueError((
  1208. "To compute Device-to-Head transformation, the same number of HPI"
  1209. " points in device and head coordinates is required. (Got {dev}"
  1210. " points in device and {head} points in head coordinate systems)"
  1211. ).format(dev=len(hpi_dev), head=len(hpi_head)))
  1212. trans = _quat_to_affine(_fit_matched_points(hpi_dev, hpi_head)[0])
  1213. return Transform(fro='meg', to='head', trans=trans)
  1214. def compute_native_head_t(montage):
  1215. """Compute the native-to-head transformation for a montage.
  1216. This uses the fiducials in the native space to transform to compute the
  1217. transform to the head coordinate frame.
  1218. Parameters
  1219. ----------
  1220. montage : instance of DigMontage
  1221. The montage.
  1222. Returns
  1223. -------
  1224. native_head_t : instance of Transform
  1225. A native-to-head transformation matrix.
  1226. """
  1227. # Get fiducial points and their coord_frame
  1228. fid_coords, coord_frame = _get_fid_coords(montage.dig, raise_error=False)
  1229. if coord_frame is None:
  1230. coord_frame = FIFF.FIFFV_COORD_UNKNOWN
  1231. if coord_frame == FIFF.FIFFV_COORD_HEAD:
  1232. native_head_t = np.eye(4)
  1233. else:
  1234. fid_keys = ('nasion', 'lpa', 'rpa')
  1235. for key in fid_keys:
  1236. if fid_coords[key] is None:
  1237. warn('Fiducial point %s not found, assuming identity %s to '
  1238. 'head transformation'
  1239. % (key, _verbose_frames[coord_frame],))
  1240. native_head_t = np.eye(4)
  1241. break
  1242. else:
  1243. native_head_t = get_ras_to_neuromag_trans(
  1244. *[fid_coords[key] for key in fid_keys])
  1245. return Transform(coord_frame, 'head', native_head_t)
  1246. def make_standard_montage(kind, head_size=HEAD_SIZE_DEFAULT):
  1247. """Read a generic (built-in) montage.
  1248. Parameters
  1249. ----------
  1250. kind : str
  1251. The name of the montage to use. See notes for valid kinds.
  1252. head_size : float
  1253. The head size (radius, in meters) to use for spherical montages.
  1254. Defaults to 95mm.
  1255. Returns
  1256. -------
  1257. montage : instan