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
|
# Authors: The MNE-Python contributors.
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.
import numpy as np
from ..._fiff._digitization import DigPoint, _ensure_fiducials_head
from ..._fiff.constants import FIFF
from ..._fiff.meas_info import create_info
from ..._fiff.pick import pick_info
from ...transforms import rotation3d_align_z_axis
from ...utils import _check_pandas_installed, warn
_supported_megs = ["neuromag306"]
_unit_dict = {
"m": 1,
"cm": 1e-2,
"mm": 1e-3,
"V": 1,
"mV": 1e-3,
"uV": 1e-6,
"T": 1,
"T/m": 1,
"T/cm": 1e2,
}
NOINFO_WARNING = (
"Importing FieldTrip data without an info dict from the "
"original file. Channel locations, orientations and types "
"will be incorrect. The imported data cannot be used for "
"source analysis, channel interpolation etc."
)
def _validate_ft_struct(ft_struct):
"""Run validation checks on the ft_structure."""
if isinstance(ft_struct, list):
raise RuntimeError("Loading of data in cell arrays is not supported")
def _create_info(ft_struct, raw_info):
"""Create MNE info structure from a FieldTrip structure."""
if raw_info is None:
warn(NOINFO_WARNING)
sfreq = _set_sfreq(ft_struct)
ch_names = ft_struct["label"]
if raw_info:
info = raw_info.copy()
missing_channels = set(ch_names) - set(info["ch_names"])
if missing_channels:
warn(
"The following channels are present in the FieldTrip data "
f"but cannot be found in the provided info: {missing_channels}.\n"
"These channels will be removed from the resulting data!"
)
missing_chan_idx = [ch_names.index(ch) for ch in missing_channels]
new_chs = [ch for ch in ch_names if ch not in missing_channels]
ch_names = new_chs
ft_struct["label"] = ch_names
if "trial" in ft_struct:
ft_struct["trial"] = _remove_missing_channels_from_trial(
ft_struct["trial"], missing_chan_idx
)
if "avg" in ft_struct:
if ft_struct["avg"].ndim == 2:
ft_struct["avg"] = np.delete(
ft_struct["avg"], missing_chan_idx, axis=0
)
with info._unlock():
info["sfreq"] = sfreq
ch_idx = [info["ch_names"].index(ch) for ch in ch_names]
pick_info(info, ch_idx, copy=False)
else:
info = create_info(ch_names, sfreq)
chs, dig = _create_info_chs_dig(ft_struct)
with info._unlock(update_redundant=True):
info.update(chs=chs, dig=dig)
return info
def _remove_missing_channels_from_trial(trial, missing_chan_idx):
if isinstance(trial, list):
for idx_trial in range(len(trial)):
trial[idx_trial] = _remove_missing_channels_from_trial(
trial[idx_trial], missing_chan_idx
)
elif isinstance(trial, np.ndarray):
if trial.ndim == 2:
trial = np.delete(trial, missing_chan_idx, axis=0)
else:
raise ValueError(
'"trial" field of the FieldTrip structure has an unknown format.'
)
return trial
def _create_info_chs_dig(ft_struct):
"""Create the chs info field from the FieldTrip structure."""
all_channels = ft_struct["label"]
ch_defaults = dict(
coord_frame=FIFF.FIFFV_COORD_UNKNOWN,
cal=1.0,
range=1.0,
unit_mul=FIFF.FIFF_UNITM_NONE,
loc=np.array([0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1]),
unit=FIFF.FIFF_UNIT_V,
)
try:
elec = ft_struct["elec"]
except KeyError:
elec = None
try:
grad = ft_struct["grad"]
except KeyError:
grad = None
if elec is None and grad is None:
warn(
"The supplied FieldTrip structure does not have an elec or grad "
"field. No channel locations will extracted and the kind of "
"channel might be inaccurate."
)
if "chanpos" not in (elec or grad or {"chanpos": None}):
raise RuntimeError(
"This file was created with an old version of FieldTrip. You can "
"convert the data to the new version by loading it into FieldTrip "
"and applying ft_selectdata with an empty cfg structure on it. "
"Otherwise you can supply the Info field."
)
chs = list()
dig = list()
counter = 0
for idx_chan, cur_channel_label in enumerate(all_channels):
cur_ch = ch_defaults.copy()
cur_ch["ch_name"] = cur_channel_label
cur_ch["logno"] = idx_chan + 1
cur_ch["scanno"] = idx_chan + 1
if elec and cur_channel_label in elec["label"]:
cur_ch = _process_channel_eeg(cur_ch, elec)
assert cur_ch["coord_frame"] == FIFF.FIFFV_COORD_HEAD
# Ref gets ident=0 and we don't have it, so start at 1
counter += 1
d = DigPoint(
r=cur_ch["loc"][:3],
coord_frame=FIFF.FIFFV_COORD_HEAD,
kind=FIFF.FIFFV_POINT_EEG,
ident=counter,
)
dig.append(d)
elif grad and cur_channel_label in grad["label"]:
cur_ch = _process_channel_meg(cur_ch, grad)
else:
if cur_channel_label.startswith("EOG"):
cur_ch["kind"] = FIFF.FIFFV_EOG_CH
cur_ch["coil_type"] = FIFF.FIFFV_COIL_EEG
elif cur_channel_label.startswith("ECG"):
cur_ch["kind"] = FIFF.FIFFV_ECG_CH
cur_ch["coil_type"] = FIFF.FIFFV_COIL_EEG_BIPOLAR
elif cur_channel_label.startswith("STI"):
cur_ch["kind"] = FIFF.FIFFV_STIM_CH
cur_ch["coil_type"] = FIFF.FIFFV_COIL_NONE
else:
warn(
f"Cannot guess the correct type of channel {cur_channel_label}. "
"Making it a MISC channel."
)
cur_ch["kind"] = FIFF.FIFFV_MISC_CH
cur_ch["coil_type"] = FIFF.FIFFV_COIL_NONE
chs.append(cur_ch)
_ensure_fiducials_head(dig)
return chs, dig
def _set_sfreq(ft_struct):
"""Set the sample frequency."""
try:
sfreq = ft_struct["fsample"]
except KeyError:
try:
time = ft_struct["time"]
except KeyError:
raise ValueError("No Source for sfreq found")
else:
t1, t2 = float(time[0]), float(time[1])
sfreq = 1 / (t2 - t1)
try:
sfreq = float(sfreq)
except TypeError:
warn(
"FieldTrip structure contained multiple sample rates, trying the "
f"first of:\n{sfreq} Hz"
)
sfreq = float(sfreq.ravel()[0])
return sfreq
def _set_tmin(ft_struct):
"""Set the start time before the event in evoked data if possible."""
times = ft_struct["time"]
time_check = all(times[i][0] == times[i - 1][0] for i, x in enumerate(times))
if time_check:
tmin = times[0][0]
else:
raise RuntimeError(
"Loading data with non-uniform times per epoch is not supported"
)
return tmin
def _create_events(ft_struct, trialinfo_column):
"""Create an event matrix from the FieldTrip structure."""
if "trialinfo" not in ft_struct:
return None
event_type = ft_struct["trialinfo"]
event_number = range(len(event_type))
if trialinfo_column < 0:
raise ValueError("trialinfo_column must be positive")
available_ti_cols = 1
if event_type.ndim == 2:
available_ti_cols = event_type.shape[1]
if trialinfo_column > (available_ti_cols - 1):
raise ValueError(
"trialinfo_column is higher than the amount of columns in trialinfo."
)
event_trans_val = np.zeros(len(event_type))
if event_type.ndim == 2:
event_type = event_type[:, trialinfo_column]
events = (
np.vstack([np.array(event_number), event_trans_val, event_type]).astype("int").T
)
return events
def _create_event_metadata(ft_struct):
"""Create event metadata from trialinfo."""
pandas = _check_pandas_installed(strict=False)
if not pandas:
warn(
"The Pandas library is not installed. Not returning the original "
"trialinfo matrix as metadata."
)
return None
metadata = pandas.DataFrame(ft_struct["trialinfo"])
return metadata
def _process_channel_eeg(cur_ch, elec):
"""Convert EEG channel from FieldTrip to MNE.
Parameters
----------
cur_ch: dict
Channel specific dictionary to populate.
elec: dict
elec dict as loaded from the FieldTrip structure
Returns
-------
cur_ch: dict
The original dict (cur_ch) with the added information
"""
all_labels = np.asanyarray(elec["label"])
chan_idx_in_elec = np.where(all_labels == cur_ch["ch_name"])[0][0]
position = np.squeeze(elec["chanpos"][chan_idx_in_elec, :])
# chanunit = elec['chanunit'][chan_idx_in_elec] # not used/needed yet
position_unit = elec["unit"]
position = position * _unit_dict[position_unit]
cur_ch["loc"] = np.hstack((position, np.zeros((9,))))
cur_ch["unit"] = FIFF.FIFF_UNIT_V
cur_ch["kind"] = FIFF.FIFFV_EEG_CH
cur_ch["coil_type"] = FIFF.FIFFV_COIL_EEG
cur_ch["coord_frame"] = FIFF.FIFFV_COORD_HEAD
return cur_ch
def _process_channel_meg(cur_ch, grad):
"""Convert MEG channel from FieldTrip to MNE.
Parameters
----------
cur_ch: dict
Channel specific dictionary to populate.
grad: dict
grad dict as loaded from the FieldTrip structure
Returns
-------
dict: The original dict (cur_ch) with the added information
"""
all_labels = np.asanyarray(grad["label"])
chan_idx_in_grad = np.where(all_labels == cur_ch["ch_name"])[0][0]
gradtype = grad["type"]
chantype = grad["chantype"][chan_idx_in_grad]
position_unit = grad["unit"]
position = np.squeeze(grad["chanpos"][chan_idx_in_grad, :])
position = position * _unit_dict[position_unit]
if gradtype == "neuromag306" and "tra" in grad and "coilpos" in grad:
# Try to regenerate original channel pos.
idx_in_coilpos = np.where(grad["tra"][chan_idx_in_grad, :] != 0)[0]
cur_coilpos = grad["coilpos"][idx_in_coilpos, :]
cur_coilpos = cur_coilpos * _unit_dict[position_unit]
cur_coilori = grad["coilori"][idx_in_coilpos, :]
if chantype == "megmag":
position = cur_coilpos[0] - 0.0003 * cur_coilori[0]
if chantype == "megplanar":
tmp_pos = cur_coilpos - 0.0003 * cur_coilori
position = np.average(tmp_pos, axis=0)
original_orientation = np.squeeze(grad["chanori"][chan_idx_in_grad, :])
try:
orientation = rotation3d_align_z_axis(original_orientation).T
except AssertionError:
orientation = np.eye(3)
assert orientation.shape == (3, 3)
orientation = orientation.flatten()
# chanunit = grad['chanunit'][chan_idx_in_grad] # not used/needed yet
cur_ch["loc"] = np.hstack((position, orientation))
cur_ch["kind"] = FIFF.FIFFV_MEG_CH
if chantype == "megmag":
cur_ch["coil_type"] = FIFF.FIFFV_COIL_POINT_MAGNETOMETER
cur_ch["unit"] = FIFF.FIFF_UNIT_T
elif chantype == "megplanar":
cur_ch["coil_type"] = FIFF.FIFFV_COIL_VV_PLANAR_T1
cur_ch["unit"] = FIFF.FIFF_UNIT_T_M
elif chantype == "refmag":
cur_ch["coil_type"] = FIFF.FIFFV_COIL_MAGNES_REF_MAG
cur_ch["unit"] = FIFF.FIFF_UNIT_T
elif chantype == "refgrad":
cur_ch["coil_type"] = FIFF.FIFFV_COIL_MAGNES_REF_GRAD
cur_ch["unit"] = FIFF.FIFF_UNIT_T
elif chantype == "meggrad":
cur_ch["coil_type"] = FIFF.FIFFV_COIL_AXIAL_GRAD_5CM
cur_ch["unit"] = FIFF.FIFF_UNIT_T
else:
raise RuntimeError(f"Unexpected coil type: {chantype}.")
cur_ch["coord_frame"] = FIFF.FIFFV_COORD_HEAD
return cur_ch
|