def test_on_missing(): """Test _on_missing.""" msg = 'test' with pytest.raises(ValueError, match=msg): _on_missing('raise', msg) with pytest.warns(RuntimeWarning, match=msg): _on_missing('warn', msg) _on_missing('ignore', msg) with pytest.raises(ValueError, match='Invalid value for the \'on_missing\' parameter'): _on_missing('foo', msg)
def update_anat_landmarks( bids_path, landmarks, *, fs_subject=None, fs_subjects_dir=None, kind=None, on_missing='raise', verbose=None ): """Update the anatomical landmark coordinates of an MRI scan. This will change the ``AnatomicalLandmarkCoordinates`` entry in the respective JSON sidecar file, or create it if it doesn't exist. Parameters ---------- bids_path : BIDSPath Path of the MR image. landmarks : mne.channels.DigMontage | path-like An :class:`mne.channels.DigMontage` instance with coordinates for the nasion and left and right pre-auricular points in MRI voxel coordinates. Alternatively, the path to a ``*-fiducials.fif`` file as produced by the MNE-Python coregistration GUI or via :func:`mne.io.write_fiducials`. .. note:: :func:`mne_bids.get_anat_landmarks` provides a convenient and reliable way to generate the landmark coordinates in the required coordinate system. .. note:: If ``path-like``, ``fs_subject`` and ``fs_subjects_dir`` must be provided as well. .. versionchanged:: 0.10 Added support for ``path-like`` input. fs_subject : str | None The subject identifier used for FreeSurfer. Must be provided if ``landmarks`` is ``path-like``; otherwise, it will be ignored. fs_subjects_dir : path-like | None The FreeSurfer subjects directory. If ``None``, defaults to the ``SUBJECTS_DIR`` environment variable. Must be provided if ``landmarks`` is ``path-like``; otherwise, it will be ignored. kind : str | None The suffix of the anatomical landmark names in the JSON sidecar. A suffix might be present e.g. to distinguish landmarks between sessions. If provided, should not include a leading underscore ``_``. For example, if the landmark names in the JSON sidecar file are ``LPA_ses-1``, ``RPA_ses-1``, ``NAS_ses-1``, you should pass ``'ses-1'`` here. If ``None``, no suffix is appended, the landmarks named ``Nasion`` (or ``NAS``), ``LPA``, and ``RPA`` will be used. .. versionadded:: 0.10 on_missing : 'ignore' | 'warn' | 'raise' How to behave if the specified landmarks cannot be found in the MRI JSON sidecar file. .. versionadded:: 0.10 %(verbose)s Notes ----- .. versionadded:: 0.8 """ _validate_type(item=bids_path, types=BIDSPath, item_name='bids_path') _validate_type( item=landmarks, types=(DigMontage, 'path-like'), item_name='landmarks' ) _check_on_missing(on_missing) # Do some path verifications and fill in some gaps the users might have # left (datatype and extension) # XXX We could be more stringent (and less user-friendly) and insist on a # XXX full specification of all parts of the BIDSPath, thoughts? bids_path_mri = bids_path.copy() if bids_path_mri.datatype is None: bids_path_mri.datatype = 'anat' if bids_path_mri.datatype != 'anat': raise ValueError( f'Can only operate on "anat" MRI data, but the provided bids_path ' f'points to: {bids_path_mri.datatype}') if bids_path_mri.suffix is None: raise ValueError('Please specify the "suffix" entity of the provided ' 'bids_path.') elif bids_path_mri.suffix not in ('T1w', 'FLASH'): raise ValueError( f'Can only operate on "T1w" and "FLASH" images, but the bids_path ' f'suffix indicates: {bids_path_mri.suffix}') valid_extensions = ('.nii', '.nii.gz') tried_paths = [] file_exists = False if bids_path_mri.extension is None: # No extension was provided, start searching … for extension in valid_extensions: bids_path_mri.extension = extension tried_paths.append(bids_path_mri.fpath) if bids_path_mri.fpath.exists(): file_exists = True break else: # An extension was provided tried_paths.append(bids_path_mri.fpath) if bids_path_mri.fpath.exists(): file_exists = True if not file_exists: raise ValueError( f'Could not find an MRI scan. Please check the provided ' f'bids_path. Tried the following filenames: ' f'{", ".join([p.name for p in tried_paths])}') if not isinstance(landmarks, DigMontage): # it's pathlike if fs_subject is None: raise ValueError( 'You must provide the "fs_subject" parameter when passing the ' 'path to fiducials' ) landmarks = _get_landmarks_from_fiducials_file( bids_path=bids_path, fname=landmarks, fs_subject=fs_subject, fs_subjects_dir=fs_subjects_dir ) positions = landmarks.get_positions() coord_frame = positions['coord_frame'] if coord_frame != 'mri_voxel': raise ValueError( f'The landmarks must be specified in MRI voxel coordinates, but ' f'provided DigMontage is in "{coord_frame}"') # Extract the cardinal points name_to_coords_map = { 'LPA': positions['lpa'], 'NAS': positions['nasion'], 'RPA': positions['rpa'] } # Check if coordinates for any cardinal point are missing, and convert to # a list so we can easily store the data in JSON format missing_points = [] for name, coords in name_to_coords_map.items(): if coords is None: missing_points.append(name) else: # Funnily, np.float64 is JSON-serializabe, while np.float32 is not! # Thus, cast to float64 to avoid issues (which e.g. may arise when # fiducials were read from disk!) name_to_coords_map[name] = list(coords.astype('float64')) if missing_points: raise ValueError( f'The provided DigMontage did not contain all required cardinal ' f'points (nasion and left and right pre-auricular points). The ' f'following points are missing: ' f'{", ".join(missing_points)}') bids_path_json = bids_path.copy().update(extension='.json') if not bids_path_json.fpath.exists(): # Must exist before we can update it _write_json(bids_path_json.fpath, dict()) mri_json = json.loads(bids_path_json.fpath.read_text(encoding='utf-8')) if 'AnatomicalLandmarkCoordinates' not in mri_json: _on_missing( on_missing=on_missing, msg=f'No AnatomicalLandmarkCoordinates section found in ' f'{bids_path_json.fpath.name}', error_klass=KeyError ) mri_json['AnatomicalLandmarkCoordinates'] = dict() for name, coords in name_to_coords_map.items(): if kind is not None: name = f'{name}_{kind}' if name not in mri_json['AnatomicalLandmarkCoordinates']: _on_missing( on_missing=on_missing, msg=f'Anatomical landmark not found in ' f'{bids_path_json.fpath.name}: {name}', error_klass=KeyError ) mri_json['AnatomicalLandmarkCoordinates'][name] = coords update_sidecar_json(bids_path=bids_path_json, entries=mri_json)