1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726
|
# Authors: Alexandre Gramfort <alexandre.gramfort@inria.fr>
# Denis Engemann <denis.engemann@gmail.com>
# Martin Luessi <mluessi@nmr.mgh.harvard.edu>
# Eric Larson <larson.eric.d@gmail.com>
# Marijn van Vliet <w.m.vanvliet@gmail.com>
# Jona Sassenhagen <jona.sassenhagen@gmail.com>
# Teon Brooks <teon.brooks@gmail.com>
# Christian Brodbeck <christianbrodbeck@nyu.edu>
# Stefan Appelhoff <stefan.appelhoff@mailbox.org>
# Joan Massich <mailsik@gmail.com>
#
# License: Simplified BSD
from collections import OrderedDict
from dataclasses import dataclass
from copy import deepcopy
import os.path as op
import re
import numpy as np
from ..defaults import HEAD_SIZE_DEFAULT
from .._freesurfer import get_mni_fiducials
from ..viz import plot_montage
from ..transforms import (apply_trans, get_ras_to_neuromag_trans, _sph_to_cart,
_topo_to_sph, _frame_to_str, Transform,
_verbose_frames, _fit_matched_points,
_quat_to_affine, _ensure_trans)
from ..io._digitization import (_count_points_by_type, _ensure_fiducials_head,
_get_dig_eeg, _make_dig_points, write_dig,
_read_dig_fif, _format_dig_points,
_get_fid_coords, _coord_frame_const,
_get_data_as_dict_from_dig)
from ..io.meas_info import create_info
from ..io.open import fiff_open
from ..io.pick import pick_types, _picks_to_idx, channel_type
from ..io.constants import FIFF, CHANNEL_LOC_ALIASES
from ..utils import (warn, copy_function_doc_to_method_doc, _pl, verbose,
_check_option, _validate_type, _check_fname, _on_missing,
fill_doc, _docdict)
from ._dig_montage_utils import _read_dig_montage_egi
from ._dig_montage_utils import _parse_brainvision_dig_montage
@dataclass
class _BuiltinStandardMontage:
name: str
description: str
_BUILTIN_STANDARD_MONTAGES = [
_BuiltinStandardMontage(
name='standard_1005',
description='Electrodes are named and positioned according to the '
'international 10-05 system (343+3 locations)',
),
_BuiltinStandardMontage(
name='standard_1020',
description='Electrodes are named and positioned according to the '
'international 10-20 system (94+3 locations)',
),
_BuiltinStandardMontage(
name='standard_alphabetic',
description='Electrodes are named with LETTER-NUMBER combinations '
'(A1, B2, F4, …) (65+3 locations)',
),
_BuiltinStandardMontage(
name='standard_postfixed',
description='Electrodes are named according to the international '
'10-20 system using postfixes for intermediate positions '
'(100+3 locations)',
),
_BuiltinStandardMontage(
name='standard_prefixed',
description='Electrodes are named according to the international '
'10-20 system using prefixes for intermediate positions '
'(74+3 locations)',
),
_BuiltinStandardMontage(
name='standard_primed',
description="Electrodes are named according to the international "
"10-20 system using prime marks (' and '') for "
"intermediate positions (100+3 locations)",
),
_BuiltinStandardMontage(
name='biosemi16',
description='BioSemi cap with 16 electrodes (16+3 locations)',
),
_BuiltinStandardMontage(
name='biosemi32',
description='BioSemi cap with 32 electrodes (32+3 locations)',
),
_BuiltinStandardMontage(
name='biosemi64',
description='BioSemi cap with 64 electrodes (64+3 locations)',
),
_BuiltinStandardMontage(
name='biosemi128',
description='BioSemi cap with 128 electrodes (128+3 locations)',
),
_BuiltinStandardMontage(
name='biosemi160',
description='BioSemi cap with 160 electrodes (160+3 locations)',
),
_BuiltinStandardMontage(
name='biosemi256',
description='BioSemi cap with 256 electrodes (256+3 locations)',
),
_BuiltinStandardMontage(
name='easycap-M1',
description='EasyCap with 10-05 electrode names (74 locations)',
),
_BuiltinStandardMontage(
name='easycap-M10',
description='EasyCap with numbered electrodes (61 locations)',
),
_BuiltinStandardMontage(
name='EGI_256',
description='Geodesic Sensor Net (256 locations)',
),
_BuiltinStandardMontage(
name='GSN-HydroCel-32',
description='HydroCel Geodesic Sensor Net and Cz (33+3 locations)',
),
_BuiltinStandardMontage(
name='GSN-HydroCel-64_1.0',
description='HydroCel Geodesic Sensor Net (64+3 locations)',
),
_BuiltinStandardMontage(
name='GSN-HydroCel-65_1.0',
description='HydroCel Geodesic Sensor Net and Cz (65+3 locations)',
),
_BuiltinStandardMontage(
name='GSN-HydroCel-128',
description='HydroCel Geodesic Sensor Net (128+3 locations)',
),
_BuiltinStandardMontage(
name='GSN-HydroCel-129',
description='HydroCel Geodesic Sensor Net and Cz (129+3 locations)',
),
_BuiltinStandardMontage(
name='GSN-HydroCel-256',
description='HydroCel Geodesic Sensor Net (256+3 locations)',
),
_BuiltinStandardMontage(
name='GSN-HydroCel-257',
description='HydroCel Geodesic Sensor Net and Cz (257+3 locations)',
),
_BuiltinStandardMontage(
name='mgh60',
description='The (older) 60-channel cap used at MGH (60+3 locations)',
),
_BuiltinStandardMontage(
name='mgh70',
description='The (newer) 70-channel BrainVision cap used at MGH '
'(70+3 locations)',
),
_BuiltinStandardMontage(
name='artinis-octamon',
description='Artinis OctaMon fNIRS (8 sources, 2 detectors)',
),
_BuiltinStandardMontage(
name='artinis-brite23',
description='Artinis Brite23 fNIRS (11 sources, 7 detectors)',
),
_BuiltinStandardMontage(
name='brainproducts-RNP-BA-128',
description='Brain Products with 10-10 electrode names (128 channels)',
)
]
def _check_get_coord_frame(dig):
dig_coord_frames = sorted(set(d['coord_frame'] for d in dig))
if len(dig_coord_frames) != 1:
raise RuntimeError(
'Only a single coordinate frame in dig is supported, got '
f'{dig_coord_frames}')
return _frame_to_str[dig_coord_frames.pop()] if dig_coord_frames else None
def get_builtin_montages(*, descriptions=False):
"""Get a list of all standard montages shipping with MNE-Python.
The names of the montages can be passed to :func:`make_standard_montage`.
Parameters
----------
descriptions : bool
Whether to return not only the montage names, but also their
corresponding descriptions. If ``True``, a list of tuples is returned,
where the first tuple element is the montage name and the second is
the montage description. If ``False`` (default), only the names are
returned.
.. versionadded:: 1.1
Returns
-------
montages : list of str | list of tuple
If ``descriptions=False``, the names of all builtin montages that can
be used by :func:`make_standard_montage`.
If ``descriptions=True``, a list of tuples ``(name, description)``.
"""
if descriptions:
return [
(m.name, m.description) for m in _BUILTIN_STANDARD_MONTAGES
]
else:
return [m.name for m in _BUILTIN_STANDARD_MONTAGES]
def make_dig_montage(ch_pos=None, nasion=None, lpa=None, rpa=None,
hsp=None, hpi=None, coord_frame='unknown'):
r"""Make montage from arrays.
Parameters
----------
ch_pos : dict | None
Dictionary of channel positions. Keys are channel names and values
are 3D coordinates - array of shape (3,) - in native digitizer space
in m.
nasion : None | array, shape (3,)
The position of the nasion fiducial point.
This point is assumed to be in the native digitizer space in m.
lpa : None | array, shape (3,)
The position of the left periauricular fiducial point.
This point is assumed to be in the native digitizer space in m.
rpa : None | array, shape (3,)
The position of the right periauricular fiducial point.
This point is assumed to be in the native digitizer space in m.
hsp : None | array, shape (n_points, 3)
This corresponds to an array of positions of the headshape points in
3d. These points are assumed to be in the native digitizer space in m.
hpi : None | array, shape (n_hpi, 3)
This corresponds to an array of HPI points in the native digitizer
space. They only necessary if computation of a ``compute_dev_head_t``
is True.
coord_frame : str
The coordinate frame of the points. Usually this is ``'unknown'``
for native digitizer space.
Other valid values are: ``'head'``, ``'meg'``, ``'mri'``,
``'mri_voxel'``, ``'mni_tal'``, ``'ras'``, ``'fs_tal'``,
``'ctf_head'``, and ``'ctf_meg'``.
.. note::
For custom montages without fiducials, this parameter must be set
to ``'head'``.
Returns
-------
montage : instance of DigMontage
The montage.
See Also
--------
DigMontage
read_dig_captrak
read_dig_egi
read_dig_fif
read_dig_localite
read_dig_polhemus_isotrak
"""
_validate_type(ch_pos, (dict, None), 'ch_pos')
if ch_pos is None:
ch_names = None
else:
ch_names = list(ch_pos)
dig = _make_dig_points(
nasion=nasion, lpa=lpa, rpa=rpa, hpi=hpi, extra_points=hsp,
dig_ch_pos=ch_pos, coord_frame=coord_frame
)
return DigMontage(dig=dig, ch_names=ch_names)
class DigMontage(object):
"""Montage for digitized electrode and headshape position data.
.. warning:: Montages are typically created using one of the helper
functions in the ``See Also`` section below instead of
instantiating this class directly.
Parameters
----------
dig : list of dict
The object containing all the dig points.
ch_names : list of str
The names of the EEG channels.
See Also
--------
read_dig_captrak
read_dig_dat
read_dig_egi
read_dig_fif
read_dig_hpts
read_dig_localite
read_dig_polhemus_isotrak
make_dig_montage
Notes
-----
.. versionadded:: 0.9.0
"""
def __init__(self, *, dig=None, ch_names=None):
dig = list() if dig is None else dig
_validate_type(item=dig, types=list, item_name='dig')
ch_names = list() if ch_names is None else ch_names
n_eeg = sum([1 for d in dig if d['kind'] == FIFF.FIFFV_POINT_EEG])
if n_eeg != len(ch_names):
raise ValueError(
'The number of EEG channels (%d) does not match the number'
' of channel names provided (%d)' % (n_eeg, len(ch_names))
)
self.dig = dig
self.ch_names = ch_names
def __repr__(self):
"""Return string representation."""
n_points = _count_points_by_type(self.dig)
return ('<DigMontage | {extra:d} extras (headshape), {hpi:d} HPIs,'
' {fid:d} fiducials, {eeg:d} channels>').format(**n_points)
@copy_function_doc_to_method_doc(plot_montage)
def plot(self, scale_factor=20, show_names=True, kind='topomap', show=True,
sphere=None, verbose=None):
return plot_montage(self, scale_factor=scale_factor,
show_names=show_names, kind=kind, show=show,
sphere=sphere)
@fill_doc
def rename_channels(self, mapping, allow_duplicates=False):
"""Rename the channels.
Parameters
----------
%(mapping_rename_channels_duplicates)s
Returns
-------
inst : instance of DigMontage
The instance. Operates in-place.
"""
from .channels import rename_channels
temp_info = create_info(list(self._get_ch_pos()), 1000., 'eeg')
rename_channels(temp_info, mapping, allow_duplicates)
self.ch_names = temp_info['ch_names']
@verbose
def save(self, fname, *, overwrite=False, verbose=None):
"""Save digitization points to FIF.
Parameters
----------
fname : path-like
The filename to use. Should end in .fif or .fif.gz.
%(overwrite)s
%(verbose)s
"""
coord_frame = _check_get_coord_frame(self.dig)
write_dig(fname, self.dig, coord_frame, overwrite=overwrite)
def __iadd__(self, other):
"""Add two DigMontages in place.
Notes
-----
Two DigMontages can only be added if there are no duplicated ch_names
and if fiducials are present they should share the same coordinate
system and location values.
"""
def is_fid_defined(fid):
return not (
fid.nasion is None and fid.lpa is None and fid.rpa is None
)
# Check for none duplicated ch_names
ch_names_intersection = set(self.ch_names).intersection(other.ch_names)
if ch_names_intersection:
raise RuntimeError((
"Cannot add two DigMontage objects if they contain duplicated"
" channel names. Duplicated channel(s) found: {}."
).format(
', '.join(['%r' % v for v in sorted(ch_names_intersection)])
))
# Check for unique matching fiducials
self_fid, self_coord = _get_fid_coords(self.dig)
other_fid, other_coord = _get_fid_coords(other.dig)
if is_fid_defined(self_fid) and is_fid_defined(other_fid):
if self_coord != other_coord:
raise RuntimeError('Cannot add two DigMontage objects if '
'fiducial locations are not in the same '
'coordinate system.')
for kk in self_fid:
if not np.array_equal(self_fid[kk], other_fid[kk]):
raise RuntimeError('Cannot add two DigMontage objects if '
'fiducial locations do not match '
'(%s)' % kk)
# keep self
self.dig = _format_dig_points(
self.dig + [d for d in other.dig
if d['kind'] != FIFF.FIFFV_POINT_CARDINAL]
)
else:
self.dig = _format_dig_points(self.dig + other.dig)
self.ch_names += other.ch_names
return self
def copy(self):
"""Copy the DigMontage object.
Returns
-------
dig : instance of DigMontage
The copied DigMontage instance.
"""
return deepcopy(self)
def __add__(self, other):
"""Add two DigMontages."""
out = self.copy()
out += other
return out
def __eq__(self, other):
"""Compare different DigMontage objects for equality.
Returns
-------
Boolean output from comparison of .dig
"""
return self.dig == other.dig and self.ch_names == other.ch_names
def _get_ch_pos(self):
pos = [d['r'] for d in _get_dig_eeg(self.dig)]
assert len(self.ch_names) == len(pos)
return OrderedDict(zip(self.ch_names, pos))
def _get_dig_names(self):
NAMED_KIND = (FIFF.FIFFV_POINT_EEG,)
is_eeg = np.array([d['kind'] in NAMED_KIND for d in self.dig])
assert len(self.ch_names) == is_eeg.sum()
dig_names = [None] * len(self.dig)
for ch_name_idx, dig_idx in enumerate(np.where(is_eeg)[0]):
dig_names[dig_idx] = self.ch_names[ch_name_idx]
return dig_names
def get_positions(self):
"""Get all channel and fiducial positions.
Returns
-------
positions : dict
A dictionary of the positions for channels (``ch_pos``),
coordinate frame (``coord_frame``), nasion (``nasion``),
left preauricular point (``lpa``),
right preauricular point (``rpa``),
Head Shape Polhemus (``hsp``), and
Head Position Indicator(``hpi``).
E.g.::
{
'ch_pos': {'EEG061': [0, 0, 0]},
'nasion': [0, 0, 1],
'coord_frame': 'mni_tal',
'lpa': [0, 1, 0],
'rpa': [1, 0, 0],
'hsp': None,
'hpi': None
}
"""
# get channel positions as dict
ch_pos = self._get_ch_pos()
# get coordframe and fiducial coordinates
montage_bunch = _get_data_as_dict_from_dig(self.dig)
coord_frame = _frame_to_str.get(montage_bunch.coord_frame)
# return dictionary
positions = dict(
ch_pos=ch_pos,
coord_frame=coord_frame,
nasion=montage_bunch.nasion,
lpa=montage_bunch.lpa,
rpa=montage_bunch.rpa,
hsp=montage_bunch.hsp,
hpi=montage_bunch.hpi,
)
return positions
@verbose
def apply_trans(self, trans, verbose=None):
"""Apply a transformation matrix to the montage.
Parameters
----------
trans : instance of mne.transforms.Transform
The transformation matrix to be applied.
%(verbose)s
"""
_validate_type(trans, Transform, 'trans')
coord_frame = self.get_positions()['coord_frame']
trans = _ensure_trans(trans, fro=coord_frame, to=trans['to'])
for d in self.dig:
d['r'] = apply_trans(trans, d['r'])
d['coord_frame'] = trans['to']
@verbose
def add_estimated_fiducials(self, subject, subjects_dir=None,
verbose=None):
"""Estimate fiducials based on FreeSurfer ``fsaverage`` subject.
This takes a montage with the ``mri`` coordinate frame,
corresponding to the FreeSurfer RAS (xyz in the volume) T1w
image of the specific subject. It will call
:func:`mne.coreg.get_mni_fiducials` to estimate LPA, RPA and
Nasion fiducial points.
Parameters
----------
%(subject)s
%(subjects_dir)s
%(verbose)s
Returns
-------
inst : instance of DigMontage
The instance, modified in-place.
See Also
--------
:ref:`tut-source-alignment`
Notes
-----
Since MNE uses the FIF data structure, it relies on the ``head``
coordinate frame. Any coordinate frame can be transformed
to ``head`` if the fiducials (i.e. LPA, RPA and Nasion) are
defined. One can use this function to estimate those fiducials
and then use ``mne.channels.compute_native_head_t(montage)``
to get the head <-> MRI transform.
"""
# get coordframe and fiducial coordinates
montage_bunch = _get_data_as_dict_from_dig(self.dig)
# get the coordinate frame and check that it's MRI
if montage_bunch.coord_frame != FIFF.FIFFV_COORD_MRI:
raise RuntimeError(
f'Montage should be in the "mri" coordinate frame '
f'to use `add_estimated_fiducials`. The current coordinate '
f'frame is {montage_bunch.coord_frame}')
# estimate LPA, nasion, RPA from FreeSurfer fsaverage
fids_mri = list(get_mni_fiducials(subject, subjects_dir))
# add those digpoints to front of montage
self.dig = fids_mri + self.dig
return self
@verbose
def add_mni_fiducials(self, subjects_dir=None, verbose=None):
"""Add fiducials to a montage in MNI space.
Parameters
----------
%(subjects_dir)s
%(verbose)s
Returns
-------
inst : instance of DigMontage
The instance, modified in-place.
Notes
-----
``fsaverage`` is in MNI space and so its fiducials can be
added to a montage in "mni_tal". MNI is an ACPC-aligned
coordinate system (the posterior commissure is the origin)
so since BIDS requires channel locations for ECoG, sEEG and
DBS to be in ACPC space, this function can be used to allow
those coordinate to be transformed to "head" space (origin
between LPA and RPA).
"""
montage_bunch = _get_data_as_dict_from_dig(self.dig)
# get the coordinate frame and check that it's MNI TAL
if montage_bunch.coord_frame != FIFF.FIFFV_MNE_COORD_MNI_TAL:
raise RuntimeError(
f'Montage should be in the "mni_tal" coordinate frame '
f'to use `add_estimated_fiducials`. The current coordinate '
f'frame is {montage_bunch.coord_frame}')
fids_mni = get_mni_fiducials('fsaverage', subjects_dir)
for fid in fids_mni:
# "mri" and "mni_tal" are equivalent for fsaverage
assert fid['coord_frame'] == FIFF.FIFFV_COORD_MRI
fid['coord_frame'] = FIFF.FIFFV_MNE_COORD_MNI_TAL
self.dig = fids_mni + self.dig
return self
@verbose
def remove_fiducials(self, verbose=None):
"""Remove the fiducial points from a montage.
Parameters
----------
%(verbose)s
Returns
-------
inst : instance of DigMontage
The instance, modified in-place.
Notes
-----
MNE will transform a montage to the internal "head" coordinate
frame if the fiducials are present. Under most circumstances, this
is ideal as it standardizes the coordinate frame for things like
plotting. However, in some circumstances, such as saving a ``raw``
with intracranial data to BIDS format, the coordinate frame
should not be changed by removing fiducials.
"""
for d in self.dig.copy():
if d['kind'] == FIFF.FIFFV_POINT_CARDINAL:
self.dig.remove(d)
return self
VALID_SCALES = dict(mm=1e-3, cm=1e-2, m=1)
def _check_unit_and_get_scaling(unit):
_check_option('unit', unit, sorted(VALID_SCALES.keys()))
return VALID_SCALES[unit]
def transform_to_head(montage):
"""Transform a DigMontage object into head coordinate.
Parameters
----------
montage : instance of DigMontage
The montage.
Returns
-------
montage : instance of DigMontage
The montage after transforming the points to head
coordinate system.
Notes
-----
This function requires that the LPA, RPA and Nasion fiducial
points are available. If they are not, they will be added based by
projecting the fiducials onto a sphere with radius equal to the average
distance of each point to the origin (in the given coordinate frame).
This function assumes that all fiducial points are in the same coordinate
frame (e.g. 'unknown') and it will convert all the point in this coordinate
system to Neuromag head coordinate system.
.. versionchanged:: 1.2
Fiducial points will be added automatically if the montage does not
have them.
"""
# Get fiducial points and their coord_frame
native_head_t = compute_native_head_t(montage)
montage = montage.copy() # to avoid inplace modification
if native_head_t['from'] != FIFF.FIFFV_COORD_HEAD:
for d in montage.dig:
if d['coord_frame'] == native_head_t['from']:
d['r'] = apply_trans(native_head_t, d['r'])
d['coord_frame'] = FIFF.FIFFV_COORD_HEAD
_ensure_fiducials_head(montage.dig)
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 <https://compumedicsneuroscan.com/scan-acquire-
configuration-files/>`__ 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_captrak
read_dig_dat
read_dig_egi
read_dig_fif
read_dig_hpts
read_dig_localite
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.
"""
from ._standard_montage_utils import _check_dupes_odict
fname = _check_fname(fname, overwrite='read', must_exist=True)
with open(fname, 'r') as fid:
lines = fid.readlines()
ch_names, poss = list(), list()
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:
ch_names.append(items[0])
poss.append(pos)
electrodes = _check_dupes_odict(ch_names, poss)
return make_dig_montage(electrodes, nasion, lpa, rpa)
def read_dig_fif(fname):
r"""Read digitized points from a .fif file.
Note that electrode names are not present in the .fif file so
they are here defined with the convention from VectorView
systems (EEG001, EEG002, etc.)
Parameters
----------
fname : path-like
FIF file from which to read digitization locations.
Returns
-------
montage : instance of DigMontage
The montage.
See Also
--------
DigMontage
read_dig_dat
read_dig_egi
read_dig_captrak
read_dig_polhemus_isotrak
read_dig_hpts
read_dig_localite
make_dig_montage
"""
_check_fname(fname, overwrite='read', must_exist=True)
# Load the dig data
f, tree = fiff_open(fname)[:2]
with f as fid:
dig = _read_dig_fif(fid, tree)
ch_names = []
for d in dig:
if d['kind'] == FIFF.FIFFV_POINT_EEG:
ch_names.append('EEG%03d' % d['ident'])
montage = DigMontage(dig=dig, ch_names=ch_names)
return montage
def read_dig_hpts(fname, unit='mm'):
"""Read historical .hpts mne-c files.
Parameters
----------
fname : path-like
The filepath of .hpts file.
unit : 'm' | 'cm' | 'mm'
Unit of the positions. Defaults to 'mm'.
Returns
-------
montage : instance of DigMontage
The montage.
See Also
--------
DigMontage
read_dig_captrak
read_dig_dat
read_dig_egi
read_dig_fif
read_dig_localite
read_dig_polhemus_isotrak
make_dig_montage
Notes
-----
The hpts format digitzer data file may contain comment lines starting
with the pound sign (#) and data lines of the form::
<*category*> <*identifier*> <*x/mm*> <*y/mm*> <*z/mm*>
where:
``<*category*>``
defines the type of points. Allowed categories are: ``hpi``,
``cardinal`` (fiducial), ``eeg``, and ``extra`` corresponding to
head-position indicator coil locations, cardinal landmarks, EEG
electrode locations, and additional head surface points,
respectively.
``<*identifier*>``
identifies the point. The identifiers are usually sequential
numbers. For cardinal landmarks, 1 = left auricular point,
2 = nasion, and 3 = right auricular point. For EEG electrodes,
identifier = 0 signifies the reference electrode.
``<*x/mm*> , <*y/mm*> , <*z/mm*>``
Location of the point, usually in the head coordinate system
in millimeters. If your points are in [m] then unit parameter can
be changed.
For example::
cardinal 2 -5.6729 -12.3873 -30.3671
cardinal 1 -37.6782 -10.4957 91.5228
cardinal 3 -131.3127 9.3976 -22.2363
hpi 1 -30.4493 -11.8450 83.3601
hpi 2 -122.5353 9.2232 -28.6828
hpi 3 -6.8518 -47.0697 -37.0829
hpi 4 7.3744 -50.6297 -12.1376
hpi 5 -33.4264 -43.7352 -57.7756
eeg FP1 3.8676 -77.0439 -13.0212
eeg FP2 -31.9297 -70.6852 -57.4881
eeg F7 -6.1042 -68.2969 45.4939
...
"""
from ._standard_montage_utils import _str_names, _str
fname = _check_fname(fname, overwrite='read', must_exist=True)
_scale = _check_unit_and_get_scaling(unit)
out = np.genfromtxt(fname, comments='#',
dtype=(_str, _str, 'f8', 'f8', 'f8'))
kind, label = _str_names(out['f0']), _str_names(out['f1'])
kind = [k.lower() for k in kind]
xyz = np.array([out['f%d' % ii] for ii in range(2, 5)]).T
xyz *= _scale
del _scale
fid_idx_to_label = {'1': 'lpa', '2': 'nasion', '3': 'rpa'}
fid = {fid_idx_to_label[label[ii]]: this_xyz
for ii, this_xyz in enumerate(xyz) if kind[ii] == 'cardinal'}
ch_pos = {label[ii]: this_xyz
for ii, this_xyz in enumerate(xyz) if kind[ii] == 'eeg'}
hpi = np.array([this_xyz for ii, this_xyz in enumerate(xyz)
if kind[ii] == 'hpi'])
hpi.shape = (-1, 3) # in case it's empty
hsp = np.array([this_xyz for ii, this_xyz in enumerate(xyz)
if kind[ii] == 'extra'])
hsp.shape = (-1, 3) # in case it's empty
return make_dig_montage(ch_pos=ch_pos, **fid, hpi=hpi, hsp=hsp)
def read_dig_egi(fname):
"""Read electrode locations from EGI system.
Parameters
----------
fname : path-like
EGI MFF XML coordinates file from which to read digitization locations.
Returns
-------
montage : instance of DigMontage
The montage.
See Also
--------
DigMontage
read_dig_captrak
read_dig_dat
read_dig_fif
read_dig_hpts
read_dig_localite
read_dig_polhemus_isotrak
make_dig_montage
"""
_check_fname(fname, overwrite='read', must_exist=True)
data = _read_dig_montage_egi(
fname=fname,
_scaling=1.,
_all_data_kwargs_are_none=True
)
return make_dig_montage(**data)
def read_dig_captrak(fname):
"""Read electrode locations from CapTrak Brain Products system.
Parameters
----------
fname : path-like
BrainVision CapTrak coordinates file from which to read EEG electrode
locations. This is typically in XML format with the .bvct extension.
Returns
-------
montage : instance of DigMontage
The montage.
See Also
--------
DigMontage
read_dig_dat
read_dig_egi
read_dig_fif
read_dig_hpts
read_dig_localite
read_dig_polhemus_isotrak
make_dig_montage
"""
_check_fname(fname, overwrite='read', must_exist=True)
data = _parse_brainvision_dig_montage(fname, scale=1e-3)
return make_dig_montage(**data)
def read_dig_localite(fname, nasion=None, lpa=None, rpa=None):
"""Read Localite .csv file.
Parameters
----------
fname : path-like
File name.
nasion : str | None
Name of nasion fiducial point.
lpa : str | None
Name of left preauricular fiducial point.
rpa : str | None
Name of right preauricular fiducial point.
Returns
-------
montage : instance of DigMontage
The montage.
See Also
--------
DigMontage
read_dig_captrak
read_dig_dat
read_dig_egi
read_dig_fif
read_dig_hpts
read_dig_polhemus_isotrak
make_dig_montage
"""
ch_pos = {}
with open(fname) as f:
f.readline() # skip first row
for row in f:
_, name, x, y, z = row.split(",")
ch_pos[name] = np.array((float(x), float(y), float(z))) / 1000
if nasion is not None:
nasion = ch_pos.pop(nasion)
if lpa is not None:
lpa = ch_pos.pop(lpa)
if rpa is not None:
rpa = ch_pos.pop(rpa)
return make_dig_montage(ch_pos, nasion, lpa, rpa)
def _get_montage_in_head(montage):
coords = set([d['coord_frame'] for d in montage.dig])
montage = montage.copy()
if len(coords) == 1 and coords.pop() == FIFF.FIFFV_COORD_HEAD:
_ensure_fiducials_head(montage.dig)
return montage
else:
return transform_to_head(montage)
def _set_montage_fnirs(info, montage):
"""Set the montage for fNIRS data.
This needs to be different to electrodes as each channel has three
coordinates that need to be set. For each channel there is a source optode
location, a detector optode location, and a channel midpoint that must be
stored. This function modifies info['chs'][#]['loc'] and info['dig'] in
place.
"""
from ..preprocessing.nirs import _validate_nirs_info
# Validate that the fNIRS info is correctly formatted
picks = _validate_nirs_info(info)
# Modify info['chs'][#]['loc'] in place
num_ficiduals = len(montage.dig) - len(montage.ch_names)
for ch_idx in picks:
ch = info['chs'][ch_idx]['ch_name']
source, detector = ch.split(' ')[0].split('_')
source_pos = montage.dig[montage.ch_names.index(source)
+ num_ficiduals]['r']
detector_pos = montage.dig[montage.ch_names.index(detector)
+ num_ficiduals]['r']
info['chs'][ch_idx]['loc'][3:6] = source_pos
info['chs'][ch_idx]['loc'][6:9] = detector_pos
midpoint = (source_pos + detector_pos) / 2
info['chs'][ch_idx]['loc'][:3] = midpoint
info['chs'][ch_idx]['coord_frame'] = FIFF.FIFFV_COORD_HEAD
# Modify info['dig'] in place
with info._unlock():
info['dig'] = montage.dig
@fill_doc
def _set_montage(info, montage, match_case=True, match_alias=False,
on_missing='raise'):
"""Apply montage to data.
With a DigMontage, this function will replace the digitizer info with
the values specified for the particular montage.
Usually, a montage is expected to contain the positions of all EEG
electrodes and a warning is raised when this is not the case.
Parameters
----------
%(info_not_none)s
%(montage)s
%(match_case)s
%(match_alias)s
%(on_missing_montage)s
Notes
-----
This function will change the info variable in place.
"""
_validate_type(montage, (DigMontage, None, str), 'montage')
if montage is None:
# Next line modifies info['dig'] in place
with info._unlock():
info['dig'] = None
for ch in info['chs']:
# Next line modifies info['chs'][#]['loc'] in place
ch['loc'] = np.full(12, np.nan)
return
if isinstance(montage, str): # load builtin montage
_check_option(
parameter='montage', value=montage,
allowed_values=[m.name for m in _BUILTIN_STANDARD_MONTAGES]
)
montage = make_standard_montage(montage)
mnt_head = _get_montage_in_head(montage)
del montage
def _backcompat_value(pos, ref_pos):
if any(np.isnan(pos)):
return np.full(6, np.nan)
else:
return np.concatenate((pos, ref_pos))
# get the channels in the montage in head
ch_pos = mnt_head._get_ch_pos()
# only get the eeg, seeg, dbs, ecog channels
picks = pick_types(
info, meg=False, eeg=True, seeg=True, dbs=True, ecog=True,
exclude=())
non_picks = np.setdiff1d(np.arange(info['nchan']), picks)
# get the reference position from the loc[3:6]
chs = [info['chs'][ii] for ii in picks]
non_names = [info['chs'][ii]['ch_name'] for ii in non_picks]
del picks
ref_pos = [ch['loc'][3:6] for ch in chs]
# keep reference location from EEG-like channels if they
# already exist and are all the same.
custom_eeg_ref_dig = False
# Note: ref position is an empty list for fieldtrip data
if ref_pos:
if all([np.equal(ref_pos[0], pos).all() for pos in ref_pos]) \
and not np.equal(ref_pos[0], [0, 0, 0]).all():
eeg_ref_pos = ref_pos[0]
# since we have an EEG reference position, we have
# to add it into the info['dig'] as EEG000
custom_eeg_ref_dig = True
if not custom_eeg_ref_dig:
refs = set(ch_pos) & {'EEG000', 'REF'}
assert len(refs) <= 1
eeg_ref_pos = np.zeros(3) if not refs else ch_pos.pop(refs.pop())
# This raises based on info being subset/superset of montage
info_names = [ch['ch_name'] for ch in chs]
dig_names = mnt_head._get_dig_names()
ref_names = [None, 'EEG000', 'REF']
if match_case:
info_names_use = info_names
dig_names_use = dig_names
non_names_use = non_names
else:
ch_pos_use = OrderedDict(
(name.lower(), pos) for name, pos in ch_pos.items())
info_names_use = [name.lower() for name in info_names]
dig_names_use = [name.lower() if name is not None else name
for name in dig_names]
non_names_use = [name.lower() for name in non_names]
ref_names = [name.lower() if name is not None else name
for name in ref_names]
n_dup = len(ch_pos) - len(ch_pos_use)
if n_dup:
raise ValueError('Cannot use match_case=False as %s montage '
'name(s) require case sensitivity' % n_dup)
n_dup = len(info_names_use) - len(set(info_names_use))
if n_dup:
raise ValueError('Cannot use match_case=False as %s channel '
'name(s) require case sensitivity' % n_dup)
ch_pos = ch_pos_use
del ch_pos_use
del dig_names
# use lookup table to match unrecognized channel names to known aliases
if match_alias:
alias_dict = (match_alias if isinstance(match_alias, dict) else
CHANNEL_LOC_ALIASES)
if not match_case:
alias_dict = {
ch_name.lower(): ch_alias.lower()
for ch_name, ch_alias in alias_dict.items()
}
# excluded ch_alias not in info, to prevent unnecessary mapping and
# warning messages based on aliases.
alias_dict = {
ch_name: ch_alias
for ch_name, ch_alias in alias_dict.items()
}
info_names_use = [
alias_dict.get(ch_name, ch_name) for ch_name in info_names_use
]
non_names_use = [
alias_dict.get(ch_name, ch_name) for ch_name in non_names_use
]
# warn user if there is not a full overlap of montage with info_chs
missing = np.where([use not in ch_pos for use in info_names_use])[0]
if len(missing): # DigMontage is subset of info
missing_names = [info_names[ii] for ii in missing]
missing_coord_msg = (
'DigMontage is only a subset of info. There are '
f'{len(missing)} channel position{_pl(missing)} '
'not present in the DigMontage. The required channels are:\n\n'
f'{missing_names}.\n\nConsider using inst.set_channel_types '
'if these are not EEG channels, or use the on_missing '
'parameter if the channel positions are allowed to be unknown '
'in your analyses.'
)
_on_missing(on_missing, missing_coord_msg)
# set ch coordinates and names from digmontage or nan coords
for ii in missing:
ch_pos[info_names_use[ii]] = [np.nan] * 3
del info_names
assert len(non_names_use) == len(non_names)
# There are no issues here with fNIRS being in non_names_use because
# these names are like "D1_S1_760" and the ch_pos for a fNIRS montage
# will have entries "D1" and "S1".
extra = np.where([non in ch_pos for non in non_names_use])[0]
if len(extra):
types = '/'.join(sorted(set(
channel_type(info, non_picks[ii]) for ii in extra)))
names = [non_names[ii] for ii in extra]
warn(f'Not setting position{_pl(extra)} of {len(extra)} {types} '
f'channel{_pl(extra)} found in montage:\n{names}\n'
'Consider setting the channel types to be of '
f'{_docdict["montage_types"]} '
'using inst.set_channel_types before calling inst.set_montage, '
'or omit these channels when creating your montage.')
for ch, use in zip(chs, info_names_use):
# Next line modifies info['chs'][#]['loc'] in place
if use in ch_pos:
ch['loc'][:6] = _backcompat_value(ch_pos[use], eeg_ref_pos)
ch['coord_frame'] = FIFF.FIFFV_COORD_HEAD
del ch_pos
# XXX this is probably wrong as it uses the order from the montage
# rather than the order of our info['ch_names'] ...
digpoints = [
mnt_head.dig[ii] for ii, name in enumerate(dig_names_use)
if name in (info_names_use + ref_names)]
# get a copy of the old dig
if info['dig'] is not None:
old_dig = info['dig'].copy()
else:
old_dig = []
# determine if needed to add an extra EEG REF DigPoint
if custom_eeg_ref_dig:
# ref_name = 'EEG000' if match_case else 'eeg000'
ref_dig_dict = {'kind': FIFF.FIFFV_POINT_EEG,
'r': eeg_ref_pos,
'ident': 0,
'coord_frame': info['dig'].pop()['coord_frame']}
ref_dig_point = _format_dig_points([ref_dig_dict])[0]
# only append the reference dig point if it was already
# in the old dig
if ref_dig_point in old_dig:
digpoints.append(ref_dig_point)
# Next line modifies info['dig'] in place
with info._unlock():
info['dig'] = _format_dig_points(digpoints, enforce_order=True)
del digpoints
# TODO: Ideally we would have a check like this, but read_raw_bids for ECoG
# allows for a montage to be set without any fiducials, then silently the
# info['dig'] can end up in the MNI_TAL frame... only because in our
# conversion code, UNKNOWN is treated differently from any other frame
# (e.g., MNI_TAL). We should clean this up at some point...
# missing_fids = sum(
# d['kind'] == FIFF.FIFFV_POINT_CARDINAL for d in info['dig'][:3]) != 3
# if missing_fids:
# raise RuntimeError(
# 'Could not find all three fiducials in the montage, this should '
# 'not happen. Please contact MNE-Python developers.')
# Handle fNIRS with source, detector and channel
fnirs_picks = _picks_to_idx(info, 'fnirs', allow_empty=True)
if len(fnirs_picks) > 0:
_set_montage_fnirs(info, mnt_head)
def _read_isotrak_elp_points(fname):
"""Read Polhemus Isotrak digitizer data from a ``.elp`` file.
Parameters
----------
fname : str
The filepath of .elp Polhemus Isotrak file.
Returns
-------
out : dict of arrays
The dictionary containing locations for 'nasion', 'lpa', 'rpa'
and 'points'.
"""
value_pattern = r"\-?\d+\.?\d*e?\-?\d*"
coord_pattern = r"({0})\s+({0})\s+({0})\s*$".format(value_pattern)
with open(fname) as fid:
file_str = fid.read()
points_str = [m.groups() for m in re.finditer(coord_pattern, file_str,
re.MULTILINE)]
points = np.array(points_str, dtype=float)
return {
'nasion': points[0], 'lpa': points[1], 'rpa': points[2],
'points': points[3:]
}
def _read_isotrak_hsp_points(fname):
"""Read Polhemus Isotrak digitizer data from a ``.hsp`` file.
Parameters
----------
fname : str
The filepath of .hsp Polhemus Isotrak file.
Returns
-------
out : dict of arrays
The dictionary containing locations for 'nasion', 'lpa', 'rpa'
and 'points'.
"""
def get_hsp_fiducial(line):
return np.fromstring(line.replace('%F', ''), dtype=float, sep='\t')
with open(fname) as ff:
for line in ff:
if 'position of fiducials' in line.lower():
break
nasion = get_hsp_fiducial(ff.readline())
lpa = get_hsp_fiducial(ff.readline())
rpa = get_hsp_fiducial(ff.readline())
_ = ff.readline()
line = ff.readline()
if line:
n_points, n_cols = np.fromstring(line, dtype=int, sep='\t')
points = np.fromstring(
string=ff.read(), dtype=float, sep='\t',
).reshape(-1, n_cols)
assert points.shape[0] == n_points
else:
points = np.empty((0, 3))
return {
'nasion': nasion, 'lpa': lpa, 'rpa': rpa, 'points': points
}
def read_dig_polhemus_isotrak(fname, ch_names=None, unit='m'):
"""Read Polhemus digitizer data from a file.
Parameters
----------
fname : path-like
The filepath of Polhemus ISOTrak formatted file.
File extension is expected to be '.hsp', '.elp' or '.eeg'.
ch_names : None | list of str
The names of the points. This will make the points
considered as EEG channels. If None, channels will be assumed
to be HPI if the extension is ``'.elp'``, and extra headshape
points otherwise.
unit : 'm' | 'cm' | 'mm'
Unit of the digitizer file. Polhemus ISOTrak systems data is usually
exported in meters. Defaults to 'm'.
Returns
-------
montage : instance of DigMontage
The montage.
See Also
--------
DigMontage
make_dig_montage
read_polhemus_fastscan
read_dig_captrak
read_dig_dat
read_dig_egi
read_dig_fif
read_dig_localite
"""
VALID_FILE_EXT = ('.hsp', '.elp', '.eeg')
fname = _check_fname(fname, overwrite='read', must_exist=True)
_scale = _check_unit_and_get_scaling(unit)
_, ext = op.splitext(fname)
_check_option('fname', ext, VALID_FILE_EXT)
if ext == '.elp':
data = _read_isotrak_elp_points(fname)
else:
# Default case we read points as hsp since is the most likely scenario
data = _read_isotrak_hsp_points(fname)
if _scale != 1:
data = {key: val * _scale for key, val in data.items()}
else:
pass # noqa
if ch_names is None:
keyword = 'hpi' if ext == '.elp' else 'hsp'
data[keyword] = data.pop('points')
else:
points = data.pop('points')
if points.shape[0] == len(ch_names):
data['ch_pos'] = OrderedDict(zip(ch_names, points))
else:
raise ValueError((
"Length of ``ch_names`` does not match the number of points"
" in {fname}. Expected ``ch_names`` length {n_points:d},"
" given {n_chnames:d}"
).format(
fname=fname, n_points=points.shape[0], n_chnames=len(ch_names)
))
return make_dig_montage(**data)
def _is_polhemus_fastscan(fname):
header = ''
with open(fname, 'r') as fid:
for line in fid:
if not line.startswith('%'):
break
header += line
return 'FastSCAN' in header
@verbose
def read_polhemus_fastscan(fname, unit='mm', on_header_missing='raise', *,
verbose=None):
"""Read Polhemus FastSCAN digitizer data from a ``.txt`` file.
Parameters
----------
fname : path-like
The path of .txt Polhemus FastSCAN file.
unit : 'm' | 'cm' | 'mm'
Unit of the digitizer file. Polhemus FastSCAN systems data is usually
exported in millimeters. Defaults to 'mm'.
%(on_header_missing)s
%(verbose)s
Returns
-------
points : array, shape (n_points, 3)
The digitization points in digitizer coordinates.
See Also
--------
read_dig_polhemus_isotrak
make_dig_montage
"""
VALID_FILE_EXT = ['.txt']
fname = _check_fname(fname, overwrite='read', must_exist=True)
_scale = _check_unit_and_get_scaling(unit)
_, ext = op.splitext(fname)
_check_option('fname', ext, VALID_FILE_EXT)
if not _is_polhemus_fastscan(fname):
msg = "%s does not contain a valid Polhemus FastSCAN header" % fname
_on_missing(on_header_missing, msg)
points = _scale * np.loadtxt(fname, comments='%', ndmin=2)
_check_dig_shape(points)
return points
def _read_eeglab_locations(fname):
ch_names = np.genfromtxt(fname, dtype=str, usecols=3).tolist()
topo = np.loadtxt(fname, dtype=float, usecols=[1, 2])
sph = _topo_to_sph(topo)
pos = _sph_to_cart(sph)
pos[:, [0, 1]] = pos[:, [1, 0]] * [-1, 1]
return ch_names, pos
def read_custom_montage(fname, head_size=HEAD_SIZE_DEFAULT, coord_frame=None):
"""Read a montage from a file.
Parameters
----------
fname : path-like
File extension is expected to be:
'.loc' or '.locs' or '.eloc' (for EEGLAB files),
'.sfp' (BESA/EGI files), '.csd',
'.elc', '.txt', '.csd', '.elp' (BESA spherical),
'.bvef' (BrainVision files),
'.csv', '.tsv', '.xyz' (XYZ coordinates).
head_size : float | None
The size of the head (radius, in [m]). If ``None``, returns the values
read from the montage file with no modification. Defaults to 0.095m.
coord_frame : str | None
The coordinate frame of the points. Usually this is "unknown"
for native digitizer space. Defaults to None, which is "unknown" for
most readers but "head" for EEGLAB.
.. versionadded:: 0.20
Returns
-------
montage : instance of DigMontage
The montage.
See Also
--------
make_dig_montage
make_standard_montage
Notes
-----
The function is a helper to read electrode positions you may have
in various formats. Most of these format are weakly specified
in terms of units, coordinate systems. It implies that setting
a montage using a DigMontage produced by this function may
be problematic. If you use a standard/template (eg. 10/20,
10/10 or 10/05) we recommend you use :func:`make_standard_montage`.
If you can have positions in memory you can also use
:func:`make_dig_montage` that takes arrays as input.
"""
from ._standard_montage_utils import (
_read_theta_phi_in_degrees, _read_sfp, _read_csd, _read_elc,
_read_elp_besa, _read_brainvision, _read_xyz
)
SUPPORTED_FILE_EXT = {
'eeglab': ('.loc', '.locs', '.eloc', ),
'hydrocel': ('.sfp', ),
'matlab': ('.csd', ),
'asa electrode': ('.elc', ),
'generic (Theta-phi in degrees)': ('.txt', ),
'standard BESA spherical': ('.elp', ), # NB: not same as polhemus elp
'brainvision': ('.bvef', ),
'xyz': ('.csv', '.tsv', '.xyz'),
}
fname = _check_fname(fname, overwrite='read', must_exist=True)
_, ext = op.splitext(fname)
_check_option('fname', ext, list(sum(SUPPORTED_FILE_EXT.values(), ())))
if ext in SUPPORTED_FILE_EXT['eeglab']:
if head_size is None:
raise ValueError(
"``head_size`` cannot be None for '{}'".format(ext))
ch_names, pos = _read_eeglab_locations(fname)
scale = head_size / np.median(np.linalg.norm(pos, axis=-1))
pos *= scale
montage = make_dig_montage(
ch_pos=OrderedDict(zip(ch_names, pos)),
coord_frame='head',
)
elif ext in SUPPORTED_FILE_EXT['hydrocel']:
montage = _read_sfp(fname, head_size=head_size)
elif ext in SUPPORTED_FILE_EXT['matlab']:
montage = _read_csd(fname, head_size=head_size)
elif ext in SUPPORTED_FILE_EXT['asa electrode']:
montage = _read_elc(fname, head_size=head_size)
elif ext in SUPPORTED_FILE_EXT['generic (Theta-phi in degrees)']:
if head_size is None:
raise ValueError(
"``head_size`` cannot be None for '{}'".format(ext))
montage = _read_theta_phi_in_degrees(fname, head_size=head_size,
fid_names=('Nz', 'LPA', 'RPA'))
elif ext in SUPPORTED_FILE_EXT['standard BESA spherical']:
montage = _read_elp_besa(fname, head_size)
elif ext in SUPPORTED_FILE_EXT['brainvision']:
montage = _read_brainvision(fname, head_size)
elif ext in SUPPORTED_FILE_EXT['xyz']:
montage = _read_xyz(fname)
if coord_frame is not None:
coord_frame = _coord_frame_const(coord_frame)
for d in montage.dig:
d['coord_frame'] = coord_frame
return montage
def compute_dev_head_t(montage):
"""Compute device to head transform from a DigMontage.
Parameters
----------
montage : DigMontage
The `~mne.channels.DigMontage` must contain the fiducials in head
coordinate system and hpi points in both head and
meg device coordinate system.
Returns
-------
dev_head_t : Transform
A Device-to-Head transformation matrix.
"""
_, coord_frame = _get_fid_coords(montage.dig)
if coord_frame != FIFF.FIFFV_COORD_HEAD:
raise ValueError('montage should have been set to head coordinate '
'system with transform_to_head function.')
hpi_head = np.array(
[d['r'] for d in montage.dig
if (d['kind'] == FIFF.FIFFV_POINT_HPI and
d['coord_frame'] == FIFF.FIFFV_COORD_HEAD)], float)
hpi_dev = np.array(
[d['r'] for d in montage.dig
if (d['kind'] == FIFF.FIFFV_POINT_HPI and
d['coord_frame'] == FIFF.FIFFV_COORD_DEVICE)], float)
if not (len(hpi_head) == len(hpi_dev) and len(hpi_dev) > 0):
raise ValueError((
"To compute Device-to-Head transformation, the same number of HPI"
" points in device and head coordinates is required. (Got {dev}"
" points in device and {head} points in head coordinate systems)"
).format(dev=len(hpi_dev), head=len(hpi_head)))
trans = _quat_to_affine(_fit_matched_points(hpi_dev, hpi_head)[0])
return Transform(fro='meg', to='head', trans=trans)
@verbose
def compute_native_head_t(montage, *, on_missing='warn', verbose=None):
"""Compute the native-to-head transformation for a montage.
This uses the fiducials in the native space to transform to compute the
transform to the head coordinate frame.
Parameters
----------
montage : instance of DigMontage
The montage.
%(on_missing_fiducials)s
.. versionadded:: 1.2
%(verbose)s
Returns
-------
native_head_t : instance of Transform
A native-to-head transformation matrix.
"""
# Get fiducial points and their coord_frame
fid_coords, coord_frame = _get_fid_coords(montage.dig, raise_error=False)
if coord_frame is None:
coord_frame = FIFF.FIFFV_COORD_UNKNOWN
if coord_frame == FIFF.FIFFV_COORD_HEAD:
native_head_t = np.eye(4)
else:
fid_keys = ('nasion', 'lpa', 'rpa')
for key in fid_keys:
if fid_coords[key] is None:
msg = (
f'Fiducial point {key} not found, assuming identity '
f'{_verbose_frames[coord_frame]} to head transformation')
_on_missing(on_missing, msg, error_klass=RuntimeError)
native_head_t = np.eye(4)
break
else:
native_head_t = get_ras_to_neuromag_trans(
*[fid_coords[key] for key in fid_keys])
return Transform(coord_frame, 'head', native_head_t)
def make_standard_montage(kind, head_size='auto'):
"""Read a generic (built-in) standard montage that ships with MNE-Python.
Parameters
----------
kind : str
The name of the montage to use.
.. note::
You can retrieve the names of all
built-in montages via :func:`mne.channels.get_builtin_montages`.
head_size : float | None | str
The head size (radius, in meters) to use for spherical montages.
Can be None to not scale the read sizes. ``'auto'`` (default) will
use 95mm for all montages except the ``'standard*'``, ``'mgh*'``, and
``'artinis*'``, which are already in fsaverage's MRI coordinates
(same as MNI).
Returns
-------
montage : instance of DigMontage
The montage.
See Also
--------
get_builtin_montages
make_dig_montage
read_custom_montage
Notes
-----
Individualized (digitized) electrode positions should be read in using
:func:`read_dig_captrak`, :func:`read_dig_dat`, :func:`read_dig_egi`,
:func:`read_dig_fif`, :func:`read_dig_polhemus_isotrak`,
:func:`read_dig_hpts`, or manually made with :func:`make_dig_montage`.
.. versionadded:: 0.19.0
"""
from ._standard_montage_utils import standard_montage_look_up_table
_validate_type(kind, str, 'kind')
_check_option(
parameter='kind', value=kind,
allowed_values=[m.name for m in _BUILTIN_STANDARD_MONTAGES]
)
_validate_type(head_size, ('numeric', str, None), 'head_size')
if isinstance(head_size, str):
_check_option('head_size', head_size, ('auto',), extra='when str')
if kind.startswith(('standard', 'mgh', 'artinis')):
head_size = None
else:
head_size = HEAD_SIZE_DEFAULT
return standard_montage_look_up_table[kind](head_size=head_size)
def _check_dig_shape(pts):
_validate_type(pts, np.ndarray, 'points')
if pts.ndim != 2 or pts.shape[-1] != 3:
raise ValueError(
f'Points must be of shape (n, 3) instead of {pts.shape}')
|