Source code for meeg_utils.io.head
"""Head positions across MEG recordings."""
from __future__ import annotations
from collections.abc import Sequence
from pathlib import Path
import numpy as np
from mne import Transform
from mne.io import BaseRaw
from mne_bids import BIDSPath
from .read import read
[docs]
def average_dev_head_t(sources: Sequence[BaseRaw | str | Path | BIDSPath]) -> Transform:
"""Average device-to-head transform of several MEG recordings.
Each recording's head position (``info["dev_head_t"]``) is weighted by
its duration outside ``BAD`` annotations; rotations are averaged as
quaternions (:func:`mne.preprocessing.compute_average_dev_head_t`). Use
the result as the ``destination`` of
:class:`~meeg_utils.steps.HeadAlign` so that every run is mapped to the
same position, the one that needs the least movement on average.
Parameters
----------
sources : sequence of Raw | path | BIDSPath
The recordings (typically the runs of one session). Paths are read
without loading the data.
Returns
-------
Transform
The average device-to-head transform.
Raises
------
ValueError
If there are no recordings or one lacks a head transform.
"""
from mne.preprocessing import compute_average_dev_head_t
from mne.transforms import rot_to_quat
if not sources:
raise ValueError("average_dev_head_t needs at least one recording.")
raws = [s if isinstance(s, BaseRaw) else read(s, preload=False) for s in sources]
positions = []
for source, raw in zip(sources, raws, strict=True):
trans = raw.info["dev_head_t"]
if trans is None:
raise ValueError(f"{source} has no device-to-head transform.")
quat = rot_to_quat(trans["trans"][:3, :3])
# one cHPI-like sample per recording: t, quaternion, translation, gof, err, v
positions.append(
np.array([[raw.first_samp / raw.info["sfreq"], *quat, *trans["trans"][:3, 3], 1, 0, 0]])
)
return compute_average_dev_head_t(raws, positions, verbose=False)