# Authors: The MNE-Python contributors. # License: BSD-3-Clause # Copyright the MNE-Python contributors. import inspect import os import os.path as op import platform import queue import re import threading import time from contextlib import contextmanager from functools import partial from pathlib import Path import numpy as np from traitlets import Bool, Float, HasTraits, Unicode, observe from .._fiff.constants import FIFF from .._fiff.meas_info import _empty_info, read_fiducials, read_info, write_fiducials from .._fiff.open import dir_tree_find, fiff_open from .._fiff.pick import pick_types from ..bem import make_bem_solution, write_bem_solution from ..channels import read_dig_fif from ..coreg import ( Coregistration, _find_head_bem, _is_mri_subject, _map_fid_name_to_idx, _mri_subject_has_bem, bem_fname, fid_fname, scale_mri, ) from ..defaults import DEFAULTS from ..io._read_raw import _get_supported, read_raw from ..surface import _CheckInside, _DistanceQuery from ..transforms import ( _ensure_trans, _get_trans, _get_transforms_to_coord_frame, read_trans, rotation_angles, write_trans, ) from ..utils import ( _check_fname, _validate_type, check_fname, fill_doc, get_subjects_dir, logger, verbose, ) from ..viz._3d import ( _plot_head_fiducials, _plot_head_shape_points, _plot_head_surface, _plot_helmet, _plot_hpi_coils, _plot_mri_fiducials, _plot_sensors_3d, ) from ..viz.backends._utils import _qt_app_exec, _qt_safe_window from ..viz.utils import safe_event class _WorkerData: def __init__(self, name, params=None): self._name = name self._params = params def _get_subjects(sdir): # XXX: would be nice to move this function to util is_dir = sdir and op.isdir(sdir) if is_dir: dir_content = os.listdir(sdir) subjects = [s for s in dir_content if _is_mri_subject(s, sdir)] if len(subjects) == 0: subjects.append("") else: subjects = [""] return sorted(subjects) @fill_doc class CoregistrationUI(HasTraits): """Class for coregistration assisted by graphical interface. Parameters ---------- info_file : None | str The FIFF file with digitizer data for coregistration. %(subject)s %(subjects_dir)s %(fiducials)s head_resolution : bool If True, use a high-resolution head surface. Defaults to False. head_opacity : float The opacity of the head surface. Defaults to 0.8. hpi_coils : bool If True, display the HPI coils. Defaults to True. head_shape_points : bool If True, display the head shape points. Defaults to True. eeg_channels : bool If True, display the EEG channels. Defaults to True. meg_channels : bool If True, display the MEG channels. Defaults to False. fnirs_channels : bool If True, display the fNIRS channels. Defaults to True. orient_glyphs : bool If True, orient the sensors towards the head surface. Default to False. scale_by_distance : bool If True, scale the sensors based on their distance to the head surface. Defaults to True. mark_inside : bool If True, mark the head shape points that are inside the head surface with a different color. Defaults to True. sensor_opacity : float The opacity of the sensors between 0 and 1. Defaults to 1.0. trans : path-like The path to the Head<->MRI transform FIF file ("-trans.fif"). size : tuple The dimensions (width, height) of the rendering view. The default is (800, 600). bgcolor : tuple | str The background color as a tuple (red, green, blue) of float values between 0 and 1 or a valid color name (i.e. 'white' or 'w'). Defaults to 'grey'. show : bool Display the window as soon as it is ready. Defaults to True. block : bool Whether to halt program execution until the GUI has been closed (``True``) or not (``False``, default). %(fullscreen)s The default is False. .. versionadded:: 1.1 %(interaction_scene)s Defaults to ``'terrain'``. .. versionadded:: 1.0 %(verbose)s Attributes ---------- coreg : mne.coreg.Coregistration The coregistration instance used by the graphical interface. """ _subject = Unicode() _subjects_dir = Unicode() _lock_fids = Bool() _current_fiducial = Unicode() _info_file = Unicode() _orient_glyphs = Bool() _scale_by_distance = Bool() _mark_inside = Bool() _hpi_coils = Bool() _head_shape_points = Bool() _eeg_channels = Bool() _meg_channels = Bool() _fnirs_channels = Bool() _head_resolution = Bool() _head_opacity = Float() _helmet = Bool() _grow_hair = Float() _subject_to = Unicode() _scale_mode = Unicode() _icp_fid_match = Unicode() @_qt_safe_window( splash="_renderer.figure.splash", window="_renderer.figure.plotter" ) @verbose def __init__( self, info_file, *, subject=None, subjects_dir=None, fiducials="auto", head_resolution=None, head_opacity=None, hpi_coils=None, head_shape_points=None, eeg_channels=None, meg_channels=None, fnirs_channels=None, orient_glyphs=None, scale_by_distance=None, mark_inside=None, sensor_opacity=None, trans=None, size=None, bgcolor=None, show=True, block=False, fullscreen=False, interaction="terrain", verbose=None, ): from ..viz.backends.renderer import _get_renderer def _get_default(var, val): return var if var is not None else val self._actors = dict() self._surfaces = dict() self._widgets = dict() self._verbose = verbose self._plot_locked = False self._params_locked = False self._refresh_rate_ms = max(int(round(1000.0 / 60.0)), 1) self._redraws_pending = set() self._parameter_mutex = threading.Lock() self._redraw_mutex = threading.Lock() self._job_queue = queue.Queue() self._parameter_queue = queue.Queue() self._head_geo = None self._check_inside = None self._nearest = None self._coord_frame = "mri" self._mouse_no_mvt = -1 self._to_cf_t = None self._omit_hsp_distance = 0.0 self._fiducials_file = None self._trans_modified = False self._mri_fids_modified = False self._mri_scale_modified = False self._accept_close_event = True self._fid_colors = tuple( DEFAULTS["coreg"][f"{key}_color"] for key in ("lpa", "nasion", "rpa") ) self._defaults = dict( size=_get_default(size, (800, 600)), bgcolor=_get_default(bgcolor, "grey"), orient_glyphs=_get_default(orient_glyphs, True), scale_by_distance=_get_default(scale_by_distance, True), mark_inside=_get_default(mark_inside, True), hpi_coils=_get_default(hpi_coils, True), head_shape_points=_get_default(head_shape_points, True), eeg_channels=_get_default(eeg_channels, True), meg_channels=_get_default(meg_channels, False), fnirs_channels=_get_default(fnirs_channels, True), head_resolution=_get_default(head_resolution, True), head_opacity=_get_default(head_opacity, 0.8), helmet=False, sensor_opacity=_get_default(sensor_opacity, 1.0), fiducials=("LPA", "Nasion", "RPA"), fiducial="LPA", lock_fids=True, grow_hair=0.0, subject_to="", scale_modes=["None", "uniform", "3-axis"], scale_mode="None", icp_fid_matches=("nearest", "matched"), icp_fid_match="matched", icp_n_iterations=20, omit_hsp_distance=10.0, lock_head_opacity=self._head_opacity < 1.0, weights=dict( lpa=1.0, nasion=10.0, rpa=1.0, hsp=1.0, eeg=1.0, hpi=1.0, ), ) # process requirements info = None subjects_dir = str( get_subjects_dir(subjects_dir=subjects_dir, raise_error=True) ) subject = _get_default(subject, _get_subjects(subjects_dir)[0]) # setup the window splash = "Initializing coregistration GUI..." if show else False self._renderer = _get_renderer( size=self._defaults["size"], bgcolor=self._defaults["bgcolor"], splash=splash, fullscreen=fullscreen, ) self._renderer._window_close_connect(self._clean) self._renderer._window_close_connect(self._close_callback, after=False) self._renderer.set_interaction(interaction) # coregistration model setup self._immediate_redraw = self._renderer._kind != "qt" self._info = info self._fiducials = fiducials self.coreg = Coregistration( info=self._info, subject=subject, subjects_dir=subjects_dir, fiducials=fiducials, on_defects="ignore", # safe due to interactive visual inspection ) fid_accurate = self.coreg._fid_accurate for fid in self._defaults["weights"].keys(): setattr(self, f"_{fid}_weight", self._defaults["weights"][fid]) # set main traits self._set_head_opacity(self._defaults["head_opacity"]) self._old_head_opacity = self._head_opacity self._set_subjects_dir(subjects_dir) self._set_subject(subject) self._set_info_file(info_file) self._set_orient_glyphs(self._defaults["orient_glyphs"]) self._set_scale_by_distance(self._defaults["scale_by_distance"]) self._set_mark_inside(self._defaults["mark_inside"]) self._set_hpi_coils(self._defaults["hpi_coils"]) self._set_head_shape_points(self._defaults["head_shape_points"]) self._set_eeg_channels(self._defaults["eeg_channels"]) self._set_meg_channels(self._defaults["meg_channels"]) self._set_fnirs_channels(self._defaults["fnirs_channels"]) self._set_head_resolution(self._defaults["head_resolution"]) self._set_helmet(self._defaults["helmet"]) self._set_grow_hair(self._defaults["grow_hair"]) self._set_omit_hsp_distance(self._defaults["omit_hsp_distance"]) self._set_icp_n_iterations(self._defaults["icp_n_iterations"]) self._set_icp_fid_match(self._defaults["icp_fid_match"]) # configure UI self._reset_fitting_parameters() self._configure_dialogs() self._configure_status_bar() self._configure_dock() self._configure_picking() self._configure_legend() # once the docks are initialized self._set_current_fiducial(self._defaults["fiducial"]) self._set_scale_mode(self._defaults["scale_mode"]) self._set_subject_to(self._defaults["subject_to"]) if trans is not None: self._load_trans(trans) self._redraw() # we need the elements to be present now if fid_accurate: assert self.coreg._fid_filename is not None # _set_fiducials_file() calls _update_fiducials_label() # internally self._set_fiducials_file(self.coreg._fid_filename) else: self._set_head_resolution("high") self._forward_widget_command("high_res_head", "set_value", True) self._set_lock_fids(True) # hack to make the dig disappear self._update_fiducials_label() self._update_fiducials() self._set_lock_fids(fid_accurate) # configure worker self._configure_worker() # must be done last if show: self._renderer.show() # update the view once shown views = { True: dict(azimuth=90, elevation=90), # front False: dict(azimuth=180, elevation=90), } # left self._renderer.set_camera(distance="auto", **views[self._lock_fids]) self._redraw() # XXX: internal plotter/renderer should not be exposed if not self._immediate_redraw: self._renderer.plotter.add_callback(self._redraw, self._refresh_rate_ms) self._renderer.plotter.show_axes() # initialization does not count as modification by the user self._trans_modified = False self._mri_fids_modified = False self._mri_scale_modified = False if block and self._renderer._kind != "notebook": _qt_app_exec(self._renderer.figure.store["app"]) def _set_subjects_dir(self, subjects_dir): if subjects_dir is None or not subjects_dir: return try: subjects_dir = str( _check_fname( subjects_dir, overwrite="read", must_exist=True, need_dir=True, ) ) subjects = _get_subjects(subjects_dir) low_res_path = _find_head_bem(subjects[0], subjects_dir, high_res=False) high_res_path = _find_head_bem(subjects[0], subjects_dir, high_res=True) valid = low_res_path is not None or high_res_path is not None except Exception: valid = False if valid: style = dict(border="initial") self._subjects_dir = subjects_dir else: style = dict(border="2px solid #ff0000") self._forward_widget_command("subjects_dir_field", "set_style", style) def _set_subject(self, subject): self._subject = subject def _set_lock_fids(self, state): self._lock_fids = bool(state) def _set_fiducials_file(self, fname): if fname is None: fids = "auto" else: fname = str( _check_fname( fname, overwrite="read", must_exist=True, need_dir=False, ) ) fids, _ = read_fiducials(fname) self._fiducials_file = fname self.coreg._setup_fiducials(fids) self._update_distance_estimation() self._update_fiducials_label() self._update_fiducials() self._reset(keep_trans=True) if fname is None: self._set_lock_fids(False) self._forward_widget_command("reload_mri_fids", "set_enabled", False) else: self._set_lock_fids(True) self._forward_widget_command("reload_mri_fids", "set_enabled", True) self._display_message(f"Loading MRI fiducials from {fname}... Done!") def _set_current_fiducial(self, fid): self._current_fiducial = fid.lower() def _set_info_file(self, fname): if fname is None: return # info file can be anything supported by read_raw supported = _get_supported() try: check_fname( fname, "info", tuple(supported), endings_err=tuple(supported), ) fname = str(_check_fname(fname, overwrite="read")) # cast to str # ctf ds `files` are actually directories if fname.endswith((".ds",)): info_file = _check_fname( fname, overwrite="read", must_exist=True, need_dir=True ) else: info_file = _check_fname( fname, overwrite="read", must_exist=True, need_dir=False ) valid = True except OSError: valid = False if valid: style = dict(border="initial") self._info_file = str(info_file) else: style = dict(border="2px solid #ff0000") self._forward_widget_command("info_file_field", "set_style", style) def _set_omit_hsp_distance(self, distance): self._omit_hsp_distance = distance def _set_orient_glyphs(self, state): self._orient_glyphs = bool(state) def _set_scale_by_distance(self, state): self._scale_by_distance = bool(state) def _set_mark_inside(self, state): self._mark_inside = bool(state) def _set_hpi_coils(self, state): self._hpi_coils = bool(state) def _set_head_shape_points(self, state): self._head_shape_points = bool(state) def _set_eeg_channels(self, state): self._eeg_channels = bool(state) def _set_meg_channels(self, state): self._meg_channels = bool(state) def _set_fnirs_channels(self, state): self._fnirs_channels = bool(state) def _set_head_resolution(self, state): self._head_resolution = bool(state) def _set_head_opacity(self, value): self._head_opacity = value def _set_helmet(self, state): self._helmet = bool(state) def _set_grow_hair(self, value): self._grow_hair = value def _set_subject_to(self, value): self._subject_to = value self._forward_widget_command("save_subject", "set_enabled", len(value) > 0) if self._check_subject_exists(): style = dict(border="2px solid #ff0000") else: style = dict(border="initial") self._forward_widget_command("subject_to", "set_style", style) def _set_scale_mode(self, mode): self._scale_mode = mode def _set_fiducial(self, value, coord): self._mri_fids_modified = True fid = self._current_fiducial fid_idx = _map_fid_name_to_idx(name=fid) coords = ["X", "Y", "Z"] coord_idx = coords.index(coord) self.coreg.fiducials.dig[fid_idx]["r"][coord_idx] = value / 1e3 self._update_plot("mri_fids") def _set_parameter(self, value, mode_name, coord, plot_locked=False): if mode_name == "scale": self._mri_scale_modified = True else: self._trans_modified = True if self._params_locked: return if mode_name == "scale" and self._scale_mode == "uniform": with self._lock(params=True): self._forward_widget_command(["sY", "sZ"], "set_value", value) with self._parameter_mutex: self._set_parameter_safe(value, mode_name, coord) if not plot_locked: self._update_plot("sensors") def _set_parameter_safe(self, value, mode_name, coord): params = dict( rotation=self.coreg._rotation, translation=self.coreg._translation, scale=self.coreg._scale, ) idx = ["X", "Y", "Z"].index(coord) if mode_name == "rotation": params[mode_name][idx] = np.deg2rad(value) elif mode_name == "translation": params[mode_name][idx] = value / 1e3 else: assert mode_name == "scale" if self._scale_mode == "uniform": params[mode_name][:] = value / 1e2 else: params[mode_name][idx] = value / 1e2 self._update_plot("head") self.coreg._update_params( rot=params["rotation"], tra=params["translation"], sca=params["scale"], ) def _set_icp_n_iterations(self, n_iterations): self._icp_n_iterations = n_iterations def _set_icp_fid_match(self, method): self._icp_fid_match = method def _set_point_weight(self, weight, point): funcs = { "hpi": "_set_hpi_coils", "hsp": "_set_head_shape_points", "eeg": "_set_eeg_channels", "meg": "_set_meg_channels", "fnirs": "_set_fnirs_channels", } if point in funcs.keys(): getattr(self, funcs[point])(weight > 0) setattr(self, f"_{point}_weight", weight) setattr(self.coreg, f"_{point}_weight", weight) self._update_distance_estimation() @observe("_subjects_dir") def _subjects_dir_changed(self, change=None): # XXX: add coreg.set_subjects_dir self.coreg._subjects_dir = self._subjects_dir subjects = _get_subjects(self._subjects_dir) if self._subject not in subjects: # Just pick the first available one self._subject = subjects[0] self._reset() @observe("_subject") def _subject_changed(self, change=None): # XXX: add coreg.set_subject() self.coreg._subject = self._subject self.coreg._setup_bem() self.coreg._setup_fiducials(self._fiducials) self._reset() default_fid_fname = fid_fname.format( subjects_dir=self._subjects_dir, subject=self._subject ) if Path(default_fid_fname).exists(): fname = default_fid_fname else: fname = None self._set_fiducials_file(fname) self._reset_fiducials() @observe("_lock_fids") def _lock_fids_changed(self, change=None): locked_widgets = [ # MRI fiducials "save_mri_fids", # View options "helmet", "meg", "head_opacity", "high_res_head", # Digitization source "info_file", "grow_hair", "omit_distance", "omit", "reset_omit", # Scaling "scaling_mode", "sX", "sY", "sZ", # Transformation "tX", "tY", "tZ", "rX", "rY", "rZ", # Fitting buttons "fit_fiducials", "fit_icp", # Transformation I/O "save_trans", "load_trans", "reset_trans", # ICP "icp_n_iterations", "icp_fid_match", "reset_fitting_options", # Weights "hsp_weight", "eeg_weight", "hpi_weight", "lpa_weight", "nasion_weight", "rpa_weight", ] fits_widgets = ["fits_fiducials", "fits_icp"] fid_widgets = ["fid_X", "fid_Y", "fid_Z", "fids_file", "fids"] if self._lock_fids: self._forward_widget_command(locked_widgets, "set_enabled", True) self._forward_widget_command( "head_opacity", "set_value", self._old_head_opacity ) self._scale_mode_changed() self._display_message() self._update_distance_estimation() else: self._old_head_opacity = self._head_opacity self._forward_widget_command("head_opacity", "set_value", 1.0) self._forward_widget_command(locked_widgets, "set_enabled", False) self._forward_widget_command(fits_widgets, "set_enabled", False) self._display_message( f"Placing MRI fiducials - {self._current_fiducial.upper()}" ) self._set_sensors_visibility(self._lock_fids) self._forward_widget_command("lock_fids", "set_value", self._lock_fids) self._forward_widget_command(fid_widgets, "set_enabled", not self._lock_fids) @observe("_current_fiducial") def _current_fiducial_changed(self, change=None): self._update_fiducials() self._follow_fiducial_view() if not self._lock_fids: self._display_message( f"Placing MRI fiducials - {self._current_fiducial.upper()}" ) @observe("_info_file") def _info_file_changed(self, change=None): if not self._info_file: return elif self._info_file.endswith((".fif", ".fif.gz")): fid, tree, _ = fiff_open(self._info_file) fid.close() if len(dir_tree_find(tree, FIFF.FIFFB_MEAS_INFO)) > 0: self._info = read_info(self._info_file, verbose=False) elif len(dir_tree_find(tree, FIFF.FIFFB_ISOTRAK)) > 0: self._info = _empty_info(1) self._info["dig"] = read_dig_fif(fname=self._info_file).dig self._info._unlocked = False else: self._info = read_raw(self._info_file).info # XXX: add coreg.set_info() self.coreg._info = self._info self.coreg._setup_digs() self._reset() @observe("_orient_glyphs") def _orient_glyphs_changed(self, change=None): self._update_plot(["hpi", "hsp", "sensors"]) @observe("_scale_by_distance") def _scale_by_distance_changed(self, change=None): self._update_plot(["hpi", "hsp", "sensors"]) @observe("_mark_inside") def _mark_inside_changed(self, change=None): self._update_plot("hsp") @observe("_hpi_coils") def _hpi_coils_changed(self, change=None): self._update_plot("hpi") @observe("_head_shape_points") def _head_shape_point_changed(self, change=None): self._update_plot("hsp") @observe("_eeg_channels") def _eeg_channels_changed(self, change=None): self._update_plot("sensors") @observe("_meg_channels") def _meg_channels_changed(self, change=None): self._update_plot("sensors") @observe("_fnirs_channels") def _fnirs_channels_changed(self, change=None): self._update_plot("sensors") @observe("_head_resolution") def _head_resolution_changed(self, change=None): self._update_plot(["head", "hsp"]) @observe("_head_opacity") def _head_opacity_changed(self, change=None): if "head" in self._actors: self._actors["head"].GetProperty().SetOpacity(self._head_opacity) self._renderer._update() @observe("_helmet") def _helmet_changed(self, change=None): self._update_plot("helmet") @observe("_grow_hair") def _grow_hair_changed(self, change=None): self.coreg.set_grow_hair(self._grow_hair) self._update_plot("head") self._update_plot("hsp") # inside/outside could change @observe("_scale_mode") def _scale_mode_changed(self, change=None): locked_widgets = ["sX", "sY", "sZ", "fits_icp", "subject_to"] mode = None if self._scale_mode == "None" else self._scale_mode self.coreg.set_scale_mode(mode) if self._lock_fids: self._forward_widget_command( locked_widgets, "set_enabled", mode is not None ) self._forward_widget_command( "fits_fiducials", "set_enabled", mode not in (None, "3-axis") ) if self._scale_mode == "uniform": self._forward_widget_command(["sY", "sZ"], "set_enabled", False) @observe("_icp_fid_match") def _icp_fid_match_changed(self, change=None): self.coreg.set_fid_match(self._icp_fid_match) def _run_worker(self, queue, jobs): while True: data = queue.get() func = jobs[data._name] if data._params is not None: func(**data._params) else: func() queue.task_done() def _configure_dialogs(self): from ..viz.backends.renderer import MNE_3D_BACKEND_TESTING for name, buttons in zip( ["overwrite_subject", "overwrite_subject_exit"], [["Yes", "No"], ["Yes", "Discard", "Cancel"]], ): self._widgets[name] = self._renderer._dialog_create( title="CoregistrationUI", text="The name of the output subject used to " "save the scaled anatomy already exists.", info_text="Do you want to overwrite?", callback=self._overwrite_subject_callback, buttons=buttons, modal=not MNE_3D_BACKEND_TESTING, ) def _configure_worker(self): work_plan = { "_job_queue": dict(save_subject=self._save_subject), "_parameter_queue": dict(set_parameter=self._set_parameter), } for queue_name, jobs in work_plan.items(): t = threading.Thread( target=partial( self._run_worker, queue=getattr(self, queue_name), jobs=jobs, ) ) t.daemon = True t.start() def _configure_picking(self): self._renderer._update_picking_callback( self._on_mouse_move, self._on_button_press, self._on_button_release, self._on_pick, ) def _configure_legend(self): colors = [ np.array(DEFAULTS["coreg"][f"{fid.lower()}_color"]).astype(float) for fid in self._defaults["fiducials"] ] labels = list(zip(self._defaults["fiducials"], colors)) mri_fids_legend_actor = self._renderer.legend(labels=labels) self._update_actor("mri_fids_legend", mri_fids_legend_actor) @safe_event @verbose def _redraw(self, *, verbose=None): if not self._redraws_pending: return draw_map = dict( head=self._add_head_surface, mri_fids=self._add_mri_fiducials, hsp=self._add_head_shape_points, hpi=self._add_hpi_coils, sensors=self._add_channels, head_fids=self._add_head_fiducials, helmet=self._add_helmet, ) with self._redraw_mutex: # We need at least "head" before "hsp", because the grow_hair param # for head sets the rr that are used for inside/outside hsp redraws_ordered = sorted( self._redraws_pending, key=lambda key: list(draw_map).index(key) ) logger.debug(f"Redrawing {redraws_ordered}") for ki, key in enumerate(redraws_ordered): logger.debug(f"{ki}. Drawing {repr(key)}") draw_map[key]() self._redraws_pending.clear() self._renderer._update() # necessary for MacOS if platform.system() == "Darwin": self._renderer._process_events() def _on_mouse_move(self, vtk_picker, event): if self._mouse_no_mvt: self._mouse_no_mvt -= 1 def _on_button_press(self, vtk_picker, event): self._mouse_no_mvt = 2 def _on_button_release(self, vtk_picker, event): if self._mouse_no_mvt > 0: x, y = vtk_picker.GetEventPosition() # XXX: internal plotter/renderer should not be exposed plotter = self._renderer.figure.plotter picked_renderer = self._renderer.figure.plotter.renderer # trigger the pick plotter.picker.Pick(x, y, 0, picked_renderer) self._mouse_no_mvt = 0 def _on_pick(self, vtk_picker, event): if self._lock_fids: return # XXX: taken from Brain, can be refactored cell_id = vtk_picker.GetCellId() mesh = vtk_picker.GetDataSet() if mesh is None or cell_id == -1 or not self._mouse_no_mvt: return if not getattr(mesh, "_picking_target", False): return pos = np.array(vtk_picker.GetPickPosition()) vtk_cell = mesh.GetCell(cell_id) cell = [ vtk_cell.GetPointId(point_id) for point_id in range(vtk_cell.GetNumberOfPoints()) ] vertices = mesh.points[cell] idx = np.argmin(abs(vertices - pos), axis=0) vertex_id = cell[idx[0]] fiducials = [s.lower() for s in self._defaults["fiducials"]] idx = fiducials.index(self._current_fiducial.lower()) # XXX: add coreg.set_fids self.coreg._fid_points[idx] = self._surfaces["head"].points[vertex_id] self.coreg._reset_fiducials() self._update_fiducials() self._update_plot("mri_fids") def _reset_fitting_parameters(self): self._forward_widget_command( "icp_n_iterations", "set_value", self._defaults["icp_n_iterations"] ) self._forward_widget_command( "icp_fid_match", "set_value", self._defaults["icp_fid_match"] ) weights_widgets = [f"{w}_weight" for w in self._defaults["weights"].keys()] self._forward_widget_command( weights_widgets, "set_value", list(self._defaults["weights"].values()) ) def _reset_fiducials(self): self._set_current_fiducial(self._defaults["fiducial"]) def _omit_hsp(self): self.coreg.omit_head_shape_points(self._omit_hsp_distance / 1e3) n_omitted = np.sum(~self.coreg._extra_points_filter) n_remaining = len(self.coreg._dig_dict["hsp"]) - n_omitted self._update_plot("hsp") self._update_distance_estimation() self._display_message( f"{n_omitted} head shape points omitted, {n_remaining} remaining." ) def _reset_omit_hsp_filter(self): self.coreg._extra_points_filter = None self.coreg._update_params(force_update=True) self._update_plot("hsp") self._update_distance_estimation() n_total = len(self.coreg._dig_dict["hsp"]) self._display_message( f"No head shape point is omitted, the total is {n_total}." ) @verbose def _update_plot(self, changes="all", verbose=None): # Update list of things that need to be updated/plotted (and maybe # draw them immediately) try: fun_name = inspect.currentframe().f_back.f_back.f_code.co_name except Exception: # just in case one of these attrs is missing fun_name = "unknown" logger.debug(f"Updating plots based on {fun_name}: {repr(changes)}") if self._plot_locked: return if self._info is None: changes = ["head", "mri_fids"] self._to_cf_t = dict(mri=dict(trans=np.eye(4)), head=None) else: self._to_cf_t = _get_transforms_to_coord_frame( self._info, self.coreg.trans, coord_frame=self._coord_frame ) all_keys = ( "head", "mri_fids", # MRI first "hsp", "hpi", "sensors", "head_fids", # then dig "helmet", ) if changes == "all": changes = list(all_keys) elif changes == "sensors": changes = all_keys[2:] # omit MRI ones elif isinstance(changes, str): changes = [changes] changes = set(changes) # ideally we would maybe have this in: # with self._redraw_mutex: # it would reduce "jerkiness" of the updates, but this should at least # work okay bad = changes.difference(set(all_keys)) assert len(bad) == 0, f"Unknown changes: {bad}" self._redraws_pending.update(changes) if self._immediate_redraw: self._redraw() @contextmanager def _lock(self, plot=False, params=False, scale_mode=False, fitting=False): """Which part of the UI to temporarily disable.""" if plot: old_plot_locked = self._plot_locked self._plot_locked = True if params: old_params_locked = self._params_locked self._params_locked = True if scale_mode: old_scale_mode = self.coreg._scale_mode self.coreg._scale_mode = None if fitting: widgets = [ "sX", "sY", "sZ", "tX", "tY", "tZ", "rX", "rY", "rZ", "fit_icp", "fit_fiducials", "fits_icp", "fits_fiducials", ] states = [ self._forward_widget_command( w, "is_enabled", None, input_value=False, output_value=True ) for w in widgets ] self._forward_widget_command(widgets, "set_enabled", False) try: yield finally: if plot: self._plot_locked = old_plot_locked if params: self._params_locked = old_params_locked if scale_mode: self.coreg._scale_mode = old_scale_mode if fitting: for idx, w in enumerate(widgets): self._forward_widget_command(w, "set_enabled", states[idx]) def _display_message(self, msg=""): self._forward_widget_command("status_message", "set_value", msg) self._forward_widget_command("status_message", "show", None, input_value=False) self._forward_widget_command( "status_message", "update", None, input_value=False ) if msg: logger.info(msg) def _follow_fiducial_view(self): fid = self._current_fiducial.lower() view = dict(lpa="left", rpa="right", nasion="front") kwargs = dict(front=(90.0, 90.0), left=(180, 90), right=(0.0, 90)) kwargs = dict(zip(("azimuth", "elevation"), kwargs[view[fid]])) if not self._lock_fids: self._renderer.set_camera(distance="auto", **kwargs) def _update_fiducials(self): fid = self._current_fiducial if not fid: return idx = _map_fid_name_to_idx(name=fid) val = self.coreg.fiducials.dig[idx]["r"] * 1e3 with self._lock(plot=True): self._forward_widget_command(["fid_X", "fid_Y", "fid_Z"], "set_value", val) def _update_distance_estimation(self): value = ( self.coreg._get_fiducials_distance_str() + "\n" + self.coreg._get_point_distance_str() ) dists = self.coreg.compute_dig_mri_distances() * 1e3 if self._hsp_weight > 0: if len(dists) == 0: value += "\nNo head shape points found." else: value += ( "\nHSP <-> MRI (mean/min/max): " f"{np.mean(dists):.2f} " f"/ {np.min(dists):.2f} / {np.max(dists):.2f} mm" ) self._forward_widget_command("fit_label", "set_value", value) def _update_parameters(self): with self._lock(plot=True, params=True): # rotation deg = np.rad2deg(self.coreg._rotation) logger.debug(f" Rotation: {deg}") self._forward_widget_command(["rX", "rY", "rZ"], "set_value", deg) # translation mm = self.coreg._translation * 1e3 logger.debug(f" Translation: {mm}") self._forward_widget_command(["tX", "tY", "tZ"], "set_value", mm) # scale sc = self.coreg._scale * 1e2 logger.debug(f" Scale: {sc}") self._forward_widget_command(["sX", "sY", "sZ"], "set_value", sc) def _reset(self, keep_trans=False): """Refresh the scene, and optionally reset transformation & scaling. Parameters ---------- keep_trans : bool Whether to retain translation, rotation, and scaling; or reset them to their default values (no translation, no rotation, no scaling). """ if not keep_trans: self.coreg.set_scale(self.coreg._default_parameters[6:9]) self.coreg.set_rotation(self.coreg._default_parameters[:3]) self.coreg.set_translation(self.coreg._default_parameters[3:6]) self._update_plot() self._update_parameters() self._update_distance_estimation() def _forward_widget_command( self, names, command, value, input_value=True, output_value=False ): """Invoke a method of one or more widgets if the widgets exist. Parameters ---------- names : str | array-like of str The widget names to operate on. command : str The method to invoke. value : object | array-like The value(s) to pass to the method. input_value : bool Whether the ``command`` accepts a ``value``. If ``False``, no ``value`` will be passed to ``command``. output_value : bool Whether to return the return value of ``command``. Returns ------- ret : object | None ``None`` if ``output_value`` is ``False``, and the return value of ``command`` otherwise. """ _validate_type(item=names, types=(str, list), item_name="names") if isinstance(names, str): names = [names] if not isinstance(value, (str, float, int, dict, type(None))): value = list(value) assert len(names) == len(value) for idx, name in enumerate(names): val = value[idx] if isinstance(value, list) else value if name in self._widgets and self._widgets[name] is not None: if input_value: ret = getattr(self._widgets[name], command)(val) else: ret = getattr(self._widgets[name], command)() if output_value: return ret def _set_sensors_visibility(self, state): sensors = [ "head_fiducials", "hpi_coils", "head_shape_points", "sensors", "helmet", ] for sensor in sensors: if sensor in self._actors and self._actors[sensor] is not None: actors = self._actors[sensor] actors = actors if isinstance(actors, list) else [actors] for actor in actors: actor.SetVisibility(state) self._renderer._update() def _update_actor(self, actor_name, actor): # XXX: internal plotter/renderer should not be exposed # Work around PyVista sequential update bug with iterable until > 0.42.3 is req # https://github.com/pyvista/pyvista/pull/5046 actors = self._actors.get(actor_name) or [] # convert None to list if not isinstance(actors, list): actors = [actors] for this_actor in actors: self._renderer.plotter.remove_actor(this_actor, render=False) self._actors[actor_name] = actor def _add_mri_fiducials(self): mri_fids_actors = _plot_mri_fiducials( self._renderer, self.coreg._fid_points, self._subjects_dir, self._subject, self._to_cf_t, self._fid_colors, ) # disable picking on the markers for actor in mri_fids_actors: actor.SetPickable(False) self._update_actor("mri_fiducials", mri_fids_actors) def _add_head_fiducials(self): head_fids_actors = _plot_head_fiducials( self._renderer, self._info, self._to_cf_t, self._fid_colors ) self._update_actor("head_fiducials", head_fids_actors) def _add_hpi_coils(self): if self._hpi_coils: hpi_actors = _plot_hpi_coils( self._renderer, self._info, self._to_cf_t, opacity=self._defaults["sensor_opacity"], scale=DEFAULTS["coreg"]["extra_scale"], orient_glyphs=self._orient_glyphs, scale_by_distance=self._scale_by_distance, surf=self._head_geo, check_inside=self._check_inside, nearest=self._nearest, ) else: hpi_actors = None self._update_actor("hpi_coils", hpi_actors) def _add_head_shape_points(self): if self._head_shape_points: hsp_actors = _plot_head_shape_points( self._renderer, self._info, self._to_cf_t, opacity=self._defaults["sensor_opacity"], orient_glyphs=self._orient_glyphs, scale_by_distance=self._scale_by_distance, mark_inside=self._mark_inside, surf=self._head_geo, mask=self.coreg._extra_points_filter, check_inside=self._check_inside, nearest=self._nearest, ) else: hsp_actors = None self._update_actor("head_shape_points", hsp_actors) def _add_channels(self): plot_types = dict(eeg=False, meg=False, fnirs=False) if self._eeg_channels: plot_types["eeg"] = ["original"] if self._meg_channels: plot_types["meg"] = ["sensors"] if self._fnirs_channels: plot_types["fnirs"] = ["sources", "detectors"] sensor_alpha = dict( eeg=self._defaults["sensor_opacity"], fnirs=self._defaults["sensor_opacity"], meg=0.25, ) picks = pick_types(self._info, ref_meg=False, meg=True, eeg=True, fnirs=True) these_actors = _plot_sensors_3d( self._renderer, self._info, self._to_cf_t, picks=picks, warn_meg=False, head_surf=self._head_geo, units="m", sensor_alpha=sensor_alpha, orient_glyphs=self._orient_glyphs, scale_by_distance=self._scale_by_distance, surf=self._head_geo, check_inside=self._check_inside, nearest=self._nearest, **plot_types, ) sens_actors = sum(these_actors.values(), list()) self._update_actor("sensors", sens_actors) def _add_head_surface(self): bem = None if self._head_resolution: surface = "head-dense" key = "high" else: surface = "head" key = "low" try: head_actor, head_surf, _ = _plot_head_surface( self._renderer, surface, self._subject, self._subjects_dir, bem, self._coord_frame, self._to_cf_t, alpha=self._head_opacity, ) except OSError: head_actor, head_surf, _ = _plot_head_surface( self._renderer, "head", self._subject, self._subjects_dir, bem, self._coord_frame, self._to_cf_t, alpha=self._head_opacity, ) key = "low" self._update_actor("head", head_actor) # mark head surface mesh to restrict picking head_surf._picking_target = True # We need to use _get_processed_mri_points to incorporate grow_hair rr = self.coreg._get_processed_mri_points(key) * self.coreg._scale.T head_surf.points = rr head_surf.compute_normals() self._surfaces["head"] = head_surf tris = self._surfaces["head"].faces.reshape(-1, 4)[:, 1:] assert tris.ndim == 2 and tris.shape[1] == 3, tris.shape nn = self._surfaces["head"].point_normals assert nn.shape == (len(rr), 3), nn.shape self._head_geo = dict(rr=rr, tris=tris, nn=nn) self._check_inside = _CheckInside(head_surf, mode="pyvista") self._nearest = _DistanceQuery(rr) def _add_helmet(self): if self._helmet: logger.debug("Drawing helmet") head_mri_t = _get_trans(self.coreg.trans, "head", "mri")[0] helmet_actor, _, _ = _plot_helmet( self._renderer, self._info, self._to_cf_t, head_mri_t, self._coord_frame ) else: helmet_actor = None self._update_actor("helmet", helmet_actor) def _fit_fiducials(self): with self._lock(scale_mode=True): self._fits_fiducials() def _fits_fiducials(self): with self._lock(params=True, fitting=True): start = time.time() self.coreg.fit_fiducials( lpa_weight=self._lpa_weight, nasion_weight=self._nasion_weight, rpa_weight=self._rpa_weight, verbose=self._verbose, ) end = time.time() self._display_message( f"Fitting fiducials finished in {end - start:.2f} seconds." ) self._update_plot("sensors") self._update_parameters() self._update_distance_estimation() def _fit_icp(self): with self._lock(scale_mode=True): self._fit_icp_real(update_head=False) def _fits_icp(self): self._fit_icp_real(update_head=True) def _fit_icp_real(self, *, update_head): with self._lock(params=True, fitting=True): self._current_icp_iterations = 0 updates = ["hsp", "hpi", "sensors", "head_fids", "helmet"] if update_head: updates.insert(0, "head") def callback(iteration, n_iterations): self._display_message(f"Fitting ICP - iteration {iteration + 1}") self._update_plot(updates) self._current_icp_iterations += 1 self._update_distance_estimation() self._update_parameters() self._renderer._process_events() # allow a draw or cancel start = time.time() self.coreg.fit_icp( n_iterations=self._icp_n_iterations, lpa_weight=self._lpa_weight, nasion_weight=self._nasion_weight, rpa_weight=self._rpa_weight, callback=callback, verbose=self._verbose, ) end = time.time() self._display_message() self._display_message( f"Fitting ICP finished in {end - start:.2f} seconds and " f"{self._current_icp_iterations} iterations." ) del self._current_icp_iterations def _task_save_subject(self): from ..viz.backends.renderer import MNE_3D_BACKEND_TESTING if MNE_3D_BACKEND_TESTING: self._save_subject() else: self._job_queue.put(_WorkerData("save_subject", None)) def _task_set_parameter(self, value, mode_name, coord): from ..viz.backends.renderer import MNE_3D_BACKEND_TESTING if MNE_3D_BACKEND_TESTING: self._set_parameter(value, mode_name, coord, self._plot_locked) else: self._parameter_queue.put( _WorkerData( "set_parameter", dict( value=value, mode_name=mode_name, coord=coord, plot_locked=self._plot_locked, ), ) ) def _overwrite_subject_callback(self, button_name): if button_name == "Yes": self._save_subject_callback(overwrite=True) elif button_name == "Cancel": self._accept_close_event = False else: assert button_name == "No" or button_name == "Discard" def _check_subject_exists(self): if not self._subject_to: return False subject_dirname = os.path.join("{subjects_dir}", "{subject}") dest = subject_dirname.format( subject=self._subject_to, subjects_dir=self._subjects_dir ) return os.path.exists(dest) def _save_subject(self, exit_mode=False): dialog = "overwrite_subject_exit" if exit_mode else "overwrite_subject" if self._check_subject_exists(): self._forward_widget_command(dialog, "show", True) else: self._save_subject_callback() def _save_subject_callback(self, overwrite=False): self._display_message(f"Saving {self._subject_to}...") default_cursor = self._renderer._window_get_cursor() self._renderer._window_set_cursor( self._renderer._window_new_cursor("WaitCursor") ) # prepare bem bem_names = [] if self._scale_mode != "None": can_prepare_bem = _mri_subject_has_bem(self._subject, self._subjects_dir) else: can_prepare_bem = False if can_prepare_bem: pattern = bem_fname.format( subjects_dir=self._subjects_dir, subject=self._subject, name="(.+-bem)" ) bem_dir, pattern = os.path.split(pattern) for filename in os.listdir(bem_dir): match = re.match(pattern, filename) if match: bem_names.append(match.group(1)) # save the scaled MRI try: self._display_message(f"Scaling {self._subject_to}...") scale_mri( subject_from=self._subject, subject_to=self._subject_to, scale=self.coreg._scale, overwrite=overwrite, subjects_dir=self._subjects_dir, skip_fiducials=True, labels=True, annot=True, on_defects="ignore", ) except Exception: logger.error(f"Error scaling {self._subject_to}") bem_names = [] else: self._display_message(f"Scaling {self._subject_to}... Done!") # Precompute BEM solutions for bem_name in bem_names: try: self._display_message(f"Computing {bem_name} solution...") bem_file = bem_fname.format( subjects_dir=self._subjects_dir, subject=self._subject_to, name=bem_name, ) bemsol = make_bem_solution(bem_file) write_bem_solution(bem_file[:-4] + "-sol.fif", bemsol) except Exception: logger.error(f"Error computing {bem_name} solution") else: self._display_message(f"Computing {bem_name} solution... Done!") self._display_message(f"Saving {self._subject_to}... Done!") self._renderer._window_set_cursor(default_cursor) self._mri_scale_modified = False def _save_mri_fiducials(self, fname): self._display_message(f"Saving {fname}...") dig_montage = self.coreg.fiducials write_fiducials( fname=fname, pts=dig_montage.dig, coord_frame="mri", overwrite=True ) self._set_fiducials_file(fname) self._display_message(f"Saving {fname}... Done!") self._mri_fids_modified = False def _save_trans(self, fname): write_trans(fname, self.coreg.trans, overwrite=True) self._display_message(f"{fname} transform file is saved.") self._trans_modified = False def _load_trans(self, fname): mri_head_t = _ensure_trans(read_trans(fname, return_all=True), "mri", "head")[ "trans" ] rot_x, rot_y, rot_z = rotation_angles(mri_head_t) x, y, z = mri_head_t[:3, 3] self.coreg._update_params( rot=np.array([rot_x, rot_y, rot_z]), tra=np.array([x, y, z]), ) self._update_parameters() self._update_distance_estimation() self._update_plot() self._display_message(f"{fname} transform file is loaded.") def _update_fiducials_label(self): if self._fiducials_file is None: text = ( "
No custom MRI fiducials loaded!
" "MRI fiducials could not be found in the standard " "location. The displayed initial MRI fiducial locations " "(diamonds) were derived from fsaverage. Place, lock, and " "save fiducials to discard this message.
" ) else: assert self._fiducials_file == fid_fname.format( subjects_dir=self._subjects_dir, subject=self._subject ) assert self.coreg._fid_accurate is True text = ( f"MRI fiducials (diamonds) loaded from " f"standard location:
" f"{self._fiducials_file}
" ) self._forward_widget_command("mri_fiducials_label", "set_value", text) def _configure_dock(self): if self._renderer._kind == "notebook": collapse = True # collapsible and collapsed else: collapse = None # not collapsible self._renderer._dock_initialize(name="Input", area="left", max_width="375px") mri_subject_layout = self._renderer._dock_add_group_box( name="MRI Subject", collapse=collapse, ) subjects_dir_layout = self._renderer._dock_add_layout(vertical=False) self._widgets["subjects_dir_field"] = self._renderer._dock_add_text( name="subjects_dir_field", value=self._subjects_dir, placeholder="Subjects Directory", callback=self._set_subjects_dir, layout=subjects_dir_layout, ) self._widgets["subjects_dir"] = self._renderer._dock_add_file_button( name="subjects_dir", desc="Load", func=self._set_subjects_dir, is_directory=True, icon=True, tooltip="Load the path to the directory containing the " "FreeSurfer subjects", layout=subjects_dir_layout, ) self._renderer._layout_add_widget( layout=mri_subject_layout, widget=subjects_dir_layout, ) self._widgets["subject"] = self._renderer._dock_add_combo_box( name="Subject", value=self._subject, rng=_get_subjects(self._subjects_dir), callback=self._set_subject, compact=True, tooltip="Select the FreeSurfer subject name", layout=mri_subject_layout, ) mri_fiducials_layout = self._renderer._dock_add_group_box( name="MRI Fiducials", collapse=collapse, ) # Add MRI fiducials I/O widgets self._widgets["mri_fiducials_label"] = self._renderer._dock_add_label( value="", # Will be filled via _update_fiducials_label() layout=mri_fiducials_layout, selectable=True, ) # Reload & Save buttons go into their own layout widget mri_fiducials_button_layout = self._renderer._dock_add_layout(vertical=False) self._renderer._layout_add_widget( layout=mri_fiducials_layout, widget=mri_fiducials_button_layout ) self._widgets["reload_mri_fids"] = self._renderer._dock_add_button( name="Reload MRI Fid.", callback=lambda: self._set_fiducials_file(self._fiducials_file), tooltip="Reload MRI fiducials from the standard location", layout=mri_fiducials_button_layout, ) # Disable reload button until we've actually loaded a fiducial file # (happens in _set_fiducials_file method) self._forward_widget_command("reload_mri_fids", "set_enabled", False) self._widgets["save_mri_fids"] = self._renderer._dock_add_button( name="Save MRI Fid.", callback=lambda: self._save_mri_fiducials( fid_fname.format(subjects_dir=self._subjects_dir, subject=self._subject) ), tooltip="Save MRI fiducials to the standard location. Fiducials " "must be locked first!", layout=mri_fiducials_button_layout, ) self._widgets["lock_fids"] = self._renderer._dock_add_check_box( name="Lock fiducials", value=self._lock_fids, callback=self._set_lock_fids, tooltip="Lock/Unlock interactive fiducial editing", layout=mri_fiducials_layout, ) self._widgets["fids"] = self._renderer._dock_add_radio_buttons( value=self._defaults["fiducial"], rng=self._defaults["fiducials"], callback=self._set_current_fiducial, vertical=False, layout=mri_fiducials_layout, ) fiducial_coords_layout = self._renderer._dock_add_layout() for coord in ("X", "Y", "Z"): name = f"fid_{coord}" self._widgets[name] = self._renderer._dock_add_spin_box( name=coord, value=0.0, rng=[-1e3, 1e3], callback=partial( self._set_fiducial, coord=coord, ), compact=True, double=True, step=1, tooltip=f"Set the {coord} fiducial coordinate", layout=fiducial_coords_layout, ) self._renderer._layout_add_widget(mri_fiducials_layout, fiducial_coords_layout) dig_source_layout = self._renderer._dock_add_group_box( name="Info source with digitization", collapse=collapse, ) info_file_layout = self._renderer._dock_add_layout(vertical=False) self._widgets["info_file_field"] = self._renderer._dock_add_text( name="info_file_field", value=self._info_file, placeholder="Path to info", callback=self._set_info_file, layout=info_file_layout, ) self._widgets["info_file"] = self._renderer._dock_add_file_button( name="info_file", desc="Load", func=self._set_info_file, icon=True, tooltip="Load the FIFF file with digitization data for coregistration", layout=info_file_layout, ) self._renderer._layout_add_widget( layout=dig_source_layout, widget=info_file_layout, ) self._widgets["grow_hair"] = self._renderer._dock_add_spin_box( name="Grow Hair (mm)", value=self._grow_hair, rng=[0.0, 10.0], callback=self._set_grow_hair, tooltip="Compensate for hair on the digitizer head shape", layout=dig_source_layout, ) omit_hsp_layout_1 = self._renderer._dock_add_layout(vertical=False) omit_hsp_layout_2 = self._renderer._dock_add_layout(vertical=False) self._widgets["omit_distance"] = self._renderer._dock_add_spin_box( name="Omit Distance (mm)", value=self._omit_hsp_distance, rng=[0.0, 100.0], callback=self._set_omit_hsp_distance, tooltip="Set the head shape points exclusion distance", layout=omit_hsp_layout_1, ) self._widgets["omit"] = self._renderer._dock_add_button( name="Omit", callback=self._omit_hsp, tooltip="Exclude the head shape points that are far away from " "the MRI head", layout=omit_hsp_layout_2, ) self._widgets["reset_omit"] = self._renderer._dock_add_button( name="Reset", callback=self._reset_omit_hsp_filter, tooltip="Reset all excluded head shape points", layout=omit_hsp_layout_2, ) self._renderer._layout_add_widget(dig_source_layout, omit_hsp_layout_1) self._renderer._layout_add_widget(dig_source_layout, omit_hsp_layout_2) view_options_layout = self._renderer._dock_add_group_box( name="View Options", collapse=collapse, ) self._widgets["helmet"] = self._renderer._dock_add_check_box( name="Show MEG helmet", value=self._helmet, callback=self._set_helmet, tooltip="Enable/Disable MEG helmet", layout=view_options_layout, ) self._widgets["meg"] = self._renderer._dock_add_check_box( name="Show MEG sensors", value=self._helmet, callback=self._set_meg_channels, tooltip="Enable/Disable MEG sensors", layout=view_options_layout, ) self._widgets["high_res_head"] = self._renderer._dock_add_check_box( name="Show high-resolution head", value=self._head_resolution, callback=self._set_head_resolution, tooltip="Enable/Disable high resolution head surface", layout=view_options_layout, ) self._widgets["head_opacity"] = self._renderer._dock_add_slider( name="Head opacity", value=self._head_opacity, rng=[0.25, 1.0], callback=self._set_head_opacity, compact=True, double=True, layout=view_options_layout, ) self._renderer._dock_add_stretch() self._renderer._dock_initialize( name="Parameters", area="right", max_width="375px" ) mri_scaling_layout = self._renderer._dock_add_group_box( name="MRI Scaling", collapse=collapse, ) self._widgets["scaling_mode"] = self._renderer._dock_add_combo_box( name="Scaling Mode", value=self._defaults["scale_mode"], rng=self._defaults["scale_modes"], callback=self._set_scale_mode, tooltip="Select the scaling mode", compact=True, layout=mri_scaling_layout, ) scale_params_layout = self._renderer._dock_add_group_box( name="Scaling Parameters", layout=mri_scaling_layout, ) coords = ["X", "Y", "Z"] for coord in coords: name = f"s{coord}" attr = getattr(self.coreg, "_scale") self._widgets[name] = self._renderer._dock_add_spin_box( name=name, value=attr[coords.index(coord)] * 1e2, rng=[1.0, 10000.0], # percent callback=partial( self._set_parameter, mode_name="scale", coord=coord, ), compact=True, double=True, step=1, tooltip=f"Set the {coord} scaling parameter (in %)", layout=scale_params_layout, ) fit_scale_layout = self._renderer._dock_add_layout(vertical=False) self._widgets["fits_fiducials"] = self._renderer._dock_add_button( name="Fit fiducials with scaling", callback=self._fits_fiducials, tooltip="Find MRI scaling, rotation, and translation to fit all " "3 fiducials", layout=fit_scale_layout, ) self._widgets["fits_icp"] = self._renderer._dock_add_button( name="Fit ICP with scaling", callback=self._fits_icp, tooltip="Find MRI scaling, rotation, and translation to match the " "head shape points", layout=fit_scale_layout, ) self._renderer._layout_add_widget(scale_params_layout, fit_scale_layout) subject_to_layout = self._renderer._dock_add_layout(vertical=False) self._widgets["subject_to"] = self._renderer._dock_add_text( name="subject-to", value=self._subject_to, placeholder="subject name", callback=self._set_subject_to, layout=subject_to_layout, ) self._widgets["save_subject"] = self._renderer._dock_add_button( name="Save scaled anatomy", callback=self._task_save_subject, tooltip="Save scaled anatomy", layout=subject_to_layout, ) self._renderer._layout_add_widget(mri_scaling_layout, subject_to_layout) param_layout = self._renderer._dock_add_group_box( name="Translation (t) and Rotation (r)", collapse=collapse, ) for coord in coords: coord_layout = self._renderer._dock_add_layout(vertical=False) for mode, mode_name in (("t", "Translation"), ("r", "Rotation")): name = f"{mode}{coord}" attr = getattr(self.coreg, f"_{mode_name.lower()}") rng = [-360, 360] if mode_name == "Rotation" else [-100, 100] unit = "°" if mode_name == "Rotation" else "mm" self._widgets[name] = self._renderer._dock_add_spin_box( name=name, value=attr[coords.index(coord)] * 1e3, rng=np.array(rng), callback=partial( self._task_set_parameter, mode_name=mode_name.lower(), coord=coord, ), compact=True, double=True, step=1, tooltip=f"Set the {coord} {mode_name.lower()}" f" parameter (in {unit})", layout=coord_layout, ) self._renderer._layout_add_widget(param_layout, coord_layout) fit_layout = self._renderer._dock_add_layout(vertical=False) self._widgets["fit_fiducials"] = self._renderer._dock_add_button( name="Fit fiducials", callback=self._fit_fiducials, tooltip="Find rotation and translation to fit all 3 fiducials", layout=fit_layout, ) self._widgets["fit_icp"] = self._renderer._dock_add_button( name="Fit ICP", callback=self._fit_icp, tooltip="Find rotation and translation to match the head shape points", layout=fit_layout, ) self._renderer._layout_add_widget(param_layout, fit_layout) trans_layout = self._renderer._dock_add_group_box( name="HEAD <> MRI Transform", collapse=collapse, ) save_trans_layout = self._renderer._dock_add_layout(vertical=False) self._widgets["save_trans"] = self._renderer._dock_add_file_button( name="save_trans", desc="Save...", save=True, func=self._save_trans, tooltip="Save the transform file to disk", layout=save_trans_layout, filter_="Head->MRI transformation (*-trans.fif *_trans.fif)", initial_directory=str(Path(self._info_file).parent), ) self._widgets["load_trans"] = self._renderer._dock_add_file_button( name="load_trans", desc="Load...", func=self._load_trans, tooltip="Load the transform file from disk", layout=save_trans_layout, filter_="Head->MRI transformation (*-trans.fif *_trans.fif)", initial_directory=str(Path(self._info_file).parent), ) self._renderer._layout_add_widget(trans_layout, save_trans_layout) self._widgets["reset_trans"] = self._renderer._dock_add_button( name="Reset Parameters", callback=self._reset, tooltip="Reset all the parameters affecting the coregistration", layout=trans_layout, ) fitting_options_layout = self._renderer._dock_add_group_box( name="Fitting Options", collapse=collapse, ) self._widgets["fit_label"] = self._renderer._dock_add_label( value="", layout=fitting_options_layout, ) self._widgets["icp_n_iterations"] = self._renderer._dock_add_spin_box( name="Number Of ICP Iterations", value=self._defaults["icp_n_iterations"], rng=[1, 100], callback=self._set_icp_n_iterations, compact=True, double=False, tooltip="Set the number of ICP iterations", layout=fitting_options_layout, ) self._widgets["icp_fid_match"] = self._renderer._dock_add_combo_box( name="Fiducial point matching", value=self._defaults["icp_fid_match"], rng=self._defaults["icp_fid_matches"], callback=self._set_icp_fid_match, compact=True, tooltip="Select the fiducial point matching method", layout=fitting_options_layout, ) weights_layout = self._renderer._dock_add_group_box( name="Weights", layout=fitting_options_layout, ) for point, fid in zip(("HSP", "EEG", "HPI"), self._defaults["fiducials"]): weight_layout = self._renderer._dock_add_layout(vertical=False) point_lower = point.lower() name = f"{point_lower}_weight" self._widgets[name] = self._renderer._dock_add_spin_box( name=point, value=getattr(self, f"_{point_lower}_weight"), rng=[0.0, 100.0], callback=partial(self._set_point_weight, point=point_lower), compact=True, double=True, tooltip=f"Set the {point} weight", layout=weight_layout, ) fid_lower = fid.lower() name = f"{fid_lower}_weight" self._widgets[name] = self._renderer._dock_add_spin_box( name=fid, value=getattr(self, f"_{fid_lower}_weight"), rng=[0.0, 100.0], callback=partial(self._set_point_weight, point=fid_lower), compact=True, double=True, tooltip=f"Set the {fid} weight", layout=weight_layout, ) self._renderer._layout_add_widget(weights_layout, weight_layout) self._widgets["reset_fitting_options"] = self._renderer._dock_add_button( name="Reset Fitting Options", callback=self._reset_fitting_parameters, tooltip="Reset all the fitting parameters to default value", layout=fitting_options_layout, ) self._renderer._dock_add_stretch() def _configure_status_bar(self): self._renderer._status_bar_initialize() self._widgets["status_message"] = self._renderer._status_bar_add_label( "", stretch=1 ) self._forward_widget_command( "status_message", "hide", value=None, input_value=False ) def _clean(self): if not self._accept_close_event: return self._renderer = None self._widgets.clear() self._actors.clear() self._surfaces.clear() self._defaults.clear() self._head_geo = None self._check_inside = None self._nearest = None self._redraw = None @safe_event def close(self): """Close interface and cleanup data structure.""" if self._renderer is not None: self._renderer.close() def _close_dialog_callback(self, button_name): from ..viz.backends.renderer import MNE_3D_BACKEND_TESTING self._accept_close_event = True if button_name == "Save": if self._trans_modified: self._forward_widget_command("save_trans", "set_value", None) # cancel means _save_trans is not called if self._trans_modified: self._accept_close_event = False if self._mri_fids_modified: self._forward_widget_command("save_mri_fids", "set_value", None) if self._mri_scale_modified: if self._subject_to: self._save_subject(exit_mode=True) else: dialog = self._renderer._dialog_create( title="CoregistrationUI", text="The name of the output subject used to " "save the scaled anatomy is not set.", info_text="Please set a subject name", callback=lambda x: None, buttons=["Ok"], modal=not MNE_3D_BACKEND_TESTING, ) dialog.show() self._accept_close_event = False elif button_name == "Cancel": self._accept_close_event = False else: assert button_name == "Discard" def _close_callback(self): if self._trans_modified or self._mri_fids_modified or self._mri_scale_modified: from ..viz.backends.renderer import MNE_3D_BACKEND_TESTING # prepare the dialog's text text = "The following is/are not saved:" text += "