Skip to content

Commit

Permalink
enh: finalize migration of reporting interfaces
Browse files Browse the repository at this point in the history
Resolves: #54.
  • Loading branch information
oesteban committed Apr 18, 2023
1 parent dd4fd48 commit 7ea0b3a
Show file tree
Hide file tree
Showing 14 changed files with 1,723 additions and 21 deletions.
40 changes: 40 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ jobs:
steps:
- checkout

- run:
name: Git config - pacify datalad
command: |
git config --global user.name 'NiPreps Bot'
git config --global user.email '[email protected]'
- restore_cache:
keys:
- tf-v0-{{ .Branch }}-{{ .Revision }}
Expand All @@ -32,12 +38,46 @@ jobs:
command: |
python -c "from templateflow.api import get; get('Fischer344', desc=None, suffix='T2w')"
python -c "from templateflow.api import get; get('MNI152NLin6Asym', resolution=2, desc='LR', suffix='T1w')"
python -c "from templateflow.api import get; get('OASIS30ANTs', resolution=1, desc=None, suffix='T1w')"
python -c "from templateflow.api import get; get('OASIS30ANTs', resolution=1, desc='brain', suffix='mask')"
python -c "from templateflow.api import get; get('OASIS30ANTs', resolution=1, label='brain', suffix='probseg')"
python -c "from templateflow.api import get; get('OASIS30ANTs', resolution=1, desc='BrainCerebellumRegistration', suffix='mask')"
python -c "from templateflow.api import get; get('OASIS30ANTs', resolution=1, desc='4', suffix='dseg')"
- save_cache:
key: tf-v0-{{ .Branch }}-{{ .Revision }}
paths:
- /tmp/templateflow

