针对pulse-transit的工具
This commit is contained in:
8
dist/client/mne/preprocessing/ieeg/__init__.py
vendored
Normal file
8
dist/client/mne/preprocessing/ieeg/__init__.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
"""Intracranial EEG specific preprocessing functions."""
|
||||
|
||||
# Authors: The MNE-Python contributors.
|
||||
# License: BSD-3-Clause
|
||||
# Copyright the MNE-Python contributors.
|
||||
|
||||
from ._projection import project_sensors_onto_brain
|
||||
from ._volume import make_montage_volume, warp_montage
|
||||
BIN
dist/client/mne/preprocessing/ieeg/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
BIN
dist/client/mne/preprocessing/ieeg/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/client/mne/preprocessing/ieeg/__pycache__/_projection.cpython-310.pyc
vendored
Normal file
BIN
dist/client/mne/preprocessing/ieeg/__pycache__/_projection.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/client/mne/preprocessing/ieeg/__pycache__/_volume.cpython-310.pyc
vendored
Normal file
BIN
dist/client/mne/preprocessing/ieeg/__pycache__/_volume.cpython-310.pyc
vendored
Normal file
Binary file not shown.
209
dist/client/mne/preprocessing/ieeg/_projection.py
vendored
Normal file
209
dist/client/mne/preprocessing/ieeg/_projection.py
vendored
Normal file
@@ -0,0 +1,209 @@
|
||||
# Authors: The MNE-Python contributors.
|
||||
# License: BSD-3-Clause
|
||||
# Copyright the MNE-Python contributors.
|
||||
|
||||
from itertools import combinations
|
||||
|
||||
import numpy as np
|
||||
from scipy.spatial.distance import pdist, squareform
|
||||
|
||||
from ..._fiff.pick import _picks_to_idx
|
||||
from ...channels import make_dig_montage
|
||||
from ...surface import (
|
||||
_compute_nearest,
|
||||
_read_mri_surface,
|
||||
_read_patch,
|
||||
fast_cross_3d,
|
||||
read_surface,
|
||||
)
|
||||
from ...transforms import _cart_to_sph, _ensure_trans, apply_trans, invert_transform
|
||||
from ...utils import _ensure_int, _validate_type, get_subjects_dir, verbose
|
||||
|
||||
|
||||
@verbose
|
||||
def project_sensors_onto_brain(
|
||||
info,
|
||||
trans,
|
||||
subject,
|
||||
subjects_dir=None,
|
||||
picks=None,
|
||||
n_neighbors=10,
|
||||
copy=True,
|
||||
verbose=None,
|
||||
):
|
||||
"""Project sensors onto the brain surface.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
%(info_not_none)s
|
||||
%(trans_not_none)s
|
||||
%(subject)s
|
||||
%(subjects_dir)s
|
||||
%(picks_base)s only ``ecog`` channels.
|
||||
n_neighbors : int
|
||||
The number of neighbors to use to compute the normal vectors
|
||||
for the projection. Must be 2 or greater. More neighbors makes
|
||||
a normal vector with greater averaging which preserves the grid
|
||||
structure. Fewer neighbors has less averaging which better
|
||||
preserves contours in the grid.
|
||||
copy : bool
|
||||
If ``True``, return a new instance of ``info``, if ``False``
|
||||
``info`` is modified in place.
|
||||
%(verbose)s
|
||||
|
||||
Returns
|
||||
-------
|
||||
%(info_not_none)s
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is useful in ECoG analysis for compensating for "brain shift"
|
||||
or shrinking of the brain away from the skull due to changes
|
||||
in pressure during the craniotomy.
|
||||
|
||||
To use the brain surface, a BEM model must be created e.g. using
|
||||
:ref:`mne watershed_bem` using the T1 or :ref:`mne flash_bem`
|
||||
using a FLASH scan.
|
||||
"""
|
||||
n_neighbors = _ensure_int(n_neighbors, "n_neighbors")
|
||||
_validate_type(copy, bool, "copy")
|
||||
if copy:
|
||||
info = info.copy()
|
||||
if n_neighbors < 2:
|
||||
raise ValueError(f"n_neighbors must be 2 or greater, got {n_neighbors}")
|
||||
subjects_dir = get_subjects_dir(subjects_dir, raise_error=True)
|
||||
try:
|
||||
surf = _read_mri_surface(subjects_dir / subject / "bem" / "brain.surf")
|
||||
except FileNotFoundError as err:
|
||||
raise RuntimeError(
|
||||
f"{err}\n\nThe brain surface requires generating "
|
||||
"a BEM using `mne flash_bem` (if you have "
|
||||
"the FLASH scan) or `mne watershed_bem` (to "
|
||||
"use the T1)"
|
||||
) from None
|
||||
# get channel locations
|
||||
picks_idx = _picks_to_idx(info, "ecog" if picks is None else picks)
|
||||
locs = np.array([info["chs"][idx]["loc"][:3] for idx in picks_idx])
|
||||
trans = _ensure_trans(trans, "head", "mri")
|
||||
locs = apply_trans(trans, locs)
|
||||
# compute distances for nearest neighbors
|
||||
dists = squareform(pdist(locs))
|
||||
# find angles for brain surface and points
|
||||
angles = _cart_to_sph(locs)
|
||||
surf_angles = _cart_to_sph(surf["rr"])
|
||||
# initialize projected locs
|
||||
proj_locs = np.zeros(locs.shape) * np.nan
|
||||
for i, loc in enumerate(locs):
|
||||
neighbor_pts = locs[np.argsort(dists[i])[: n_neighbors + 1]]
|
||||
pt1, pt2, pt3 = map(np.array, zip(*combinations(neighbor_pts, 3)))
|
||||
normals = fast_cross_3d(pt1 - pt2, pt1 - pt3)
|
||||
normals[normals @ loc < 0] *= -1
|
||||
normal = np.mean(normals, axis=0)
|
||||
normal /= np.linalg.norm(normal)
|
||||
# find the correct orientation brain surface point nearest the line
|
||||
# https://mathworld.wolfram.com/Point-LineDistance3-Dimensional.html
|
||||
use_rr = surf["rr"][
|
||||
abs(surf_angles[:, 1:] - angles[i, 1:]).sum(axis=1) < np.pi / 4
|
||||
]
|
||||
surf_dists = np.linalg.norm(
|
||||
fast_cross_3d(use_rr - loc, use_rr - loc + normal), axis=1
|
||||
)
|
||||
proj_locs[i] = use_rr[np.argmin(surf_dists)]
|
||||
# back to the "head" coordinate frame for storing in ``raw``
|
||||
proj_locs = apply_trans(invert_transform(trans), proj_locs)
|
||||
montage = info.get_montage()
|
||||
montage_kwargs = (
|
||||
montage.get_positions() if montage else dict(ch_pos=dict(), coord_frame="head")
|
||||
)
|
||||
for idx, loc in zip(picks_idx, proj_locs):
|
||||
# surface RAS-> head and mm->m
|
||||
montage_kwargs["ch_pos"][info.ch_names[idx]] = loc
|
||||
info.set_montage(make_dig_montage(**montage_kwargs))
|
||||
return info
|
||||
|
||||
|
||||
@verbose
|
||||
def _project_sensors_onto_inflated(
|
||||
info,
|
||||
trans,
|
||||
subject,
|
||||
subjects_dir=None,
|
||||
picks=None,
|
||||
max_dist=0.004,
|
||||
flat=False,
|
||||
verbose=None,
|
||||
):
|
||||
"""Project sensors onto the brain surface.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
%(info_not_none)s
|
||||
%(trans_not_none)s
|
||||
%(subject)s
|
||||
%(subjects_dir)s
|
||||
%(picks_base)s only ``seeg`` channels.
|
||||
%(max_dist_ieeg)s
|
||||
flat : bool
|
||||
Whether to project the sensors onto the flat map of the
|
||||
inflated brain instead of the normal inflated brain.
|
||||
%(verbose)s
|
||||
|
||||
Returns
|
||||
-------
|
||||
%(info_not_none)s
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is useful in sEEG analysis for visualization
|
||||
"""
|
||||
subjects_dir = get_subjects_dir(subjects_dir, raise_error=True)
|
||||
surf_data = dict(lh=dict(), rh=dict())
|
||||
x_dir = np.array([1.0, 0.0, 0.0])
|
||||
surfs = ("pial", "inflated")
|
||||
if flat:
|
||||
surfs += ("cortex.patch.flat",)
|
||||
for hemi in ("lh", "rh"):
|
||||
for surf in surfs:
|
||||
for img in ("", ".T1", ".T2", ""):
|
||||
surf_fname = subjects_dir / subject / "surf" / f"{hemi}.{surf}"
|
||||
if surf_fname.is_file():
|
||||
break
|
||||
if surf.split(".")[-1] == "flat":
|
||||
surf = "flat"
|
||||
coords, faces, orig_faces = _read_patch(surf_fname)
|
||||
# rotate 90 degrees to get to a more standard orientation
|
||||
# where X determines the distance between the hemis
|
||||
coords = coords[:, [1, 0, 2]]
|
||||
coords[:, 1] *= -1
|
||||
else:
|
||||
coords, faces = read_surface(surf_fname)
|
||||
if surf in ("inflated", "flat"):
|
||||
x_ = coords @ x_dir
|
||||
coords -= np.max(x_) * x_dir if hemi == "lh" else np.min(x_) * x_dir
|
||||
surf_data[hemi][surf] = (coords / 1000, faces) # mm -> m
|
||||
# get channel locations
|
||||
picks_idx = _picks_to_idx(info, "seeg" if picks is None else picks)
|
||||
locs = np.array([info["chs"][idx]["loc"][:3] for idx in picks_idx])
|
||||
trans = _ensure_trans(trans, "head", "mri")
|
||||
locs = apply_trans(trans, locs)
|
||||
# initialize projected locs
|
||||
proj_locs = np.zeros(locs.shape) * np.nan
|
||||
surf = "flat" if flat else "inflated"
|
||||
for hemi in ("lh", "rh"):
|
||||
hemi_picks = np.where(locs[:, 0] <= 0 if hemi == "lh" else locs[:, 0] > 0)[0]
|
||||
# compute distances to pial vertices
|
||||
nearest, dists = _compute_nearest(
|
||||
surf_data[hemi]["pial"][0], locs[hemi_picks], return_dists=True
|
||||
)
|
||||
mask = dists / 1000 < max_dist
|
||||
proj_locs[hemi_picks[mask]] = surf_data[hemi][surf][0][nearest[mask]]
|
||||
# back to the "head" coordinate frame for storing in ``raw``
|
||||
proj_locs = apply_trans(invert_transform(trans), proj_locs)
|
||||
montage = info.get_montage()
|
||||
montage_kwargs = (
|
||||
montage.get_positions() if montage else dict(ch_pos=dict(), coord_frame="head")
|
||||
)
|
||||
for idx, loc in zip(picks_idx, proj_locs):
|
||||
montage_kwargs["ch_pos"][info.ch_names[idx]] = loc
|
||||
info.set_montage(make_dig_montage(**montage_kwargs))
|
||||
return info
|
||||
240
dist/client/mne/preprocessing/ieeg/_volume.py
vendored
Normal file
240
dist/client/mne/preprocessing/ieeg/_volume.py
vendored
Normal file
@@ -0,0 +1,240 @@
|
||||
# Authors: The MNE-Python contributors.
|
||||
# License: BSD-3-Clause
|
||||
# Copyright the MNE-Python contributors.
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ...channels import DigMontage, make_dig_montage
|
||||
from ...surface import _voxel_neighbors
|
||||
from ...transforms import Transform, _frame_to_str, apply_trans
|
||||
from ...utils import _check_option, _pl, _require_version, _validate_type, verbose, warn
|
||||
|
||||
|
||||
@verbose
|
||||
def warp_montage(montage, moving, static, reg_affine, sdr_morph, verbose=None):
|
||||
"""Warp a montage to a template with image volumes using SDR.
|
||||
|
||||
.. note:: This is likely only applicable for channels inside the brain
|
||||
(intracranial electrodes).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
montage : instance of mne.channels.DigMontage
|
||||
The montage object containing the channels.
|
||||
%(moving)s
|
||||
%(static)s
|
||||
%(reg_affine)s
|
||||
%(sdr_morph)s
|
||||
%(verbose)s
|
||||
|
||||
Returns
|
||||
-------
|
||||
montage_warped : mne.channels.DigMontage
|
||||
The modified montage object containing the channels.
|
||||
"""
|
||||
_require_version("nibabel", "warp montage", "2.1.0")
|
||||
_require_version("dipy", "warping points using SDR", "1.6.0")
|
||||
|
||||
from dipy.align.imwarp import DiffeomorphicMap
|
||||
from nibabel import MGHImage
|
||||
from nibabel.spatialimages import SpatialImage
|
||||
|
||||
_validate_type(moving, SpatialImage, "moving")
|
||||
_validate_type(static, SpatialImage, "static")
|
||||
_validate_type(reg_affine, np.ndarray, "reg_affine")
|
||||
_check_option("reg_affine.shape", reg_affine.shape, ((4, 4),))
|
||||
_validate_type(sdr_morph, (DiffeomorphicMap, None), "sdr_morph")
|
||||
_validate_type(montage, DigMontage, "montage")
|
||||
|
||||
moving_mgh = MGHImage(np.array(moving.dataobj).astype(np.float32), moving.affine)
|
||||
static_mgh = MGHImage(np.array(static.dataobj).astype(np.float32), static.affine)
|
||||
del moving, static
|
||||
|
||||
# get montage channel coordinates
|
||||
ch_dict = montage.get_positions()
|
||||
if ch_dict["coord_frame"] != "mri":
|
||||
bad_coord_frames = np.unique([d["coord_frame"] for d in montage.dig])
|
||||
bad_coord_frames = ", ".join(
|
||||
[
|
||||
_frame_to_str[cf] if cf in _frame_to_str else str(cf)
|
||||
for cf in bad_coord_frames
|
||||
]
|
||||
)
|
||||
raise RuntimeError(
|
||||
f'Coordinate frame not supported, expected "mri", got {bad_coord_frames}'
|
||||
)
|
||||
ch_names = list(ch_dict["ch_pos"].keys())
|
||||
ch_coords = np.array([ch_dict["ch_pos"][name] for name in ch_names])
|
||||
|
||||
ch_coords = apply_trans( # convert to moving voxel space
|
||||
np.linalg.inv(moving_mgh.header.get_vox2ras_tkr()), ch_coords * 1000
|
||||
)
|
||||
# next, to moving scanner RAS
|
||||
ch_coords = apply_trans(moving_mgh.header.get_vox2ras(), ch_coords)
|
||||
|
||||
# now, apply reg_affine
|
||||
ch_coords = apply_trans(
|
||||
Transform( # to static ras
|
||||
fro="ras", to="ras", trans=np.linalg.inv(reg_affine)
|
||||
),
|
||||
ch_coords,
|
||||
)
|
||||
|
||||
# now, apply SDR morph
|
||||
if sdr_morph is not None:
|
||||
ch_coords = sdr_morph.transform_points(
|
||||
ch_coords, sdr_morph.domain_grid2world, sdr_morph.domain_world2grid
|
||||
)
|
||||
|
||||
# back to voxels but now for the static image
|
||||
ch_coords = apply_trans(np.linalg.inv(static_mgh.header.get_vox2ras()), ch_coords)
|
||||
|
||||
# finally, back to surface RAS
|
||||
ch_coords = apply_trans(static_mgh.header.get_vox2ras_tkr(), ch_coords) / 1000
|
||||
|
||||
# make warped montage
|
||||
montage_warped = make_dig_montage(dict(zip(ch_names, ch_coords)), coord_frame="mri")
|
||||
return montage_warped
|
||||
|
||||
|
||||
def _warn_missing_chs(info, dig_image, after_warp=False):
|
||||
"""Warn that channels are missing."""
|
||||
# ensure that each electrode contact was marked in at least one voxel
|
||||
missing = set(np.arange(1, len(info.ch_names) + 1)).difference(
|
||||
set(np.unique(np.array(dig_image.dataobj)))
|
||||
)
|
||||
missing_ch = [info.ch_names[idx - 1] for idx in missing]
|
||||
if missing_ch:
|
||||
warn(
|
||||
f"Channel{_pl(missing_ch)} "
|
||||
f'{", ".join(repr(ch) for ch in missing_ch)} not assigned '
|
||||
"voxels " + (f" after applying {after_warp}" if after_warp else "")
|
||||
)
|
||||
|
||||
|
||||
@verbose
|
||||
def make_montage_volume(
|
||||
montage,
|
||||
base_image,
|
||||
thresh=0.5,
|
||||
max_peak_dist=1,
|
||||
voxels_max=100,
|
||||
use_min=False,
|
||||
verbose=None,
|
||||
):
|
||||
"""Make a volume from intracranial electrode contact locations.
|
||||
|
||||
Find areas of the input volume with intensity greater than
|
||||
a threshold surrounding local extrema near the channel location.
|
||||
Monotonicity from the peak is enforced to prevent channels
|
||||
bleeding into each other.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
montage : instance of mne.channels.DigMontage
|
||||
The montage object containing the channels.
|
||||
base_image : path-like | nibabel.spatialimages.SpatialImage
|
||||
Path to a volumetric scan (e.g. CT) of the subject. Can be in any
|
||||
format readable by nibabel. Can also be a nibabel image object.
|
||||
Local extrema (max or min) should be nearby montage channel locations.
|
||||
thresh : float
|
||||
The threshold relative to the peak to determine the size
|
||||
of the sensors on the volume.
|
||||
max_peak_dist : int
|
||||
The number of voxels away from the channel location to
|
||||
look in the ``image``. This will depend on the accuracy of
|
||||
the channel locations, the default (one voxel in all directions)
|
||||
will work only with localizations that are that accurate.
|
||||
voxels_max : int
|
||||
The maximum number of voxels for each channel.
|
||||
use_min : bool
|
||||
Whether to hypointensities in the volume as channel locations.
|
||||
Default False uses hyperintensities.
|
||||
%(verbose)s
|
||||
|
||||
Returns
|
||||
-------
|
||||
elec_image : nibabel.spatialimages.SpatialImage
|
||||
An image in Freesurfer surface RAS space with voxel values
|
||||
corresponding to the index of the channel. The background
|
||||
is 0s and this index starts at 1.
|
||||
"""
|
||||
_require_version("nibabel", "montage volume", "2.1.0")
|
||||
import nibabel as nib
|
||||
|
||||
_validate_type(montage, DigMontage, "montage")
|
||||
_validate_type(base_image, nib.spatialimages.SpatialImage, "base_image")
|
||||
_validate_type(thresh, float, "thresh")
|
||||
if thresh < 0 or thresh >= 1:
|
||||
raise ValueError(f"`thresh` must be between 0 and 1, got {thresh}")
|
||||
_validate_type(max_peak_dist, int, "max_peak_dist")
|
||||
_validate_type(voxels_max, int, "voxels_max")
|
||||
_validate_type(use_min, bool, "use_min")
|
||||
|
||||
# load image and make sure it's in surface RAS
|
||||
if not isinstance(base_image, nib.spatialimages.SpatialImage):
|
||||
base_image = nib.load(base_image)
|
||||
|
||||
base_image_mgh = nib.MGHImage(
|
||||
np.array(base_image.dataobj).astype(np.float32), base_image.affine
|
||||
)
|
||||
del base_image
|
||||
|
||||
# get montage channel coordinates
|
||||
ch_dict = montage.get_positions()
|
||||
if ch_dict["coord_frame"] != "mri":
|
||||
bad_coord_frames = np.unique([d["coord_frame"] for d in montage.dig])
|
||||
bad_coord_frames = ", ".join(
|
||||
[
|
||||
_frame_to_str[cf] if cf in _frame_to_str else str(cf)
|
||||
for cf in bad_coord_frames
|
||||
]
|
||||
)
|
||||
raise RuntimeError(
|
||||
f'Coordinate frame not supported, expected "mri", got {bad_coord_frames}'
|
||||
)
|
||||
|
||||
ch_names = list(ch_dict["ch_pos"].keys())
|
||||
ch_coords = np.array([ch_dict["ch_pos"][name] for name in ch_names])
|
||||
|
||||
# convert to voxel space
|
||||
ch_coords = apply_trans(
|
||||
np.linalg.inv(base_image_mgh.header.get_vox2ras_tkr()), ch_coords * 1000
|
||||
)
|
||||
|
||||
# take channel coordinates and use the image to transform them
|
||||
# into a volume where all the voxels over a threshold nearby
|
||||
# are labeled with an index
|
||||
image_data = np.array(base_image_mgh.dataobj)
|
||||
if use_min:
|
||||
image_data *= -1
|
||||
elec_image = np.zeros(base_image_mgh.shape, dtype=int)
|
||||
for i, ch_coord in enumerate(ch_coords):
|
||||
if np.isnan(ch_coord).any():
|
||||
continue
|
||||
# this looks up to a voxel away, it may be marked imperfectly
|
||||
volume = _voxel_neighbors(
|
||||
ch_coord,
|
||||
image_data,
|
||||
thresh=thresh,
|
||||
max_peak_dist=max_peak_dist,
|
||||
voxels_max=voxels_max,
|
||||
)
|
||||
for voxel in volume:
|
||||
if elec_image[voxel] != 0:
|
||||
# some voxels ambiguous because the contacts are bridged on
|
||||
# the image so assign the voxel to the nearest contact location
|
||||
dist_old = np.sqrt(
|
||||
(ch_coords[elec_image[voxel] - 1] - voxel) ** 2
|
||||
).sum()
|
||||
dist_new = np.sqrt((ch_coord - voxel) ** 2).sum()
|
||||
if dist_new < dist_old:
|
||||
elec_image[voxel] = i + 1
|
||||
else:
|
||||
elec_image[voxel] = i + 1
|
||||
|
||||
# assemble the volume
|
||||
elec_image = nib.spatialimages.SpatialImage(elec_image, base_image_mgh.affine)
|
||||
_warn_missing_chs(montage, elec_image, after_warp=False)
|
||||
|
||||
return elec_image
|
||||
Reference in New Issue
Block a user