Exemplo n.º 1
0
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)
Exemplo n.º 2
0
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)