- restore_cache:
keys:
- data-v1-{{ .Branch }}-
- data-v1-master-
- data-v1-
- run:
name: Get test data from ds000003
command: |
mkdir -p /tmp/data
pushd /tmp/data
if [[ ! -d ds000003 ]]; then
datalad install -r https://github.com/nipreps-data/ds000003.git
fi
datalad update -r --merge -d ds000003/
datalad get -J 2 -r -d ds000003/ ds000003/*
popd
- save_cache:
key: data-v1-{{ .Branch }}-{{ epoch }}
paths:
- /tmp/data/ds000003

- run:
name: Store FreeSurfer license file
command: |
mkdir -p /tmp/fslicense
cd /tmp/fslicense
echo "cHJpbnRmICJrcnp5c3p0b2YuZ29yZ29sZXdza2lAZ21haWwuY29tXG41MTcyXG4gKkN2dW12RVYzelRmZ1xuRlM1Si8yYzFhZ2c0RVxuIiA+IGxpY2Vuc2UudHh0Cg==" | base64 -d | sh
- run:
name: Run unit tests
no_output_timeout: 2h
Expand Down
16 changes: 11 additions & 5 deletions nireports/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@
import pytest
import tempfile


# disable ET
os.environ['NO_ET'] = '1'

_datadir = (Path(__file__).parent / "tests" / "data").resolve(strict=True)
niprepsdev_path = os.getenv(
niprepsdev_path = Path(os.getenv(
"TEST_DATA_HOME", str(Path.home() / ".cache" / "nipreps-dev")
)
))
test_output_dir = os.getenv("TEST_OUTPUT_DIR")
test_workdir = os.getenv("TEST_WORK_DIR")

Expand All @@ -50,8 +51,8 @@ def expand_namespace(doctest_namespace):
doctest_namespace["os"] = os
doctest_namespace["pytest"] = pytest
doctest_namespace["Path"] = Path
doctest_namespace["testdata_path"] = _datadir
doctest_namespace["niprepsdev_path"] = niprepsdev_path
doctest_namespace["test_data_package"] = _datadir
doctest_namespace["test_data_home"] = niprepsdev_path

doctest_namespace["os"] = os
doctest_namespace["Path"] = Path
Expand All @@ -72,10 +73,15 @@ def expand_namespace(doctest_namespace):


@pytest.fixture
def testdata_path():
def test_data_package():
return _datadir


@pytest.fixture
def test_data_home():
return niprepsdev_path


@pytest.fixture
def workdir():
return None if test_workdir is None else Path(test_workdir)
Expand Down
Empty file.
199 changes: 199 additions & 0 deletions nireports/interfaces/reporting/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
# vi: set ft=python sts=4 ts=4 sw=4 et:
#
# Copyright 2023 The NiPreps Developers <[email protected]>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# We support and encourage derived works from this project, please read
# about our expectations at
#
# https://www.nipreps.org/community/licensing/
#
"""class mixin and utilities for enabling reports for nipype interfaces."""
from nipype.interfaces.base import File, traits
from nipype.interfaces.mixins import reporting
from nipype import logging
from nireports.reportlets.utils import cuts_from_bbox, compose_view

_LOGGER = logging.getLogger("nipype.interface")


class _SVGReportCapableInputSpec(reporting.ReportCapableInputSpec):
out_report = File(
"report.svg", usedefault=True, desc="filename for the visual report"
)
compress_report = traits.Enum(
"auto",
True,
False,
usedefault=True,
desc="Compress the reportlet using SVGO or"
"WEBP. 'auto' - compress if relevant "
"software is installed, True = force,"
"False - don't attempt to compress",
)


class RegistrationRC(reporting.ReportCapableInterface):
"""An abstract mixin to registration nipype interfaces."""

_fixed_image = None
_moving_image = None
_fixed_image_mask = None
_fixed_image_label = "fixed"
_moving_image_label = "moving"
_contour = None
_dismiss_affine = False

def _generate_report(self):
"""Generate the visual report."""
from nilearn.image import threshold_img, load_img
from nilearn.masking import apply_mask, unmask
from nireports.reportlets.mosaic import plot_registration

_LOGGER.info("Generating visual report")

fixed_image_nii = load_img(self._fixed_image)
moving_image_nii = load_img(self._moving_image)
contour_nii = load_img(self._contour) if self._contour is not None else None

if self._fixed_image_mask:
fixed_image_nii = unmask(
apply_mask(fixed_image_nii, self._fixed_image_mask),
self._fixed_image_mask,
)
# since the moving image is already in the fixed image space we
# should apply the same mask
moving_image_nii = unmask(
apply_mask(moving_image_nii, self._fixed_image_mask),
self._fixed_image_mask,
)
mask_nii = load_img(self._fixed_image_mask)
else:
mask_nii = threshold_img(fixed_image_nii, 1e-3)

n_cuts = 7
if not self._fixed_image_mask and contour_nii:
cuts = cuts_from_bbox(contour_nii, cuts=n_cuts)
else:
cuts = cuts_from_bbox(mask_nii, cuts=n_cuts)

# Call composer
compose_view(
plot_registration(
fixed_image_nii,
"fixed-image",
estimate_brightness=True,
cuts=cuts,
label=self._fixed_image_label,
contour=contour_nii,
compress=self.inputs.compress_report,
dismiss_affine=self._dismiss_affine,
),
plot_registration(
moving_image_nii,
"moving-image",
estimate_brightness=True,
cuts=cuts,
label=self._moving_image_label,
contour=contour_nii,
compress=self.inputs.compress_report,
dismiss_affine=self._dismiss_affine,
),
out_file=self._out_report,
)


class SegmentationRC(reporting.ReportCapableInterface):
"""An abstract mixin to segmentation nipype interfaces."""

def _generate_report(self):
from nireports.reportlets.mosaic import plot_segs

compose_view(
plot_segs(
image_nii=self._anat_file,
seg_niis=self._seg_files,
bbox_nii=self._mask_file,
out_file=self.inputs.out_report,
masked=self._masked,
compress=self.inputs.compress_report,
),
fg_svgs=None,
out_file=self._out_report,
)


class SurfaceSegmentationRC(reporting.ReportCapableInterface):
"""An abstract mixin to registration nipype interfaces."""

_anat_file = None
_mask_file = None
_contour = None

def _generate_report(self):
"""Generate the visual report."""
from nilearn.image import threshold_img, load_img
from nilearn.masking import apply_mask, unmask
from nireports.reportlets.mosaic import plot_registration

_LOGGER.info("Generating visual report")

anat = load_img(self._anat_file)
contour_nii = load_img(self._contour) if self._contour is not None else None

if self._mask_file:
anat = unmask(apply_mask(anat, self._mask_file), self._mask_file)
mask_nii = load_img(self._mask_file)
else:
mask_nii = threshold_img(anat, 1e-3)

n_cuts = 7
if not self._mask_file and contour_nii:
cuts = cuts_from_bbox(contour_nii, cuts=n_cuts)
else:
cuts = cuts_from_bbox(mask_nii, cuts=n_cuts)

# Call composer
compose_view(
plot_registration(
anat,
"fixed-image",
estimate_brightness=True,
cuts=cuts,
contour=contour_nii,
compress=self.inputs.compress_report,
),
[],
out_file=self._out_report,
)


class ReportingInterface(reporting.ReportCapableInterface):
"""
Interface that always generates a report.
A subclass must define an ``input_spec`` and override ``_generate_report``.
"""

output_spec = reporting.ReportCapableOutputSpec

def __init__(self, generate_report=True, **kwargs):
super(ReportingInterface, self).__init__(
generate_report=generate_report, **kwargs
)

def _run_interface(self, runtime):
return runtime
Loading

0 comments on commit 7ea0b3a

Please sign in to comment.