Repository: gift-surg/NiftyMIC
Branch: master
Commit: 553bce0824e7
Files: 127
Total size: 1020.4 KB
Directory structure:
gitextract_rkx8168q/
├── .gitattributes
├── .gitignore
├── .gitlab-ci.yml
├── .gitmodules
├── LICENSE
├── MANIFEST.in
├── README.md
├── data/
│ └── templates/
│ ├── LICENSE
│ ├── README
│ └── templates_info.json
├── doc/
│ └── doxyfile
├── docker/
│ ├── docker-compose.yml
│ ├── itk_niftymic/
│ │ ├── .dockerignore
│ │ └── Dockerfile
│ ├── niftymic/
│ │ ├── .dockerignore
│ │ └── Dockerfile
│ └── simplereg_dependencies/
│ ├── .dockerignore
│ └── Dockerfile
├── niftymic/
│ ├── __about__.py
│ ├── __init__.py
│ ├── application/
│ │ ├── __init__.py
│ │ ├── correct_bias_field.py
│ │ ├── correct_intensities.py
│ │ ├── multiply.py
│ │ ├── nifti2dicom.py
│ │ ├── propagate_mask.py
│ │ ├── reconstruct_volume.py
│ │ ├── reconstruct_volume_from_slices.py
│ │ ├── register_image.py
│ │ ├── rsfmri_estimate_motion.py
│ │ ├── rsfmri_reconstruct_volume_from_slices.py
│ │ ├── run_reconstruction_parameter_study.py
│ │ ├── run_reconstruction_pipeline.py
│ │ ├── segment_fetal_brains.py
│ │ └── show_slice_coverage.py
│ ├── base/
│ │ ├── __init__.py
│ │ ├── data_reader.py
│ │ ├── data_writer.py
│ │ ├── exceptions.py
│ │ ├── psf.py
│ │ ├── slice.py
│ │ └── stack.py
│ ├── definitions.py
│ ├── reconstruction/
│ │ ├── __init__.py
│ │ ├── admm_solver.py
│ │ ├── linear_operators.py
│ │ ├── primal_dual_solver.py
│ │ ├── scattered_data_approximation.py
│ │ ├── solver.py
│ │ └── tikhonov_solver.py
│ ├── registration/
│ │ ├── __init__.py
│ │ ├── flirt.py
│ │ ├── intra_stack_registration.py
│ │ ├── niftyreg.py
│ │ ├── registration_method.py
│ │ ├── simple_itk_registration.py
│ │ ├── stack_registration_base.py
│ │ ├── transform_initializer.py
│ │ └── wrap_itk_registration.py
│ ├── utilities/
│ │ ├── __init__.py
│ │ ├── binary_mask_from_mask_srr_estimator.py
│ │ ├── brain_stripping.py
│ │ ├── data_preprocessing.py
│ │ ├── input_arparser.py
│ │ ├── intensity_correction.py
│ │ ├── joint_image_mask_builder.py
│ │ ├── motion_updater.py
│ │ ├── n4_bias_field_correction.py
│ │ ├── outlier_rejector.py
│ │ ├── parameter_normalization.py
│ │ ├── segmentation_propagation.py
│ │ ├── siena.py
│ │ ├── stack_mask_morphological_operations.py
│ │ ├── target_stack_estimator.py
│ │ ├── template_stack_estimator.py
│ │ ├── toolkit_executor.py
│ │ └── volumetric_reconstruction_pipeline.py
│ └── validation/
│ ├── __init__.py
│ ├── evaluate_image_similarity.py
│ ├── evaluate_simulated_stack_similarity.py
│ ├── evaluate_slice_residual_similarity.py
│ ├── export_side_by_side_simulated_vs_original_slice_comparison.py
│ ├── image_similarity_evaluator.py
│ ├── motion_evaluator.py
│ ├── motion_simulator.py
│ ├── residual_evaluator.py
│ ├── show_evaluated_simulated_stack_similarity.py
│ ├── simulate_stacks_from_reconstruction.py
│ ├── slice_acquisition.py
│ ├── slice_coverage.py
│ └── write_random_motion_transforms.py
├── niftymic_correct_bias_field.py
├── niftymic_multiply.py
├── niftymic_nifti2dicom.py
├── niftymic_reconstruct_volume.py
├── niftymic_reconstruct_volume_from_slices.py
├── niftymic_register_image.py
├── niftymic_rsfmri_estimate_motion.py
├── niftymic_rsfmri_reconstruct_volume_from_slices.py
├── niftymic_run_reconstruction_parameter_study.py
├── niftymic_run_reconstruction_pipeline.py
├── niftymic_segment_fetal_brains.py
├── niftymic_show_reconstruction_parameter_study.py
├── requirements-monaifbs.txt
├── requirements.txt
├── setup.py
└── tests/
├── __init__.py
├── brain_stripping_test.py
├── case_study_fetal_brain_test.py
├── case_study_rsfmri_test.py
├── data_reader_test.py
├── image_similarity_evaluator_test.py
├── installation_test.py
├── installation_test_fetal_brain_seg.py
├── installation_test_monaifbs.py
├── intensity_correction_test.py
├── intra_stack_registration_test.py
├── linear_image_quality_transfer_test.py
├── linear_operators_test.py
├── niftyreg_test.py
├── parameter_normalization_test.py
├── registration_test.py
├── residual_evaluator_test.py
├── run_tests.py
├── segmentation_propagation_test.py
├── simulator_slice_acquisition_test.py
└── stack_test.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
*.pt filter=lfs diff=lfs merge=lfs -text
================================================
FILE: .gitignore
================================================
build/
html/
data/tests/
dist/
latex/
monaifbs/models/
prototyping/
*.nii
*.nii.gz
*.tar.gz
*.deb
*.zip
*.pyc
*iterate.dat
*.egg-info
*.idea
*.wiki
/.project
.DS_Store
**/.DS_Store
================================================
FILE: .gitlab-ci.yml
================================================
# -----------------------------------Set Up------------------------------------
variables:
PY_VERSION: 3
PRIVATE: 0
TMPDIR: ./tmp
DATADIR: /home/mebner/data/ci/FetalBrain
VENV: pysitk-test-py${PY_VERSION}
ITK_DIR: /mnt/shared/mebner/environment/ITK/ITK_NiftyMIC-python${PY_VERSION}-build
FSL_DIR: /mnt/shared/mebner/environment/FSL/fsl
TEST_DIR: /home/mebner/Development/NiftyMIC/data/tests
NIFTYREG_INSTALL: /mnt/shared/mebner/environment/NiftyReg/NiftyReg-master-install
CONVERT3D_INSTALL: /mnt/shared/mebner/environment/Convert3D/c3d-git-install
before_script:
# add NiftyReg to PATH
- export PATH="${NIFTYREG_INSTALL}/bin:$PATH"
# add FSL
- PATH=${FSL_DIR}/bin:${PATH}
- export PATH="${FSL_INSTALL}/bin:$PATH"
- export FSLOUTPUTTYPE=NIFTI_GZ
# add Convert3D to PATH
- export PATH="${CONVERT3D_INSTALL}/bin:$PATH"
# save current folder path
- cwd_dir=$(pwd)
# create virtual environment
- rm -rf ${VENV}
- mypython=$(which python${PY_VERSION})
- virtualenv -p $mypython ${VENV}
- cd ${VENV}
- venv_dir=$(pwd)
- source bin/activate
# print Python version to CI output
- which python
- python --version
# copy ITK_NiftyMIC-build WrapITK to site-packages of python venv
- py_sitepkg=${venv_dir}/lib/python*/site-packages
- cp -v ${ITK_DIR}/Wrapping/Generators/Python/WrapITK.pth ${py_sitepkg}
- cd $cwd_dir
# If PRIVATE is used:
# add CI_JOB_TOKEN for cloning dependent repositories in requirements.txt
# (https://docs.gitlab.com/ee/user/project/new_ci_build_permissions_model.html#dependent-repositories)
- >
(if [ ${PRIVATE} == 1 ];
then sed -i -- "s#github.com/gift-surg#gitlab-ci-token:${CI_JOB_TOKEN}@PRIVATE.cs.ucl.ac.uk/GIFT-Surg#g" requirements.txt;
fi);
# install requirements
- pip install -r requirements.txt
# set environment variables for installation
- export NIFTYMIC_ITK_DIR=$ITK_DIR
# run installation
- pip install -e .
# replace TEST_DIR in niftymic/definitions.py file
- sed -i -- 's:DIR_TEST = os.path.join(DIR_ROOT, "data", "tests"):DIR_TEST = "'"$TEST_DIR"'":g' niftymic/definitions.py
- cat niftymic/definitions.py
after_script:
# delete tmp-directory
- rm -rfv ${TMPDIR}
# ----------------------------------Test Jobs----------------------------------
builddocs:
# only:
# - master
script:
- cd doc
- doxygen doxyfile
tags:
- gift-adelie
installation:
# only:
# - master
script:
- python -m nose tests/installation_test.py
tags:
- gift-adelie
reconstruct_volume_tk1l2:
# only:
# - master
script:
- >
niftymic_reconstruct_volume
--filenames ${DATADIR}/input_data/axial.nii.gz ${DATADIR}/input_data/coronal.nii.gz ${DATADIR}/input_data/sagittal.nii.gz
--output ${TMPDIR}/srr_from_slices_tk1l2.nii.gz
--suffix-mask _mask
--verbose 0
--isotropic-resolution 2
--reconstruction-type TK1L2
--two-step-cycles 1
--iter-max 2
--iter-max-first 2
tags:
- gift-adelie
reconstruct_volume_huberl2:
# only:
# - master
script:
- >
niftymic_reconstruct_volume
--filenames ${DATADIR}/input_data/axial.nii.gz ${DATADIR}/input_data/coronal.nii.gz ${DATADIR}/input_data/sagittal.nii.gz
--output ${TMPDIR}/srr_from_slices_huberl2.nii.gz
--suffix-mask _mask
--verbose 0
--isotropic-resolution 2
--reconstruction-type HuberL2
--two-step-cycles 1
--iter-max 2
--iter-max-first 2
--iterations 1
tags:
- gift-adelie
reconstruct_volume_from_slices:
# only:
# - master
script:
- >
niftymic_reconstruct_volume_from_slices
--filenames ${DATADIR}/input_data/axial.nii.gz ${DATADIR}/input_data/coronal.nii.gz ${DATADIR}/input_data/sagittal.nii.gz
--dir-input-mc ${DATADIR}/motion_correction_oriented
--suffix-mask _mask
--reconstruction-space ${DATADIR}/SRR_stacks3_TK1_lsmr_alpha0p03_itermax10_oriented.nii.gz
--output ${TMPDIR}/srr_from_slices.nii.gz
--verbose 0
--isotropic-resolution 2
--iter-max 2
tags:
- gift-adelie
run_reconstruction_pipeline:
# only:
# - master
script:
- >
niftymic_run_reconstruction_pipeline
--filenames ${DATADIR}/input_data/axial.nii.gz
--filenames-masks ${DATADIR}/input_data/axial_mask.nii.gz
--dir-output ${TMPDIR}
--suffix-mask _mask
--bias-field-correction 1
--two-step-cycles 0
--iter-max 1
--run-diagnostics 1
--slice-thicknesses 3.85
--verbose 0
tags:
- gift-adelie
param_study_huberl2:
# only:
# - master
script:
- recon_type=HuberL2
- >
niftymic_run_reconstruction_parameter_study
--filenames ${DATADIR}/input_data/axial.nii.gz ${DATADIR}/input_data/coronal.nii.gz ${DATADIR}/input_data/sagittal.nii.gz
--dir-input-mc ${DATADIR}/motion_correction_oriented
--suffix-mask _mask
--reference ${DATADIR}/SRR_stacks3_TK1_lsmr_alpha0p03_itermax10_oriented.nii.gz
--dir-output ${TMPDIR}/param_study
--alphas 0.01 0.05
--iter-max 2
--verbose 0
--iterations 2
--reconstruction-type ${recon_type}
tags:
- gift-adelie
rsfmri_estimate_motion:
# only:
# - master
script:
- recon_type=HuberL2
- >
niftymic_rsfmri_estimate_motion
--filename ${DATADIR}/rsfmri/bold.nii.gz
--filename-mask ${DATADIR}/rsfmri/bold_4Dmask.nii.gz
--dir-output ${TMPDIR}/rsfmri
--two-step-cycles 1
--iterations 2
--sda
--alpha 1
--verbose 0
--prototyping
tags:
- gift-adelie
rsfmri_reconstruct_volume_from_slices:
# only:
# - master
script:
- recon_type=TK1L2
- >
niftymic_rsfmri_reconstruct_volume_from_slices
--filename ${DATADIR}/rsfmri/bold.nii.gz
--filename-mask ${DATADIR}/rsfmri/bold_4Dmask.nii.gz
--output ${TMPDIR}/rsfmri/foo.nii.gz
--alpha 0.05
--iter-max 2
--verbose 0
--reconstruction-type ${recon_type}
--reconstruction-spacing 2 2 5
--use-masks-srr 1
--prototyping
tags:
- gift-adelie
##
# Results can (slightly) change depending on the downloaded library version;
# enough so that the accuracy limits are not met. Run them locally instead.
# unit_tests:
# # only:
# # - master
# script:
# - python -m nose tests/brain_stripping_test.py
# - python -m nose tests/case_study_fetal_brain_test.py
# - python -m nose tests/data_reader_test.py
# - python -m nose tests/image_similarity_evaluator_test.py
# - python -m nose tests/intensity_correction_test.py
# - python -m nose tests/linear_operators_test.py
# - python -m nose tests/niftyreg_test.py
# - python -m nose tests/residual_evaluator_test.py
# - python -m nose tests/segmentation_propagation_test.py
# - python -m nose tests/simulator_slice_acquisition_test.py
# - python -m nose tests/stack_test.py
# tags:
# - gift-adelie
================================================
FILE: .gitmodules
================================================
[submodule "MONAIfbs"]
path = MONAIfbs
url = https://github.com/gift-surg/MONAIfbs
================================================
FILE: LICENSE
================================================
Copyright (c) 2017, University College London
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: MANIFEST.in
================================================
include LICENSE
include requirements.txt
================================================
FILE: README.md
================================================
# Motion Correction and Volumetric Image Reconstruction of 2D Ultra-fast MRI
NiftyMIC is a Python-based open-source toolkit for research developed within the [GIFT-Surg][giftsurg] project to reconstruct an isotropic, high-resolution volume from multiple, possibly motion-corrupted, stacks of low-resolution 2D slices. The framework relies on slice-to-volume registration algorithms for motion correction and reconstruction-based Super-Resolution (SR) techniques for the volumetric reconstruction.
The algorithm and software were developed by [Michael Ebner][mebner]
at the [Wellcome/EPSRC Centre for Interventional and Surgical Sciences][weiss], [University College London (UCL)][ucl] (2015 -- 2019), and the [Department of Surgical and Interventional Sciences][sie], [King's College London (KCL)][kcl] (since 2019).
A detailed description of the NiftyMIC algorithm is found in [EbnerWang2020][ebner-wang-2020]:
* Ebner, M., Wang, G., Li, W., Aertsen, M., Patel, P. A., Aughwane, R., Melbourne, A., Doel, T., Dymarkowski, S., De Coppi, P., David, A. L., Deprest, J., Ourselin, S., Vercauteren, T. (2020). An automated framework for localization, segmentation and super-resolution reconstruction of fetal brain MRI. NeuroImage, 206, 116324.
A later extension to this paper for fetal brain automatic segmentation building on [MONAI][monai] was provided by [Marta B.M. Ranzini][martaranzini].
More information about `MONAIfbs` can be found [here][monaifbs].
An extension of NiftyMIC for fetal functional MRI was developed in collaboration with members of the [Computational Image Research Lab][cir], Department of Biomedical Imaging and Image-guided Therapy at the [Medical University of Vienna][muw]. A detailed description of the NiftyMIC algorithm for fetal fMRI is found in [Sobotka2022][sobotka-2022]:
* Sobotka, D., Ebner, M., Schwartz, E., Nenning, K.-H., Taymourtash, A., Vercauteren, T., Ourselin, S., Kasprian, G., Prayer, D., Langs, G., Licandro, R. (2022). Motion Correction and Volumetric Reconstruction for Fetal fMRI. arXiv.
If you have any questions or comments, please drop an email to `michael.ebner@kcl.ac.uk`.
## NiftyMIC applied to Fetal Brain MRI
Given a set of low-resolution, possibly motion-corrupted, stacks of 2D slices, NiftyMIC produces an isotropic, high-resolution 3D volume. As an example, we illustrate its use for fetal MRI by computing a high-resolution visualization of the brain for a neck mass subject. Standard clinical HASTE sequences were used to acquire the low-resolution images in multiple orientations.
The associated brain masks for motion correction can be obtained with the included automated segmentation tool [MONAIfbs][monaifbs] (a legacy method for automated segmentation can also be found in the [fetal_brain_seg][fetal_brain_seg] package).
Full working examples on automated segmentation and high-resolution reconstruction of fetal brain MRI using NiftyMIC is described in the [Usage](https://github.com/gift-surg/NiftyMIC#automatic-segmentation-and-high-resolution-reconstruction-of-fetal-brain-mri) section below.
Figure 1. NiftyMIC -- a volumetric MRI reconstruction tool based on rigid slice-to-volume registration and outlier-robust super-resolution reconstruction steps -- applied to fetal brain MRI.
Figure 2. Qualitative comparison of the original low-resolution input data and the obtained high-resolution volumetric reconstructions in both the original patient-specific and standard anatomical orientations. Five input stacks (two axial, one coronal and two sagittal) were used.
## Algorithm
Several methods have been implemented to solve the **Robust Super-Resolution Reconstruction (SRR)** problem
to obtain the (vectorized) high-resolution 3D MRI volume  from multiple, possibly motion corrupted, low-resolution stacks of (vectorized) 2D MR slices  with  for 
for a variety of regularizers  and data loss functions .
The linear operator  represents the combined operator describing the (rigid) motion , the blurring operator  and the downsampling operator .
The toolkit relies on an iterative motion-correction/reconstruction approach whereby **complete outlier rejection** of misregistered slices is achieved by iteratively solving the SRR problem for a slice-index set
\ge\sigma\\}\subset&space;\mathcal{K}) containing only slices that are in high agreement with their simulated counterparts projected from a previous high-resolution iterate  according to a similarity measure  and parameter . In the current implementation, the similarity measure  corresponds to Normalized Cross Correlation (NCC).
---
The provided **data loss functions**  are motivated by [SciPy](https://docs.scipy.org/doc/scipy-0.19.0/reference/generated/scipy.optimize.least_squares.html) and allow for additional robust outlier handling during the SRR step. Implemented data loss functions are:
* `linear`:
* `soft_l1`:
* `huber`:
* `arctan`:
* `cauchy`:
---
The **available regularizers** include
* Zeroth-order Tikhonov (TK0):
* First-order Tikhonov (TK1):
* Isotropic Total Variation (TV):
* Huber Function:
---
Additionally, the choice of finding **optimal reconstruction parameters** is facilitated by the [Numerical Solver Library (NSoL)][nsol].
## Disclaimer
NiftyMIC supports medical image registration and volumetric reconstruction for ultra-fast 2D MRI. **NiftyMIC is not intended for clinical use**.
## How to cite
If you use this software in your work for structural MRI reconstruction, please cite
* [[EbnerWang2020]][ebner-wang-2020] Ebner, M., Wang, G., Li, W., Aertsen, M., Patel, P. A., Aughwane, R., Melbourne, A., Doel, T., Dymarkowski, S., De Coppi, P., David, A. L., Deprest, J., Ourselin, S., Vercauteren, T. (2020). An automated framework for localization, segmentation and super-resolution reconstruction of fetal brain MRI. NeuroImage, 206, 116324.
* [[EbnerWang2018]][ebner-wang-2018] Ebner, M., Wang, G., Li, W., Aertsen, M., Patel, P. A., Melbourne, A., Doel, T., David, A. L., Deprest, J., Ourselin, S., Vercauteren, T. (2018). An Automated Localization, Segmentation and Reconstruction Framework for Fetal Brain MRI. In Medical Image Computing and Computer-Assisted Intervention -- MICCAI 2018 (pp. 313–320). Springer.
* [[Ebner2018]](https://www.sciencedirect.com/science/article/pii/S1053811917308042) Ebner, M., Chung, K. K., Prados, F., Cardoso, M. J., Chard, D. T., Vercauteren, T., Ourselin, S. (2018). Volumetric reconstruction from printed films: Enabling 30 year longitudinal analysis in MR neuroimaging. NeuroImage, 165, 238–250.
If you use this software in your work for functional MRI reconstruction, please cite
* [[Sobotka2022]][sobotka-2022] Sobotka, D., Ebner, M., Schwartz, E., Nenning, K.-H., Taymourtash, A., Vercauteren, T., Ourselin, S., Kasprian, G., Prayer, D., Langs, G., Licandro, R. (2022). Motion Correction and Volumetric Reconstruction for Fetal fMRI. arXiv.
* [[EbnerWang2020]][ebner-wang-2020] Ebner, M., Wang, G., Li, W., Aertsen, M., Patel, P. A., Aughwane, R., Melbourne, A., Doel, T., Dymarkowski, S., De Coppi, P., David, A. L., Deprest, J., Ourselin, S., Vercauteren, T. (2020). An automated framework for localization, segmentation and super-resolution reconstruction of fetal brain MRI. NeuroImage, 206, 116324.
## Installation
### From Source
NiftyMIC was developed in Ubuntu 16.04, 18.04 and Mac OS X 10.12 and tested for Python 2.7, 3.5 and 3.6.
It builds on a couple of additional libraries developed within the [GIFT-Surg][giftsurg] project including
* [NSoL][nsol]
* [SimpleReg][simplereg]
* [PySiTK][pysitk]
* [ITK_NiftyMIC][itkniftymic]
whose installation requirements need to be met. Therefore, the local installation comes in three steps:
1. [Installation of ITK_NiftyMIC][itkniftymic]
1. [Installation of SimpleReg dependencies][simplereg-dependencies]
1. [Installation of NiftyMIC (including optional MONAIfbs)][niftymic-install]
Note: The optional use of the automated fetal brain segmentation tool [MONAIfbs][monaifbs] requires Python version >= 3.6.
### Virtual Machine and Docker
To avoid manual installation from source, NiftyMIC is also available as a [virtual machine][niftymic-vm] and [Docker image][niftymic-docker].
## Usage
Provided the input MR image data in NIfTI format (`nii` or `nii.gz`), NiftyMIC can reconstruct an isotropic, high-resolution volume from multiple, possibly motion-corrupted, stacks of low-resolution 2D slices.
A recommended workflow is [associated applications in square brackets]:
1. Segmentation of the anatomy of interest for all input images. For fetal brain MRI reconstructions, we recommend the use of the fully automatic segmentation tool [MONAIfbs][monaifbs] already integrated in NiftyMIC [`niftymic_segment_fetal_brains`].
1. Bias-field correction [`niftymic_correct_bias_field`]
1. Volumetric reconstruction in subject space using two-step iterative approach based on rigid slice-to-volume registration and SRR cycles [`niftymic_reconstruct_volume`]
In case reconstruction in a template space is desired (like for fetal MRI) additional steps could be:
1. Register obtained SRR to template and update respective slice motion corrections [`niftymic_register_image`]
1. Volumetric reconstruction in template space [`niftymic_reconstruct_volume_from_slices`]
Additional information on how to use NiftyMIC and its applications is provided in the following.
### Volumetric MR Reconstruction from Motion Corrupted 2D Slices
Leveraging a two-step registration-reconstruction approach an isotropic, high-resolution 3D volume can be generated from multiple stacks of low-resolution slices.
An example for a basic usage reads
```
niftymic_reconstruct_volume \
--filenames path-to-stack-1.nii.gz ... path-to-stack-N.nii.gz \
--filenames-masks path-to-stack-1_mask.nii.gz ... path-to-stack-N_mask.nii.gz \
--output path-to-srr.nii.gz \
```
whereby complete outlier removal during SRR is activated by default (`--outlier-rejection 1`).
A more elaborate example could be
```
niftymic_reconstruct_volume \
--filenames path-to-stack-1.nii.gz ... path-to-stack-N.nii.gz \
--filenames-masks path-to-stack-1_mask.nii.gz ... path-to-stack-N_mask.nii.gz \
--alpha 0.01 \
--outlier-rejection 1 \
--threshold-first 0.5 \
--threshold 0.85 \
--intensity-correction 1 \
--isotropic-resolution 0.8 \
--two-step-cycles 3 \
--output path-to-output-dir/srr.nii.gz \
--subfolder-motion-correction motion_correction \ # created in 'path-to-output-dir'
--verbose 1
```
The obtained motion-correction transformations in `motion_correction` can be used for further processing, e.g. by using `niftymic_reconstruct_volume_from_slices.py` to solve the SRR problem for a variety of different regularization and data loss function types.
### Transformation to Template Space
If a template is available, it is possible to obtain a SRR in its associated standard anatomical space. Using the subject-space SRR outcome of `niftymic_reconstruct_volume` a rigid alignment step maps all slice motion correction transformations accordingly using
```
niftymic_register_image \
--fixed path-to-template.nii.gz \
--fixed-mask path-to-template_mask.nii.gz \
--moving path-to-subject-space-srr.nii.gz \
--moving-mask path-to-subject-space-srr_mask.nii.gz \
--dir-input-mc dir-to-motion_correction \
--output path-to-registration-transform.txt
```
For fetal brain template space alignment, a [spatio-temporal atlas][gholipour_atlas] is provided in [`data/templates`](data/templates). If you make use of it, please cite
* Gholipour, A., Rollins, C. K., Velasco-Annis, C., Ouaalam, A., Akhondi-Asl, A., Afacan, O., Ortinau, C. M., Clancy, S., Limperopoulos, C., Yang, E., Estroff, J. A. & Warfield, S. K. (2017). A normative spatiotemporal MRI atlas of the fetal brain for automatic segmentation and analysis of early brain growth. Scientific Reports 7, 476.
and abide by the license agreement as described in [`data/templates/LICENSE`](data/templates/LICENSE).
### SRR Methods for Motion Corrected (or Static) Data
After performed/updated motion correction (or having static data in the first place) several options are available:
* Volumetric reconstruction in template space
* Parameter tuning for SRR:
* different solvers and regularizers can be used to solve the SRR problem for comparison
* parameter studies can be performed to find optimal reconstruction parameters.
#### SRR from Motion Corrected (or Static) Slice Acquisitions
Solve the SRR problem for motion corrected data (or static data if `--dir-input-mc` is omitted):
```
niftymic_reconstruct_volume_from_slices \
--filenames path-to-stack-1.nii.gz ... path-to-stack-N.nii.gz \
--filenames-masks path-to-stack-1_mask.nii.gz ... path-to-stack-N_mask.nii.gz \
--dir-input-mc dir-to-motion_correction \ # optional
--output path-to-srr.nii.gz \
--reconstruction-type TK1L2 \
--reconstruction-space path-to-template.nii.gz \ # optional
--alpha 0.01
```
```
niftymic_reconstruct_volume_from_slices \
--filenames path-to-stack-1.nii.gz ... path-to-stack-N.nii.gz \
--dir-input-mc dir-to-motion_correction \
--output path-to-srr.nii.gz \
--reconstruction-type HuberL2 \
--alpha 0.003
```
Slices that were rejected during the `niftymic_reconstruct_volume` run are recognized as outliers based on the content of `dir-input-mc` and will not be incorporated during the volumetric reconstruction.
#### Parameter Studies to Determine Optimal SRR Parameters
The optimal choice for reconstruction parameters like the regularization parameter or data loss function can be found by running parameter studies. This includes L-curve studies and direct comparison against a reference volume for various cost functions.
In case a reference is available, similarity measures are evaluated against this "ground-truth" as well.
Example are:
```
niftymic_run_reconstruction_parameter_study \
--filenames path-to-stack-1.nii.gz ... path-to-stack-N.nii.gz \
--filenames-masks path-to-stack-1_mask.nii.gz ... path-to-stack-N_mask.nii.gz \
--dir-input-mc dir-to-motion_correction \
--dir-output dir-to-param-study-output \
--reconstruction-type TK1L2 \
--reconstruction-space path-to-reconstruction-space.nii.gz \ # define reconstruction space
--alphas 0.005 0.01 0.02 0.05 0.1 # regularization parameters to sweep through
--append # if given, append a previously performed parameter study in output directory (if available)
```
```
niftymic_run_reconstruction_parameter_study \
--filenames path-to-stack-1.nii.gz ... path-to-stack-N.nii.gz \
--filenames-masks path-to-stack-1_mask.nii.gz ... path-to-stack-N_mask.nii.gz \
--dir-input-mc dir-to-motion_correction \
--dir-output dir-to-param-study-output \
--reconstruction-type HuberL2 \
--reference path-to-reference-volume.nii.gz \ # in case reference ("ground-truth") is available (reconstruction space is defined by this reference)
--measures MAE RMSE PSNR NCC NMI SSIM \ # evaluate reconstruction similarities against reference
--reference-mask path-to-reference-volume_mask.nii.gz \ # if given, evaluate similarities (--measures) on masked region only
--alphas 0.001 0.003 0.005 0.001 0.003 \ # regularization parameters to sweep through
--append # if given, append a previously performed parameter study in output directory (if available)
```
The results can be assessed by accessing the [NSoL][nsol]-script `show_parameter_study.py` via
```
niftymic_show_parameter_study \
--dir-input dir-to-param-study-output \
--study-name TK1L2 \
--dir-output-figures dir-to-figures
```
### Automatic Segmentation and High-Resolution Reconstruction of Fetal Brain MRI
An automated framework is implemented to obtain a high-resolution fetal brain MRI reconstruction in the standard anatomical planes (Figure 2).
This includes two main subsequent blocks:
1. Automatic segmentation to generate fetal brain masks
2. Automatic high-resolution reconstruction.
The latter is based on the work described in [EbnerWang2018][ebner-wang-2018] and [EbnerWang2020][ebner-wang-2020].
Compared to the segmentation approach proposed in [EbnerWang2020][ebner-wang-2020], a new automated segmentation tool has
been included in the NiftyMIC package, called [MONAIfbs][monaifbs].
This implements a single-step segmentation approach based on the [dynUNet][dynUNet] implemented in [MONAI][monai].
The current NiftyMIC version is still compatible with the older segmentation pipeline based on the [fetal_brain_seg][fetal_brain_seg]
package and presented in [EbnerWang2020][ebner-wang-2020]. Details on its use are available at [this wiki page][wikifetalbrainseg],
although `MONAIfbs` is the recommended option.
#### Automatic segmentation
Provided the dependencies for `MONAIfbs` are [installed][niftymic-install], create the automatic fetal brain masks of HASTE-like images:
```
niftymic_segment_fetal_brains \
--filenames \
nifti/name-of-stack-1.nii.gz \
nifti/name-of-stack-2.nii.gz \
nifti/name-of-stack-N.nii.gz \
--filenames-masks \
seg/name-of-stack-1.nii.gz \
seg/name-of-stack-2.nii.gz \
seg/name-of-stack-N.nii.gz
```
#### Automatic reconstruction
Afterwards, four consecutive steps including
1. bias field correction (`niftymic_correct_bias_field`),
1. subject-space reconstruction (`niftymic_reconstruct_volume`),
1. template-space alignment (`niftymic_register_image`), and
1. template-space reconstruction (`niftymic_reconstruct_volume_from_slices`)
are performed to create a high-resolution fetal brain MRI reconstruction in the standard anatomical planes:
```
niftymic_run_reconstruction_pipeline \
--filenames \
nifti/name-of-stack-1.nii.gz \
nifti/name-of-stack-2.nii.gz \
nifti/name-of-stack-N.nii.gz \
--filenames-masks \
seg/name-of-stack-1.nii.gz \
seg/name-of-stack-2.nii.gz \
seg/name-of-stack-N.nii.gz \
--dir-output srr
```
Additional parameters such as the regularization parameter `alpha` can be specified too. For more information please execute `niftymic_run_reconstruction_pipeline -h`.
*Note*: In case a suffix distinguishes image segmentation (`--filenames-masks`) from the associated image filenames (`--filenames`), the argument `--suffix-mask` needs to be provided for reconstructing the HR brain volume mask as part of the pipeline. E.g. if images `name-of-stack-i.nii.gz` are associated with the mask `name-of-stack-i_mask.nii.gz`, then the additional argument `--suffix-mask _mask` needs to be specified.
### NiftyMIC applied to Fetal Brain Functional MRI
An extension of NiftyMIC for fetal functional MRI is described in [Sobotka2022][sobotka-2022]:
Figure 3: Overview of the proposed motion correction and volumetric reconstruction algorithm for rs-fMRI. With the first n=15 (default) time points a HR reference volume with outlier rejection is estimated (Ebner, Wang et al., 2020). Afterwards each slice of a time point is registered to the HR reference volume following which individual time points are reconstructed using Huber L2 regularization.
Figure 4: Qualitative comparison of the original low-resolution input sequence (top row) and the obtained volumetric reconstruction of the functional MRI (bottom row) in patient-specific orientation. A sequence of axial stacks were used as input.
A recommended workflow is [associated applications in square brackets]:
1. Volumetric segmentation of the fetal brain for all input images in the sequence. We tested the algorithm with fetal functional brains masks created using the Brain Extraction Tool incorporated in the [FSL Toolbox][fsl] (Smith et al. 2002).
1. Motion estimation [`rsfmri_estimate_motion`]
```
rsfmri_estimate_motion \
--filename path-to-bold.nii.gz \
--filename-mask path-to-bold_mask.nii.gz \
--dir-output path-to-dir-output
```
1. Functional volumetric reconstruction [`rsfmri_reconstruct_volume_from_slices`]
An example for a basic usage reads
```
rsfmri_reconstruct_volume_from_slices \
--filename path-to-bold.nii.gz \
--filename-mask path-to-bold_mask.nii.gz \
--dir-input-mc path-to-motion_correction \ # created in `dir-output` in step 2 above
--output path-to-bold_reconstructed.nii.gz
```
A more elaborate example using a different reconstruction approach `HuberL2` could be
```
rsfmri_reconstruct_volume_from_slices \
--filename path-to-bold.nii.gz \
--filename-mask path-to-bold_mask.nii.gz \
--dir-input-mc path-to-motion_correction \ # created in `dir-output` in step 2 above
--output path-to-bold_reconstructed.nii.gz \
--reconstruction-type HuberL2
--alpha 0.1 \
--iter-max 20
```
If you have any questions or comments with regards to the use of NiftyMIC for functional MRI, please drop an email to `daniel.sobotka@meduniwien.ac.at`.
## Licensing and Copyright
Copyright (c) 2022 Michael Ebner and contributors.
This framework is made available as free open-source software under the [BSD-3-Clause License][bsd]. Other licenses may apply for dependencies.
## Funding
This work is partially funded by the UCL [Engineering and Physical Sciences Research Council (EPSRC)][epsrc] Centre for Doctoral Training in Medical Imaging (EP/L016478/1), the Innovative Engineering for Health award ([Wellcome Trust][wellcometrust] [WT101957] and [EPSRC][epsrc] [NS/A000027/1]), and supported by researchers at the [National Institute for Health Research][nihr] [University College London Hospitals (UCLH)][uclh] Biomedical Research Centre.
## References
Selected publications associated with NiftyMIC are:
* [[Sobotka2022]][sobotka-2022] Sobotka, D., Ebner, M., Schwartz, E., Nenning, K.-H., Taymourtash, A., Vercauteren, T., Ourselin, S., Kasprian, G., Prayer, D., Langs, G., Licandro, R. (2022). Motion Correction and Volumetric Reconstruction for Fetal fMRI. arXiv.
* [[EbnerWang2020]][ebner-wang-2020] Ebner, M., Wang, G., Li, W., Aertsen, M., Patel, P. A., Aughwane, R., Melbourne, A., Doel, T., Dymarkowski, S., De Coppi, P., David, A. L., Deprest, J., Ourselin, S., Vercauteren, T. (2020). An automated framework for localization, segmentation and super-resolution reconstruction of fetal brain MRI. NeuroImage, 206, 116324.
* [[Ebner2019]](https://onlinelibrary.wiley.com/doi/abs/10.1002/mrm.27852) Ebner, M., Patel, P. A., Atkinson, D., Caselton, C., Firmin, F., Amin, Z., Bainbridge, A., De Coppi, P., Taylor, S. A., Ourselin, S., Chouhan, M. D., Vercauteren, T. (2019). Super‐resolution for upper abdominal MRI: Acquisition and post‐processing protocol optimization using brain MRI control data and expert reader validation. Magnetic Resonance in Medicine, 82(5), 1905–1919.
* [[Sobotka2019]](http://link.springer.com/10.1007/978-3-030-32875-7_14) Sobotka, D., Licandro, R., Ebner, M., Schwartz, E., Vercauteren, T., Ourselin, S., Kasprian, G., Prayer, D., Langs, G. (2019). Reproducibility of Functional Connectivity Estimates in Motion Corrected Fetal fMRI. Smart Ultrasound Imaging and Perinatal, Preterm and Paediatric Image Analysis (pp. 123–132). Cham: Springer International Publishing.
* [[EbnerWang2018]][ebner-wang-2018] Ebner, M., Wang, G., Li, W., Aertsen, M., Patel, P. A., Melbourne, A., Doel, T., David, A. L., Deprest, J., Ourselin, S., Vercauteren, T. (2018). An Automated Localization, Segmentation and Reconstruction Framework for Fetal Brain MRI. In Medical Image Computing and Computer-Assisted Intervention -- MICCAI 2018 (pp. 313–320). Springer
* [[Ebner2018]](https://www.sciencedirect.com/science/article/pii/S1053811917308042) Ebner, M., Chung, K. K., Prados, F., Cardoso, M. J., Chard, D. T., Vercauteren, T., Ourselin, S. (2018). Volumetric reconstruction from printed films: Enabling 30 year longitudinal analysis in MR neuroimaging. NeuroImage, 165, 238–250.
* [[Ranzini2017]](https://mski2017.files.wordpress.com/2017/09/miccai-mski2017.pdf) Ranzini, M. B., Ebner, M., Cardoso, M. J., Fotiadou, A., Vercauteren, T., Henckel, J., Hart, A., Ourselin, S., and Modat, M. (2017). Joint Multimodal Segmentation of Clinical CT and MR from Hip Arthroplasty Patients. MICCAI Workshop on Computational Methods and Clinical Applications in Musculoskeletal Imaging (MSKI) 2017.
* [[Ebner2017]](https://link.springer.com/chapter/10.1007%2F978-3-319-52280-7_1) Ebner, M., Chouhan, M., Patel, P. A., Atkinson, D., Amin, Z., Read, S., Punwani, S., Taylor, S., Vercauteren, T., Ourselin, S. (2017). Point-Spread-Function-Aware Slice-to-Volume Registration: Application to Upper Abdominal MRI Super-Resolution. In Zuluaga, M. A., Bhatia, K., Kainz, B., Moghari, M. H., and Pace, D. F., editors, Reconstruction, Segmentation, and Analysis of Medical Images. RAMBO 2016, volume 10129 of Lecture Notes in Computer Science, pages 3–13. Springer International Publishing.
[bsd]: https://opensource.org/licenses/BSD-3-Clause
[cir]: https://www.cir.meduniwien.ac.at/
[cmic]: http://cmic.cs.ucl.ac.uk
[dynUNet]: https://github.com/Project-MONAI/tutorials/blob/master/modules/dynunet_tutorial.ipynb
[ebner-wang-2018]: http://link.springer.com/10.1007/978-3-030-00928-1_36
[ebner-wang-2020]: https://www.sciencedirect.com/science/article/pii/S1053811919309152
[epsrc]: http://www.epsrc.ac.uk
[fetal_brain_seg]: https://github.com/gift-surg/fetal_brain_seg
[fsl]: https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/BET
[gholipour_atlas]: https://www.nature.com/articles/s41598-017-00525-w
[giftsurg]: http://www.gift-surg.ac.uk
[guarantors]: https://guarantorsofbrain.org/
[itkniftymic]: https://github.com/gift-surg/ITK_NiftyMIC/wikis/home
[kcl]: https://www.kcl.ac.uk
[martaranzini]: https://www.linkedin.com/in/marta-bianca-maria-ranzini
[mebner]: https://www.linkedin.com/in/ebnermichael
[monai]: https://monai.io/
[monaifbs]: https://github.com/gift-surg/MONAIfbs
[mssociety]: https://www.mssociety.org.uk/
[muw]: https://www.meduniwien.ac.at
[niftymic-docker]: https://hub.docker.com/r/renbem/niftymic
[niftymic-install]: https://github.com/gift-surg/NiftyMIC/wikis/niftymic-installation
[niftymic-vm]: https://github.com/gift-surg/NiftyMIC/wiki/niftymic-virtualbox
[nihr]: http://www.nihr.ac.uk/research
[nsol]: https://github.com/gift-surg/NSoL
[pysitk]: https://github.com/gift-surg/PySiTK
[sie]: https://www.kcl.ac.uk/bmeis/our-departments/surgical-interventional-engineering
[simplereg-dependencies]: https://github.com/gift-surg/SimpleReg/wikis/simplereg-dependencies
[simplereg]: https://github.com/gift-surg/SimpleReg
[sobotka-2022]: https://arxiv.org/abs/2202.05863
[ucl]: http://www.ucl.ac.uk
[uclh]: http://www.uclh.nhs.uk
[weiss]: https://www.ucl.ac.uk/interventional-surgical-sciences
[wellcometrust]: http://www.wellcome.ac.uk
[wikifetalbrainseg]: https://github.com/gift-surg/NiftyMIC/wiki/Legacy-Segmentation-Tool-(fetal_brain_seg)
================================================
FILE: data/templates/LICENSE
================================================
CRL (Computational Radiology Laboratory) hereby grants a license to use, including the right to share, copy, distribute, and transmit, the information, images and data that appears on this webpage solely for non-commercial educational and research purposes, subject to the following restrictions: (1) You must at all times acknowledge and attribute ownership of the information, images and data to CRL, including reproducing CRL's copyright to the extent that the same appears thereon; for publications referencing the fetal structural atlas, cite Gholipour et al., Scientific Reports, 2017; for publications referencing the DTI atlas, cite Khan et al., NeuroImage, 2018 and Khan et al., MICCAI, 2018 (full citations found on the Fetal Brain Atlas webpage); and (2) You may not post the information, images and data on any website, electronic bulletin board or any other electronic medium without the express written permission of CRL or required acknowledgements. As the information, images and data is experimental in nature and is being provided solely to facilitate academic and scientific research, you agree that you will use this information, images and data at your own risk and without liability of any kind to CRL. CRL MAKES NO REPRESENTATIONS AND EXTENDS NO WARRANTIES OF ANY KIND, EITHER EXPRESSED OR IMPLIED WITH RESPECT TO THE INFORMATION, IMAGES AND DATA, AND THERE ARE NO EXPRESS OR IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Use of the information, images or data for any commercial purposes is strictly prohibited without the express written consent of CRL. By requesting access through submitting this form, you acknowledge that you understand and accept all the terms and conditions in this license agreement.
================================================
FILE: data/templates/README
================================================
2016 Fetal Brain Atlas
Copyright 2016, Computational Radiology Lab, Boston Childrens Hospital
Contact:
Ali Gholipour: Ali.Gholipour@childrens.harvard.edu
Clemente Velasco-Annis: Clemente.Velasco-Annis@childrens.harvard.edu
This version of the Spatio-Temporal Atlas (STA) was last edited on 11/21/16
This spatio-temporal atlas and the included parcellations are not intended for medical use and have no warranty. Structures received varying amounts of attention and refinement based on the research goals at the time of creation and thus no guarantees can be made regarding the accuracy or precision of the segmentations, nor the withholding to any particular labeling convention or protocol between structures. Any output derived from the use of these atlases or parcellations shouled be checked in order to validate the accuracy of the results.
# # # # # # # # # # # # # # # # # # # # # #
New to this update (11/21/16) "Olympic edition":
CHANGES TO LABELING SCHEME:
The parcellation scheme (label numbers) has been reorganized
All atlas images now share the same label key
The full key can be found below
Left and right white matter have been separated into separate labels
Labels for left and right internal capsule have been added to all images
REFINEMENT TO PARCELLATIONS:
"Miscellanous_brain_tissue" has been mostly removed as most of the label was internal capsule
Developing white matter zones (SP, IZ & VZ) have been removed from STA31-33 because they were not felt to be accurate (differentiation between layers was arbitrary)
*There are two versions of the STA30 parcellation, one with developing white matter zones, one without*
Cortical plate was reduced in the region anterior to the corpus callosum (reduced the portion "wrapping" into hemisphere)
Subplate has been expanded to the appropriate width as per reviewer feedback
The intermediate zone/ventricular zone has been edited to shrink the ventricular zone in line with reviewer feedback
Deep gray matter structures have been edited to increase segmentation consistency between gestational ages
# # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # #
New to this update (06/07/16):
White and gray cerebellum have been recombined because we could not ensure the accuracy of the delineation (very little refinement had been done since initial propagation of the parcellation scheme to this atlas).
The files have been renamed slightly.
# # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # #
New to this update (03/30/16):
ADDED TEMPLATES:
There are now atlas images and labels for gestational age (GA) weeks 21-37.
DEVELOPING WHITE MATTER ZONES:
Labels for developing white matter zones have been added for GA 21-33
This includes left and right "ventricular zone", "intermediate zone" (similar to subventricular zone), and "subplate"
In these images, label 51 is now "miscellaneous brain tissue", mostly deep gray matter found between the larger subcortical structures
Images with the labeling convention are marked with "wmz"; for example, STA27_WMZparc.nii.gz
There are two versions of labels for STA31, STA32, and STA33
STA31_parc.nii.gz = All white matter is label 51
STA31_WMZparc.nii.gz = Developing white matter layers have been separated into miscellaneous brain tissue (#51), subplate (#73,74), intermediate zone (#75,76), and ventricular zone (#77,78)
Note: As the white matter homogenizes in composition during development, the visual differentiation between the layers also decreases. Because of this the accuracy of the developmental layers may not be great for the older brains (i.e. STA30-33). This primarily pertains to the border between intermediate zone and subplate.
ADDITIONAL ADDED LABELS:
#5: Hippocampal commisure
#6: Fornix
LABEL REFINEMENTS:
Many of the labels have been refined to increase accuracy, in particular:
hippocampus
caudate
thalamus
lentiform
corpus callosum
cortical plate
# # # # # # # # # # # # # # # # # # # # # #
Label key (all images):
37 Hippocampus_L
38 Hippocampus_R
41 Amygdala_L
42 Amygdala_R
71 Caudate_L
72 Caudate_R
73 Lentiform_L
74 Lentiform_R
77 Thalamus_L
78 Thalamus_R
91 CorpusCallosum
92 Lateral_Ventricle_L
93 Lateral_Ventricle_R
94 Brainstem
100 Cerebellum_L
101 Cerebellum_R
108 Subthalamic_Nuc_L
109 Subthalamic_Nuc_R
110 Hippocampal_Comm
111 Fornix
112 Cortical_Plate_L
113 Cortical_Plate_R
114 Subplate_L
115 Subplate_R
116 Inter_Zone_L
117 Inter_Zone_R
118 Vent_Zone_L
119 Vent_Zone_R
120 White_Matter_L
121 White_Matter_R
122 Internal_Capsule_L
123 Internal_Capsule_R
124 CSF
125 Misc
# # # # # # # # # # # # # # # # # # # # # #
Michael Ebner, Feb 16, 2018:
- All image headers have been updated using fslreorient2std using `for i in *.nii.gz; do echo $i; fslreorient2std $i $i; done`
- _mask: binarized image labels
- _mask_dil: dilated binary masks; applied to SRR it approximates brain tissue only
- templates_info.json: Additional information on templates like volume
================================================
FILE: data/templates/templates_info.json
================================================
{
"21": {
"image": "STA21.nii.gz",
"mask": "STA21_mask.nii.gz",
"mask_dil": "STA21_mask_dil.nii.gz",
"volume_mask": 89246.38487071976,
"volume_mask_dil": 120038.46124333153
},
"22": {
"image": "STA22.nii.gz",
"mask": "STA22_mask.nii.gz",
"mask_dil": "STA22_mask_dil.nii.gz",
"volume_mask": 101490.30689354343,
"volume_mask_dil": 134909.44540126887
},
"23": {
"image": "STA23.nii.gz",
"mask": "STA23_mask.nii.gz",
"mask_dil": "STA23_mask_dil.nii.gz",
"volume_mask": 106235.50507484014,
"volume_mask_dil": 140715.503598928
},
"24": {
"image": "STA24.nii.gz",
"mask": "STA24_mask.nii.gz",
"mask_dil": "STA24_mask_dil.nii.gz",
"volume_mask": 130878.99653601555,
"volume_mask_dil": 170357.1202916332
},
"25": {
"image": "STA25.nii.gz",
"mask": "STA25_mask.nii.gz",
"mask_dil": "STA25_mask_dil.nii.gz",
"volume_mask": 178492.25774336213,
"volume_mask_dil": 227175.59493246424
},
"26": {
"image": "STA26.nii.gz",
"mask": "STA26_mask.nii.gz",
"mask_dil": "STA26_mask_dil.nii.gz",
"volume_mask": 201635.08283969283,
"volume_mask_dil": 253806.15093200956
},
"27": {
"image": "STA27.nii.gz",
"mask": "STA27_mask.nii.gz",
"mask_dil": "STA27_mask_dil.nii.gz",
"volume_mask": 210435.81779203523,
"volume_mask_dil": 263941.1528740433
},
"28": {
"image": "STA28.nii.gz",
"mask": "STA28_mask.nii.gz",
"mask_dil": "STA28_mask_dil.nii.gz",
"volume_mask": 233175.18840337868,
"volume_mask_dil": 290329.5337829808
},
"29": {
"image": "STA29.nii.gz",
"mask": "STA29_mask.nii.gz",
"mask_dil": "STA29_mask_dil.nii.gz",
"volume_mask": 257051.194746539,
"volume_mask_dil": 317684.56706204003
},
"30": {
"image": "STA30.nii.gz",
"mask": "STA30_mask.nii.gz",
"mask_dil": "STA30_mask_dil.nii.gz",
"volume_mask": 282285.5319890282,
"volume_mask_dil": 346364.6513653975
},
"31": {
"image": "STA31.nii.gz",
"mask": "STA31_mask.nii.gz",
"mask_dil": "STA31_mask_dil.nii.gz",
"volume_mask": 312587.62620157294,
"volume_mask_dil": 380993.6413300073
},
"32": {
"image": "STA32.nii.gz",
"mask": "STA32_mask.nii.gz",
"mask_dil": "STA32_mask_dil.nii.gz",
"volume_mask": 340744.4484698328,
"volume_mask_dil": 413634.54276009236
},
"33": {
"image": "STA33.nii.gz",
"mask": "STA33_mask.nii.gz",
"mask_dil": "STA33_mask_dil.nii.gz",
"volume_mask": 356126.40670901025,
"volume_mask_dil": 430765.4864316511
},
"34": {
"image": "STA34.nii.gz",
"mask": "STA34_mask.nii.gz",
"mask_dil": "STA34_mask_dil.nii.gz",
"volume_mask": 392819.77292167663,
"volume_mask_dil": 472869.6483262277
},
"35": {
"image": "STA35.nii.gz",
"mask": "STA35_mask.nii.gz",
"mask_dil": "STA35_mask_dil.nii.gz",
"volume_mask": 418491.86852033855,
"volume_mask_dil": 502023.33085117553
},
"36": {
"image": "STA36.nii.gz",
"mask": "STA36_mask.nii.gz",
"mask_dil": "STA36_mask_dil.nii.gz",
"volume_mask": 422497.7414778769,
"volume_mask_dil": 506874.512634493
},
"37": {
"image": "STA37.nii.gz",
"mask": "STA37_mask.nii.gz",
"mask_dil": "STA37_mask_dil.nii.gz",
"volume_mask": 390531.14151572104,
"volume_mask_dil": 470833.9439705052
}
}
================================================
FILE: doc/doxyfile
================================================
# Doxyfile 1.8.9.1
# This file describes the settings to be used by the documentation system
# doxygen (www.doxygen.org) for a project.
#
# All text after a double hash (##) is considered a comment and is placed in
# front of the TAG it is preceding.
#
# All text after a single hash (#) is considered a comment and will be ignored.
# The format is:
# TAG = value [value, ...]
# For lists, items can also be appended using:
# TAG += value [value, ...]
# Values that contain spaces should be placed between quotes (\" \").
#---------------------------------------------------------------------------
# Project related configuration options
#---------------------------------------------------------------------------
# This tag specifies the encoding used for all characters in the config file
# that follow. The default is UTF-8 which is also the encoding used for all text
# before the first occurrence of this tag. Doxygen uses libiconv (or the iconv
# built into libc) for the transcoding. See http://www.gnu.org/software/libiconv
# for the list of possible encodings.
# The default value is: UTF-8.
DOXYFILE_ENCODING = UTF-8
# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by
# double-quotes, unless you are using Doxywizard) that should identify the
# project for which the documentation is generated. This name is used in the
# title of most generated pages and in a few other places.
# The default value is: My Project.
PROJECT_NAME = "Motion Correction and Volumetric Image Reconstruction of 2D Ultra-fast MRI"
# The PROJECT_NUMBER tag can be used to enter a project or revision number. This
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER =
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
# quick idea about the purpose of the project. Keep the description short.
PROJECT_BRIEF =
# With the PROJECT_LOGO tag one can specify a logo or an icon that is included
# in the documentation. The maximum height of the logo should not exceed 55
# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy
# the logo to the output directory.
PROJECT_LOGO =
# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path
# into which the generated documentation will be written. If a relative path is
# entered, it will be relative to the location where doxygen was started. If
# left blank the current directory will be used.
OUTPUT_DIRECTORY = "./"
# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-
# directories (in 2 levels) under the output directory of each output format and
# will distribute the generated files over these directories. Enabling this
# option can be useful when feeding doxygen a huge amount of source files, where
# putting all generated files in the same directory would otherwise causes
# performance problems for the file system.
# The default value is: NO.
CREATE_SUBDIRS = NO
# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII
# characters to appear in the names of generated files. If set to NO, non-ASCII
# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode
# U+3044.
# The default value is: NO.
ALLOW_UNICODE_NAMES = NO
# The OUTPUT_LANGUAGE tag is used to specify the language in which all
# documentation generated by doxygen is written. Doxygen will use this
# information to generate all constant output in the proper language.
# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese,
# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States),
# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian,
# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages),
# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian,
# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian,
# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish,
# Ukrainian and Vietnamese.
# The default value is: English.
OUTPUT_LANGUAGE = English
# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member
# descriptions after the members that are listed in the file and class
# documentation (similar to Javadoc). Set to NO to disable this.
# The default value is: YES.
BRIEF_MEMBER_DESC = YES
# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief
# description of a member or function before the detailed description
#
# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
# brief descriptions will be completely suppressed.
# The default value is: YES.
REPEAT_BRIEF = YES
# This tag implements a quasi-intelligent brief description abbreviator that is
# used to form the text in various listings. Each string in this list, if found
# as the leading text of the brief description, will be stripped from the text
# and the result, after processing the whole list, is used as the annotated
# text. Otherwise, the brief description is used as-is. If left blank, the
# following values are used ($name is automatically replaced with the name of
# the entity):The $name class, The $name widget, The $name file, is, provides,
# specifies, contains, represents, a, an and the.
ABBREVIATE_BRIEF = "The $name class" \
"The $name widget" \
"The $name file" \
is \
provides \
specifies \
contains \
represents \
a \
an \
the
# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
# doxygen will generate a detailed section even if there is only a brief
# description.
# The default value is: NO.
ALWAYS_DETAILED_SEC = NO
# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
# inherited members of a class in the documentation of that class as if those
# members were ordinary class members. Constructors, destructors and assignment
# operators of the base classes will not be shown.
# The default value is: NO.
INLINE_INHERITED_MEMB = YES
# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path
# before files name in the file list and in the header files. If set to NO the
# shortest path that makes the file name unique will be used
# The default value is: YES.
FULL_PATH_NAMES = YES
# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path.
# Stripping is only done if one of the specified strings matches the left-hand
# part of the path. The tag can be used to show relative paths in the file list.
# If left blank the directory from which doxygen is run is used as the path to
# strip.
#
# Note that you can specify absolute paths here, but also relative paths, which
# will be relative from the directory where doxygen is started.
# This tag requires that the tag FULL_PATH_NAMES is set to YES.
STRIP_FROM_PATH =
# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the
# path mentioned in the documentation of a class, which tells the reader which
# header file to include in order to use a class. If left blank only the name of
# the header file containing the class definition is used. Otherwise one should
# specify the list of include paths that are normally passed to the compiler
# using the -I flag.
STRIP_FROM_INC_PATH =
# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but
# less readable) file names. This can be useful is your file systems doesn't
# support long names like on DOS, Mac, or CD-ROM.
# The default value is: NO.
SHORT_NAMES = NO
# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the
# first line (until the first dot) of a Javadoc-style comment as the brief
# description. If set to NO, the Javadoc-style will behave just like regular Qt-
# style comments (thus requiring an explicit @brief command for a brief
# description.)
# The default value is: NO.
JAVADOC_AUTOBRIEF = NO
# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first
# line (until the first dot) of a Qt-style comment as the brief description. If
# set to NO, the Qt-style will behave just like regular Qt-style comments (thus
# requiring an explicit \brief command for a brief description.)
# The default value is: NO.
QT_AUTOBRIEF = NO
# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a
# multi-line C++ special comment block (i.e. a block of //! or /// comments) as
# a brief description. This used to be the default behavior. The new default is
# to treat a multi-line C++ comment block as a detailed description. Set this
# tag to YES if you prefer the old behavior instead.
#
# Note that setting this tag to YES also means that rational rose comments are
# not recognized any more.
# The default value is: NO.
MULTILINE_CPP_IS_BRIEF = NO
# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the
# documentation from any documented member that it re-implements.
# The default value is: YES.
INHERIT_DOCS = YES
# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new
# page for each member. If set to NO, the documentation of a member will be part
# of the file/class/namespace that contains it.
# The default value is: NO.
SEPARATE_MEMBER_PAGES = NO
# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen
# uses this value to replace tabs by spaces in code fragments.
# Minimum value: 1, maximum value: 16, default value: 4.
TAB_SIZE = 4
# This tag can be used to specify a number of aliases that act as commands in
# the documentation. An alias has the form:
# name=value
# For example adding
# "sideeffect=@par Side Effects:\n"
# will allow you to put the command \sideeffect (or @sideeffect) in the
# documentation, which will result in a user-defined paragraph with heading
# "Side Effects:". You can put \n's in the value part of an alias to insert
# newlines.
ALIASES =
# This tag can be used to specify a number of word-keyword mappings (TCL only).
# A mapping has the form "name=value". For example adding "class=itcl::class"
# will allow you to use the command class in the itcl::class meaning.
TCL_SUBST =
# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
# only. Doxygen will then generate output that is more tailored for C. For
# instance, some of the names that are used will be different. The list of all
# members will be omitted, etc.
# The default value is: NO.
OPTIMIZE_OUTPUT_FOR_C = NO
# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or
# Python sources only. Doxygen will then generate output that is more tailored
# for that language. For instance, namespaces will be presented as packages,
# qualified scopes will look different, etc.
# The default value is: NO.
OPTIMIZE_OUTPUT_JAVA = YES
# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran
# sources. Doxygen will then generate output that is tailored for Fortran.
# The default value is: NO.
OPTIMIZE_FOR_FORTRAN = NO
# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL
# sources. Doxygen will then generate output that is tailored for VHDL.
# The default value is: NO.
OPTIMIZE_OUTPUT_VHDL = NO
# Doxygen selects the parser to use depending on the extension of the files it
# parses. With this tag you can assign which parser to use for a given
# extension. Doxygen has a built-in mapping, but you can override or extend it
# using this tag. The format is ext=language, where ext is a file extension, and
# language is one of the parsers supported by doxygen: IDL, Java, Javascript,
# C#, C, C++, D, PHP, Objective-C, Python, Fortran (fixed format Fortran:
# FortranFixed, free formatted Fortran: FortranFree, unknown formatted Fortran:
# Fortran. In the later case the parser tries to guess whether the code is fixed
# or free formatted code, this is the default for Fortran type files), VHDL. For
# instance to make doxygen treat .inc files as Fortran files (default is PHP),
# and .f files as C (default is Fortran), use: inc=Fortran f=C.
#
# Note: For files without extension you can use no_extension as a placeholder.
#
# Note that for custom extensions you also need to set FILE_PATTERNS otherwise
# the files are not read by doxygen.
EXTENSION_MAPPING =
# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments
# according to the Markdown format, which allows for more readable
# documentation. See http://daringfireball.net/projects/markdown/ for details.
# The output of markdown processing is further processed by doxygen, so you can
# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in
# case of backward compatibilities issues.
# The default value is: YES.
MARKDOWN_SUPPORT = YES
# When enabled doxygen tries to link words that correspond to documented
# classes, or namespaces to their corresponding documentation. Such a link can
# be prevented in individual cases by putting a % sign in front of the word or
# globally by setting AUTOLINK_SUPPORT to NO.
# The default value is: YES.
AUTOLINK_SUPPORT = YES
# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want
# to include (a tag file for) the STL sources as input, then you should set this
# tag to YES in order to let doxygen match functions declarations and
# definitions whose arguments contain STL classes (e.g. func(std::string);
# versus func(std::string) {}). This also make the inheritance and collaboration
# diagrams that involve STL classes more complete and accurate.
# The default value is: NO.
BUILTIN_STL_SUPPORT = NO
# If you use Microsoft's C++/CLI language, you should set this option to YES to
# enable parsing support.
# The default value is: NO.
CPP_CLI_SUPPORT = NO
# Set the SIP_SUPPORT tag to YES if your project consists of sip (see:
# http://www.riverbankcomputing.co.uk/software/sip/intro) sources only. Doxygen
# will parse them like normal C++ but will assume all classes use public instead
# of private inheritance when no explicit protection keyword is present.
# The default value is: NO.
SIP_SUPPORT = NO
# For Microsoft's IDL there are propget and propput attributes to indicate
# getter and setter methods for a property. Setting this option to YES will make
# doxygen to replace the get and set methods by a property in the documentation.
# This will only work if the methods are indeed getting or setting a simple
# type. If this is not the case, or you want to show the methods anyway, you
# should set this option to NO.
# The default value is: YES.
IDL_PROPERTY_SUPPORT = YES
# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
# tag is set to YES then doxygen will reuse the documentation of the first
# member in the group (if any) for the other members of the group. By default
# all members of a group must be documented explicitly.
# The default value is: NO.
DISTRIBUTE_GROUP_DOC = NO
# Set the SUBGROUPING tag to YES to allow class member groups of the same type
# (for instance a group of public functions) to be put as a subgroup of that
# type (e.g. under the Public Functions section). Set it to NO to prevent
# subgrouping. Alternatively, this can be done per class using the
# \nosubgrouping command.
# The default value is: YES.
SUBGROUPING = YES
# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions
# are shown inside the group in which they are included (e.g. using \ingroup)
# instead of on a separate page (for HTML and Man pages) or section (for LaTeX
# and RTF).
#
# Note that this feature does not work in combination with
# SEPARATE_MEMBER_PAGES.
# The default value is: NO.
INLINE_GROUPED_CLASSES = NO
# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions
# with only public data fields or simple typedef fields will be shown inline in
# the documentation of the scope in which they are defined (i.e. file,
# namespace, or group documentation), provided this scope is documented. If set
# to NO, structs, classes, and unions are shown on a separate page (for HTML and
# Man pages) or section (for LaTeX and RTF).
# The default value is: NO.
INLINE_SIMPLE_STRUCTS = NO
# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or
# enum is documented as struct, union, or enum with the name of the typedef. So
# typedef struct TypeS {} TypeT, will appear in the documentation as a struct
# with name TypeT. When disabled the typedef will appear as a member of a file,
# namespace, or class. And the struct will be named TypeS. This can typically be
# useful for C code in case the coding convention dictates that all compound
# types are typedef'ed and only the typedef is referenced, never the tag name.
# The default value is: NO.
TYPEDEF_HIDES_STRUCT = NO
# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This
# cache is used to resolve symbols given their name and scope. Since this can be
# an expensive process and often the same symbol appears multiple times in the
# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small
# doxygen will become slower. If the cache is too large, memory is wasted. The
# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range
# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536
# symbols. At the end of a run doxygen will report the cache usage and suggest
# the optimal cache size from a speed point of view.
# Minimum value: 0, maximum value: 9, default value: 0.
LOOKUP_CACHE_SIZE = 0
#---------------------------------------------------------------------------
# Build related configuration options
#---------------------------------------------------------------------------
# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in
# documentation are documented, even if no documentation was available. Private
# class members and static file members will be hidden unless the
# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES.
# Note: This will also disable the warnings about undocumented members that are
# normally produced when WARNINGS is set to YES.
# The default value is: NO.
EXTRACT_ALL = YES
# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will
# be included in the documentation.
# The default value is: NO.
EXTRACT_PRIVATE = YES
# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal
# scope will be included in the documentation.
# The default value is: NO.
EXTRACT_PACKAGE = YES
# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be
# included in the documentation.
# The default value is: NO.
EXTRACT_STATIC = YES
# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined
# locally in source files will be included in the documentation. If set to NO,
# only classes defined in header files are included. Does not have any effect
# for Java sources.
# The default value is: YES.
EXTRACT_LOCAL_CLASSES = YES
# This flag is only useful for Objective-C code. If set to YES, local methods,
# which are defined in the implementation section but not in the interface are
# included in the documentation. If set to NO, only methods in the interface are
# included.
# The default value is: NO.
EXTRACT_LOCAL_METHODS = YES
# If this flag is set to YES, the members of anonymous namespaces will be
# extracted and appear in the documentation as a namespace called
# 'anonymous_namespace{file}', where file will be replaced with the base name of
# the file that contains the anonymous namespace. By default anonymous namespace
# are hidden.
# The default value is: NO.
EXTRACT_ANON_NSPACES = NO
# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all
# undocumented members inside documented classes or files. If set to NO these
# members will be included in the various overviews, but no documentation
# section is generated. This option has no effect if EXTRACT_ALL is enabled.
# The default value is: NO.
HIDE_UNDOC_MEMBERS = NO
# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all
# undocumented classes that are normally visible in the class hierarchy. If set
# to NO, these classes will be included in the various overviews. This option
# has no effect if EXTRACT_ALL is enabled.
# The default value is: NO.
HIDE_UNDOC_CLASSES = NO
# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend
# (class|struct|union) declarations. If set to NO, these declarations will be
# included in the documentation.
# The default value is: NO.
HIDE_FRIEND_COMPOUNDS = NO
# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any
# documentation blocks found inside the body of a function. If set to NO, these
# blocks will be appended to the function's detailed documentation block.
# The default value is: NO.
HIDE_IN_BODY_DOCS = NO
# The INTERNAL_DOCS tag determines if documentation that is typed after a
# \internal command is included. If the tag is set to NO then the documentation
# will be excluded. Set it to YES to include the internal documentation.
# The default value is: NO.
INTERNAL_DOCS = NO
# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file
# names in lower-case letters. If set to YES, upper-case letters are also
# allowed. This is useful if you have classes or files whose names only differ
# in case and if your file system supports case sensitive file names. Windows
# and Mac users are advised to set this option to NO.
# The default value is: system dependent.
CASE_SENSE_NAMES = NO
# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with
# their full class and namespace scopes in the documentation. If set to YES, the
# scope will be hidden.
# The default value is: NO.
HIDE_SCOPE_NAMES = NO
# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will
# append additional text to a page's title, such as Class Reference. If set to
# YES the compound reference will be hidden.
# The default value is: NO.
HIDE_COMPOUND_REFERENCE= NO
# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of
# the files that are included by a file in the documentation of that file.
# The default value is: YES.
SHOW_INCLUDE_FILES = YES
# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each
# grouped member an include statement to the documentation, telling the reader
# which file to include in order to use the member.
# The default value is: NO.
SHOW_GROUPED_MEMB_INC = NO
# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include
# files with double quotes in the documentation rather than with sharp brackets.
# The default value is: NO.
FORCE_LOCAL_INCLUDES = NO
# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the
# documentation for inline members.
# The default value is: YES.
INLINE_INFO = YES
# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the
# (detailed) documentation of file and class members alphabetically by member
# name. If set to NO, the members will appear in declaration order.
# The default value is: YES.
SORT_MEMBER_DOCS = YES
# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief
# descriptions of file, namespace and class members alphabetically by member
# name. If set to NO, the members will appear in declaration order. Note that
# this will also influence the order of the classes in the class list.
# The default value is: NO.
SORT_BRIEF_DOCS = NO
# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the
# (brief and detailed) documentation of class members so that constructors and
# destructors are listed first. If set to NO the constructors will appear in the
# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS.
# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief
# member documentation.
# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting
# detailed member documentation.
# The default value is: NO.
SORT_MEMBERS_CTORS_1ST = NO
# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy
# of group names into alphabetical order. If set to NO the group names will
# appear in their defined order.
# The default value is: NO.
SORT_GROUP_NAMES = NO
# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by
# fully-qualified names, including namespaces. If set to NO, the class list will
# be sorted only by class name, not including the namespace part.
# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
# Note: This option applies only to the class list, not to the alphabetical
# list.
# The default value is: NO.
SORT_BY_SCOPE_NAME = NO
# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper
# type resolution of all parameters of a function it will reject a match between
# the prototype and the implementation of a member function even if there is
# only one candidate or it is obvious which candidate to choose by doing a
# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still
# accept a match between prototype and implementation in such cases.
# The default value is: NO.
STRICT_PROTO_MATCHING = NO
# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo
# list. This list is created by putting \todo commands in the documentation.
# The default value is: YES.
GENERATE_TODOLIST = YES
# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test
# list. This list is created by putting \test commands in the documentation.
# The default value is: YES.
GENERATE_TESTLIST = YES
# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug
# list. This list is created by putting \bug commands in the documentation.
# The default value is: YES.
GENERATE_BUGLIST = YES
# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO)
# the deprecated list. This list is created by putting \deprecated commands in
# the documentation.
# The default value is: YES.
GENERATE_DEPRECATEDLIST= YES
# The ENABLED_SECTIONS tag can be used to enable conditional documentation
# sections, marked by \if ... \endif and \cond
# ... \endcond blocks.
ENABLED_SECTIONS =
# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the
# initial value of a variable or macro / define can have for it to appear in the
# documentation. If the initializer consists of more lines than specified here
# it will be hidden. Use a value of 0 to hide initializers completely. The
# appearance of the value of individual variables and macros / defines can be
# controlled using \showinitializer or \hideinitializer command in the
# documentation regardless of this setting.
# Minimum value: 0, maximum value: 10000, default value: 30.
MAX_INITIALIZER_LINES = 30
# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at
# the bottom of the documentation of classes and structs. If set to YES, the
# list will mention the files that were used to generate the documentation.
# The default value is: YES.
SHOW_USED_FILES = YES
# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This
# will remove the Files entry from the Quick Index and from the Folder Tree View
# (if specified).
# The default value is: YES.
SHOW_FILES = YES
# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces
# page. This will remove the Namespaces entry from the Quick Index and from the
# Folder Tree View (if specified).
# The default value is: YES.
SHOW_NAMESPACES = YES
# The FILE_VERSION_FILTER tag can be used to specify a program or script that
# doxygen should invoke to get the current version for each file (typically from
# the version control system). Doxygen will invoke the program by executing (via
# popen()) the command command input-file, where command is the value of the
# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided
# by doxygen. Whatever the program writes to standard output is used as the file
# version. For an example see the documentation.
FILE_VERSION_FILTER =
# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed
# by doxygen. The layout file controls the global structure of the generated
# output files in an output format independent way. To create the layout file
# that represents doxygen's defaults, run doxygen with the -l option. You can
# optionally specify a file name after the option, if omitted DoxygenLayout.xml
# will be used as the name of the layout file.
#
# Note that if you run doxygen from a directory containing a file called
# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE
# tag is left empty.
LAYOUT_FILE =
# The CITE_BIB_FILES tag can be used to specify one or more bib files containing
# the reference definitions. This must be a list of .bib files. The .bib
# extension is automatically appended if omitted. This requires the bibtex tool
# to be installed. See also http://en.wikipedia.org/wiki/BibTeX for more info.
# For LaTeX the style of the bibliography can be controlled using
# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the
# search path. See also \cite for info how to create references.
CITE_BIB_FILES =
#---------------------------------------------------------------------------
# Configuration options related to warning and progress messages
#---------------------------------------------------------------------------
# The QUIET tag can be used to turn on/off the messages that are generated to
# standard output by doxygen. If QUIET is set to YES this implies that the
# messages are off.
# The default value is: NO.
QUIET = NO
# The WARNINGS tag can be used to turn on/off the warning messages that are
# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES
# this implies that the warnings are on.
#
# Tip: Turn warnings on while writing the documentation.
# The default value is: YES.
WARNINGS = YES
# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate
# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag
# will automatically be disabled.
# The default value is: YES.
WARN_IF_UNDOCUMENTED = YES
# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
# potential errors in the documentation, such as not documenting some parameters
# in a documented function, or documenting parameters that don't exist or using
# markup commands wrongly.
# The default value is: YES.
WARN_IF_DOC_ERROR = YES
# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that
# are documented, but have no documentation for their parameters or return
# value. If set to NO, doxygen will only warn about wrong or incomplete
# parameter documentation, but not about the absence of documentation.
# The default value is: NO.
WARN_NO_PARAMDOC = NO
# The WARN_FORMAT tag determines the format of the warning messages that doxygen
# can produce. The string should contain the $file, $line, and $text tags, which
# will be replaced by the file and line number from which the warning originated
# and the warning text. Optionally the format may contain $version, which will
# be replaced by the version of the file (if it could be obtained via
# FILE_VERSION_FILTER)
# The default value is: $file:$line: $text.
WARN_FORMAT = "$file:$line: $text"
# The WARN_LOGFILE tag can be used to specify a file to which warning and error
# messages should be written. If left blank the output is written to standard
# error (stderr).
WARN_LOGFILE =
#---------------------------------------------------------------------------
# Configuration options related to the input files
#---------------------------------------------------------------------------
# The INPUT tag is used to specify the files and/or directories that contain
# documented source files. You may enter file names like myfile.cpp or
# directories like /usr/src/myproject. Separate the files or directories with
# spaces.
# Note: If this tag is empty the current directory is searched.
INPUT = "../niftymic"
# This tag can be used to specify the character encoding of the source files
# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
# libiconv (or the iconv built into libc) for the transcoding. See the libiconv
# documentation (see: http://www.gnu.org/software/libiconv) for the list of
# possible encodings.
# The default value is: UTF-8.
INPUT_ENCODING = UTF-8
# If the value of the INPUT tag contains directories, you can use the
# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and
# *.h) to filter out the source-files in the directories. If left blank the
# following patterns are tested:*.c, *.cc, *.cxx, *.cpp, *.c++, *.java, *.ii,
# *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp,
# *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown,
# *.md, *.mm, *.dox, *.py, *.f90, *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf,
# *.qsf, *.as and *.js.
FILE_PATTERNS = *.c \
*.cc \
*.cxx \
*.cpp \
*.c++ \
*.java \
*.ii \
*.ixx \
*.ipp \
*.i++ \
*.inl \
*.idl \
*.ddl \
*.odl \
*.h \
*.hh \
*.hxx \
*.hpp \
*.h++ \
*.cs \
*.d \
*.php \
*.php4 \
*.php5 \
*.phtml \
*.inc \
*.m \
*.markdown \
*.md \
*.mm \
*.dox \
*.py \
*.f90 \
*.f \
*.for \
*.tcl \
*.vhd \
*.vhdl \
*.ucf \
*.qsf \
*.as \
*.js
# The RECURSIVE tag can be used to specify whether or not subdirectories should
# be searched for input files as well.
# The default value is: NO.
RECURSIVE = YES
# The EXCLUDE tag can be used to specify files and/or directories that should be
# excluded from the INPUT source files. This way you can easily exclude a
# subdirectory from a directory tree whose root is specified with the INPUT tag.
#
# Note that relative paths are relative to the directory from which doxygen is
# run.
EXCLUDE =
# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
# directories that are symbolic links (a Unix file system feature) are excluded
# from the input.
# The default value is: NO.
EXCLUDE_SYMLINKS = NO
# If the value of the INPUT tag contains directories, you can use the
# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
# certain files from those directories.
#
# Note that the wildcards are matched against the file with absolute path, so to
# exclude all test directories for example use the pattern */test/*
EXCLUDE_PATTERNS =
# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
# (namespaces, classes, functions, etc.) that should be excluded from the
# output. The symbol name can be a fully qualified name, a word, or if the
# wildcard * is used, a substring. Examples: ANamespace, AClass,
# AClass::ANamespace, ANamespace::*Test
#
# Note that the wildcards are matched against the file with absolute path, so to
# exclude all test directories use the pattern */test/*
EXCLUDE_SYMBOLS =
# The EXAMPLE_PATH tag can be used to specify one or more files or directories
# that contain example code fragments that are included (see the \include
# command).
EXAMPLE_PATH =
# If the value of the EXAMPLE_PATH tag contains directories, you can use the
# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and
# *.h) to filter out the source-files in the directories. If left blank all
# files are included.
EXAMPLE_PATTERNS = *
# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
# searched for input files to be used with the \include or \dontinclude commands
# irrespective of the value of the RECURSIVE tag.
# The default value is: NO.
EXAMPLE_RECURSIVE = NO
# The IMAGE_PATH tag can be used to specify one or more files or directories
# that contain images that are to be included in the documentation (see the
# \image command).
IMAGE_PATH =
# The INPUT_FILTER tag can be used to specify a program that doxygen should
# invoke to filter for each input file. Doxygen will invoke the filter program
# by executing (via popen()) the command:
#
#
#
# where is the value of the INPUT_FILTER tag, and is the
# name of an input file. Doxygen will then use the output that the filter
# program writes to standard output. If FILTER_PATTERNS is specified, this tag
# will be ignored.
#
# Note that the filter must not add or remove lines; it is applied before the
# code is scanned, but not when the output code is generated. If lines are added
# or removed, the anchors will not be placed correctly.
INPUT_FILTER =
# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern
# basis. Doxygen will compare the file name with each pattern and apply the
# filter if there is a match. The filters are a list of the form: pattern=filter
# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how
# filters are used. If the FILTER_PATTERNS tag is empty or if none of the
# patterns match the file name, INPUT_FILTER is applied.
FILTER_PATTERNS =
# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
# INPUT_FILTER) will also be used to filter the input files that are used for
# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES).
# The default value is: NO.
FILTER_SOURCE_FILES = NO
# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file
# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and
# it is also possible to disable source filtering for a specific pattern using
# *.ext= (so without naming a filter).
# This tag requires that the tag FILTER_SOURCE_FILES is set to YES.
FILTER_SOURCE_PATTERNS =
# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that
# is part of the input, its contents will be placed on the main page
# (index.html). This can be useful if you have a project on for instance GitHub
# and want to reuse the introduction page also for the doxygen output.
USE_MDFILE_AS_MAINPAGE =
#---------------------------------------------------------------------------
# Configuration options related to source browsing
#---------------------------------------------------------------------------
# If the SOURCE_BROWSER tag is set to YES then a list of source files will be
# generated. Documented entities will be cross-referenced with these sources.
#
# Note: To get rid of all source code in the generated output, make sure that
# also VERBATIM_HEADERS is set to NO.
# The default value is: NO.
SOURCE_BROWSER = YES
# Setting the INLINE_SOURCES tag to YES will include the body of functions,
# classes and enums directly into the documentation.
# The default value is: NO.
INLINE_SOURCES = NO
# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any
# special comment blocks from generated source code fragments. Normal C, C++ and
# Fortran comments will always remain visible.
# The default value is: YES.
STRIP_CODE_COMMENTS = YES
# If the REFERENCED_BY_RELATION tag is set to YES then for each documented
# function all documented functions referencing it will be listed.
# The default value is: NO.
REFERENCED_BY_RELATION = NO
# If the REFERENCES_RELATION tag is set to YES then for each documented function
# all documented entities called/used by that function will be listed.
# The default value is: NO.
REFERENCES_RELATION = NO
# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set
# to YES then the hyperlinks from functions in REFERENCES_RELATION and
# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will
# link to the documentation.
# The default value is: YES.
REFERENCES_LINK_SOURCE = YES
# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the
# source code will show a tooltip with additional information such as prototype,
# brief description and links to the definition and documentation. Since this
# will make the HTML file larger and loading of large files a bit slower, you
# can opt to disable this feature.
# The default value is: YES.
# This tag requires that the tag SOURCE_BROWSER is set to YES.
SOURCE_TOOLTIPS = YES
# If the USE_HTAGS tag is set to YES then the references to source code will
# point to the HTML generated by the htags(1) tool instead of doxygen built-in
# source browser. The htags tool is part of GNU's global source tagging system
# (see http://www.gnu.org/software/global/global.html). You will need version
# 4.8.6 or higher.
#
# To use it do the following:
# - Install the latest version of global
# - Enable SOURCE_BROWSER and USE_HTAGS in the config file
# - Make sure the INPUT points to the root of the source tree
# - Run doxygen as normal
#
# Doxygen will invoke htags (and that will in turn invoke gtags), so these
# tools must be available from the command line (i.e. in the search path).
#
# The result: instead of the source browser generated by doxygen, the links to
# source code will now point to the output of htags.
# The default value is: NO.
# This tag requires that the tag SOURCE_BROWSER is set to YES.
USE_HTAGS = NO
# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a
# verbatim copy of the header file for each class for which an include is
# specified. Set to NO to disable this.
# See also: Section \class.
# The default value is: YES.
VERBATIM_HEADERS = YES
# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the
# clang parser (see: http://clang.llvm.org/) for more accurate parsing at the
# cost of reduced performance. This can be particularly helpful with template
# rich C++ code for which doxygen's built-in parser lacks the necessary type
# information.
# Note: The availability of this option depends on whether or not doxygen was
# compiled with the --with-libclang option.
# The default value is: NO.
CLANG_ASSISTED_PARSING = NO
# If clang assisted parsing is enabled you can provide the compiler with command
# line options that you would normally use when invoking the compiler. Note that
# the include paths will already be set by doxygen for the files and directories
# specified with INPUT and INCLUDE_PATH.
# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES.
CLANG_OPTIONS =
#---------------------------------------------------------------------------
# Configuration options related to the alphabetical class index
#---------------------------------------------------------------------------
# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all
# compounds will be generated. Enable this if the project contains a lot of
# classes, structs, unions or interfaces.
# The default value is: YES.
ALPHABETICAL_INDEX = YES
# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in
# which the alphabetical index list will be split.
# Minimum value: 1, maximum value: 20, default value: 5.
# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
COLS_IN_ALPHA_INDEX = 5
# In case all classes in a project start with a common prefix, all classes will
# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag
# can be used to specify a prefix (or a list of prefixes) that should be ignored
# while generating the index headers.
# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
IGNORE_PREFIX =
#---------------------------------------------------------------------------
# Configuration options related to the HTML output
#---------------------------------------------------------------------------
# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
# The default value is: YES.
GENERATE_HTML = YES
# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a
# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
# it.
# The default directory is: html.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_OUTPUT = html
# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each
# generated HTML page (for example: .htm, .php, .asp).
# The default value is: .html.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_FILE_EXTENSION = .html
# The HTML_HEADER tag can be used to specify a user-defined HTML header file for
# each generated HTML page. If the tag is left blank doxygen will generate a
# standard header.
#
# To get valid HTML the header file that includes any scripts and style sheets
# that doxygen needs, which is dependent on the configuration options used (e.g.
# the setting GENERATE_TREEVIEW). It is highly recommended to start with a
# default header using
# doxygen -w html new_header.html new_footer.html new_stylesheet.css
# YourConfigFile
# and then modify the file new_header.html. See also section "Doxygen usage"
# for information on how to generate the default header that doxygen normally
# uses.
# Note: The header is subject to change so you typically have to regenerate the
# default header when upgrading to a newer version of doxygen. For a description
# of the possible markers and block names see the documentation.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_HEADER =
# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each
# generated HTML page. If the tag is left blank doxygen will generate a standard
# footer. See HTML_HEADER for more information on how to generate a default
# footer and what special commands can be used inside the footer. See also
# section "Doxygen usage" for information on how to generate the default footer
# that doxygen normally uses.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_FOOTER =
# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style
# sheet that is used by each HTML page. It can be used to fine-tune the look of
# the HTML output. If left blank doxygen will generate a default style sheet.
# See also section "Doxygen usage" for information on how to generate the style
# sheet that doxygen normally uses.
# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as
# it is more robust and this tag (HTML_STYLESHEET) will in the future become
# obsolete.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_STYLESHEET =
# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined
# cascading style sheets that are included after the standard style sheets
# created by doxygen. Using this option one can overrule certain style aspects.
# This is preferred over using HTML_STYLESHEET since it does not replace the
# standard style sheet and is therefore more robust against future updates.
# Doxygen will copy the style sheet files to the output directory.
# Note: The order of the extra style sheet files is of importance (e.g. the last
# style sheet in the list overrules the setting of the previous ones in the
# list). For an example see the documentation.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_EXTRA_STYLESHEET =
# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
# other source files which should be copied to the HTML output directory. Note
# that these files will be copied to the base HTML output directory. Use the
# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these
# files. In the HTML_STYLESHEET file, use the file name only. Also note that the
# files will be copied as-is; there are no commands or markers available.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_EXTRA_FILES =
# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
# will adjust the colors in the style sheet and background images according to
# this color. Hue is specified as an angle on a colorwheel, see
# http://en.wikipedia.org/wiki/Hue for more information. For instance the value
# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300
# purple, and 360 is red again.
# Minimum value: 0, maximum value: 359, default value: 220.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_COLORSTYLE_HUE = 220
# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors
# in the HTML output. For a value of 0 the output will use grayscales only. A
# value of 255 will produce the most vivid colors.
# Minimum value: 0, maximum value: 255, default value: 100.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_COLORSTYLE_SAT = 100
# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the
# luminance component of the colors in the HTML output. Values below 100
# gradually make the output lighter, whereas values above 100 make the output
# darker. The value divided by 100 is the actual gamma applied, so 80 represents
# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not
# change the gamma.
# Minimum value: 40, maximum value: 240, default value: 80.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_COLORSTYLE_GAMMA = 80
# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
# page will contain the date and time when the page was generated. Setting this
# to NO can help when comparing the output of multiple runs.
# The default value is: YES.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_TIMESTAMP = YES
# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
# documentation will contain sections that can be hidden and shown after the
# page has loaded.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_DYNAMIC_SECTIONS = NO
# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
# shown in the various tree structured indices initially; the user can expand
# and collapse entries dynamically later on. Doxygen will expand the tree to
# such a level that at most the specified number of entries are visible (unless
# a fully collapsed tree already exceeds this amount). So setting the number of
# entries 1 will produce a full collapsed tree by default. 0 is a special value
# representing an infinite number of entries and will result in a full expanded
# tree by default.
# Minimum value: 0, maximum value: 9999, default value: 100.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_INDEX_NUM_ENTRIES = 100
# If the GENERATE_DOCSET tag is set to YES, additional index files will be
# generated that can be used as input for Apple's Xcode 3 integrated development
# environment (see: http://developer.apple.com/tools/xcode/), introduced with
# OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a
# Makefile in the HTML output directory. Running make will produce the docset in
# that directory and running make install will install the docset in
# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at
# startup. See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html
# for more information.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.
GENERATE_DOCSET = NO
# This tag determines the name of the docset feed. A documentation feed provides
# an umbrella under which multiple documentation sets from a single provider
# (such as a company or product suite) can be grouped.
# The default value is: Doxygen generated docs.
# This tag requires that the tag GENERATE_DOCSET is set to YES.
DOCSET_FEEDNAME = "Doxygen generated docs"
# This tag specifies a string that should uniquely identify the documentation
# set bundle. This should be a reverse domain-name style string, e.g.
# com.mycompany.MyDocSet. Doxygen will append .docset to the name.
# The default value is: org.doxygen.Project.
# This tag requires that the tag GENERATE_DOCSET is set to YES.
DOCSET_BUNDLE_ID = org.doxygen.Project
# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify
# the documentation publisher. This should be a reverse domain-name style
# string, e.g. com.mycompany.MyDocSet.documentation.
# The default value is: org.doxygen.Publisher.
# This tag requires that the tag GENERATE_DOCSET is set to YES.
DOCSET_PUBLISHER_ID = org.doxygen.Publisher
# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.
# The default value is: Publisher.
# This tag requires that the tag GENERATE_DOCSET is set to YES.
DOCSET_PUBLISHER_NAME = Publisher
# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
# additional HTML index files: index.hhp, index.hhc, and index.hhk. The
# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on
# Windows.
#
# The HTML Help Workshop contains a compiler that can convert all HTML output
# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
# files are now used as the Windows 98 help format, and will replace the old
# Windows help format (.hlp) on all Windows platforms in the future. Compressed
# HTML files also contain an index, a table of contents, and you can search for
# words in the documentation. The HTML workshop also contains a viewer for
# compressed HTML files.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.
GENERATE_HTMLHELP = NO
# The CHM_FILE tag can be used to specify the file name of the resulting .chm
# file. You can add a path in front of the file if the result should not be
# written to the html output directory.
# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
CHM_FILE =
# The HHC_LOCATION tag can be used to specify the location (absolute path
# including file name) of the HTML help compiler (hhc.exe). If non-empty,
# doxygen will try to run the HTML help compiler on the generated index.hhp.
# The file has to be specified with full path.
# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
HHC_LOCATION =
# The GENERATE_CHI flag controls if a separate .chi index file is generated
# (YES) or that it should be included in the master .chm file (NO).
# The default value is: NO.
# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
GENERATE_CHI = NO
# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc)
# and project file content.
# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
CHM_INDEX_ENCODING =
# The BINARY_TOC flag controls whether a binary table of contents is generated
# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it
# enables the Previous and Next buttons.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
BINARY_TOC = NO
# The TOC_EXPAND flag can be set to YES to add extra items for group members to
# the table of contents of the HTML help documentation and to the tree view.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
TOC_EXPAND = NO
# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
# (.qch) of the generated HTML documentation.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.
GENERATE_QHP = NO
# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify
# the file name of the resulting .qch file. The path specified is relative to
# the HTML output folder.
# This tag requires that the tag GENERATE_QHP is set to YES.
QCH_FILE =
# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help
# Project output. For more information please see Qt Help Project / Namespace
# (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#namespace).
# The default value is: org.doxygen.Project.
# This tag requires that the tag GENERATE_QHP is set to YES.
QHP_NAMESPACE = org.doxygen.Project
# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt
# Help Project output. For more information please see Qt Help Project / Virtual
# Folders (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#virtual-
# folders).
# The default value is: doc.
# This tag requires that the tag GENERATE_QHP is set to YES.
QHP_VIRTUAL_FOLDER = doc
# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom
# filter to add. For more information please see Qt Help Project / Custom
# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom-
# filters).
# This tag requires that the tag GENERATE_QHP is set to YES.
QHP_CUST_FILTER_NAME =
# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the
# custom filter to add. For more information please see Qt Help Project / Custom
# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom-
# filters).
# This tag requires that the tag GENERATE_QHP is set to YES.
QHP_CUST_FILTER_ATTRS =
# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this
# project's filter section matches. Qt Help Project / Filter Attributes (see:
# http://qt-project.org/doc/qt-4.8/qthelpproject.html#filter-attributes).
# This tag requires that the tag GENERATE_QHP is set to YES.
QHP_SECT_FILTER_ATTRS =
# The QHG_LOCATION tag can be used to specify the location of Qt's
# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the
# generated .qhp file.
# This tag requires that the tag GENERATE_QHP is set to YES.
QHG_LOCATION =
# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be
# generated, together with the HTML files, they form an Eclipse help plugin. To
# install this plugin and make it available under the help contents menu in
# Eclipse, the contents of the directory containing the HTML and XML files needs
# to be copied into the plugins directory of eclipse. The name of the directory
# within the plugins directory should be the same as the ECLIPSE_DOC_ID value.
# After copying Eclipse needs to be restarted before the help appears.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.
GENERATE_ECLIPSEHELP = NO
# A unique identifier for the Eclipse help plugin. When installing the plugin
# the directory name containing the HTML and XML files should also have this
# name. Each documentation set should have its own identifier.
# The default value is: org.doxygen.Project.
# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.
ECLIPSE_DOC_ID = org.doxygen.Project
# If you want full control over the layout of the generated HTML pages it might
# be necessary to disable the index and replace it with your own. The
# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top
# of each HTML page. A value of NO enables the index and the value YES disables
# it. Since the tabs in the index contain the same information as the navigation
# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.
DISABLE_INDEX = NO
# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
# structure should be generated to display hierarchical information. If the tag
# value is set to YES, a side panel will be generated containing a tree-like
# index structure (just like the one that is generated for HTML Help). For this
# to work a browser that supports JavaScript, DHTML, CSS and frames is required
# (i.e. any modern browser). Windows users are probably better off using the
# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can
# further fine-tune the look of the index. As an example, the default style
# sheet generated by doxygen has an example that shows how to put an image at
# the root of the tree instead of the PROJECT_NAME. Since the tree basically has
# the same information as the tab index, you could consider setting
# DISABLE_INDEX to YES when enabling this option.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.
GENERATE_TREEVIEW = YES
# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that
# doxygen will group on one line in the generated HTML documentation.
#
# Note that a value of 0 will completely suppress the enum values from appearing
# in the overview section.
# Minimum value: 0, maximum value: 20, default value: 4.
# This tag requires that the tag GENERATE_HTML is set to YES.
ENUM_VALUES_PER_LINE = 4
# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used
# to set the initial width (in pixels) of the frame in which the tree is shown.
# Minimum value: 0, maximum value: 1500, default value: 250.
# This tag requires that the tag GENERATE_HTML is set to YES.
TREEVIEW_WIDTH = 250
# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to
# external symbols imported via tag files in a separate window.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.
EXT_LINKS_IN_WINDOW = NO
# Use this tag to change the font size of LaTeX formulas included as images in
# the HTML documentation. When you change the font size after a successful
# doxygen run you need to manually remove any form_*.png images from the HTML
# output directory to force them to be regenerated.
# Minimum value: 8, maximum value: 50, default value: 10.
# This tag requires that the tag GENERATE_HTML is set to YES.
FORMULA_FONTSIZE = 10
# Use the FORMULA_TRANPARENT tag to determine whether or not the images
# generated for formulas are transparent PNGs. Transparent PNGs are not
# supported properly for IE 6.0, but are supported on all modern browsers.
#
# Note that when changing this option you need to delete any form_*.png files in
# the HTML output directory before the changes have effect.
# The default value is: YES.
# This tag requires that the tag GENERATE_HTML is set to YES.
FORMULA_TRANSPARENT = YES
# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see
# http://www.mathjax.org) which uses client side Javascript for the rendering
# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX
# installed or if you want to formulas look prettier in the HTML output. When
# enabled you may also need to install MathJax separately and configure the path
# to it using the MATHJAX_RELPATH option.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.
USE_MATHJAX = NO
# When MathJax is enabled you can set the default output format to be used for
# the MathJax output. See the MathJax site (see:
# http://docs.mathjax.org/en/latest/output.html) for more details.
# Possible values are: HTML-CSS (which is slower, but has the best
# compatibility), NativeMML (i.e. MathML) and SVG.
# The default value is: HTML-CSS.
# This tag requires that the tag USE_MATHJAX is set to YES.
MATHJAX_FORMAT = HTML-CSS
# When MathJax is enabled you need to specify the location relative to the HTML
# output directory using the MATHJAX_RELPATH option. The destination directory
# should contain the MathJax.js script. For instance, if the mathjax directory
# is located at the same level as the HTML output directory, then
# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax
# Content Delivery Network so you can quickly see the result without installing
# MathJax. However, it is strongly recommended to install a local copy of
# MathJax from http://www.mathjax.org before deployment.
# The default value is: http://cdn.mathjax.org/mathjax/latest.
# This tag requires that the tag USE_MATHJAX is set to YES.
MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest
# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
# extension names that should be enabled during MathJax rendering. For example
# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
# This tag requires that the tag USE_MATHJAX is set to YES.
MATHJAX_EXTENSIONS =
# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces
# of code that will be used on startup of the MathJax code. See the MathJax site
# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an
# example see the documentation.
# This tag requires that the tag USE_MATHJAX is set to YES.
MATHJAX_CODEFILE =
# When the SEARCHENGINE tag is enabled doxygen will generate a search box for
# the HTML output. The underlying search engine uses javascript and DHTML and
# should work on any modern browser. Note that when using HTML help
# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET)
# there is already a search function so this one should typically be disabled.
# For large projects the javascript based search engine can be slow, then
# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to
# search using the keyboard; to jump to the search box use + S
# (what the is depends on the OS and browser, but it is typically
# , /, or both). Inside the search box use the to jump into the search results window, the results can be navigated
# using the . Press to select an item or to cancel
# the search. The filter options can be selected when the cursor is inside the
# search box by pressing +. Also here use the
# to select a filter and or to activate or cancel the filter
# option.
# The default value is: YES.
# This tag requires that the tag GENERATE_HTML is set to YES.
SEARCHENGINE = YES
# When the SERVER_BASED_SEARCH tag is enabled the search engine will be
# implemented using a web server instead of a web client using Javascript. There
# are two flavors of web server based searching depending on the EXTERNAL_SEARCH
# setting. When disabled, doxygen will generate a PHP script for searching and
# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing
# and searching needs to be provided by external tools. See the section
# "External Indexing and Searching" for details.
# The default value is: NO.
# This tag requires that the tag SEARCHENGINE is set to YES.
SERVER_BASED_SEARCH = NO
# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP
# script for searching. Instead the search results are written to an XML file
# which needs to be processed by an external indexer. Doxygen will invoke an
# external search engine pointed to by the SEARCHENGINE_URL option to obtain the
# search results.
#
# Doxygen ships with an example indexer (doxyindexer) and search engine
# (doxysearch.cgi) which are based on the open source search engine library
# Xapian (see: http://xapian.org/).
#
# See the section "External Indexing and Searching" for details.
# The default value is: NO.
# This tag requires that the tag SEARCHENGINE is set to YES.
EXTERNAL_SEARCH = NO
# The SEARCHENGINE_URL should point to a search engine hosted by a web server
# which will return the search results when EXTERNAL_SEARCH is enabled.
#
# Doxygen ships with an example indexer (doxyindexer) and search engine
# (doxysearch.cgi) which are based on the open source search engine library
# Xapian (see: http://xapian.org/). See the section "External Indexing and
# Searching" for details.
# This tag requires that the tag SEARCHENGINE is set to YES.
SEARCHENGINE_URL =
# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed
# search data is written to a file for indexing by an external tool. With the
# SEARCHDATA_FILE tag the name of this file can be specified.
# The default file is: searchdata.xml.
# This tag requires that the tag SEARCHENGINE is set to YES.
SEARCHDATA_FILE = searchdata.xml
# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the
# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is
# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple
# projects and redirect the results back to the right project.
# This tag requires that the tag SEARCHENGINE is set to YES.
EXTERNAL_SEARCH_ID =
# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen
# projects other than the one defined by this configuration file, but that are
# all added to the same external search index. Each project needs to have a
# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of
# to a relative location where the documentation can be found. The format is:
# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ...
# This tag requires that the tag SEARCHENGINE is set to YES.
EXTRA_SEARCH_MAPPINGS =
#---------------------------------------------------------------------------
# Configuration options related to the LaTeX output
#---------------------------------------------------------------------------
# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.
# The default value is: YES.
GENERATE_LATEX = YES
# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a
# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
# it.
# The default directory is: latex.
# This tag requires that the tag GENERATE_LATEX is set to YES.
LATEX_OUTPUT = latex
# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be
# invoked.
#
# Note that when enabling USE_PDFLATEX this option is only used for generating
# bitmaps for formulas in the HTML output, but not in the Makefile that is
# written to the output directory.
# The default file is: latex.
# This tag requires that the tag GENERATE_LATEX is set to YES.
LATEX_CMD_NAME = latex
# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate
# index for LaTeX.
# The default file is: makeindex.
# This tag requires that the tag GENERATE_LATEX is set to YES.
MAKEINDEX_CMD_NAME = makeindex
# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX
# documents. This may be useful for small projects and may help to save some
# trees in general.
# The default value is: NO.
# This tag requires that the tag GENERATE_LATEX is set to YES.
COMPACT_LATEX = NO
# The PAPER_TYPE tag can be used to set the paper type that is used by the
# printer.
# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x
# 14 inches) and executive (7.25 x 10.5 inches).
# The default value is: a4.
# This tag requires that the tag GENERATE_LATEX is set to YES.
PAPER_TYPE = a4
# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names
# that should be included in the LaTeX output. To get the times font for
# instance you can specify
# EXTRA_PACKAGES=times
# If left blank no extra packages will be included.
# This tag requires that the tag GENERATE_LATEX is set to YES.
EXTRA_PACKAGES = amsmath,amssymb,latexsym
# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the
# generated LaTeX document. The header should contain everything until the first
# chapter. If it is left blank doxygen will generate a standard header. See
# section "Doxygen usage" for information on how to let doxygen write the
# default header to a separate file.
#
# Note: Only use a user-defined header if you know what you are doing! The
# following commands have a special meaning inside the header: $title,
# $datetime, $date, $doxygenversion, $projectname, $projectnumber,
# $projectbrief, $projectlogo. Doxygen will replace $title with the empty
# string, for the replacement values of the other commands the user is referred
# to HTML_HEADER.
# This tag requires that the tag GENERATE_LATEX is set to YES.
LATEX_HEADER =
# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the
# generated LaTeX document. The footer should contain everything after the last
# chapter. If it is left blank doxygen will generate a standard footer. See
# LATEX_HEADER for more information on how to generate a default footer and what
# special commands can be used inside the footer.
#
# Note: Only use a user-defined footer if you know what you are doing!
# This tag requires that the tag GENERATE_LATEX is set to YES.
LATEX_FOOTER =
# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined
# LaTeX style sheets that are included after the standard style sheets created
# by doxygen. Using this option one can overrule certain style aspects. Doxygen
# will copy the style sheet files to the output directory.
# Note: The order of the extra style sheet files is of importance (e.g. the last
# style sheet in the list overrules the setting of the previous ones in the
# list).
# This tag requires that the tag GENERATE_LATEX is set to YES.
LATEX_EXTRA_STYLESHEET =
# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or
# other source files which should be copied to the LATEX_OUTPUT output
# directory. Note that the files will be copied as-is; there are no commands or
# markers available.
# This tag requires that the tag GENERATE_LATEX is set to YES.
LATEX_EXTRA_FILES =
# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is
# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will
# contain links (just like the HTML output) instead of page references. This
# makes the output suitable for online browsing using a PDF viewer.
# The default value is: YES.
# This tag requires that the tag GENERATE_LATEX is set to YES.
PDF_HYPERLINKS = YES
# If the USE_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate
# the PDF file directly from the LaTeX files. Set this option to YES, to get a
# higher quality PDF documentation.
# The default value is: YES.
# This tag requires that the tag GENERATE_LATEX is set to YES.
USE_PDFLATEX = YES
# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode
# command to the generated LaTeX files. This will instruct LaTeX to keep running
# if errors occur, instead of asking the user for help. This option is also used
# when generating formulas in HTML.
# The default value is: NO.
# This tag requires that the tag GENERATE_LATEX is set to YES.
LATEX_BATCHMODE = NO
# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the
# index chapters (such as File Index, Compound Index, etc.) in the output.
# The default value is: NO.
# This tag requires that the tag GENERATE_LATEX is set to YES.
LATEX_HIDE_INDICES = NO
# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source
# code with syntax highlighting in the LaTeX output.
#
# Note that which sources are shown also depends on other settings such as
# SOURCE_BROWSER.
# The default value is: NO.
# This tag requires that the tag GENERATE_LATEX is set to YES.
LATEX_SOURCE_CODE = NO
# The LATEX_BIB_STYLE tag can be used to specify the style to use for the
# bibliography, e.g. plainnat, or ieeetr. See
# http://en.wikipedia.org/wiki/BibTeX and \cite for more info.
# The default value is: plain.
# This tag requires that the tag GENERATE_LATEX is set to YES.
LATEX_BIB_STYLE = plain
#---------------------------------------------------------------------------
# Configuration options related to the RTF output
#---------------------------------------------------------------------------
# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The
# RTF output is optimized for Word 97 and may not look too pretty with other RTF
# readers/editors.
# The default value is: NO.
GENERATE_RTF = NO
# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a
# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
# it.
# The default directory is: rtf.
# This tag requires that the tag GENERATE_RTF is set to YES.
RTF_OUTPUT = rtf
# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF
# documents. This may be useful for small projects and may help to save some
# trees in general.
# The default value is: NO.
# This tag requires that the tag GENERATE_RTF is set to YES.
COMPACT_RTF = NO
# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will
# contain hyperlink fields. The RTF file will contain links (just like the HTML
# output) instead of page references. This makes the output suitable for online
# browsing using Word or some other Word compatible readers that support those
# fields.
#
# Note: WordPad (write) and others do not support links.
# The default value is: NO.
# This tag requires that the tag GENERATE_RTF is set to YES.
RTF_HYPERLINKS = NO
# Load stylesheet definitions from file. Syntax is similar to doxygen's config
# file, i.e. a series of assignments. You only have to provide replacements,
# missing definitions are set to their default value.
#
# See also section "Doxygen usage" for information on how to generate the
# default style sheet that doxygen normally uses.
# This tag requires that the tag GENERATE_RTF is set to YES.
RTF_STYLESHEET_FILE =
# Set optional variables used in the generation of an RTF document. Syntax is
# similar to doxygen's config file. A template extensions file can be generated
# using doxygen -e rtf extensionFile.
# This tag requires that the tag GENERATE_RTF is set to YES.
RTF_EXTENSIONS_FILE =
# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code
# with syntax highlighting in the RTF output.
#
# Note that which sources are shown also depends on other settings such as
# SOURCE_BROWSER.
# The default value is: NO.
# This tag requires that the tag GENERATE_RTF is set to YES.
RTF_SOURCE_CODE = NO
#---------------------------------------------------------------------------
# Configuration options related to the man page output
#---------------------------------------------------------------------------
# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for
# classes and files.
# The default value is: NO.
GENERATE_MAN = NO
# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a
# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
# it. A directory man3 will be created inside the directory specified by
# MAN_OUTPUT.
# The default directory is: man.
# This tag requires that the tag GENERATE_MAN is set to YES.
MAN_OUTPUT = man
# The MAN_EXTENSION tag determines the extension that is added to the generated
# man pages. In case the manual section does not start with a number, the number
# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is
# optional.
# The default value is: .3.
# This tag requires that the tag GENERATE_MAN is set to YES.
MAN_EXTENSION = .3
# The MAN_SUBDIR tag determines the name of the directory created within
# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by
# MAN_EXTENSION with the initial . removed.
# This tag requires that the tag GENERATE_MAN is set to YES.
MAN_SUBDIR =
# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it
# will generate one additional man file for each entity documented in the real
# man page(s). These additional files only source the real man page, but without
# them the man command would be unable to find the correct page.
# The default value is: NO.
# This tag requires that the tag GENERATE_MAN is set to YES.
MAN_LINKS = NO
#---------------------------------------------------------------------------
# Configuration options related to the XML output
#---------------------------------------------------------------------------
# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that
# captures the structure of the code including all documentation.
# The default value is: NO.
GENERATE_XML = NO
# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a
# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
# it.
# The default directory is: xml.
# This tag requires that the tag GENERATE_XML is set to YES.
XML_OUTPUT = xml
# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program
# listings (including syntax highlighting and cross-referencing information) to
# the XML output. Note that enabling this will significantly increase the size
# of the XML output.
# The default value is: YES.
# This tag requires that the tag GENERATE_XML is set to YES.
XML_PROGRAMLISTING = YES
#---------------------------------------------------------------------------
# Configuration options related to the DOCBOOK output
#---------------------------------------------------------------------------
# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files
# that can be used to generate PDF.
# The default value is: NO.
GENERATE_DOCBOOK = NO
# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put.
# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in
# front of it.
# The default directory is: docbook.
# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
DOCBOOK_OUTPUT = docbook
# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the
# program listings (including syntax highlighting and cross-referencing
# information) to the DOCBOOK output. Note that enabling this will significantly
# increase the size of the DOCBOOK output.
# The default value is: NO.
# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
DOCBOOK_PROGRAMLISTING = NO
#---------------------------------------------------------------------------
# Configuration options for the AutoGen Definitions output
#---------------------------------------------------------------------------
# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an
# AutoGen Definitions (see http://autogen.sf.net) file that captures the
# structure of the code including all documentation. Note that this feature is
# still experimental and incomplete at the moment.
# The default value is: NO.
GENERATE_AUTOGEN_DEF = NO
#---------------------------------------------------------------------------
# Configuration options related to the Perl module output
#---------------------------------------------------------------------------
# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module
# file that captures the structure of the code including all documentation.
#
# Note that this feature is still experimental and incomplete at the moment.
# The default value is: NO.
GENERATE_PERLMOD = NO
# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary
# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI
# output from the Perl module output.
# The default value is: NO.
# This tag requires that the tag GENERATE_PERLMOD is set to YES.
PERLMOD_LATEX = NO
# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely
# formatted so it can be parsed by a human reader. This is useful if you want to
# understand what is going on. On the other hand, if this tag is set to NO, the
# size of the Perl module output will be much smaller and Perl will parse it
# just the same.
# The default value is: YES.
# This tag requires that the tag GENERATE_PERLMOD is set to YES.
PERLMOD_PRETTY = YES
# The names of the make variables in the generated doxyrules.make file are
# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful
# so different doxyrules.make files included by the same Makefile don't
# overwrite each other's variables.
# This tag requires that the tag GENERATE_PERLMOD is set to YES.
PERLMOD_MAKEVAR_PREFIX =
#---------------------------------------------------------------------------
# Configuration options related to the preprocessor
#---------------------------------------------------------------------------
# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all
# C-preprocessor directives found in the sources and include files.
# The default value is: YES.
ENABLE_PREPROCESSING = YES
# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names
# in the source code. If set to NO, only conditional compilation will be
# performed. Macro expansion can be done in a controlled way by setting
# EXPAND_ONLY_PREDEF to YES.
# The default value is: NO.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
MACRO_EXPANSION = NO
# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then
# the macro expansion is limited to the macros specified with the PREDEFINED and
# EXPAND_AS_DEFINED tags.
# The default value is: NO.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
EXPAND_ONLY_PREDEF = NO
# If the SEARCH_INCLUDES tag is set to YES, the include files in the
# INCLUDE_PATH will be searched if a #include is found.
# The default value is: YES.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
SEARCH_INCLUDES = YES
# The INCLUDE_PATH tag can be used to specify one or more directories that
# contain include files that are not input files but should be processed by the
# preprocessor.
# This tag requires that the tag SEARCH_INCLUDES is set to YES.
INCLUDE_PATH =
# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard
# patterns (like *.h and *.hpp) to filter out the header-files in the
# directories. If left blank, the patterns specified with FILE_PATTERNS will be
# used.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
INCLUDE_FILE_PATTERNS =
# The PREDEFINED tag can be used to specify one or more macro names that are
# defined before the preprocessor is started (similar to the -D option of e.g.
# gcc). The argument of the tag is a list of macros of the form: name or
# name=definition (no spaces). If the definition and the "=" are omitted, "=1"
# is assumed. To prevent a macro definition from being undefined via #undef or
# recursively expanded use the := operator instead of the = operator.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
PREDEFINED =
# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
# tag can be used to specify a list of macro names that should be expanded. The
# macro definition that is found in the sources will be used. Use the PREDEFINED
# tag if you want to use a different macro definition that overrules the
# definition found in the source code.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
EXPAND_AS_DEFINED =
# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will
# remove all references to function-like macros that are alone on a line, have
# an all uppercase name, and do not end with a semicolon. Such function macros
# are typically used for boiler-plate code, and will confuse the parser if not
# removed.
# The default value is: YES.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
SKIP_FUNCTION_MACROS = YES
#---------------------------------------------------------------------------
# Configuration options related to external references
#---------------------------------------------------------------------------
# The TAGFILES tag can be used to specify one or more tag files. For each tag
# file the location of the external documentation should be added. The format of
# a tag file without this location is as follows:
# TAGFILES = file1 file2 ...
# Adding location for the tag files is done as follows:
# TAGFILES = file1=loc1 "file2 = loc2" ...
# where loc1 and loc2 can be relative or absolute paths or URLs. See the
# section "Linking to external documentation" for more information about the use
# of tag files.
# Note: Each tag file must have a unique name (where the name does NOT include
# the path). If a tag file is not located in the directory in which doxygen is
# run, you must also specify the path to the tagfile here.
TAGFILES =
# When a file name is specified after GENERATE_TAGFILE, doxygen will create a
# tag file that is based on the input files it reads. See section "Linking to
# external documentation" for more information about the usage of tag files.
GENERATE_TAGFILE =
# If the ALLEXTERNALS tag is set to YES, all external class will be listed in
# the class index. If set to NO, only the inherited external classes will be
# listed.
# The default value is: NO.
ALLEXTERNALS = NO
# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed
# in the modules index. If set to NO, only the current project's groups will be
# listed.
# The default value is: YES.
EXTERNAL_GROUPS = YES
# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in
# the related pages index. If set to NO, only the current project's pages will
# be listed.
# The default value is: YES.
EXTERNAL_PAGES = YES
# The PERL_PATH should be the absolute path and name of the perl script
# interpreter (i.e. the result of 'which perl').
# The default file (with absolute path) is: /usr/bin/perl.
PERL_PATH = /usr/bin/perl
#---------------------------------------------------------------------------
# Configuration options related to the dot tool
#---------------------------------------------------------------------------
# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram
# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to
# NO turns the diagrams off. Note that this option also works with HAVE_DOT
# disabled, but it is recommended to install and use dot, since it yields more
# powerful graphs.
# The default value is: YES.
CLASS_DIAGRAMS = YES
# You can define message sequence charts within doxygen comments using the \msc
# command. Doxygen will then run the mscgen tool (see:
# http://www.mcternan.me.uk/mscgen/)) to produce the chart and insert it in the
# documentation. The MSCGEN_PATH tag allows you to specify the directory where
# the mscgen tool resides. If left empty the tool is assumed to be found in the
# default search path.
MSCGEN_PATH =
# You can include diagrams made with dia in doxygen documentation. Doxygen will
# then run dia to produce the diagram and insert it in the documentation. The
# DIA_PATH tag allows you to specify the directory where the dia binary resides.
# If left empty dia is assumed to be found in the default search path.
DIA_PATH =
# If set to YES the inheritance and collaboration graphs will hide inheritance
# and usage relations if the target is undocumented or is not a class.
# The default value is: YES.
HIDE_UNDOC_RELATIONS = NO
# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
# available from the path. This tool is part of Graphviz (see:
# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
# Bell Labs. The other options in this section have no effect if this option is
# set to NO
# The default value is: NO.
HAVE_DOT = YES
# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed
# to run in parallel. When set to 0 doxygen will base this on the number of
# processors available in the system. You can set it explicitly to a value
# larger than 0 to get control over the balance between CPU load and processing
# speed.
# Minimum value: 0, maximum value: 32, default value: 0.
# This tag requires that the tag HAVE_DOT is set to YES.
DOT_NUM_THREADS = 0
# When you want a differently looking font in the dot files that doxygen
# generates you can specify the font name using DOT_FONTNAME. You need to make
# sure dot is able to find the font, which can be done by putting it in a
# standard location or by setting the DOTFONTPATH environment variable or by
# setting DOT_FONTPATH to the directory containing the font.
# The default value is: Helvetica.
# This tag requires that the tag HAVE_DOT is set to YES.
DOT_FONTNAME = Helvetica
# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
# dot graphs.
# Minimum value: 4, maximum value: 24, default value: 10.
# This tag requires that the tag HAVE_DOT is set to YES.
DOT_FONTSIZE = 10
# By default doxygen will tell dot to use the default font as specified with
# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
# the path where dot can find it using this tag.
# This tag requires that the tag HAVE_DOT is set to YES.
DOT_FONTPATH =
# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for
# each documented class showing the direct and indirect inheritance relations.
# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.
CLASS_GRAPH = YES
# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a
# graph for each documented class showing the direct and indirect implementation
# dependencies (inheritance, containment, and class references variables) of the
# class with other documented classes.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.
COLLABORATION_GRAPH = YES
# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
# groups, showing the direct groups dependencies.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.
GROUP_GRAPHS = YES
# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and
# collaboration diagrams in a style similar to the OMG's Unified Modeling
# Language.
# The default value is: NO.
# This tag requires that the tag HAVE_DOT is set to YES.
UML_LOOK = YES
# If the UML_LOOK tag is enabled, the fields and methods are shown inside the
# class node. If there are many fields or methods and many nodes the graph may
# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the
# number of items for each type to make the size more manageable. Set this to 0
# for no limit. Note that the threshold may be exceeded by 50% before the limit
# is enforced. So when you set the threshold to 10, up to 15 fields may appear,
# but if the number exceeds 15, the total amount of fields shown is limited to
# 10.
# Minimum value: 0, maximum value: 100, default value: 10.
# This tag requires that the tag HAVE_DOT is set to YES.
UML_LIMIT_NUM_FIELDS = 100
# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and
# collaboration graphs will show the relations between templates and their
# instances.
# The default value is: NO.
# This tag requires that the tag HAVE_DOT is set to YES.
TEMPLATE_RELATIONS = NO
# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to
# YES then doxygen will generate a graph for each documented file showing the
# direct and indirect include dependencies of the file with other documented
# files.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.
INCLUDE_GRAPH = YES
# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are
# set to YES then doxygen will generate a graph for each documented file showing
# the direct and indirect include dependencies of the file with other documented
# files.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.
INCLUDED_BY_GRAPH = YES
# If the CALL_GRAPH tag is set to YES then doxygen will generate a call
# dependency graph for every global function or class method.
#
# Note that enabling this option will significantly increase the time of a run.
# So in most cases it will be better to enable call graphs for selected
# functions only using the \callgraph command.
# The default value is: NO.
# This tag requires that the tag HAVE_DOT is set to YES.
CALL_GRAPH = YES
# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller
# dependency graph for every global function or class method.
#
# Note that enabling this option will significantly increase the time of a run.
# So in most cases it will be better to enable caller graphs for selected
# functions only using the \callergraph command.
# The default value is: NO.
# This tag requires that the tag HAVE_DOT is set to YES.
CALLER_GRAPH = YES
# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical
# hierarchy of all classes instead of a textual one.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.
GRAPHICAL_HIERARCHY = YES
# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the
# dependencies a directory has on other directories in a graphical way. The
# dependency relations are determined by the #include relations between the
# files in the directories.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.
DIRECTORY_GRAPH = YES
# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
# generated by dot.
# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order
# to make the SVG files visible in IE 9+ (other browsers do not have this
# requirement).
# Possible values are: png, jpg, gif and svg.
# The default value is: png.
# This tag requires that the tag HAVE_DOT is set to YES.
DOT_IMAGE_FORMAT = svg
# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to
# enable generation of interactive SVG images that allow zooming and panning.
#
# Note that this requires a modern browser other than Internet Explorer. Tested
# and working are Firefox, Chrome, Safari, and Opera.
# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make
# the SVG files visible. Older versions of IE do not have SVG support.
# The default value is: NO.
# This tag requires that the tag HAVE_DOT is set to YES.
INTERACTIVE_SVG = YES
# The DOT_PATH tag can be used to specify the path where the dot tool can be
# found. If left blank, it is assumed the dot tool can be found in the path.
# This tag requires that the tag HAVE_DOT is set to YES.
DOT_PATH =
# The DOTFILE_DIRS tag can be used to specify one or more directories that
# contain dot files that are included in the documentation (see the \dotfile
# command).
# This tag requires that the tag HAVE_DOT is set to YES.
DOTFILE_DIRS =
# The MSCFILE_DIRS tag can be used to specify one or more directories that
# contain msc files that are included in the documentation (see the \mscfile
# command).
MSCFILE_DIRS =
# The DIAFILE_DIRS tag can be used to specify one or more directories that
# contain dia files that are included in the documentation (see the \diafile
# command).
DIAFILE_DIRS =
# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the
# path where java can find the plantuml.jar file. If left blank, it is assumed
# PlantUML is not used or called during a preprocessing step. Doxygen will
# generate a warning when it encounters a \startuml command in this case and
# will not generate output for the diagram.
PLANTUML_JAR_PATH =
# When using plantuml, the specified paths are searched for files specified by
# the !include statement in a plantuml block.
PLANTUML_INCLUDE_PATH =
# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes
# that will be shown in the graph. If the number of nodes in a graph becomes
# larger than this value, doxygen will truncate the graph, which is visualized
# by representing a node as a red box. Note that doxygen if the number of direct
# children of the root node in a graph is already larger than
# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that
# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.
# Minimum value: 0, maximum value: 10000, default value: 50.
# This tag requires that the tag HAVE_DOT is set to YES.
DOT_GRAPH_MAX_NODES = 50
# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs
# generated by dot. A depth value of 3 means that only nodes reachable from the
# root by following a path via at most 3 edges will be shown. Nodes that lay
# further from the root node will be omitted. Note that setting this option to 1
# or 2 may greatly reduce the computation time needed for large code bases. Also
# note that the size of a graph can be further restricted by
# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.
# Minimum value: 0, maximum value: 1000, default value: 0.
# This tag requires that the tag HAVE_DOT is set to YES.
MAX_DOT_GRAPH_DEPTH = 0
# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
# background. This is disabled by default, because dot on Windows does not seem
# to support this out of the box.
#
# Warning: Depending on the platform used, enabling this option may lead to
# badly anti-aliased labels on the edges of a graph (i.e. they become hard to
# read).
# The default value is: NO.
# This tag requires that the tag HAVE_DOT is set to YES.
DOT_TRANSPARENT = NO
# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
# files in one run (i.e. multiple -o and -T options on the command line). This
# makes dot run faster, but since only newer versions of dot (>1.8.10) support
# this, this feature is disabled by default.
# The default value is: NO.
# This tag requires that the tag HAVE_DOT is set to YES.
DOT_MULTI_TARGETS = NO
# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
# explaining the meaning of the various boxes and arrows in the dot generated
# graphs.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.
GENERATE_LEGEND = YES
# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate dot
# files that are used to generate the various graphs.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.
DOT_CLEANUP = YES
================================================
FILE: docker/docker-compose.yml
================================================
# Comments:
# - should work in principle, but not tested
# - seems not possible to add two tags per "service" with one run
# - existing docker images were created by executing Dockerfiles individually
# SSH_PRIVATE_KEY="$(cat ~/.ssh/id_rsa)" docker-compose build
version: "3.7"
services:
itk_niftymic:
build:
context: ./itk_niftymic
args:
VERSION: v4.13.1-niftymic-v1
image: itk_niftymic:v4.13.1-niftymic-v1
simplereg_dependencies:
build:
context: ./simplereg_dependencies
image: simplereg_dependencies:noitksnap
niftymic:
build:
context: ./niftymic
args:
VERSION: latest
FETAL_SEG_TOOL: monaifbs
image: niftymic:latest
================================================
FILE: docker/itk_niftymic/.dockerignore
================================================
.git*
.cache
================================================
FILE: docker/itk_niftymic/Dockerfile
================================================
#
# Building of Docker image:
# docker build --build-arg VERSION=v? -t renbem/itk_niftymic:v? -t renbem/itk_niftymic .
ARG VERSION=latest
ARG REPO=ITK_NiftyMIC
ARG IMAGE=python:3.6-slim
# -----------------------------------------------------------------------------
FROM $IMAGE as compile-image
ARG REPO
ARG VERSION
RUN apt-get update && \
apt-get install -y \
build-essential \
cmake \
git \
&& \
rm -rf /var/lib/apt/lists/*
RUN if [ "$VERSION" = "latest" ] ; then \
git clone \
https://github.com/gift-surg/${REPO}.git /code/${REPO}/${REPO} \
;else \
git clone \
--branch ${VERSION} \
https://github.com/gift-surg/${REPO}.git /code/${REPO}/${REPO} \
;fi
RUN mkdir -p /code/${REPO}/${REPO}-build && \
cd /code/${REPO}/${REPO}-build && \
cmake \
-D BUILD_EXAMPLES=OFF \
-D BUILD_SHARED_LIBS=ON \
-D BUILD_TESTING=OFF \
-D CMAKE_BUILD_TYPE=Release \
-D ITK_LEGACY_SILENT=ON \
-D ITK_WRAP_covariant_vector_double=ON \
-D ITK_WRAP_double=ON \
-D ITK_WRAP_float=ON \
-D ITK_WRAP_PYTHON=ON \
-D ITK_WRAP_signed_char=ON \
-D ITK_WRAP_signed_long=ON \
-D ITK_WRAP_signed_short=ON \
-D ITK_WRAP_unsigned_char=ON \
-D ITK_WRAP_unsigned_long=ON \
-D ITK_WRAP_unsigned_short=ON \
-D ITK_WRAP_vector_double=ON \
-D ITK_WRAP_vector_float=ON \
-D Module_BridgeNumPy=ON \
-D Module_ITKReview=ON \
-D Module_SmoothingRecursiveYvvGaussianFilter=ON \
/code/${REPO}/${REPO}
RUN cd /code/${REPO}/${REPO}-build && make -j 4
# install files to /usr/local
RUN cd /code/${REPO}/${REPO}-build && make install
# make shared libraries available to Python
ENV LD_LIBRARY_PATH="/usr/local/lib:$LD_LIBRARY_PATH"
# remove unnecessary .git folders
RUN rm -r /code/${REPO}/${REPO}/.git*
# -----------------------------------------------------------------------------
FROM $IMAGE AS runtime-image
ARG REPO
ARG VERSION
LABEL author="Michael Ebner"
LABEL email="michael.ebner@kcl.ac.uk"
LABEL title="$REPO"
LABEL version="$VERSION"
LABEL uri="https://github.com/gift-surg/${REPO}"
# copy compiled ITK files and make libraries available to Python
COPY --from=compile-image /usr/local /usr/local
ENV LD_LIBRARY_PATH="/usr/local/lib:$LD_LIBRARY_PATH"
# add Dockerfile to image
ADD Dockerfile /
# use bash with color output
RUN echo 'alias ls="ls --color=auto"' >> ~/.bashrc
CMD bash
================================================
FILE: docker/niftymic/.dockerignore
================================================
.git*
.cache
================================================
FILE: docker/niftymic/Dockerfile
================================================
#
# Building of Docker image with default monaifbs segmentation tool:
# docker build --build-arg VERSION=v? -t renbem/niftymic:v? -t renbem/niftymic .
#
# If building with fetal_brain_seg as segmentation pipeline:
# docker build --build-arg VERSION=v? --build-arg FETAL_SEG_TOOL=fetal_brain_seg -t renbem/niftymic:v? -t renbem/niftymic .
ARG VERSION=latest
ARG REPO=NiftyMIC
# default use monaifbs for segmentation. Define this arg as fetal_brain_seg to use previous app
# https://github.com/gift-surg/fetal_brain_seg.git
ARG FETAL_SEG_TOOL=monaifbs
# GUI with ITK-Snap does not work at the moment, unfortunately
ARG IMAGE=renbem/simplereg_dependencies:noitksnap
# -----------------------------------------------------------------------------
FROM $IMAGE as compile-image
ARG REPO
ARG VERSION
ARG FETAL_SEG_TOOL
RUN apt-get update && \
apt-get install -y \
build-essential \
git \
&& \
rm -rf /var/lib/apt/lists/*
# download NiftyMIC
RUN if [ "$VERSION" = "latest" ] ; then \
git clone \
https://github.com/gift-surg/${REPO}.git /app/${REPO} \
;else \
git clone \
--branch ${VERSION} \
https://github.com/gift-surg/${REPO}.git /app/${REPO} \
;fi
# fetch MONAIfbs and download pretrained model for MONAIfbs
RUN if [ "$FETAL_SEG_TOOL" = "monaifbs" ] ; then \
cd /app/${REPO} && \
git submodule update --init && \
# fetch the pretrained model
cd /app && \
pip install zenodo-get && \
zenodo_get 10.5281/zenodo.4282679 && \
tar xvf models.tar.gz && \
mv models /app/${REPO}/MONAIfbs/monaifbs/ && \
# remove the downloaded compressed file
rm -r /app/models.tar.gz \
;fi
# download fetal_brain_seg if required (need to create an empty directory for following copy, line 105)
RUN mkdir /app/fetal_brain_seg
ADD https://github.com/taigw/Demic/archive/v0.1.tar.gz /app/Demic-0.1.tar.gz
RUN if [ "$FETAL_SEG_TOOL" = "fetal_brain_seg" ] ; then \
git clone \
https://github.com/gift-surg/fetal_brain_seg.git /app/fetal_brain_seg && \
cd /app && \
tar xvf Demic-0.1.tar.gz && \
mv Demic-0.1 /app/fetal_brain_seg/Demic && \
# remove unecessary .git folders
rm -r /app/fetal_brain_seg/.git* && \
rm -r /app/fetal_brain_seg/Demic/.git* \
;fi
# remove unnecessary folders
RUN rm -r /app/${REPO}/.git*
RUN rm -r /app/Demic-0.1.tar.gz
# -----------------------------------------------------------------------------
FROM $IMAGE AS runtime-image
ARG REPO
ARG VERSION
ARG FETAL_SEG_TOOL
LABEL author="Michael Ebner"
LABEL email="michael.ebner@kcl.ac.uk"
LABEL title="$REPO"
LABEL version="$VERSION"
LABEL uri="https://github.com/gift-surg/${REPO}"
# install NiftyMIC with specific python library versions
COPY --from=compile-image /app/${REPO} /app/${REPO}
WORKDIR /app/${REPO}
RUN apt-get update && \
apt-get install -y \
build-essential \
nifti2dicom \
&& \
rm -rf /var/lib/apt/lists/*
RUN pip install \
matplotlib==3.1.1 \
natsort==6.0.0 \
nibabel==2.4.1 \
nipype==1.2.0 \
nose==1.3.7 \
numpy==1.16.4 \
pandas==0.25.0 \
pydicom==1.3.0 \
scikit_image==0.15.0 \
scipy==1.3.0 \
seaborn==0.9.0 \
SimpleITK==1.2.4 \
six==1.12.0 \
pysitk==0.2.19 \
simplereg==0.3.2 \
nsol==0.1.14
# install monaifbs dependencies
RUN if [ "$FETAL_SEG_TOOL" = "monaifbs" ] ; then \
pip install \
torch==1.4.0 \
torch-summary==1.4.3 \
monai==0.3.0 \
pyyaml==5.3.1 \
pytorch-ignite==0.4.2 \
tensorboard==2.3.0 \
;fi
# install packages for niftymic and monaifbs
RUN pip install -e .
RUN if [ "$FETAL_SEG_TOOL" = "monaifbs" ] ; then \
pip install -e /app/${REPO}/MONAIfbs/ \
;fi
# prepare fetal_brain_seg with specific python library versions if required
COPY --from=compile-image /app/fetal_brain_seg /app/fetal_brain_seg
RUN if [ "$FETAL_SEG_TOOL" = "fetal_brain_seg" ] ; then \
cd /app/fetal_brain_seg && \
pip install \
niftynet==0.2 \
tensorflow==1.12.0 && \
SITEDIR=$(python -m site --user-site) && \
mkdir -p $SITEDIR && \
echo /app/fetal_brain_seg > $SITEDIR/Demic.pth && \
export FETAL_BRAIN_SEG=/app/fetal_brain_seg \
;else \
rm -r /app/fetal_brain_seg \
;fi
# add Dockerfile to image
ADD Dockerfile /
WORKDIR /app
# use bash with color output
RUN echo 'alias ls="ls --color=auto"' >> ~/.bashrc
CMD bash
================================================
FILE: docker/simplereg_dependencies/.dockerignore
================================================
.git*
.cache
================================================
FILE: docker/simplereg_dependencies/Dockerfile
================================================
#
# Building of Docker image:
# docker build -t renbem/simplereg_dependencies .
#
# Run with GUI (however, does not work unfortunately):
# xhost +local:docker # needed only the first time
# docker run --rm -ti --net=host --env="DISPLAY" --volume="$HOME/.Xauthority:/root/.Xauthority:rw" renbem/simplereg_dependencies
ARG VERSION=noitksnap
ARG REPO=SimpleReg-dependencies
ARG IMAGE=renbem/itk_niftymic:v4.13.1-niftymic-v1
# -----------------------------------------------------------------------------
FROM $IMAGE as compile-image-fsl
ARG REPO
ARG VERSION
RUN apt-get update && \
apt-get install -y \
build-essential \
wget \
&& \
rm -rf /var/lib/apt/lists/*
# install FSL
RUN wget -O- http://neuro.debian.net/lists/stretch.au.full | \
tee /etc/apt/sources.list.d/neurodebian.sources.list
RUN apt-key adv --recv-keys --keyserver \
hkp://pool.sks-keyservers.net:80 0xA5D32F012649A5A9
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive \
apt-get install -y fsl-core \
&& \
rm -rf /var/lib/apt/lists/*
# -----------------------------------------------------------------------------
FROM $IMAGE as compile-image-niftyreg
RUN apt-get update && \
apt-get install -y \
build-essential \
cmake \
git \
&& \
rm -rf /var/lib/apt/lists/*
# install NiftyReg
RUN git clone https://github.com/KCL-BMEIS/niftyreg.git /code/niftyreg
RUN mkdir -p /code/niftyreg-build && \
mkdir -p /usr/share/niftyreg
RUN cd /code/niftyreg-build && \
cmake \
-D CMAKE_INSTALL_PREFIX=/usr/share/niftyreg \
/code/niftyreg
RUN cd /code/niftyreg-build && make -j4
RUN cd /code/niftyreg-build && make install
# -----------------------------------------------------------------------------
FROM $IMAGE as compile-image-itksnap
# itksnap GUI not working unfortunately
# convert3D
ADD c3d-1.0.0-Linux-x86_64.tar.gz /code/
RUN mv /code/c3d-1.0.0-Linux-x86_64 /code/c3d
# itksnap with QT4 opens but GUI has issues then
# a)
ADD itksnap-3.8.0-20190612-Linux-x86_64-qt4.tar.gz /code/
RUN mv /code/itksnap-3.8.0-20190612-Linux-gcc64-qt4 /code/itksnap
# b)
# ADD itksnap-nightly-master-Linux-x86_64-qt4.tar.gz /code/
# RUN mv /code/itksnap-3.6.0-20170401-Linux-x86_64-qt4 /code/itksnap
# versions do not work: after 'itksnap', the following error:
#
# [7]: The last reference on a connection was dropped without closing the connection. This is a bug in an application. See dbus_connection_unref() documentation for details.
# Most likely, the application was supposed to call dbus_connection_close(), since this is a private connection.
# D-Bus not built with -rdynamic so unable to print a backtrace
# Aborted (core dumped)
#
# a)
# ADD itksnap-3.8.0-20190612-Linux-x86_64.tar.gz /code/
# RUN mv /code/itksnap-3.8.0-20190612-Linux-gcc64 /code/itksnap
# b)
# ADD itksnap-nightly-master-Linux-x86_64.tar.gz /code/
# RUN mv /code/itksnap-3.6.0-20170401-Linux-x86_64 /code/itksnap
# -----------------------------------------------------------------------------
FROM $IMAGE AS runtime-image
ARG REPO
ARG VERSION
LABEL author="Michael Ebner"
LABEL email="michael.ebner@kcl.ac.uk"
LABEL title="$REPO"
LABEL version="$VERSION"
LABEL uri="https://github.com/gift-surg/SimpleReg/wiki/simplereg-dependencies"
# copy compiled FSL files and link associated binaries
COPY --from=compile-image-fsl /etc/fsl /etc/fsl
COPY --from=compile-image-fsl /usr/lib /usr/lib
COPY --from=compile-image-fsl /usr/share/fsl /usr/share/fsl
COPY --from=compile-image-fsl /usr/bin/fsl5.0-* /usr/bin/
RUN ln -s /usr/bin/fsl5.0-flirt /usr/local/bin/flirt && \
ln -s /usr/bin/fsl5.0-fslhd /usr/local/bin/fslhd && \
ln -s /usr/bin/fsl5.0-fslmodhd /usr/local/bin/fslmodhd && \
ln -s /usr/bin/fsl5.0-fslorient /usr/local/bin/fslorient && \
ln -s /usr/bin/fsl5.0-fslreorient2std /usr/local/bin/fslreorient2std && \
ln -s /usr/bin/fsl5.0-fslswapdim /usr/local/bin/fslswapdim && \
ln -s /usr/bin/fsl5.0-bet /usr/local/bin/bet
# copy compiled NiftyReg files and link associated binaries
COPY --from=compile-image-niftyreg /usr/share/niftyreg /usr/share/niftyreg
ENV PATH="/usr/share/niftyreg/bin:$PATH"
# copy compiled itksnap files
COPY --from=compile-image-itksnap /code/c3d/bin /usr/local/bin
COPY --from=compile-image-itksnap /code/c3d/lib /usr/local/lib
COPY --from=compile-image-itksnap /code/c3d/share /usr/local/share
COPY --from=compile-image-itksnap /code/itksnap/bin /usr/local/bin
COPY --from=compile-image-itksnap /code/itksnap/lib /usr/local/lib
# to make use of itksnap GUI within docker (in principle;
# but errors/problems as above; thus, deactivated for now)
# RUN apt-get update && \
# apt-get install -y \
# wget \
# libglu1 \
# libcurl4-openssl-dev \
# libsm6 \
# libxt6 \
# libfreetype6 \
# libxrender1 \
# libfontconfig1 \
# libglib2.0-0 \
# libqt4-dev \
# libgtk2.0-dev \
# curl \
# libgtk2.0 \
# qt5dxcb-plugin \
# && \
# rm -rf /var/lib/apt/lists/*
# RUN apt-get update && \
# apt-get install -y \
# libxcb1 libxcb1-dev \
# libx11-dev \
# libgl1-mesa-dev \
# libxt-dev libxft-dev \
# && \
# rm -rf /var/lib/apt/lists/*
# ADD libpng12-0_1.2.54-1ubuntu1.1_amd64.deb /code/
# RUN dpkg -i /code/libpng12-0_1.2.54-1ubuntu1.1_amd64.deb
# add Dockerfile to image
ADD Dockerfile /
# use bash with color output
RUN echo 'alias ls="ls --color=auto"' >> ~/.bashrc
CMD bash
================================================
FILE: niftymic/__about__.py
================================================
__author__ = "Michael Ebner"
__email__ = "michael.ebner@kcl.ac.uk"
__license__ = "BSD-3-Clause"
__version__ = "0.9"
__summary__ = "NiftyMIC is a research-focused toolkit for " \
"motion-correction and volumetric image reconstruction of " \
"2D ultra-fast MRI."
================================================
FILE: niftymic/__init__.py
================================================
from niftymic.__about__ import (
__author__,
__email__,
__license__,
__summary__,
__version__,
)
__all__ = [
"__author__",
"__email__",
"__license__",
"__summary__",
"__version__",
]
================================================
FILE: niftymic/application/__init__.py
================================================
================================================
FILE: niftymic/application/correct_bias_field.py
================================================
##
# \file correct_bias_field.py
# \brief Script to correct for bias field. Based on N4ITK
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date October 2017
#
# Import libraries
import numpy as np
import os
import niftymic.base.stack as st
import niftymic.base.data_writer as dw
import niftymic.utilities.n4_bias_field_correction as n4itk
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
from niftymic.utilities.input_arparser import InputArgparser
from niftymic.definitions import ALLOWED_EXTENSIONS
def main():
time_start = ph.start_timing()
np.set_printoptions(precision=3)
input_parser = InputArgparser(
description="Perform Bias Field correction using N4ITK.",
)
input_parser.add_filename(required=True)
input_parser.add_output(required=True)
input_parser.add_filename_mask()
input_parser.add_option(
option_string="--convergence-threshold",
type=float,
help="Specify the convergence threshold.",
default=1e-6,
)
input_parser.add_option(
option_string="--spline-order",
type=int,
help="Specify the spline order defining the bias field estimate.",
default=3,
)
input_parser.add_option(
option_string="--wiener-filter-noise",
type=float,
help="Specify the noise estimate defining the Wiener filter.",
default=0.11,
)
input_parser.add_option(
option_string="--bias-field-fwhm",
type=float,
help="Specify the full width at half maximum parameter characterizing "
"the width of the Gaussian deconvolution.",
default=0.15,
)
input_parser.add_log_config(default=1)
input_parser.add_verbose(default=0)
args = input_parser.parse_args()
input_parser.print_arguments(args)
if np.alltrue([not args.output.endswith(t) for t in ALLOWED_EXTENSIONS]):
raise ValueError(
"output filename invalid; allowed extensions are: %s" %
", ".join(ALLOWED_EXTENSIONS))
if args.log_config:
input_parser.log_config(os.path.abspath(__file__))
# Read data
stack = st.Stack.from_filename(
file_path=args.filename,
file_path_mask=args.filename_mask,
extract_slices=False,
)
# Perform Bias Field Correction
# ph.print_title("Perform Bias Field Correction")
bias_field_corrector = n4itk.N4BiasFieldCorrection(
stack=stack,
use_mask=True if args.filename_mask is not None else False,
convergence_threshold=args.convergence_threshold,
spline_order=args.spline_order,
wiener_filter_noise=args.wiener_filter_noise,
bias_field_fwhm=args.bias_field_fwhm,
)
ph.print_info("N4ITK Bias Field Correction ... ", newline=False)
bias_field_corrector.run_bias_field_correction()
stack_corrected = bias_field_corrector.get_bias_field_corrected_stack()
print("done")
dw.DataWriter.write_image(stack_corrected.sitk, args.output)
elapsed_time = ph.stop_timing(time_start)
if args.verbose:
ph.show_niftis([args.filename, args.output])
ph.print_title("Summary")
exe_file_info = os.path.basename(os.path.abspath(__file__)).split(".")[0]
print("%s | Computational Time for Bias Field Correction: %s" % (
exe_file_info, elapsed_time))
return 0
if __name__ == '__main__':
main()
================================================
FILE: niftymic/application/correct_intensities.py
================================================
##
# \file correct_intensities.py
# \brief Script to perform intensity correction across images given a
# reference image
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date October 2017
#
# Import libraries
import numpy as np
import os
# Import modules
import niftymic.base.data_reader as dr
import niftymic.base.stack as st
import niftymic.registration.flirt as regflirt
import niftymic.utilities.intensity_correction as ic
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
from niftymic.utilities.input_arparser import InputArgparser
def main():
time_start = ph.start_timing()
np.set_printoptions(precision=3)
input_parser = InputArgparser(
description="Perform (linear) intensity correction across "
"stacks/images given a reference stack/image",
)
input_parser.add_filenames(required=True)
input_parser.add_dir_output(required=True)
input_parser.add_reference(required=True)
input_parser.add_suffix_mask(default="_mask")
input_parser.add_search_angle(default=180)
input_parser.add_prefix_output(default="IC_")
input_parser.add_log_config(default=1)
input_parser.add_option(
option_string="--registration",
type=int,
help="Turn on/off registration from image to reference prior to "
"intensity correction.",
default=0)
input_parser.add_verbose(default=0)
args = input_parser.parse_args()
input_parser.print_arguments(args)
if args.log_config:
input_parser.log_config(os.path.abspath(__file__))
if args.reference in args.filenames:
args.filenames.remove(args.reference)
# Read data
data_reader = dr.MultipleImagesReader(
args.filenames, suffix_mask=args.suffix_mask, extract_slices=False)
data_reader.read_data()
stacks = data_reader.get_data()
data_reader = dr.MultipleImagesReader(
[args.reference], suffix_mask=args.suffix_mask, extract_slices=False)
data_reader.read_data()
reference = data_reader.get_data()[0]
if args.registration:
# Define search angle ranges for FLIRT in all three dimensions
search_angles = ["-searchr%s -%d %d" %
(x, args.search_angle, args.search_angle)
for x in ["x", "y", "z"]]
search_angles = (" ").join(search_angles)
registration = regflirt.FLIRT(
moving=reference,
registration_type="Rigid",
use_fixed_mask=True,
use_moving_mask=True,
options=search_angles,
use_verbose=False,
)
# Perform Intensity Correction
ph.print_title("Perform Intensity Correction")
intensity_corrector = ic.IntensityCorrection(
use_reference_mask=True,
use_individual_slice_correction=False,
prefix_corrected=args.prefix_output,
use_verbose=False,
)
stacks_corrected = [None] * len(stacks)
for i, stack in enumerate(stacks):
if args.registration:
ph.print_info("Image %d/%d: Registration ... "
% (i+1, len(stacks)), newline=False)
registration.set_fixed(stack)
registration.run()
transform_sitk = registration.get_registration_transform_sitk()
stack.update_motion_correction(transform_sitk)
print("done")
ph.print_info("Image %d/%d: Intensity Correction ... "
% (i+1, len(stacks)), newline=False)
ref = reference.get_resampled_stack(stack.sitk)
ref = st.Stack.from_sitk_image(
image_sitk=ref.sitk,
image_sitk_mask=stack.sitk_mask*ref.sitk_mask,
filename=reference.get_filename()
)
intensity_corrector.set_stack(stack)
intensity_corrector.set_reference(ref)
intensity_corrector.run_linear_intensity_correction()
# intensity_corrector.run_affine_intensity_correction()
stacks_corrected[i] = \
intensity_corrector.get_intensity_corrected_stack()
print("done (c1 = %g) " % intensity_corrector.get_intensity_correction_coefficients())
# Write Data
stacks_corrected[i].write(
args.dir_output, write_mask=True, suffix_mask=args.suffix_mask)
if args.verbose:
sitkh.show_stacks([
reference, stacks_corrected[i],
# stacks[i],
],
segmentation=stacks_corrected[i])
# ph.pause()
# Write reference too (although not intensity corrected)
reference.write(args.dir_output,
filename=args.prefix_output+reference.get_filename(),
write_mask=True, suffix_mask=args.suffix_mask)
elapsed_time = ph.stop_timing(time_start)
ph.print_title("Summary")
exe_file_info = os.path.basename(os.path.abspath(__file__)).split(".")[0]
print("%s | Computational Time for Intensity Correction(s): %s" %
(exe_file_info, elapsed_time))
return 0
if __name__ == '__main__':
main()
================================================
FILE: niftymic/application/multiply.py
================================================
##
# \file multiply.py
# \brief Script multiply images with each other.
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date October 2017
#
import os
import SimpleITK as sitk
import pysitk.python_helper as ph
import niftymic.base.data_writer as dw
from niftymic.utilities.input_arparser import InputArgparser
def main():
input_parser = InputArgparser(
description="Multiply images. "
"Pixel type is determined by first given image.",
)
input_parser.add_filenames(required=True)
input_parser.add_output(required=True)
input_parser.add_verbose(default=0)
args = input_parser.parse_args()
input_parser.print_arguments(args)
if len(args.filenames) < 2:
raise IOError("At least two images must be provided")
out_sitk = sitk.ReadImage(args.filenames[0])
for f in args.filenames[1:]:
im_sitk = sitk.Cast(sitk.ReadImage(f), out_sitk.GetPixelIDValue())
out_sitk = out_sitk * im_sitk
dw.DataWriter.write_image(out_sitk, args.output)
if args.verbose:
args.filenames.insert(0, args.output)
ph.show_niftis(args.filenames)
if __name__ == '__main__':
main()
================================================
FILE: niftymic/application/nifti2dicom.py
================================================
##
# \file convert_nifti_to_dicom.py
# \brief Script to convert a 3D NIfTI image to DICOM.
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date Mar 2018
#
# NOTES (quite some candidates were tried to get a working solution):
#
# - nifti2dicom (https://packages.ubuntu.com/xenial/science/nifti2dicom; tested version 0.4.11):
# Although nifti2dicom allows the import of a DICOM header from a template
# (-d) not all tags would be set correctly. E.g. if DOB is not given at
# template, it would just be set to 01.01.1990 which would prevent the
# resulting dcm file to be grouped correctly with the original data.
# Moreover, annoying tags like 'InstitutionName' are set to their predefined
# value which cannot be deleted (only overwritten).
# Apart from that, only a relatively small selection of tags can be edited.
# However, it does a good job in creating a series of 2D DICOM slices from a
# NIfTI file (including correct image orientation!).
#
# - medcon (https://packages.ubuntu.com/xenial/medcon; tested version 0.14.1):
# A single 3D dcm file can be created but image orientation is flawed
# when created from a nifti file directly.
# However, if a 3D stack is created from a set of 2D dicoms, the orientation
# stays correct.
#
# - pydicom (https://github.com/pydicom/pydicom; tested version 1.0.2):
# Can only read a single 3D dcm file. In particular, it is not possible
# to read a set of 2D slices unless a DICOMDIR is provided which is not
# always guaranteed to exist (I tried to create it from 2D slices using
# dcmmkdir from dcmtk and dcm4che -- neither seemed to work reliably)
# Once the dicom file is read, pydicom does a really good job of updating
# DICOM tags + there are plenty of tags available to be chosen from!
# Saving a single 3D DICOM file is very easy too then.
import os
import pydicom
import pysitk.python_helper as ph
import niftymic.base.data_reader as dr
from niftymic.utilities.input_arparser import InputArgparser
from niftymic.definitions import DIR_TMP
COPY_DICOM_TAGS = {
# important for grouping
"PatientID",
"PatientName",
"PatientBirthDate",
"StudyInstanceUID",
# additional information
"StudyID",
"AcquisitionDate",
"PatientSex",
"MagneticFieldStrength",
"Manufacturer",
"ManufacturerModelName",
"Modality",
"StudyDescription",
}
ACCESSION_NUMBER = "1"
SERIES_NUMBER = "0"
IMAGE_COMMENTS = "*** NOT APPROVED FOR CLINICAL USE ***"
def main():
input_parser = InputArgparser(
description="Convert NIfTI to DICOM image",
)
input_parser.add_filename(required=True)
input_parser.add_option(
option_string="--template",
type=str,
required=True,
help="Template DICOM to extract relevant DICOM tags.",
)
input_parser.add_dir_output(required=True)
input_parser.add_label(
help="Label used for series description of DICOM output.",
default="SRR_NiftyMIC")
input_parser.add_argument(
"--volume", "-volume",
action='store_true',
help="If given, the output DICOM file is combined as 3D volume"
)
args = input_parser.parse_args()
input_parser.print_arguments(args)
# Prepare for final DICOM output
ph.create_directory(args.dir_output)
if args.volume:
dir_output_2d_slices = os.path.join(DIR_TMP, "dicom_slices")
else:
dir_output_2d_slices = os.path.join(args.dir_output, args.label)
ph.create_directory(dir_output_2d_slices, delete_files=True)
# read NiftyMIC version (if available)
data_reader = dr.ImageHeaderReader(args.filename)
data_reader.read_data()
niftymic_version = data_reader.get_niftymic_version()
if niftymic_version is None:
niftymic_version = "NiftyMIC"
else:
niftymic_version = "NiftyMIC-v%s" % niftymic_version
# Create set of 2D DICOM slices from 3D NIfTI image
# (correct image orientation!)
ph.print_title("Create set of 2D DICOM slices from 3D NIfTI image")
cmd_args = ["nifti2dicom"]
cmd_args.append("-i '%s'" % args.filename)
cmd_args.append("-o '%s'" % dir_output_2d_slices)
cmd_args.append("-d '%s'" % args.template)
cmd_args.append("--prefix ''")
cmd_args.append("--seriesdescription '%s'" % args.label)
cmd_args.append("--accessionnumber '%s'" % ACCESSION_NUMBER)
cmd_args.append("--seriesnumber '%s'" % SERIES_NUMBER)
cmd_args.append("--institutionname '%s'" % IMAGE_COMMENTS)
# Overwrite default "nifti2dicom" tags which would be added otherwise
# (no deletion/update with empty '' sufficient to overwrite them)
cmd_args.append("--manufacturersmodelname '%s'" % "NiftyMIC")
cmd_args.append("--protocolname '%s'" % niftymic_version)
cmd_args.append("-y")
ph.execute_command(" ".join(cmd_args))
if args.volume:
path_to_output = os.path.join(args.dir_output, "%s.dcm" % args.label)
# Combine set of 2D DICOM slices to form 3D DICOM image
# (image orientation stays correct)
ph.print_title("Combine set of 2D DICOM slices to form 3D DICOM image")
cmd_args = ["medcon"]
cmd_args.append("-f '%s'/*.dcm" % dir_output_2d_slices)
cmd_args.append("-o '%s'" % path_to_output)
cmd_args.append("-c dicom")
cmd_args.append("-stack3d")
cmd_args.append("-n")
cmd_args.append("-qc")
cmd_args.append("-w")
ph.execute_command(" ".join(cmd_args))
# Update all relevant DICOM tags accordingly
ph.print_title("Update all relevant DICOM tags accordingly")
print("")
dataset_template = pydicom.dcmread(args.template)
dataset = pydicom.dcmread(path_to_output)
# Copy tags from template (to guarantee grouping with original data)
update_dicom_tags = {}
for tag in COPY_DICOM_TAGS:
try:
update_dicom_tags[tag] = getattr(dataset_template, tag)
except:
update_dicom_tags[tag] = ""
# Additional tags
update_dicom_tags["SeriesDescription"] = args.label
update_dicom_tags["InstitutionName"] = institution_name
update_dicom_tags["ImageComments"] = IMAGE_COMMENTS
update_dicom_tags["AccessionNumber"] = ACCESSION_NUMBER
update_dicom_tags["SeriesNumber"] = SERIES_NUMBER
for tag in sorted(update_dicom_tags.keys()):
value = update_dicom_tags[tag]
setattr(dataset, tag, value)
ph.print_info("%s: '%s'" % (tag, value))
dataset.save_as(path_to_output)
print("")
ph.print_info("3D DICOM image written to '%s'" % path_to_output)
else:
ph.print_info("DICOM images written to '%s'" % dir_output_2d_slices)
return 0
if __name__ == '__main__':
main()
================================================
FILE: niftymic/application/propagate_mask.py
================================================
##
# \file propagate_mask.py
# \brief Script to propagate an image mask using rigid registration
#
# \author Michael Ebner (michael.ebner@kcl.ac.uk)
# \date Aug 2019
#
import os
import numpy as np
import SimpleITK as sitk
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
import niftymic.base.data_writer as dw
import niftymic.base.stack as st
import niftymic.registration.flirt as regflirt
import niftymic.registration.niftyreg as niftyreg
import niftymic.utilities.stack_mask_morphological_operations as stmorph
from niftymic.utilities.input_arparser import InputArgparser
from niftymic.definitions import V2V_METHOD_OPTIONS, ALLOWED_EXTENSIONS
def main():
time_start = ph.start_timing()
# Set print options for numpy
np.set_printoptions(precision=3)
input_parser = InputArgparser(
description="Propagate image mask using rigid registration.",
)
input_parser.add_moving(required=True)
input_parser.add_moving_mask(required=True)
input_parser.add_fixed(required=True)
input_parser.add_output(required=True)
input_parser.add_v2v_method(
option_string="--method",
help="Registration method used for the registration (%s)." % (
", or ".join(V2V_METHOD_OPTIONS)),
default="RegAladin",
)
input_parser.add_option(
option_string="--use-moving-mask",
type=int,
help="Turn on/off use of moving mask to constrain the registration.",
default=0,
)
input_parser.add_dilation_radius(default=1)
input_parser.add_verbose(default=0)
input_parser.add_log_config(default=0)
args = input_parser.parse_args()
input_parser.print_arguments(args)
if np.alltrue([not args.output.endswith(t) for t in ALLOWED_EXTENSIONS]):
raise ValueError(
"output filename invalid; allowed extensions are: %s" %
", ".join(ALLOWED_EXTENSIONS))
if args.method not in V2V_METHOD_OPTIONS:
raise ValueError("method must be in {%s}" % (
", ".join(V2V_METHOD_OPTIONS)))
if args.log_config:
input_parser.log_config(os.path.abspath(__file__))
stack = st.Stack.from_filename(
file_path=args.fixed,
extract_slices=False,
)
template = st.Stack.from_filename(
file_path=args.moving,
file_path_mask=args.moving_mask,
extract_slices=False,
)
if args.method == "FLIRT":
# Define search angle ranges for FLIRT in all three dimensions
# search_angles = ["-searchr%s -%d %d" %
# (x, args.search_angle, args.search_angle)
# for x in ["x", "y", "z"]]
# options = (" ").join(search_angles)
# options += " -noresample"
registration = regflirt.FLIRT(
registration_type="Rigid",
fixed=stack,
moving=template,
use_fixed_mask=False,
use_moving_mask=args.use_moving_mask,
# options=options,
use_verbose=False,
)
else:
registration = niftyreg.RegAladin(
registration_type="Rigid",
fixed=stack,
moving=template,
use_fixed_mask=False,
use_moving_mask=args.use_moving_mask,
# options="-ln 2",
use_verbose=False,
)
try:
registration.run()
except RuntimeError as e:
raise RuntimeError(
"%s\n\n"
"Have you tried running the script with '--use-moving-mask 0'?" % e)
transform_sitk = registration.get_registration_transform_sitk()
stack.sitk_mask = sitk.Resample(
template.sitk_mask,
stack.sitk_mask,
transform_sitk,
sitk.sitkNearestNeighbor,
0,
template.sitk_mask.GetPixelIDValue()
)
if args.dilation_radius > 0:
stack_mask_morpher = stmorph.StackMaskMorphologicalOperations.from_sitk_mask(
mask_sitk=stack.sitk_mask,
dilation_radius=args.dilation_radius,
dilation_kernel="Ball",
use_dilation_in_plane_only=True,
)
stack_mask_morpher.run_dilation()
stack.sitk_mask = stack_mask_morpher.get_processed_mask_sitk()
dw.DataWriter.write_mask(stack.sitk_mask, args.output)
elapsed_time = ph.stop_timing(time_start)
if args.verbose:
ph.show_nifti(args.fixed, segmentation=args.output)
ph.print_title("Summary")
exe_file_info = os.path.basename(os.path.abspath(__file__)).split(".")[0]
print("%s | Computational Time for Segmentation Propagation: %s" % (
exe_file_info, elapsed_time))
return 0
if __name__ == '__main__':
main()
================================================
FILE: niftymic/application/reconstruct_volume.py
================================================
##
# \file reconstruct_volume.py
# \brief Script to reconstruct an isotropic, high-resolution volume from
# multiple stacks of low-resolution 2D slices including
# motion-correction.
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date March 2017
#
import os
import numpy as np
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
import niftymic.base.stack as st
import niftymic.base.data_reader as dr
import niftymic.base.data_writer as dw
import niftymic.registration.flirt as regflirt
import niftymic.registration.niftyreg as niftyreg
import niftymic.registration.simple_itk_registration as regsitk
import niftymic.reconstruction.tikhonov_solver as tk
import niftymic.reconstruction.primal_dual_solver as pd
import niftymic.reconstruction.scattered_data_approximation as sda
import niftymic.utilities.data_preprocessing as dp
import niftymic.utilities.outlier_rejector as outre
import niftymic.utilities.intensity_correction as ic
import niftymic.utilities.joint_image_mask_builder as imb
import niftymic.utilities.segmentation_propagation as segprop
import niftymic.utilities.volumetric_reconstruction_pipeline as pipeline
from niftymic.utilities.input_arparser import InputArgparser
from niftymic.definitions import V2V_METHOD_OPTIONS, ALLOWED_EXTENSIONS
def main():
time_start = ph.start_timing()
# Set print options for numpy
np.set_printoptions(precision=3)
input_parser = InputArgparser(
description="Volumetric MRI reconstruction framework to reconstruct "
"an isotropic, high-resolution 3D volume from multiple stacks of 2D "
"slices with motion correction. The resolution of the computed "
"Super-Resolution Reconstruction (SRR) is given by the in-plane "
"spacing of the selected target stack. A region of interest can be "
"specified by providing a mask for the selected target stack. Only "
"this region will then be reconstructed by the SRR algorithm which "
"can substantially reduce the computational time.",
)
input_parser.add_filenames(required=True)
input_parser.add_filenames_masks()
input_parser.add_output(required=True)
input_parser.add_suffix_mask(default="_mask")
input_parser.add_target_stack(default=None)
input_parser.add_search_angle(default=45)
input_parser.add_multiresolution(default=0)
input_parser.add_shrink_factors(default=[3, 2, 1])
input_parser.add_smoothing_sigmas(default=[1.5, 1, 0])
input_parser.add_sigma(default=1)
input_parser.add_reconstruction_type(default="TK1L2")
input_parser.add_iterations(default=15)
input_parser.add_alpha(default=0.015)
input_parser.add_alpha_first(default=0.2)
input_parser.add_iter_max(default=10)
input_parser.add_iter_max_first(default=5)
input_parser.add_dilation_radius(default=3)
input_parser.add_extra_frame_target(default=10)
input_parser.add_bias_field_correction(default=0)
input_parser.add_intensity_correction(default=1)
input_parser.add_isotropic_resolution(default=1)
input_parser.add_log_config(default=1)
input_parser.add_subfolder_motion_correction()
input_parser.add_write_motion_correction(default=1)
input_parser.add_verbose(default=0)
input_parser.add_two_step_cycles(default=3)
input_parser.add_use_masks_srr(default=0)
input_parser.add_boundary_stacks(default=[10, 10, 0])
input_parser.add_metric(default="Correlation")
input_parser.add_metric_radius(default=10)
input_parser.add_reference()
input_parser.add_reference_mask()
input_parser.add_outlier_rejection(default=1)
input_parser.add_threshold_first(default=0.5)
input_parser.add_threshold(default=0.8)
input_parser.add_interleave(default=3)
input_parser.add_slice_thicknesses(default=None)
input_parser.add_viewer(default="itksnap")
input_parser.add_v2v_method(default="RegAladin")
input_parser.add_argument(
"--v2v-robust", "-v2v-robust",
action='store_true',
help="If given, a more robust volume-to-volume registration step is "
"performed, i.e. four rigid registrations are performed using four "
"rigid transform initializations based on "
"principal component alignment of associated masks."
)
input_parser.add_argument(
"--s2v-hierarchical", "-s2v-hierarchical",
action='store_true',
help="If given, a hierarchical approach for the first slice-to-volume "
"registration cycle is used, i.e. sub-packages defined by the "
"specified interleave (--interleave) are registered until each "
"slice is registered independently."
)
input_parser.add_argument(
"--sda", "-sda",
action='store_true',
help="If given, the volumetric reconstructions are performed using "
"Scattered Data Approximation (Vercauteren et al., 2006). "
"'alpha' is considered the final 'sigma' for the "
"iterative adjustment. "
"Recommended value is, e.g., --alpha 0.8"
)
input_parser.add_option(
option_string="--transforms-history",
type=int,
help="Write entire history of applied slice motion correction "
"transformations to motion correction output directory",
default=0,
)
args = input_parser.parse_args()
input_parser.print_arguments(args)
rejection_measure = "NCC"
threshold_v2v = -2 # 0.3
debug = False
if args.v2v_method not in V2V_METHOD_OPTIONS:
raise ValueError("v2v-method must be in {%s}" % (
", ".join(V2V_METHOD_OPTIONS)))
if np.alltrue([not args.output.endswith(t) for t in ALLOWED_EXTENSIONS]):
raise ValueError(
"output filename invalid; allowed extensions are: %s" %
", ".join(ALLOWED_EXTENSIONS))
if args.alpha_first < args.alpha and not args.sda:
raise ValueError("It must hold alpha-first >= alpha")
if args.threshold_first > args.threshold:
raise ValueError("It must hold threshold-first <= threshold")
dir_output = os.path.dirname(args.output)
ph.create_directory(dir_output)
if args.log_config:
input_parser.log_config(os.path.abspath(__file__))
# --------------------------------Read Data--------------------------------
ph.print_title("Read Data")
data_reader = dr.MultipleImagesReader(
file_paths=args.filenames,
file_paths_masks=args.filenames_masks,
suffix_mask=args.suffix_mask,
stacks_slice_thicknesses=args.slice_thicknesses,
)
if len(args.boundary_stacks) is not 3:
raise IOError(
"Provide exactly three values for '--boundary-stacks' to define "
"cropping in i-, j-, and k-dimension of the input stacks")
data_reader.read_data()
stacks = data_reader.get_data()
ph.print_info("%d input stacks read for further processing" % len(stacks))
if all(s.is_unity_mask() is True for s in stacks):
ph.print_warning("No mask is provided! "
"Generated reconstruction space may be very big!")
ph.print_warning("Consider using a mask to speed up computations")
# args.extra_frame_target = 0
# ph.wrint_warning("Overwritten: extra-frame-target set to 0")
# Specify target stack for intensity correction and reconstruction space
if args.target_stack is None:
target_stack_index = 0
else:
try:
target_stack_index = args.filenames.index(args.target_stack)
except ValueError as e:
raise ValueError(
"--target-stack must correspond to an image as provided by "
"--filenames")
# ---------------------------Data Preprocessing---------------------------
ph.print_title("Data Preprocessing")
segmentation_propagator = segprop.SegmentationPropagation(
# registration_method=regflirt.FLIRT(use_verbose=args.verbose),
# registration_method=niftyreg.RegAladin(use_verbose=False),
dilation_radius=args.dilation_radius,
dilation_kernel="Ball",
)
data_preprocessing = dp.DataPreprocessing(
stacks=stacks,
segmentation_propagator=segmentation_propagator,
use_cropping_to_mask=True,
use_N4BiasFieldCorrector=args.bias_field_correction,
target_stack_index=target_stack_index,
boundary_i=args.boundary_stacks[0],
boundary_j=args.boundary_stacks[1],
boundary_k=args.boundary_stacks[2],
unit="mm",
)
data_preprocessing.run()
time_data_preprocessing = data_preprocessing.get_computational_time()
# Get preprocessed stacks
stacks = data_preprocessing.get_preprocessed_stacks()
# Define reference/target stack for registration and reconstruction
if args.reference is not None:
reference = st.Stack.from_filename(
file_path=args.reference,
file_path_mask=args.reference_mask,
extract_slices=False)
else:
reference = st.Stack.from_stack(stacks[target_stack_index])
# ------------------------Volume-to-Volume Registration--------------------
if len(stacks) > 1:
if args.v2v_method == "FLIRT":
# Define search angle ranges for FLIRT in all three dimensions
search_angles = ["-searchr%s -%d %d" %
(x, args.search_angle, args.search_angle)
for x in ["x", "y", "z"]]
options = (" ").join(search_angles)
# options += " -noresample"
vol_registration = regflirt.FLIRT(
registration_type="Rigid",
use_fixed_mask=True,
use_moving_mask=True,
options=options,
use_verbose=False,
)
else:
vol_registration = niftyreg.RegAladin(
registration_type="Rigid",
use_fixed_mask=True,
use_moving_mask=True,
# options="-ln 2 -voff",
use_verbose=False,
)
v2vreg = pipeline.VolumeToVolumeRegistration(
stacks=stacks,
reference=reference,
registration_method=vol_registration,
verbose=debug,
robust=args.v2v_robust,
)
v2vreg.run()
stacks = v2vreg.get_stacks()
time_registration = v2vreg.get_computational_time()
else:
time_registration = ph.get_zero_time()
# ---------------------------Intensity Correction--------------------------
if args.intensity_correction:
ph.print_title("Intensity Correction")
intensity_corrector = ic.IntensityCorrection()
intensity_corrector.use_individual_slice_correction(False)
intensity_corrector.use_reference_mask(True)
intensity_corrector.use_stack_mask(True)
intensity_corrector.use_verbose(False)
for i, stack in enumerate(stacks):
if i == target_stack_index:
ph.print_info("Stack %d (%s): Reference image. Skipped." % (
i + 1, stack.get_filename()))
continue
else:
ph.print_info("Stack %d (%s): Intensity Correction ... " % (
i + 1, stack.get_filename()), newline=False)
intensity_corrector.set_stack(stack)
intensity_corrector.set_reference(
stacks[target_stack_index].get_resampled_stack(
resampling_grid=stack.sitk,
interpolator="NearestNeighbor",
))
intensity_corrector.run_linear_intensity_correction()
stacks[i] = intensity_corrector.get_intensity_corrected_stack()
print("done (c1 = %g) " %
intensity_corrector.get_intensity_correction_coefficients())
# ---------------------------Create first volume---------------------------
time_tmp = ph.start_timing()
# Isotropic resampling to define HR target space
ph.print_title("Reconstruction Space Generation")
HR_volume = reference.get_isotropically_resampled_stack(
resolution=args.isotropic_resolution)
ph.print_info(
"Isotropic reconstruction space with %g mm resolution is created" %
HR_volume.sitk.GetSpacing()[0])
if args.reference is None:
# Create joint image mask in target space
joint_image_mask_builder = imb.JointImageMaskBuilder(
stacks=stacks,
target=HR_volume,
dilation_radius=1,
)
joint_image_mask_builder.run()
HR_volume = joint_image_mask_builder.get_stack()
ph.print_info(
"Isotropic reconstruction space is centered around "
"joint stack masks. ")
# Crop to space defined by mask (plus extra margin)
HR_volume = HR_volume.get_cropped_stack_based_on_mask(
boundary_i=args.extra_frame_target,
boundary_j=args.extra_frame_target,
boundary_k=args.extra_frame_target,
unit="mm",
)
# Create first volume
# If outlier rejection is activated, eliminate obvious outliers early
# from stack and re-run SDA to get initial volume without them
ph.print_title("First Estimate of HR Volume")
if args.outlier_rejection and threshold_v2v > -1:
ph.print_subtitle("SDA Approximation")
SDA = sda.ScatteredDataApproximation(
stacks, HR_volume, sigma=args.sigma)
SDA.run()
HR_volume = SDA.get_reconstruction()
# Identify and reject outliers
ph.print_subtitle("Eliminate slice outliers (%s < %g)" % (
rejection_measure, threshold_v2v))
outlier_rejector = outre.OutlierRejector(
stacks=stacks,
reference=HR_volume,
threshold=threshold_v2v,
measure=rejection_measure,
verbose=True,
)
outlier_rejector.run()
stacks = outlier_rejector.get_stacks()
ph.print_subtitle("SDA Approximation Image")
SDA = sda.ScatteredDataApproximation(
stacks, HR_volume, sigma=args.sigma)
SDA.run()
HR_volume = SDA.get_reconstruction()
ph.print_subtitle("SDA Approximation Image Mask")
SDA = sda.ScatteredDataApproximation(
stacks, HR_volume, sigma=args.sigma, sda_mask=True)
SDA.run()
# HR volume contains updated mask based on SDA
HR_volume = SDA.get_reconstruction()
HR_volume.set_filename(SDA.get_setting_specific_filename())
time_reconstruction = ph.stop_timing(time_tmp)
if args.verbose:
tmp = list(stacks)
tmp.insert(0, HR_volume)
sitkh.show_stacks(tmp, segmentation=HR_volume, viewer=args.viewer)
# -----------Two-step Slice-to-Volume Registration-Reconstruction----------
if args.two_step_cycles > 0:
# Slice-to-volume registration set-up
if args.metric == "ANTSNeighborhoodCorrelation":
metric_params = {"radius": args.metric_radius}
else:
metric_params = None
registration = regsitk.SimpleItkRegistration(
moving=HR_volume,
use_fixed_mask=True,
use_moving_mask=True,
interpolator="Linear",
metric=args.metric,
metric_params=metric_params,
use_multiresolution_framework=args.multiresolution,
shrink_factors=args.shrink_factors,
smoothing_sigmas=args.smoothing_sigmas,
initializer_type="SelfGEOMETRY",
optimizer="ConjugateGradientLineSearch",
optimizer_params={
"learningRate": 1,
"numberOfIterations": 100,
"lineSearchUpperLimit": 2,
},
scales_estimator="Jacobian",
use_verbose=debug,
)
# Volumetric reconstruction set-up
if args.sda:
recon_method = sda.ScatteredDataApproximation(
stacks,
HR_volume,
sigma=args.sigma,
use_masks=args.use_masks_srr,
)
alpha_range = [args.sigma, args.alpha]
else:
recon_method = tk.TikhonovSolver(
stacks=stacks,
reconstruction=HR_volume,
reg_type="TK1",
minimizer="lsmr",
alpha=args.alpha_first,
iter_max=np.min([args.iter_max_first, args.iter_max]),
verbose=True,
use_masks=args.use_masks_srr,
)
alpha_range = [args.alpha_first, args.alpha]
# Define the regularization parameters for the individual
# reconstruction steps in the two-step cycles
alphas = np.linspace(
alpha_range[0], alpha_range[1], args.two_step_cycles)
# Define outlier rejection threshold after each S2V-reg step
thresholds = np.linspace(
args.threshold_first, args.threshold, args.two_step_cycles)
two_step_s2v_reg_recon = \
pipeline.TwoStepSliceToVolumeRegistrationReconstruction(
stacks=stacks,
reference=HR_volume,
registration_method=registration,
reconstruction_method=recon_method,
cycles=args.two_step_cycles,
alphas=alphas[0:args.two_step_cycles - 1],
outlier_rejection=args.outlier_rejection,
threshold_measure=rejection_measure,
thresholds=thresholds,
interleave=args.interleave,
viewer=args.viewer,
verbose=args.verbose,
use_hierarchical_registration=args.s2v_hierarchical,
)
two_step_s2v_reg_recon.run()
HR_volume_iterations = \
two_step_s2v_reg_recon.get_iterative_reconstructions()
time_registration += \
two_step_s2v_reg_recon.get_computational_time_registration()
time_reconstruction += \
two_step_s2v_reg_recon.get_computational_time_reconstruction()
stacks = two_step_s2v_reg_recon.get_stacks()
# no two-step s2v-registration/reconstruction iterations
else:
HR_volume_iterations = []
# Write motion-correction results
ph.print_title("Write Motion Correction Results")
if args.write_motion_correction:
dir_output_mc = os.path.join(
dir_output, args.subfolder_motion_correction)
ph.clear_directory(dir_output_mc)
for stack in stacks:
stack.write(
dir_output_mc,
write_stack=False,
write_mask=False,
write_slices=False,
write_transforms=True,
write_transforms_history=args.transforms_history,
)
if args.outlier_rejection:
deleted_slices_dic = {}
for i, stack in enumerate(stacks):
deleted_slices = stack.get_deleted_slice_numbers()
deleted_slices_dic[stack.get_filename()] = deleted_slices
# check whether any stack was removed entirely
stacks0 = data_preprocessing.get_preprocessed_stacks()
if len(stacks) != len(stacks0):
stacks_remain = [s.get_filename() for s in stacks]
for stack in stacks0:
if stack.get_filename() in stacks_remain:
continue
# add info that all slices of this stack were rejected
deleted_slices = [
slice.get_slice_number()
for slice in stack.get_slices()
]
deleted_slices_dic[stack.get_filename()] = deleted_slices
ph.print_info(
"All slices of stack '%s' were rejected entirely. "
"Information added." % stack.get_filename())
ph.write_dictionary_to_json(
deleted_slices_dic,
os.path.join(
dir_output,
args.subfolder_motion_correction,
"rejected_slices.json"
)
)
# ---------------------Final Volumetric Reconstruction---------------------
ph.print_title("Final Volumetric Reconstruction")
if args.sda:
recon_method = sda.ScatteredDataApproximation(
stacks,
HR_volume,
sigma=args.alpha,
use_masks=args.use_masks_srr,
)
else:
if args.reconstruction_type in ["TVL2", "HuberL2"]:
recon_method = pd.PrimalDualSolver(
stacks=stacks,
reconstruction=HR_volume,
reg_type="TV" if args.reconstruction_type == "TVL2" else "huber",
iterations=args.iterations,
use_masks=args.use_masks_srr,
)
else:
recon_method = tk.TikhonovSolver(
stacks=stacks,
reconstruction=HR_volume,
reg_type="TK1" if args.reconstruction_type == "TK1L2" else "TK0",
use_masks=args.use_masks_srr,
)
recon_method.set_alpha(args.alpha)
recon_method.set_iter_max(args.iter_max)
recon_method.set_verbose(True)
recon_method.run()
time_reconstruction += recon_method.get_computational_time()
HR_volume_final = recon_method.get_reconstruction()
ph.print_subtitle("Final SDA Approximation Image Mask")
SDA = sda.ScatteredDataApproximation(
stacks, HR_volume_final, sigma=args.sigma, sda_mask=True)
SDA.run()
# HR volume contains updated mask based on SDA
HR_volume_final = SDA.get_reconstruction()
time_reconstruction += SDA.get_computational_time()
elapsed_time_total = ph.stop_timing(time_start)
# Write SRR result
filename = recon_method.get_setting_specific_filename()
HR_volume_final.set_filename(filename)
dw.DataWriter.write_image(
HR_volume_final.sitk,
args.output,
description=filename)
dw.DataWriter.write_mask(
HR_volume_final.sitk_mask,
ph.append_to_filename(args.output, "_mask"),
description=SDA.get_setting_specific_filename())
HR_volume_iterations.insert(0, HR_volume_final)
for stack in stacks:
HR_volume_iterations.append(stack)
if args.verbose:
sitkh.show_stacks(
HR_volume_iterations,
segmentation=HR_volume_final,
viewer=args.viewer,
)
# Summary
ph.print_title("Summary")
exe_file_info = os.path.basename(os.path.abspath(__file__)).split(".")[0]
print("%s | Computational Time for Data Preprocessing: %s" %
(exe_file_info, time_data_preprocessing))
print("%s | Computational Time for Registrations: %s" %
(exe_file_info, time_registration))
print("%s | Computational Time for Reconstructions: %s" %
(exe_file_info, time_reconstruction))
print("%s | Computational Time for Entire Reconstruction Pipeline: %s" %
(exe_file_info, elapsed_time_total))
ph.print_line_separator()
return 0
if __name__ == '__main__':
main()
================================================
FILE: niftymic/application/reconstruct_volume_from_slices.py
================================================
##
# \file reconstruct_volume_from_slices.py
# \brief Script to reconstruct an isotropic, high-resolution volume from
# multiple motion-corrected (or static) stacks of low-resolution 2D
# slices.
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date March 2017
#
import os
import numpy as np
import SimpleITK as sitk
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
import niftymic.base.stack as st
import niftymic.base.data_reader as dr
import niftymic.base.data_writer as dw
import niftymic.reconstruction.admm_solver as admm
import niftymic.utilities.intensity_correction as ic
import niftymic.reconstruction.primal_dual_solver as pd
import niftymic.reconstruction.tikhonov_solver as tk
import niftymic.reconstruction.scattered_data_approximation as sda
import niftymic.utilities.binary_mask_from_mask_srr_estimator as bm
from niftymic.utilities.input_arparser import InputArgparser
from niftymic.definitions import ALLOWED_EXTENSIONS
def main():
time_start = ph.start_timing()
# Set print options for numpy
np.set_printoptions(precision=3)
# Read input
input_parser = InputArgparser(
description="Volumetric MRI reconstruction framework to reconstruct "
"an isotropic, high-resolution 3D volume from multiple "
"motion-corrected (or static) stacks of low-resolution slices.",
)
input_parser.add_filenames(required=True)
input_parser.add_filenames_masks()
input_parser.add_dir_input_mc()
input_parser.add_output(required=True)
input_parser.add_suffix_mask(default="_mask")
input_parser.add_target_stack(default=None)
input_parser.add_extra_frame_target(default=10)
input_parser.add_isotropic_resolution(default=None)
input_parser.add_intensity_correction(default=1)
input_parser.add_reconstruction_space(default=None)
input_parser.add_minimizer(default="lsmr")
input_parser.add_iter_max(default=10)
input_parser.add_reconstruction_type(default="TK1L2")
input_parser.add_data_loss(default="linear")
input_parser.add_data_loss_scale(default=1)
input_parser.add_alpha(
default=0.01 # TK1L2 @ isotropic_resolution = 0.8
# default=0.006 #TVL2, HuberL2 @ isotropic_resolution = 0.8
)
input_parser.add_rho(default=0.1)
input_parser.add_tv_solver(default="PD")
input_parser.add_pd_alg_type(default="ALG2")
input_parser.add_iterations(default=15)
input_parser.add_log_config(default=1)
input_parser.add_use_masks_srr(default=0)
input_parser.add_slice_thicknesses(default=None)
input_parser.add_verbose(default=0)
input_parser.add_viewer(default="itksnap")
input_parser.add_argument(
"--mask", "-mask",
action='store_true',
help="If given, input images are interpreted as image masks. "
"Obtained volumetric reconstruction will be exported in uint8 format."
)
input_parser.add_argument(
"--sda", "-sda",
action='store_true',
help="If given, the volume is reconstructed using "
"Scattered Data Approximation (Vercauteren et al., 2006). "
"--alpha is considered the value for the standard deviation then. "
"Recommended value is, e.g., --alpha 0.8"
)
args = input_parser.parse_args()
input_parser.print_arguments(args)
if args.reconstruction_type not in ["TK1L2", "TVL2", "HuberL2"]:
raise IOError("Reconstruction type unknown")
if np.alltrue([not args.output.endswith(t) for t in ALLOWED_EXTENSIONS]):
raise ValueError(
"output filename '%s' invalid; "
"allowed image extensions are: %s" % (
args.output, ", ".join(ALLOWED_EXTENSIONS)))
dir_output = os.path.dirname(args.output)
ph.create_directory(dir_output)
debug = 0
if args.log_config:
input_parser.log_config(os.path.abspath(__file__))
if args.verbose:
show_niftis = []
# show_niftis = [f for f in args.filenames]
# --------------------------------Read Data--------------------------------
ph.print_title("Read Data")
if args.mask:
filenames_masks = args.filenames
else:
filenames_masks = args.filenames_masks
data_reader = dr.MultipleImagesReader(
file_paths=args.filenames,
file_paths_masks=filenames_masks,
suffix_mask=args.suffix_mask,
dir_motion_correction=args.dir_input_mc,
stacks_slice_thicknesses=args.slice_thicknesses,
)
data_reader.read_data()
stacks = data_reader.get_data()
ph.print_info("%d input stacks read for further processing" % len(stacks))
# Specify target stack for intensity correction and reconstruction space
if args.target_stack is None:
target_stack_index = 0
else:
# TODO: deal with case when target stack got rejected in previous step
filenames = ["%s.nii.gz" % s.get_filename() for s in stacks]
filename_target_stack = os.path.basename(args.target_stack)
try:
target_stack_index = filenames.index(filename_target_stack)
except ValueError as e:
raise ValueError(
"--target-stack must correspond to an image as provided by "
"--filenames")
# ---------------------------Intensity Correction--------------------------
if args.intensity_correction and not args.mask:
ph.print_title("Intensity Correction")
intensity_corrector = ic.IntensityCorrection()
intensity_corrector.use_individual_slice_correction(False)
intensity_corrector.use_stack_mask(True)
intensity_corrector.use_reference_mask(True)
intensity_corrector.use_verbose(False)
for i, stack in enumerate(stacks):
if i == target_stack_index:
ph.print_info("Stack %d (%s): Reference image. Skipped." % (
i + 1, stack.get_filename()))
continue
else:
ph.print_info("Stack %d (%s): Intensity Correction ... " % (
i + 1, stack.get_filename()), newline=False)
intensity_corrector.set_stack(stack)
intensity_corrector.set_reference(
stacks[target_stack_index].get_resampled_stack(
resampling_grid=stack.sitk,
interpolator="NearestNeighbor",
))
intensity_corrector.run_linear_intensity_correction()
stacks[i] = intensity_corrector.get_intensity_corrected_stack()
print("done (c1 = %g) " %
intensity_corrector.get_intensity_correction_coefficients())
# -------------------------Volumetric Reconstruction-----------------------
ph.print_title("Volumetric Reconstruction")
# Reconstruction space defined by isotropically resampled,
# bounding box-cropped target stack
if args.reconstruction_space is None:
recon0 = stacks[target_stack_index].get_isotropically_resampled_stack(
resolution=args.isotropic_resolution,
extra_frame=args.extra_frame_target,
)
recon0 = recon0.get_cropped_stack_based_on_mask(
boundary_i=args.extra_frame_target,
boundary_j=args.extra_frame_target,
boundary_k=args.extra_frame_target,
unit="mm",
)
# Reconstruction space was provided by user
else:
recon0 = st.Stack.from_filename(args.reconstruction_space,
extract_slices=False)
# Change resolution for isotropic resolution if provided by user
if args.isotropic_resolution is not None:
recon0 = recon0.get_isotropically_resampled_stack(
args.isotropic_resolution)
# Use image information of selected target stack as recon0 serves
# as initial value for reconstruction
recon0 = stacks[target_stack_index].get_resampled_stack(recon0.sitk)
recon0 = recon0.get_stack_multiplied_with_mask()
ph.print_info(
"Reconstruction space defined with %s mm3 resolution" %
" x ".join(["%.2f" % s for s in recon0.sitk.GetSpacing()])
)
if debug:
# visualize (intensity corrected) data alongside recon0 init
show = [st.Stack.from_stack(s) for s in stacks]
show.insert(0, recon0)
sitkh.show_stacks(show)
if args.sda:
ph.print_title("Compute SDA reconstruction")
SDA = sda.ScatteredDataApproximation(
stacks, recon0, sigma=args.alpha, sda_mask=args.mask)
SDA.run()
recon = SDA.get_reconstruction()
filename = SDA.get_setting_specific_filename()
if args.mask:
dw.DataWriter.write_mask(
recon.sitk_mask, args.output, description=filename)
else:
dw.DataWriter.write_image(
recon.sitk, args.output, description=filename)
if args.verbose:
show_niftis.insert(0, args.output)
else:
if args.reconstruction_type in ["TVL2", "HuberL2"]:
ph.print_title(
"Compute Initial value for %s" % args.reconstruction_type)
SRR0 = sda.ScatteredDataApproximation(stacks, recon0, sigma=0.8)
else:
ph.print_title(
"Compute %s reconstruction" % args.reconstruction_type)
SRR0 = tk.TikhonovSolver(
stacks=stacks,
reconstruction=recon0,
alpha=args.alpha,
iter_max=args.iter_max,
reg_type="TK1",
minimizer=args.minimizer,
data_loss=args.data_loss,
data_loss_scale=args.data_loss_scale,
use_masks=args.use_masks_srr,
# verbose=args.verbose,
)
SRR0.run()
recon = SRR0.get_reconstruction()
filename = SRR0.get_setting_specific_filename()
if args.verbose and args.reconstruction_type in ["TVL2", "HuberL2"]:
output = ph.append_to_filename(args.output, "_init")
if args.mask:
mask_estimator = bm.BinaryMaskFromMaskSRREstimator(recon.sitk)
mask_estimator.run()
mask_sitk = mask_estimator.get_mask_sitk()
dw.DataWriter.write_mask(
mask_sitk, output, description=filename)
else:
dw.DataWriter.write_image(
recon.sitk, output, description=filename)
show_niftis.insert(0, output)
if args.reconstruction_type in ["TVL2", "HuberL2"]:
ph.print_title("Compute %s reconstruction" %
args.reconstruction_type)
if args.tv_solver == "ADMM":
SRR = admm.ADMMSolver(
stacks=stacks,
reconstruction=st.Stack.from_stack(
SRR0.get_reconstruction()),
minimizer=args.minimizer,
alpha=args.alpha,
iter_max=args.iter_max,
rho=args.rho,
data_loss=args.data_loss,
iterations=args.iterations,
use_masks=args.use_masks_srr,
verbose=args.verbose,
)
else:
SRR = pd.PrimalDualSolver(
stacks=stacks,
reconstruction=st.Stack.from_stack(
SRR0.get_reconstruction()),
minimizer=args.minimizer,
alpha=args.alpha,
iter_max=args.iter_max,
iterations=args.iterations,
alg_type=args.pd_alg_type,
reg_type="TV" if args.reconstruction_type == "TVL2" else "huber",
data_loss=args.data_loss,
use_masks=args.use_masks_srr,
verbose=args.verbose,
)
SRR.run()
recon = SRR.get_reconstruction()
filename = SRR.get_setting_specific_filename()
if args.mask:
mask_estimator = bm.BinaryMaskFromMaskSRREstimator(recon.sitk)
mask_estimator.run()
mask_sitk = mask_estimator.get_mask_sitk()
dw.DataWriter.write_mask(
mask_sitk, args.output, description=filename)
else:
dw.DataWriter.write_image(
recon.sitk, args.output, description=filename)
if args.verbose:
show_niftis.insert(0, args.output)
if args.verbose:
ph.show_niftis(show_niftis, viewer=args.viewer)
ph.print_line_separator()
elapsed_time = ph.stop_timing(time_start)
ph.print_title("Summary")
exe_file_info = os.path.basename(os.path.abspath(__file__)).split(".")[0]
print("%s | Computational Time for Volumetric Reconstruction: %s" % (
exe_file_info, elapsed_time))
return 0
if __name__ == '__main__':
main()
================================================
FILE: niftymic/application/register_image.py
================================================
##
# \file register_image.py
# \brief Script to register the obtained reconstruction to a template
# space.
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date October 2017
#
import re
import os
import numpy as np
import SimpleITK as sitk
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
import niftymic.base.stack as st
import niftymic.base.data_reader as dr
import niftymic.registration.niftyreg as niftyreg
import niftymic.registration.transform_initializer as tinit
from niftymic.utilities.input_arparser import InputArgparser
from niftymic.definitions import REGEX_FILENAMES, DIR_TMP
def main():
time_start = ph.start_timing()
np.set_printoptions(precision=3)
input_parser = InputArgparser(
description="Register an obtained reconstruction (moving) "
"to a template image/space (fixed) using rigid registration. "
"The resulting registration can optionally be applied to previously "
"obtained motion correction slice transforms so that a volumetric "
"reconstruction is possible in the (standard anatomical) space "
"defined by the fixed.",
)
input_parser.add_fixed(required=True)
input_parser.add_moving(required=True)
input_parser.add_output(
help="Path to registration transform (.txt)",
required=True)
input_parser.add_fixed_mask(required=False)
input_parser.add_moving_mask(required=False)
input_parser.add_option(
option_string="--initial-transform",
type=str,
help="Path to initial transform. "
"If not provided, registration will be initialized based on "
"rigid alignment of eigenbasis of the fixed/moving image masks "
"using principal component analysis",
default=None)
input_parser.add_v2v_method(
option_string="--method",
help="Registration method used for the registration.",
default="RegAladin",
)
input_parser.add_argument(
"--init-pca", "-init-pca",
action='store_true',
help="If given, PCA-based initializations will be refined using "
"RegAladin registrations."
)
input_parser.add_dir_input_mc()
input_parser.add_verbose(default=0)
input_parser.add_log_config(default=1)
args = input_parser.parse_args()
input_parser.print_arguments(args)
if args.log_config:
input_parser.log_config(os.path.abspath(__file__))
if not args.output.endswith(".txt"):
raise IOError(
"output filename '%s' invalid; "
"allowed transformation extensions are: '.txt'" % (
args.output))
if args.initial_transform is not None and args.init_pca:
raise IOError(
"Both --initial-transform and --init-pca cannot be activated. "
"Choose one.")
dir_output = os.path.dirname(args.output)
ph.create_directory(dir_output)
debug = False
# --------------------------------Read Data--------------------------------
ph.print_title("Read Data")
fixed = st.Stack.from_filename(
file_path=args.fixed,
file_path_mask=args.fixed_mask,
extract_slices=False)
moving = st.Stack.from_filename(
file_path=args.moving,
file_path_mask=args.moving_mask,
extract_slices=False)
path_to_tmp_output = os.path.join(
DIR_TMP,
ph.append_to_filename(os.path.basename(args.moving), "_warped"))
# ---------------------------- Initialization ----------------------------
if args.initial_transform is None and args.init_pca:
ph.print_title("Estimate (initial) transformation using PCA")
if args.moving_mask is None or args.fixed_mask is None:
ph.print_warning("Fixed and moving masks are strongly recommended")
transform_initializer = tinit.TransformInitializer(
fixed=fixed,
moving=moving,
similarity_measure="NMI",
refine_pca_initializations=True,
)
transform_initializer.run()
transform_init_sitk = transform_initializer.get_transform_sitk()
elif args.initial_transform is not None:
transform_init_sitk = sitkh.read_transform_sitk(args.initial_transform)
else:
transform_init_sitk = None
if transform_init_sitk is not None:
sitk.WriteTransform(transform_init_sitk, args.output)
# -------------------Register Reconstruction to Template-------------------
ph.print_title("Registration")
# If --init-pca given, RegAladin run already performed
if args.method == "RegAladin" and not args.init_pca:
path_to_transform_regaladin = os.path.join(
DIR_TMP, "transform_regaladin.txt")
# Convert SimpleITK to RegAladin transform
if transform_init_sitk is not None:
cmd = "simplereg_transform -sitk2nreg %s %s" % (
args.output, path_to_transform_regaladin)
ph.execute_command(cmd, verbose=False)
# Run NiftyReg
cmd_args = ["reg_aladin"]
cmd_args.append("-ref '%s'" % args.fixed)
cmd_args.append("-flo '%s'" % args.moving)
cmd_args.append("-res '%s'" % path_to_tmp_output)
if transform_init_sitk is not None:
cmd_args.append("-inaff '%s'" % path_to_transform_regaladin)
cmd_args.append("-aff '%s'" % path_to_transform_regaladin)
cmd_args.append("-rigOnly")
cmd_args.append("-ln 2") # seems to perform better for spina bifida
cmd_args.append("-voff")
if args.fixed_mask is not None:
cmd_args.append("-rmask '%s'" % args.fixed_mask)
# To avoid error "0 correspondences between blocks were found" that can
# occur for some cases. Also, disable moving mask, as this would be ignored
# anyway
cmd_args.append("-noSym")
# if args.moving_mask is not None:
# cmd_args.append("-fmask '%s'" % args.moving_mask)
ph.print_info("Run Registration (RegAladin) ... ", newline=False)
ph.execute_command(" ".join(cmd_args), verbose=debug)
print("done")
# Convert RegAladin to SimpleITK transform
cmd = "simplereg_transform -nreg2sitk '%s' '%s'" % (
path_to_transform_regaladin, args.output)
ph.execute_command(cmd, verbose=False)
elif args.method == "FLIRT":
path_to_transform_flirt = os.path.join(DIR_TMP, "transform_flirt.txt")
# Convert SimpleITK into FLIRT transform
if transform_init_sitk is not None:
cmd = "simplereg_transform -sitk2flirt '%s' '%s' '%s' '%s'" % (
args.output, args.fixed, args.moving, path_to_transform_flirt)
ph.execute_command(cmd, verbose=False)
# Define search angle ranges for FLIRT in all three dimensions
# search_angles = ["-searchr%s -%d %d" % (x, 180, 180)
# for x in ["x", "y", "z"]]
cmd_args = ["flirt"]
cmd_args.append("-in '%s'" % args.moving)
cmd_args.append("-ref '%s'" % args.fixed)
if transform_init_sitk is not None:
cmd_args.append("-init '%s'" % path_to_transform_flirt)
cmd_args.append("-omat '%s'" % path_to_transform_flirt)
cmd_args.append("-out '%s'" % path_to_tmp_output)
cmd_args.append("-dof 6")
# cmd_args.append((" ").join(search_angles))
if args.moving_mask is not None:
cmd_args.append("-inweight '%s'" % args.moving_mask)
if args.fixed_mask is not None:
cmd_args.append("-refweight '%s'" % args.fixed_mask)
ph.print_info("Run Registration (FLIRT) ... ", newline=False)
ph.execute_command(" ".join(cmd_args), verbose=debug)
print("done")
# Convert FLIRT to SimpleITK transform
cmd = "simplereg_transform -flirt2sitk '%s' '%s' '%s' '%s'" % (
path_to_transform_flirt, args.fixed, args.moving, args.output)
ph.execute_command(cmd, verbose=False)
ph.print_info("Registration transformation written to '%s'" % args.output)
if args.dir_input_mc is not None:
ph.print_title("Update Motion-Correction Transformations")
transform_sitk = sitkh.read_transform_sitk(
args.output, inverse=1)
if args.dir_input_mc.endswith("/"):
subdir_mc = args.dir_input_mc.split("/")[-2]
else:
subdir_mc = args.dir_input_mc.split("/")[-1]
dir_output_mc = os.path.join(dir_output, subdir_mc)
ph.create_directory(dir_output_mc, delete_files=True)
pattern = REGEX_FILENAMES + "[.]tfm"
p = re.compile(pattern)
trafos = [t for t in os.listdir(args.dir_input_mc) if p.match(t)]
for t in trafos:
path_to_input_transform = os.path.join(args.dir_input_mc, t)
path_to_output_transform = os.path.join(dir_output_mc, t)
t_sitk = sitkh.read_transform_sitk(path_to_input_transform)
t_sitk = sitkh.get_composite_sitk_affine_transform(
transform_sitk, t_sitk)
sitk.WriteTransform(t_sitk, path_to_output_transform)
ph.print_info("%d transformations written to '%s'" % (
len(trafos), dir_output_mc))
# Copy rejected_slices.json file
path_to_rejected_slices = os.path.join(
args.dir_input_mc, "rejected_slices.json")
if ph.file_exists(path_to_rejected_slices):
ph.copy_file(path_to_rejected_slices, dir_output_mc)
if args.verbose:
cmd_args = ["simplereg_resample"]
cmd_args.append("-f '%s'" % args.fixed)
cmd_args.append("-m '%s'" % args.moving)
cmd_args.append("-t '%s'" % args.output)
cmd_args.append("-o '%s'" % path_to_tmp_output)
ph.execute_command(" ".join(cmd_args))
ph.show_niftis([args.fixed, path_to_tmp_output])
elapsed_time_total = ph.stop_timing(time_start)
# Summary
ph.print_title("Summary")
exe_file_info = os.path.basename(os.path.abspath(__file__)).split(".")[0]
print("%s | Computational Time: %s" % (exe_file_info, elapsed_time_total))
return 0
if __name__ == '__main__':
main()
================================================
FILE: niftymic/application/rsfmri_estimate_motion.py
================================================
##
# \file rsfmri_estimate_motion.py
# \brief Estimate motion in resting-state fMRI volumes
#
# \author Michael Ebner (michael.ebner@kcl.ac.uk)
# \date July 2019
#
import os
import numpy as np
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
import niftymic.base.stack as st
import niftymic.base.data_reader as dr
import niftymic.base.data_writer as dw
import niftymic.registration.flirt as regflirt
import niftymic.registration.niftyreg as regniftyreg
import niftymic.registration.simple_itk_registration as regsitk
import niftymic.reconstruction.tikhonov_solver as tk
import niftymic.reconstruction.primal_dual_solver as pd
import niftymic.reconstruction.scattered_data_approximation as sda
import niftymic.utilities.data_preprocessing as dp
import niftymic.utilities.joint_image_mask_builder as imb
import niftymic.utilities.segmentation_propagation as segprop
import niftymic.utilities.volumetric_reconstruction_pipeline as pipeline
from niftymic.utilities.input_arparser import InputArgparser
from niftymic.definitions import V2V_METHOD_OPTIONS
def main():
time_start = ph.start_timing()
# Set print options for numpy
np.set_printoptions(precision=3)
# Read input
input_parser = InputArgparser(
description="Estimate motion in resting-state fMRI volumes",
)
input_parser.add_filename(required=True)
input_parser.add_filename_mask()
input_parser.add_dir_output(required=True)
input_parser.add_reference(required=False)
input_parser.add_reference_mask(required=False)
input_parser.add_alpha(default=0.03)
input_parser.add_alpha_first(default=0.05)
input_parser.add_data_loss(default="linear")
input_parser.add_dilation_radius(default=3)
input_parser.add_extra_frame_target(default=5)
input_parser.add_isotropic_resolution(default=1)
input_parser.add_iter_max(default=10)
input_parser.add_iter_max_first(default=5)
input_parser.add_iterations(default=10)
input_parser.add_log_config(default=1)
input_parser.add_minimizer(default="lsmr")
input_parser.add_argument(
"--prototyping", "-prototyping",
action='store_true',
help="If given, only a small subset of all time points is selected "
"for quicker test computations."
)
input_parser.add_reconstruction_type(default="TK1L2")
input_parser.add_rho(default=0.5)
input_parser.add_sigma(default=0.8)
input_parser.add_stack_recon_range(default=15)
input_parser.add_target_stack_index(default=0)
input_parser.add_two_step_cycles(default=3)
input_parser.add_use_masks_srr(default=0)
input_parser.add_verbose(default=0)
input_parser.add_v2v_method(default="RegAladin")
input_parser.add_outlier_rejection(default=1)
input_parser.add_threshold_first(default=0.5)
input_parser.add_threshold(default=0.8)
input_parser.add_argument(
"--sda", "-sda",
action='store_true',
help="If given, the volumetric reconstructions are performed using "
"Scattered Data Approximation (Vercauteren et al., 2006). "
"'alpha' is considered the final 'sigma' for the "
"iterative adjustment. "
"Recommended value is, e.g., --alpha 0.8"
)
args = input_parser.parse_args()
input_parser.print_arguments(args)
if args.v2v_method not in V2V_METHOD_OPTIONS:
raise ValueError("v2v-method must be in {%s}" % (
", ".join(V2V_METHOD_OPTIONS)))
if args.alpha_first < args.alpha and not args.sda:
raise ValueError("It must hold alpha-first >= alpha")
if args.threshold_first > args.threshold:
raise ValueError("It must hold threshold-first <= threshold")
# Write script execution call
if args.log_config:
input_parser.log_config(os.path.abspath(__file__))
# --------------------------------Read Data--------------------------------
ph.print_title("Read Data")
data_reader = dr.MultiComponentImageReader(
args.filename, args.filename_mask)
data_reader.read_data()
stacks = data_reader.get_data()
# ------------------------------DELETE LATER------------------------------
if args.prototyping:
stacks = stacks[0:2]
# ------------------------------DELETE LATER------------------------------
# ---------------------------Data Preprocessing---------------------------
ph.print_title("Data Preprocessing")
segmentation_propagator = segprop.SegmentationPropagation(
# registration_method=regniftyreg.RegAladin(use_verbose=args.verbose),
# registration_method=regsitk.SimpleItkRegistration(use_verbose=args.verbose),
dilation_radius=args.dilation_radius,
dilation_kernel="Ball",
)
data_preprocessing = dp.DataPreprocessing(
stacks=stacks,
segmentation_propagator=segmentation_propagator,
# Not ideal: Entire FOV more desirable but registration is worse if not
# cropped
use_cropping_to_mask=args.use_masks_srr,
target_stack_index=args.target_stack_index,
boundary_i=0,
boundary_j=0,
boundary_k=0,
unit="mm",
)
data_preprocessing.run()
time_data_preprocessing = data_preprocessing.get_computational_time()
# Get preprocessed stacks
stacks = data_preprocessing.get_preprocessed_stacks()
# Define volume-to-volume registration method
if args.v2v_method == "FLIRT":
registration_v2v = regflirt.FLIRT(
registration_type="Rigid",
use_fixed_mask=True,
use_moving_mask=True,
use_verbose=False,
)
else:
registration_v2v = regniftyreg.RegAladin(
registration_type="Rigid",
use_fixed_mask=True,
use_moving_mask=True,
# options="-ln 2",
use_verbose=False,
)
# Define slice-to-volume registration method
registration_s2v = regsitk.SimpleItkRegistration(
use_fixed_mask=True,
use_moving_mask=True,
use_verbose=False,
interpolator="Linear",
metric="Correlation",
# metric="MattesMutualInformation", # Might cause error messages
# like "Too many samples map outside moving image buffer."
# use_multiresolution_framework=True,
shrink_factors=[2, 1],
smoothing_sigmas=[1, 0],
initializer_type="SelfGEOMETRY",
optimizer="ConjugateGradientLineSearch",
optimizer_params={
"learningRate": 1,
"numberOfIterations": 100,
"lineSearchUpperLimit": 2,
},
# optimizer="RegularStepGradientDescent",
# optimizer_params={
# "minStep": 1e-6,
# "numberOfIterations": 200,
# "gradientMagnitudeTolerance": 1e-6,
# "learningRate": 1,
# },
scales_estimator="Jacobian",
)
if args.reference is None:
time_ref_estimate_start = ph.start_timing()
# Stacks used for outlier-robust SRR algorithm
i_min = args.target_stack_index
i_max = np.min([args.target_stack_index + args.stack_recon_range,
len(stacks)])
stacks_srr = [st.Stack.from_stack(s) for s in stacks[i_min: i_max]]
# ----------------------Volume-to-Volume Registration------------------
if args.two_step_cycles > 0:
v2vreg = pipeline.VolumeToVolumeRegistration(
stacks=stacks_srr,
reference=stacks_srr[0],
registration_method=registration_v2v,
verbose=args.verbose,
)
v2vreg.run()
stacks_srr = v2vreg.get_stacks()
time_registration = v2vreg.get_computational_time()
else:
time_registration = ph.get_zero_time()
# ---------------------------Create first volume-----------------------
time_tmp = ph.start_timing()
# Isotropic resampling to define HR target space
ph.print_title("Isotropic Resampling")
reference = stacks_srr[0].get_isotropically_resampled_stack(
resolution=args.isotropic_resolution,
extra_frame=args.extra_frame_target)
# Scattered Data Approximation to get first estimate of HR volume
ph.print_title("Scattered Data Approximation")
SDA = sda.ScatteredDataApproximation(
stacks=stacks_srr,
HR_volume=reference,
sigma=args.sigma,
use_masks=args.use_masks_srr,
)
SDA.run()
reference = SDA.get_reconstruction()
joint_image_mask_builder = imb.JointImageMaskBuilder(
stacks=stacks_srr,
target=reference,
dilation_radius=1,
)
joint_image_mask_builder.run()
reference = joint_image_mask_builder.get_stack()
reference.set_filename(SDA.get_setting_specific_filename())
# Crop to space defined by mask (plus extra margin)
reference = reference.get_cropped_stack_based_on_mask(
boundary_i=args.extra_frame_target,
boundary_j=args.extra_frame_target,
boundary_k=args.extra_frame_target,
unit="mm",
)
time_reconstruction = ph.stop_timing(time_tmp)
# ----------------Two-step Slice-to-Volume Registration SRR------------
if args.two_step_cycles > 0:
# Volumetric reconstruction set-up
if args.sda:
recon_method = sda.ScatteredDataApproximation(
stacks=stacks_srr,
HR_volume=reference,
sigma=args.sigma,
use_masks=args.use_masks_srr,
)
alpha_range = [args.sigma, args.alpha]
else:
recon_method = tk.TikhonovSolver(
stacks=stacks_srr,
reconstruction=reference,
reg_type="TK1",
minimizer="lsmr",
alpha=args.alpha_first,
iter_max=np.min([args.iter_max_first, args.iter_max]),
verbose=True,
use_masks=args.use_masks_srr,
)
alpha_range = [args.alpha_first, args.alpha]
# Define the regularization parameters for the individual
# reconstruction steps in the two-step cycles
alphas = np.linspace(
alpha_range[0], alpha_range[1], args.two_step_cycles)
# Define outlier rejection threshold after each S2V-reg step
thresholds = np.linspace(
args.threshold_first, args.threshold, args.two_step_cycles)
two_step_s2v_reg_recon = \
pipeline.TwoStepSliceToVolumeRegistrationReconstruction(
stacks=stacks_srr,
reference=reference,
registration_method=registration_s2v,
reconstruction_method=recon_method,
cycles=args.two_step_cycles,
alphas=alphas[0:args.two_step_cycles - 1],
verbose=args.verbose,
outlier_rejection=args.outlier_rejection,
thresholds=thresholds,
)
two_step_s2v_reg_recon.run()
reference_iterations = \
two_step_s2v_reg_recon.get_iterative_reconstructions()
time_registration += \
two_step_s2v_reg_recon.get_computational_time_registration()
time_reconstruction += \
two_step_s2v_reg_recon.get_computational_time_reconstruction()
if args.verbose:
sitkh.show_stacks(reference_iterations, segmentation=reference)
# # Write to output
# HR_volume_tmp.write(args.dir_output)
ph.print_title("Final Reference Reconstruction")
if args.sda:
recon_method = sda.ScatteredDataApproximation(
stacks_srr,
reference,
sigma=args.alpha,
use_masks=args.use_masks_srr,
)
else:
if args.reconstruction_type in ["TVL2", "HuberL2"]:
recon_method = pd.PrimalDualSolver(
stacks=stacks_srr,
reconstruction=reference,
reg_type="TV" if args.reconstruction_type == "TVL2" else "huber",
iterations=args.iterations,
use_masks=args.use_masks_srr,
)
else:
recon_method = tk.TikhonovSolver(
stacks=stacks_srr,
reconstruction=reference,
reg_type="TK1" if args.reconstruction_type == "TK1L2" else "TK0",
use_masks=args.use_masks_srr,
)
recon_method.set_alpha(args.alpha)
recon_method.set_iter_max(args.iter_max)
recon_method.set_verbose(True)
recon_method.run()
reference = recon_method.get_reconstruction()
time_reconstruction += recon_method.get_computational_time()
ph.print_subtitle("Final SDA Approximation Image Mask")
SDA = sda.ScatteredDataApproximation(
stacks_srr, reference, sigma=args.sigma, sda_mask=True)
SDA.run()
# Reference contains updated mask based on SDA
reference = SDA.get_reconstruction()
time_reconstruction += SDA.get_computational_time()
description = recon_method.get_setting_specific_filename()
reference.set_filename(description)
name = "SDA" if args.sda else "SRR"
path_to_reference = os.path.join(
args.dir_output, "%s_reference.nii.gz" % name)
dw.DataWriter.write_image(
image_sitk=reference.sitk,
path_to_file=path_to_reference,
description=description)
dw.DataWriter.write_mask(
reference.sitk_mask,
ph.append_to_filename(path_to_reference, "_mask"),
description=SDA.get_setting_specific_filename())
time_ref_estimate = ph.stop_timing(time_ref_estimate_start)
else:
reference = st.Stack.from_filename(args.reference, args.reference_mask)
time_ref_estimate = ph.get_zero_time()
ph.print_info("Reference image for V2V and S2V registrations provided")
# --------------------Volume-to-Volume Registrations-----------------
v2vreg = pipeline.VolumeToVolumeRegistration(
stacks=stacks,
reference=reference,
registration_method=registration_v2v,
verbose=False,
)
v2vreg.run()
time_v2v_reg = v2vreg.get_computational_time()
# --------------------Slice-to-Volume Registrations-----------------
s2vreg = pipeline.SliceToVolumeRegistration(
stacks=stacks,
reference=reference,
registration_method=registration_s2v,
verbose=False,
)
s2vreg.run()
time_s2v_reg = s2vreg.get_computational_time()
# ------------------Write Slice Motion Correction Results------------------
ph.print_title("Write Slice Motion Correction Results")
dir_output_mc = os.path.join(
args.dir_output,
"motion_correction",
)
ph.clear_directory(dir_output_mc)
for stack in stacks:
stack.write(
dir_output_mc,
write_stack=False,
write_mask=False,
write_slices=False,
write_transforms=True,
)
elapsed_time_total = ph.stop_timing(time_start)
# Summary
ph.print_title("Summary")
exe_file_info = os.path.basename(os.path.abspath(__file__)).split(".")[0]
print("%s | Computational Time for Data Preprocessing: %s" % (
exe_file_info, time_data_preprocessing))
print("%s | Computational Time for Reference (Estimate): %s" % (
exe_file_info, time_ref_estimate))
print("%s | Computational Time for V2V-Registration: %s" % (
exe_file_info, time_v2v_reg))
print("%s | Computational Time for S2V-Registration: %s" % (
exe_file_info, time_s2v_reg))
print("%s | Computational Time for Pipeline: %s" % (
exe_file_info, elapsed_time_total))
ph.print_line_separator()
return 0
if __name__ == '__main__':
main()
================================================
FILE: niftymic/application/rsfmri_reconstruct_volume_from_slices.py
================================================
##
# \file reconstruct_volume_from_slices_rsfmri.py
# \brief Script to reconstruct an isotropic, high-resolution volume from
# multiple motion-corrected (or static) stacks of low-resolution 2D
# slices.
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date July 2019
#
import os
import numpy as np
import SimpleITK as sitk
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
import niftymic.base.stack as st
import niftymic.base.data_reader as dr
import niftymic.base.data_writer as dw
import niftymic.reconstruction.admm_solver as admm
import niftymic.utilities.intensity_correction as ic
import niftymic.reconstruction.primal_dual_solver as pd
import niftymic.reconstruction.tikhonov_solver as tk
import niftymic.reconstruction.scattered_data_approximation as sda
import niftymic.utilities.binary_mask_from_mask_srr_estimator as bm
from niftymic.utilities.input_arparser import InputArgparser
import niftymic.utilities.volumetric_reconstruction_pipeline as pipeline
from niftymic.definitions import ALLOWED_EXTENSIONS
def main():
time_start = ph.start_timing()
# Set print options for numpy
np.set_printoptions(precision=3)
# Read input
input_parser = InputArgparser(
description="Volumetric MRI reconstruction framework to reconstruct "
"resting-state fMRI based on motion-corrected slice transformations "
"obtained using reconstruct_volume_rsfmri.",
)
input_parser.add_filename(required=True)
input_parser.add_filename_mask()
input_parser.add_output(required=True)
input_parser.add_dir_input_mc()
input_parser.add_argument(
"--volume", "-vol",
action='store_true',
help="If given, reconstructions for each time point are performed "
"based on volumetric stack position update only. "
"Otherwise, reconstructions are based after performed motion updates "
"for each individual slice for each time point."
)
input_parser.add_reconstruction_space(default=None)
input_parser.add_alpha(default=0.05)
input_parser.add_reconstruction_type(default="TK1L2")
input_parser.add_data_loss(default="linear")
input_parser.add_minimizer(default="lsmr")
input_parser.add_iter_max(default=20)
input_parser.add_iterations(default=10)
input_parser.add_argument(
"--prototyping", "-prototyping",
action='store_true',
help="If given, only a small subset of all time points is selected "
"for quicker test computations."
)
input_parser.add_option(
option_string="--reconstruction-spacing",
type=float,
nargs="+",
help="Specify spacing of reconstruction space in case a change is desired",
default=None)
input_parser.add_argument(
"--sda", "-sda",
action='store_true',
help="If given, the volumetric reconstructions are performed using "
"Scattered Data Approximation (Vercauteren et al., 2006). "
"'alpha' is considered the final 'sigma' for the "
"iterative adjustment. "
"Recommended value is, e.g., --alpha 0.8"
)
input_parser.add_option(
"--beta",
help="Regularization parameter beta to solve the Super-Resolution "
"Reconstruction problem with temporal regularization: "
"SRR = argmin_{x^t} ["
"sum_t sum_k ||y_k^t - A_k^t x^t||^2 "
"+ alpha * R(x) "
"+ beta * sum_t ||x^{t+1} - x^t||^2"
"].",
type=float,
default=0,
)
input_parser.add_use_masks_srr(default=1)
input_parser.add_log_config(default=1)
input_parser.add_verbose(default=0)
args = input_parser.parse_args()
input_parser.print_arguments(args)
# Write script execution call
if args.log_config:
input_parser.log_config(os.path.abspath(__file__))
# --------------------------------Read Data--------------------------------
ph.print_title("Read Data")
data_reader = dr.MultiComponentImageReader(
path_to_image=args.filename,
path_to_image_mask=args.filename_mask,
dir_motion_correction=args.dir_input_mc,
volume_motion_only=args.volume,
)
data_reader.read_data()
stacks = data_reader.get_data()
# Define reconstruction space for rsfmri
if args.reconstruction_space is None:
path_to_recon_space = args.filename
else:
path_to_recon_space = args.reconstruction_space
image_sitk = sitk.ReadImage(path_to_recon_space)
# Standard 3D image
# Multi-component 3D image
if len(image_sitk.GetSize()) == 4:
# Extract first component (3D image) of the 4D image
shape = list(image_sitk.GetSize())
shape[-1] = 0
index = [0] * 4
recon_space_sitk = sitk.Extract(image_sitk, shape, index)
elif len(image_sitk.GetSize()) == 3:
recon_space_sitk = image_sitk
else:
raise ValueError("Provide either a multi-component or a standard "
"3D image to define the reconstruction space")
reconstruction_space = st.Stack.from_sitk_image(
recon_space_sitk * 0,
slice_thickness=recon_space_sitk.GetSpacing()[-1],
filename=ph.strip_filename_extension(
os.path.basename(path_to_recon_space))[0],
)
if args.reconstruction_spacing is not None:
reconstruction_space = reconstruction_space.get_resampled_stack(
spacing=args.reconstruction_spacing)
# ------------------------------DELETE LATER------------------------------
if args.prototyping:
stacks = stacks[0:2]
# ------------------------------DELETE LATER------------------------------
# ----Define solver for rsfMRI reconstructions of individual timepoints----
if args.sda:
recon_method = sda.ScatteredDataApproximation(
stacks,
reconstruction_space,
sigma=args.alpha,
use_masks=args.use_masks_srr,
)
else:
if args.beta < 0:
if args.reconstruction_type in ["TVL2", "HuberL2"]:
recon_method = pd.PrimalDualSolver(
stacks=stacks,
reconstruction=reconstruction_space,
reg_type="TV" if args.reconstruction_type == "TVL2" else "huber",
iterations=args.iterations,
use_masks=args.use_masks_srr,
)
else:
recon_method = tk.TikhonovSolver(
stacks=stacks,
reconstruction=reconstruction_space,
reg_type="TK1" if args.reconstruction_type == "TK1L2" else "TK0",
use_masks=args.use_masks_srr,
)
recon_method.set_alpha(args.alpha)
recon_method.set_iter_max(args.iter_max)
recon_method.set_verbose(True)
# ------Update individual timepoints based on updated slice positio
multi_component_reconstruction = pipeline.MultiComponentReconstruction(
stacks=stacks,
reconstruction_method=recon_method,
suffix="_recon_v2v")
multi_component_reconstruction.run()
time_reconstruction = \
multi_component_reconstruction.get_computational_time()
stacks_recon = multi_component_reconstruction.get_reconstructions()
description = multi_component_reconstruction.\
get_reconstruction_method().get_setting_specific_filename()
else:
if not args.reconstruction_type in ["TK0L2", "TK1L2"]:
raise ValueError(
"Temporal Tikhonov regularization, i.e. beta>0, "
"only possible for TK0L2 and TK1L2 currently")
recon_method = tk.TemporalTikhonovSolver(
stacks=stacks,
reconstruction=reconstruction_space,
reg_type="TK1" if args.reconstruction_type == "TK1L2" else "TK0",
use_masks=args.use_masks_srr,
beta=args.beta,
alpha=args.alpha,
iter_max=args.iter_max,
verbose=True,
)
recon_method.run()
time_reconstruction = recon_method.get_computational_time()
stacks_recon = recon_method.get_reconstructions()
description = recon_method.get_setting_specific_filename()
# --------------------------------Write Data------------------------------
ph.print_title("Write Data")
data_writer = dw.MultiComponentImageWriter(
stacks_recon, args.output, description=description)
data_writer.write_data()
if args.verbose:
ph.show_nifti(args.output)
elapsed_time = ph.stop_timing(time_start)
ph.print_title("Summary")
exe_file_info = os.path.basename(os.path.abspath(__file__)).split(".")[0]
print("%s | Computational Time for Volumetric Reconstruction: %s" %
(exe_file_info, elapsed_time))
return 0
if __name__ == '__main__':
main()
================================================
FILE: niftymic/application/run_reconstruction_parameter_study.py
================================================
##
# \file run_reconstruction_parameter_study.py
# \brief Script to study reconstruction parameters and their impact on the
# volumetric reconstruction quality.
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date September 2017
#
import os
import numpy as np
import SimpleITK as sitk
import pysitk.python_helper as ph
import nsol.deconvolution_solver_parameter_study_interface as deconv_interface
import niftymic.base.data_reader as dr
import niftymic.base.stack as st
import niftymic.reconstruction.tikhonov_solver as tk
import niftymic.reconstruction.scattered_data_approximation as sda
from niftymic.utilities.input_arparser import InputArgparser
def main():
time_start = ph.start_timing()
# Set print options for numpy
np.set_printoptions(precision=3)
# Read input
input_parser = InputArgparser(
description="Script to study reconstruction parameters and their "
"impact on the volumetric reconstruction quality. "
"This script can only be used to sweep through one single parameter, "
"e.g. the regularization parameter 'alpha'. "
)
input_parser.add_filenames(required=True)
input_parser.add_filenames_masks()
input_parser.add_suffix_mask(default="_mask")
input_parser.add_dir_input_mc()
input_parser.add_dir_output(required=True)
input_parser.add_reconstruction_space()
input_parser.add_reference(
help="Path to reference NIfTI image file. If given the volumetric "
"reconstructed is performed in this physical space. "
"Either a reconstruction space or a reference must be provided",
required=False)
input_parser.add_reference_mask(default=None)
input_parser.add_study_name()
input_parser.add_reconstruction_type(default="TK1L2")
input_parser.add_measures(
default=["PSNR", "MAE", "RMSE", "SSIM", "NCC", "NMI"])
input_parser.add_tv_solver(default="PD")
input_parser.add_iterations(default=50)
input_parser.add_rho(default=0.1)
input_parser.add_iter_max(default=10)
input_parser.add_minimizer(default="lsmr")
input_parser.add_log_config(default=1)
input_parser.add_use_masks_srr(default=0)
input_parser.add_verbose(default=1)
input_parser.add_slice_thicknesses(default=None)
input_parser.add_argument(
"--append", "-append",
action='store_true',
help="If given, results are appended to previously executed parameter "
"study with identical parameters and study name store in the output "
"directory."
)
# Range for parameter sweeps
input_parser.add_alphas(default=list(np.linspace(0.01, 0.5, 5)))
input_parser.add_data_losses(
default=["linear"]
# default=["linear", "arctan"]
)
input_parser.add_data_loss_scales(
default=[1]
# default=[0.1, 0.5, 1.5]
)
args = input_parser.parse_args()
input_parser.print_arguments(args)
if args.reference is None and args.reconstruction_space is None:
raise IOError("Either reference (--reference) or reconstruction space "
"(--reconstruction-space) must be provided.")
if args.log_config:
input_parser.log_config(os.path.abspath(__file__))
# --------------------------------Read Data--------------------------------
ph.print_title("Read Data")
data_reader = dr.MultipleImagesReader(
file_paths=args.filenames,
file_paths_masks=args.filenames_masks,
suffix_mask=args.suffix_mask,
dir_motion_correction=args.dir_input_mc,
stacks_slice_thicknesses=args.slice_thicknesses,
)
data_reader.read_data()
stacks = data_reader.get_data()
ph.print_info("%d input stacks read for further processing" % len(stacks))
ph.print_title("Compute Initial Value")
if args.reference is not None:
reference = st.Stack.from_filename(
file_path=args.reference,
file_path_mask=args.reference_mask,
extract_slices=False)
x_ref = sitk.GetArrayFromImage(reference.sitk).flatten()
x_ref_mask = sitk.GetArrayFromImage(reference.sitk_mask).flatten()
else:
reference = st.Stack.from_filename(
file_path=args.reconstruction_space,
extract_slices=False)
x_ref = None
x_ref_mask = None
SRR0 = sda.ScatteredDataApproximation(stacks, reference, sigma=0.8)
SRR0.run()
reconstruction_space = SRR0.get_reconstruction()
if args.use_masks_srr:
# Add mask based on SDA
SRR0 = sda.ScatteredDataApproximation(
stacks, reconstruction_space, sigma=1, sda_mask=True)
SRR0.run()
reconstruction_space = SRR0.get_reconstruction()
reconstruction_space = \
reconstruction_space.get_stack_multiplied_with_mask()
# ----------------------------Set Up Parameters----------------------------
parameters = {}
parameters["alpha"] = args.alphas
if len(args.data_losses) > 1:
parameters["data_loss"] = args.data_losses
if len(args.data_loss_scales) > 1:
parameters["data_loss_scale"] = args.data_loss_scales
# --------------------------Set Up Parameter Study-------------------------
ph.print_title("Run Parameter Study")
if args.study_name is None:
name = args.reconstruction_type
else:
name = args.study_name
reconstruction_info = {
"shape": reconstruction_space.sitk.GetSize()[::-1],
"origin": reconstruction_space.sitk.GetOrigin(),
"spacing": reconstruction_space.sitk.GetSpacing(),
"direction": reconstruction_space.sitk.GetDirection(),
}
# Create Tikhonov solver from which all information can be extracted
# (also for other reconstruction types)
tmp = tk.TikhonovSolver(
stacks=stacks,
reconstruction=reconstruction_space,
alpha=args.alphas[0],
iter_max=args.iter_max,
data_loss=args.data_losses[0],
data_loss_scale=args.data_loss_scales[0],
reg_type="TK1",
minimizer=args.minimizer,
verbose=args.verbose,
use_masks=args.use_masks_srr,
)
solver = tmp.get_solver()
parameter_study_interface = \
deconv_interface.DeconvolutionParameterStudyInterface(
A=solver.get_A(),
A_adj=solver.get_A_adj(),
D=solver.get_B(),
D_adj=solver.get_B_adj(),
b=solver.get_b(),
x0=solver.get_x0(),
alpha=solver.get_alpha(),
x_scale=solver.get_x_scale(),
data_loss=solver.get_data_loss(),
data_loss_scale=solver.get_data_loss_scale(),
iter_max=solver.get_iter_max(),
minimizer=solver.get_minimizer(),
iterations=args.iterations,
measures=args.measures,
dimension=3,
L2=16. / reconstruction_space.sitk.GetSpacing()[0]**2,
reconstruction_type=args.reconstruction_type,
rho=args.rho,
dir_output=args.dir_output,
parameters=parameters,
name=name,
reconstruction_info=reconstruction_info,
x_ref=x_ref,
x_ref_mask=x_ref_mask,
tv_solver=args.tv_solver,
verbose=args.verbose,
append=args.append,
)
parameter_study_interface.set_up_parameter_study()
parameter_study = parameter_study_interface.get_parameter_study()
# Run parameter study
parameter_study.run()
print("\nComputational time for Deconvolution Parameter Study %s: %s" %
(name, parameter_study.get_computational_time()))
return 0
if __name__ == '__main__':
main()
================================================
FILE: niftymic/application/run_reconstruction_pipeline.py
================================================
##
# \file run_reconstruction_pipeline.py
# \brief Script to execute entire reconstruction pipeline
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date October 2017
#
import os
import re
import numpy as np
import pysitk.python_helper as ph
import niftymic.validation.simulate_stacks_from_reconstruction as \
simulate_stacks_from_reconstruction
import niftymic.validation.evaluate_simulated_stack_similarity as \
evaluate_simulated_stack_similarity
import niftymic.validation.show_evaluated_simulated_stack_similarity as \
show_evaluated_simulated_stack_similarity
import niftymic.application.show_slice_coverage as show_slice_coverage
import niftymic.validation.export_side_by_side_simulated_vs_original_slice_comparison as \
export_side_by_side_simulated_vs_original_slice_comparison
import niftymic.utilities.target_stack_estimator as ts_estimator
from niftymic.utilities.input_arparser import InputArgparser
import niftymic.utilities.template_stack_estimator as tse
from niftymic.definitions import DIR_TEMPLATES
def main():
time_start_total = ph.start_timing()
np.set_printoptions(precision=3)
input_parser = InputArgparser(
description="Run reconstruction pipeline including "
"(i) bias field correction, "
"(ii) volumetric reconstruction in subject space, "
"(iii) volumetric reconstruction in template space, "
"and (iv) some diagnostics to assess the obtained reconstruction.",
)
input_parser.add_filenames(required=True)
input_parser.add_filenames_masks(required=True)
input_parser.add_target_stack(required=False)
input_parser.add_suffix_mask(default="")
input_parser.add_dir_output(required=True)
input_parser.add_alpha(default=0.01)
input_parser.add_verbose(default=0)
input_parser.add_prefix_output(default="srr_")
input_parser.add_search_angle(default=180)
input_parser.add_multiresolution(default=0)
input_parser.add_log_config(default=1)
input_parser.add_isotropic_resolution()
input_parser.add_reference()
input_parser.add_reference_mask()
input_parser.add_bias_field_correction(default=1)
input_parser.add_intensity_correction(default=1)
input_parser.add_iter_max(default=10)
input_parser.add_two_step_cycles(default=3)
input_parser.add_slice_thicknesses(default=None)
input_parser.add_option(
option_string="--template",
type=str,
required=False,
help="Template image used for template space alignment and to define "
"the reconstruction space. "
"If not given, it is automatically estimated using the fetal brain "
"atlas",
)
input_parser.add_option(
option_string="--template-mask",
type=str,
required=False,
help="Template image mask. "
"Must be given in case template is specified.",
)
input_parser.add_option(
option_string="--run-bias-field-correction",
type=int,
help="Turn on/off bias field correction. "
"If off, it is assumed that this step was already performed "
"if --bias-field-correction is active.",
default=1)
input_parser.add_option(
option_string="--run-recon-subject-space",
type=int,
help="Turn on/off reconstruction in subject space. "
"If off, it is assumed that this step was already performed.",
default=1)
input_parser.add_option(
option_string="--run-recon-template-space",
type=int,
help="Turn on/off reconstruction in template space. "
"If off, it is assumed that this step was already performed.",
default=1)
input_parser.add_option(
option_string="--run-diagnostics",
type=int,
help="Turn on/off diagnostics of the obtained volumetric "
"reconstruction. ",
default=0)
input_parser.add_option(
option_string="--initial-transform",
type=str,
help="Set initial transform to be used for register_image.",
default=None)
input_parser.add_outlier_rejection(default=1)
input_parser.add_threshold_first(default=0.5)
input_parser.add_threshold(default=0.8)
input_parser.add_argument(
"--sda", "-sda",
action='store_true',
help="If given, the volume is reconstructed using "
"Scattered Data Approximation (Vercauteren et al., 2006). "
"--alpha is considered the value for the standard deviation then. "
"Recommended value is, e.g., --alpha 0.8"
)
input_parser.add_argument(
"--v2v-robust", "-v2v-robust",
action='store_true',
help="If given, a more robust volume-to-volume registration step is "
"performed, i.e. four rigid registrations are performed using four "
"rigid transform initializations based on "
"principal component alignment of associated masks."
)
input_parser.add_interleave(default=3)
input_parser.add_argument(
"--s2v-hierarchical", "-s2v-hierarchical",
action='store_true',
help="If given, a hierarchical approach for the first slice-to-volume "
"registration cycle is used, i.e. sub-packages defined by the "
"specified interleave (--interleave) are registered until each "
"slice is registered independently."
)
input_parser.add_option(
option_string="--automatic-target-stack",
type=int,
help="If true, and no specific target stack is provided, "
"a target stack is automatically estimated. "
"A motion score similar to the one presented in Kainz et al. (2015). "
"is used to estimate the least motion-affected stack as initial "
"reference/target stack.",
default=1,
)
args = input_parser.parse_args()
input_parser.print_arguments(args)
if args.template is not None:
if args.template_mask is None:
raise ValueError(
"If template image is given, also its mask needs to be "
"provided")
if args.log_config:
input_parser.log_config(os.path.abspath(__file__))
dir_output_preprocessing = os.path.join(
args.dir_output, "preprocessing_n4itk")
dir_output_recon_subject_space = os.path.join(
args.dir_output, "recon_subject_space")
dir_output_recon_template_space = os.path.join(
args.dir_output, "recon_template_space")
dir_output_diagnostics = os.path.join(
args.dir_output, "diagnostics")
srr_subject = os.path.join(
dir_output_recon_subject_space,
"%ssubject.nii.gz" % args.prefix_output)
srr_subject_mask = ph.append_to_filename(srr_subject, "_mask")
srr_template = os.path.join(
dir_output_recon_template_space,
"%stemplate.nii.gz" % args.prefix_output)
srr_template_mask = ph.append_to_filename(srr_template, "_mask")
trafo_template = os.path.join(
dir_output_recon_template_space,
"%stemplate_transform_sitk.txt" % args.prefix_output)
srr_slice_coverage = os.path.join(
dir_output_diagnostics,
"%stemplate_slicecoverage.nii.gz" % args.prefix_output)
if args.bias_field_correction and args.run_bias_field_correction:
time_start = ph.start_timing()
for i, f in enumerate(args.filenames):
output = os.path.join(
dir_output_preprocessing, os.path.basename(f))
cmd_args = []
cmd_args.append("--filename '%s'" % f)
cmd_args.append("--filename-mask '%s'" % args.filenames_masks[i])
cmd_args.append("--output '%s'" % output)
# cmd_args.append("--verbose %d" % args.verbose)
cmd_args.append("--log-config %d" % args.log_config)
cmd = "niftymic_correct_bias_field %s" % (" ").join(cmd_args)
exit_code = ph.execute_command(cmd)
if exit_code != 0:
raise RuntimeError("Bias field correction failed")
elapsed_time_bias = ph.stop_timing(time_start)
filenames = [os.path.join(dir_output_preprocessing, os.path.basename(f))
for f in args.filenames]
elif args.bias_field_correction and not args.run_bias_field_correction:
elapsed_time_bias = ph.get_zero_time()
filenames = [os.path.join(dir_output_preprocessing, os.path.basename(f))
for f in args.filenames]
else:
elapsed_time_bias = ph.get_zero_time()
filenames = args.filenames
# Specify target stack for intensity correction and reconstruction space
elapsed_time_target_stack = ph.get_zero_time()
if args.target_stack is None:
if args.automatic_target_stack:
ph.print_info(
"Searching for suitable target stack ... ", newline=False)
target_stack_estimator = \
ts_estimator.TargetStackEstimator.from_motion_score(
file_paths=filenames,
file_paths_masks=args.filenames_masks,
)
print("done")
elapsed_time_target_stack = \
target_stack_estimator.get_computational_time()
ph.print_info(
"Computational time for target stack selection: %s" % (elapsed_time_target_stack))
target_stack_index = \
target_stack_estimator.get_target_stack_index()
target_stack = filenames[target_stack_index]
ph.print_info("Chosen target stack: %s" % target_stack)
else:
target_stack = filenames[0]
else:
try:
target_stack_index = args.filenames.index(args.target_stack)
except ValueError as e:
raise ValueError(
"--target-stack must correspond to an image as provided by "
"--filenames")
target_stack = filenames[target_stack_index]
# Add single quotes around individual filenames to account for whitespaces
filenames = ["'" + f + "'" for f in filenames]
filenames_masks = ["'" + f + "'" for f in args.filenames_masks]
if args.run_recon_subject_space:
time_start = ph.start_timing()
cmd_args = ["niftymic_reconstruct_volume"]
cmd_args.append("--filenames %s" % (" ").join(filenames))
cmd_args.append("--filenames-masks %s" % (" ").join(filenames_masks))
cmd_args.append("--multiresolution %d" % args.multiresolution)
cmd_args.append("--target-stack '%s'" % target_stack)
cmd_args.append("--output '%s'" % srr_subject)
cmd_args.append("--suffix-mask '%s'" % args.suffix_mask)
cmd_args.append("--intensity-correction %d" %
args.intensity_correction)
cmd_args.append("--alpha %s" % args.alpha)
cmd_args.append("--iter-max %d" % args.iter_max)
cmd_args.append("--two-step-cycles %d" % args.two_step_cycles)
cmd_args.append("--outlier-rejection %d" % args.outlier_rejection)
cmd_args.append("--threshold-first %f" % args.threshold_first)
cmd_args.append("--threshold %f" % args.threshold)
cmd_args.append("--verbose %d" % args.verbose)
cmd_args.append("--log-config %d" % args.log_config)
if args.isotropic_resolution is not None:
isotropic_resolution = args.isotropic_resolution
else:
# Gholipour et al. (2015) atlas grid spacing (0.7999989986419678)
# for comparability
isotropic_resolution = 0.8
cmd_args.append("--isotropic-resolution %f" % isotropic_resolution)
if args.slice_thicknesses is not None:
cmd_args.append("--slice-thicknesses %s" %
" ".join(map(str, args.slice_thicknesses)))
if args.reference is not None:
cmd_args.append("--reference '%s'" % args.reference)
if args.reference_mask is not None:
cmd_args.append("--reference-mask '%s'" % args.reference_mask)
if args.sda:
cmd_args.append("--sda")
if args.v2v_robust:
cmd_args.append("--v2v-robust")
if args.s2v_hierarchical:
cmd_args.append("--s2v-hierarchical")
cmd = (" ").join(cmd_args)
exit_code = ph.execute_command(cmd)
if exit_code != 0:
raise RuntimeError("Reconstruction in subject space failed")
elapsed_time_recon_subject_space = ph.stop_timing(time_start)
# Compute SRR mask in subject space
# (Approximated using SDA within reconstruct_volume)
if 0:
dir_motion_correction = os.path.join(
dir_output_recon_subject_space, "motion_correction")
cmd_args = ["niftymic_reconstruct_volume_from_slices"]
cmd_args.append("--filenames %s" % " ".join(filenames_masks))
cmd_args.append("--dir-input-mc '%s'" % dir_motion_correction)
cmd_args.append("--output '%s'" % srr_subject_mask)
cmd_args.append("--reconstruction-space '%s'" % srr_subject)
cmd_args.append("--suffix-mask '%s'" % args.suffix_mask)
cmd_args.append("--mask")
cmd_args.append("--log-config %d" % args.log_config)
if args.slice_thicknesses is not None:
cmd_args.append("--slice-thicknesses %s" %
" ".join(map(str, args.slice_thicknesses)))
if args.sda:
cmd_args.append("--sda")
cmd_args.append("--alpha 1")
else:
cmd_args.append("--alpha 0.1")
cmd_args.append("--iter-max 5")
cmd = (" ").join(cmd_args)
ph.execute_command(cmd)
else:
elapsed_time_recon_subject_space = ph.get_zero_time()
if args.run_recon_template_space:
time_start = ph.start_timing()
if args.template is not None:
template = args.template
template_mask = args.template_mask
else:
template_stack_estimator = \
tse.TemplateStackEstimator.from_mask(srr_subject_mask)
gestational_age = template_stack_estimator.get_estimated_gw()
ph.print_info("Estimated gestational age: %d" % gestational_age)
template = os.path.join(
DIR_TEMPLATES, "STA%d.nii.gz" % gestational_age)
template_mask = os.path.join(
DIR_TEMPLATES, "STA%d_mask.nii.gz" % gestational_age)
# Register SRR to template space
cmd_args = ["niftymic_register_image"]
cmd_args.append("--fixed '%s'" % template)
cmd_args.append("--moving '%s'" % srr_subject)
cmd_args.append("--fixed-mask '%s'" % template_mask)
cmd_args.append("--moving-mask '%s'" % srr_subject_mask)
cmd_args.append("--dir-input-mc '%s'" % os.path.join(
dir_output_recon_subject_space, "motion_correction"))
cmd_args.append("--output '%s'" % trafo_template)
cmd_args.append("--verbose %s" % args.verbose)
cmd_args.append("--log-config %d" % args.log_config)
if args.initial_transform is None:
cmd_args.append("--init-pca")
else:
cmd_args.append(
"--initial-transform '%s'" % args.initial_transform)
cmd = (" ").join(cmd_args)
exit_code = ph.execute_command(cmd)
if exit_code != 0:
raise RuntimeError("Registration to template space failed")
elapsed_time_register_image = ph.stop_timing(time_start)
time_start = ph.start_timing()
# Compute SRR in template space
dir_input_mc = os.path.join(
dir_output_recon_template_space, "motion_correction")
cmd_args = ["niftymic_reconstruct_volume_from_slices"]
cmd_args.append("--filenames %s" % (" ").join(filenames))
cmd_args.append("--filenames-masks %s" % (" ").join(filenames_masks))
cmd_args.append("--dir-input-mc '%s'" % dir_input_mc)
cmd_args.append("--output '%s'" % srr_template)
cmd_args.append("--reconstruction-space '%s'" % template)
cmd_args.append("--target-stack '%s'" % target_stack)
cmd_args.append("--iter-max %d" % args.iter_max)
cmd_args.append("--alpha %s" % args.alpha)
cmd_args.append("--suffix-mask '%s'" % args.suffix_mask)
cmd_args.append("--log-config %d" % args.log_config)
if args.isotropic_resolution is not None:
cmd_args.append("--isotropic-resolution %f" %
args.isotropic_resolution)
if args.slice_thicknesses is not None:
cmd_args.append("--slice-thicknesses %s" %
" ".join(map(str, args.slice_thicknesses)))
if args.sda:
cmd_args.append("--sda")
cmd = (" ").join(cmd_args)
exit_code = ph.execute_command(cmd)
if exit_code != 0:
raise RuntimeError("Reconstruction in template space failed")
elapsed_time_recon_template_space = ph.stop_timing(time_start)
# Compute mask in template space
if 1:
time_start = ph.start_timing()
dir_motion_correction = os.path.join(
dir_output_recon_template_space, "motion_correction")
cmd_args = ["niftymic_reconstruct_volume_from_slices"]
cmd_args.append("--filenames %s" % " ".join(filenames_masks))
cmd_args.append("--dir-input-mc '%s'" % dir_motion_correction)
cmd_args.append("--output '%s'" % srr_template_mask)
cmd_args.append("--reconstruction-space '%s'" % srr_template)
cmd_args.append("--suffix-mask '%s'" % args.suffix_mask)
cmd_args.append("--log-config %d" % args.log_config)
cmd_args.append("--mask")
if args.isotropic_resolution is not None:
cmd_args.append("--isotropic-resolution %f" %
args.isotropic_resolution)
if args.slice_thicknesses is not None:
cmd_args.append("--slice-thicknesses %s" %
" ".join(map(str, args.slice_thicknesses)))
# SRR approach
# cmd_args.append("--alpha 0.1")
# cmd_args.append("--iter-max 5")
# SDA much faster than SRR and visually barely different for mask
cmd_args.append("--sda")
cmd_args.append("--alpha 1")
cmd = (" ").join(cmd_args)
ph.execute_command(cmd)
elapsed_time_recon_template_space_mask = ph.stop_timing(
time_start)
if args.verbose:
ph.show_nifti(srr_template, segmentation=srr_template_mask)
# Copy SRR to output directory
if 0:
output = "%sSRR_Stacks%d.nii.gz" % (
args.prefix_output, len(args.filenames))
path_to_output = os.path.join(args.dir_output, output)
cmd = "cp -p '%s' '%s'" % (srr_template, path_to_output)
exit_code = ph.execute_command(cmd)
if exit_code != 0:
raise RuntimeError("Copy of SRR to output directory failed")
# Multiply template mask with reconstruction
if 0:
cmd_args = ["niftymic_multiply"]
fnames = [
srr_template,
srr_template_mask,
]
output_masked = "Masked_%s" % output
path_to_output_masked = os.path.join(
args.dir_output, output_masked)
cmd_args.append("--filenames %s" % " ".join(fnames))
cmd_args.append("--output '%s'" % path_to_output_masked)
cmd = (" ").join(cmd_args)
exit_code = ph.execute_command(cmd)
if exit_code != 0:
raise RuntimeError("SRR brain masking failed")
else:
elapsed_time_register_image = ph.get_zero_time()
elapsed_time_recon_template_space = ph.get_zero_time()
elapsed_time_recon_template_space_mask = ph.get_zero_time()
if args.run_diagnostics:
time_start = ph.start_timing()
dir_input_mc = os.path.join(
dir_output_recon_template_space, "motion_correction")
dir_output_orig_vs_proj = os.path.join(
dir_output_diagnostics, "original_vs_projected")
dir_output_selfsimilarity = os.path.join(
dir_output_diagnostics, "selfsimilarity")
dir_output_orig_vs_proj_pdf = os.path.join(
dir_output_orig_vs_proj, "pdf")
# Show slice coverage over reconstruction space
exe = os.path.abspath(show_slice_coverage.__file__)
cmd_args = ["python %s" % exe]
cmd_args.append("--filenames %s" % (" ").join(filenames))
cmd_args.append("--dir-input-mc '%s'" % dir_input_mc)
cmd_args.append("--reconstruction-space '%s'" % srr_template)
cmd_args.append("--output '%s'" % srr_slice_coverage)
cmd = (" ").join(cmd_args)
exit_code = ph.execute_command(cmd)
if exit_code != 0:
raise RuntimeError("Slice coverage visualization failed")
# Get simulated/projected slices
exe = os.path.abspath(simulate_stacks_from_reconstruction.__file__)
cmd_args = ["python %s" % exe]
cmd_args.append("--filenames %s" % (" ").join(filenames))
if args.filenames_masks is not None:
cmd_args.append("--filenames-masks %s" %
(" ").join(filenames_masks))
cmd_args.append("--dir-input-mc '%s'" % dir_input_mc)
cmd_args.append("--dir-output '%s'" % dir_output_orig_vs_proj)
cmd_args.append("--reconstruction '%s'" % srr_template)
cmd_args.append("--copy-data 1")
if args.slice_thicknesses is not None:
cmd_args.append("--slice-thicknesses %s" %
" ".join(map(str, args.slice_thicknesses)))
# cmd_args.append("--verbose %s" % args.verbose)
cmd = (" ").join(cmd_args)
exit_code = ph.execute_command(cmd)
if exit_code != 0:
raise RuntimeError("SRR slice projections failed")
filenames_simulated = [
"'%s" % os.path.join(dir_output_orig_vs_proj, os.path.basename(f))
for f in filenames]
# Evaluate slice similarities to ground truth
exe = os.path.abspath(evaluate_simulated_stack_similarity.__file__)
cmd_args = ["python %s" % exe]
cmd_args.append("--filenames %s" % (" ").join(filenames_simulated))
if args.filenames_masks is not None:
cmd_args.append("--filenames-masks %s" %
(" ").join(filenames_masks))
cmd_args.append("--measures NCC SSIM")
cmd_args.append("--dir-output '%s'" % dir_output_selfsimilarity)
cmd = (" ").join(cmd_args)
exit_code = ph.execute_command(cmd)
if exit_code != 0:
raise RuntimeError("Evaluation of stack similarities failed")
# Generate figures showing the quantitative comparison
exe = os.path.abspath(
show_evaluated_simulated_stack_similarity.__file__)
cmd_args = ["python %s" % exe]
cmd_args.append("--dir-input '%s'" % dir_output_selfsimilarity)
cmd_args.append("--dir-output '%s'" % dir_output_selfsimilarity)
cmd = (" ").join(cmd_args)
exit_code = ph.execute_command(cmd)
if exit_code != 0:
ph.print_warning("Visualization of stack similarities failed")
# Generate pdfs showing all the side-by-side comparisons
if 0:
exe = os.path.abspath(
export_side_by_side_simulated_vs_original_slice_comparison.__file__)
cmd_args = ["python %s" % exe]
cmd_args.append("--filenames %s" % (" ").join(filenames_simulated))
cmd_args.append("--dir-output '%s'" % dir_output_orig_vs_proj_pdf)
cmd = "python %s %s" % (exe, (" ").join(cmd_args))
cmd = (" ").join(cmd_args)
exit_code = ph.execute_command(cmd)
if exit_code != 0:
raise RuntimeError("Generation of PDF overview failed")
elapsed_time_diagnostics = ph.stop_timing(time_start)
ph.print_title("Summary")
exe_file_info = os.path.basename(os.path.abspath(__file__)).split(".")[0]
print("%s | Computational Time for Bias Field Corrections: %s" % (
exe_file_info, elapsed_time_bias))
print("%s | Computational Time for Automatic Target Stack Selection: %s" %
(exe_file_info, elapsed_time_target_stack))
print("%s | Computational Time for Subject Space Reconstruction: %s" % (
exe_file_info, elapsed_time_recon_subject_space))
print("%s | Computational Time for Template Space Alignment: %s" % (
exe_file_info, elapsed_time_register_image))
print("%s | Computational Time for Template Space Reconstruction: %s" % (
exe_file_info, elapsed_time_recon_template_space))
print("%s | Computational Time for Template Space Reconstruction (Mask): %s" % (
exe_file_info, elapsed_time_recon_template_space_mask))
if args.run_diagnostics:
print("%s | Computational Time for Diagnostics: %s" % (
exe_file_info, elapsed_time_diagnostics))
print("%s | Computational Time for Pipeline: %s" % (
exe_file_info, ph.stop_timing(time_start_total)))
return 0
if __name__ == '__main__':
main()
================================================
FILE: niftymic/application/segment_fetal_brains.py
================================================
##
# \file correct_bias_field.py
# \brief Script to apply automated fetal brain mask segmentation using monaifbs/fetal_brain_seg.py
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date October 2017
#
# Import libraries
import numpy as np
import os
import niftymic.base.stack as st
import niftymic.base.data_writer as dw
import niftymic.utilities.n4_bias_field_correction as n4itk
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
from niftymic.utilities.input_arparser import InputArgparser
from niftymic.definitions import ALLOWED_EXTENSIONS
def main():
time_start = ph.start_timing()
np.set_printoptions(precision=3)
input_parser = InputArgparser(
description="Perform automatic brain masking using "
"fetal_brain_seg, part of the MONAIfbs package "
"(https://github.com/gift-surg/MONAIfbs). ",
)
input_parser.add_filenames(required=True)
input_parser.add_filenames_masks(required=False)
input_parser.add_dir_output(required=False)
input_parser.add_verbose(default=0)
input_parser.add_log_config(default=0)
input_parser.add_option(
option_string="--neuroimage-legacy-seg",
type=int,
required=False,
default=0,
help="If set to 1, use the legacy method for fetal brain segmentation "
"i.e. the two-step approach proposed in Ebner, Wang et al "
"NeuroImage (2020)"
)
args = input_parser.parse_args()
input_parser.print_arguments(args)
if args.neuroimage_legacy_seg:
try:
DIR_FETAL_BRAIN_SEG = os.environ["FETAL_BRAIN_SEG"]
except KeyError as e:
raise RuntimeError(
"Environment variable FETAL_BRAIN_SEG is not specified. "
"Specify the root directory of fetal_brain_seg "
"(https://github.com/gift-surg/fetal_brain_seg) "
"using "
"'export FETAL_BRAIN_SEG=path_to_fetal_brain_seg_dir' "
"(in bashrc).")
else:
try:
import monaifbs
DIR_FETAL_BRAIN_SEG = os.path.dirname(monaifbs.__file__)
except ImportError as e:
raise RuntimeError(
"monaifbs not correctly installed. "
"Please check its installation running "
"pip install -e MONAIfbs/ "
)
print("Using executable from {}".format(DIR_FETAL_BRAIN_SEG))
if args.filenames_masks is None and args.dir_output is None:
raise IOError("Either --filenames-masks or --dir-output must be set")
if args.dir_output is not None:
args.filenames_masks = [
os.path.join(args.dir_output, os.path.basename(f))
for f in args.filenames
]
if len(args.filenames) != len(args.filenames_masks):
raise IOError("Number of filenames and filenames-masks must match")
if args.log_config:
input_parser.log_config(os.path.abspath(__file__))
cd_fetal_brain_seg = "cd %s" % DIR_FETAL_BRAIN_SEG
for f, m in zip(args.filenames, args.filenames_masks):
if not ph.file_exists(f):
raise IOError("File '%s' does not exist" % f)
# use absolute path for input image
f = os.path.abspath(f)
# use absolute path for output image
dir_output = os.path.dirname(m)
if not os.path.isabs(dir_output):
dir_output = os.path.realpath(
os.path.join(os.getcwd(), dir_output))
m = os.path.join(dir_output, os.path.basename(m))
ph.create_directory(dir_output)
# Change to root directory of fetal_brain_seg
cmds = [cd_fetal_brain_seg]
# Run masking independently (Takes longer but ensures that it does
# not terminate because of provided 'non-brain images')
cmd_args = ["python fetal_brain_seg.py"]
cmd_args.append("--input_names '%s'" % f)
cmd_args.append("--segment_output_names '%s'" % m)
cmds.append(" ".join(cmd_args))
# Execute both steps
cmd = " && ".join(cmds)
flag = ph.execute_command(cmd)
if flag != 0:
ph.print_warning(
"Error using fetal_brain_seg. \n"
"Execute '%s' for further investigation" %
cmd)
ph.print_info("Fetal brain segmentation written to '%s'" % m)
if args.verbose:
ph.show_nifti(f, segmentation=m)
elapsed_time_total = ph.stop_timing(time_start)
ph.print_title("Summary")
exe_file_info = os.path.basename(os.path.abspath(__file__)).split(".")[0]
print("%s | Computational Time: %s" % (exe_file_info, elapsed_time_total))
return 0
if __name__ == '__main__':
main()
================================================
FILE: niftymic/application/show_slice_coverage.py
================================================
##
# \file show_slice_coverage.py
# \brief Script to show slice coverage available over reconstruction
# space.
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date Feb 2019
#
import os
import SimpleITK as sitk
import pysitk.python_helper as ph
import niftymic.base.data_reader as dr
import niftymic.base.data_writer as dw
import niftymic.validation.slice_coverage as sc
from niftymic.utilities.input_arparser import InputArgparser
def main():
input_parser = InputArgparser(
description="Show data/slice coverage over specified reconstruction "
"space.",
)
input_parser.add_filenames(required=True)
input_parser.add_reconstruction_space(required=True)
input_parser.add_output(required=True)
input_parser.add_dir_input_mc()
input_parser.add_slice_thicknesses()
input_parser.add_verbose(default=0)
args = input_parser.parse_args()
input_parser.print_arguments(args)
data_reader = dr.MultipleImagesReader(
file_paths=args.filenames,
dir_motion_correction=args.dir_input_mc,
stacks_slice_thicknesses=args.slice_thicknesses,
)
data_reader.read_data()
stacks = data_reader.get_data()
reconstruction_space_sitk = sitk.ReadImage(args.reconstruction_space)
slice_coverage = sc.SliceCoverage(
stacks=stacks,
reconstruction_sitk=reconstruction_space_sitk,
)
slice_coverage.run()
coverage_sitk = slice_coverage.get_coverage_sitk()
dw.DataWriter.write_mask(coverage_sitk, args.output)
if args.verbose:
niftis = [
args.reconstruction_space,
args.output,
]
ph.show_niftis(niftis)
if __name__ == '__main__':
main()
================================================
FILE: niftymic/base/__init__.py
================================================
================================================
FILE: niftymic/base/data_reader.py
================================================
##
# \file DataReader.py
# \brief Reads data and returns Stack objects
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date July 2017
#
import os
import re
import six
import natsort
import numpy as np
import nibabel as nib
import SimpleITK as sitk
from abc import ABCMeta, abstractmethod
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
import niftymic.base.stack as st
import niftymic.base.exceptions as exceptions
import niftymic.utilities.motion_updater as mu
from niftymic.definitions import ALLOWED_EXTENSIONS
from niftymic.definitions import REGEX_FILENAMES
from niftymic.definitions import REGEX_FILENAME_EXTENSIONS
##
# DataReader is an abstract class to read data.
# \date 2017-07-12 11:38:07+0100
#
class DataReader(object):
__metaclass__ = ABCMeta
@abstractmethod
def read_data(self):
pass
@abstractmethod
def get_data(self):
pass
class ImageHeaderReader(DataReader):
def __init__(self, path_to_image):
self._path_to_image = path_to_image
self._image_nib = None
def read_data(self):
self._image_nib = nib.load(self._path_to_image)
def get_data(self):
return self._image_nib.header
def get_niftymic_version(self):
if self._image_nib is None:
raise RuntimeError("Execute 'read_data' first.")
# NiftyMIC version embedded in the aux_file field
aux_file_entry = str(self._image_nib.header["aux_file"])
if "NiftyMIC" in aux_file_entry:
version_str = re.sub("NiftyMIC-v", "", aux_file_entry)
# remove annoying byte code format embedded in string "b'...'":
if "b'" == version_str[0:2] and "'" == version_str[-1]:
version_str = version_str[2:-1]
return version_str
else:
return None
##
# DataReader is an abstract class to read 3D images.
# \date 2017-07-12 11:38:07+0100
#
class ImageDataReader(DataReader):
__metaclass__ = ABCMeta
def __init__(self):
DataReader.__init__(self)
self._stacks = None
##
# Returns the read data as list of Stack objects
# \date 2017-07-12 11:38:52+0100
#
# \param self The object
#
# \return The stacks.
#
def get_data(self):
if type(self._stacks) is not list:
raise exceptions.ObjectNotCreated("read_data")
return [st.Stack.from_stack(s) for s in self._stacks]
##
# ImageDirectoryReader reads images and their masks from a given directory and
# returns them as a list of Stack objects.
# \date 2017-07-12 11:36:22+0100
#
class ImageDirectoryReader(ImageDataReader):
##
# Store relevant information to images and their potential masks from a
# specified directory
# \date 2017-07-11 19:04:25+0100
#
# \param self The object
# \param path_to_directory String to specify the path to input
# directory.
# \param suffix_mask extension of stack filename as string
# indicating associated mask, e.g. "_mask"
# for "A_mask.nii".
# \param extract_slices Boolean to indicate whether given 3D image
# shall be split into its slices along the
# k-direction.
#
def __init__(self,
path_to_directory,
suffix_mask="_mask",
extract_slices=True):
super(self.__class__, self).__init__()
self._path_to_directory = path_to_directory
self._suffix_mask = suffix_mask
self._extract_slices = extract_slices
##
# Reads the image data from the given folder.
# \date 2017-07-11 17:10:40+0100
#
def read_data(self):
if not ph.directory_exists(self._path_to_directory):
raise exceptions.DirectoryNotExistent(self._path_to_directory)
abs_path_to_directory = os.path.abspath(self._path_to_directory)
# Get data filenames of images without filename extension
pattern = "(" + REGEX_FILENAMES + ")[.]" + REGEX_FILENAME_EXTENSIONS
pattern_mask = "(" + REGEX_FILENAMES + ")" + self._suffix_mask + \
"[.]" + REGEX_FILENAME_EXTENSIONS
p = re.compile(pattern)
p_mask = re.compile(pattern_mask)
# TODO:
# - If folder contains A.nii and A.nii.gz that ambiguity will not
# be detected
# - exclude potential mask filenames
# - hidden files are not excluded
dic_filenames = {p.match(f).group(1): p.match(f).group(0)
for f in os.listdir(abs_path_to_directory)
if p.match(f) and not p_mask.match(f)}
dic_filenames_mask = {p_mask.match(f).group(1):
p_mask.match(f).group(0)
for f in os.listdir(abs_path_to_directory)
if p_mask.match(f)}
# Filenames without filename ending as sorted list
filenames = natsort.natsorted(
dic_filenames.keys(), key=lambda y: y.lower())
self._stacks = [None] * len(filenames)
for i, filename in enumerate(filenames):
abs_path_image = os.path.join(abs_path_to_directory,
dic_filenames[filename])
if filename in dic_filenames_mask.keys():
abs_path_mask = os.path.join(abs_path_to_directory,
dic_filenames_mask[filename])
else:
ph.print_info("No mask found for '%s'." %
(abs_path_image))
abs_path_mask = None
self._stacks[i] = st.Stack.from_filename(
abs_path_image,
abs_path_mask,
extract_slices=self._extract_slices)
##
# MultipleImagesReader reads multiple nifti images and returns them as a list
# of Stack objects.
# \date 2017-07-12 11:28:10+0100
#
class MultipleImagesReader(ImageDataReader):
##
# Store relevant information to read multiple images and their potential
# masks.
# \date 2017-07-11 19:04:25+0100
#
# \param self The object
# \param file_paths The paths to filenames as list of strings,
# e.g. ["A.nii.gz", "B.nii", "C.nii.gz"]
# \param file_paths_masks The paths to the filename masks as list of
# strings. If given 'suffix_mask' is ignored.
# It is assumed the the sequence matches the
# filename order.
# \param suffix_mask extension of stack filename as string
# indicating associated mask, e.g. "_mask"
# for "A_mask.nii".
# \param extract_slices Boolean to indicate whether given 3D image
# shall be split into its slices along the
# k-direction.
#
def __init__(self,
file_paths,
file_paths_masks=None,
suffix_mask="_mask",
extract_slices=True,
dir_motion_correction=None,
prefix_slice="_slice",
stacks_slice_thicknesses=None,
):
super(self.__class__, self).__init__()
if stacks_slice_thicknesses is not None:
if len(stacks_slice_thicknesses) is not len(file_paths):
raise IOError("Number of given slice thicknesses must "
"correspond to the number of input images.")
self._stacks_slice_thicknesses = stacks_slice_thicknesses
else:
self._stacks_slice_thicknesses = [None] * len(file_paths)
# Get list of paths to image
self._file_paths = file_paths
self._file_paths_masks = file_paths_masks
self._suffix_mask = suffix_mask
self._dir_motion_correction = dir_motion_correction
self._extract_slices = extract_slices
self._prefix_slice = prefix_slice
##
# Reads the data of multiple images.
# \date 2017-07-12 11:30:35+0100
#
def read_data(self):
self._check_input()
self._stacks = [None] * len(self._file_paths)
for i, file_path in enumerate(self._file_paths):
if self._file_paths_masks is None:
file_path_mask = self._get_path_to_potential_mask(file_path)
else:
if i < len(self._file_paths_masks):
file_path_mask = self._file_paths_masks[i]
else:
file_path_mask = None
self._stacks[i] = st.Stack.from_filename(
file_path,
file_path_mask,
slice_thickness=self._stacks_slice_thicknesses[i],
extract_slices=self._extract_slices,
)
# if given image is actually a mask, update the filename so that
# subsequent MotionUpdater can associate the slice transformation
# files
if file_path == file_path_mask:
filename = self._stacks[i].get_filename()
filename = re.sub(self._suffix_mask, "", filename)
self._stacks[i].set_filename(filename)
if self._dir_motion_correction is not None:
motion_updater = mu.MotionUpdater(
stacks=self._stacks,
dir_motion_correction=self._dir_motion_correction)
motion_updater.run()
self._stacks = motion_updater.get_data()
def _check_input(self):
if type(self._file_paths) is not list:
raise IOError("file_paths must be provided as list")
if self._file_paths_masks is not None:
if type(self._file_paths_masks) is not list:
raise IOError("file_paths_masks must be provided as list")
##
# Gets the path to potential mask for a given file_path.
# \date 2018-01-25 12:34:23+0000
#
# \param self The object
# \param file_path The file path
#
# \return The path the mask associated with the file or None in case no
# mask was found.
#
def _get_path_to_potential_mask(self, file_path):
# Build absolute path to directory of image
path_to_directory = os.path.dirname(file_path)
filename = os.path.basename(file_path)
if not ph.directory_exists(path_to_directory):
raise exceptions.DirectoryNotExistent(path_to_directory)
abs_path_to_directory = os.path.abspath(path_to_directory)
# Get absolute path mask to image
pattern = "(" + REGEX_FILENAMES + \
")[.]" + REGEX_FILENAME_EXTENSIONS
p = re.compile(pattern)
# filename = [p.match(f).group(1) if p.match(file_path)][0]
if not file_path.endswith(tuple(ALLOWED_EXTENSIONS)):
raise IOError("Input image type not correct. Allowed types %s"
% "(" + (", or ").join(ALLOWED_EXTENSIONS) + ")")
# Strip extension from filename to find associated mask
filename = [re.sub("." + ext, "", filename)
for ext in ALLOWED_EXTENSIONS
if file_path.endswith(ext)][0]
pattern_mask = filename + self._suffix_mask + "[.]" + \
REGEX_FILENAME_EXTENSIONS
p_mask = re.compile(pattern_mask)
filename_mask = [p_mask.match(f).group(0)
for f in os.listdir(abs_path_to_directory)
if p_mask.match(f)]
if len(filename_mask) == 0:
abs_path_mask = None
else:
# exclude non-integer valued image as candidate (to avoid using
# the same image as mask in case of suffix_mask = '')
candidate = os.path.join(abs_path_to_directory, filename_mask[0])
candidate_sitk = sitk.ReadImage(candidate)
if "int" in candidate_sitk.GetPixelIDTypeAsString():
abs_path_mask = candidate
else:
abs_path_mask = None
return abs_path_mask
##
# ImageSlicesDirectoryReader reads multiple stacks and their associated
# individual slices from a directory.
# Rationale: Read individual slices after performed slice-to-volume
# registration steps.
# \date 2017-07-17 22:32:11+0100
#
class ImageSlicesDirectoryReader(ImageDataReader):
##
# Store relevant information to images, slices and their potential masks
# from a specified directory
# \date 2017-07-17 22:32:01+0100
#
# \param self The object
# \param path_to_directory String to specify the path to input
# directory where images and associated
# slices are stored.
# \param suffix_mask extension of stack filename as string
# indicating associated mask, e.g. "_mask"
# for "A_mask.nii".
#
def __init__(self,
path_to_directory,
image_selection=None,
suffix_mask="_mask",
prefix_slice="_slice"):
super(self.__class__, self).__init__()
self._path_to_directory = path_to_directory
self._suffix_mask = suffix_mask
self._prefix_slice = prefix_slice
self._image_selection = image_selection
self._transforms_sitk = None
def read_data(self):
if not ph.directory_exists(self._path_to_directory):
raise exceptions.DirectoryNotExistent(self._path_to_directory)
abs_path_to_directory = os.path.abspath(self._path_to_directory)
# Get data filenames of images by finding the prefixes associated
# to the slices which are build as filename_slice[0-9]+.nii.gz
pattern = "(" + REGEX_FILENAMES + ")" + \
self._prefix_slice + "[0-9]+[.]" + REGEX_FILENAME_EXTENSIONS
p = re.compile(pattern)
dic_filenames = {
p.match(f).group(1): p.match(f).group(0)
for f in os.listdir(abs_path_to_directory) if p.match(f)
}
# Filenames without filename ending as sorted list
filenames = natsort.natsorted(
dic_filenames.keys(), key=lambda y: y.lower())
# Reduce filenames to be read to selection only
if self._image_selection is not None:
filenames = [f for f in self._image_selection if f in filenames]
self._stacks = [None] * len(filenames)
self._slice_transforms_sitk = [None] * len(filenames)
for i, filename in enumerate(filenames):
# Get slice names associated to stack
pattern = "(" + filenames[i] + self._prefix_slice + \
")([0-9]+)[.]" + REGEX_FILENAME_EXTENSIONS
p = re.compile(pattern)
# Dictionary linking slice number with filename (without extension)
dic_slice_filenames = {
int(p.match(f).group(2)): p.match(f).group(1) + p.match(f).group(2)
for f in os.listdir(abs_path_to_directory) if p.match(f)
}
# Build stack from image and its found slices
self._stacks[i] = st.Stack.from_slice_filenames(
dir_input=self._path_to_directory,
prefix_stack=filename,
suffix_mask=self._suffix_mask,
dic_slice_filenames=dic_slice_filenames)
# Read
self._slice_transforms_sitk[i] = [
sitk.ReadTransform(os.path.join(
self._path_to_directory,
"%s.tfm" % dic_slice_filenames[k]))
for k in sorted(dic_slice_filenames.keys())
]
##
# Gets the transforms associated with each individual slice for all stacks.
# \date 2017-09-20 01:20:30+0100
#
# \param self The object
#
# \return List of slice transform lists. Each transform is of type
# sitk.Transform
#
def get_slice_transforms_sitk(self):
return self._slice_transforms_sitk
##
# MultiComponentImageReader reads a single image which has multiple components
# \date 2017-08-05 23:39:24+0100
#
class MultiComponentImageReader(ImageDataReader):
def __init__(self,
path_to_image,
path_to_image_mask=None,
dir_motion_correction=None,
volume_motion_only=False,
slice_thickness=None,
):
super(self.__class__, self).__init__()
self._path_to_image = path_to_image
self._path_to_image_mask = path_to_image_mask
self._dir_motion_correction = dir_motion_correction
self._volume_motion_only = volume_motion_only
self._slice_thickness = slice_thickness
def read_data(self):
vector_image_sitk = sitkh.read_sitk_vector_image(
self._path_to_image,
dtype=np.float64)
if self._path_to_image_mask is not None:
vector_image_sitk_mask = sitkh.read_sitk_vector_image(
self._path_to_image_mask,
dtype=np.uint8,
)
N_components = vector_image_sitk.GetNumberOfComponentsPerPixel()
self._stacks = [None] * N_components
filename_base = os.path.basename(self._path_to_image).split(".")[0]
for i in range(N_components):
image_sitk = sitk.VectorIndexSelectionCast(
vector_image_sitk, i)
if self._path_to_image_mask is not None:
image_sitk_mask = sitk.VectorIndexSelectionCast(
vector_image_sitk_mask, i)
else:
image_sitk_mask = None
if self._slice_thickness is None:
slice_thickness = image_sitk.GetSpacing()[-1]
else:
slice_thickness = self._slice_thickness
filename = filename_base + "_" + str(i)
self._stacks[i] = st.Stack.from_sitk_image(
image_sitk=image_sitk,
filename=filename,
image_sitk_mask=image_sitk_mask,
slice_thickness=float(slice_thickness),
)
if self._dir_motion_correction is not None:
motion_updater = mu.MotionUpdater(
stacks=self._stacks,
dir_motion_correction=self._dir_motion_correction,
volume_motion_only=self._volume_motion_only,
)
motion_updater.run()
self._stacks = motion_updater.get_data()
class TransformationDataReader(DataReader):
__metaclass__ = ABCMeta
def __init__(self):
DataReader.__init__(self)
self._transforms_sitk = None
# Third line in *.tfm file contains information on the transform type
self._transform_type = {
"Euler3DTransform_double_3_3": sitk.Euler3DTransform,
"AffineTransform_double_3_3": sitk.AffineTransform,
}
def get_data(self):
return self._transforms_sitk
def _get_sitk_transform_from_filepath(self, path_to_sitk_transform):
# Read transform as type sitk.Transform
transform_sitk = sitk.ReadTransform(path_to_sitk_transform)
# Convert transform to respective type, e.g. Euler, Affine etc
# Third line in *.tfm file contains information on the transform type
with open(path_to_sitk_transform) as f:
content = f.readlines()
transform_type = content[2]
transform_type = re.sub("\n", "", transform_type)
transform_type = transform_type.split(" ")[1]
transform_sitk = self._transform_type[transform_type](transform_sitk)
return transform_sitk
##
# Reads slice transformations stored in the format 'filename_slice#.tfm'.
#
# Rationale: Read only slice transformations associated with
# 'motion_correction' export achieved by the volumetric reconstruction
# algorithm
# \date 2018-01-31 19:16:00+0000
#
class SliceTransformationDirectoryReader(TransformationDataReader):
def __init__(self, directory, suffix_slice="_slice"):
TransformationDataReader.__init__(self)
self._directory = directory
self._suffix_slice = suffix_slice
def read_data(self):
if not ph.directory_exists(self._directory):
raise exceptions.DirectoryNotExistent(self._directory)
# Create absolute path for directory
directory = os.path.abspath(self._directory)
pattern = "(" + REGEX_FILENAMES + \
")%s([0-9]+)[.]tfm" % self._suffix_slice
p = re.compile(pattern)
dic_tmp = {
(p.match(f).group(1), int(p.match(f).group(2))):
os.path.join(directory, p.match(f).group(0))
for f in os.listdir(directory) if p.match(f)
}
fnames = list(set([k[0] for k in dic_tmp.keys()]))
self._transforms_sitk = {fname: {} for fname in fnames}
for (fname, slice_number), path in six.iteritems(dic_tmp):
self._transforms_sitk[fname][slice_number] = \
self._get_sitk_transform_from_filepath(path)
##
# Reads all transformations in a given directory and stores them in an ordered
# list
# \date 2018-01-31 19:34:52+0000
#
class TransformationDirectoryReader(TransformationDataReader):
def __init__(self, directory):
TransformationDataReader.__init__(self)
self._directory = directory
##
# Reads all transformations in a given directory and stores them in an
# ordered list
# \date 2018-01-31 19:32:45+0000
#
# \param self The object
# \param extension The extension
# \post self._transforms_sitk contains transformations as list of
# sitk.Transformation objects
#
# \return { description_of_the_return_value }
#
def read_data(self, extension="tfm"):
pattern = REGEX_FILENAMES + "[.]" + extension
p = re.compile(pattern)
filenames = [
os.path.join(self._directory, f)
for f in os.listdir(self._directory) if p.match(f)
]
filenames = natsort.natsorted(filenames, key=lambda y: y.lower())
transforms_reader = MultipleTransformationsReader(filenames)
transforms_reader.read_data()
self._transforms_sitk = transforms_reader.get_data()
##
# Reads multiple transformations and store them as lists
# \date 2018-01-31 19:33:51+0000
#
class MultipleTransformationsReader(TransformationDataReader):
def __init__(self, file_paths):
super(self.__class__, self).__init__()
self._file_paths = file_paths
##
# Reads multiple transformations and store them as lists
# \date 2018-01-31 19:32:45+0000
#
# \param self The object
# \post self._transforms_sitk contains transformations as list of
# sitk.Transformation objects
#
def read_data(self):
self._transforms_sitk = [
self._get_sitk_transform_from_filepath(self._file_paths[i])
for i in range(len(self._file_paths))
]
================================================
FILE: niftymic/base/data_writer.py
================================================
##
# \file DataWriter.py
# \brief Writes data to HDD
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date Aug 2017
#
import os
import sys
import numpy as np
import SimpleITK as sitk
from abc import ABCMeta, abstractmethod
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
import niftymic
class DataWriter(object):
##
# Gets the header update.
#
# aux_file: 23 characters max
# descrip: 79 characters max
#
# \see https://nifti.nimh.nih.gov/nifti-1/documentation/nifti1fields
# \date 2019-07-05 18:09:40+0100
#
# \param description description of NIfTI image (max 79 chars)
#
# \return dictionary that carries NIfTI header information updates.
#
@staticmethod
def _get_header_update(description=None):
header_update = {
"aux_file": "NiftyMIC-v%s" % niftymic.__version__
}
if description is None:
header_update["descrip"] = ""
else:
header_update["descrip"] = description
return header_update
@staticmethod
def write_image(
image_sitk,
path_to_file,
compress=True,
verbose=True,
description=None,
):
info = "Write image to %s" % path_to_file
if compress:
image_sitk = sitk.Cast(image_sitk, sitk.sitkFloat32)
info += " (float32)"
if verbose:
ph.print_info("%s ... " % info, newline=False)
header_update = DataWriter._get_header_update(description=description)
sitkh.write_nifti_image_sitk(
image_sitk, path_to_file, header_update=header_update)
if verbose:
print("done")
@staticmethod
def write_mask(
mask_sitk,
path_to_file,
compress=True,
verbose=True,
description=None,
):
info = "Write mask to %s" % path_to_file
if compress:
mask_sitk = sitk.Cast(mask_sitk, sitk.sitkUInt8)
info += " (uint8)"
if verbose:
ph.print_info("%s ... " % info, newline=False)
header_update = DataWriter._get_header_update(description=description)
sitkh.write_nifti_image_sitk(
mask_sitk, path_to_file, header_update=header_update)
if verbose:
print("done")
class StacksWriter(object):
__metaclass__ = ABCMeta
def __init__(self, stacks):
self._stacks = stacks
def set_stacks(self, stacks):
self._stacks = stacks
@abstractmethod
def write_data(self):
pass
class MultipleStacksWriter(StacksWriter):
def __init__(self,
stacks,
directory,
write_mask=False,
write_slices=False,
write_transforms=False,
suffix_mask="_mask",
):
StacksWriter.__init__(self, stacks=stacks)
self._directory = directory
self._write_mask = write_mask
self._write_slices = write_slices
self._write_transforms = write_transforms
self._suffix_mask = suffix_mask
def set_directory(self, directory):
self._directory = directory
def write_data(self):
for stack in self._stacks:
stack.write(self._directory,
write_mask=self._write_mask,
write_slices=self._write_slices,
write_transforms=self._write_transforms,
suffix_mask=self._suffix_mask)
class MultiComponentImageWriter(StacksWriter):
def __init__(self,
stacks,
filename=None,
write_mask=False,
suffix_mask="_mask",
compress=True,
description=None,
):
StacksWriter.__init__(self, stacks=stacks)
self._filename = filename
self._write_mask = write_mask
self._suffix_mask = suffix_mask
self._compress = compress
self._description = description
def set_filename(self, filename):
self._filename = filename
def write_data(self):
if self._filename is None:
raise ValueError("Filename is not set")
ph.create_directory(os.path.dirname(self._filename))
header_update = DataWriter._get_header_update(
description=self._description)
info = "Write image to '%s'" % self._filename
vector_image_sitk = sitkh.get_sitk_vector_image_from_components(
[stack.sitk for stack in self._stacks])
if self._compress:
if not "integer" in vector_image_sitk.GetPixelIDTypeAsString():
vector_image_sitk = sitk.Cast(
vector_image_sitk, sitk.sitkVectorFloat32)
info += " (float32)"
ph.print_info("%s ... " % info, newline=False)
sitkh.write_sitk_vector_image(
vector_image_sitk,
self._filename,
verbose=False,
header_update=header_update,
)
print("done")
if self._write_mask:
info = "Write image mask to '%s'" % self._filename
filename_split = (self._filename).split(".")
filename = filename_split[0]
filename += self._suffix_mask + "." + \
(".").join(filename_split[1:])
vector_image_sitk = sitkh.get_sitk_vector_image_from_components(
[stack.sitk_mask for stack in self._stacks])
if self._compress:
vector_image_sitk = sitk.Cast(
vector_image_sitk, sitk.sitkVectorUInt8)
info += " (uint8)"
ph.print_info("%s ... " % info, newline=False)
sitkh.write_sitk_vector_image(
vector_image_sitk,
filename,
verbose=False,
header_update=header_update,
)
print("done")
================================================
FILE: niftymic/base/exceptions.py
================================================
##
# \file exceptions.py
# \brief User-specific exceptions
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date June 2017
#
##
# Error handling in case the directory does not contain valid nifti file
# \date 2017-06-14 11:11:37+0100
#
class InputFilesNotValid(Exception):
##
# \date 2017-06-14 11:12:55+0100
#
# \param self The object
# \param directory Path to empty folder, string
#
def __init__(self, directory):
self.directory = directory
def __str__(self):
error = "Folder '%s' does not contain valid nifti files." % (
self.directory)
return error
##
# Error handling in case of an attempted object access which is not being
# created yet
# \date 2017-06-14 11:20:33+0100
#
class ObjectNotCreated(Exception):
##
# Store name of function which shall be executed to create desired object.
# \date 2017-06-14 11:20:52+0100
#
# \param self The object
# \param function_call function call missing to create the object
#
def __init__(self, function_call):
self.function_call = function_call
def __str__(self):
error = "Object has not been created yet. Run '%s' first." % (
self.function_call)
return error
##
# Error handling in case specified file does not exist
#
class FileNotExistent(Exception):
##
# Store information on the missing file
# \date 2017-06-29 12:49:18+0100
#
# \param self The object
# \param missing_file string of missing file
#
def __init__(self, missing_file):
self.missing_file = missing_file
def __str__(self):
error = "File '%s' does not exist" % (self.missing_file)
return error
##
# Error handling in case specified directory does not exist
# \date 2017-07-11 17:02:12+0100
#
class DirectoryNotExistent(Exception):
##
# Store information on the missing directory
# \date 2017-07-11 17:02:46+0100
#
# \param self The object
# \param missing_directory string of missing directory
#
def __init__(self, missing_directory):
self.missing_directory = missing_directory
def __str__(self):
error = "Directory '%s' does not exist" % (self.missing_directory)
return error
##
# Error handling in case multiple filenames exist
# (e.g. same filename but two different extensions)
# \date 2017-06-29 14:09:27+0100
#
class FilenameAmbiguous(Exception):
##
# Store information on the ambiguous file
# \date 2017-06-29 14:10:34+0100
#
# \param self The object
# \param ambiguous_filename string of ambiguous file
#
def __init__(self, ambiguous_filename):
self.ambiguous_filename = ambiguous_filename
def __str__(self):
error = "Filename '%s' ambiguous" % (self.ambiguous_filename)
return error
##
# Error handling in case IO is not correct
# \date 2017-07-11 20:21:19+0100
#
class IOError(Exception):
##
# Store information on the IO error
# \date 2017-07-11 20:21:38+0100
#
# \param self The object
# \param error The error
#
def __init__(self, error):
self.error = error
def __str__(self):
return self.error
================================================
FILE: niftymic/base/psf.py
================================================
##
# \file psf.py
# \brief Compute the Gaussian point spread function (PSF) associated with
# a slice acquisition in the coordinates of the reconstruction
# space
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date April 2016
#
# Import libraries
import numpy as np
class PSF:
##
# Compute rotated covariance matrix which expresses the PSF of the slice in
# the coordinates of the HR volume
# \date 2017-11-01 16:16:20+0000
#
# \param self The object
# \param slice Slice object which is aimed to be simulated
# according to the slice acquisition model
# \param reconstruction Stack object containing the HR volume
#
# \return Covariance matrix U*Sigma_diag*U' where U represents the
# orthogonal trafo between slice and reconstruction
#
def get_covariance_matrix_in_reconstruction_space(
self,
slice,
reconstruction):
cov = self.get_covariance_matrix_in_reconstruction_space_sitk(
reconstruction.sitk.GetDirection(),
slice.sitk.GetDirection(),
slice.sitk.GetSpacing())
return cov
##
# Gets the axis-aligned covariance matrix describing the PSF in
# reconstruction space coordinates.
# \date 2017-11-01 16:21:31+0000
#
# \param self The object
# \param reconstruction_direction_sitk Image header (sitk) direction
# of reconstruction space
# \param slice_direction_sitk Image header (sitk) direction
# of slice space
# \param slice_spacing Spacing of slice space
#
# \return Axis-aligned covariance matrix describing the PSF.
#
def get_covariance_matrix_in_reconstruction_space_sitk(
self,
reconstruction_direction_sitk,
slice_direction_sitk,
slice_spacing):
# Compute rotation matrix to express the PSF in the coordinate system
# of the reconstruction space
U = self._get_relative_rotation_matrix(
slice_direction_sitk, reconstruction_direction_sitk)
# Get axis-aligned PSF
cov = self.get_gaussian_psf_covariance_matrix_from_spacing(
slice_spacing)
# Return Gaussian blurring variance covariance matrix of slice in
# reconstruction space coordinates
return U.dot(cov).dot(U.transpose())
##
# Gets the predefined covariance matrix in reconstruction space.
# \date 2017-10-31 23:27:44+0000
#
# \param self The object
# \param reconstruction_direction_sitk Image header (sitk) direction
# of reconstruction space
# \param slice_direction_sitk Image header (sitk) direction
# of slice space
# \param cov Axis-aligned covariance matrix
# describing the PSF
#
# \return The predefined covariance matrix in reconstruction space
# coordinates.
#
def get_predefined_covariance_matrix_in_reconstruction_space(
self,
reconstruction_direction_sitk,
slice_direction_sitk,
cov):
# Compute rotation matrix to express the PSF in the coordinate system
# of the reconstruction space
U = self._get_relative_rotation_matrix(
slice_direction_sitk, reconstruction_direction_sitk)
# Return Gaussian blurring variance covariance matrix of slice in
# reconstruction space coordinates
return U.dot(cov).dot(U.transpose())
##
# Compute (axis aligned) covariance matrix from spacing The PSF is modelled
# as Gaussian with
# FWHM = 1.2*in-plane-resolution (in-plane)
# FWHM = slice thickness (through-plane)
# \date 2017-11-01 16:16:36+0000
#
# \param spacing 3D array containing in-plane and through-plane
# dimensions
#
# \return (axis aligned) covariance matrix representing PSF modelled
# Gaussian as 3x3 np.array
#
@staticmethod
def get_gaussian_psf_covariance_matrix_from_spacing(spacing):
# Compute Gaussian to approximate in-plane PSF:
sigma_x2 = (1.2*spacing[0])**2/(8*np.log(2))
sigma_y2 = (1.2*spacing[1])**2/(8*np.log(2))
# Compute Gaussian to approximate through-plane PSF:
sigma_z2 = spacing[2]**2/(8*np.log(2))
return np.diag([sigma_x2, sigma_y2, sigma_z2])
##
# Gets the relative rotation matrix to express slice-axis aligned
# covariance matrix in coordinates of HR volume
# \date 2016-10-14 16:37:57+0100
#
# \param slice_direction_sitk Image header (sitk) direction
# of slice space
# \param reconstruction_direction_sitk Image header (sitk) direction
# of reconstruction space
#
# \return The relative rotation matrix as 3x3 numpy array
#
@staticmethod
def _get_relative_rotation_matrix(slice_direction_sitk,
reconstruction_direction_sitk):
# Compute rotation matrix to express the PSF in the coordinate system
# of the HR volume
dim = np.sqrt(len(slice_direction_sitk)).astype('int')
direction_matrix_reconstruction = np.array(
reconstruction_direction_sitk).reshape(dim, dim)
direction_matrix_slice = np.array(
slice_direction_sitk).reshape(dim, dim)
U = direction_matrix_reconstruction.transpose().dot(
direction_matrix_slice)
return U
================================================
FILE: niftymic/base/slice.py
================================================
##
# \file Slice.py
# \brief { item_description }
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date September 2015
#
import os
import numpy as np
import SimpleITK as sitk
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
import niftymic.base.data_writer as dw
import niftymic.base.exceptions as exceptions
from niftymic.definitions import VIEWER
# In addition to the nifti-image as being stored as sitk.Image for a single
# 3D slice \f$ \in R^3 \times R^3 \times 1\f$ the class Slice
# also contains additional variables helpful to work with the data
class Slice:
# Create Slice instance with additional information to actual slice
# \param[in] slice_sitk 3D slice in \R x \R x 1, sitk.Image object
# \param[in] filename of parent stack, string
# \param[in] slice_number number of slice within parent stack, integer
# \param[in] slice_sitk_mask associated mask of slice, sitk.Image object (optional)
@classmethod
def from_sitk_image(cls,
slice_sitk,
slice_number,
slice_thickness,
filename="unknown",
slice_sitk_mask=None,
):
slice = cls()
# Directory
# dir_input = "/".join(filename.split("/")[0:-1]) + "/"
# Filename without extension
# filename = filename.split("/")[-1:][0].split(".")[0]
slice._dir_input = None
slice._filename = filename
slice._slice_number = slice_number
slice._slice_thickness = slice_thickness
# Explicit cast (+ creation of other image instance)
slice.sitk = sitk.Cast(slice_sitk, sitk.sitkFloat64)
slice.itk = sitkh.get_itk_from_sitk_image(slice.sitk)
# Append masks (if provided)
if slice_sitk_mask is not None:
slice.sitk_mask = sitk.Cast(slice_sitk_mask, sitk.sitkUInt8)
try:
# ensure mask occupies the same physical space
slice.sitk_mask.CopyInformation(slice.sitk)
except RuntimeError as e:
raise IOError(
"Given image and its mask do not occupy the same space: %s" %
e.message)
slice.itk_mask = sitkh.get_itk_from_sitk_image(slice.sitk_mask)
else:
slice.sitk_mask = slice._generate_identity_mask()
slice.itk_mask = sitkh.get_itk_from_sitk_image(slice.sitk_mask)
# slice._sitk_upsampled = None
# HACK (for current Slice-to-Volume Registration)
# See class SliceToVolumeRegistration
# slice._sitk_upsampled = slice._get_upsampled_isotropic_resolution_slice(slice_sitk)
# slice._itk_upsampled =
# sitkh.get_itk_from_sitk_image(slice._sitk_upsampled)
# if slice_sitk_mask is not None:
# slice._sitk_mask_upsampled = slice._get_upsampled_isotropic_resolution_slice(slice_sitk_mask)
# slice._itk_mask_upsampled = sitkh.get_itk_from_sitk_image(slice._sitk_mask_upsampled)
# else:
# slice._sitk_mask_upsampled = None
# slice._itk_mask_upsampled = None
# Store current affine transform of image
slice._affine_transform_sitk = sitkh.get_sitk_affine_transform_from_sitk_image(
slice.sitk)
# Prepare history of affine transforms, i.e. encoded spatial
# position+orientation of slice, and rigid motion estimates of slice
# obtained in the course of the registration/reconstruction process
slice._history_affine_transforms = []
slice._history_affine_transforms.append(slice._affine_transform_sitk)
slice._history_motion_corrections = []
slice._history_motion_corrections.append(sitk.Euler3DTransform())
return slice
# Create Stack instance from file and add corresponding mask. Mask is
# either provided in the directory or created as binary mask consisting
# of ones.
# \param[in] dir_input string to input directory of nifti-file to read
# \param[in] filename string of nifti-file to read
# \param[in] stack_filename filename extension of parent stack, string
# \param[in] slice_number number of slice within parent stack, integer
# \param[in] suffix_mask extension of slice filename which indicates associated mask
# \return Stack object including its slices with corresponding masks
@classmethod
def from_filename(cls,
file_path,
slice_number,
slice_thickness,
file_path_mask=None,
verbose=False,
):
slice = cls()
if not ph.file_exists(file_path):
raise exceptions.FileNotExistent(file_path)
slice._dir_input = os.path.dirname(file_path)
slice._filename = os.path.basename(file_path).split(".")[0]
slice._slice_number = slice_number
slice._slice_thickness = slice_thickness
# Append stacks as SimpleITK and ITK Image objects
slice.sitk = sitkh.read_nifti_image_sitk(file_path, sitk.sitkFloat64)
slice.itk = sitkh.get_itk_from_sitk_image(slice.sitk)
# Append masks (if provided)
if file_path_mask is None:
slice.sitk_mask = slice._generate_identity_mask()
if verbose:
ph.print_info(
"Identity mask created for '%s'." % (file_path))
else:
if not ph.file_exists(file_path_mask):
raise exceptions.FileNotExistent(file_path_mask)
slice.sitk_mask = sitkh.read_nifti_image_sitk(
file_path_mask, sitk.sitkUInt8)
try:
# ensure mask occupies the same physical space
slice.sitk_mask.CopyInformation(slice.sitk)
except RuntimeError as e:
raise IOError(
"Given image and its mask do not occupy the same space: %s" %
e.message)
slice.itk_mask = sitkh.get_itk_from_sitk_image(slice.sitk_mask)
# Store current affine transform of image
slice._affine_transform_sitk = sitkh.get_sitk_affine_transform_from_sitk_image(
slice.sitk)
# Prepare history of affine transforms, i.e. encoded spatial
# position+orientation of slice, and motion estimates of slice
# obtained in the course of the registration/reconstruction process
slice._history_affine_transforms = []
slice._history_affine_transforms.append(slice._affine_transform_sitk)
slice._history_motion_corrections = []
slice._history_motion_corrections.append(sitk.Euler3DTransform())
return slice
# Copy constructor
# \param[in] slice_to_copy Slice object to be copied
# \return copied Slice object
# TODO: That's not really well done!
@classmethod
def from_slice(cls, slice_to_copy):
slice = cls()
if not isinstance(slice_to_copy, Slice):
raise ValueError("Input must be of type Slice. Given: %s" %
type(slice_to_copy))
# Copy image slice and mask
slice.sitk = sitk.Image(slice_to_copy.sitk)
slice.itk = sitkh.get_itk_from_sitk_image(slice.sitk)
slice.sitk_mask = sitk.Image(slice_to_copy.sitk_mask)
slice.itk_mask = sitkh.get_itk_from_sitk_image(slice.sitk_mask)
slice._filename = slice_to_copy.get_filename()
slice._slice_number = slice_to_copy.get_slice_number()
slice._dir_input = slice_to_copy.get_directory()
slice._slice_thickness = slice_to_copy.get_slice_thickness()
# slice._history_affine_transforms, slice._history_motion_corrections =
# slice_to_copy.get_registration_history()
# Store current affine transform of image
slice._affine_transform_sitk = sitkh.get_sitk_affine_transform_from_sitk_image(
slice.sitk)
# Prepare history of affine transforms, i.e. encoded spatial
# position+orientation of slice, and rigid motion estimates of slice
# obtained in the course of the registration/reconstruction process
slice._history_affine_transforms, slice._history_motion_corrections = slice_to_copy.get_registration_history()
return slice
##
# Motion correction update.
# \date 2016-09-21 00:50:08+0100
#
# Update motion correction of slice and update its position in physical
# space accordingly.
#
# \param self The object
# \param[in] affine_transform_sitk transform as sitk.AffineTransform
# object
# \post origin and direction of slice gets updated based on transform
#
def update_motion_correction(self, affine_transform_sitk):
# Update rigid motion estimate
current_rigid_motion_estimate = sitkh.get_composite_sitk_affine_transform(
affine_transform_sitk, self._history_motion_corrections[-1])
self._history_motion_corrections.append(current_rigid_motion_estimate)
# New affine transform of slice after rigid motion correction
affine_transform = sitkh.get_composite_sitk_affine_transform(
affine_transform_sitk, self._affine_transform_sitk)
# Update affine transform of slice, i.e. change image origin and
# direction in physical space
self._update_affine_transform(affine_transform)
# ## Update rigid motion estimate of slice and update its position in
# # physical space accordingly.
# # \param[in] rigid_transform_sitk rigid transform as sitk object
# # \post origin and direction of slice gets updated based on rigid transform
# def update_rigid_motion_estimate(self, rigid_transform_sitk):
# ## Update rigid motion estimate
# current_rigid_motion_estimate = sitkh.get_composite_sitk_euler_transform(rigid_transform_sitk, self._history_rigid_motion_estimates[-1])
# self._history_rigid_motion_estimates.append(current_rigid_motion_estimate)
# ## New affine transform of slice after rigid motion correction
# affine_transform =
# sitkh.get_composite_sitk_affine_transform(rigid_transform_sitk,
# self._affine_transform_sitk)
# ## Update affine transform of slice, i.e. change image origin and direction in physical space
# self._update_affine_transform(affine_transform)
# Get filename of slice, e.g. name of parent stack
# \return filename, string
def get_filename(self):
return self._filename
def set_filename(self, filename):
self._filename = filename
# Get number of slice within parent stack
# \return slice number, integer
def get_slice_number(self):
return self._slice_number
def get_slice_thickness(self):
return float(self._slice_thickness)
def get_inplane_resolution(self):
return float(self.sitk.GetSpacing()[0])
# Get directory where parent stack is stored
# \return directory, string
def get_directory(self):
return self._dir_input
# Get current affine transformation defining the spatial position in
# physical space of slice
# \return affine transformation, sitk.AffineTransform object
def get_affine_transform(self):
return self._affine_transform_sitk
##
# Get applied motion correction transform to slice
# \date 2017-08-08 13:16:28+0100
#
# \param self The object
#
# \return The motion correction transform.
#
def get_motion_correction_transform(self):
return self._history_motion_corrections[-1]
# Get history history of affine transforms, i.e. encoded spatial
# position+orientation of slice, and rigid motion estimates of slice
# obtained in the course of the registration/reconstruction process
# \return list of sitk.AffineTransform and sitk.Euler3DTransform objects
def get_registration_history(self):
affine_transforms = list(self._history_affine_transforms)
motion_corrections = list(self._history_motion_corrections)
return affine_transforms, motion_corrections
def set_registration_history(self, registration_history):
affine_transform_sitk = registration_history[0][-1]
self._update_affine_transform(affine_transform_sitk)
self._history_affine_transforms = [a for a in registration_history[0]]
self._history_motion_corrections = [t for t in registration_history[1]]
# Display slice with external viewer (ITK-Snap)
# \param[in] show_segmentation display slice with or without associated segmentation (default=0)
def show(self, show_segmentation=0, label=None, viewer=VIEWER, verbose=True):
if label is None:
label = self._filename + "_" + str(self._slice_number)
if show_segmentation:
segmentation = self.sitk_mask
else:
segmentation = None
sitkh.show_sitk_image(
self.sitk,
segmentation=segmentation,
label=label,
viewer=viewer,
verbose=verbose)
# Write information of Slice to HDD to given diretory:
# - sitk.Image object of slice
# - affine transformation describing physical space position of slice
# \param[in] directory string specifying where the output will be written to (default="/tmp/")
# \param[in] filename string specifyig the filename. If not given, filename of parent stack is used
def write(self,
directory,
filename=None,
write_slice=True,
write_transform=True,
suffix_mask="_mask",
prefix_slice="_slice",
write_transforms_history=False,
):
# Create directory if not existing
ph.create_directory(directory)
# Construct filename
if filename is None:
filename_out = self._filename + \
prefix_slice + str(self._slice_number)
else:
filename_out = filename + prefix_slice + str(self._slice_number)
full_file_name = os.path.join(directory, filename_out)
# Write slice and affine transform
if write_slice:
dw.DataWriter.write_image(
self.sitk, "%s.nii.gz" % full_file_name, verbose=False)
# Write mask to specified location if given
if self.sitk_mask is not None:
nda = sitk.GetArrayFromImage(self.sitk_mask)
# Write mask if it does not consist of only ones
if not np.all(nda):
dw.DataWriter.write_mask(
self.sitk_mask,
"%s%s.nii.gz" % (full_file_name, suffix_mask),
verbose=False,
)
if write_transform:
sitk.WriteTransform(
# self.get_affine_transform(),
self.get_motion_correction_transform(),
full_file_name + ".tfm")
if write_transforms_history:
dir_output = os.path.join(directory, "motion_correction_history")
ph.create_directory(dir_output)
registration_transforms = self.get_registration_history()[1]
for i, transform_sitk in enumerate(registration_transforms):
path_to_transform = os.path.join(
dir_output, "%s_%d.tfm" % (filename_out, i))
sitk.WriteTransform(transform_sitk, path_to_transform)
# print("Slice %r of stack %s was successfully written to %s" %(self._slice_number, self._filename, full_file_name))
# print("Transformation of slice %r of stack %s was successfully
# written to %s" %(self._slice_number, self._filename, full_file_name))
# Update slice with new affine transform, specifying updated spatial
# position of slice in physical space. The transform is obtained via
# slice-to-volume registration step, e.g.
# \param[in] affine_transform_sitk affine transform as sitk-object
def _update_affine_transform(self, affine_transform_sitk):
# Ensure correct object type
self._affine_transform_sitk = sitk.AffineTransform(
affine_transform_sitk)
# Append transform to registration history
self._history_affine_transforms.append(affine_transform_sitk)
# Get origin and direction of transformed 3D slice given the new
# spatial transform
origin = sitkh.get_sitk_image_origin_from_sitk_affine_transform(
affine_transform_sitk, self.sitk)
direction = sitkh.get_sitk_image_direction_from_sitk_affine_transform(
affine_transform_sitk, self.sitk)
# Update image objects
self.sitk.SetOrigin(origin)
self.sitk.SetDirection(direction)
self.itk.SetOrigin(origin)
self.itk.SetDirection(sitkh.get_itk_from_sitk_direction(direction))
# Update image mask objects
if self.sitk_mask is not None:
self.sitk_mask.SetOrigin(origin)
self.sitk_mask.SetDirection(direction)
self.itk_mask.SetOrigin(origin)
self.itk_mask.SetDirection(
sitkh.get_itk_from_sitk_direction(direction))
# ## Upsample slices in k-direction to in-plane resolution.
# # \param[in] slice_sitk slice as sitk.Image object to be upsampled
# # \return upsampled slice as sitk.Image object
# # \warning only used for Slice-to-Volume Registration and shall me removed at some point
# def _get_upsampled_isotropic_resolution_slice(self, slice_sitk):
# ## Fetch info used for upsampling
# spacing = np.array(slice_sitk.GetSpacing())
# size = np.array(slice_sitk.GetSize())
# ## Set dimension of each slice in k-direction accordingly
# size[2] = np.round(spacing[2]/spacing[0])
# ## Update spacing in k-direction to be equal to in-plane spacing
# spacing[2] = spacing[0]
# ## Upsample slice to isotropic resolution
# default_pixel_value = 0
# slice_upsampled_sitk = sitk.Resample(
# slice_sitk,
# size,
# sitk.Euler3DTransform(),
# sitk.sitkNearestNeighbor,
# slice_sitk.GetOrigin(),
# spacing,
# slice_sitk.GetDirection(),
# default_pixel_value,
# slice_sitk.GetPixelIDValue())
# return slice_upsampled_sitk
# Create a binary mask consisting of ones
# \return binary_mask as sitk.Image object consisting of ones
def _generate_identity_mask(self):
shape = sitk.GetArrayFromImage(self.sitk).shape
nda = np.ones(shape, dtype=np.uint8)
binary_mask = sitk.GetImageFromArray(nda)
binary_mask.CopyInformation(self.sitk)
return binary_mask
================================================
FILE: niftymic/base/stack.py
================================================
##
# \file stack.py
# \brief { item_description }
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date September 2015
#
import os
import re
import numpy as np
import SimpleITK as sitk
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
import simplereg.resampler
import niftymic.base.slice as sl
import niftymic.base.exceptions as exceptions
import niftymic.base.data_writer as dw
from niftymic.definitions import ALLOWED_EXTENSIONS, VIEWER
##
# In addition to the nifti-image (stored as sitk.Image object) this class Stack
# also contains additional variables helpful to work with the data.
#
class Stack:
def __init__(self):
self._is_unity_mask = True
self._deleted_slices = []
self._history_affine_transforms = []
self._history_motion_corrections = []
##
# Create Stack instance from file and add corresponding mask. Mask is
# either provided in the directory or created as binary mask consisting of
# ones.
# \param[in] dir_input string to input directory of nifti-file to read
# \param[in] filename string of nifti-file to read
# \param[in] suffix_mask extension of stack filename which indicates
# associated mask
# \return Stack object including its slices with corresponding masks
#
@classmethod
def from_filename(cls,
file_path,
file_path_mask=None,
extract_slices=True,
verbose=False,
slice_thickness=None,
):
stack = cls()
if not ph.file_exists(file_path):
raise exceptions.FileNotExistent(file_path)
path_to_directory = os.path.dirname(file_path)
# Strip extension from filename and remove potentially included "."
filename = [re.sub("." + ext, "", os.path.basename(file_path))
for ext in ALLOWED_EXTENSIONS
if file_path.endswith(ext)][0]
# filename = filename.replace(".", "p")
stack._dir = os.path.dirname(file_path)
stack._filename = filename
# Append stacks as SimpleITK and ITK Image objects
stack.sitk = sitkh.read_nifti_image_sitk(file_path, sitk.sitkFloat64)
stack.itk = sitkh.get_itk_from_sitk_image(stack.sitk)
# Set slice thickness of acquisition
if slice_thickness is None:
stack._slice_thickness = stack.sitk.GetSpacing()[-1]
else:
stack._slice_thickness = slice_thickness
# Append masks (either provided or binary mask)
if file_path_mask is None:
stack.sitk_mask = stack._generate_identity_mask()
if verbose:
ph.print_info(
"Identity mask created for '%s'." % (file_path))
else:
if not ph.file_exists(file_path_mask):
raise exceptions.FileNotExistent(file_path_mask)
stack.sitk_mask = sitkh.read_nifti_image_sitk(
file_path_mask, sitk.sitkUInt8)
try:
# ensure masks occupy same physical space
stack.sitk_mask.CopyInformation(stack.sitk)
except RuntimeError as e:
raise IOError(
"Given image and its mask do not occupy the same space: %s" %
e.message)
stack._is_unity_mask = False
# Check that binary mask is provided
nda_mask = sitk.GetArrayFromImage(stack.sitk_mask)
if nda_mask.max() > 1:
raise ValueError(
"Mask values > 1 encountered in '%s'. "
"Only binary masks are allowed." % file_path_mask)
# Append itk object
stack.itk_mask = sitkh.get_itk_from_sitk_image(stack.sitk_mask)
# Store current affine transform of image
stack._affine_transform_sitk = sitkh.get_sitk_affine_transform_from_sitk_image(
stack.sitk)
# Prepare history of affine transforms, i.e. encoded spatial
# position+orientation of stack, and motion estimates of stack
# obtained in the course of the registration/reconstruction process
stack._history_affine_transforms = []
stack._history_affine_transforms.append(stack._affine_transform_sitk)
stack._history_motion_corrections = []
stack._history_motion_corrections.append(sitk.Euler3DTransform())
# Extract all slices and their masks from the stack and store them
if extract_slices:
dimenson = stack.sitk.GetDimension()
if dimenson == 3:
stack._N_slices = stack.sitk.GetSize()[-1]
stack._slices = stack._extract_slices(
slice_thickness=stack.get_slice_thickness())
elif dimenson == 2:
stack._N_slices = 1
stack._slices = [stack.sitk[:, :]]
else:
stack._N_slices = 0
stack._slices = None
if verbose:
ph.print_info(
"Stack (image + mask) associated to '%s' successfully read." %
(file_path))
return stack
##
# Create Stack instance from stack slices in specified directory and add
# corresponding mask.
# \date 2017-08-15 19:18:56+0100
#
# \param cls The cls
# \param[in] dir_input string to input directory where bundle
# of slices are stored
# \param[in] prefix_stack prefix indicating the corresponding
# stack
# \param[in] suffix_mask extension of stack filename which
# indicates associated mask
# \param dic_slice_filenames Dictionary linking slice number (int)
# with filename (without extension)
# \param prefix_slice The prefix slice
#
# \return Stack object including its slices with corresponding masks
# \example mask (suffix_mask) of slice j of stack i (prefix_stack)
# reads: i_slicej_mask.nii.gz
#
# TODO: Code cleaning
@classmethod
def from_slice_filenames(cls,
dir_input,
prefix_stack,
suffix_mask=None,
dic_slice_filenames=None,
prefix_slice="_slice",
slice_thickness=None,
):
stack = cls()
if dir_input[-1] is not "/":
dir_input += "/"
stack._dir = dir_input
stack._filename = prefix_stack
# Get 3D images
stack.sitk = sitkh.read_nifti_image_sitk(
dir_input + prefix_stack + ".nii.gz", sitk.sitkFloat64)
stack.itk = sitkh.get_itk_from_sitk_image(stack.sitk)
# Store current affine transform of image
stack._affine_transform_sitk = sitkh.get_sitk_affine_transform_from_sitk_image(
stack.sitk)
# Prepare history of affine transforms, i.e. encoded spatial
# position+orientation of stack, and motion estimates of stack
# obtained in the course of the registration/reconstruction process
stack._history_affine_transforms = []
stack._history_affine_transforms.append(stack._affine_transform_sitk)
stack._history_motion_corrections = []
stack._history_motion_corrections.append(sitk.Euler3DTransform())
# Set slice thickness of acquisition
if slice_thickness is None:
stack._slice_thickness = float(stack.sitk.GetSpacing()[-1])
else:
stack._slice_thickness = float(slice_thickness)
# Append masks (either provided or binary mask)
if suffix_mask is not None and \
os.path.isfile(dir_input +
prefix_stack + suffix_mask + ".nii.gz"):
stack.sitk_mask = sitkh.read_nifti_image_sitk(
dir_input + prefix_stack + suffix_mask + ".nii.gz",
sitk.sitkUInt8)
stack.itk_mask = sitkh.get_itk_from_sitk_image(stack.sitk_mask)
stack._is_unity_mask = False
else:
stack.sitk_mask = stack._generate_identity_mask()
stack.itk_mask = sitkh.get_itk_from_sitk_image(stack.sitk_mask)
stack._is_unity_mask = True
# Get slices
if dic_slice_filenames is None:
stack._N_slices = stack.sitk.GetDepth()
stack._slices = [None] * stack._N_slices
# Append slices as Slice objects
for i in range(0, stack._N_slices):
path_to_slice = os.path.join(
dir_input,
prefix_stack + prefix_slice + str(i) + ".nii.gz")
path_to_slice_mask = os.path.join(
dir_input,
prefix_stack + prefix_slice + str(i) + suffix_mask + ".nii.gz")
if ph.file_exists(path_to_slice_mask):
stack._slices[i] = sl.Slice.from_filename(
file_path=path_to_slice,
slice_number=i,
file_path_mask=path_to_slice_mask)
else:
stack._slices[i] = sl.Slice.from_filename(
file_path=path_to_slice,
slice_number=i)
else:
slice_numbers = sorted(dic_slice_filenames.keys())
stack._N_slices = len(slice_numbers)
stack._slices = [None] * stack._N_slices
for i, slice_number in enumerate(slice_numbers):
path_to_slice = os.path.join(
dir_input,
dic_slice_filenames[slice_number] + ".nii.gz")
path_to_slice_mask = os.path.join(
dir_input, dic_slice_filenames[slice_number] + suffix_mask + ".nii.gz")
if ph.file_exists(path_to_slice_mask):
stack._slices[i] = sl.Slice.from_filename(
file_path=path_to_slice,
slice_number=slice_number,
file_path_mask=path_to_slice_mask,
slice_thickness=stack.get_slice_thickness(),
)
else:
stack._slices[i] = sl.Slice.from_filename(
file_path=path_to_slice,
slice_number=slice_number,
slice_thickness=stack.get_slice_thickness(),
)
return stack
# Create Stack instance from exisiting sitk.Image instance. Slices are
# not extracted and stored separately in the object. The idea is to use
# this function when the stack is regarded as entire volume (like the
# reconstructed HR volume).
# \param[in] image_sitk sitk.Image created from nifti-file
# \param[in] name string containing the chosen name for the stack
# \param[in] image_sitk_mask associated mask of stack, sitk.Image object (optional)
# \return Stack object without slice information
@classmethod
def from_sitk_image(cls,
image_sitk,
slice_thickness,
filename="unknown",
image_sitk_mask=None,
extract_slices=True,
slice_numbers=None,
):
stack = cls()
# Explicit cast (+ creation of other image instance)
stack.sitk = sitk.Cast(image_sitk, sitk.sitkFloat64)
stack.itk = sitkh.get_itk_from_sitk_image(stack.sitk)
# Set slice thickness of acquisition
if not ph.is_float(slice_thickness):
raise ValueError("Slice thickness must be of type float")
stack._slice_thickness = float(slice_thickness)
stack._filename = filename
stack._dir = None
# Append masks (if provided)
if image_sitk_mask is not None:
stack.sitk_mask = sitk.Cast(image_sitk_mask, sitk.sitkUInt8)
try:
# ensure mask occupies the same physical space
stack.sitk_mask.CopyInformation(stack.sitk)
except RuntimeError as e:
raise IOError(
"Given image and its mask do not occupy the same space: %s" %
e.message)
stack.itk_mask = sitkh.get_itk_from_sitk_image(stack.sitk_mask)
if sitk.GetArrayFromImage(stack.sitk_mask).prod() == 1:
stack._is_unity_mask = True
else:
stack._is_unity_mask = False
else:
stack.sitk_mask = stack._generate_identity_mask()
stack.itk_mask = sitkh.get_itk_from_sitk_image(stack.sitk_mask)
stack._is_unity_mask = True
# Extract all slices and their masks from the stack and store them
if extract_slices:
stack._N_slices = stack.sitk.GetSize()[-1]
stack._slices = stack._extract_slices(
slice_numbers=slice_numbers,
slice_thickness=slice_thickness,
)
else:
stack._N_slices = 0
stack._slices = None
# Store current affine transform of image
stack._affine_transform_sitk = sitkh.get_sitk_affine_transform_from_sitk_image(
stack.sitk)
stack._history_affine_transforms = []
stack._history_affine_transforms.append(stack._affine_transform_sitk)
stack._history_motion_corrections = []
stack._history_motion_corrections.append(sitk.Euler3DTransform())
return stack
##
# Copy constructor
# \date 2019-01-15 16:55:09+0000
#
# \param cls The cls
# \param stack_to_copy Stack object to be copied
# \param filename The filename
#
# \return copied Stack object TODO: That's not really well done
#
@classmethod
def from_stack(cls, stack_to_copy, filename=None):
stack = cls()
if not isinstance(stack_to_copy, Stack):
raise ValueError("Input must be of type Stack. Given: %s" %
type(stack_to_copy))
# Copy image stack and mask
stack.sitk = sitk.Image(stack_to_copy.sitk)
stack.itk = sitkh.get_itk_from_sitk_image(stack.sitk)
stack._slice_thickness = stack_to_copy.get_slice_thickness()
stack.sitk_mask = sitk.Image(stack_to_copy.sitk_mask)
stack.itk_mask = sitkh.get_itk_from_sitk_image(stack.sitk_mask)
stack._is_unity_mask = stack_to_copy.is_unity_mask()
if filename is None:
stack._filename = stack_to_copy.get_filename()
else:
stack._filename = filename
stack._dir = stack_to_copy.get_directory()
stack._deleted_slices = stack_to_copy.get_deleted_slice_numbers()
# Store current affine transform of image
stack.set_registration_history(
stack_to_copy.get_registration_history())
# Extract all slices and their masks from the stack and store them if
# given
if stack_to_copy.get_slices() is not None:
stack._N_slices = stack_to_copy.get_number_of_slices()
stack._slices = [None] * stack._N_slices
slices_to_copy = stack_to_copy.get_slices()
for j, slice_j in enumerate(slices_to_copy):
stack._slices[j] = sl.Slice.from_slice(slice_j)
else:
stack._N_slices = 0
stack._slices = None
return stack
# @classmethod
# def from_Stack(cls, class_instance):
# data = copy.deepcopy(class_instance) # if deepcopy is necessary
# return cls(data)
# def __deepcopy__(self, memo):
# print '__deepcopy__(%s)' % str(memo)
# return Stack(copy.deepcopy(memo))
# def copy(self):
# return copy.deepcopy(self)
# Get all slices of current stack
# \return Array of sitk.Images containing slices in 3D space
def get_slices(self):
if self._slices is None:
return None
else:
return [s for s in self._slices if s is not None]
##
# Get one particular slice of current stack
# \date 2018-04-18 22:06:38-0600
#
# \param self The object
# \param index slice index as integer
#
# \return requested 3D slice of stack as Slice object
#
def get_slice(self, index):
index = int(index)
if abs(index) > self._N_slices - 1:
raise ValueError(
"Enter a valid index between -%s and %s. Tried: %s" %
(self._N_slices - 1, self._N_slices - 1, index))
return self._slices[index]
def get_slice_thickness(self):
return float(self._slice_thickness)
def get_inplane_resolution(self):
return float(self.sitk.GetSpacing()[0])
##
# Gets the deleted slice numbers, i.e. misregistered slice numbers detected
# by robust outlier algorithm. Indices refer to slice numbers within
# original stack
# \date 2018-07-08 23:06:24-0600
#
# \param self The object
#
# \return The deleted slice numbers as list of integers.
#
def get_deleted_slice_numbers(self):
return list(self._deleted_slices)
##
# Sets the slice.
# \date 2018-04-18 22:05:28-0600
#
# \param self The object
# \param slice slice as Slice object
# \param index slice index as integer
#
def set_slice(self, slice, index):
if not isinstance(slice, sl.Slice):
raise IOError("Input must be of type Slice")
index = int(index)
if abs(index) > self._N_slices - 1:
raise ValueError(
"Enter a valid index between -%s and %s. Tried: %s" %
(self._N_slices - 1, self._N_slices - 1, index))
self._slices[index] = slice
##
# Delete slice at given index
# \date 2017-12-01 00:38:56+0000
#
# Note that index refers to list index of slices (0 ... N_slices_current) whereas
# "deleted slice index" refers to actual slice number within original stack
#
# \param self The object
# \param index slice as Slice object to be deleted
#
def delete_slice(self, slice):
if not isinstance(slice, sl.Slice):
raise IOError("Input must be of type Slice")
# keep slice number (w.r.t. original stack)
self._deleted_slices.append(int(slice.get_slice_number()))
self._deleted_slices = sorted((self._deleted_slices))
# delete slice
index = self._slices.index(slice)
self._slices[index] = None
def get_deleted_slice_numbers(self):
return list(self._deleted_slices)
# Get name of directory where nifti was read from
# \return string of directory wher nifti was read from
# \bug Does not exist for all created instances! E.g. Stack.from_sitk_image
def get_directory(self):
return self._dir
def set_filename(self, filename):
self._filename = filename
slices = self.get_slices()
if slices is not None:
for s in slices:
s.set_filename(filename)
# Get filename of read/assigned nifti file (Stack.from_filename vs Stack.from_sitk_image)
# \return string of filename
def get_filename(self):
return self._filename
# Get history history of affine transforms, i.e. encoded spatial
# position+orientation of slice, and rigid motion estimates of slice
# obtained in the course of the registration/reconstruction process
# \return list of sitk.AffineTransform and sitk.Euler3DTransform objects
def get_registration_history(self):
affine_transforms = list(self._history_affine_transforms)
motion_corrections = list(self._history_motion_corrections)
return affine_transforms, motion_corrections
def set_registration_history(self, registration_history):
affine_transform_sitk = registration_history[0][-1]
self._update_affine_transform(affine_transform_sitk)
self._history_affine_transforms = [a for a in registration_history[0]]
self._history_motion_corrections = [t for t in registration_history[1]]
# Get number of slices of stack
# \return number of slices of stack
def get_number_of_slices(self):
return len(self.get_slices())
def is_unity_mask(self):
return self._is_unity_mask
# Display stack with external viewer (ITK-Snap)
# \param[in][in] show_segmentation display stack with or without associated segmentation (default=0)
def show(self, show_segmentation=0, label=None, viewer=VIEWER, verbose=True):
if label is None:
label = self._filename
if show_segmentation:
sitk_mask = self.sitk_mask
else:
sitk_mask = None
sitkh.show_sitk_image(
self.sitk,
label=label,
segmentation=sitk_mask,
viewer=viewer,
verbose=verbose)
def show_slices(self):
sitkh.plot_stack_of_slices(
self.sitk, cmap="Greys_r", title=self.get_filename())
# Write information of Stack to HDD to given directory:
# - sitk.Image object as entire volume
# - each single slice with its associated spatial transformation (optional)
# \param[in] directory string specifying where the output will be written to (default="/tmp/")
# \param[in] filename string specifying the filename. If not given the assigned one within Stack will be chosen.
# \param[in] write_slices boolean indicating whether each Slice of the stack shall be written (default=False)
def write(self,
directory,
filename=None,
write_stack=True,
write_mask=False,
write_slices=False,
write_transforms=False,
suffix_mask="_mask",
write_transforms_history=False,
):
# Create directory if not existing
ph.create_directory(directory)
# Construct filename
if filename is None:
filename = self._filename
full_file_name = os.path.join(directory, filename)
# Write file to specified location
if write_stack:
dw.DataWriter.write_image(self.sitk, "%s.nii.gz" % full_file_name)
# Write mask to specified location if given
if self.sitk_mask is not None:
# nda = sitk.GetArrayFromImage(self.sitk_mask)
# Write mask if it does not consist of only ones
if not self._is_unity_mask and write_mask:
dw.DataWriter.write_mask(
self.sitk_mask, "%s%s.nii.gz" % (full_file_name, suffix_mask))
if write_transforms:
stack_transform_sitk = self._history_motion_corrections[-1]
sitk.WriteTransform(
stack_transform_sitk,
os.path.join(directory, self.get_filename() + ".tfm")
)
# Write each separate Slice of stack (if they exist)
if write_slices or write_transforms:
try:
# Check whether variable exists
# if 'self._slices' not in locals() or all(i is None for i in
# self._slices):
if not hasattr(self, '_slices'):
raise ValueError(
"Error occurred in attempt to write %s.nii.gz: "
"No separate slices of object Slice are found" %
full_file_name)
# Write slices
else:
if write_transforms and write_slices:
ph.print_info(
"Write %s image slices and slice transforms to %s ... " % (
self.get_filename(), directory),
newline=False)
elif write_transforms and not write_slices:
ph.print_info(
"Write %s slice transforms to %s ... " % (
self.get_filename(), directory),
newline=False)
else:
ph.print_info(
"Write %s image slices to %s ... " % (
self.get_filename(), directory),
newline=False)
for slice in self.get_slices():
slice.write(
directory=directory,
filename=filename,
write_transform=write_transforms,
write_slice=write_slices,
suffix_mask=suffix_mask,
write_transforms_history=write_transforms_history,
)
print("done")
except ValueError as err:
print(err.message)
##
# Apply transform on stack and all its slices
# \date 2016-11-05 19:15:57+0000
#
# \param self The object
# \param affine_transform_sitk The affine transform sitk
#
def update_motion_correction(self, affine_transform_sitk):
# Update rigid motion estimate
current_rigid_motion_estimate = sitkh.get_composite_sitk_affine_transform(
affine_transform_sitk, self._history_motion_corrections[-1])
self._history_motion_corrections.append(current_rigid_motion_estimate)
# New affine transform of slice after rigid motion correction
affine_transform = sitkh.get_composite_sitk_affine_transform(
affine_transform_sitk, self._affine_transform_sitk)
# Update affine transform of stack, i.e. change image origin and
# direction in physical space
self._update_affine_transform(affine_transform)
# Update slices
if self.get_slices() is not None:
for i in range(0, self._N_slices):
self._slices[i].update_motion_correction(affine_transform_sitk)
##
# Apply transforms on all the slices of the stack. Stack itself
# is not getting transformed
# \date 2016-11-05 19:16:33+0000
#
# \param self The object
# \param affine_transforms_sitk List of sitk transform instances
#
def update_motion_correction_of_slices(self, affine_transforms_sitk):
if [type(affine_transforms_sitk) is list or type(affine_transforms_sitk) is np.array] \
and len(affine_transforms_sitk) is self._N_slices:
for i in range(0, self._N_slices):
self._slices[i].update_motion_correction(
affine_transforms_sitk[i])
else:
raise ValueError("Number of affine transforms does not match the "
"number of slices")
def _update_affine_transform(self, affine_transform_sitk):
# Ensure correct object type
self._affine_transform_sitk = sitk.AffineTransform(
affine_transform_sitk)
# Append transform to registration history
self._history_affine_transforms.append(affine_transform_sitk)
# Get origin and direction of transformed 3D slice given the new
# spatial transform
origin = sitkh.get_sitk_image_origin_from_sitk_affine_transform(
affine_transform_sitk, self.sitk)
direction = sitkh.get_sitk_image_direction_from_sitk_affine_transform(
affine_transform_sitk, self.sitk)
# Update image objects
self.sitk.SetOrigin(origin)
self.sitk.SetDirection(direction)
self.sitk_mask.SetOrigin(origin)
self.sitk_mask.SetDirection(direction)
self.itk.SetOrigin(origin)
self.itk.SetDirection(sitkh.get_itk_from_sitk_direction(direction))
self.itk_mask.SetOrigin(origin)
self.itk_mask.SetDirection(
sitkh.get_itk_from_sitk_direction(direction))
##
# Gets the resampled stack from slices.
# \date 2016-09-26 17:28:43+0100
#
# After slice-based registrations slice j does not correspond to the
# physical space of stack[:,:,j:j+1] anymore. With this method resample all
# containing slices to the physical space defined by the stack itself (or
# by a given resampling_pace). Overlapping slices get averaged.
#
# \param self The object
# \param resampling_grid Define the space to which the stack of
# slices shall be resampled; given as Stack
# object
# \param interpolator The interpolator
#
# \return resampled stack based on current position of slices as Stack
# object
#
def get_resampled_stack_from_slices(self, resampling_grid=None, interpolator="NearestNeighbor", default_pixel_value=0.0, filename=None):
# Choose interpolator
try:
interpolator_str = interpolator
interpolator = eval("sitk.sitk" + interpolator_str)
except:
raise ValueError("Error: interpolator is not known")
# Use resampling grid defined by original volumetric image
if resampling_grid is None:
resampling_grid = Stack.from_sitk_image(
image_sitk=self.sitk,
slice_thickness=self.get_slice_thickness(),
)
else:
# Use resampling grid defined by first slice (which might be
# shifted already)
if resampling_grid in ["on_first_slice"]:
stack_sitk = sitk.Image(self.sitk)
foo_sitk = sitk.Image(self._slices[0].sitk)
stack_sitk.SetDirection(foo_sitk.GetDirection())
stack_sitk.SetOrigin(foo_sitk.GetOrigin())
stack_sitk.SetSpacing(foo_sitk.GetSpacing())
resampling_grid = Stack.from_sitk_image(stack_sitk)
# Use resampling grid defined by given sitk.Image
elif type(resampling_grid) is sitk.Image:
resampling_grid = Stack.from_sitk_image(resampling_grid)
# Get shape of image data array
nda_shape = resampling_grid.sitk.GetSize()[::-1]
# Create zero image and its mask aligned with sitk.Image
nda = np.zeros(nda_shape)
stack_resampled_sitk = sitk.GetImageFromArray(nda)
stack_resampled_sitk.CopyInformation(resampling_grid.sitk)
stack_resampled_sitk = sitk.Cast(
stack_resampled_sitk, resampling_grid.sitk.GetPixelIDValue())
stack_resampled_sitk_mask = sitk.GetImageFromArray(nda.astype("uint8"))
stack_resampled_sitk_mask.CopyInformation(resampling_grid.sitk_mask)
stack_resampled_sitk_mask = sitk.Cast(
stack_resampled_sitk_mask, resampling_grid.sitk_mask.GetPixelIDValue())
# Create helper used for normalization at the end
nda_stack_covered_indices = np.zeros(nda_shape)
for i in range(0, self._N_slices):
slice = self._slices[i]
# Resample slice and its mask to stack space (volume)
stack_resampled_slice_sitk = sitk.Resample(
slice.sitk,
resampling_grid.sitk,
sitk.Euler3DTransform(),
interpolator,
default_pixel_value,
resampling_grid.sitk.GetPixelIDValue())
stack_resampled_slice_sitk_mask = sitk.Resample(
slice.sitk_mask,
resampling_grid.sitk_mask,
sitk.Euler3DTransform(),
sitk.sitkNearestNeighbor,
0,
resampling_grid.sitk_mask.GetPixelIDValue())
# Add resampled slice and mask to stack space
stack_resampled_sitk += stack_resampled_slice_sitk
stack_resampled_sitk_mask += stack_resampled_slice_sitk_mask
# Get indices which are updated in stack space
nda_stack_resampled_slice_ind = sitk.GetArrayFromImage(
stack_resampled_slice_sitk)
ind = np.nonzero(nda_stack_resampled_slice_ind)
# Increment counter for respective updated voxels
nda_stack_covered_indices[ind] += 1
# Set voxels with zero counter to 1 so as to have well-defined
# normalization
nda_stack_covered_indices[nda_stack_covered_indices == 0] = 1
# Normalize resampled image
stack_normalization = sitk.GetImageFromArray(nda_stack_covered_indices)
stack_normalization.CopyInformation(resampling_grid.sitk)
stack_normalization = sitk.Cast(
stack_normalization, resampling_grid.sitk.GetPixelIDValue())
stack_resampled_sitk /= stack_normalization
# Get valid binary mask
stack_resampled_slice_sitk_mask /= stack_resampled_slice_sitk_mask
if filename is None:
filename = self._filename + "_" + interpolator_str
stack = self.from_sitk_image(
image_sitk=stack_resampled_sitk,
filename=filename,
image_sitk_mask=stack_resampled_sitk_mask,
slice_thickness=stack_resampled_sitk.GetSpacing()[-1],
)
return stack
##
# Gets the resampled stack.
# \date 2016-12-02 17:05:10+0000
#
# \param self The object
# \param resampling_grid The resampling grid as SimpleITK image
# \param interpolator The interpolator
# \param default_pixel_value The default pixel value
#
# \return The resampled stack as Stack object
#
def get_resampled_stack(self, resampling_grid=None, spacing=None, interpolator="Linear", default_pixel_value=0.0, filename=None):
if (resampling_grid is None and spacing is None) or \
(resampling_grid is not None and spacing is not None):
raise IOError(
"Either 'resampling_grid' or 'spacing' must be specified")
# Get SimpleITK-interpolator
try:
interpolator_str = interpolator
interpolator = eval("sitk.sitk" + interpolator_str)
except:
raise ValueError(
"Error: interpolator is not known. "
"Must fit sitk.InterpolatorEnum format. "
"Possible examples include "
"'NearestNeighbor', 'Linear', or 'BSpline'.")
if resampling_grid is not None:
resampled_stack_sitk = sitk.Resample(
self.sitk,
resampling_grid,
sitk.Euler3DTransform(),
interpolator,
default_pixel_value,
self.sitk.GetPixelIDValue())
resampled_stack_sitk_mask = sitk.Resample(
self.sitk_mask,
resampling_grid,
sitk.Euler3DTransform(),
sitk.sitkNearestNeighbor,
0,
self.sitk_mask.GetPixelIDValue())
else:
resampler = simplereg.resampler.Resampler
resampled_stack_sitk = resampler.get_resampled_image_sitk(
image_sitk=self.sitk,
spacing=spacing,
interpolator=interpolator,
padding=default_pixel_value,
add_to_grid_unit="mm",
)
resampled_stack_sitk_mask = resampler.get_resampled_image_sitk(
image_sitk=self.sitk_mask,
spacing=spacing,
interpolator=sitk.sitkNearestNeighbor,
padding=0,
add_to_grid_unit="mm",
)
# Create Stack instance
if filename is None:
filename = self._filename + "_" + interpolator_str
stack = self.from_sitk_image(
image_sitk=resampled_stack_sitk,
slice_thickness=resampled_stack_sitk.GetSpacing()[-1],
filename=filename,
image_sitk_mask=resampled_stack_sitk_mask,
)
return stack
##
# Gets the stack multiplied with its mask. Rationale behind is to obtain
# "cleaner" looking HR images after the SRR step where motion-correction
# might have dispersed some slices
# \date 2017-05-26 13:50:39+0100
#
# \param self The object
# \param filename The filename
#
# \return The stack multiplied with its mask.
#
def get_stack_multiplied_with_mask(self, filename=None, mask_sitk=None):
if mask_sitk is None:
mask_sitk = self.sitk_mask
# Multiply stack with its mask
image_sitk = self.sitk * \
sitk.Cast(mask_sitk, self.sitk.GetPixelIDValue())
if filename is None:
filename = self.get_filename()
return Stack.from_sitk_image(
image_sitk=image_sitk,
filename=filename,
image_sitk_mask=mask_sitk,
slice_thickness=self.get_slice_thickness(),
)
# Get stack resampled on isotropic grid based on the actual position of
# its slices
# \param[in] resolution length of voxel side, scalar
# \return isotropically, resampled stack as Stack object
def get_isotropically_resampled_stack_from_slices(self, resolution=None, interpolator="NearestNeighbor", default_pixel_value=0.0, filename=None):
resampled_stack = self.get_resampled_stack_from_slices()
# Choose interpolator
try:
interpolator_str = interpolator
interpolator = eval("sitk.sitk" + interpolator_str)
except:
raise ValueError("Error: interpolator is not known")
# Read original spacing (voxel dimension) and size of target stack:
spacing = np.array(resampled_stack.sitk.GetSpacing())
size = np.array(resampled_stack.sitk.GetSize()).astype("int")
if resolution is None:
size_new = size
spacing_new = spacing
# Update information according to isotropic resolution
size_new[2] = np.round(
spacing[2] / spacing[0] * size[2]).astype("int")
spacing_new[2] = spacing[0]
else:
spacing_new = np.ones(3) * resolution
size_new = np.round(spacing / spacing_new * size).astype("int")
# For Python3: sitk.Resample in Python3 does not like np.int types!
size_new = [int(i) for i in size_new]
# Resample image and its mask to isotropic grid
isotropic_resampled_stack_sitk = sitk.Resample(
resampled_stack.sitk,
size_new,
sitk.Euler3DTransform(),
interpolator,
resampled_stack.sitk.GetOrigin(),
spacing_new,
resampled_stack.sitk.GetDirection(),
default_pixel_value,
resampled_stack.sitk.GetPixelIDValue())
isotropic_resampled_stack_sitk_mask = sitk.Resample(
resampled_stack.sitk_mask,
size_new,
sitk.Euler3DTransform(),
sitk.sitkNearestNeighbor,
resampled_stack.sitk.GetOrigin(),
spacing_new,
resampled_stack.sitk.GetDirection(),
0,
resampled_stack.sitk_mask.GetPixelIDValue())
# Create Stack instance
if filename is None:
filename = self._filename + "_" + interpolator_str + "Iso"
stack = self.from_sitk_image(
isotropic_resampled_stack_sitk, filename, isotropic_resampled_stack_sitk_mask)
return stack
##
# Gets the isotropically resampled stack.
# \date 2017-02-03 16:34:24+0000
#
# \param self The object
# \param resolution length of voxel side, scalar
# \param interpolator choose type of interpolator for
# resampling
# \param extra_frame additional extra frame of zero
# intensities surrounding the stack in mm
# \param filename Filename of resampled stack
# \param mask_dilation_radius The mask dilation radius
# \param mask_dilation_kernel The kernel in "Ball", "Box", "Annulus"
# or "Cross"
#
# \return The isotropically resampled stack.
#
def get_isotropically_resampled_stack(self, resolution=None, interpolator="Linear", extra_frame=0, filename=None, mask_dilation_radius=0, mask_dilation_kernel="Ball"):
# Choose interpolator
try:
interpolator_str = interpolator
interpolator = eval("sitk.sitk" + interpolator_str)
except:
raise ValueError("Error: interpolator is not known")
if resolution is None:
spacing = self.sitk.GetSpacing()[0]
else:
spacing = resolution
# Resample image and its mask to isotropic grid
resampler = simplereg.resampler.Resampler
isotropic_resampled_stack_sitk = resampler.get_resampled_image_sitk(
image_sitk=self.sitk,
spacing=spacing,
interpolator=interpolator,
padding=0.0,
add_to_grid=extra_frame,
add_to_grid_unit="mm",
)
isotropic_resampled_stack_sitk_mask = resampler.get_resampled_image_sitk(
image_sitk=self.sitk_mask,
spacing=spacing,
interpolator=sitk.sitkNearestNeighbor,
padding=0,
add_to_grid=extra_frame,
add_to_grid_unit="mm",
)
if mask_dilation_radius > 0:
dilater = sitk.BinaryDilateImageFilter()
dilater.SetKernelType(eval("sitk.sitk" + mask_dilation_kernel))
dilater.SetKernelRadius(mask_dilation_radius)
isotropic_resampled_stack_sitk_mask = dilater.Execute(
isotropic_resampled_stack_sitk_mask)
# Create Stack instance
if filename is None:
filename = self._filename + "_" + interpolator_str + "Iso"
stack = self.from_sitk_image(
image_sitk=isotropic_resampled_stack_sitk,
filename=filename,
slice_thickness=isotropic_resampled_stack_sitk.GetSpacing()[-1],
image_sitk_mask=isotropic_resampled_stack_sitk_mask,
)
return stack
# Increase stack by adding zero voxels in respective directions
# \remark Used for MS project to add empty slices on top of (chopped) brain
# \param[in] resolution length of voxel side, scalar
# \param[in] interpolator choose type of interpolator for resampling
# \param[in] extra_frame additional extra frame of zero intensities surrounding the stack in mm
# \return isotropically, resampled stack as Stack object
def get_increased_stack(self, extra_slices_z=0):
interpolator = sitk.sitkNearestNeighbor
# Read original spacing (voxel dimension) and size of target stack:
spacing = np.array(self.sitk.GetSpacing())
size = np.array(self.sitk.GetSize()).astype("int")
origin = np.array(self.sitk.GetOrigin())
direction = self.sitk.GetDirection()
# Update information according to isotropic resolution
size[2] += extra_slices_z
# Resample image and its mask to isotropic grid
default_pixel_value = 0.0
isotropic_resampled_stack_sitk = sitk.Resample(
self.sitk,
size,
sitk.Euler3DTransform(),
interpolator,
origin,
spacing,
direction,
default_pixel_value,
self.sitk.GetPixelIDValue())
isotropic_resampled_stack_sitk_mask = sitk.Resample(
self.sitk_mask,
size,
sitk.Euler3DTransform(),
sitk.sitkNearestNeighbor,
origin,
spacing,
direction,
0,
self.sitk_mask.GetPixelIDValue())
# Create Stack instance
stack = self.from_sitk_image(
isotropic_resampled_stack_sitk, "zincreased_" + self._filename, isotropic_resampled_stack_sitk_mask)
return stack
def get_cropped_stack_based_on_mask(self, boundary_i=0, boundary_j=0, boundary_k=0, unit="mm"):
# Get rectangular region surrounding the masked voxels
[x_range, y_range, z_range] = self._get_rectangular_masked_region(
self.sitk_mask)
if np.array([x_range, y_range, z_range]).all() is None:
raise RuntimeError(
"Cropping to bounding box of mask led to an empty image. "
"Check the image stack to see whether the region of interest "
"is presented in '%s'." % self._filename)
if unit == "mm":
spacing = self.sitk.GetSpacing()
boundary_i = np.round(boundary_i / float(spacing[0]))
boundary_j = np.round(boundary_j / float(spacing[1]))
boundary_k = np.round(boundary_k / float(spacing[2]))
shape = self.sitk.GetSize()
x_range[0] = np.max([0, x_range[0] - boundary_i])
x_range[1] = np.min([shape[0], x_range[1] + boundary_i])
y_range[0] = np.max([0, y_range[0] - boundary_j])
y_range[1] = np.min([shape[1], y_range[1] + boundary_j])
z_range[0] = np.max([0, z_range[0] - boundary_k])
z_range[1] = np.min([shape[2], z_range[1] + boundary_k])
# Crop to image region defined by rectangular mask
image_crop_sitk = self._crop_image_to_region(
self.sitk, x_range, y_range, z_range)
mask_crop_sitk = self._crop_image_to_region(
self.sitk_mask, x_range, y_range, z_range)
slice_numbers = range(z_range[0], z_range[1])
stack = self.from_sitk_image(
image_sitk=image_crop_sitk,
slice_thickness=self.get_slice_thickness(),
filename=self._filename,
image_sitk_mask=mask_crop_sitk,
slice_numbers=slice_numbers)
return stack
# Return rectangular region surrounding masked region.
# \param[in] mask_sitk sitk.Image representing the mask
# \return range_x pair defining x interval of mask in voxel space
# \return range_y pair defining y interval of mask in voxel space
# \return range_z pair defining z interval of mask in voxel space
def _get_rectangular_masked_region(self, mask_sitk):
spacing = np.array(mask_sitk.GetSpacing())
# Get mask array
nda = sitk.GetArrayFromImage(mask_sitk)
# Return in case no masked pixel available
if np.sum(abs(nda)) == 0:
return None, None, None
# Get shape defining the dimension in each direction
shape = nda.shape
# Compute sum of pixels of each slice along specified directions
sum_xy = np.sum(nda, axis=(0, 1)) # sum within x-y-plane
sum_xz = np.sum(nda, axis=(0, 2)) # sum within x-z-plane
sum_yz = np.sum(nda, axis=(1, 2)) # sum within y-z-plane
# Find masked regions (non-zero sum!)
range_x = np.zeros(2)
range_y = np.zeros(2)
range_z = np.zeros(2)
# Non-zero elements of numpy array nda defining x_range
ran = np.nonzero(sum_yz)[0]
range_x[0] = np.max([0, ran[0]])
range_x[1] = np.min([shape[0], ran[-1] + 1])
# Non-zero elements of numpy array nda defining y_range
ran = np.nonzero(sum_xz)[0]
range_y[0] = np.max([0, ran[0]])
range_y[1] = np.min([shape[1], ran[-1] + 1])
# Non-zero elements of numpy array nda defining z_range
ran = np.nonzero(sum_xy)[0]
range_z[0] = np.max([0, ran[0]])
range_z[1] = np.min([shape[2], ran[-1] + 1])
# Numpy reads the array as z,y,x coordinates! So swap them accordingly
return range_z.astype(int), range_y.astype(int), range_x.astype(int)
# Crop given image to region defined by voxel space ranges
# \param[in] image_sitk image which will be cropped
# \param[in] range_x pair defining x interval in voxel space for image cropping
# \param[in] range_y pair defining y interval in voxel space for image cropping
# \param[in] range_z pair defining z interval in voxel space for image cropping
# \return image cropped to defined region
def _crop_image_to_region(self, image_sitk, range_x, range_y, range_z):
image_cropped_sitk = image_sitk[
range_x[0]:range_x[1],
range_y[0]:range_y[1],
range_z[0]:range_z[1]
]
return image_cropped_sitk
# Burst the stack into its slices and return all slices of the stack
# return list of Slice objects
def _extract_slices(self, slice_thickness, slice_numbers=None):
slices = [None] * self._N_slices
if slice_numbers is None:
slice_numbers = range(0, self._N_slices)
if len(slice_numbers) != self._N_slices:
raise ValueError(
"slice_numbers must correspond to the number of slices "
"of the image volume")
# Extract slices and add masks
for i in range(0, self._N_slices):
slices[i] = sl.Slice.from_sitk_image(
slice_sitk=self.sitk[:, :, i:i + 1],
filename=self._filename,
slice_number=slice_numbers[i],
slice_sitk_mask=self.sitk_mask[:, :, i:i + 1],
slice_thickness=slice_thickness,
)
return slices
# Create a binary mask consisting of ones
# \return binary_mask as sitk.Image object consisting of ones
def _generate_identity_mask(self):
shape = sitk.GetArrayFromImage(self.sitk).shape
nda = np.ones(shape, dtype=np.uint8)
binary_mask = sitk.GetImageFromArray(nda)
binary_mask.CopyInformation(self.sitk)
return binary_mask
================================================
FILE: niftymic/definitions.py
================================================
import os
import sys
from pysitk.definitions import DIR_TMP
from pysitk.definitions import ITKSNAP_EXE
from pysitk.definitions import FSLVIEW_EXE
from pysitk.definitions import NIFTYVIEW_EXE
DIR_ROOT = os.path.realpath(
os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
DIR_TEST = os.path.join(DIR_ROOT, "data", "tests")
DIR_TEMPLATES = os.path.join(DIR_ROOT, "data", "templates")
DIR_CPP_BUILD = os.path.join(DIR_ROOT, "build", "cpp")
ALLOWED_EXTENSIONS = ["nii.gz", "nii"]
REGEX_FILENAMES = "[A-Za-z0-9+-_]+"
REGEX_FILENAME_EXTENSIONS = "(" + "|".join(ALLOWED_EXTENSIONS) + ")"
ALLOWED_INTERPOLATORS = ["NearestNeighbor", "Linear", "BSpline"]
TEMPLATES_INFO = "templates_info.json"
# Set default viewer
VIEWER = ITKSNAP_EXE
VIEWER_OPTIONS = ["itksnap", "fsleyes"]
V2V_METHOD_OPTIONS = ["FLIRT", "RegAladin"]
================================================
FILE: niftymic/reconstruction/__init__.py
================================================
================================================
FILE: niftymic/reconstruction/admm_solver.py
================================================
##
# \file admm_solver.py
# \brief Implementation to get an approximate solution of the TVL2-SRR
# problem via the Alternating Direction Method of Multipliers (ADMM)
# method.
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date July 2016
#
# Import libraries
import SimpleITK as sitk
import numpy as np
import nsol.admm_linear_solver as admm
import nsol.linear_operators as linop
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
from niftymic.reconstruction.solver import Solver
# This class implements the framework to iteratively solve
# \f$ \vec{y}_k = A_k \vec{x} \f$ for every slice \f$ \vec{y}_k,\,k=1,\dots,K \f$
# via TV-L2-regularization via an augmented least-square approach
# TODO
class ADMMSolver(Solver):
##
# Constructor
# \date 2016-08-01 22:57:21+0100
#
# \param self The object
# \param[in] stacks list of Stack objects containing
# all stacks used for the
# reconstruction
# \param[in,out] reconstruction Stack object containing the current
# estimate of the reconstruction
# volume (used as initial value +
# space definition)
# \param[in] alpha_cut Cut-off distance for Gaussian
# blurring filter
# \param[in] alpha regularization parameter, scalar
# \param[in] iter_max number of maximum iterations,
# scalar
# \param minimizer The minimizer
# \param deconvolution_mode The deconvolution mode
# \param predefined_covariance The predefined covariance
# \param[in] rho regularization parameter of
# augmented Lagrangian term, scalar
# \param[in] iterations number of ADMM iterations, scalar
# \param verbose The verbose
#
def __init__(self,
stacks,
reconstruction,
alpha_cut=3,
alpha=0.03,
iter_max=10,
minimizer="lsmr",
x_scale="max",
data_loss="linear",
data_loss_scale=1,
huber_gamma=1.345,
deconvolution_mode="full_3D",
predefined_covariance=None,
rho=0.5,
iterations=10,
use_masks=1,
verbose=1,
):
# Run constructor of superclass
Solver.__init__(self,
stacks=stacks,
reconstruction=reconstruction,
alpha=alpha,
alpha_cut=alpha_cut,
iter_max=iter_max,
minimizer=minimizer,
x_scale=x_scale,
data_loss=data_loss,
data_loss_scale=data_loss_scale,
huber_gamma=huber_gamma,
deconvolution_mode=deconvolution_mode,
predefined_covariance=predefined_covariance,
use_masks=use_masks,
verbose=verbose,
)
# Settings for optimizer
self._rho = rho
self._iterations = iterations
# Set regularization parameter used for augmented Lagrangian in TV-L2 regularization
# \[$
# \sum_{k=1}^K \frac{1}{2} \Vert y_k - A_k x \Vert_{\ell^2}^2 + \alpha\,\Psi(x)
# + \mu \cdot (\nabla x - v) + \frac{\rho}{2} \Vert \nabla x - v \Vert_{\ell^2}^2
# \]$
# \param[in] rho regularization parameter of augmented Lagrangian term, scalar
def set_rho(self, rho):
self._rho = rho
# Get regularization parameter used for augmented Lagrangian in TV-L2 regularization
# \return regularization parameter of augmented Lagrangian term, scalar
def get_rho(self):
return self._rho
# Set ADMM iterations to solve TV-L2 reconstruction problem
# \[$
# \sum_{k=1}^K \frac{1}{2} \Vert y_k - A_k x \Vert_{\ell^2}^2 + \alpha\,\Psi(x)
# + \mu \cdot (\nabla x - v) + \frac{\rho}{2} \Vert \nabla x - v \Vert_{\ell^2}^2
# \]$
# \param[in] iterations number of ADMM iterations, scalar
def set_iterations(self, iterations):
self._iterations = iterations
# Get chosen value of ADMM iterations to solve TV-L2 reconstruction problem
# \return number of ADMM iterations, scalar
def get_iterations(self):
return self._iterations
##
# Gets the setting specific filename indicating the information
# used for the reconstruction step
# \date 2016-11-17 15:41:58+0000
#
# \param self The object
# \param prefix The prefix as string
#
# \return The setting specific filename as string.
#
def get_setting_specific_filename(self, prefix="SRR_"):
# Build filename
filename = prefix
filename += "stacks" + str(len(self._stacks))
if self._alpha > 0:
filename += "_ADMM"
filename += "_TV"
filename += "_" + self._minimizer
filename += "_alpha" + str(self._alpha)
filename += "_itermax" + str(self._iter_max)
filename += "_rho" + str(self._rho)
filename += "_ADMMiterations" + str(self._iterations)
# Replace dots by 'p'
filename = filename.replace(".", "p")
return filename
def get_solver(self):
# Get operators
A = self.get_A()
A_adj = self.get_A_adj()
b = self.get_b()
x0 = self.get_x0()
x_scale = self.get_x_scale()
spacing = np.array(self._reconstruction.sitk.GetSpacing())
linear_operators = linop.LinearOperators3D(spacing=spacing)
grad, grad_adj = linear_operators.get_gradient_operators()
X_shape = self._reconstruction_shape
Z_shape = grad(x0.reshape(*X_shape)).shape
B = lambda x: grad(x.reshape(*X_shape)).flatten()
B_adj = lambda x: grad_adj(x.reshape(*Z_shape)).flatten()
# Set up solver
solver = admm.ADMMLinearSolver(
dimension=3,
A=A, A_adj=A_adj,
B=B, B_adj=B_adj,
b=b,
x0=x0,
x_scale=x_scale,
alpha=self._alpha,
data_loss=self._data_loss,
minimizer=self._minimizer,
iter_max=self._iter_max,
rho=self._rho,
iterations=self._iterations,
verbose=self._verbose,
)
return solver
##
# Reconstruct volume using TV-L2 regularization via Alternating
# Direction Method of Multipliers (ADMM) method.
# \post self._reconstruction is updated with new volume and can be fetched
# via \p get_recon
# \date 2016-08-01 23:22:50+0100
#
# \param self The object
# \param estimate_initial_value Estimate initial value by running one
# first-order Tikhonov reconstruction
# step prior the ADMM algorithm
#
def _run(self):
solver = self.get_solver()
self._print_info_text()
# Run reconstruction
solver.run()
# Get computational time
self._computational_time = solver.get_computational_time()
# Update volume
self._reconstruction.itk = self._get_itk_image_from_array_vec(
solver.get_x(), self._reconstruction.itk)
self._reconstruction.sitk = sitkh.get_sitk_from_itk_image(
self._reconstruction.itk)
def _print_info_text(self):
ph.print_subtitle("ADMM Solver:")
ph.print_info("Chosen regularization type: TV")
ph.print_info("Regularization parameter alpha: " + str(self._alpha))
ph.print_info(
"Regularization parameter of augmented Lagrangian term rho: " + str(self._rho))
ph.print_info("Number of ADMM iterations: " + str(self._iterations))
ph.print_info(
"Maximum number of TK1 solver iterations: " + str(self._iter_max))
# ph.print_info("Tolerance: %.0e" %(self._tolerance))
================================================
FILE: niftymic/reconstruction/linear_operators.py
================================================
##
# \file linear_operators.py
# \brief Implementation of linear operations associated with the physical
# slice acquisition model.
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date October 2017
# Import libraries
import itk
import numpy as np
import pysitk.simple_itk_helper as sitkh
import niftymic.base.psf as psf
import niftymic.base.slice as sl
import niftymic.base.stack as st
##
# Class implementing linear operations associated with the physical slice
# acquisition model
# \date 2017-11-28 22:25:27+0000
#
class LinearOperators(object):
##
# Store relevant information
# \date 2017-11-01 16:29:41+0000
#
# \param self The object
# \param deconvolution_mode Either "full_3D" or "only_in_plane".
# Indicates whether full 3D or only
# in-plane deconvolution is considered
# \param predefined_covariance Either only diagonal entries
# (sigma_x2, sigma_y2, sigma_z2) or as
# full 3x3 numpy array
# \param alpha_cut Cut-off distance for Gaussian blurring
# filter
# \param image_type itk.Image type
# \param default_pixel_type The default pixel type for resampling
#
def __init__(self,
deconvolution_mode="full_3D",
predefined_covariance=None,
alpha_cut=3,
image_type=itk.Image.D3,
default_pixel_type=0.0,
):
self._deconvolution_mode = deconvolution_mode
# In case only diagonal entries are given, create diagonal matrix
if predefined_covariance is not None:
if predefined_covariance.size is 3:
self._predefined_covariance = np.diag(
np.array(predefined_covariance))
else:
self._predefined_covariance = np.array(predefined_covariance)
self._psf = psf.PSF()
# Allocate and initialize Oriented Gaussian Interpolate Image Filter
self._filter_oriented_gaussian = \
itk.OrientedGaussianInterpolateImageFilter[
image_type, image_type].New()
self._filter_oriented_gaussian.SetDefaultPixelValue(default_pixel_type)
self._filter_oriented_gaussian.SetAlpha(alpha_cut)
# Allocate and initialize Adjoint Oriented Gaussian Interpolate Image
# Filter
self._filter_adjoint_oriented_gaussian = \
itk.AdjointOrientedGaussianInterpolateImageFilter[
image_type, image_type].New()
self._filter_adjoint_oriented_gaussian.SetDefaultPixelValue(
default_pixel_type)
self._filter_adjoint_oriented_gaussian.SetAlpha(alpha_cut)
# Allocate and initialize masking image filter
self._masking = itk.MultiplyImageFilter[
image_type, image_type, image_type].New()
self._get_covariance = {
"full_3D": self._get_covariance_full_3d,
"only_in_plane": self._get_covariance_only_in_plane,
"predefined_covariance": self._get_covariance_predefined,
}
##
# Perform forward operation on reconstruction image, i.e.
# \f$y = D B x =: A(x)
# \f$ with
# \f$ D\f$ and
# \f$ B
# \f$ being the downsampling and blurring operators, respectively.
# \date 2017-10-31 23:36:18+0000
#
# \param self The object
# \param reconstruction_itk Reconstruction image as itk.Image object
# \param slice_itk Slice image as itk.Image object. Required
# to define output space and orientation
# for PSF.
# \param slice_spacing Slice spacing as list/array that holds
# [in-plane x, in-plane y, slice-thickness]
# resolution information. Required to
# estimate Gaussian blurring.
#
# \return Image A(x) as itk.Image object in slice_itk image space
#
def A_itk(self, reconstruction_itk, slice_itk, slice_spacing):
# Get covariance describing PSF orientation of slice in reconstruction
# space
cov = self._get_covariance[self._deconvolution_mode](
reconstruction_itk, slice_itk, slice_spacing)
reconstruction_itk.Update()
self._filter_oriented_gaussian.SetCovariance(cov.flatten())
self._filter_oriented_gaussian.SetInput(reconstruction_itk)
self._filter_oriented_gaussian.SetOutputParametersFromImage(slice_itk)
self._filter_oriented_gaussian.UpdateLargestPossibleRegion()
self._filter_oriented_gaussian.Update()
A_itk_reconstruction = self._filter_oriented_gaussian.GetOutput()
A_itk_reconstruction.DisconnectPipeline()
return A_itk_reconstruction
##
# Perform forward operation using Stack/Slice objects.
#
# If reconstruction holds a (non-unity) mask, it is mapped to the
# Stack/Slice objects as well using standard interpolation techniques.
# \date 2017-11-01 19:35:08+0000
#
# \param self The object
# \param reconstruction Reconstruction image as Stack object
# \param stack_slice Slice image as Slice object. Required to
# define output space and orientation for
# PSF.
# \param interpolator_mask Interpolator used for resampling
# reconstruction mask (if given) to
# Stack/Slice object space as string.
# Examples are "NearestNeighbor", or
# "Linear".
#
# \return Image A(x) as Slice object in slice image space
#
def A(self, reconstruction, stack_slice, interpolator_mask="Linear"):
# Get slice spacing relevant for Gaussian blurring estimate
in_plane_res = stack_slice.get_inplane_resolution()
slice_thickness = stack_slice.get_slice_thickness()
slice_spacing = np.array([in_plane_res, in_plane_res, slice_thickness])
simulated_itk = self.A_itk(
reconstruction_itk=reconstruction.itk,
slice_itk=stack_slice.itk,
slice_spacing=slice_spacing,
)
simulated_sitk = sitkh.get_sitk_from_itk_image(simulated_itk)
# Update stack/slice mask, in case provided for reconstruction
if not reconstruction.is_unity_mask():
slice_tmp = reconstruction.get_resampled_stack(
stack_slice.sitk, interpolator=interpolator_mask)
simulated_sitk_mask = slice_tmp.sitk_mask
# PSF-aware resampling omitted as results less plausible for mask
# simulated_itk_mask = self.A_itk(
# reconstruction.itk_mask, stack_slice.itk_mask)
# simulated_sitk_mask = sitkh.get_sitk_from_itk_image(
# simulated_itk_mask)
else:
simulated_sitk_mask = None
if isinstance(stack_slice, sl.Slice):
simulated = sl.Slice.from_sitk_image(
slice_sitk=simulated_sitk,
slice_number=stack_slice.get_slice_number(),
filename=stack_slice.get_filename(),
slice_sitk_mask=simulated_sitk_mask,
slice_thickness=stack_slice.get_slice_thickness(),
)
elif isinstance(stack_slice, st.Stack):
simulated = st.Stack.from_sitk_image(
image_sitk=simulated_sitk,
image_sitk_mask=simulated_sitk_mask,
filename=stack_slice.get_filename(),
slice_thickness=stack_slice.get_slice_thickness(),
)
return simulated
##
# Perform backward operation on slice image, i.e.
# \f$z = B^* D^* y =: A^*(y)
# \f$ with
# \f$ D^*
# \f$ and
# \f$ B^*
# \f$ being the adjoint downsampling and blurring operators, respectively.
# \date 2017-10-31 23:44:41+0000
#
# \param self The object
# \param slice_itk Slice image as itk.Image object
# \param reconstruction_itk Reconstruction image as itk.Image object.
# Required to define output space and
# orientation for PSF
# \param slice_spacing Slice spacing as list/array that holds
# [in-plane x, in-plane y, slice-thickness]
# resolution information. Required to
# estimate Gaussian blurring.
#
# \return Image A^*(y) as itk.Image object in reconstruction_itk image
# space
#
def A_adj_itk(self, slice_itk, reconstruction_itk, slice_spacing):
# Get covariance describing PSF orientation of slice in reconstruction
# space
cov = self._get_covariance[self._deconvolution_mode](
reconstruction_itk, slice_itk, slice_spacing)
reconstruction_itk.Update()
self._filter_adjoint_oriented_gaussian.SetCovariance(cov.flatten())
self._filter_adjoint_oriented_gaussian.SetInput(slice_itk)
self._filter_adjoint_oriented_gaussian.SetOutputParametersFromImage(
reconstruction_itk)
self._filter_adjoint_oriented_gaussian.UpdateLargestPossibleRegion()
self._filter_adjoint_oriented_gaussian.Update()
A_adj_itk_slice = self._filter_adjoint_oriented_gaussian.GetOutput()
A_adj_itk_slice.DisconnectPipeline()
return A_adj_itk_slice
##
# Perform masking operation on itk.Image object
# \date 2017-10-31 23:59:00+0000
#
# \param self The object
# \param image_itk Image as itk.Image object
# \param image_itk_mask Image mask as itk.Image object
#
# \return Masked image as itk.Image object
#
def M_itk(self, image_itk, image_itk_mask):
self._masking.SetInput1(image_itk_mask)
self._masking.SetInput2(image_itk)
self._masking.UpdateLargestPossibleRegion()
self._masking.Update()
Mk_slice_itk = self._masking.GetOutput()
Mk_slice_itk.DisconnectPipeline()
return Mk_slice_itk
def _get_covariance_full_3d(
self,
reconstruction_itk,
slice_itk,
slice_spacing,
):
reconstruction_direction_sitk = sitkh.get_sitk_from_itk_direction(
reconstruction_itk.GetDirection())
slice_direction_sitk = sitkh.get_sitk_from_itk_direction(
slice_itk.GetDirection())
cov = self._psf.get_covariance_matrix_in_reconstruction_space_sitk(
reconstruction_direction_sitk=reconstruction_direction_sitk,
slice_direction_sitk=slice_direction_sitk,
slice_spacing=slice_spacing)
return cov
def _get_covariance_only_in_plane(
self,
reconstruction_itk,
slice_itk,
slice_spacing,
):
reconstruction_direction_sitk = sitkh.get_sitk_from_itk_direction(
reconstruction_itk.GetDirection())
slice_direction_sitk = sitkh.get_sitk_from_itk_direction(
slice_itk.GetDirection())
# Get spacing of slice and set it very small so that the corresponding
# covariance is negligibly small in through-plane direction. Hence,
# only in-plane deconvolution is approximated
slice_spacing[2] = 1e-6
cov = self._psf.get_covariance_matrix_in_reconstruction_space_sitk(
reconstruction_direction_sitk=reconstruction_direction_sitk,
slice_direction_sitk=slice_direction_sitk,
slice_spacing=slice_spacing)
return cov
def _get_covariance_predefined(
self,
reconstruction_itk,
slice_itk,
slice_spacing=None,
):
if slice_spacing is not None:
raise ValueError(
"Slice spacing cannot be specified for predefined covariance "
"use.")
reconstruction_direction_sitk = sitkh.get_sitk_from_itk_direction(
reconstruction_itk.GetDirection())
slice_direction_sitk = sitkh.get_sitk_from_itk_direction(
slice_itk.GetDirection())
cov = \
self._psf.get_predefined_covariance_matrix_in_reconstruction_space(
reconstruction_direction_sitk=reconstruction_direction_sitk,
slice_direction_sitk=slice_direction_sitk,
cov=self._predefined_covariance)
return cov
================================================
FILE: niftymic/reconstruction/primal_dual_solver.py
================================================
##
# \file primal_dual_solver.py
# \brief Solve reconstruction problem A_k x = y_k for all slices k via
# Primal-Dual solver.
#
# Implementation to get an approximate solution of the inverse problem
# \f$ y_k = A_k x
# \f$ for each slice
# \f$ y_k,\,k=1,\dots,K
# \f$ by using first-order primal-dual algorithms for convex problems as
# introduced in Chambolle, A. & Pock, T., 2011. A First-Order Primal-Dual
# Algorithm for Convex Problems with Applications to Imaging. Journal of
# Mathematical Imaging and Vision, 40(1), pp.120-145.
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date July 2017
#
# Import libraries
import numpy as np
import nsol.linear_operators as linop
import nsol.primal_dual_solver as pd
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
from niftymic.reconstruction.solver import Solver
from nsol.proximal_operators import ProximalOperators as prox
# This class implements the framework to iteratively solve
# \f$ \vec{y}_k = A_k \vec{x} \f$ for every slice \f$ \vec{y}_k,\,k=1,\dots,K \f$
# via first-order primal dual algorithms.
# TODO
class PrimalDualSolver(Solver):
def __init__(self,
stacks,
reconstruction,
alpha=0.03,
alpha_cut=3,
iter_max=10,
minimizer="lsmr",
x_scale="max",
data_loss="linear",
data_loss_scale=1,
huber_gamma=1.345,
deconvolution_mode="full_3D",
predefined_covariance=None,
reg_type="TV",
reg_huber_gamma=0.05,
iterations=10,
alg_type="ALG2",
use_masks=1,
verbose=0,
):
super(self.__class__, self).__init__(
stacks=stacks,
reconstruction=reconstruction,
alpha=alpha,
alpha_cut=alpha_cut,
iter_max=iter_max,
minimizer=minimizer,
x_scale=x_scale,
data_loss=data_loss,
data_loss_scale=data_loss_scale,
huber_gamma=huber_gamma,
deconvolution_mode=deconvolution_mode,
predefined_covariance=predefined_covariance,
use_masks=use_masks,
verbose=verbose,
)
# regularization type
self._reg_type = reg_type
# number of primal-dual iterations
self._iterations = iterations
# parameter used for Huber regularizer
self._reg_huber_gamma = reg_huber_gamma
# define method to update parameter
self._alg_type = alg_type
def get_setting_specific_filename(self, prefix="SRR_"):
# Build filename
filename = prefix
filename += "stacks" + str(len(self._stacks))
if self._alpha > 0:
filename += "_PrimalDual"
filename += "_" + self._reg_type
if self._reg_type == "huber":
filename += "_gamma" + str(self._reg_huber_gamma)
# filename += "_" + self._alg_type
filename += "_" + self._minimizer
if self._data_loss not in ["linear"] or self._minimizer in ["L-BFGS-B"]:
filename += "_" + self._data_loss
if self._data_loss in ["huber"]:
filename += str(self._huber_gamma)
filename += "_alpha" + str(self._alpha)
filename += "_itermax" + str(self._iter_max)
filename += "_PDiterations" + str(self._iterations)
# Replace dots by 'p'
filename = filename.replace(".", "p")
return filename
def get_solver(self):
if self._reg_type not in ["TV", "huber"]:
raise ValueError("Error: regularization type can only be either "
"'TV' or 'huber'")
# L^2 = ||K||^2 = ||\nabla||^2 = ||div||^2 <= 16/h^2 in 3D
# However, it seems that the smaller L2 the bigger the effect of TV
# regularization. Try, e.g. L2 = 1.
L2 = 16. / self._reconstruction.sitk.GetSpacing()[0]**2
# Get operators
A = self.get_A()
A_adj = self.get_A_adj()
b = self.get_b()
x0 = self.get_x0()
x_scale = self.get_x_scale()
spacing = np.array(self._reconstruction.sitk.GetSpacing())
linear_operators = linop.LinearOperators3D(spacing=spacing)
grad, grad_adj = linear_operators.get_gradient_operators()
X_shape = self._reconstruction_shape
Z_shape = grad(x0.reshape(*X_shape)).shape
B = lambda x: grad(x.reshape(*X_shape)).flatten()
B_adj = lambda x: grad_adj(x.reshape(*Z_shape)).flatten()
prox_f = lambda x, tau: prox.prox_linear_least_squares(
x=x, tau=tau,
A=A, A_adj=A_adj,
b=b, x0=x0,
iter_max=self._iter_max,
x_scale=x_scale,
data_loss=self._data_loss,
data_loss_scale=self._data_loss_scale,
minimizer=self._minimizer,
verbose=self._verbose)
if self._reg_type == "TV":
prox_g_conj = prox.prox_tv_conj
elif self._reg_type == "huber":
prox_g_conj = lambda x, sigma: prox.prox_huber_conj(
x, sigma, self._reg_huber_gamma)
# Set up solver
solver = pd.PrimalDualSolver(
prox_f=prox_f,
prox_g_conj=prox_g_conj,
B=B,
B_conj=B_adj,
L2=L2,
x0=x0,
x_scale=x_scale,
alpha=self._alpha,
iterations=self._iterations,
verbose=self._verbose,
alg_type=self._alg_type,
)
return solver
def _run(self, verbose=0):
solver = self.get_solver()
self._print_info_text()
# Run reconstruction
solver.run()
# Get computational time
self._computational_time = solver.get_computational_time()
# Update volume
self._reconstruction.itk = self._get_itk_image_from_array_vec(
solver.get_x(), self._reconstruction.itk)
self._reconstruction.sitk = sitkh.get_sitk_from_itk_image(
self._reconstruction.itk)
def _print_info_text(self):
ph.print_subtitle("Primal-Dual Solver:")
ph.print_info("Chosen regularization type: %s" %
(self._reg_type), newline=False)
if self._reg_type == "huber":
print(" (gamma = %g)" % (self._reg_huber_gamma))
else:
print("")
ph.print_info("Strategy for parameter update: %s"
% (self._alg_type))
ph.print_info(
"Regularization parameter alpha: %g" % (self._alpha))
if self._data_loss in ["huber"]:
ph.print_info("Loss function: %s (gamma = %g)" %
(self._data_loss, self._huber_gamma))
else:
ph.print_info("Loss function: %s" % (self._data_loss))
ph.print_info("Number of Primal-Dual iterations: %d" %
(self._iterations))
ph.print_info("Minimizer: %s" % (self._minimizer))
ph.print_info(
"Maximum number of iterations: %d" % (self._iter_max))
================================================
FILE: niftymic/reconstruction/scattered_data_approximation.py
================================================
##
# \file ScatteredDataApproximation.py
# \brief Implementation of two different approaches for Scattered Data
# Approximation (SDA)
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date April 2016
#
import os
import sys
import itk
import time
import numpy as np
import SimpleITK as sitk
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
import niftymic.base.stack as st
import niftymic.utilities.binary_mask_from_mask_srr_estimator as bm
# Class implementing Scattered Data Approximation
class ScatteredDataApproximation:
##
# Constructor
# \date 2017-07-12 15:48:33+0100
#
# \param self The object
# \param stacks list of Stack objects containing all stacks
# used for the reconstruction
# \param[in,out] HR_volume Stack object containing the current estimate
# of the HR volume (required for defining HR
# space)
# \param sigma Sigma is measured in the units of image
# spacing
# \param sigma_array Sigma is measured in the units of image
# spacing; set sigma_array if you need
# different values along each axis
# \post HR_volume is updated with current volumetric estimate
#
def __init__(self,
stacks,
HR_volume,
sigma=1,
sigma_array=None,
use_masks=False,
sda_mask=False,
verbose=True,
):
# Initialize variables
self._stacks = stacks
self._N_stacks = len(stacks)
self._HR_volume = HR_volume
self._use_masks = use_masks
self._sda_mask = sda_mask
self._verbose = verbose
self._get_slice = {
# (use_mask, sda_mask)
(False, False): self._get_image_slice,
(True, False): self._get_masked_image_slice,
(False, True): self._get_mask_slice,
(True, True): self._get_mask_slice,
}
# Define sigma for recursive smoothing filter
if sigma_array is None:
self._sigma_array = np.ones(3) * sigma
elif len(sigma_array) is not 3:
raise ValueError("Error: Sigma array must contain 3 elements")
else:
self._sigma_array = np.array(sigma_array)
# Define dictionary to choose computational approach for SDA
self._run = {
"Shepard-YVV": self._run_discrete_shepard_reconstruction,
"Shepard-Deriche": self._run_discrete_shepard_based_on_Deriche_reconstruction,
}
self._sda_approach = "Shepard-YVV" # default approximation approach
# Set sigma used for recursive Gaussian smoothing. Same sigma is used
# in each axis, i.e. isotropic smoothing is applied. Sigma is measured
# in the units of image spacing.
# \param[in] sigma, scalar
def set_sigma(self, sigma):
self._sigma_array = np.ones(3) * sigma
def set_stacks(self, stacks):
self._stacks = stacks
self._N_stacks = len(stacks)
# ## Get sigma used for recursive Gaussian smoothing.
# # \return sigma array, numpy array
# def get_sigma(self):
# return self._sigma_array
# Set array of standard deviations used for recursive Gaussian smoothing
# in each direction. Sigmas are measured in the units of image spacing.
# You may use the method SetSigma to set the same value across each axis or
# use the method SetSigmaArray if you need different values along each axis
# \param[in] sigma_array 3D array containing the standard deviation in each direction
def set_sigma_array(self, sigma_array):
if len(sigma_array) is not 3:
raise ValueError("Error: Sigma array must contain 3 elements")
self._sigma_array = np.array(sigma_array)
# Get array of standard deviations used for recursive Gaussian smoothing
# in each direction. Sigmas are measured in the units of image spacing.
# \return sigma array, numpy array
def get_sigma_array(self):
return self._sigma_array
# Set approach for approximating the HR volume. It can be either
# 'Shepard-YVV' or 'Shepard-Deriche'
# \param[in] sda_approach either 'Shepard-YVV' or 'Shepard-Deriche', string
def set_approach(self, sda_approach):
if sda_approach not in ["Shepard-YVV", "Shepard-Deriche"]:
raise ValueError(
"Error: SDA approach can only be either 'Shepard-YVV' or 'Shepard-Deriche'")
self._sda_approach = sda_approach
# Get chosen type of regularization.
# \return regularization type as string
def get_approach(self):
return self._sda_approach
# Get current estimate of HR volume
# \return current estimate of HR volume, instance of Stack
def get_reconstruction(self):
return self._HR_volume
def get_setting_specific_filename(self, prefix="SDA_"):
# Build filename
filename = prefix
filename += "stacks" + str(len(self._stacks))
# Only prints the first entry, i.e. assumes identical sigmas
filename += "_sigma" + str(self._sigma_array[0])
# Replace dots by 'p'
filename = filename.replace(".", "p")
return filename
##
# Gets the computational time it took to obtain the numerical estimate.
# \date 2017-07-20 23:40:17+0100
#
# \param self The object
#
# \return The computational time as string
#
def get_computational_time(self):
return self._computational_time
# Computed reconstructed volume based on current estimated positions of
# slices
def run(self):
ph.print_info("Chosen SDA approach: " + self._sda_approach)
ph.print_info("Smoothing parameter sigma = " + str(self._sigma_array))
time_start = ph.start_timing()
self._run[self._sda_approach]()
# Get computational time
self._computational_time = ph.stop_timing(time_start)
if self._verbose:
ph.print_info("Required computational time: %s" %
(self.get_computational_time()))
# print("Elapsed time for SDA: %s seconds" %(time_elapsed))
##
# Add mask based on union of all masks
# \date 2017-02-03 16:46:33+0000
#
# \param self The object
# \param mask_dilation_radius The mask dilation radius
# \param mask_dilation_kernel The kernel in "Ball", "Box", "Annulus"
# or "Cross"
#
def generate_mask_from_stack_mask_unions(self,
mask_dilation_radius=0,
mask_dilation_kernel="Ball",
):
# Define helpers to obtain averaged stack
shape = sitk.GetArrayFromImage(self._HR_volume.sitk).shape
array_mask = np.zeros(shape, dtype=np.uint8)
# Average over domain specified by the joint mask ("union mask")
for i in range(0, self._N_stacks):
# Resample warped stack masks
stack_sitk_mask = sitk.Resample(
self._stacks[i].sitk_mask,
self._HR_volume.sitk_mask,
sitk.Euler3DTransform(),
sitk.sitkNearestNeighbor,
0,
self._HR_volume.sitk_mask.GetPixelIDValue())
# Get arrays of resampled warped stack and mask
array_mask_tmp = sitk.GetArrayFromImage(
stack_sitk_mask).astype(np.uint8)
# Sum intensities of stack and mask
array_mask += array_mask_tmp
# Create (joint) binary mask. Mask represents union of all masks
array_mask[array_mask > 0] = 1
HR_volume_mask_sitk = sitk.GetImageFromArray(array_mask)
HR_volume_mask_sitk.CopyInformation(self._HR_volume.sitk)
if mask_dilation_radius > 0:
dilater = sitk.BinaryDilateImageFilter()
dilater.SetKernelType(eval("sitk.sitk" + mask_dilation_kernel))
dilater.SetKernelRadius(mask_dilation_radius)
HR_volume_mask_sitk = dilater.Execute(HR_volume_mask_sitk)
self._HR_volume = st.Stack.from_sitk_image(
image_sitk=self._HR_volume.sitk,
filename=self._HR_volume.get_filename(),
image_sitk_mask=HR_volume_mask_sitk,
slice_thickness=self._HR_volume.get_slice_thickness(),
)
##
# Add mask based on union of intersection of masks
# \date 2017-02-03 16:46:33+0000
#
# \param self The object
# \param mask_dilation_radius The mask dilation radius
# \param mask_dilation_kernel The kernel in "Ball", "Box", "Annulus"
# or "Cross"
def generate_mask_from_stack_mask_intersections(self,
mask_dilation_radius=0,
mask_dilation_kernel="Ball",
):
# Define helpers to obtain averaged stack
shape = sitk.GetArrayFromImage(self._HR_volume.sitk).shape
array_mask = np.ones(shape, dtype=np.uint8)
# Average over domain specified by the joint mask ("union mask")
for i in range(0, self._N_stacks):
# Resample warped stack masks
stack_sitk_mask = sitk.Resample(
self._stacks[i].sitk_mask,
self._HR_volume.sitk_mask,
sitk.Euler3DTransform(),
sitk.sitkNearestNeighbor,
0,
self._HR_volume.sitk_mask.GetPixelIDValue())
# Get arrays of resampled warped stack and mask
array_mask_tmp = sitk.GetArrayFromImage(
stack_sitk_mask).astype(np.uint8)
# Sum intensities of stack and mask
array_mask *= array_mask_tmp
# Create (joint) binary mask. Mask represents union of all masks
array_mask[array_mask > 0] = 1
HR_volume_mask_sitk = sitk.GetImageFromArray(array_mask)
HR_volume_mask_sitk.CopyInformation(self._HR_volume.sitk)
if mask_dilation_radius > 0:
dilater = sitk.BinaryDilateImageFilter()
dilater.SetKernelType(eval("sitk.sitk" + mask_dilation_kernel))
dilater.SetKernelRadius(mask_dilation_radius)
HR_volume_mask_sitk = dilater.Execute(HR_volume_mask_sitk)
self._HR_volume = st.Stack.from_sitk_image(
image_sitk=self._HR_volume.sitk,
filename=self._HR_volume.get_filename(),
image_sitk_mask=HR_volume_mask_sitk,
slice_thickness=self._HR_volume.get_slice_thickness(),
)
@staticmethod
def _get_image_slice(slice):
return slice.sitk
@staticmethod
def _get_masked_image_slice(slice):
slice_sitk = slice.sitk * \
sitk.Cast(slice.sitk_mask, slice.sitk.GetPixelIDValue())
return slice_sitk
@staticmethod
def _get_mask_slice(slice):
return slice.sitk_mask
# Recontruct volume based on discrete Shepard's like method, cf. Vercauteren2006, equation (19).
# The computation here is based on the YVV variant of Recursive Gaussian Filter and executed
# via ITK
# \remark Obtained intensity values are positive.
def _run_discrete_shepard_reconstruction(self):
shape = sitk.GetArrayFromImage(self._HR_volume.sitk).shape
helper_N_nda = np.zeros(shape)
helper_D_nda = np.zeros(shape)
default_pixel_value = 0.0
for i in range(0, self._N_stacks):
if self._verbose:
ph.print_info("Stack %s/%s" % (i + 1, self._N_stacks))
stack = self._stacks[i]
slices = stack.get_slices()
N_slices = stack.get_number_of_slices()
# for j in range(10, 11):
for j in range(0, N_slices):
# print("\t\tSlice %s/%s" %(j,N_slices-1))
slice = slices[j]
slice_sitk = self._get_slice[(
bool(self._use_masks), bool(self._sda_mask))](slice)
# Add intensity offset so that a "zero" intensity can be
# identified as contribution of image slice (line 353/356)
slice_sitk += 1
# Nearest neighbour resampling of slice to target space (HR
# volume)
slice_resampled_sitk = sitk.Resample(
slice_sitk,
self._HR_volume.sitk,
sitk.Euler3DTransform(),
sitk.sitkNearestNeighbor,
default_pixel_value,
self._HR_volume.sitk.GetPixelIDValue())
# sitkh.show_sitk_image(slice_resampled_sitk)
# Extract array of pixel intensities
nda_slice = sitk.GetArrayFromImage(slice_resampled_sitk)
# Get voxels in HR volume space which are struck by the slice
ind_nonzero = nda_slice > 0
# update numerator (correct previous intensity offset)
helper_N_nda[ind_nonzero] += nda_slice[ind_nonzero] - 1
# update denominator
helper_D_nda[ind_nonzero] += 1
# test = sitk.GetImageFromArray(helper_N_nda)
# sitkh.show_sitk_image(test,title="N")
# test = sitk.GetImageFromArray(helper_D_nda)
# sitkh.show_sitk_image(test,title="D")
# print("helper_N_nda: (min, max) = (%s, %s)" %(np.min(helper_N_nda), np.max(helper_N_nda)))
# print("helper_D_nda: (min, max) = (%s, %s)" %(np.min(helper_D_nda), np.max(helper_D_nda)))
# TODO: Set zero entries to one; Otherwise results are very weird!?
helper_D_nda[helper_D_nda == 0] = 1
# Create itk-images with correct header data
pixel_type = itk.D
dimension = 3
image_type = itk.Image[pixel_type, dimension]
itk2np = itk.PyBuffer[image_type]
helper_N = itk2np.GetImageFromArray(helper_N_nda)
helper_D = itk2np.GetImageFromArray(helper_D_nda)
helper_N.SetSpacing(self._HR_volume.sitk.GetSpacing())
helper_N.SetDirection(
sitkh.get_itk_direction_from_sitk_image(self._HR_volume.sitk))
helper_N.SetOrigin(self._HR_volume.sitk.GetOrigin())
helper_D.SetSpacing(self._HR_volume.sitk.GetSpacing())
helper_D.SetDirection(
sitkh.get_itk_direction_from_sitk_image(self._HR_volume.sitk))
helper_D.SetOrigin(self._HR_volume.sitk.GetOrigin())
# Apply Recursive Gaussian YVV filter
gaussian = itk.SmoothingRecursiveYvvGaussianImageFilter[
image_type, image_type].New() # YVV-based Filter
# gaussian = itk.SmoothingRecursiveGaussianImageFilter[image_type,
# image_type].New() # Deriche-based Filter
gaussian.SetSigmaArray(self._sigma_array)
gaussian.SetInput(helper_N)
gaussian.Update()
HR_volume_update_N = gaussian.GetOutput()
HR_volume_update_N.DisconnectPipeline()
gaussian.SetInput(helper_D)
gaussian.Update()
HR_volume_update_D = gaussian.GetOutput()
HR_volume_update_D.DisconnectPipeline()
# Convert numerator and denominator back to data array
nda_N = itk2np.GetArrayFromImage(HR_volume_update_N)
nda_D = itk2np.GetArrayFromImage(HR_volume_update_D)
# Compute data array of HR volume:
# nda_D[nda_D==0]=1
nda = nda_N / nda_D.astype(float)
# Update HR volume image file within Stack-object HR_volume
HR_volume_update = sitk.GetImageFromArray(nda)
HR_volume_update.CopyInformation(self._HR_volume.sitk)
if not self._sda_mask:
self._HR_volume.sitk = HR_volume_update
self._HR_volume.itk = sitkh.get_itk_from_sitk_image(
HR_volume_update)
else:
# Approximate uint8 mask from float SDA outcome
mask_estimator = bm.BinaryMaskFromMaskSRREstimator(
HR_volume_update)
mask_estimator.run()
HR_volume_update = mask_estimator.get_mask_sitk()
self._HR_volume.sitk_mask = HR_volume_update
self._HR_volume.itk_mask = sitkh.get_itk_from_sitk_image(
HR_volume_update)
# Recontruct volume based on discrete Shepard's like method, cf. Vercauteren2006, equation (19).
# The computation here is based on the Deriche variant of Recursive Gaussian Filter and executed
# via SimpleITK.
# \remark Obtained intensity values can be negative.
def _run_discrete_shepard_based_on_Deriche_reconstruction(self):
shape = sitk.GetArrayFromImage(self._HR_volume.sitk).shape
helper_N_nda = np.zeros(shape)
helper_D_nda = np.zeros(shape)
default_pixel_value = 0.0
for i in range(0, self._N_stacks):
if self._verbose:
ph.print_info("Stack %s/%s" % (i + 1, self._N_stacks))
stack = self._stacks[i]
slices = stack.get_slices()
N_slices = stack.get_number_of_slices()
for j in range(0, N_slices):
slice = slices[j]
slice_sitk = self._get_slice[(
bool(self._use_masks), bool(self._sda_mask))](slice)
# Nearest neighbour resampling of slice to target space (HR
# volume)
slice_resampled_sitk = sitk.Resample(
slice_sitk,
self._HR_volume.sitk,
sitk.Euler3DTransform(),
sitk.sitkNearestNeighbor,
default_pixel_value,
self._HR_volume.sitk.GetPixelIDValue())
# Extract array of pixel intensities
nda_slice = sitk.GetArrayFromImage(slice_resampled_sitk)
# Look for indices which are stroke by the slice in the
# isotropic grid
ind_nonzero = nda_slice > 0
# update arrays of numerator and denominator
helper_N_nda[ind_nonzero] += nda_slice[ind_nonzero]
helper_D_nda[ind_nonzero] += 1
# print("helper_N_nda: (min, max) = (%s, %s)" %(np.min(helper_N_nda), np.max(helper_N_nda)))
# print("helper_D_nda: (min, max) = (%s, %s)" %(np.min(helper_D_nda), np.max(helper_D_nda)))
# TODO: Set zero entries to one; Otherwise results are very weird!?
helper_D_nda[helper_D_nda == 0] = 1
# Create sitk-images with correct header data
helper_N = sitk.GetImageFromArray(helper_N_nda)
helper_D = sitk.GetImageFromArray(helper_D_nda)
helper_N.CopyInformation(self._HR_volume.sitk)
helper_D.CopyInformation(self._HR_volume.sitk)
# Apply recursive Gaussian smoothing
gaussian = sitk.SmoothingRecursiveGaussianImageFilter()
gaussian.SetSigma(self._sigma_array[1])
HR_volume_update_N = gaussian.Execute(helper_N)
HR_volume_update_D = gaussian.Execute(helper_D)
# ## Avoid undefined division by zero
# """
# HACK start
# """
# ## HACK for denominator
# nda = sitk.GetArrayFromImage(HR_volume_update_D)
# ind_min = np.unravel_index(np.argmin(nda), nda.shape)
# # print(nda[nda<0])
# # print(nda[ind_min])
# eps = 1e-8
# # nda[nda<=eps]=1
# print("denominator min = %s" % np.min(nda))
# HR_volume_update_D = sitk.GetImageFromArray(nda)
# HR_volume_update_D.CopyInformation(self._HR_volume.sitk)
# ## HACK for numerator given that some intensities are negative!?
# nda = sitk.GetArrayFromImage(HR_volume_update_N)
# ind_min = np.unravel_index(np.argmin(nda), nda.shape)
# # nda[nda<=eps]=0
# # print(nda[nda<0])
# print("numerator min = %s" % np.min(nda))
# """
# HACK end
# """
# Compute HR volume based on scattered data approximation with correct
# header (might be redundant):
HR_volume_update = HR_volume_update_N / HR_volume_update_D
HR_volume_update.CopyInformation(self._HR_volume.sitk)
if not self._sda_mask:
self._HR_volume.sitk = HR_volume_update
self._HR_volume.itk = sitkh.get_itk_from_sitk_image(
HR_volume_update)
else:
# Approximate uint8 mask from float SDA outcome
mask_estimator = bm.BinaryMaskFromMaskSRREstimator(
HR_volume_update)
mask_estimator.run()
HR_volume_update = mask_estimator.get_mask_sitk()
self._HR_volume.sitk_mask = HR_volume_update
self._HR_volume.itk_mask = sitkh.get_itk_from_sitk_image(
HR_volume_update)
"""
Additional info
"""
if self._verbose:
nda = sitk.GetArrayFromImage(HR_volume_update)
print("Minimum of data array = %s" % np.min(nda))
================================================
FILE: niftymic/reconstruction/solver.py
================================================
##
# \file solver.py
# \brief Base class to solve the SRR problem y_k = D_k B_k W_k x = A_k x
# for all slices k = 1, ..., K.
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date July 2016
#
# Import libraries
from abc import ABCMeta, abstractmethod
import sys
import itk
import SimpleITK as sitk
import numpy as np
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
import niftymic.reconstruction.linear_operators as lin_op
# Allowed data loss functions
DATA_LOSS = ['linear', 'soft_l1', 'huber', 'cauchy', 'arctan']
##
# This class contains the common functions/attributes of the solvers
# \date 2017-11-01 01:04:31+0000
#
class Solver(object):
__metaclass__ = ABCMeta
##
# Constructor
# \date 2016-08-01 22:53:37+0100
#
# \param self The object
# \param stacks list of Stack objects containing
# all stacks used for the
# reconstruction
# \param[in,out] reconstruction Stack object containing the current
# estimate of the reconstruction
# (used as initial value + space
# definition)
# \param alpha_cut Cut-off distance for Gaussian
# blurring filter
# \param alpha regularization parameter, scalar
# \param iter_max number of maximum iterations,
# scalar
# \param minimizer The minimizer
# \param deconvolution_mode Either "full_3D" or
# "only_in_plane". Indicates whether
# full 3D or only in-plane
# deconvolution is considered
# \param data_loss The data loss
# \param huber_gamma The huber gamma
# \param predefined_covariance Either only diagonal entries
# (sigma_x2, sigma_y2, sigma_z2) or
# as full 3x3 numpy array
# \param verbose The verbose
#
def __init__(self,
stacks,
reconstruction,
alpha_cut,
alpha,
iter_max,
minimizer,
x_scale,
data_loss,
data_loss_scale,
huber_gamma,
deconvolution_mode,
predefined_covariance,
verbose,
image_type=itk.Image.D3,
use_masks=True,
):
# Initialize variables
self._stacks = stacks
self._reconstruction = reconstruction
# Cut-off distance for Gaussian blurring filter
self._alpha_cut = alpha_cut
self._deconvolution_mode = deconvolution_mode
self._predefined_covariance = predefined_covariance
self._linear_operators = lin_op.LinearOperators(
deconvolution_mode=self._deconvolution_mode,
predefined_covariance=self._predefined_covariance,
alpha_cut=self._alpha_cut,
image_type=image_type
)
# Settings for solver
self._alpha = alpha
self._iter_max = iter_max
self._use_masks = use_masks
self._minimizer = minimizer
self._data_loss = data_loss
self._data_loss_scale = data_loss_scale
if x_scale == "max":
self._x_scale = sitk.GetArrayFromImage(
reconstruction.sitk).max()
# Avoid zero in case zero-image is given
if self._x_scale == 0:
self._x_scale = 1
else:
self._x_scale = x_scale
self._huber_gamma = huber_gamma
self._verbose = verbose
# Allocate variables containing information about statistics of
# reconstruction
self._computational_time = None
self._residual_ell2 = None
self._residual_prior = None
# Create PyBuffer object for conversion between NumPy arrays and ITK
# images
self._itk2np = itk.PyBuffer[image_type]
# -----------------------------Set helpers-----------------------------
self._N_stacks = len(self._stacks)
# Compute total amount of pixels for all slices
self._N_total_slice_voxels = 0
for i in range(0, self._N_stacks):
N_stack_voxels = np.array(self._stacks[i].sitk.GetSize()).prod()
self._N_total_slice_voxels += N_stack_voxels
# Extract information ready to use for itk image conversion operations
self._reconstruction_shape = sitk.GetArrayFromImage(
self._reconstruction.sitk).shape
# Compute total amount of voxels of x:
self._N_voxels_recon = np.array(
self._reconstruction.sitk.GetSize()).prod()
def set_stacks(self, stacks):
self._stacks = stacks
# Update helpers
self._N_stacks = len(self._stacks)
# Compute total amount of pixels for all slices
self._N_total_slice_voxels = 0
for i in range(0, self._N_stacks):
N_stack_voxels = np.array(self._stacks[i].sitk.GetSize()).prod()
self._N_total_slice_voxels += N_stack_voxels
##
# Specify whether masks shall be used during reconstruction, i.e. whether
# masking operator is applied (and, thus, zeros everything outside it).
# \date 2018-11-12 11:26:58+0000
#
# \param self The object
# \param use_masks boolean
#
def set_use_masks(self, use_masks):
self._use_masks = use_masks
def set_reconstruction(self, reconstruction):
self._reconstruction = reconstruction
# Extract information ready to use for itk image conversion operations
self._reconstruction_shape = sitk.GetArrayFromImage(
self._reconstruction.sitk).shape
# Compute total amount of voxels of x:
self._N_voxels_recon = np.array(
self._reconstruction.sitk.GetSize()).prod()
#
# Set regularization parameter for Tikhonov regularization
# \date 2017-07-25 15:15:54+0100
#
# \param self The object
# \param alpha regularization parameter, scalar
#
# \return { description_of_the_return_value }
#
def set_alpha(self, alpha):
self._alpha = alpha
# Get value of chosen regularization parameter for Tikhonov regularization
# \return regularization parameter, scalar
def get_alpha(self):
return self._alpha
##
# Sets the maximum number of iterations for Tikhonov solver.
# \date 2016-08-01 16:35:09+0100
#
# \param self The object
# \param iter_max number of maximum iterations, scalar
#
# \return { description_of_the_return_value }
#
def set_iter_max(self, iter_max):
self._iter_max = iter_max
# Get chosen value of maximum number of iterations for minimizer for Tikhonov regularization
# \return maximum number of iterations set for minimizer, scalar
def get_iter_max(self):
return self._iter_max
##
# Sets the minimizer.
# \date 2016-11-05 23:40:31+0000
#
# \param self The object
# \param minimizer The minimizer
#
def set_minimizer(self, minimizer):
self._minimizer = minimizer
def get_minimizer(self):
return self._minimizer
##
# Sets the data loss rho in 1/2 ||rho(Ax-b)||^2
# \date 2017-05-15 11:30:25+0100
#
# \param self The object
# \param data_loss string
#
def set_data_loss(self, data_loss):
if data_loss not in DATA_LOSS:
raise ValueError("Loss function must be in " + str(DATA_LOSS))
self._data_loss = data_loss
def set_huber_gamma(self, huber_gamma):
self._huber_gamma = huber_gamma
def get_huber_gamma(self):
return self._huber_gamma
def set_verbose(self, verbose):
self._verbose = verbose
def get_verbose(self):
return self._verbose
def run(self):
# Run solver specific reconstruction
self._run()
# Get current estimate of reconstruction
# \return current estimate of reconstruction, instance of Stack
def get_reconstruction(self):
return self._reconstruction
# Get cut-off distance
# \return scalar value
def get_alpha_cut(self):
return self._alpha_cut
# Get computational time for reconstruction
# \return computational time in seconds
def get_computational_time(self):
return self._computational_time
##
# Get function call A = lambda x: A(x) with A: R^n -> R^m
# \date 2017-07-25 16:02:47+0100
#
# \param self The object
#
# \return Function call mapping from and to 1D numpy array.
#
def get_A(self):
return lambda x: self._MA(x)
##
# Gets function call A^* = lambda y: A^*(y) with A: R^m -> R^n
# \date 2017-07-25 16:18:52+0100
#
# \param self The object
#
# \return Function call mapping from and to 1D numpy array.
#
def get_A_adj(self):
return lambda x: self._A_adj_M(x)
##
# Gets the right hand-side vector b \in R^m
# \date 2017-07-25 16:19:30+0100
#
# \param self The object
#
# \return 1D numpy array
#
def get_b(self):
return self._get_M_y()
##
# Gets the initial value given by the flattened reconstruction numpy data
# array in R^n.
# \date 2017-07-25 16:20:00+0100
#
# \param self The object
#
# \return 1D numpy array
#
def get_x0(self):
return sitk.GetArrayFromImage(self._reconstruction.sitk).flatten()
def get_x_scale(self):
return self._x_scale
##
# Gets the setting specific filename indicating the information
# used for the reconstruction step
# \date 2016-11-17 15:41:58+0000
#
# \param self The object
# \param prefix The prefix as string
#
# \return The setting specific filename as string.
#
@abstractmethod
def get_setting_specific_filename(self, prefix=""):
pass
@abstractmethod
def _run(self):
pass
@abstractmethod
def get_solver(self):
pass
##
# Gets the predefined covariance.
# \date 2016-10-14 16:52:10+0100
#
# \param self The object
#
# \return The predefined covariance as 3x3 numpy array
#
def get_predefined_covariance(self):
return self._predefined_covariance
##
# Evaluate
# \f$ M \vec{y}
# \f$
# \f$ = \begin{pmatrix} M_1 \vec{y}_1 \\ M_2 \vec{y}_2 \\ \vdots \\ M_K
# \vec{y}_K \end{pmatrix} \vec{x}\f$
# \date 2017-11-01 00:17:28+0000
#
# \param self The object
#
# \return My, i.e. all masked slices stacked to 1D array
#
def _get_M_y(self):
# Allocate memory
My = np.zeros(self._N_total_slice_voxels)
# Define index for first voxel of first slice within array
i_min = 0
for i, stack in enumerate(self._stacks):
slices = stack.get_slices()
# Get number of voxels of each slice in current stack
N_slice_voxels = np.array(slices[0].sitk.GetSize()).prod()
for j, slice_j in enumerate(slices):
# Define index for last voxel to specify current slice
# (exlusive)
i_max = i_min + N_slice_voxels
# Apply M_k y_k
if self._use_masks:
slice_itk = self._linear_operators.M_itk(
slice_j.itk, slice_j.itk_mask)
else:
slice_itk = slice_j.itk
slice_nda_vec = self._itk2np.GetArrayFromImage(
slice_itk).flatten()
# Fill respective elements
My[i_min:i_max] = slice_nda_vec
# Define index for first voxel to specify subsequent slice
# (inclusive)
i_min = i_max
return My
##
# Operation M_k A_k x
# \date 2017-07-25 15:15:53+0100
#
# \param self The object
# \param reconstruction_itk reconstruction image as itk.Image object
# \param slice_k Slice object which defines operator M_k and A_k
#
# \return { description_of_the_return_value }
#
def _Mk_Ak(self, reconstruction_itk, slice_k):
# Get slice spacing relevant for Gaussian blurring estimate
in_plane_res = slice_k.get_inplane_resolution()
slice_thickness = slice_k.get_slice_thickness()
slice_spacing = np.array([in_plane_res, in_plane_res, slice_thickness])
# Compute A_k x
Ak_reconstruction_itk = self._linear_operators.A_itk(
reconstruction_itk, slice_k.itk, slice_spacing)
if not self._use_masks:
return Ak_reconstruction_itk
# Compute M_k A_k x
Ak_reconstruction_itk = self._linear_operators.M_itk(
Ak_reconstruction_itk, slice_k.itk_mask)
return Ak_reconstruction_itk
##
# Operation A_k^* M_k y_k
# \date 2017-07-25 15:15:53+0100
#
# \param self The object
# \param slice_itk LR image as itk.Image object
# \param slice_k Slice object which defines operator A_k^*
#
# \return image in reconstruction space as itk.Image object after
# performed backward operation
#
def _Ak_adj_Mk(self, slice_itk, slice_k):
# Compute M_k y_k
if self._use_masks:
Mk_slice_itk = self._linear_operators.M_itk(
slice_itk, slice_k.itk_mask)
else:
Mk_slice_itk = slice_itk
# Get slice spacing relevant for Gaussian blurring estimate
in_plane_res = slice_k.get_inplane_resolution()
slice_thickness = slice_k.get_slice_thickness()
slice_spacing = np.array([in_plane_res, in_plane_res, slice_thickness])
# Compute A_k^* M_k y_k
Mk_slice_itk = self._linear_operators.A_adj_itk(
Mk_slice_itk, self._reconstruction.itk, slice_spacing)
return Mk_slice_itk
#
# Evaluate
# \f$ MA \vec{x}
# \f$
# \f$ = \b egin{pmatrix} M_1 A_1 \\ M_2 A_2 \\ \vdots \\ M_K A_K \em
# nd{pmatrix} \vec{x}
# \f$
# \date 2017-07-25 15:15:53+0100
#
# \param self The object
# \param reconstruction_nda_vec reconstruction data as 1D array
#
# \return evaluated MAx as part of augmented linear operator as 1D
# array
#
def _MA(self, reconstruction_nda_vec):
# Convert reconstruction data array back to itk.Image object
x_itk = self._get_itk_image_from_array_vec(
reconstruction_nda_vec, self._reconstruction.itk)
# Allocate memory
MA_x = np.zeros(self._N_total_slice_voxels)
# Define index for first voxel of first slice within array
i_min = 0
for i, stack in enumerate(self._stacks):
slices = stack.get_slices()
# Get number of voxels of each slice in current stack
N_slice_voxels = np.array(slices[0].sitk.GetSize()).prod()
for j, slice_j in enumerate(slices):
# Define index for last voxel to specify current slice
# (exclusive)
i_max = i_min + N_slice_voxels
# Compute M_k A_k y_k
slice_itk = self._Mk_Ak(x_itk, slice_j)
slice_nda = self._itk2np.GetArrayFromImage(slice_itk)
# Fill corresponding elements
MA_x[i_min:i_max] = slice_nda.flatten()
# Define index for first voxel to specify subsequent slice
# (inclusive)
i_min = i_max
return MA_x
##
# Evaluate
# \f$ A^* M \vec{y} = \begin{bmatrix} A_1^* M_1 && A_2^* M_2 && \c dots &&
# A_K^* M_K \end{bmatrix} \vec{y}
# \f$
# \date 2017-07-18 22:21:53+0100
#
# \param self The object
# \param stacked_slices_nda_vec stacked slice data as 1D array
#
# \return evaluated A'My as part of augmented adjoint linear operator
# as 1D array
#
def _A_adj_M(self, stacked_slices_nda_vec):
# Allocate memory
A_adj_M_y = np.zeros(self._N_voxels_recon)
# Define index for first voxel of first slice within array
i_min = 0
for i, stack in enumerate(self._stacks):
slices = stack.get_slices()
# Get number of voxels of each slice in current stack
N_slice_voxels = np.array(slices[0].sitk.GetSize()).prod()
for j, slice_j in enumerate(slices):
# Define index for last voxel to specify current slice
# (exlusive)
i_max = i_min + N_slice_voxels
# Extract 1D corresponding to current slice and convert it to
# itk.Object
slice_itk = self._get_itk_image_from_array_vec(
stacked_slices_nda_vec[i_min:i_max], slice_j.itk)
# Apply A_k' M_k on current slice
Ak_adj_Mk_slice_itk = self._Ak_adj_Mk(slice_itk, slice_j)
Ak_adj_Mk_slice_nda_vec = self._itk2np.GetArrayFromImage(
Ak_adj_Mk_slice_itk).flatten()
# Add contribution
A_adj_M_y += Ak_adj_Mk_slice_nda_vec
# Define index for first voxel to specify subsequent slice
# (inclusive)
i_min = i_max
return A_adj_M_y
#
# Convert numpy data array (vector format) back to itk.Image object
# \date 2017-07-25 15:15:53+0100
#
# \param self The object
# \param nda_vec reconstruction data as 1D array
# \param image_itk_ref The image itk reference
#
# \return reconstruction with intensities according to reconstruction_nda_vec as
# itk.Image object
#
def _get_itk_image_from_array_vec(self, nda_vec, image_itk_ref):
shape_nda = np.array(
image_itk_ref.GetLargestPossibleRegion().GetSize())[::-1]
image_itk = self._itk2np.GetImageFromArray(nda_vec.reshape(shape_nda))
image_itk.SetOrigin(image_itk_ref.GetOrigin())
image_itk.SetSpacing(image_itk_ref.GetSpacing())
image_itk.SetDirection(image_itk_ref.GetDirection())
return image_itk
================================================
FILE: niftymic/reconstruction/tikhonov_solver.py
================================================
##
# \file tikhonov_solver.py
# \brief Implementation to get an approximate solution of the SRR
# problem using Tikhonov-regularization
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date July 2016
#
import scipy
import numpy as np
import SimpleITK as sitk
from nsol.definitions import EPS
import nsol.linear_operators as linop
import nsol.tikhonov_linear_solver as tk
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
from niftymic.reconstruction.solver import Solver
import niftymic.base.stack as st
# This class implements the framework to iteratively solve
# \f$ \vec{y}_k = A_k \vec{x} \f$ for every slice \f$ \vec{y}_k,\,k=1,\dots,K \f$
# via Tikhonov-regularization via an augmented least-square approach
# where \f$A_k=D_k B_k W_k\in\mathbb{R}^{N_k}\f$ denotes the combined warping, blurring and downsampling
# operation, \f$ M_k \f$ the masking operator and \f$G\f$ represents either
# the identity matrix \f$I\f$ (zeroth-order Tikhonov) or
# the (flattened, stacked vector) gradient
# \f$ \nabla = \begin{pmatrix} D_x \\ D_y \\ D_z \end{pmatrix} \f$
# (first-order Tikhonov).
# The minimization problem reads
# \f[
# \text{arg min}_{\vec{x}} \Big( \sum_{k=1}^K \frac{1}{2} \Vert M_k (\vec{y}_k - A_k \vec{x} )\Vert_{\ell^2}^2
# + \frac{\alpha}{2}\,\Vert G\vec{x} \Vert_{\ell^2}^2 \Big)
# =
# \text{arg min}_{\vec{x}} \Bigg( \Bigg\Vert
# \begin{pmatrix} M_1 A_1 \\ M_2 A_2 \\ \vdots \\ M_K A_K \\ \sqrt{\alpha} G \end{pmatrix} \vec{x}
# - \begin{pmatrix} M_1 \vec{y}_1 \\ M_2 \vec{y}_2 \\ \vdots \\ M_K \vec{y}_K \\ \vec{0} \end{pmatrix}
# \Bigg\Vert_{\ell^2}^2 \Bigg)
# \f]
# By defining the shorthand
# \f[
# MA := \begin{pmatrix} M_1 A_1 \\ M_2 A_2 \\ \vdots \\ M_K A_K \end{pmatrix}\in\mathbb{R}^{\sum_k N_k} \quad\text{and}\quad
# M\vec{y} := \begin{pmatrix} M_1 \vec{y}_1 \\ M_2 \vec{y}_2 \\ \vdots \\ M_K \vec{y}_K \end{pmatrix}\in\mathbb{R}^{\sum_k N_k}
# \f]
# the problem can be compactly written as
# \f[
# \text{arg min}_{\vec{x}} \Bigg( \Bigg\Vert
# \begin{pmatrix} MA \\ \sqrt{\alpha} G \end{pmatrix} \vec{x}
# - \begin{pmatrix} M\vec{y} \\ \vec{0} \end{pmatrix}
# \Bigg\Vert_{\ell^2}^2 \Bigg)
# \f]
# with \f$ G\in\mathbb{R}^N \f$ in case of \f$G=I\f$ or
# \f$G\in\mathbb{R}^{3N}\f$ in case of \f$G\f$ representing the gradient.
# \see \p itkAdjointOrientedGaussianInterpolateImageFilter of \p ITK
# \see \p itOrientedGaussianInterpolateImageFunction of \p ITK
class TikhonovSolver(Solver):
##
# Constructor
# \date 2016-08-01 23:00:04+0100
#
# \param self The object
# \param stacks list of Stack objects containing
# all stacks used for the
# reconstruction
# \param[in,out] reconstruction Stack object containing the current
# estimate of the reconstruction
# volume (used as initial value +
# space definition)
# \param alpha_cut Cut-off distance for Gaussian
# blurring filter
# \param alpha regularization parameter, scalar
# \param iter_max number of maximum iterations,
# scalar
# \param reg_type Type of Tikhonov regualrization,
# i.e. TK0 or TK1 for either zeroth-
# or first order Tikhonov
# \param minimizer Type of minimizer used to solve
# minimization problem, possible
# types: 'lsmr', 'lsqr', 'L-BFGS-B' #
# \param deconvolution_mode Either "full_3D" or
# "only_in_plane". Indicates whether
# full 3D or only in-plane
# deconvolution is considered
# \param data_loss The loss
# \param huber_gamma The huber gamma
# \param predefined_covariance The predefined covariance
# \param verbose The verbose
#
def __init__(self,
stacks,
reconstruction,
alpha_cut=3,
alpha=0.03,
iter_max=10,
reg_type="TK1",
minimizer="lsmr",
deconvolution_mode="full_3D",
x_scale="max",
data_loss="linear",
data_loss_scale=1,
huber_gamma=1.345,
predefined_covariance=None,
use_masks=True,
verbose=1,
):
# Run constructor of superclass
Solver.__init__(self,
stacks=stacks,
reconstruction=reconstruction,
alpha_cut=alpha_cut,
alpha=alpha,
iter_max=iter_max,
minimizer=minimizer,
deconvolution_mode=deconvolution_mode,
x_scale=x_scale,
data_loss=data_loss,
data_loss_scale=data_loss_scale,
huber_gamma=huber_gamma,
predefined_covariance=predefined_covariance,
verbose=verbose,
use_masks=use_masks,
)
# Settings for optimizer
self._reg_type = reg_type
#
# Set type of regularization. It can be either 'TK0' or 'TK1'
# \date 2017-07-25 15:19:17+0100
#
# \param self The object
# \param reg_type Either 'TK0' or 'TK1', string
#
# \return { description_of_the_return_value }
#
def set_regularization_type(self, reg_type):
self._reg_type = reg_type
# Get chosen type of regularization.
# \return regularization type as string
def get_regularization_type(self):
return self._reg_type
##
# Gets the setting specific filename indicating the information
# used for the reconstruction step
# \date 2016-11-17 15:41:58+0000
#
# \param self The object
# \param prefix The prefix as string
#
# \return The setting specific filename as string.
#
def get_setting_specific_filename(self, prefix="SRR_"):
# Build filename
filename = prefix
filename += "stacks" + str(len(self._stacks))
if self._alpha > 0:
filename += "_" + self._reg_type
filename += "_" + self._minimizer
if self._data_loss not in ["linear"]:
filename += "_" + self._data_loss
if self._data_loss in ["huber"]:
filename += str(self._huber_gamma)
filename += "_fscale%g" % self._data_loss_scale
filename += "_alpha" + str(self._alpha)
filename += "_itermax" + str(self._iter_max)
# Replace dots by 'p'
filename = filename.replace(".", "p")
return filename
def get_solver(self):
if self._reg_type not in ["TK0", "TK1"]:
raise ValueError(
"Error: regularization type can only be either 'TK0' or 'TK1'")
# Get operators
A = self.get_A()
A_adj = self.get_A_adj()
b = self.get_b()
x0 = self.get_x0()
x_scale = self.get_x_scale()
if self._reg_type == "TK0":
B = lambda x: x.flatten()
B_adj = lambda x: x.flatten()
elif self._reg_type == "TK1":
spacing = np.array(self._reconstruction.sitk.GetSpacing())
linear_operators = linop.LinearOperators3D(spacing=spacing)
grad, grad_adj = linear_operators.get_gradient_operators()
X_shape = self._reconstruction_shape
Z_shape = grad(x0.reshape(*X_shape)).shape
B = lambda x: grad(x.reshape(*X_shape)).flatten()
B_adj = lambda x: grad_adj(x.reshape(*Z_shape)).flatten()
# Set up solver
solver = tk.TikhonovLinearSolver(
A=A,
A_adj=A_adj,
B=B,
B_adj=B_adj,
b=b,
x0=x0,
x_scale=x_scale,
alpha=self._alpha,
data_loss=self._data_loss,
data_loss_scale=self._data_loss_scale,
verbose=self._verbose,
minimizer=self._minimizer,
iter_max=self._iter_max,
bounds=(0, np.inf),
)
return solver
##
# Run the reconstruction algorithm based on Tikhonov regularization
# \date 2016-07-29 12:35:01+0100
# \post self._reconstruction is updated with new volume and can be
# fetched by \p get_recon
#
# \param self The object
# \param provide_initial_value Use reconstruction volume during
# initialization as initial value, boolean.
# Otherwise, assume zero initial vale.
#
def _run(self):
solver = self.get_solver()
self._print_info_text()
# Run reconstruction
solver.run()
# Get computational time
self._computational_time = solver.get_computational_time()
# After reconstruction: Update member attribute
self._reconstruction.itk = self._get_itk_image_from_array_vec(
solver.get_x(), self._reconstruction.itk)
self._reconstruction.sitk = sitkh.get_sitk_from_itk_image(
self._reconstruction.itk)
def _print_info_text(self):
ph.print_subtitle("Tikhonov Solver:")
ph.print_info("Chosen regularization type: ", newline=False)
if self._reg_type in ["TK0"]:
print("Zeroth-order Tikhonov")
else:
print("First-order Tikhonov")
if self._deconvolution_mode in ["only_in_plane"]:
ph.print_info("(Only in-plane deconvolution is performed)")
elif self._deconvolution_mode in ["predefined_covariance"]:
ph.print_info("(Predefined covariance used: cov = %s)"
% (np.diag(self._predefined_covariance)))
if self._data_loss in ["huber"]:
ph.print_info("Loss function: %s (gamma = %g)" %
(self._data_loss, self._huber_gamma))
else:
ph.print_info("Loss function: %s" % (self._data_loss))
if self._data_loss != "linear":
ph.print_info("Loss function scale: %g" % (self._data_loss_scale))
ph.print_info("Regularization parameter: " + str(self._alpha))
ph.print_info("Minimizer: " + self._minimizer)
ph.print_info(
"Maximum number of iterations: " + str(self._iter_max))
# ph.print_info("Tolerance: %.0e" %(self._tolerance))
class TemporalTikhonovSolver(object):
def __init__(self,
stacks,
reconstruction,
alpha_cut=3,
alpha=0.03,
beta=0.1,
iter_max=10,
reg_type="TK1",
minimizer="lsmr",
deconvolution_mode="full_3D",
x_scale="max",
data_loss="linear",
data_loss_scale=1,
huber_gamma=1.345,
predefined_covariance=None,
use_masks=True,
verbose=1,
):
self._solvers = [
TikhonovSolver(
stacks=[s],
reconstruction=st.Stack.from_stack(reconstruction),
alpha_cut=alpha_cut,
alpha=alpha,
iter_max=iter_max,
reg_type=reg_type,
minimizer=minimizer,
deconvolution_mode=deconvolution_mode,
x_scale=x_scale,
data_loss=data_loss,
data_loss_scale=data_loss_scale,
huber_gamma=huber_gamma,
predefined_covariance=predefined_covariance,
use_masks=use_masks,
verbose=verbose,
)
for s in stacks
]
self._reg_type = reg_type
self._alpha = alpha
self._beta = beta
self._iter_max = iter_max
self._verbose = verbose
self._stacks = stacks
self._reconstruction = reconstruction
self._computational_time = None
self._reconstructions = None
self._bounds = (0, np.inf)
def get_computational_time(self):
return self._computational_time
def get_reconstructions(self):
return self._reconstructions
def run(self):
time_start = ph.start_timing()
self._print_info_text()
shape_x = self._solvers[0]._reconstruction_shape
self._n_x = np.array(shape_x).prod()
self._n_x_total = len(self._solvers) * self._n_x
x0 = self._solvers[0].get_x0()
if self._reg_type == "TK0":
self._B = lambda x: x.flatten()
self._B_adj = lambda x: x.flatten()
elif self._reg_type == "TK1":
spacing = np.array(self._reconstruction.sitk.GetSpacing())
linear_operators = linop.LinearOperators3D(spacing=spacing)
grad, grad_adj = linear_operators.get_gradient_operators()
X_shape = shape_x
Z_shape = grad(x0.reshape(*X_shape)).shape
self._B = lambda x: grad(x.reshape(*X_shape)).flatten()
self._B_adj = lambda x: grad_adj(x.reshape(*Z_shape)).flatten()
self._B_shape = (self._B(x0).size, x0.size)
n_rhs = 0
self._rhs = []
self._A = []
self._A_adj = []
self._D = []
self._D_adj = []
for solver in self._solvers:
b = solver.get_b()
n_rhs += len(b)
self._rhs.append(b)
self._A.append(solver.get_A())
self._A_adj.append(solver.get_A_adj())
if self._alpha > EPS:
n_rhs += len(self._solvers) * self._B_shape[0]
if self._beta > EPS:
n_rhs += (len(self._solvers) - 1) * self._n_x
self._x_1D = np.zeros(self._n_x_total)
self._rhs_1D = np.zeros(n_rhs)
A_fw = lambda x: self._A_fw(
x, np.sqrt(self._alpha), np.sqrt(self._beta))
A_bw = lambda x: self._A_bw(
x, np.sqrt(self._alpha), np.sqrt(self._beta))
# Construct (sparse) linear operator A
A = scipy.sparse.linalg.LinearOperator(
shape=(self._rhs_1D.size, self._x_1D.size),
matvec=A_fw,
rmatvec=A_bw)
b = np.zeros_like(A(self._x_1D))
b_upper = np.concatenate(self._rhs)
b[:b_upper.size] = b_upper
x = scipy.sparse.linalg.lsmr(
A, b,
maxiter=self._iter_max,
show=self._verbose,
atol=0,
btol=0)[0]
if self._bounds is not None:
# Clip to bounds
x = np.clip(x, self._bounds[0], self._bounds[1])
self._reconstructions = self._get_reconstructions(x)
# y_vec = s.get_b()
self._computational_time = ph.stop_timing(time_start)
def _A_fw(self, x, sqrt_alpha, sqrt_beta):
i0 = 0
# cost
for i, solver in enumerate(self._solvers):
i1 = i0 + len(self._rhs[i])
self._rhs_1D[i0:i1] = self._A[i](
x[i * self._n_x:(i + 1) * self._n_x]
)
i0 = i1
# tikhonov
if sqrt_alpha > EPS:
for i, solver in enumerate(self._solvers):
self._rhs_1D[
i0 + self._B_shape[0] * i:
i0 + self._B_shape[0] * (i + 1)
] = sqrt_alpha * self._B(x[i * self._n_x:(i + 1) * self._n_x])
i1 = i0 + self._B_shape[0] * (i + 1)
# temporal
if sqrt_beta > EPS:
for i in range(len(self._solvers) - 1):
self._rhs_1D[
i1 + self._n_x * i:
i1 + self._n_x * (i + 1)
] = sqrt_beta * (
x[(i + 1) * self._n_x:(i + 2) * self._n_x]
- x[i * self._n_x:(i + 1) * self._n_x]
)
return self._rhs_1D
def _A_bw(self, b, sqrt_alpha, sqrt_beta):
i0 = 0
self._x_1D[:] = 0
# cost
for i, solver in enumerate(self._solvers):
i1 = i0 + len(self._rhs[i])
self._x_1D[i * self._n_x:(i + 1) * self._n_x] = \
self._A_adj[i](b[i0:i1])
i0 = i1
# tikhonov
if sqrt_alpha > EPS:
for i, solver in enumerate(self._solvers):
self._x_1D[i * self._n_x:(i + 1) * self._n_x] += \
sqrt_alpha * self._B_adj(
b[i0:i0 + self._B_shape[0]]
)
i0 += self._B_shape[0]
# temporal
if sqrt_beta > EPS:
i = 0
self._x_1D[i * self._n_x:(i + 1) * self._n_x] += \
- sqrt_beta * b[i0 + self._n_x * i: i0 + self._n_x * (i + 1)]
for i in range(1, len(self._solvers) - 1):
self._x_1D[i * self._n_x:(i + 1) * self._n_x] += \
sqrt_beta * (
b[i0 + self._n_x * (i - 1): i0 + self._n_x * i]
- b[i0 + self._n_x * i: i0 + self._n_x * (i + 1)]
)
i = len(self._solvers) - 1
self._x_1D[i * self._n_x:(i + 1) * self._n_x] += \
sqrt_beta * (
b[i0 + self._n_x * (i - 1): i0 + self._n_x * i]
)
return self._x_1D
def _get_reconstructions(self, x):
reconstructions = []
for i, solver in enumerate(self._solvers):
x_vec = x[i * self._n_x:(i + 1) * self._n_x]
recon_itk = solver._get_itk_image_from_array_vec(
x_vec, self._reconstruction.itk)
recon_sitk = sitkh.get_sitk_from_itk_image(recon_itk)
reconstructions.append(
st.Stack.from_sitk_image(
image_sitk=recon_sitk,
slice_thickness=self._reconstruction.get_slice_thickness(),
image_sitk_mask=self._reconstruction.sitk_mask,
)
)
return reconstructions
def _print_info_text(self):
ph.print_subtitle("Temporal Tikhonov Solver:")
ph.print_info("Chosen regularization type: ", newline=False)
if self._reg_type in ["TK0"]:
print("Zeroth-order Tikhonov")
else:
print("First-order Tikhonov")
# if self._deconvolution_mode in ["only_in_plane"]:
# ph.print_info("(Only in-plane deconvolution is performed)")
# elif self._deconvolution_mode in ["predefined_covariance"]:
# ph.print_info("(Predefined covariance used: cov = %s)"
# % (np.diag(self._predefined_covariance)))
# if self._data_loss in ["huber"]:
# ph.print_info("Loss function: %s (gamma = %g)" %
# (self._data_loss, self._huber_gamma))
# else:
# ph.print_info("Loss function: %s" % (self._data_loss))
# if self._data_loss != "linear":
# ph.print_info("Loss function scale: %g" % (self._data_loss_scale))
ph.print_info(
"Regularization parameter alpha (spatial reg): " + str(self._alpha))
ph.print_info(
"Regularization parameter beta (temporal reg): " + str(self._beta))
# ph.print_info("Minimizer: " + self._minimizer)
ph.print_info("Maximum number of iterations: " + str(self._iter_max))
# ph.print_info("Tolerance: %.0e" %(self._tolerance))
def get_setting_specific_filename(self, prefix="SRR_"):
# Build filename
filename = prefix
filename += "stacks" + str(len(self._stacks))
if self._alpha > 0 or self._beta > 0:
filename += "_" + self._reg_type
# filename += "_" + self._minimizer
# if self._data_loss not in ["linear"]:
# filename += "_" + self._data_loss
# if self._data_loss in ["huber"]:
# filename += str(self._huber_gamma)
# filename += "_fscale%g" % self._data_loss_scale
filename += "_alpha" + str(self._alpha)
filename += "_beta" + str(self._beta)
filename += "_itermax" + str(self._iter_max)
# Replace dots by 'p'
filename = filename.replace(".", "p")
return filename
================================================
FILE: niftymic/registration/__init__.py
================================================
================================================
FILE: niftymic/registration/flirt.py
================================================
##
# \file flirt.py
# \brief Class to use registration method FLIRT
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date Aug 2017
#
# Import libraries
import os
import numpy as np
import SimpleITK as sitk
import pysitk.python_helper as ph
import simplereg.flirt
import niftymic.base.stack as st
from niftymic.registration.registration_method \
import AffineRegistrationMethod
##
# Class to use registration method FLIRT
# \date 2017-08-09 11:22:33+0100
#
class FLIRT(AffineRegistrationMethod):
def __init__(self,
fixed=None,
moving=None,
use_fixed_mask=False,
use_moving_mask=False,
use_verbose=False,
registration_type="Rigid",
options="",
):
AffineRegistrationMethod.__init__(self,
fixed=fixed,
moving=moving,
use_fixed_mask=use_fixed_mask,
use_moving_mask=use_moving_mask,
use_verbose=use_verbose,
registration_type=registration_type,
)
# Allowed registration types for FLIRT
self._REGISTRATION_TYPES = ["Rigid", "Affine"]
self._options = options
##
# Sets the options used for FLIRT
# \date 2017-08-08 19:57:47+0100
#
# \param self The object
# \param options The options as string
#
def set_options(self, options):
self._options = options
##
# Gets the options.
# \date 2017-08-08 19:58:14+0100
#
# \param self The object
#
# \return The options as string.
#
def get_options(self):
return self._options
def _run(self):
if self._use_fixed_mask:
fixed_sitk_mask = self._fixed.sitk_mask
else:
fixed_sitk_mask = None
if self._use_moving_mask:
moving_sitk_mask = self._moving.sitk_mask
else:
moving_sitk_mask = None
options = self._options
if self.get_registration_type() == "Rigid":
options += " -dof 6"
elif self.get_registration_type() == "Affine":
options += " -dof 12"
self._registration_method = simplereg.flirt.FLIRT(
fixed_sitk=self._fixed.sitk,
moving_sitk=self._moving.sitk,
fixed_sitk_mask=fixed_sitk_mask,
moving_sitk_mask=moving_sitk_mask,
options=options,
verbose=self._use_verbose,
)
self._registration_method.run()
self._registration_transform_sitk = \
self._registration_method.get_registration_transform_sitk()
def _get_warped_moving_sitk(self):
return self._registration_method.get_warped_moving_sitk()
================================================
FILE: niftymic/registration/intra_stack_registration.py
================================================
##
# \file intra_stack_registration.py
# \brief Intra-stack registration steps where slices are only transformed
# 2D in-plane.
#
# Class has been mainly developed for the CIS30FU project.
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date Nov 2016
#
# Import libraries
import SimpleITK as sitk
import itk
import numpy as np
import niftymic.base.slice as sl
import niftymic.base.stack as st
import niftymic.utilities.intensity_correction as ic
# Import modules
import pysitk.simple_itk_helper as sitkh
from niftymic.registration.stack_registration_base import StackRegistrationBase
class IntraStackRegistration(StackRegistrationBase):
##
# { constructor_description }
# \date 2017-07-14 14:24:00+0100
#
# \param self The object
# \param stack The stack to
# be aligned as
# Stack object
# \param reference The reference
# used for
# alignment as
# Stack object
# \param use_stack_mask Use stack mask
# for
# registration,
# bool
# \param use_reference_mask Use reference
# mask for
# registration,
# bool
# \param use_verbose Verbose
# output, bool
# \param transform_initializer_type The transform
# initializer
# type, e.g.
# "identity",
# "moments" or
# "geometry"
# \param interpolator The interpolator
# \param alpha_neighbour Weight >= 0
# for neighbour
# term
# \param alpha_reference Weight >= 0
# for reference
# term
# \param alpha_parameter Weight >= 0
# for prior term
# \param transform_type The transform
# type, "rigid",
# "similarity",
# "affine"
# \param optimizer Either
# "least_squares"
# to use
# scipy.optimize.least_squares
# or any method
# used in
# "scipy.optimize.minimize",
# e.g.
# "L-BFGS-B".
# \param optimizer_iter_max Maximum number
# of
# iterations/function
# evaluations
# \param optimizer_loss Loss function,
# e.g. "linear",
# "soft_l1" or
# "huber".
# \param optimizer_method The optimizer
# method used
# for
# "least_squares"
# algorithm.
# E.g. "trf"
# \param use_parameter_normalization Use parameter
# normalization
# for optimizer,
# bool
# \param intensity_correction_initializer_type The intensity
# correction
# initializer
# type; None,
# "linear" or
# "affine"
# \param intensity_correction_type_slice_neighbour_fit The intensity
# correction
# type used for
# slice
# neighbour
# term, None,
# "linear" or
# "affine"
# \param prior_intensity_correction_coefficients Prior used for
# intensity
# correction
# coefficients
# \param prior_scale Prior used for
# scaling; only
# valid for
# "similarity"
# \param image_transform_reference_fit_term The image
# transform
# reference fit
# term; Either
# "identity",
# "gradient_magnitude",
# "partial_derivative"
#
def __init__(self,
stack=None,
reference=None,
use_stack_mask=False,
use_reference_mask=False,
use_verbose=False,
transform_initializer_type="identity",
interpolator="Linear",
alpha_neighbour=1,
alpha_reference=1,
alpha_parameter=0,
transform_type="rigid",
optimizer="least_squares",
optimizer_iter_max=20,
optimizer_loss="soft_l1",
optimizer_method="trf",
use_parameter_normalization=False,
intensity_correction_initializer_type=None,
intensity_correction_type_slice_neighbour_fit=None,
prior_intensity_correction_coefficients=np.array([1, 0]),
prior_scale=1.0,
image_transform_reference_fit_term="identity",
):
# Run constructor of superclass
StackRegistrationBase.__init__(
self,
stack=stack,
reference=reference,
use_stack_mask=use_stack_mask,
use_reference_mask=use_reference_mask,
use_verbose=use_verbose,
transform_initializer_type=transform_initializer_type,
use_parameter_normalization=use_parameter_normalization,
optimizer=optimizer,
optimizer_iter_max=optimizer_iter_max,
optimizer_loss=optimizer_loss,
optimizer_method=optimizer_method,
interpolator=interpolator,
alpha_neighbour=alpha_neighbour,
alpha_reference=alpha_reference,
alpha_parameter=alpha_parameter,
)
# Chosen transform type
self._transform_type = transform_type
# Dictionaries to create new transform depending on the chosen
# transform type
self._new_transform_sitk = {
"rigid": self._new_rigid_transform_sitk,
"similarity": self._new_similarity_transform_sitk,
"affine": self._new_affine_transform_sitk
}
self._new_transform_itk = {
"rigid": self._new_rigid_transform_itk,
"similarity": self._new_similarity_transform_itk,
"affine": self._new_affine_transform_itk
}
# Chosen intensity correction type
self._intensity_correction_type_slice_neighbour_fit = \
intensity_correction_type_slice_neighbour_fit
self._intensity_correction_type_reference_fit = \
intensity_correction_type_slice_neighbour_fit
# Define image type for reference cost
self._image_transform_reference_fit_term = image_transform_reference_fit_term
# Dictionary to apply requested intensity correction
self._apply_intensity_correction = {
None: self._apply_intensity_correction_None,
"linear": self._apply_intensity_correction_linear,
"affine": self._apply_intensity_correction_affine,
}
self._add_gradient_with_respect_to_intensity_correction_parameters = {
None: self._add_gradient_with_respect_to_intensity_correction_parameters_None,
"linear": self._add_gradient_with_respect_to_intensity_correction_parameters_linear,
"affine": self._add_gradient_with_respect_to_intensity_correction_parameters_affine,
}
# Specifies how the initial values for the intensity correction shall
# be computed
self._intensity_correction_initializer_type = intensity_correction_initializer_type
# Dictionary to get initial values for intensity correction
self._get_initial_intensity_correction_parameters = {
None: self._get_initial_intensity_correction_parameters_None,
"linear": self._get_initial_intensity_correction_parameters_linear,
"affine": self._get_initial_intensity_correction_parameters_affine
}
# Scale prior
self._prior_scale = prior_scale
# Intensity correction coefficient priors
self._prior_intensity_correction_coefficients = prior_intensity_correction_coefficients
##
self._get_residual_intensity_coefficients = {
None: self._get_residual_intensity_coefficients_None,
"linear": self._get_residual_intensity_coefficients_linear,
"affine": self._get_residual_intensity_coefficients_affine
}
self._get_jacobian_residual_intensity_coefficients = {
None: self._get_jacobian_residual_intensity_coefficients_None,
"linear": self._get_jacobian_residual_intensity_coefficients_linear,
"affine": self._get_jacobian_residual_intensity_coefficients_affine
}
# Dictionary, to update the the slices according to the obtained
# registration
self._apply_motion_correction_and_compute_slice_transforms = {
"rigid": self._apply_rigid_motion_correction_and_compute_slice_transforms,
"similarity": self._apply_similarity_motion_correction_and_compute_slice_transforms,
"affine": self._apply_affine_motion_correction_and_compute_slice_transforms
}
# Gradient Magnitude Filter
self._gradient_magnitude_filter_sitk = sitk.GradientMagnitudeImageFilter()
self._gradient_magnitude_filter_sitk.SetUseImageSpacing(True)
# Gradient Image Filter
self._gradient_image_filter_sitk = sitk.GradientImageFilter()
self._gradient_image_filter_sitk.SetUseImageDirection(True)
self._gradient_image_filter_sitk.SetUseImageSpacing(True)
self._apply_image_transform = {
"identity": self._apply_image_transform_identity,
"dx": self._apply_image_transform_dx,
"dy": self._apply_image_transform_dy,
"gradient_magnitude": self._apply_image_transform_gradient_magnitude
}
# Costs
self._final_cost = 0
self._residual_paramters_ell2 = 0
self._residual_reference_fit_ell2 = 0
self._residual_slice_neighbours_ell2 = 0
self._use_stack_mask_reference_fit_term = self._use_stack_mask
self._use_stack_mask_neighbour_fit_term = self._use_stack_mask
##
# Sets the transform type.
# \date 2016-11-10 01:53:58+0000
#
# \param self The object
# \param transform_type The transform type
#
def set_transform_type(self, transform_type):
if transform_type not in self._new_transform_sitk.keys():
raise ValueError("Transform type " + transform_type +
" not possible.\nAllowed values: " +
str(self._new_transform_sitk.keys()))
self._transform_type = transform_type
def get_transform_type(self):
return self._transform_type
##
# Set the intensity correction type
# \date 2016-11-10 01:58:39+0000
#
# \param self The object
# \param flag The flag
#
def set_intensity_correction_type_slice_neighbour_fit(self, intensity_correction_type_slice_neighbour_fit):
if intensity_correction_type_slice_neighbour_fit \
not in self._apply_intensity_correction.keys():
raise ValueError("Intensity correction type " +
intensity_correction_type_slice_neighbour_fit +
" not possible.\nAllowed values: " + str(self._apply_intensity_correction.keys()))
self._intensity_correction_type_slice_neighbour_fit = \
intensity_correction_type_slice_neighbour_fit
def get_intensity_correction_type_slice_neighbour_fit(self):
return self._intensity_correction_type_slice_neighbour_fit
##
# Set the intensity correction type for neighbour fit
# \date 2016-11-10 01:58:39+0000
#
# \param self The object
# \param flag The flag
#
def set_intensity_correction_type_reference_fit(self, intensity_correction_type_slice_reference_fit):
if intensity_correction_type_slice_reference_fit \
not in self._apply_intensity_correction.keys():
raise ValueError("Intensity correction type " +
intensity_correction_type_slice_reference_fit +
" not possible.\nAllowed values: " +
str(self._apply_intensity_correction.keys()))
self._intensity_correction_type_reference_fit = \
intensity_correction_type_slice_reference_fit
def get_intensity_correction_type_reference_fit(self):
return self._intensity_correction_type_reference_fit
##
# Sets the intensity correction initializer type. It specifies how the
# initial values for the intensity correction shall be computed.
# \date 2016-11-21 19:36:26+0000
#
# \param self The object
# \param intensity_correction_initializer_type The intensity
# correction initializer
# type
#
def set_intensity_correction_initializer_type(self, intensity_correction_initializer_type):
if intensity_correction_initializer_type \
not in self._apply_intensity_correction.keys():
raise ValueError("Intensity correction initializer type " +
intensity_correction_initializer_type +
" not possible.\nAllowed values: " +
str(self._apply_intensity_correction.keys()))
self._intensity_correction_initializer_type = \
intensity_correction_initializer_type
def get_intensity_correction_initializer_type(self):
return self._intensity_correction_initializer_type
##
# Sets the estimated scale.
# \date 2016-11-21 15:45:14+0000
#
# \param self The object
# \param prior_scale The estimated scale
#
def set_prior_scale(self, prior_scale):
self._prior_scale = prior_scale
# self._parameters_prior_transform["similarity"][0] = prior_scale
def set_prior_intensity_coefficients(self, coefficients):
coefficients = np.array(coefficients)
if coefficients.size is 1:
self._prior_intensity_correction_coefficients[0] = coefficients
elif coefficients.size is 2:
self._prior_intensity_correction_coefficients = coefficients
else:
raise ValueError("Coefficients must be of length 1 or 2")
##
# Set image type used to compute the reference cost
# \date 2016-11-30 14:14:36+0000
#
# \param self The object
# \param image_transform_reference_fit_term The image type reference cost
#
def set_image_transform_reference_fit_term(self, image_transform_reference_fit_term):
if image_transform_reference_fit_term \
not in ["identity", "gradient_magnitude", "partial_derivative"]:
raise ValueError("Registration image type" +
image_transform_reference_fit_term +
" for the reference residuals is not possible.")
self._image_transform_reference_fit_term = \
image_transform_reference_fit_term
def use_stack_mask_reference_fit_term(self, flag):
self._use_stack_mask_reference_fit_term = flag
def use_stack_mask_neighbour_fit_term(self, flag):
self._use_stack_mask_neighbour_fit_term = flag
def get_final_cost(self):
if self._final_cost is None:
self._compute_statistics_residuals_ell2()
return self._final_cost
def print_statistics(self):
# Compute ell2-norm of residuals
self._compute_statistics_residuals_ell2()
StackRegistrationBase.print_statistics(self)
if self._alpha_reference > self._ZERO:
print("\tell^2-residual sum_k ||slice_k(T(theta_k)) - ref||_2^2 = %.3e" %
(self._residual_reference_fit_ell2))
if self._alpha_neighbour > self._ZERO:
print("\tell^2-residual sum_k ||slice_k(T(theta_k)) - slice_{k+1}(T(theta_{k+1}))||_2^2 = %.3e" % (
self._residual_slice_neighbours_ell2))
if self._alpha_parameter > self._ZERO:
print("\tell^2-residual sum_k ||theta_k - theta_k0||_2^2 = %.3e" %
(self._residual_paramters_ell2))
print("\tFinal cost: %.3e" % (self._final_cost))
def get_setting_specific_filename(self, prefix="_"):
dictionary_method = {
"trf": "TRF",
"dogbox": "DogBox",
"lm": "LM"
}
dictionary_loss = {
"linear": "Linear",
"soft_l1": "Softl1",
"huber": "Huber"
}
# Build filename
filename = prefix
filename += self._transform_type.capitalize()
filename += "_IC" + \
str(self._intensity_correction_type_slice_neighbour_fit)
filename += "_Opt"
filename += dictionary_method[self._optimizer_method]
filename += dictionary_loss[self._optimizer_loss]
filename += "_maskStack" + str(int(self._use_stack_mask))
if self._reference is not None:
filename += "_maskRef" + str(int(self._use_reference_mask))
filename += "_Nfevmax" + str(self._optimizer_iter_max)
filename += "_alphaR" + "%.g" % (self._alpha_reference)
filename += "_alphaN" + "%.g" % (self._alpha_neighbour)
filename += "_alphaP" + "%.g" % (self._alpha_parameter)
# Replace dots by 'p'
filename = filename.replace(".", "p")
return filename
def _print_info_text_least_squares(self):
print("Minimization via least_squares solver (scipy.optimize.least_squares)")
print("\tMethod: " + self._optimizer_method)
print("\tLoss: " + self._optimizer_loss)
print("\tMaximum number of function evaluations: " +
str(self._optimizer_iter_max))
self._print_into_text_common()
def _print_info_text_minimize(self):
print("Minimization via %s solver (scipy.optimize.minimize)" %
(self._optimizer))
print("\tLoss: " + self._optimizer_loss)
print("\tMaximum number of iterations: " +
str(self._optimizer_iter_max))
self._print_into_text_common()
def _print_into_text_common(self):
print("\tTransform type: " + self._transform_type +
" (Initialization: " + str(self._transform_initializer_type) + ")")
if self._alpha_neighbour > self._ZERO:
print("\tSlice neighbour fit term:")
print("\t\tIntensity correction type: " +
str(self._intensity_correction_type_slice_neighbour_fit) +
" (Initialization: " +
str(self._intensity_correction_initializer_type) + ")")
print("\t\tStack mask used: " +
str(self._use_stack_mask_neighbour_fit_term))
if self._alpha_reference > self._ZERO:
print("\tReference fit term:")
print("\t\tIntensity correction type: " +
str(self._intensity_correction_type_reference_fit) +
" (Initialization: " +
str(self._intensity_correction_initializer_type) + ")")
print("\t\tImage transform: " +
self._image_transform_reference_fit_term)
print("\t\tStack mask used: " +
str(self._use_stack_mask_reference_fit_term))
print("\t\tReference mask used: " + str(self._use_reference_mask))
print("\tRegularization coefficients: %.g (reference), %.g (neighbour), %.g (parameter)" % (
self._alpha_reference,
self._alpha_neighbour,
self._alpha_parameter))
##
# { function_description }
# \date 2016-11-08 14:59:26+0000
#
# \param self The object
#
def _run_registration_pipeline_initialization(self):
self._transform_type_dofs = len(
self._new_transform_sitk[self._transform_type]().GetParameters())
# Get number of voxels in the x-y image plane
self._N_slice_voxels = self._stack.sitk.GetWidth() * \
self._stack.sitk.GetHeight()
# Get projected 2D slices onto x-y image plane
self._slices_2D = self._get_projected_2D_slices_of_stack(
self._stack, registration_image_type="identity")
# If reference is given, precompute required data
if self._reference is not None:
# Get numpy data arrays from reference image mask
self._reference_nda_mask = sitk.GetArrayFromImage(
self._reference.sitk_mask)
# Since self._intensity_correction_type_slice_neighbour_fit defines
# the used intensity correction type, i.e. the intensity parameters
# for optimisation, set them equal in case no neighbour desired.
if abs(self._alpha_neighbour) < self._ZERO:
self._intensity_correction_type_slice_neighbour_fit = self._intensity_correction_type_reference_fit
# slice_i(T(theta_i, x)) - ref(x))
if self._image_transform_reference_fit_term in ["identity"]:
# Used to get initial intensity correction
# parameters/coefficients
self._init_stack = self._stack
self._init_reference = self._reference
# Used to get initial transform parameters
self._init_slices_2D_stack_reference_term = \
self._get_projected_2D_slices_of_stack(
self._stack, registration_image_type="identity")
self._init_slices_2D_reference = \
self._get_projected_2D_slices_of_stack(
self._reference, registration_image_type="identity")
# Used to compare slice data arrays against in residual
# evaluation
self._reference_nda = sitk.GetArrayFromImage(
self._reference.sitk)
# |grad slice_i|(T(theta_i, x)) - |grad ref|(x))
elif self._image_transform_reference_fit_term in ["gradient_magnitude"]:
# Used to get initial intensity correction
# parameters/coefficients
gradient_magnitude_stack_sitk = \
self._gradient_magnitude_filter_sitk.Execute(
self._stack.sitk)
self._init_stack = st.Stack.from_sitk_image(
gradient_magnitude_stack_sitk,
"GradMagn_" + self._stack.get_filename(),
self._stack.sitk_mask)
gradient_magnitude_reference_sitk = \
self._gradient_magnitude_filter_sitk.Execute(
self._reference.sitk)
self._init_reference = st.Stack.from_sitk_image(
gradient_magnitude_reference_sitk,
"GradMagn_" + self._reference.get_filename(),
self._stack.sitk_mask)
# Used to get initial transform parameters
self._init_slices_2D_stack_reference_term = \
self._get_projected_2D_slices_of_stack(
self._stack,
registration_image_type="gradient_magnitude")
self._init_slices_2D_reference = \
self._get_projected_2D_slices_of_stack(
self._reference,
registration_image_type="gradient_magnitude")
# Used to compare slice data arrays against in residual
# evaluation
self._gradient_magnitude_reference_nda = np.zeros(
np.array(self._reference.sitk.GetSize())[::-1])
for i in range(0, self._N_slices):
self._gradient_magnitude_reference_nda[i, :, :] = \
sitk.GetArrayFromImage(
self._init_slices_2D_reference[i].sitk)
# ||dx(slice_i)(T(theta_i)) - dx(ref)|| + ||dy(slice_i)(T(theta_i)) - dy(ref)||
elif self._image_transform_reference_fit_term in ["partial_derivative"]:
# Used to get initial intensity correction
# parameters/coefficients
gradient_magnitude_stack_sitk = \
self._gradient_magnitude_filter_sitk.Execute(
self._stack.sitk)
self._init_stack = st.Stack.from_sitk_image(
gradient_magnitude_stack_sitk,
"GradMagn_" + self._stack.get_filename(),
self._stack.sitk_mask)
gradient_magnitude_reference_sitk = \
self._gradient_magnitude_filter_sitk.Execute(
self._reference.sitk)
self._init_reference = st.Stack.from_sitk_image(
gradient_magnitude_reference_sitk,
"GradMagn_" + self._reference.get_filename(),
self._stack.sitk_mask)
# Used to get initial transform parameters
self._init_slices_2D_stack_reference_term = \
self._get_projected_2D_slices_of_stack(
self._stack,
registration_image_type="gradient_magnitude")
self._init_slices_2D_reference = \
self._get_projected_2D_slices_of_stack(
self._reference,
registration_image_type="gradient_magnitude")
# Used to compare slice data arrays against in residual
# evaluation
dx_slices_2D_reference, dy_slices_2D_reference = \
self._get_projected_2D_slices_of_stack(
self._reference,
registration_image_type="partial_derivative")
self._dx_reference_nda = np.zeros(
np.array(self._reference.sitk.GetSize())[::-1])
self._dy_reference_nda = np.zeros_like(self._dx_reference_nda)
for i in range(0, self._N_slices):
self._dx_reference_nda[i, :, :] = sitk.GetArrayFromImage(
dx_slices_2D_reference[i].sitk)
self._dy_reference_nda[i, :, :] = sitk.GetArrayFromImage(
dy_slices_2D_reference[i].sitk)
# Resampling grid, i.e. the fixed image space during registration
self._slice_grid_2D_sitk = sitk.Image(
self._init_slices_2D_reference[0].sitk)
else:
# Resampling grid, i.e. the fixed image space during registration
self._slice_grid_2D_sitk = sitk.Image(self._slices_2D[0].sitk)
# Get inital transform and the respective initial transform parameters
# used for further optimisation
self._transforms_2D_sitk, parameters = \
self._get_initial_transforms_and_parameters[
self._transform_initializer_type]()
if self._intensity_correction_type_slice_neighbour_fit is not None:
parameters_intensity = \
self._get_initial_intensity_correction_parameters[
self._intensity_correction_initializer_type]()
parameters = np.concatenate(
(parameters, parameters_intensity),
axis=1)
# Parameters for initialization and for regularization term
self._parameters0_vec = parameters.flatten()
# Create copy for member variable
self._parameters = np.array(parameters)
# Store number of degrees of freedom for overall optimization
self._optimization_dofs = self._parameters.shape[1]
##
# Based on the residual functions below and the chosen settings, this
# function returns the residual call used for the least_squares method
# \date 2016-11-21 20:02:32+0000
#
# \param self The object
#
# \return The residual call.
#
def _get_residual_call(self):
alpha_neighbour = abs(float(self._alpha_neighbour))
alpha_parameter = abs(float(self._alpha_parameter))
alpha_reference = abs(float(self._alpha_reference))
# ---------------------------------------------------------------------
# 1) Defines the prior term on the parameters
if alpha_parameter > self._ZERO:
if self._transform_type in ["similarity"]:
self._get_residual_parameters = lambda x: np.concatenate((
self._get_residual_scale(x),
self._get_residual_intensity_coefficients[
self._intensity_correction_type_slice_neighbour_fit](x)
))
else:
self._get_residual_parameters = \
lambda x: self._get_residual_intensity_coefficients[
self._intensity_correction_type_slice_neighbour_fit](x)
# ---------------------------------------------------------------------
# 2) Construct overall residual
if self._reference is None:
if alpha_neighbour < self._ZERO:
raise ValueError(
"A weight of alpha_neighbour <= 0 is not meaningful.")
if alpha_parameter < self._ZERO:
residual = lambda x: self._get_residual_slice_neighbours_fit(x)
else:
residual = lambda x: np.concatenate((
self._get_residual_slice_neighbours_fit(x),
alpha_parameter / alpha_neighbour *
self._get_residual_parameters(x)
))
else:
# Build total residual for reference fit
if self._image_transform_reference_fit_term in ["identity"]:
self._get_residual_reference_fit_total = lambda x: \
self._get_residual_reference_fit(
self._slices_2D,
self._reference_nda,
"identity",
x)
if self._image_transform_reference_fit_term in ["gradient_magnitude"]:
self._get_residual_reference_fit_total = \
lambda x: self._get_residual_reference_fit(
self._slices_2D,
self._gradient_magnitude_reference_nda,
"gradient_magnitude",
x)
elif self._image_transform_reference_fit_term in ["partial_derivative"]:
self._get_residual_reference_fit_total = \
lambda x: np.concatenate((
self._get_residual_reference_fit(
self._slices_2D,
self._dx_reference_nda,
"dx",
x),
self._get_residual_reference_fit(
self._slices_2D,
self._dy_reference_nda,
"dy",
x)
))
# Combine all the residuals
if alpha_reference < self._ZERO:
raise ValueError(
"A weight of alpha_reference <= 0 is not meaningful in case reference is given")
if alpha_neighbour < self._ZERO and alpha_parameter < self._ZERO:
residual = lambda x: self._get_residual_reference_fit_total(x)
elif alpha_neighbour > self._ZERO and alpha_parameter < self._ZERO:
residual = lambda x: np.concatenate((
self._get_residual_reference_fit_total(x),
alpha_neighbour / alpha_reference *
self._get_residual_slice_neighbours_fit(x)
))
elif alpha_neighbour < self._ZERO and alpha_parameter > self._ZERO:
residual = lambda x: np.concatenate((
self._get_residual_reference_fit_total(x),
alpha_parameter / alpha_reference *
self._get_residual_parameters(x)
))
elif alpha_neighbour > self._ZERO and alpha_parameter > self._ZERO:
residual = lambda x: np.concatenate((
self._get_residual_reference_fit_total(x),
alpha_neighbour / alpha_reference *
self._get_residual_slice_neighbours_fit(x),
alpha_parameter / alpha_reference *
self._get_residual_parameters(x)
))
return residual
##
# Based on the Jacobian of the residual functions below and the chosen
# settings, this function returns the Jacobian call used for the
# least_squares method.
# \date 2016-11-21 20:04:37+0000
#
# \param self The object
#
# \return The Jacobian call.
#
def _get_jacobian_residual_call(self):
alpha_neighbour = abs(float(self._alpha_neighbour))
alpha_parameter = abs(float(self._alpha_parameter))
alpha_reference = abs(float(self._alpha_reference))
# ---------------------------------------------------------------------
# 1) Define Jacobian of the prior term on the parameters
if alpha_parameter > self._ZERO:
if self._transform_type in ["similarity"]:
self._get_jacobian_residual_parameters = \
lambda x: np.concatenate((
self._get_jacobian_residual_scale(x),
self._get_jacobian_residual_intensity_coefficients[
self._intensity_correction_type_slice_neighbour_fit](x)
))
else:
self._get_jacobian_residual_parameters = \
lambda x: self._get_jacobian_residual_intensity_coefficients[
self._intensity_correction_type_slice_neighbour_fit](x)
# ---------------------------------------------------------------------
# 2) Construct overall Jacobian of residual
if self._reference is None:
self._alpha_reference = 0
if alpha_neighbour < self._ZERO:
raise ValueError(
"A weight of alpha_neighbour <= 0 is not meaningful.")
if alpha_parameter < self._ZERO:
jacobian = \
lambda x: self._get_jacobian_residual_slice_neighbours_fit(
x)
else:
jacobian = lambda x: np.concatenate((
self._get_jacobian_residual_slice_neighbours_fit(x),
alpha_parameter / alpha_neighbour *
self._get_jacobian_residual_parameters(x)
))
else:
if self._image_transform_reference_fit_term in ["identity"]:
self._get_jacobian_residual_reference_fit_total = \
lambda x: self._get_jacobian_residual_reference_fit(
self._slices_2D, "identity", x)
elif self._image_transform_reference_fit_term in ["gradient_magnitude"]:
self._get_jacobian_residual_reference_fit_total = \
lambda x: self._get_jacobian_residual_reference_fit(
self._slices_2D, "gradient_magnitude", x)
elif self._image_transform_reference_fit_term in ["partial_derivative"]:
self._get_jacobian_residual_reference_fit_total = \
lambda x: np.concatenate((
self._get_jacobian_residual_reference_fit(
self._slices_2D, "dx", x),
self._get_jacobian_residual_reference_fit(
self._slices_2D, "dy", x)
))
if alpha_reference < self._ZERO:
raise ValueError(
"A weight of alpha_reference <= 0 is not meaningful in case reference is given")
if alpha_neighbour < self._ZERO and alpha_parameter < self._ZERO:
jacobian = \
lambda x: self._get_jacobian_residual_reference_fit_total(
x)
elif alpha_neighbour > self._ZERO and alpha_parameter < self._ZERO:
jacobian = lambda x: np.concatenate((
self._get_jacobian_residual_reference_fit_total(x),
alpha_neighbour / alpha_reference *
self._get_jacobian_residual_slice_neighbours_fit(x)
))
elif alpha_neighbour < self._ZERO and alpha_parameter > self._ZERO:
jacobian = lambda x: np.concatenate((
self._get_jacobian_residual_reference_fit_total(x),
alpha_parameter / alpha_reference *
self._get_jacobian_residual_parameters(x)
))
elif alpha_neighbour > self._ZERO and alpha_parameter > self._ZERO:
jacobian = lambda x: np.concatenate((
self._get_jacobian_residual_reference_fit_total(x),
alpha_neighbour / alpha_reference *
self._get_jacobian_residual_slice_neighbours_fit(x),
alpha_parameter / alpha_reference *
self._get_jacobian_residual_parameters(x)
))
return jacobian
##
# Gets the residual indicating the alignment between slices and reference.
# \date 2016-11-08 20:37:49+0000
#
# It returns the stacked residual of slice_i(T(theta_i, x)) - ref(x)) for
# all slices i.
#
# \param self The object
# \param slices_2D The slices 2d
# \param reference_nda The reference nda
# \param trafo The trafo
# \param parameters_vec The parameters vector
#
# \return The residual reference fit as (N_slices * N_slice_voxels)
# numpy array
#
def _get_residual_reference_fit(self,
slices_2D,
reference_nda,
trafo,
parameters_vec):
# Allocate memory for residual
residual = np.zeros((self._N_slices, self._N_slice_voxels))
# Reshape parameters for easier access
parameters = parameters_vec.reshape(-1, self._optimization_dofs)
# Compute residuals between each slice and reference
for i in range(0, self._N_slices):
# Get slice_i(T(theta_i, x))
self._transforms_2D_sitk[i].SetParameters(
parameters[i, 0:self._transform_type_dofs])
slice_i_sitk = sitk.Resample(
slices_2D[i].sitk,
self._slice_grid_2D_sitk,
self._transforms_2D_sitk[i],
self._interpolator_sitk)
# Apply image transform, i.e. gradients etc
slice_i_sitk = self._apply_image_transform[trafo](slice_i_sitk)
# Extract data array
slice_i_nda = sitk.GetArrayFromImage(slice_i_sitk)
# Correct intensities according to chosen model
slice_i_nda = self._apply_intensity_correction[
self._intensity_correction_type_reference_fit](
slice_i_nda, parameters[i, self._transform_type_dofs:])
# Compute residual slice_i(T(theta_i, x)) - ref(x))
residual_slice_nda = slice_i_nda - reference_nda[i, :, :]
# Incorporate mask computations
if self._use_stack_mask_reference_fit_term:
slice_i_sitk_mask = sitk.Resample(
slices_2D[i].sitk_mask,
self._slice_grid_2D_sitk,
self._transforms_2D_sitk[i],
sitk.sitkNearestNeighbor)
slice_i_nda_mask = sitk.GetArrayFromImage(slice_i_sitk_mask)
residual_slice_nda *= slice_i_nda_mask
if self._use_reference_mask:
residual_slice_nda *= self._reference_nda_mask[i, :, :]
# ph.show_2D_array_list([residual_slice_nda, slice_i_nda_mask, self._reference_nda_mask[i,:,:]])
# ph.pause()
# Set residual for current slice difference
residual[i, :] = residual_slice_nda.flatten()
return residual.flatten()
##
# Gets the Jacobian to \p _get_residual_reference_fit used for the
# least_squares method.
# \date 2016-11-21 20:09:36+0000
#
# \param self The object
# \param slices_2D The slices 2d
# \param trafo The trafo
# \param parameters_vec The parameters vector
#
# \return The jacobian residual reference fit as [N_slices *
# N_slice_voxels] x [transform_type_dofs * N_slices] numpy
# array
#
def _get_jacobian_residual_reference_fit(self,
slices_2D,
trafo,
parameters_vec):
# Allocate memory for Jacobian of residual
jacobian = np.zeros(
(self._N_slices * self._N_slice_voxels,
self._optimization_dofs * self._N_slices))
jacobian_slice_i = np.zeros(
(self._N_slice_voxels, self._optimization_dofs))
# Reshape parameters for easier access
parameters = parameters_vec.reshape(-1, self._optimization_dofs)
# Compute Jacobian of residuals between each slice and reference
for i in range(0, self._N_slices):
# Update transforms
parameters_slice = parameters[i, 0:self._transform_type_dofs]
self._transforms_2D_sitk[i].SetParameters(parameters_slice)
self._transforms_2D_itk[i].SetParameters(
itk.OptimizerParameters[itk.D](parameters_slice))
# Get slice_i(T(theta, x))
slice_i_sitk = sitk.Resample(
slices_2D[i].sitk,
self._slice_grid_2D_sitk,
self._transforms_2D_sitk[i],
self._interpolator_sitk)
# Apply image transform, i.e. gradients etc
slice_i_sitk = self._apply_image_transform[trafo](slice_i_sitk)
# Get d[slice(T(theta, x))]/dx as (Ny x Nx x dim)-array
dslice_i_nda = self._get_gradient_image_nda_from_sitk_image(
slice_i_sitk)
# Get slice data array (used for intensity correction parameter
# gradient)
slice_i_nda = sitk.GetArrayFromImage(slice_i_sitk)
# Incorporate mask computations
if self._use_stack_mask_reference_fit_term:
# Slice mask
slice_i_sitk_mask = sitk.Resample(
slices_2D[i].sitk_mask,
self._slice_grid_2D_sitk,
self._transforms_2D_sitk[i],
sitk.sitkNearestNeighbor)
slice_i_nda_mask = sitk.GetArrayFromImage(slice_i_sitk_mask)
# Mask data
slice_i_nda *= slice_i_nda_mask
# Mask gradient data
dslice_i_nda *= slice_i_nda_mask[:, :, np.newaxis]
# ph.show_2D_array_list
if self._use_reference_mask:
# Reference mask
reference_i_nda_mask = self._reference_nda_mask[i, :, :]
# Mask data
slice_i_nda *= reference_i_nda_mask
# Mask gradient data
dslice_i_nda *= reference_i_nda_mask[:, :, np.newaxis]
# Get Jacobian of slice w.r.t to transform parameters
jacobian_slice_nda = \
self._get_gradient_with_respect_to_transform_parameters(
dslice_i_nda, self._transforms_2D_itk[i], slice_i_sitk)
# Get d[slice_i(T(theta_i, x))]/dtheta_i:
# Add Jacobian w.r.t. to intensity correction parameters
jacobian_slice_i_tmp = \
self._add_gradient_with_respect_to_intensity_correction_parameters[
self._intensity_correction_type_reference_fit](
jacobian_slice_nda, slice_i_nda)
# Second dimension is decided by intensity_correction_type_slice_neighbour_fit
# as being of "higher order"
# (e.g. affine for slice fit term and linear for reference fit term)
jacobian_slice_i[:, 0:jacobian_slice_i_tmp.shape[
1]] = jacobian_slice_i_tmp
# Set elements in Jacobian for entire stack
jacobian[
i * self._N_slice_voxels:
(i + 1) * self._N_slice_voxels,
i * self._optimization_dofs:
(i + 1) * self._optimization_dofs] = jacobian_slice_i
return jacobian
##
# Gets the residual indicating the alignment between neighbouring slices.
# \date 2016-11-21 20:07:41+0000
#
# It returns the stacked residual of slice_i(T(theta_i, x)) -
# slice_{i+1}(T(theta_{i+1}, x)) for all voxels x of all slices i.
#
# \param self The object
# \param parameters_vec The parameters vector
#
# \return The residual slice neighbours fit as
# (N_slices-1) * N_slice_voxels numpy array
#
def _get_residual_slice_neighbours_fit(self, parameters_vec):
# Allocate memory for residual
residual = np.zeros((self._N_slices - 1, self._N_slice_voxels))
# Reshape parameters for easier access
parameters = parameters_vec.reshape(-1, self._optimization_dofs)
# Update transform
i = 0
parameters_slice_i = parameters[i, 0:self._transform_type_dofs]
self._transforms_2D_sitk[i].SetParameters(parameters_slice_i)
# Get slice_i(T(theta_i, x)) for i=0
slice_i_sitk = sitk.Resample(
self._slices_2D[i].sitk,
self._slice_grid_2D_sitk,
self._transforms_2D_sitk[i],
self._interpolator_sitk)
slice_i_nda = sitk.GetArrayFromImage(slice_i_sitk)
# Correct intensities according to chosen model
slice_i_nda = self._apply_intensity_correction[
self._intensity_correction_type_slice_neighbour_fit](
slice_i_nda, parameters[i, self._transform_type_dofs:])
if self._use_stack_mask_neighbour_fit_term:
slice_i_sitk_mask = sitk.Resample(
self._slices_2D[i].sitk_mask,
self._slice_grid_2D_sitk,
self._transforms_2D_sitk[i],
sitk.sitkNearestNeighbor)
slice_i_nda_mask = sitk.GetArrayFromImage(slice_i_sitk_mask)
# Compute residuals for neighbouring slices
for i in range(0, self._N_slices - 1):
# Update transform
parameters_slice_ip1 = parameters[
i + 1, 0:self._transform_type_dofs]
self._transforms_2D_sitk[i + 1].SetParameters(parameters_slice_ip1)
# Get slice_{i+1}(T(theta_{i+1}, x))
slice_ip1_sitk = sitk.Resample(
self._slices_2D[i + 1].sitk,
self._slice_grid_2D_sitk,
self._transforms_2D_sitk[i + 1],
self._interpolator_sitk)
slice_ip1_nda = sitk.GetArrayFromImage(slice_ip1_sitk)
# Correct intensities according to chosen model
slice_ip1_nda = self._apply_intensity_correction[
self._intensity_correction_type_slice_neighbour_fit](
slice_ip1_nda, parameters[i + 1, self._transform_type_dofs:])
# Compute residual slice_i(T(theta_i, x)) -
# slice_{i+1}(T(theta_{i+1}, x))
residual_slice_nda = slice_i_nda - slice_ip1_nda
# Eliminate residual for non-masked regions
if self._use_stack_mask_neighbour_fit_term:
slice_ip1_sitk_mask = sitk.Resample(
self._slices_2D[i + 1].sitk_mask,
self._slice_grid_2D_sitk,
self._transforms_2D_sitk[i + 1],
sitk.sitkNearestNeighbor)
slice_ip1_nda_mask = sitk.GetArrayFromImage(
slice_ip1_sitk_mask)
residual_slice_nda = residual_slice_nda * slice_i_nda_mask * \
slice_ip1_nda_mask
slice_i_nda_mask = slice_ip1_nda_mask
# Set residual for current slice difference
residual[i, :] = residual_slice_nda.flatten()
# Prepare for next iteration
slice_i_nda = slice_ip1_nda
return residual.flatten()
##
# Gets the Jacobian to \p _get_residual_slice_neighbours_fit used for the
# least_squares method.
# \date 2016-11-21 20:08:48+0000
#
# \param self The object
# \param parameters_vec The parameters vector
#
# \return The Jacobian residual slice neighbours fit as [(N_slices-1) *
# N_slice_voxels] x [transform_type_dofs * N_slices] numpy
# array
#
def _get_jacobian_residual_slice_neighbours_fit(self, parameters_vec):
# Allocate memory for Jacobian of residual
jacobian = np.zeros((
(self._N_slices - 1) * self._N_slice_voxels,
self._optimization_dofs * self._N_slices))
# Reshape parameters for easier access
parameters = parameters_vec.reshape(-1, self._optimization_dofs)
# Update transforms
i = 0
parameters_slice_i = parameters[i, 0:self._transform_type_dofs]
self._transforms_2D_sitk[i].SetParameters(parameters_slice_i)
self._transforms_2D_itk[i].SetParameters(
itk.OptimizerParameters[itk.D](parameters_slice_i))
# Get d[slice_i(T(theta_i, x))]/dtheta_i
jacobian_slice_i = self._get_jacobian_slice_in_slice_neighbours_fit(
self._slices_2D[i],
self._transforms_2D_sitk[i],
self._transforms_2D_itk[i])
# Compute Jacobian of residuals
for i in range(0, self._N_slices - 1):
# Update transforms
parameters_slice_ip1 = parameters[
i + 1, 0:self._transform_type_dofs]
self._transforms_2D_sitk[i + 1].SetParameters(parameters_slice_ip1)
self._transforms_2D_itk[i + 1].SetParameters(
itk.OptimizerParameters[itk.D](parameters_slice_ip1))
# Get d[slice_{i+1}(T(theta_{i+1}, x))]/dtheta_{i+1}
jacobian_slice_ip1 = \
self._get_jacobian_slice_in_slice_neighbours_fit(
self._slices_2D[i + 1],
self._transforms_2D_sitk[i + 1],
self._transforms_2D_itk[i + 1])
# Set elements in Jacobian for entire stack
jacobian[i * self._N_slice_voxels:
(i + 1) * self._N_slice_voxels,
i * self._optimization_dofs:
(i + 1) * self._optimization_dofs] = jacobian_slice_i
jacobian[i * self._N_slice_voxels:
(i + 1) * self._N_slice_voxels,
(i + 1) * self._optimization_dofs:
(i + 2) * self._optimization_dofs] = -jacobian_slice_ip1
# Prepare for next iteration
jacobian_slice_i = jacobian_slice_ip1
return jacobian
##
# Gets the Jacobian of a slice based on the spatial transformation.
# \date 2016-11-21 18:23:53+0000
#
# Compute the Jacobian
# \f$ \frac{dI(T(\theta, x))}{d\theta} =
# \frac{dI}{dy}(T(\theta,x))\,\frac{dT}{d\theta}(\theta, x)
# \f$. It also considers the (affine) intensity correction model
#
# \param self The object
# \param slice The slice
# \param transform_sitk The transform sitk
# \param transform_itk The transform itk
#
# \return The Jacobian of a slice as (N_slice_voxels x
# transform_type_dofs)-array.
#
def _get_jacobian_slice_in_slice_neighbours_fit(self,
slice,
transform_sitk,
transform_itk):
# Get slice(T(theta, x))
slice_sitk = sitk.Resample(
slice.sitk,
self._slice_grid_2D_sitk,
transform_sitk,
self._interpolator_sitk)
# Get d[slice(T(theta, x))]/dx as (Ny x Nx x dim)-array
dslice_nda = self._get_gradient_image_nda_from_sitk_image(slice_sitk)
# Get slice data array (used for intensity correction parameter
# gradient)
slice_nda = sitk.GetArrayFromImage(slice_sitk)
if self._use_stack_mask_neighbour_fit_term:
slice_sitk_mask = sitk.Resample(
slice.sitk_mask,
self._slice_grid_2D_sitk,
transform_sitk,
sitk.sitkNearestNeighbor)
slice_nda_mask = sitk.GetArrayFromImage(slice_sitk_mask)
# slice_nda *= slice_nda_mask[:,:,np.newaxis]
slice_nda *= slice_nda_mask
# Get Jacobian of slice w.r.t to transform parameters
jacobian_slice_nda = \
self._get_gradient_with_respect_to_transform_parameters(
dslice_nda, transform_itk, slice_sitk)
# Add Jacobian w.r.t. to intensity correction parameters
jacobian_slice_nda = \
self._add_gradient_with_respect_to_intensity_correction_parameters[
self._intensity_correction_type_slice_neighbour_fit](
jacobian_slice_nda, slice_nda)
return jacobian_slice_nda
##
# Gets the gradient with respect to transform parameters of all voxels
# within a slice.
# \date 2017-07-15 23:03:10+0100
#
# \param self The object
# \param dslice_nda The dslice nda
# \param transform_itk The transform itk
# \param slice_sitk The slice sitk
#
# \return The gradient with respect to transform parameters;
# (N_slice_voxels x transform_type_dofs) numpy array
#
def _get_gradient_with_respect_to_transform_parameters(self,
dslice_nda,
transform_itk,
slice_sitk):
# Reshape to (N_slice_voxels x dim)-array
dslice_nda = dslice_nda.reshape(self._N_slice_voxels, -1)
# Get d[T(theta, x)]/dtheta as (N_slice_voxels x dim x
# transform_type_dofs)-array
dT_nda = \
sitkh.get_numpy_array_of_jacobian_itk_transform_applied_on_sitk_image(
transform_itk, slice_sitk)
# Compute Jacobian for slice as (N_slice_voxels x
# transform_type_dofs)-array
jacobian_slice = np.sum(dslice_nda[:, :, np.newaxis] * dT_nda, axis=1)
return jacobian_slice
def _get_gradient_image_nda_from_sitk_image(self, slice_sitk):
# Compute d[slice(T(theta, x))]/dx
dslice_sitk = self._gradient_image_filter_sitk.Execute(slice_sitk)
# Get associated (Ny x Nx x dim)-array
dslice_nda = sitk.GetArrayFromImage(dslice_sitk)
return dslice_nda
# ##
# # Gets the residual parameters for all optimization parameters
# # \date 2016-11-21 18:09:24+0000
# #
# # \param self The object
# # \param parameters_vec The parameters vector
# #
# # \return The residual parameters.
# #
# def _get_residual_parameters(self, parameters_vec):
# ## Reshape parameters for easier access
# parameters = parameters_vec.reshape(-1, self._optimization_dofs)
# parameters_prior = np.zeros(parameters.shape)
# ## Prior for transform parameters
# parameters_prior[:, 0: self._transform_type_dofs] = self._parameters_prior_transform[self._transform_type]
# ## Prior for intensity correction parameters
# parameters_prior[:,self._transform_type_dofs:] = self._parameters_prior_intensity_correction[self._intensity_correction_type_slice_neighbour_fit]
# # return parameters_vec/self._parameters0_vec
# return parameters_vec - parameters_prior.flatten()
# def _get_jacobian_residual_parameters(self, parameters_vec):
# ## Reshape parameters for easier access
# parameters = parameters_vec.reshape(-1, self._optimization_dofs)
# parameters_prior = np.zeros(parameters.shape)
# # parameters_prior[:, self._transform_type_dofs:] = np.array([10.,50.])
# ## Allocate memory for Jacobian of residual
# jacobian = np.eye(self._N_slices*self._optimization_dofs)
# # jacobian = np.diag(1/parameters_prior.flatten())
# return jacobian
##
# Gets the residual scale.
# \date 2016-11-21 18:09:42+0000
#
# \param self The object
# \param parameters_vec The parameters vector
#
# \return The residual scale.
#
def _get_residual_scale(self, parameters_vec):
# Reshape parameters for easier access
parameters = parameters_vec.reshape(-1, self._optimization_dofs)
parameters_scale = parameters[:, 0]
return parameters_scale - self._prior_scale
def _get_jacobian_residual_scale(self, parameters_vec):
jacobian = np.zeros(
(self._N_slices, self._N_slices * self._optimization_dofs))
for i in range(0, self._N_slices):
jacobian[i, i * self._optimization_dofs] = 1
return jacobian
##
# Gets the residual intensity coefficients for different intensity
# correction models.
# \date 2016-11-21 18:12:27+0000
#
# \param self The object
# \param parameters_vec The parameters vector
#
# \return The residual intensity coefficients for different types.
#
def _get_residual_intensity_coefficients_None(self, parameters_vec):
return np.zeros(1)
def _get_jacobian_residual_intensity_coefficients_None(self,
parameters_vec):
return np.zeros((1, self._N_slices * self._optimization_dofs))
def _get_residual_intensity_coefficients_linear(self, parameters_vec):
# Reshape parameters for easier access
parameters = parameters_vec.reshape(-1, self._optimization_dofs)
parameters_coefficients = parameters[:, self._transform_type_dofs]
return parameters_coefficients - \
self._prior_intensity_correction_coefficients[0]
def _get_jacobian_residual_intensity_coefficients_linear(self,
parameters_vec):
jacobian = np.zeros(
(self._N_slices, self._N_slices * self._optimization_dofs))
for i in range(0, self._N_slices):
jacobian[i, self._transform_type_dofs +
i * self._optimization_dofs] = 1
return jacobian
def _get_residual_intensity_coefficients_affine(self, parameters_vec):
# Reshape parameters for easier access
parameters = parameters_vec.reshape(-1, self._optimization_dofs)
parameters_coefficients = parameters[:, self._transform_type_dofs:]
return (parameters_coefficients -
self._prior_intensity_correction_coefficients).flatten()
def _get_jacobian_residual_intensity_coefficients_affine(self,
parameters_vec):
jacobian = np.zeros(
(2 * self._N_slices, self._N_slices * self._optimization_dofs))
for i in range(0, self._N_slices):
jacobian[2 * i, self._transform_type_dofs +
i * self._optimization_dofs] = 1
jacobian[2 * i + 1, self._transform_type_dofs +
i * self._optimization_dofs + 1] = 1
return jacobian
##
# Compute several transforms on image like identity, \f$ \partial_x \f$,
# \f$ \partial_y \f$ and \f$ |\nabla | \f$.
# \date 2016-12-01 03:08:50+0000
#
# \param self The object
# \param slice_2D_sitk The slice 2d sitk
#
def _apply_image_transform_identity(self, slice_2D_sitk):
return slice_2D_sitk
def _apply_image_transform_dx(self, slice_2D_sitk):
dx_slice_2D_sitk = self._get_dx_image_sitk(slice_2D_sitk)
# Debug
# sitkh.show_sitk_image([slice_2D_sitk, dx_slice_2D_sitk], title=["original", "dx"])
return dx_slice_2D_sitk
def _apply_image_transform_dy(self, slice_2D_sitk):
dy_slice_2D_sitk = self._get_dy_image_sitk(slice_2D_sitk)
# Debug
# sitkh.show_sitk_image([slice_2D_sitk, dy_slice_2D_sitk], title=["original", "dy"])
return dy_slice_2D_sitk
def _apply_image_transform_gradient_magnitude(self, slice_2D_sitk):
gradient_magnitude_slice_2D_sitk = \
self._gradient_magnitude_filter_sitk.Execute(
slice_2D_sitk)
# Debug
# sitkh.show_sitk_image([slice_2D_sitk, gradient_magnitude_slice_2D_sitk], title=["original", "gradient_magnitude"])
return gradient_magnitude_slice_2D_sitk
def _get_dx_image_sitk(self, image_sitk):
dimage_sitk = self._gradient_image_filter_sitk.Execute(image_sitk)
dx_image_sitk = sitk.VectorIndexSelectionCast(dimage_sitk, 0)
return dx_image_sitk
def _get_dy_image_sitk(self, image_sitk):
dimage_sitk = self._gradient_image_filter_sitk.Execute(image_sitk)
dy_image_sitk = sitk.VectorIndexSelectionCast(dimage_sitk, 1)
return dy_image_sitk
##
# Calculates the statistics of residuals based on ell^2 norm
# \date 2016-11-30 14:16:20+0000
#
# \param self The object
#
# \return The statistics residuals ell 2.
#
def _compute_statistics_residuals_ell2(self):
self._final_cost = 0
if self._alpha_reference > self._ZERO:
self._residual_reference_fit_ell2 = np.sum(
self._get_residual_reference_fit_total(
self._parameters.flatten())**2)
self._final_cost += self._alpha_reference * \
self._residual_reference_fit_ell2
if self._alpha_neighbour > self._ZERO:
self._residual_slice_neighbours_ell2 = np.sum(
self._get_residual_slice_neighbours_fit(
self._parameters.flatten())**2)
self._final_cost += self._alpha_neighbour * \
self._residual_slice_neighbours_ell2
if self._alpha_parameter > self._ZERO:
self._residual_paramters_ell2 = np.sum(
self._get_residual_parameters(self._parameters.flatten())**2)
self._final_cost += self._alpha_parameter * \
self._residual_paramters_ell2
##
# Gets the initial parameters for 'None', i.e. for identity
# transform.
# \date 2016-11-08 15:06:54+0000
#
# \param self The object
#
# \return The initial parameters corresponding to identity transform as
# (N_slices x DOF)-array
#
def _get_initial_transforms_and_parameters_identity(self):
# Create list of identity transforms for all slices
transforms_2D_sitk = [None] * self._N_slices
# Get list of identity transform parameters for all slices
parameters = np.zeros((self._N_slices, self._transform_type_dofs))
for i in range(0, self._N_slices):
transforms_2D_sitk[i] = self._new_transform_sitk[
self._transform_type]()
parameters[i, :] = transforms_2D_sitk[i].GetParameters()
return transforms_2D_sitk, parameters
##
# Gets the initial parameters for either 'GEOMETRY' or 'MOMENTS'.
# \date 2016-11-08 15:08:07+0000
#
# \param self The object
#
# \return The initial parameters corresponding to 'GEOMETRY' or
# 'MOMENTS' as (N_slices x DOF)-array
#
def _get_initial_transforms_and_parameters_geometry_moments(self):
transform_initializer_type_sitk = \
self._dictionary_transform_initializer_type_sitk[
self._transform_initializer_type]
# Create list of identity transforms
transforms_2D_sitk = [self._new_transform_sitk[
self._transform_type]()] * self._N_slices
# Get list of identity transform parameters for all slices
parameters = np.zeros((self._N_slices, self._transform_type_dofs))
# Set identity parameters for first slice
parameters[0, :] = transforms_2D_sitk[0].GetParameters()
# No reference is given and slices are initialized to align with
# neighbouring slice
if self._reference is None:
# Create identity transform for first slice
compensation_transform_sitk = self._new_transform_sitk[
self._transform_type]()
# First slice is kept at position and others are aligned
# accordingly
for i in range(1, self._N_slices):
# Take into account the initialization of slice i-1
slice_im1_sitk = sitk.Image(self._slices_2D[i - 1].sitk)
if self._use_stack_mask_neighbour_fit_term:
slice_im1_sitk *= sitk.Cast(
self._slices_2D[i - 1].sitk_mask,
slice_im1_sitk.GetPixelIDValue())
slice_im1_sitk = sitkh.get_transformed_sitk_image(
slice_im1_sitk, compensation_transform_sitk)
# Use sitk.CenteredTransformInitializerFilter to get initial
# transform
fixed_sitk = slice_im1_sitk
moving_sitk = sitk.Image(self._slices_2D[i].sitk)
if self._use_stack_mask_neighbour_fit_term:
moving_sitk *= sitk.Cast(self._slices_2D[i].sitk_mask,
moving_sitk.GetPixelIDValue())
initial_transform_sitk = self._new_transform_sitk[
self._transform_type]()
operation_mode_sitk = eval(
"sitk.CenteredTransformInitializerFilter." +
transform_initializer_type_sitk)
# Get transform
try:
# For operation_mode_sitk="MOMENTS" errors can occur!
initial_transform_sitk = sitk.CenteredTransformInitializer(
fixed_sitk, moving_sitk, initial_transform_sitk, operation_mode_sitk)
except:
print("WARNING: Slice %d/%d" % (i, self._N_slices - 1))
print("\tsitk.CenteredTransformInitializerFilter with " +
transform_initializer_type_sitk +
" does not work. Identity transform is used instead for initialization")
initial_transform_sitk = self._new_transform_sitk[
self._transform_type]()
transforms_2D_sitk[i] = eval(
"sitk." + initial_transform_sitk.GetName() +
"(initial_transform_sitk)")
# Get parameters
parameters[i, :] = transforms_2D_sitk[i].GetParameters()
# Store compensation transform for subsequent slice
compensation_transform_sitk.SetParameters(
transforms_2D_sitk[i].GetParameters())
compensation_transform_sitk.SetFixedParameters(
transforms_2D_sitk[i].GetFixedParameters())
compensation_transform_sitk = eval(
"sitk." + compensation_transform_sitk.GetName() +
"(compensation_transform_sitk.GetInverse())")
# Initialize transform to match each slice with the reference
else:
# print self._use_reference_mask
# print self._use_stack_mask_reference_fit_term
for i in range(0, self._N_slices):
# Use sitk.CenteredTransformInitializerFilter to get initial
# transform
fixed_sitk = self._init_slices_2D_reference[i].sitk
if self._use_reference_mask:
fixed_sitk *= sitk.Cast(
self._init_slices_2D_reference[i].sitk_mask,
fixed_sitk.GetPixelIDValue())
moving_sitk = self._init_slices_2D_stack_reference_term[i].sitk
if self._use_stack_mask_reference_fit_term:
moving_sitk *= sitk.Cast(
self._init_slices_2D_stack_reference_term[i].sitk_mask,
moving_sitk.GetPixelIDValue())
initial_transform_sitk = self._new_transform_sitk[
self._transform_type]()
operation_mode_sitk = eval(
"sitk.CenteredTransformInitializerFilter." +
transform_initializer_type_sitk)
# Get transform
try:
# For operation_mode_sitk="MOMENTS" errors can occur!
initial_transform_sitk = sitk.CenteredTransformInitializer(
fixed_sitk, moving_sitk, initial_transform_sitk, operation_mode_sitk)
except:
print("WARNING: Slice %d/%d" % (i, self._N_slices - 1))
print("\tsitk.CenteredTransformInitializerFilter with " +
transform_initializer_type_sitk +
" does not work. Identity transform is used instead for initialization")
initial_transform_sitk = \
self._new_transform_sitk[self._transform_type]()
transforms_2D_sitk[i] = eval(
"sitk." + initial_transform_sitk.GetName() +
"(initial_transform_sitk)")
# Get parameters
parameters[i, :] = transforms_2D_sitk[i].GetParameters()
return transforms_2D_sitk, parameters
##
# Gets the initial intensity correction parameters.
# \date 2016-11-10 02:38:17+0000
#
# \param self The object
#
# \return The initial intensity correction parameters as (N_slices x
# DOF)-array with DOF being either 1 (linear) or 2 (affine)
#
def _get_initial_intensity_correction_parameters_None(self):
# Set intensity correction parameters to identity
if self._intensity_correction_type_slice_neighbour_fit in ["linear"]:
return np.ones((self._N_slices, 1))
# affine intensity correction type requires additional column (but set
# to zero)
elif self._intensity_correction_type_slice_neighbour_fit in ["affine"]:
return np.concatenate((np.ones((self._N_slices, 1)),
np.zeros((self._N_slices, 1))),
axis=1)
def _get_initial_intensity_correction_parameters_linear(self):
if self._reference is None:
print(
"No reference given. Initial intensity correction parameters are set to identity")
intensity_corrections_coefficients = \
self._get_initial_intensity_correction_parameters_None()
else:
intensity_correction = ic.IntensityCorrection(
stack=self._init_stack,
reference=self._init_reference.get_resampled_stack_from_slices(
resampling_grid=self._init_stack.sitk),
use_individual_slice_correction=True,
use_verbose=False)
intensity_correction.run_linear_intensity_correction()
intensity_corrections_coefficients = intensity_correction.get_intensity_correction_coefficients()
# affine intensity correction type requires additional column (but
# set to zero)
if self._intensity_correction_type_slice_neighbour_fit in ["affine"]:
intensity_corrections_coefficients = np.concatenate(
(intensity_corrections_coefficients,
np.zeros((self._N_slices, 1))),
axis=1)
return intensity_corrections_coefficients
def _get_initial_intensity_correction_parameters_affine(self):
if self._reference is not None:
intensity_correction = ic.IntensityCorrection(
stack=self._init_stack,
reference=self._init_reference.get_resampled_stack_from_slices(
resampling_grid=self._init_stack.sitk),
use_individual_slice_correction=False,
use_verbose=False)
intensity_correction.run_affine_intensity_correction()
intensity_corrections_coefficients = \
intensity_correction.get_intensity_correction_coefficients()
else:
print(
"No reference given. Initial intensity correction parameters are set to identity")
intensity_corrections_coefficients = np.ones((self._N_slices, 1))
return intensity_corrections_coefficients
##
# Correct intensity implementations
# \date 2016-11-10 23:01:34+0000
#
# \param self The object
# \param slice_nda The slice nda
# \param correction_coefficients The correction coefficients
#
# \return intensity corrected slice / 2D data array
#
def _apply_intensity_correction_None(self,
slice_nda,
correction_coefficients):
return slice_nda
def _apply_intensity_correction_linear(self,
slice_nda,
correction_coefficients):
return slice_nda * correction_coefficients[0]
def _apply_intensity_correction_affine(self,
slice_nda,
correction_coefficients):
return slice_nda * correction_coefficients[0] + \
correction_coefficients[1]
##
# Adds the Jacobian w.r.t to the intensity correction coefficients
# depending on the chosen correction model to the existing Jacobian.
# \date 2016-11-21 19:47:41+0000
#
# \param self The object
# \param jacobian_slice The jacobian slice
# \param slice_sitk The slice sitk
# \param mask_nda The mask nda
#
# \return Jacobian including intensity correction parameters
#
def _add_gradient_with_respect_to_intensity_correction_parameters_None(
self,
jacobian_slice_nda,
slice_nda):
return jacobian_slice_nda
def _add_gradient_with_respect_to_intensity_correction_parameters_linear(
self,
jacobian_slice_nda,
slice_nda):
# Add the Jacobian w.r.t. intensity correction parameter (slope) to
# existing Jacobian
jacobian_slice_nda = np.concatenate(
(jacobian_slice_nda, slice_nda.reshape(self._N_slice_voxels, -1)),
axis=1)
return jacobian_slice_nda
def _add_gradient_with_respect_to_intensity_correction_parameters_affine(
self,
jacobian_slice_nda,
slice_nda):
# Add the Jacobian w.r.t. intensity correction parameter (slope) to
# existing Jacobian
jacobian_slice_nda = \
self._add_gradient_with_respect_to_intensity_correction_parameters_linear(
jacobian_slice_nda, slice_nda)
# Add the Jacobian w.r.t. intensity correction parameter (bias) to
# existing Jacobian
jacobian_slice_nda = np.concatenate(
(jacobian_slice_nda, np.ones((self._N_slice_voxels, 1))), axis=1)
return jacobian_slice_nda
##
# Gets the projected 2d slices of stack.
# \date 2016-11-21 19:59:13+0000
#
# \param self The object
# \param stack The stack
# \param image_transform_reference_fit_term Either "identity" or "gradient_magnitude"
#
# \return The projected 2d slices of stack.
#
def _get_projected_2D_slices_of_stack(self,
stack,
registration_image_type="identity"):
slices_3D = stack.get_slices()
slices_2D = [None] * self._N_slices
if registration_image_type in ["partial_derivative"]:
dy_slices_2D = [None] * self._N_slices
for i in range(0, self._N_slices):
# Create copy of the slices (since its header will be updated)
slice_3D = sl.Slice.from_slice(slices_3D[i])
# Get transform to get axis aligned slice of original stack
# T_PP = self._get_TPP_transform(slice_3D.sitk)
T_PP = self._get_TPP_transform(slices_3D[0].sitk)
# Get current transform from image to physical space of slice
T_PI = sitkh.get_sitk_affine_transform_from_sitk_image(
slice_3D.sitk)
# Get transform to align slice with physical coordinate system
# (perhaps already shifted there)
T_PI_align = sitkh.get_composite_sitk_affine_transform(T_PP, T_PI)
# Set direction and origin of image accordingly
origin_3D_sitk = \
sitkh.get_sitk_image_origin_from_sitk_affine_transform(
T_PI_align, slice_3D.sitk)
direction_3D_sitk = \
sitkh.get_sitk_image_direction_from_sitk_affine_transform(
T_PI_align, slice_3D.sitk)
slice_3D.sitk.SetDirection(direction_3D_sitk)
slice_3D.sitk.SetOrigin(origin_3D_sitk)
slice_3D.sitk_mask.SetDirection(direction_3D_sitk)
slice_3D.sitk_mask.SetOrigin(origin_3D_sitk)
# Get filename and slice number for name propagation
filename = slice_3D.get_filename()
slice_number = slice_3D.get_slice_number()
slice_2D_sitk = slice_3D.sitk[:, :, 0]
slice_2D_sitk_mask = slice_3D.sitk_mask[:, :, 0]
if registration_image_type in ["identity"]:
slices_2D[i] = sl.Slice.from_sitk_image(
slice_sitk=slice_2D_sitk,
filename=filename,
slice_number=slice_number,
slice_sitk_mask=slice_2D_sitk_mask,
slice_thickness=slice_3D.get_slice_thickness(),
)
elif registration_image_type in ["gradient_magnitude"]:
# print("Gradient magnitude of image")
gradient_magnitude_slice_2D_sitk = \
self._gradient_magnitude_filter_sitk.Execute(slice_2D_sitk)
slices_2D[i] = sl.Slice.from_sitk_image(
slice_sitk=gradient_magnitude_slice_2D_sitk,
filename="GradMagn_" + filename,
slice_number=slice_number,
slice_sitk_mask=slice_2D_sitk_mask,
slice_thickness=slice_3D.get_slice_thickness(),
)
elif registration_image_type in ["partial_derivative"]:
# print("Partial derivatives of image")
dx_slice_2D_sitk = self._get_dx_image_sitk(slice_2D_sitk)
dy_slice_2D_sitk = self._get_dy_image_sitk(slice_2D_sitk)
slices_2D[i] = sl.Slice.from_sitk_image(
slice_sitk=dx_slice_2D_sitk,
dir_input=None,
filename="dx_" + filename,
slice_number=slice_number,
slice_sitk_mask=slice_2D_sitk_mask,
slice_thickness=slice_3D.get_slice_thickness(),
)
dy_slices_2D[i] = sl.Slice.from_sitk_image(
slice_sitk=dy_slice_2D_sitk,
dir_input=None,
filename="dy_" + filename,
slice_number=slice_number,
slice_sitk_mask=slice_2D_sitk_mask,
slice_thickness=slice_3D.get_slice_thickness(),
)
# Debug
# sitkh.show_sitk_image([slice_3D.sitk[:,:,0],slice_2D_sitk], title=["standard_slice"+str(i), "gradient_magnitude_slice"+str(i)])
# ph.pause()
# ph.killall_itksnap()
if registration_image_type in ["partial_derivative"]:
return slices_2D, dy_slices_2D
else:
return slices_2D
##
# Get the 3D rigid transforms to arrive at the positions of original 3D
# slices starting from the physically aligned space with the main image
# axes.
# \date 2016-09-20 23:37:05+0100
#
# The rigid transform is given as composed translation and rotation
# transform, i.e. T_PP = (T_t \c irc T_rot)^{-1}.
#
# \param self The object
#
# \return List of 3D rigid transforms (sitk.AffineTransform(3) objects)
# to arrive at the positions of the original 3D slices.
#
# TODO: Change to make simpler
#
def _get_TPP_transform(self, slice_sitk):
origin_3D_sitk = np.array(slice_sitk.GetOrigin())
direction_3D_sitk = np.array(slice_sitk.GetDirection())
T_PP = sitk.AffineTransform(3)
T_PP.SetMatrix(direction_3D_sitk)
T_PP.SetTranslation(origin_3D_sitk)
T_PP = sitk.AffineTransform(T_PP.GetInverse())
return T_PP
"""
Transform specific parts from here
"""
def _new_rigid_transform_sitk(self):
return sitk.Euler2DTransform()
def _new_rigid_transform_itk(self):
return itk.Euler2DTransform.New()
def _new_similarity_transform_sitk(self):
return sitk.Similarity2DTransform()
def _new_similarity_transform_itk(self):
return itk.Similarity2DTransform.New()
def _new_affine_transform_sitk(self):
return sitk.AffineTransform(2)
def _new_affine_transform_itk(self):
return itk.AffineTransform.D2.New()
##
# Perform motion correction based on performed registration to get motion
# corrected stack and associated slice transforms.
# \date 2016-11-21 20:11:53+0000
#
# \param self The object
# \post self._stack_corrected updated
# \post self._slice_transforms_sitk updated
#
def _apply_motion_correction(self):
self._apply_motion_correction_and_compute_slice_transforms[
self._transform_type]()
##
# Apply motion correction after rigid registration
# \date 2016-11-21 20:14:05+0000
#
# \param self The object
# \post self._stack_corrected updated
# \post self._slice_transforms_sitk updated
#
def _apply_rigid_motion_correction_and_compute_slice_transforms(self):
stack_corrected = st.Stack.from_stack(self._stack)
slices_corrected = stack_corrected.get_slices()
slices = self._stack.get_slices()
slice_transforms_sitk = [None] * self._N_slices
for i in range(0, self._N_slices):
# Set transform for the 2D slice based on registration transform
self._transforms_2D_sitk[i].SetParameters(
self._parameters[i, 0:self._transform_type_dofs])
# Invert it to physically move the slice
transform_2D_sitk = sitk.Euler2DTransform(
self._transforms_2D_sitk[i].GetInverse())
# Expand to 3D transform
transform_3D_sitk = self._get_3D_from_2D_rigid_transform_sitk(
transform_2D_sitk)
# Get transform to get axis aligned slice
# T_PP = self._get_TPP_transform(slices[i].sitk)
T_PP = self._get_TPP_transform(slices[0].sitk)
# Compose to 3D in-plane transform
affine_transform_sitk = sitkh.get_composite_sitk_affine_transform(
transform_3D_sitk, T_PP)
affine_transform_sitk = sitkh.get_composite_sitk_affine_transform(
sitk.AffineTransform(T_PP.GetInverse()), affine_transform_sitk)
# Update motion correction of slice
slices_corrected[i].update_motion_correction(affine_transform_sitk)
# Keep slice transform
slice_transforms_sitk[i] = affine_transform_sitk
self._stack_corrected = stack_corrected
self._slice_transforms_sitk = slice_transforms_sitk
##
# Apply motion correction after similarity registration
# \date 2016-11-21 20:14:42+0000
#
# \param self The object
# \post self._stack_corrected updated
# \post self._slice_transforms_sitk updated
# \return { description_of_the_return_value }
#
def _apply_similarity_motion_correction_and_compute_slice_transforms(self):
stack_corrected = st.Stack.from_stack(self._stack)
slices_corrected = stack_corrected.get_slices()
slices = self._stack.get_slices()
slice_transforms_sitk = [None] * self._N_slices
for i in range(0, self._N_slices):
# Set transform for the 2D slice based on registration transform
self._transforms_2D_sitk[i].SetParameters(
self._parameters[i, 0:self._transform_type_dofs])
# Invert it to physically move the slice
similarity_2D_sitk = sitk.Similarity2DTransform(
self._transforms_2D_sitk[i].GetInverse())
# Convert to 2D rigid registration transform
scale = similarity_2D_sitk.GetScale()
origin = np.array(self._slices_2D[i].sitk.GetOrigin())
center = np.array(similarity_2D_sitk.GetCenter())
angle = similarity_2D_sitk.GetAngle()
translation = np.array(similarity_2D_sitk.GetTranslation())
R = np.array(similarity_2D_sitk.GetMatrix()).reshape(2, 2) / scale
# if self._use_verbose:
# print("Slice %2d/%d: in-plane scaling factor = %.3f" %(i, self._N_slices-1, 1/scale))
rigid_2D_sitk = sitk.Euler2DTransform()
rigid_2D_sitk.SetAngle(angle)
rigid_2D_sitk.SetTranslation(
scale * R.dot(origin - center) - R.dot(origin) + translation + center)
# Expand to 3D rigid transform
rigid_3D_sitk = self._get_3D_from_2D_rigid_transform_sitk(
rigid_2D_sitk)
# Get transform to get axis aligned slice
# T_PP = self._get_TPP_transform(slices[i].sitk)
T_PP = self._get_TPP_transform(slices[0].sitk)
# Compose to 3D in-plane transform
affine_transform_sitk = sitkh.get_composite_sitk_affine_transform(
rigid_3D_sitk, T_PP)
affine_transform_sitk = sitkh.get_composite_sitk_affine_transform(
sitk.AffineTransform(T_PP.GetInverse()), affine_transform_sitk)
# Update motion correction of slice
slices_corrected[i].update_motion_correction(affine_transform_sitk)
# Update spacing of slice accordingly
spacing = np.array(slices[i].sitk.GetSpacing())
spacing[0:-1] *= scale
slices_corrected[i].sitk.SetSpacing(spacing)
slices_corrected[i].sitk_mask.SetSpacing(spacing)
slices_corrected[i].itk = sitkh.get_itk_from_sitk_image(
slices_corrected[i].sitk)
slices_corrected[i].itk_mask = \
sitkh.get_itk_from_sitk_image(slices_corrected[i].sitk_mask)
# Update affine transform (including scaling information)
affine_3D_sitk = sitk.AffineTransform(3)
affine_matrix_sitk = np.array(
rigid_3D_sitk.GetMatrix()).reshape(3, 3)
affine_matrix_sitk[0:-1, 0:-1] *= scale
affine_3D_sitk.SetMatrix(affine_matrix_sitk.flatten())
affine_3D_sitk.SetCenter(rigid_3D_sitk.GetCenter())
affine_3D_sitk.SetTranslation(rigid_3D_sitk.GetTranslation())
affine_3D_sitk = sitkh.get_composite_sitk_affine_transform(
affine_3D_sitk, T_PP)
affine_3D_sitk = sitkh.get_composite_sitk_affine_transform(
sitk.AffineTransform(T_PP.GetInverse()), affine_3D_sitk)
# Keep affine slice transform
slice_transforms_sitk[i] = affine_3D_sitk
self._stack_corrected = stack_corrected
self._slice_transforms_sitk = slice_transforms_sitk
##
# Apply motion correction after affine registration
# \date 2016-11-21 20:14:05+0000
#
# \param self The object
# \post self._stack_corrected updated
# \post self._slice_transforms_sitk updated
#
def _apply_affine_motion_correction_and_compute_slice_transforms(self):
stack_corrected = st.Stack.from_stack(self._stack)
slices_corrected = stack_corrected.get_slices()
slices = self._stack.get_slices()
slice_transforms_sitk = [None] * self._N_slices
for i in range(0, self._N_slices):
# Set transform for the 2D slice based on registration transform
self._transforms_2D_sitk[i].SetParameters(
self._parameters[i, 0:self._transform_type_dofs])
# Invert it to physically move the slice
transform_2D_sitk = sitk.AffineTransform(
self._transforms_2D_sitk[i].GetInverse())
# Expand to 3D transform
transform_3D_sitk = self._get_3D_from_2D_affine_transform_sitk(
transform_2D_sitk)
# Get transform to get axis aligned slice
# T_PP = self._get_TPP_transform(slices[i].sitk)
T_PP = self._get_TPP_transform(slices[0].sitk)
# Compose to 3D in-plane transform
affine_transform_sitk = sitkh.get_composite_sitk_affine_transform(
transform_3D_sitk, T_PP)
affine_transform_sitk = sitkh.get_composite_sitk_affine_transform(
sitk.AffineTransform(T_PP.GetInverse()), affine_transform_sitk)
# Update motion correction of slice
slices_corrected[i].update_motion_correction(affine_transform_sitk)
# Keep slice transform
slice_transforms_sitk[i] = affine_transform_sitk
self._stack_corrected = stack_corrected
self._slice_transforms_sitk = slice_transforms_sitk
##
# Create 3D from 2D transform.
# \date 2016-09-20 23:18:55+0100
#
# The generated 3D transform performs in-plane operations in case the
# physical coordinate system is aligned with the axis of the stack/slice
#
# \param self The object
# \param rigid_transform_2D_sitk sitk.Euler2DTransform object
#
# \return sitk.Euler3DTransform object.
#
def _get_3D_from_2D_rigid_transform_sitk(self, rigid_transform_2D_sitk):
# Get parameters of 2D registration
angle_z, translation_x, translation_y = \
rigid_transform_2D_sitk.GetParameters()
center_x, center_y = rigid_transform_2D_sitk.GetCenter()
# Expand obtained translation to 3D vector
translation_3D = (translation_x, translation_y, 0)
center_3D = (center_x, center_y, 0)
# Create 3D rigid transform based on 2D
rigid_transform_3D = sitk.Euler3DTransform()
rigid_transform_3D.SetRotation(0, 0, angle_z)
rigid_transform_3D.SetTranslation(translation_3D)
# Append zero for m_ComputeZYX = 0 (part of fixed params in SimpleITK
# 1.0.0)
rigid_transform_3D.SetFixedParameters(center_3D + (0.,))
return rigid_transform_3D
##
# Create 3D from 2D transform.
# \date 2016-09-20 23:18:55+0100
#
# The generated 3D transform performs in-plane operations in case the
# physical coordinate system is aligned with the axis of the stack/slice
#
# \param self The object
# \param rigid_transform_2D_sitk sitk.Euler2DTransform object
#
# \return sitk.Euler3DTransform object.
#
def _get_3D_from_2D_affine_transform_sitk(self, affine_transform_2D_sitk):
# Get parameters of 2D registration
a00, a01, a10, a11, translation_x, translation_y = \
affine_transform_2D_sitk.GetParameters()
center_x, center_y = affine_transform_2D_sitk.GetCenter()
# Expand obtained translation to 3D vector
translation_3D = (translation_x, translation_y, 0)
center_3D = (center_x, center_y, 0)
matrix_3D = np.eye(3).flatten()
matrix_3D[0] = a00
matrix_3D[1] = a01
matrix_3D[3] = a10
matrix_3D[4] = a11
# Create 3D affine transform based on 2D
affine_transform_3D = sitk.AffineTransform(3)
affine_transform_3D.SetMatrix(matrix_3D)
affine_transform_3D.SetTranslation(translation_3D)
affine_transform_3D.SetFixedParameters(center_3D)
return affine_transform_3D
================================================
FILE: niftymic/registration/niftyreg.py
================================================
##
# \file niftyreg.py
# \brief Class to use registration method NiftyReg
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date Aug 2017
#
# Import libraries
import os
import numpy as np
import SimpleITK as sitk
from abc import ABCMeta, abstractmethod
import pysitk.python_helper as ph
import simplereg.niftyreg
import niftymic.base.stack as st
from niftymic.registration.registration_method \
import RegistrationMethod
from niftymic.registration.registration_method \
import AffineRegistrationMethod
class RegAladin(AffineRegistrationMethod):
def __init__(self,
fixed=None,
moving=None,
use_fixed_mask=False,
use_moving_mask=False,
use_verbose=False,
options="-voff",
registration_type="Rigid",
):
AffineRegistrationMethod.__init__(self,
fixed=fixed,
moving=moving,
use_fixed_mask=use_fixed_mask,
use_moving_mask=use_moving_mask,
use_verbose=use_verbose,
registration_type=registration_type,
)
# Allowed registration types for NiftyReg
self._REGISTRATION_TYPES = ["Rigid", "Affine"]
self._options = options
##
# Sets the options used for FLIRT
# \date 2017-08-08 19:57:47+0100
#
# \param self The object
# \param options The options as string
#
def set_options(self, options):
self._options = options
##
# Gets the options.
# \date 2017-08-08 19:58:14+0100
#
# \param self The object
#
# \return The options as string.
#
def get_options(self):
return self._options
def _run(self):
if self._use_fixed_mask:
fixed_sitk_mask = self._fixed.sitk_mask
else:
fixed_sitk_mask = None
if self._use_moving_mask:
moving_sitk_mask = self._moving.sitk_mask
else:
moving_sitk_mask = None
options = self._options
if self.get_registration_type() == "Rigid":
options += " -rigOnly"
self._registration_method = simplereg.niftyreg.RegAladin(
fixed_sitk=self._fixed.sitk,
moving_sitk=self._moving.sitk,
fixed_sitk_mask=fixed_sitk_mask,
moving_sitk_mask=moving_sitk_mask,
options=options,
verbose=self._use_verbose,
)
try:
self._registration_method.run()
except RuntimeError as e:
raise RuntimeError(
"%s\n\n"
"Check whether image/mask coverage is sufficient between the "
"images '%s' (fixed) and '%s' (moving).\n" % (
e,
self._fixed.get_filename(),
self._moving.get_filename()),
)
self._registration_transform_sitk = \
self._registration_method.get_registration_transform_sitk()
def _get_warped_moving_sitk(self):
return self._registration_method.get_warped_moving_sitk()
class RegF3D(RegistrationMethod):
def __init__(self,
fixed=None,
moving=None,
use_fixed_mask=False,
use_moving_mask=False,
use_verbose=False,
options="-voff",
):
RegistrationMethod.__init__(self,
fixed=fixed,
moving=moving,
use_fixed_mask=use_fixed_mask,
use_moving_mask=use_moving_mask,
use_verbose=use_verbose,
)
self._options = options
##
# Sets the options used for FLIRT
# \date 2017-08-08 19:57:47+0100
#
# \param self The object
# \param options The options as string
#
def set_options(self, options):
self._options = options
##
# Gets the options.
# \date 2017-08-08 19:58:14+0100
#
# \param self The object
#
# \return The options as string.
#
def get_options(self):
return self._options
def _run(self):
if self._use_fixed_mask:
fixed_sitk_mask = self._fixed.sitk_mask
else:
fixed_sitk_mask = None
if self._use_moving_mask:
moving_sitk_mask = self._moving.sitk_mask
else:
moving_sitk_mask = None
options = self._options
self._registration_method = simplereg.niftyreg.RegF3D(
fixed_sitk=self._fixed.sitk,
moving_sitk=self._moving.sitk,
fixed_sitk_mask=fixed_sitk_mask,
moving_sitk_mask=moving_sitk_mask,
options=options,
verbose=self._use_verbose,
)
self._registration_method.run()
self._registration_transform_sitk = \
self._registration_method.get_registration_transform_sitk()
##
# Gets the warped moving image, i.e. moving image warped and resampled to
# the fixed grid
# \date 2017-08-08 16:58:30+0100
#
# \param self The object
#
# \return The warped moving image as Stack/Slice object
#
def get_warped_moving(self):
warped_moving_mask_sitk = \
self._registration_method.get_deformed_image_sitk(
fixed_sitk=self._fixed.sitk_mask,
moving_sitk=self._moving.sitk_mask,
interpolation_order=0,
)
if isinstance(self._moving, st.Stack):
warped_moving = st.Stack.from_sitk_image(
image_sitk=self._registration_method.get_warped_moving_sitk(),
filename=self._moving.get_filename(),
image_sitk_mask=warped_moving_mask_sitk
)
else:
warped_moving = sl.Slice.from_sitk_image(
image_sitk=self._registration_method.get_warped_moving_sitk(),
filename=self._moving.get_filename(),
image_sitk_mask=warped_moving_mask_sitk
)
return warped_moving
================================================
FILE: niftymic/registration/registration_method.py
================================================
##
# \file registration_method.py
# \brief Abstract class to define a registration method
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date Aug 2017
#
# Import libraries
import numpy as np
import SimpleITK as sitk
from abc import ABCMeta, abstractmethod
import pysitk.python_helper as ph
import niftymic.base.stack as st
import niftymic.base.slice as sl
##
# Abstract class for registration methods
# \date 2017-08-09 11:22:51+0100
#
class RegistrationMethod(object):
__metaclass__ = ABCMeta
##
# Store information for registration methods and initialize additional
# variables
# \date 2017-08-09 11:23:14+0100
#
# \param self The object
# \param fixed Fixed image as Stack/Slice object
# \param moving The moving
# \param use_fixed_mask The use fixed mask
# \param use_moving_mask The use moving mask
# \param use_verbose The use_verbose
#
def __init__(self,
fixed,
moving,
use_fixed_mask,
use_moving_mask,
use_verbose):
self._fixed = fixed
self._moving = moving
self._use_fixed_mask = use_fixed_mask
self._use_moving_mask = use_moving_mask
self._use_verbose = use_verbose
self._computational_time = ph.get_zero_time()
self._registration_method = None
##
# Sets the fixed image
# \date 2017-08-08 16:45:45+0100
#
# \param self The object
# \param fixed The fixed image as Stack/Slice object
#
def set_fixed(self, fixed):
self._fixed = fixed
##
# Gets the fixed image
# \date 2017-08-08 16:45:58+0100
#
# \param self The object
#
# \return The fixed image as Stack/Slice object.
#
def get_fixed(self):
return self._fixed
##
# Sets the moving image
# \date 2017-08-08 16:45:45+0100
#
# \param self The object
# \param moving The moving image as Stack/Slice object
#
#
def set_moving(self, moving):
self._moving = moving
##
# Gets the moving image
# \date 2017-08-08 16:45:58+0100
#
# \param self The object
#
# \return The moving image as Stack/Slice object.
#
def get_moving(self):
return self._moving
##
# Specify whether fixed mask shall be used for registration
# \date 2017-08-08 16:48:03+0100
#
# \param self The object
# \param use_fixed_mask Turn on/off use of fixed mask; bool
#
def use_fixed_mask(self, use_fixed_mask):
self._use_fixed_mask = use_fixed_mask
##
# Specify whether moving mask shall be used for registration
# \date 2017-08-08 16:48:03+0100
#
# \param self The object
# \param use_moving_mask Turn on/off use of moving mask; bool
#
def use_moving_mask(self, use_moving_mask):
self._use_moving_mask = use_moving_mask
##
# Sets the use_verbose.
# \date 2017-08-08 16:50:13+0100
#
# \param self The object
# \param use_verbose Turn on/off use_verbose output; bool
#
def use_verbose(self, use_verbose):
self._use_verbose = use_verbose
##
# Gets the computational time it took to perform the registration
# \date 2017-08-08 16:59:45+0100
#
# \param self The object
#
# \return The computational time.
#
def get_computational_time(self):
return self._computational_time
##
# Gets the obtained registration transform.
# \date 2017-08-08 16:52:36+0100
#
# \param self The object
#
# \return The registration transform as sitk object.
#
def get_registration_transform_sitk(self):
return self._registration_transform_sitk
##
# Run the registration method
# \date 2017-08-08 17:01:01+0100
#
# \param self The object
#
def run(self):
if not isinstance(self._fixed, st.Stack) and \
not isinstance(self._fixed, sl.Slice):
raise TypeError("Fixed image must be of type 'Stack' or 'Slice'")
if not isinstance(self._moving, st.Stack) and \
not isinstance(self._moving, sl.Slice):
raise TypeError("Moving image must be of type 'Stack' or 'Slice'")
time_start = ph.start_timing()
# Execute registration method
self._run()
# Get computational time
self._computational_time = ph.stop_timing(time_start)
if self._use_verbose:
ph.print_info("Required computational time: %s" %
(self.get_computational_time()))
@abstractmethod
def _run(self):
pass
##
# Gets the warped moving image, i.e. moving image warped and resampled to
# the fixed grid
# \date 2017-08-08 16:58:30+0100
#
# \param self The object
#
# \return The warped moving image as Stack/Slice object
#
@abstractmethod
def get_warped_moving(self):
pass
##
# Abstract class for affine registration methods
# \date 2017-08-09 11:22:51+0100
#
class AffineRegistrationMethod(RegistrationMethod):
__metaclass__ = ABCMeta
##
# Store information for registration methods and initialize additional
# variables
# \date 2017-08-09 11:23:14+0100
#
# \param self The object
# \param fixed Fixed image as Stack/Slice object
# \param moving The moving
# \param use_fixed_mask The use fixed mask
# \param use_moving_mask The use moving mask
# \param use_verbose The use_verbose
#
def __init__(self,
fixed,
moving,
use_fixed_mask,
use_moving_mask,
use_verbose,
registration_type,
):
RegistrationMethod.__init__(self,
fixed=fixed,
moving=moving,
use_fixed_mask=use_fixed_mask,
use_moving_mask=use_moving_mask,
use_verbose=use_verbose,
)
self._registration_type = registration_type
##
# Sets the registration type.
# \date 2017-02-02 16:42:13+0000
#
# \param self The object
# \param registration_type The registration type
#
def set_registration_type(self, registration_type):
if registration_type not in self._REGISTRATION_TYPES:
raise ValueError("Possible registration types: " +
str(self._REGISTRATION_TYPES))
self._registration_type = registration_type
##
# Gets the registration type.
# \date 2017-08-08 19:58:30+0100
#
# \param self The object
#
# \return The registration type as string.
#
def get_registration_type(self):
return self._registration_type
##
# Gets the warped moving image, i.e. moving image warped and resampled to
# the fixed grid
# \date 2017-08-08 16:58:30+0100
#
# \param self The object
#
# \return The warped moving image as Stack/Slice object
#
def get_warped_moving(self):
warped_moving_sitk_mask = sitk.Resample(
self._moving.sitk_mask,
self._fixed.sitk,
self.get_registration_transform_sitk(),
sitk.sitkNearestNeighbor,
0,
self._moving.sitk_mask.GetPixelIDValue(),
)
if isinstance(self._moving, st.Stack):
warped_moving = st.Stack.from_sitk_image(
image_sitk=self._get_warped_moving_sitk(),
filename=self._moving.get_filename(),
image_sitk_mask=warped_moving_sitk_mask,
slice_thickness=self._fixed.get_slice_thickness(),
)
else:
warped_moving = sl.Slice.from_sitk_image(
image_sitk=self._get_warped_moving_sitk(),
filename=self._moving.get_filename(),
image_sitk_mask=warped_moving_sitk_mask,
slice_thickness=self._fixed.get_slice_thickness(),
)
return warped_moving
##
# Gets the fixed image transformed by the obtained registration transform.
#
# The returned image will align the fixed image with the moving image as
# found during the registration.
# \date 2017-08-08 16:53:21+0100
#
# \param self The object
#
# \return The transformed fixed as Stack/Slice object
#
def get_transformed_fixed(self):
fixed = st.Stack.from_stack(self._fixed)
fixed.update_motion_correction(self.get_registration_transform_sitk())
return fixed
@abstractmethod
def _get_warped_moving_sitk(self):
pass
================================================
FILE: niftymic/registration/simple_itk_registration.py
================================================
##
# \file simple_itk_registration.py
# \brief Class to use registration method based on SimpleITK
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date Aug 2017
# Import libraries
import os
import numpy as np
import itk
import SimpleITK as sitk
# Used to parse variable arguments to SimpleITK object, see
# http://stackoverflow.com/questions/20263839/python-convert-a-string-to-arguments-list:
from ast import literal_eval
import pysitk.simple_itk_helper as sitkh
import pysitk.python_helper as ph
import simplereg.simple_itk_registration
import niftymic.base.psf as psf
import niftymic.base.stack as st
from niftymic.registration.registration_method \
import AffineRegistrationMethod
##
# Class to use registration method FLIRT
# \date 2017-08-09 11:22:33+0100
#
class SimpleItkRegistration(AffineRegistrationMethod):
def __init__(
self,
fixed=None,
moving=None,
use_fixed_mask=False,
use_moving_mask=False,
registration_type="Rigid",
interpolator="Linear",
metric="Correlation",
metric_params=None,
# optimizer="ConjugateGradientLineSearch",
# optimizer_params={
# "learningRate": 1,
# "numberOfIterations": 100,
# },
optimizer="RegularStepGradientDescent",
optimizer_params={
"minStep": 1e-6,
"numberOfIterations": 200,
"gradientMagnitudeTolerance": 1e-6,
"learningRate": 1,
},
scales_estimator="PhysicalShift",
initializer_type=None,
use_oriented_psf=False,
use_multiresolution_framework=False,
shrink_factors=[2, 1],
smoothing_sigmas=[1, 0],
use_verbose=False,
):
AffineRegistrationMethod.__init__(self,
fixed=fixed,
moving=moving,
use_fixed_mask=use_fixed_mask,
use_moving_mask=use_moving_mask,
use_verbose=use_verbose,
registration_type=registration_type,
)
self._REGISTRATION_TYPES = ["Rigid", "Similarity", "Affine"]
self._INITIALIZER_TYPES = [None, "MOMENTS", "GEOMETRY",
"SelfGEOMETRY", "SelfMOMENTS"]
self._SCALES_ESTIMATORS = ["IndexShift", "PhysicalShift", "Jacobian"]
self._interpolator = interpolator
self._metric = metric
self._metric_params = metric_params
self._optimizer = optimizer
self._optimizer_params = optimizer_params
self._scales_estimator = scales_estimator
self._initializer_type = initializer_type
self._use_oriented_psf = use_oriented_psf
self._use_multiresolution_framework = use_multiresolution_framework
self._shrink_factors = shrink_factors
self._smoothing_sigmas = smoothing_sigmas
# Use multiresolution framework
# \param[in] flag boolean
def use_multiresolution_framework(self, flag):
self._use_multiresolution_framework = flag
# Decide whether oriented PSF shall be applied, i.e. blur moving image
# with (axis aligned) Gaussian kernel given by the relative position of
# the coordinate systems of fixed and moving
# \param[in] flag boolean
def use_oriented_psf(self, flag):
self._use_oriented_psf = flag
# Set type of centered transform initializer
# \param[in] initializer_type
def set_initializer_type(self, initializer_type):
if initializer_type not in self._INITIALIZER_TYPES:
raise ValueError("Possible initializer types: " +
str(self._INITIALIZER_TYPES))
else:
self._initializer_type = initializer_type
# Get type of centered transform initializer
def get_initializer_type(self):
return self._initializer_type
# Set interpolator
# \param[in] interpolator_type
def set_interpolator(self, interpolator_type):
self._interpolator = interpolator_type
# Get interpolator
# \return interpolator as string
def get_interpolator(self):
return self._interpolator
def set_metric(self, metric):
self._metric = metric
def set_metric_params(self, metric_params):
self._metric_params = metric_params
def set_optimizer(self, optimizer):
self._optimizer = optimizer
def set_optimizer_params(self, optimizer_params):
self._optimizer_params = optimizer_params
# Set optimizer scales
# \param[in] scales
def set_scales_estimator(self, scales_estimator):
if scales_estimator not in self._SCALES_ESTIMATORS:
raise ValueError("Possible optimizer scales: " +
str(self._SCALES_ESTIMATORS))
else:
self._scales_estimator = scales_estimator
def _run(self):
if self._use_fixed_mask:
fixed_sitk_mask = self._fixed.sitk_mask
else:
fixed_sitk_mask = None
if self._use_moving_mask:
moving_sitk_mask = self._moving.sitk_mask
else:
moving_sitk_mask = None
# Blur moving image with oriented Gaussian prior to the registration
if self._use_oriented_psf:
# Get oriented Gaussian covariance matrix
cov_HR_coord = psf.PSF(
).get_covariance_matrix_in_reconstruction_space(
self._fixed, self._moving)
# Create recursive YVV Gaussianfilter
image_type = itk.Image[itk.D, self._fixed.sitk.GetDimension()]
gaussian_yvv = itk.SmoothingRecursiveYvvGaussianImageFilter[
image_type, image_type].New()
# Feed Gaussian filter with axis aligned covariance matrix
sigma_axis_aligned = np.sqrt(np.diagonal(cov_HR_coord))
print("Oriented PSF blurring with (axis aligned) sigma = " +
str(sigma_axis_aligned))
print("\t(Based on computed covariance matrix = ")
for i in range(0, 3):
print("\t\t" + str(cov_HR_coord[i, :]))
print("\twith square root of diagonal " +
str(np.diagonal(cov_HR_coord)) + ")")
gaussian_yvv.SetInput(self._moving.itk)
gaussian_yvv.SetSigmaArray(sigma_axis_aligned)
gaussian_yvv.Update()
moving_itk = gaussian_yvv.GetOutput()
moving_itk.DisconnectPipeline()
moving_sitk = sitkh.get_sitk_from_itk_image(moving_itk)
else:
moving_sitk = self._moving.sitk
self._registration_method = \
simplereg.simple_itk_registration.SimpleItkRegistration(
fixed_sitk=self._fixed.sitk,
moving_sitk=moving_sitk,
fixed_sitk_mask=fixed_sitk_mask,
moving_sitk_mask=moving_sitk_mask,
registration_type=self._registration_type,
interpolator=self._interpolator,
metric=self._metric,
metric_params=self._metric_params,
optimizer=self._optimizer,
optimizer_params=self._optimizer_params,
initializer_type=self._initializer_type,
use_multiresolution_framework=self._use_multiresolution_framework,
optimizer_scales=self._scales_estimator,
shrink_factors=self._shrink_factors,
smoothing_sigmas=self._smoothing_sigmas,
verbose=self._use_verbose,
)
self._registration_method.run()
self._registration_transform_sitk = \
self._registration_method.get_registration_transform_sitk()
def _get_warped_moving_sitk(self):
warped_moving_sitk = sitk.Resample(
self._moving.sitk,
self._fixed.sitk,
self.get_registration_transform_sitk(),
eval("sitk.sitk%s" % (self._interpolator)),
0.,
self._moving.sitk.GetPixelIDValue()
)
return warped_moving_sitk
================================================
FILE: niftymic/registration/stack_registration_base.py
================================================
##
# \file stack_registration_base.py
# \brief Abstract class containing the shared attributes and functions for
# registrations of stack of slices.
#
# Class has been mainly developed for the CIS30FU project.
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date Nov 2016
#
# Import libraries
from abc import ABCMeta, abstractmethod
import sys
import SimpleITK as sitk
import itk
import numpy as np
import time
from datetime import timedelta
from scipy.optimize import least_squares
from scipy.optimize import minimize
# Import modules
import pysitk.simple_itk_helper as sitkh
import pysitk.python_helper as ph
from nsol.loss_functions import LossFunctions as lf
import niftymic.base.stack as st
import niftymic.utilities.parameter_normalization as pn
##
# Abstract class containing the shared attributes and functions for
# registrations of stack of slices
# \date 2016-11-06 16:58:15+0000
#
class StackRegistrationBase(object):
__metaclass__ = ABCMeta
##
# Constructor
# \date 2016-11-06 16:58:43+0000
#
# \param self The object
# \param stack The stack to be aligned as
# Stack object
# \param reference The reference used for
# alignment as Stack object
# \param use_stack_mask Use stack mask for
# registration, bool
# \param use_reference_mask Use reference mask for
# registration, bool
# \param use_verbose Verbose output, bool
# \param transform_initializer_type The transform initializer
# type, e.g. "identity",
# "moments" or "geometry"
# \param interpolator The interpolator
# \param alpha_neighbour Weight >= 0 for neighbour
# term
# \param alpha_reference Weight >= 0 for reference
# term
# \param alpha_parameter Weight >= 0 for prior term
# \param use_parameter_normalization Use parameter
# normalization for optimizer, bool
# \param optimizer Either "least_squares" to
# use
# scipy.optimize.least_squares
# or any method used in
# "scipy.optimize.minimize",
# e.g. "L-BFGS-B".
# \param optimizer_iter_max Maximum number of
# iterations/function
# evaluations
# \param optimizer_loss Loss function, e.g.
# "linear", "soft_l1" or
# "huber".
# \param optimizer_method The optimizer method used
# for "least_squares"
# algorithm. E.g. "trf"
#
def __init__(self,
stack=None,
reference=None,
use_stack_mask=False,
use_reference_mask=False,
use_verbose=False,
transform_initializer_type="identity",
interpolator="Linear",
alpha_neighbour=1,
alpha_reference=1,
alpha_parameter=1,
use_parameter_normalization=False,
optimizer="L-BFGS-B",
optimizer_iter_max=20,
optimizer_loss="soft_l1",
optimizer_method="trf", # Only counts for least_squares
):
# Set Fixed and reference stacks
if stack is not None:
self._stack = st.Stack.from_stack(stack)
self._N_slices = self._stack.get_number_of_slices()
else:
self._stack = None
if reference is not None:
self._reference = st.Stack.from_stack(reference)
else:
self._reference = None
# Set booleans to use mask
self._use_stack_mask = use_stack_mask
self._use_reference_mask = use_reference_mask
# Parameters for solver
self._optimizer = optimizer
self._optimizer_iter_max = optimizer_iter_max
self._optimizer_loss = optimizer_loss
self._optimizer_method = optimizer_method
# Verbose computation
self._use_verbose = use_verbose
# Initializer type
self._get_initial_transforms_and_parameters = {
"identity": self._get_initial_transforms_and_parameters_identity,
"moments": self._get_initial_transforms_and_parameters_geometry_moments,
"geometry": self._get_initial_transforms_and_parameters_geometry_moments
}
self._dictionary_transform_initializer_type_sitk = {
"identity": None,
"moments": "MOMENTS",
"geometry": "GEOMETRY"
}
self._transform_initializer_type = transform_initializer_type
# Interpolator
self._interpolator = interpolator
self._interpolator_sitk = eval("sitk.sitk" + self._interpolator)
# Set weights for cost function of each term/residual
self._alpha_neighbour = alpha_neighbour
self._alpha_reference = alpha_reference
self._alpha_parameter = alpha_parameter
self._use_parameter_normalization = use_parameter_normalization
self._ZERO = 1e-8
##
# Sets stack/reference/target image.
# \date 2016-11-06 16:59:14+0000
#
# \param self The object
# \param stack stack as Stack object
#
def set_stack(self, stack):
self._stack = st.Stack.from_stack(stack)
self._N_slices = self._stack.get_number_of_slices()
def get_stack(self):
return self._stack
##
# Sets reference stack.
# \date 2016-11-06 17:00:50+0000
#
# \param self The object
# \param reference reference stack as Stack object
#
def set_reference(self, reference):
self._reference = st.Stack.from_Stack(reference)
def get_reference(self):
return self._reference
##
# Specify whether mask of stack image shall be used for
# registration
# \date 2016-11-06 17:03:05+0000
#
# \param self The object
# \param flag The flag as boolean
#
def use_stack_mask(self, flag):
self._use_stack_mask = flag
##
# Specify whether mask of reference image shall be used for
# registration
# \date 2016-11-06 17:03:05+0000
#
# \param self The object
# \param flag The flag as boolean
#
def use_reference_mask(self, flag):
self._use_reference_mask = flag
##
# Specify whether output information shall be produced.
# \date 2016-11-06 17:07:01+0000
#
# \param self The object
# \param flag The flag
#
def use_verbose(self, flag):
self._use_verbose = flag
##
# Perform parameter normalization for optimizer
# \date 2016-11-17 16:10:14+0000
#
# \param self The object
# \param flag The flag, boolean
#
def use_parameter_normalization(self, flag):
self._use_parameter_normalization = flag
##
# Sets the initializer type used to initialize the registration
# \date 2016-11-08 00:20:29+0000
#
# The initial transform can either be the identity ('None') or be based on
# the moments ('moments') or geometry ('geometry') of the stack and
# reference image.
#
# \param self The object
# \param transform_initializer_type The initializer type to be either 'None',
# 'moments' or 'geometry'
#
def set_transform_initializer_type(self, transform_initializer_type):
if transform_initializer_type not in ["identity", "moments", "geometry"]:
raise ValueError(
"Error: centered transform initializer type can only be 'identity', moments' or 'geometry'")
self._transform_initializer_type = transform_initializer_type
def get_transform_initializer_type(self):
return self._transform_initializer_type
##
# Sets the interpolator used for resampling operations
# \date 2016-11-08 16:19:33+0000
#
# \param self The object
# \param interpolator The interpolator as string
#
def set_interpolator(self, interpolator):
self._interpolator = interpolator
self._interpolator_sitk = eval("sitk.sitk" + self._interpolator)
def get_interpolator(self):
return self._interpolator
##
# Sets the weight for the residual between the slice neighbours
# \date 2016-11-10 00:59:59+0000
#
# \param self The object
# \param alpha_neighbour The alpha neighbour
#
def set_alpha_neighbour(self, alpha_neighbour):
self._alpha_neighbour = alpha_neighbour
def get_alpha_neighbour(self):
return self._alpha_neighbour
##
# Sets the weight for the residual between the slice neighbours
# and the reference
# \date 2016-11-10 01:00:41+0000
#
# \param self The object
# \param alpha_reference The alpha reference
#
def set_alpha_reference(self, alpha_reference):
self._alpha_reference = alpha_reference
def get_alpha_reference(self):
return self._alpha_reference
##
# Sets the weight for the residual between the slice neighbours
# \date 2016-11-10 01:01:18+0000
#
# \param self The object
# \param alpha_parameter The alpha parameter
#
# \return { description_of_the_return_value }
#
def set_alpha_parameter(self, alpha_parameter):
self._alpha_parameter = alpha_parameter
def get_alpha_parameter(self):
return self._alpha_parameter
def set_optimizer(self, optimizer):
self._optimizer = optimizer
def get_optimizer(self):
return self._optimizer
##
# Set maximum number of iterations for optimizer.
#
# least_squares: Corresponds to maximum number of function evaluations
# L-BFGS-B: Corresponds to maximum number of iterations
# \date 2016-11-10 19:24:35+0000
#
# \param self The object
# \param optimizer_iter_max The nfev maximum
#
# \see https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.least_squares.html#scipy.optimize.least_squares
#
def set_optimizer_iter_max(self, optimizer_iter_max):
self._optimizer_iter_max = optimizer_iter_max
def get_optimizer_iter_max(self):
return self._optimizer_iter_max
##
# Sets the optimizer_loss function for least_squares optimizer
# \date 2016-11-17 16:07:29+0000
#
# \param self The object
# \param optimizer_loss The optimizer_loss in ["linear", "soft_l1", "huber", "cauchy",
# "arctan"]
#
# \see https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.least_squares.html#scipy.optimize.least_squares
#
def set_optimizer_loss(self, optimizer_loss):
if optimizer_loss not in ["linear", "soft_l1", "huber", "cauchy", "arctan"]:
raise ValueError(
"Optimizer optimizer_loss for least_squares must either be 'linear', 'soft_l1', 'huber', 'cauchy' or 'arctan'.")
self._optimizer_loss = optimizer_loss
def get_optimizer_loss(self):
return self._optimizer_loss
##
# Sets the optimizer_method for least_squares optimizer
# \date 2016-11-17 16:08:37+0000
#
# \param self The object
# \param optimizer_method The optimizer_method in ["trf", "lm", "dogbox"]
#
# \see https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.least_squares.html#scipy.optimize.least_squares
#
def set_optimizer_method(self, optimizer_method):
if optimizer_method not in ["trf", "lm", "dogbox"]:
raise ValueError(
"Optimizer optimizer_method for least_squares must either be 'trf', 'lm' or 'dogbox'.")
self._optimizer_method = optimizer_method
def get_optimizer_method(self):
return self._optimizer_method
##
# Gets the parameters estimated by registration algorithm.
# \date 2016-11-06 17:05:38+0000
#
# \param self The object
#
# \return The parameters.
#
def get_parameters(self):
return np.array(self._parameters)
##
# Gets the registered stack.
# \date 2016-11-08 19:44:15+0000
#
# \param self The object
#
# \return The registered stack with motion corrected slices
#
def get_corrected_stack(self):
return st.Stack.from_stack(self._stack_corrected)
##
# Gets the parameters information.
# \date 2016-11-08 14:56:12+0000
#
# \param self The object
#
# \return The parameters information as list of strings describing the
# meaning of each element in parameters
#
# @abstractmethod
# def get_parameters_info(self):
# pass
##
# Gets the registraton transform sitk.
# \date 2016-11-06 17:10:14+0000
#
# \param self The object
#
# \return The registraton transforms sitk.
#
def get_slice_transforms_sitk(self):
return np.array(self._slice_transforms_sitk)
##
# Print statistics associated to performed registration
# \date 2016-11-06 17:07:56+0000
#
# \param self The object
#
def print_statistics(self):
# print("\nStatistics for performed registration:" %(self._reg_type))
# if self._elapsed_time_sec < 0:
# raise ValueError("Error: Elapsed time has not been measured. Run 'run' first.")
# else:
print("\tElapsed time: %s" % (self._elapsed_time))
# print("\tell^2-residual sum_k ||M_k(A_k x - y_k||_2^2 = %.3e" %(self._residual_ell2))
# print("\tprior residual = %.3e" %(self._residual_prior))
##
# Run the registration
# \date 2016-11-10 01:39:03+0000
#
# \param self The object
#
def run(self):
print_precisicion = 3
print_suppress = True
if self._optimizer_method in ["lm"]:
verbose = 1
if self._optimizer_loss not in ["linear"]:
self._optimizer_loss = "linear"
print("Optimizer method 'lm' only supports 'linear' loss function. ")
else:
verbose = 2
jac = '2-point'
# jac = '3-point'
x_scale = 1.0 # or array
# x_scale = 'jac' #or array
# Initialize registration pipeline
self._run_registration_pipeline_initialization()
if self._use_verbose:
print("Initial values = ")
ph.print_numpy_array(
self._parameters, precision=print_precisicion, suppress=print_suppress)
# Parameter normalization
if self._use_parameter_normalization:
parameter_normalization = pn.ParameterNormalization(
self._parameters)
parameter_normalization.compute_normalization_coefficients()
coefficients = parameter_normalization.get_normalization_coefficients()
# Use absolute mean for normalization
scale = abs(np.array(coefficients[0]))
# scale could be zero (like for rotation)
scale[np.where(scale == 0)] = 1
if self._use_verbose:
print("Normalization parameters:")
ph.print_numpy_array(
scale, precision=print_precisicion, suppress=print_suppress)
# Each slice with the same scaling
x_scale = np.tile(scale, self._parameters.shape[0])
# HACK
self._transforms_2D_itk = [None]*self._N_slices
for i in range(0, self._N_slices):
self._transforms_2D_itk[i] = self._new_transform_itk[
self._transform_type]()
self._transforms_2D_itk[i].SetParameters(
itk.OptimizerParameters[itk.D](self._transforms_2D_sitk[i].GetParameters()))
self._transforms_2D_itk[i].SetFixedParameters(itk.OptimizerParameters[itk.D](
self._transforms_2D_sitk[i].GetFixedParameters()))
# Get cost function and its Jacobian w.r.t. the parameters
fun = self._get_residual_call()
jac = self._get_jacobian_residual_call()
x0 = self._parameters0_vec.flatten()
time_start = ph.start_timing()
if self._optimizer == "least_squares":
self._print_info_text_least_squares()
res = self._run_optimizer_least_squares(
fun=fun,
jac=jac,
x0=x0,
method=self._optimizer_method,
loss=self._optimizer_loss,
iter_max=self._optimizer_iter_max,
verbose=verbose,
x_scale=x_scale)
else:
self._print_info_text_minimize()
res = self._run_optimizer_minimize(
fun=fun,
jac=jac,
x0=x0,
method=self._optimizer,
loss=self._optimizer_loss,
iter_max=self._optimizer_iter_max,
verbose=verbose,
x_scale=x_scale)
self._elapsed_time = ph.stop_timing(time_start)
# Get and reshape final transform parameters for each slice
self._parameters = res.reshape(self._parameters.shape)
# Denormalize parameters
# self._parameters = self._parameter_normalizer.denormalize_parameters(self._parameters)
if self._use_verbose:
print("Final values = ")
ph.print_numpy_array(
self._parameters, precision=print_precisicion, suppress=print_suppress)
# if self._use_verbose:
# print("Final values = ")
# print(self._parameters)
# Apply motion correction and compute slice transforms
self._apply_motion_correction()
##
# Use scipy.opimize.least_squares solver
#
def _run_optimizer_least_squares(self, fun, jac, x0, method, loss, iter_max, verbose, x_scale):
# Non-linear least-squares optimizer_method:
res = least_squares(
fun=fun,
jac=jac,
x0=x0,
method=method,
loss=loss,
max_nfev=iter_max,
verbose=verbose,
x_scale=x_scale)
return res.x
##
# Use scipy.opimize.minimize solver
#
def _run_optimizer_minimize(self, fun, jac, x0, method, loss, iter_max, verbose, x_scale):
# Convert to cost and gradient of cost function.
fun_ = lambda x: lf.get_ell2_cost_from_residual(
fun(x),
loss=loss)
jac_ = lambda x: lf.get_gradient_ell2_cost_from_residual(
fun(x),
jac(x),
loss=loss)
# Use scipy.optimize.minimize method
res = minimize(
method=method,
fun=fun_,
jac=jac_,
x0=x0,
options={'maxiter': iter_max, 'disp': verbose},
)
return res.x
@abstractmethod
def _print_info_text_least_squares(self):
pass
@abstractmethod
def _print_info_text_minimize(self):
pass
##
# optimizer_Method to initialize the registration with all
# precomputations which can be done before the actual
# optimization.
# \date 2016-11-10 01:37:13+0000
#
# \param self The object
#
@abstractmethod
def _run_registration_pipeline_initialization(self):
pass
##
# Gets the residual call used for the least_squares
# optimization routine
# \date 2016-11-10 01:38:08+0000
#
# \param self The object
#
# \return The residual call.
#
@abstractmethod
def _get_residual_call(self):
pass
##
# Gets the initial parameters in case of identity transform.
# \date 2016-11-08 15:06:54+0000
#
# \param self The object
#
# \return The initial parameters corresponding to identity transform.
#
@abstractmethod
def _get_initial_transforms_and_parameters_identity(self):
pass
##
# Gets the initial parameters for either 'geometry' or
# 'moments'.
# \date 2016-11-08 15:08:07+0000
#
# \param self The object
#
# \return The initial parameters corresponding to 'geometry' or
# 'moments'.
#
@abstractmethod
def _get_initial_transforms_and_parameters_geometry_moments(self):
pass
##
# optimizer_Method that applies the obtained registration transforms to
# update the slices positions and to get the affine slice
# transforms capturing the performed motion correction.
# \date 2016-11-10 01:34:42+0000
#
# \param self The object
#
@abstractmethod
def _apply_motion_correction(self):
pass
================================================
FILE: niftymic/registration/transform_initializer.py
================================================
# \file transform_initializer.py
# \brief Class to obtain transform estimate to align fixed with moving
# image
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date Feb 2019
#
import os
import numpy as np
import SimpleITK as sitk
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
import nsol.principal_component_analysis as pca
from nsol.similarity_measures import SimilarityMeasures
import niftymic.base.stack as st
import niftymic.validation.image_similarity_evaluator as ise
import niftymic.utilities.template_stack_estimator as tse
from niftymic.definitions import DIR_TMP
##
# Class to obtain transform estimate to align fixed with moving image
# \date 2019-02-20 17:47:54+0000
#
class TransformInitializer(object):
def __init__(self,
fixed,
moving,
similarity_measure="NMI",
refine_pca_initializations=False,
):
if not isinstance(fixed, st.Stack):
raise TypeError("Fixed image must be of type 'Stack'.")
if not isinstance(moving, st.Stack):
raise TypeError("Moving image must be of type 'Stack'.")
self._fixed = fixed
self._moving = moving
self._similarity_measure = similarity_measure
self._refine_pca_initializations = refine_pca_initializations
self._initial_transform_sitk = None
def get_transform_sitk(self):
return self._initial_transform_sitk
def run(self, debug=False):
# perform PCAs for fixed and moving images
pca_moving = self.get_pca_from_mask(self._moving.sitk_mask)
eigvec_moving = pca_moving.get_eigvec()
mean_moving = pca_moving.get_mean()
pca_fixed = self.get_pca_from_mask(self._fixed.sitk_mask)
eigvec_fixed = pca_fixed.get_eigvec()
mean_fixed = pca_fixed.get_mean()
# test different initializations based on eigenvector orientations
orientations = [
[1, 1],
[1, -1],
[-1, 1],
[-1, -1],
]
transformations = []
for i_o, orientation in enumerate(orientations):
eigvec_moving_o = np.array(eigvec_moving)
eigvec_moving_o[:, 0] *= orientation[0]
eigvec_moving_o[:, 1] *= orientation[1]
# get right-handed coordinate system
cross = np.cross(eigvec_moving_o[:, 0], eigvec_moving_o[:, 1])
eigvec_moving_o[:, 2] = cross
# transformation to align fixed with moving eigenbasis
R = eigvec_moving_o.dot(eigvec_fixed.transpose())
t = mean_moving - R.dot(mean_fixed)
# build rigid transformation as sitk object
rigid_transform_sitk = sitk.Euler3DTransform()
rigid_transform_sitk.SetMatrix(R.flatten())
rigid_transform_sitk.SetTranslation(t)
transformations.append(rigid_transform_sitk)
# get best transformation according to selected similarity measure
self._initial_transform_sitk = self._get_best_transform(
transformations, debug=debug)
@staticmethod
def get_pca_from_mask(mask_sitk, robust=False):
mask_nda = sitk.GetArrayFromImage(mask_sitk)
# get largest connected region (if more than one connected region)
mask_nda = tse.TemplateStackEstimator.get_largest_connected_region_mask(
mask_nda)
# [z, y, x] x n_points to [x, y, z] x n_points
points = np.array(np.where(mask_nda > 0))[::-1, :]
n_points = len(points[0])
for i in range(n_points):
points[:, i] = mask_sitk.TransformIndexToPhysicalPoint(
[int(j) for j in points[:, i]])
if robust:
pca_mask = pca.AdmmRobustPrincipalComponentAnalysis(
points.transpose())
res = pca_mask.run()
pca_mask = pca.PrincipalComponentAnalysis(res["X3_admm"])
pca_mask.run()
else:
pca_mask = pca.PrincipalComponentAnalysis(points.transpose())
pca_mask.run()
return pca_mask
def _get_best_transform(self, transformations, debug=False):
if self._refine_pca_initializations:
transformations = self._run_registrations(transformations)
warps = []
for transform_sitk in transformations:
warped_moving_sitk = sitk.Resample(
self._moving.sitk,
self._fixed.sitk,
transform_sitk,
sitk.sitkLinear,
)
warps.append(
st.Stack.from_sitk_image(
warped_moving_sitk,
extract_slices=False,
slice_thickness=self._fixed.get_slice_thickness(),
))
image_similarity_evaluator = ise.ImageSimilarityEvaluator(
stacks=warps,
reference=self._fixed,
measures=[self._similarity_measure],
use_reference_mask=True,
verbose=False,
)
ph.print_info(
"Find best aligning transform as measured by %s" %
self._similarity_measure)
image_similarity_evaluator.compute_similarities()
similarities = image_similarity_evaluator.get_similarities()
# get transform which leads to highest similarity
index = np.argmax(similarities[self._similarity_measure])
transform_init_sitk = transformations[index]
if debug:
labels = ["attempt%d" % (d + 1)
for d in range(len(transformations))]
labels[index] = "best"
foo = [w.sitk for w in warps]
foo.insert(0, self._fixed.sitk)
labels.insert(0, "fixed")
sitkh.show_sitk_image(
foo,
segmentation=self._fixed.sitk_mask,
label=labels,
)
for i in range(len(transformations)):
print("%s: %.6f" % (
labels[1 + i], similarities[self._similarity_measure][i])
)
return transform_init_sitk
def _run_registrations(self, transformations):
path_to_fixed = os.path.join(DIR_TMP, "fixed.nii.gz")
path_to_moving = os.path.join(DIR_TMP, "moving.nii.gz")
path_to_fixed_mask = os.path.join(DIR_TMP, "fixed_mask.nii.gz")
path_to_moving_mask = os.path.join(DIR_TMP, "moving_mask.nii.gz")
path_to_tmp_output = os.path.join(DIR_TMP, "foo.nii.gz")
path_to_transform_regaladin = os.path.join(
DIR_TMP, "transform_regaladin.txt")
path_to_transform_sitk = os.path.join(
DIR_TMP, "transform_sitk.txt")
sitkh.write_nifti_image_sitk(self._fixed.sitk, path_to_fixed)
sitkh.write_nifti_image_sitk(self._moving.sitk, path_to_moving)
sitkh.write_nifti_image_sitk(self._fixed.sitk_mask, path_to_fixed_mask)
# sitkh.write_nifti_image_sitk(
# self._moving.sitk_mask, path_to_moving_mask)
for i in range(len(transformations)):
sitk.WriteTransform(transformations[i], path_to_transform_sitk)
# Convert SimpleITK to RegAladin transform
cmd = "simplereg_transform -sitk2nreg %s %s" % (
path_to_transform_sitk, path_to_transform_regaladin)
ph.execute_command(cmd, verbose=False)
# Run NiftyReg
cmd_args = ["reg_aladin"]
cmd_args.append("-ref %s" % path_to_fixed)
cmd_args.append("-flo %s" % path_to_moving)
cmd_args.append("-res %s" % path_to_tmp_output)
cmd_args.append("-inaff %s" % path_to_transform_regaladin)
cmd_args.append("-aff %s" % path_to_transform_regaladin)
cmd_args.append("-rigOnly")
cmd_args.append("-ln 2")
cmd_args.append("-voff")
cmd_args.append("-rmask %s" % path_to_fixed_mask)
# To avoid error "0 correspondences between blocks were found" that can
# occur for some cases. Also, disable moving mask, as this would be ignored
# anyway
cmd_args.append("-noSym")
ph.print_info(
"Run Registration (RegAladin) based on PCA-init %d ... "
% (i + 1))
ph.execute_command(" ".join(cmd_args), verbose=False)
# Convert RegAladin to SimpleITK transform
cmd = "simplereg_transform -nreg2sitk %s %s" % (
path_to_transform_regaladin, path_to_transform_sitk)
ph.execute_command(cmd, verbose=False)
transformations[i] = sitkh.read_transform_sitk(
path_to_transform_sitk)
return transformations
================================================
FILE: niftymic/registration/wrap_itk_registration.py
================================================
##
# \file wrap_itk_registration.py
# \brief Class to use registration method based on SimpleITK
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date Aug 2017
#
# Import libraries
import os
import numpy as np
import itk
import SimpleITK as sitk
# Used to parse variable arguments to SimpleITK object, see
# http://stackoverflow.com/questions/20263839/python-convert-a-string-to-arguments-list:
from ast import literal_eval
import pysitk.python_helper as ph
import simplereg.wrap_itk_registration
import niftymic.base.psf as psf
import niftymic.base.stack as st
from niftymic.registration.simple_itk_registration \
import SimpleItkRegistration
##
# Class to use registration method FLIRT
# \date 2017-08-09 11:22:33+0100
#
class WrapItkRegistration(SimpleItkRegistration):
def __init__(
self,
fixed=None,
moving=None,
use_fixed_mask=False,
use_moving_mask=False,
registration_type="Rigid",
interpolator="Linear",
metric="Correlation",
metric_params=None,
# optimizer="ConjugateGradientLineSearch",
# optimizer_params={
# "learningRate": 1,
# "numberOfIterations": 100,
# },
optimizer="RegularStepGradientDescent",
optimizer_params={
"MinimumStepLength": 1e-6,
"NumberOfIterations": 200,
"GradientMagnitudeTolerance": 1e-6,
"LearningRate": 1,
# "RelaxationFactor": 0.5,
},
scales_estimator="PhysicalShift",
initializer_type=None,
use_oriented_psf=False,
use_multiresolution_framework=False,
shrink_factors=[4, 2, 1],
smoothing_sigmas=[2, 1, 0],
use_verbose=False,
alpha_cut=3,
):
SimpleItkRegistration.__init__(
self,
fixed=fixed,
moving=moving,
use_fixed_mask=use_fixed_mask,
use_moving_mask=use_moving_mask,
registration_type=registration_type,
interpolator=interpolator,
metric=metric,
metric_params=metric_params,
optimizer=optimizer,
optimizer_params=optimizer_params,
scales_estimator=scales_estimator,
initializer_type=initializer_type,
use_oriented_psf=use_oriented_psf,
use_multiresolution_framework=use_multiresolution_framework,
shrink_factors=shrink_factors,
smoothing_sigmas=smoothing_sigmas,
use_verbose=use_verbose,
)
self._alpha_cut = alpha_cut
self._pixel_type = itk.D
def _run(self):
dimension = self._fixed.sitk.GetDimension()
if self._use_fixed_mask:
fixed_itk_mask = self._fixed.itk_mask
else:
fixed_itk_mask = None
if self._use_moving_mask:
moving_itk_mask = self._moving.itk_mask
else:
moving_itk_mask = None
# Blur moving image with oriented Gaussian prior to the registration
if self._use_oriented_psf:
image_type = itk.Image[self._pixel_type, dimension]
# Get oriented Gaussian covariance matrix
cov_HR_coord = psf.PSF(
).get_covariance_matrix_in_reconstruction_space(
self._fixed, self._moving)
itk_gaussian_interpolator = itk.OrientedGaussianInterpolateImageFunction[
image_type, self._pixel_type].New()
itk_gaussian_interpolator.SetCovariance(cov_HR_coord.flatten())
itk_gaussian_interpolator.SetAlpha(self._alpha_cut)
else:
itk_gaussian_interpolator = None
self._registration_method = \
simplereg.wrap_itk_registration.WrapItkRegistration(
dimension=dimension,
fixed_itk=self._fixed.itk,
moving_itk=self._moving.itk,
fixed_itk_mask=fixed_itk_mask,
moving_itk_mask=moving_itk_mask,
registration_type=self._registration_type,
interpolator=self._interpolator,
metric=self._metric,
# metric_params=self._metric_params,
optimizer=self._optimizer,
optimizer_params=self._optimizer_params,
initializer_type=self._initializer_type,
use_multiresolution_framework=self._use_multiresolution_framework,
# optimizer_scales=self._scales_estimator,
shrink_factors=self._shrink_factors,
smoothing_sigmas=self._smoothing_sigmas,
verbose=self._use_verbose,
itk_oriented_gaussian_interpolate_image_filter=itk_gaussian_interpolator,
)
self._registration_method.run()
self._registration_transform_sitk = \
self._registration_method.get_registration_transform_sitk()
================================================
FILE: niftymic/utilities/__init__.py
================================================
================================================
FILE: niftymic/utilities/binary_mask_from_mask_srr_estimator.py
================================================
##
# \file binary_mask_from_mask_srr_estimator.py
# \brief Class to estimate binary mask from mask SRR stack
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date January 2019
#
import os
import re
import numpy as np
import SimpleITK as sitk
import pysitk.simple_itk_helper as sitkh
import niftymic.base.stack as st
import niftymic.utilities.template_stack_estimator as tse
##
# Class to estimate binary mask from mask SRR stack
# \date 2019-01-15 16:35:36+0000
#
class BinaryMaskFromMaskSRREstimator(object):
def __init__(self,
srr_mask_sitk,
suffix="_mask",
sigma=2,
lower=0.5,
upper=100,
):
if not isinstance(srr_mask_sitk, sitk.Image):
raise ValueError("Input must be of type sitk.Image")
self._srr_mask_sitk = srr_mask_sitk
self._suffix = suffix
self._sigma = sigma
self._lower = lower
self._upper = upper
self._mask_sitk = None
self._mask = None
def get_mask_sitk(self):
return sitk.Image(self._mask_sitk)
def run(self):
mask_sitk = self._srr_mask_sitk
# Smooth mask
mask_sitk = sitk.SmoothingRecursiveGaussian(mask_sitk, self._sigma)
# Binarize images given thresholds
mask_sitk = sitk.BinaryThreshold(
mask_sitk, lowerThreshold=self._lower, upperThreshold=self._upper)
# Keep largest connected region only
nda = sitk.GetArrayFromImage(mask_sitk)
nda = tse.TemplateStackEstimator.get_largest_connected_region_mask(nda)
self._mask_sitk = sitk.GetImageFromArray(nda)
self._mask_sitk.CopyInformation(mask_sitk)
================================================
FILE: niftymic/utilities/brain_stripping.py
================================================
##
# \file brain_stripping.py
# \brief This class implements the interface to the Brain Extraction Tool
# (BET) to automatically segment the brain and/or the skull.
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date Oct 2016
#
import os
import sys
import itk
import SimpleITK as sitk
import numpy as np
import nipype.interfaces.fsl
import pysitk.simple_itk_helper as sitkh
import pysitk.python_helper as ph
import niftymic.base.stack as st
from niftymic.definitions import DIR_TMP
##
# This class implements the interface to the Brain Extraction Tool (BET)
# \date 2017-10-26 18:11:17+0100
#
class BrainStripping(object):
##
# Constructor
# \date 2016-10-12 12:43:38+0100
#
# \param self The object
# \param compute_brain_image Boolean flag for computing brain image
# \param compute_brain_mask Boolean flag for computing brain image
# mask
# \param compute_skull_image Boolean flag for computing skull mask
# \param dir_tmp Directory where temporary results are
# written to, string
# \param bet_options The bet options
#
def __init__(self,
compute_brain_image=False,
compute_brain_mask=True,
compute_skull_image=False,
dir_tmp=os.path.join(DIR_TMP, "BrainExtractionTool"),
bet_options=""):
self._compute_brain_image = compute_brain_image
self._compute_brain_mask = compute_brain_mask
self._compute_skull_image = compute_skull_image
self._dir_tmp = dir_tmp
self._bet_options = bet_options
self._sitk = None
self._sitk_brain_image = None
self._sitk_brain_mask = None
self._sitk_skull_image = None
self._stack = None
##
# Initialize brain stripping class based on image to be read
# \date 2016-10-12 12:19:18+0100
#
# \param cls The cls
# \param dir_input The dir input
# \param filename The filename
# \param compute_brain_image Boolean flag for computing brain image
# \param compute_brain_mask Boolean flag for computing brain image
# mask
# \param compute_skull_image Boolean flag for computing skull mask
# \param dir_tmp Directory where temporary results are
# written to, string
#
# \return object
#
@classmethod
def from_filename(cls,
dir_input,
filename,
compute_brain_image=False,
compute_brain_mask=True,
compute_skull_image=False,
dir_tmp=os.path.join(DIR_TMP, "BrainExtractionTool")):
self = cls(compute_brain_image=compute_brain_image,
compute_brain_mask=compute_brain_mask,
compute_skull_image=compute_skull_image,
dir_tmp=dir_tmp)
self._sitk = sitkh.read_nifti_image_sitk(
os.path.join(dir_input, "%s.nii.gz" % filename),
sitk.sitkFloat64)
return self
##
# Initialize brain stripping class based on given sitk.Image object
# \date 2016-10-12 12:18:35+0100
#
# \param cls The cls
# \param sitk_image The sitk image
# \param compute_brain_image Boolean flag for computing brain image
# \param compute_brain_mask Boolean flag for computing brain image
# mask
# \param compute_skull_image Boolean flag for computing skull mask
# \param dir_tmp Directory where temporary results are
# written to, string
#
# \return object
#
@classmethod
def from_sitk_image(cls,
sitk_image,
compute_brain_image=False,
compute_brain_mask=True,
compute_skull_image=False,
dir_tmp=os.path.join(DIR_TMP, "BrainExtractionTool")):
self = cls(compute_brain_image=compute_brain_image,
compute_brain_mask=compute_brain_mask,
compute_skull_image=compute_skull_image,
dir_tmp=dir_tmp)
self._sitk = sitk.Image(sitk_image)
return self
##
# Initialize brain stripping class based on given Stack object
# \date 2018-01-18 00:38:53+0000
#
# \param cls The cls
# \param stack image as Stack object
# \param compute_brain_image Boolean flag for computing brain image
# \param compute_brain_mask Boolean flag for computing brain image
# mask
# \param compute_skull_image Boolean flag for computing skull mask
# \param dir_tmp Directory where temporary results are
# written to, string
#
# \return object
#
@classmethod
def from_stack(cls,
stack,
compute_brain_image=False,
compute_brain_mask=True,
compute_skull_image=False,
dir_tmp=os.path.join(DIR_TMP, "BrainExtractionTool")):
self = cls(compute_brain_image=compute_brain_image,
compute_brain_mask=compute_brain_mask,
compute_skull_image=compute_skull_image,
dir_tmp=dir_tmp)
self._stack = stack
self._sitk = sitk.Image(stack.sitk)
return self
##
# Sets the sitk image for brain stripping
# \date 2016-10-12 15:46:20+0100
#
# \param self The object
# \param sitk_image The sitk image as sitk.Image object
#
#
def set_input_image_sitk(self, sitk_image):
self._sitk = sitk.Image(sitk_image)
##
# Set flag of whether or not to compute the brain image
# \date 2016-10-12 12:35:46+0100
#
# \param self The object
# \param compute_brain_image Boolean flag
#
def compute_brain_image(self, compute_brain_image):
self._compute_brain_image = compute_brain_image
##
# Set flag of whether or not to compute the brain image mask
# \date 2016-10-12 12:36:46+0100
#
# \param self The object
# \param compute_brain_mask Boolean flag
#
def compute_brain_mask(self, compute_brain_mask):
self._compute_brain_mask = compute_brain_mask
##
# Set flag of whether or not to compute the skull mask
# \date 2016-10-12 12:37:06+0100
#
# \param self The object
# \param compute_skull_image Boolean flag
#
def compute_skull_image(self, compute_skull_image):
self._compute_skull_image = compute_skull_image
##
# Set Brain Extraction Tool specific options
# \date 2016-10-12 14:38:38+0100
#
# \param self The object
# \param bet_options The bet options, string
#
def set_bet_options(self, bet_options):
self._bet_options = bet_options
##
# Gets the input image
# \date 2016-10-12 14:41:05+0100
#
# \param self The object
#
# \return The input image as sitk.Image object
#
def get_input_image_sitk(self):
if self._sitk is None:
raise ValueError("Input image was not read yet.")
return sitk.Image(self._sitk)
##
# Gets the brain masked stack.
# \date 2018-01-18 00:44:49+0000
#
# \param self The object
# \param filename The filename
# \param extract_slices Extract slices of stack; boolean
#
# \return Returns image as Stack object holding obtained brain mask
#
def get_brain_masked_stack(self, filename="Unknown", extract_slices=False):
if self._sitk_brain_mask is None:
raise ValueError("Brain mask was not asked for. "
"Set option '-m' and run again.")
if self._stack is not None:
filename = self._stack.get_filename()
stack = st.Stack.from_sitk_image(
image_sitk=self._sitk,
image_sitk_mask=self._sitk_brain_mask,
filename=filename,
extract_slices=extract_slices
)
return stack
##
# Get computed brain image
# \date 2016-10-12 14:33:53+0100
#
# \param self The object
#
# \return The brain image as sitk object.
#
def get_brain_image_sitk(self):
if self._sitk_brain_image is None:
raise ValueError("Brain was not asked for. "
"Do not set option '-n' and run again.")
return self._sitk_brain_image
##
# Get computed brain image mask
# \date 2016-10-12 14:33:53+0100
#
# \param self The object
#
# \return The brain mask as sitk.Image object
#
def get_brain_mask_sitk(self, dilate_radius=0):
if self._sitk_brain_mask is None:
raise ValueError("Brain mask was not asked for. "
"Set option '-m' and run again.")
if dilate_radius > 0:
# Chose kernel
kernel_sitk = sitk.sitkBall
# kernel_sitk = sitk.sitkBox
# kernel_sitk = sitk.sitkAnnulus
# kernel_sitk = sitk.sitkCross
# Define dilate and erode image filter
dilater = sitk.BinaryDilateImageFilter()
dilater.SetKernelType(kernel_sitk)
dilater.SetKernelRadius(dilate_radius)
brain_mask_sitk = dilater.Execute(self._sitk_brain_mask)
else:
brain_mask_sitk = sitk.Image(self._sitk_brain_mask)
return brain_mask_sitk
##
# Get computed skull image mask
# \date 2016-10-12 14:33:53+0100
#
# \param self The object
# \param dilate_radius The dilate radius
# \param erode_radius The erode radius
# \param kernel The kernel in "Ball", "Box", "Annulus" or
# "Cross"
#
# \return The skull mask image as sitk object.
#
def get_skull_mask_sitk(self,
dilate_radius=10,
erode_radius=0,
kernel="Ball"):
if self._sitk_skull_image is None:
raise ValueError(
"Skull mask was not asked for. Set option '-s' and run again.")
skull_mask_sitk = sitk.Image(self._sitk_skull_image)
# Skull mask from BET has values of either 0 or 100. Threshold to 0,1
thresholder = sitk.BinaryThresholdImageFilter()
thresholder.SetUpperThreshold(255)
thresholder.SetLowerThreshold(1)
skull_mask_sitk = thresholder.Execute(skull_mask_sitk)
# Translate kernel
kernel_sitk = eval("sitk.sitk" + kernel)
# Define dilate and erode image filter
if dilate_radius > 0:
dilater = sitk.BinaryDilateImageFilter()
dilater.SetKernelType(kernel_sitk)
dilater.SetKernelRadius(dilate_radius)
skull_mask_sitk = dilater.Execute(skull_mask_sitk)
if erode_radius > 0:
eroder = sitk.BinaryErodeImageFilter()
eroder.SetKernelType(kernel_sitk)
eroder.SetKernelRadius(erode_radius)
skull_mask_sitk = eroder.Execute(skull_mask_sitk)
return skull_mask_sitk
##
# Gets the mask around skull which covers also a bit of the brain. (It was
# used for the MS project)
# \date 2016-11-06 22:54:28+0000
#
# \param self The object
# \param dilate_radius The dilate radius
# \param erode_radius The erode radius
# \param kernel The kernel in "Ball", "Box", "Annulus" or
# "Cross"
#
# \return The mask around skull.
#
def get_mask_around_skull(self,
dilate_radius=10,
erode_radius=0,
kernel="Ball"):
# Translate kernel
kernel_sitk = eval("sitk.sitk" + kernel)
# Define dilate and erode image filter
dilater = sitk.BinaryDilateImageFilter()
dilater.SetKernelType(kernel_sitk)
dilater.SetKernelRadius(dilate_radius)
eroder = sitk.BinaryErodeImageFilter()
eroder.SetKernelType(kernel_sitk)
eroder.SetKernelRadius(erode_radius)
# Get complement of brain mask
mask_sitk = 1 - self._sitk_brain_mask
shape = np.array(self._sitk_brain_mask.GetSize()[::-1])
mask_nda = np.zeros((shape[0], shape[1], shape[2]))
# Go slice by slice
for i in range(0, shape[0]):
slice_mask_sitk = mask_sitk[:, :, i:i + 1]
# Dilate mask of slice
if dilate_radius > 0:
slice_mask_sitk = dilater.Execute(slice_mask_sitk)
# Erode mask of slice
if erode_radius > 0:
slice_mask_sitk = eroder.Execute(slice_mask_sitk)
# Fill data array information
mask_nda[i, :, :] = sitk.GetArrayFromImage(slice_mask_sitk)
# Convert mask back to 3D image
skull_mask_sitk = sitk.GetImageFromArray(mask_nda)
skull_mask_sitk.CopyInformation(self._sitk_brain_mask)
# Debug:
# sitkh.show_sitk_image(
# self._sitk,
# segmentation=skull_mask_sitk,
# title="stack_brain_mask")
return skull_mask_sitk
##
# Run Brain Extraction Tool given the chosen set of parameters
# \date 2016-10-12 14:59:01+0100
#
# \param self The object
#
def run(self):
self._run_bet_for_brain_stripping()
##
# Run Brain Extraction Tool
# \date 2016-10-12 14:59:24+0100
#
# \param self The object
# \post self._sitk* are filled with respective images
#
def _run_bet_for_brain_stripping(self, debug=0):
filename_out = "image"
self._dir_tmp = ph.create_directory(self._dir_tmp, delete_files=True)
path_to_image = os.path.join(
self._dir_tmp, filename_out + ".nii.gz")
path_to_res = os.path.join(
self._dir_tmp, filename_out + "_bet.nii.gz")
path_to_res_mask = os.path.join(
self._dir_tmp, filename_out + "_bet_mask.nii.gz")
path_to_res_skull = os.path.join(
self._dir_tmp, filename_out + "_bet_skull.nii.gz")
sitkh.write_nifti_image_sitk(self._sitk, path_to_image)
bet = nipype.interfaces.fsl.BET()
bet.inputs.in_file = path_to_image
bet.inputs.out_file = path_to_res
options = ""
if not self._compute_brain_image:
options += "-n "
if self._compute_brain_mask:
options += "-m "
if self._compute_skull_image:
options += "-s "
options += self._bet_options
bet.inputs.args = options
if debug:
print(bet.cmdline)
bet.run()
if self._compute_brain_image:
self._sitk_brain_image = sitkh.read_nifti_image_sitk(
path_to_res, sitk.sitkFloat64)
if self._compute_brain_mask:
self._sitk_brain_mask = sitkh.read_nifti_image_sitk(
path_to_res_mask, sitk.sitkUInt8)
if self._compute_skull_image:
self._sitk_skull_image = sitkh.read_nifti_image_sitk(
path_to_res_skull)
================================================
FILE: niftymic/utilities/data_preprocessing.py
================================================
##
# \file data_preprocessing.py
# \brief Performs preprocessing steps
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date May 2017
#
import numpy as np
import niftymic.base.stack as st
import niftymic.utilities.intensity_correction as ic
import niftymic.utilities.n4_bias_field_correction as n4bfc
import niftymic.base.exceptions as exceptions
import pysitk.python_helper as ph
##
# Class implementing data preprocessing steps
#
class DataPreprocessing:
##
# Initialize data preprocessing class based on list of Stacks
# \date 2017-05-12 00:49:43+0100
#
# \param self The object
# \param stacks List of Stack instances
# \param use_N4BiasFieldCorrector Use N4 bias field corrector, bool
# \param use_intensity_correction Use linear intensity correction
# \param segmentation_propagator None or SegmentationPropagation
# instance
# \param target_stack_index Index of template stack.
# \param use_cropping_to_mask The use crop to mask
# \param boundary_i added value to first coordinate
# (can also be negative)
# \param boundary_j added value to second coordinate
# (can also be negative)
# \param boundary_k added value to third coordinate
# (can also be negative)
# \param unit Unit can either be "mm" or "voxel"
# \param cls The cls
#
def __init__(self,
stacks,
use_N4BiasFieldCorrector=False,
use_intensity_correction=False,
segmentation_propagator=None,
target_stack_index=0,
use_cropping_to_mask=True,
boundary_i=0,
boundary_j=0,
boundary_k=0,
unit="mm",
):
self._use_N4BiasFieldCorrector = use_N4BiasFieldCorrector
self._use_intensity_correction = use_intensity_correction
self._segmentation_propagator = segmentation_propagator
self._target_stack_index = target_stack_index
self._use_cropping_to_mask = use_cropping_to_mask
self._boundary_i = boundary_i
self._boundary_j = boundary_j
self._boundary_k = boundary_k
self._unit = unit
# Number of stacks
self._N_stacks = len(stacks)
# Use stacks provided
self._stacks = [st.Stack.from_stack(s) for s in stacks]
ph.print_info(
"%s stacks were loaded for data preprocessing" % (self._N_stacks))
# Specify whether bias field correction based on N4 Bias Field Correction
# Filter shall be used
# \param[in] flag
def use_N4BiasFieldCorrector(self, flag):
self._use_N4BiasFieldCorrector = flag
#
# Perform data preprocessing
# \date 2017-07-25 21:13:19+0100
#
# \param self The object
#
def run(self):
time_start = ph.start_timing()
# if no mask is provided, use unity stacks for all masks
is_unity_mask = np.alltrue([s.is_unity_mask() for s in self._stacks])
if is_unity_mask:
ph.print_info(
"Keep unity masks for all stacks. "
"It is recommended to provide anatomical masks for increased "
"accuracy.")
# Segmentation propagation
if self._segmentation_propagator is not None and not is_unity_mask:
stacks_to_propagate_indices = []
for i in range(0, self._N_stacks):
if self._stacks[i].is_unity_mask():
stacks_to_propagate_indices.append(i)
stacks_to_propagate_indices = \
list(set(stacks_to_propagate_indices) -
set([self._target_stack_index]))
# Set target mask
target = self._stacks[self._target_stack_index]
# Propagate masks
self._segmentation_propagator.set_template(target)
for i in stacks_to_propagate_indices:
ph.print_info("Propagate mask from stack '%s' to '%s'" % (
target.get_filename(),
self._stacks[i].get_filename()))
self._segmentation_propagator.set_stack(
self._stacks[i])
self._segmentation_propagator.run_segmentation_propagation()
self._stacks[i] = \
self._segmentation_propagator.get_segmented_stack()
# self._stacks[i].show(1)
# Crop to mask
if self._use_cropping_to_mask and not is_unity_mask:
ph.print_info("Crop stacks to their masks")
for i in range(0, self._N_stacks):
self._stacks[i] = self._stacks[i].get_cropped_stack_based_on_mask(
boundary_i=self._boundary_i,
boundary_j=self._boundary_j,
boundary_k=self._boundary_k,
unit=self._unit)
# N4 Bias Field Correction
if self._use_N4BiasFieldCorrector:
bias_field_corrector = n4bfc.N4BiasFieldCorrection()
for i in range(0, self._N_stacks):
ph.print_info(
"Perform N4 Bias Field Correction for stack %d ... "
% (i + 1), newline=False)
bias_field_corrector.set_stack(self._stacks[i])
bias_field_corrector.run_bias_field_correction()
self._stacks[i] = \
bias_field_corrector.get_bias_field_corrected_stack()
print("done")
# Linear Intensity Correction
if self._use_intensity_correction:
stacks_to_intensity_correct = list(
set(range(0, self._N_stacks)) - set([self._target_stack_index]))
intensity_corrector = ic.IntensityCorrection()
intensity_corrector.use_individual_slice_correction(False)
intensity_corrector.use_reference_mask(True)
intensity_corrector.use_verbose(True)
for i in stacks_to_intensity_correct:
stack = self._stacks[i]
intensity_corrector.set_stack(stack)
intensity_corrector.set_reference(
target.get_resampled_stack(resampling_grid=stack.sitk))
# intensity_corrector.run_affine_intensity_correction()
intensity_corrector.run_linear_intensity_correction()
self._stacks[i] = \
intensity_corrector.get_intensity_corrected_stack()
self._computational_time = ph.stop_timing(time_start)
# Get preprocessed stacks
# \return preprocessed stacks as list of Stack objects
def get_preprocessed_stacks(self):
# Return a copy of preprocessed stacks
return [st.Stack.from_stack(stack) for stack in self._stacks]
def get_computational_time(self):
return self._computational_time
# Write preprocessed data to specified output directory
# \param[in] dir_output output directory
def write_preprocessed_data(self, dir_output):
if all(x is None for x in self._stacks):
raise exceptions.ObjectNotCreated("run")
# Write all slices
for i in range(0, self._N_stacks):
slices = self._stacks[i].write(
directory=dir_output, write_mask=True, write_slices=False)
================================================
FILE: niftymic/utilities/input_arparser.py
================================================
##
# \file input_argparser.py
# \brief Class holding a collection of possible arguments to parse for
# scripts
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date August 2017
#
import os
import re
import six
import sys
import inspect
import argparse
import platform
import datetime
import pysitk.python_helper as ph
from nsol.similarity_measures import SimilarityMeasures as SimilarityMeasures
from nsol.loss_functions import LossFunctions as LossFunctions
import niftymic
from niftymic.definitions import ALLOWED_EXTENSIONS
from niftymic.definitions import ALLOWED_INTERPOLATORS
from niftymic.definitions import VIEWER_OPTIONS, V2V_METHOD_OPTIONS
# Allowed image types
IMAGE_TYPES = "(" + (", or ").join(ALLOWED_EXTENSIONS) + ")"
INTERPOLATOR_TYPES = "(%s, or %s)" % (
(", ").join(ALLOWED_INTERPOLATORS[0:-1]), ALLOWED_INTERPOLATORS[-1])
##
# Class holding a collection of possible arguments to parse for scripts
# \date 2017-08-07 01:26:11+0100
#
class InputArgparser(object):
def __init__(self,
description=None,
prog=None,
epilog="NiftyMIC version: %s, Author: %s (%s)" % (
niftymic.__version__,
niftymic.__author__,
niftymic.__email__
),
config_arg="--config"
):
config_helper = "Args that start with '--' (eg. --dir-output) " \
"can also be set in a config file (specified via %s). " \
"If an arg is specified in more than one place, then " \
"commandline values override config file values which " \
"override defaults." % (config_arg)
kwargs = {}
if description is not None:
kwargs['description'] = "%s %s" % (description, config_helper)
if prog is not None:
kwargs['prog'] = prog
if epilog is not None:
kwargs['epilog'] = epilog
self._parser = argparse.ArgumentParser(**kwargs)
self._parser.add_argument(
config_arg,
help="Configuration file in JSON format.")
self._parser.add_argument(
"--version",
action="version",
help="Show NiftyMIC's version number and exit",
version="%s" % niftymic.__version__,
)
self._config_arg = config_arg
def get_parser(self):
return self._parser
def parse_args(self):
# read config file if available
if self._config_arg in sys.argv:
self._parse_config_file()
return self._parser.parse_args()
def print_arguments(self, args, title="Configuration:"):
ph.print_title(title)
for arg in sorted(vars(args)):
ph.print_info("%s: " % (arg), newline=False)
vals = getattr(args, arg)
if type(vals) is list:
# print list element in new lines, unless only one entry in list
# if len(vals) == 1:
# print(vals[0])
# else:
print("")
for val in vals:
print("\t%s" % val)
else:
print(vals)
print("\nNiftyMIC version: %s" % niftymic.__version__)
ph.print_line_separator(add_newline=False)
print("")
##
# Writes a performed script execution.
# \date 2018-01-16 16:05:53+0000
#
# \param self The object
# \param file path to executed file obtained, e.g. via
# os.path.abspath(__file__)
# \param prefix filename prefix
#
def log_config(self,
file,
prefix="config"):
# parser returns options with underscores, e.g. 'dir_output'
dic_with_underscores = vars(self._parser.parse_args())
# get output directory to write log/config file
try:
dir_output = dic_with_underscores["dir_output"]
except KeyError:
dir_output = os.path.dirname(dic_with_underscores["output"])
# build output file name
name = os.path.basename(file).split(".")[0]
now = datetime.datetime.now()
time_stamp = now.strftime("%Y%m%d-%H%M%S")
path_to_config_file = os.path.join(
dir_output,
"%s_%s_%s.json" % (prefix, name, time_stamp))
# exclude config file (as setting in parameters reflected anyway)
dic_with_underscores.pop(re.sub("--", "", self._config_arg))
# replace underscore by dashes for correct commandline parsing,
# e.g. "dir-output" instead of "dir_output"
dic = {
re.sub("_", "-", k): v
for k, v in six.iteritems(dic_with_underscores)}
# add user info to config file
try:
login = os.getlogin()
except OSError as e:
login = "unknown_login"
node = platform.node()
info_args = []
info_args.append("Python %s" % platform.python_version())
info_args.append(platform.system())
info_args.append(platform.release())
info_args.append(platform.version())
info_args.append(platform.machine())
info_args.append(platform.processor())
user = "%s @ %s (%s)" % (login, node, ", ".join(info_args))
dic["user"] = user
# add date of execution to config file
dic["date"] = now.strftime("%Y-%m-%d %H:%M:%S")
# add version number to config file
dic["version"] = niftymic.__version__
# write config file to output
ph.write_dictionary_to_json(dic, path_to_config_file, verbose=True)
def add_filename(
self,
option_string="--filename",
type=str,
help="Path to NIfTI file %s." % (IMAGE_TYPES),
default=None,
required=True,
):
self._add_argument(dict(locals()))
def add_filename_mask(
self,
option_string="--filename-mask",
type=str,
help="Path to NIfTI file mask %s." % (IMAGE_TYPES),
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_dir_input(
self,
option_string="--dir-input",
type=str,
help="Input directory with NIfTI files %s." % (IMAGE_TYPES),
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_dir_input_mc(
self,
option_string="--dir-input-mc",
type=str,
help="Input directory where transformation files (.tfm) for "
"motion-corrected slices are stored.",
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_subfolder_motion_correction(
self,
option_string="--subfolder-motion-correction",
type=str,
help="Name of folder within output directory where all motion "
"correction results are stored",
default="motion_correction",
required=False,
):
self._add_argument(dict(locals()))
def add_subfolder_comparison(
self,
option_string="--subfolder-comparison",
type=str,
help="Name of folder within output directory where all comparison "
"results are stored",
default="comparison",
required=False,
):
self._add_argument(dict(locals()))
def add_dir_inputs(
self,
option_string="--dir-inputs",
nargs="+",
type=str,
help="Input directories with NIfTI files %s." % (IMAGE_TYPES),
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_filenames(
self,
option_string="--filenames",
nargs="+",
help="Paths to NIfTI file images %s." % (IMAGE_TYPES),
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_filenames_masks(
self,
option_string="--filenames-masks",
nargs="+",
help="Paths to NIfTI file image masks %s." % (IMAGE_TYPES),
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_label(
self,
option_string="--label",
type=str,
help="Label for image given by filename.",
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_fixed(
self,
option_string="--fixed",
type=str,
help="Path to fixed image %s." % (IMAGE_TYPES),
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_fixed_mask(
self,
option_string="--fixed-mask",
type=str,
help="Path to fixed image mask %s." % (IMAGE_TYPES),
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_moving(
self,
option_string="--moving",
nargs=None,
type=str,
help="Path to moving image %s." % (IMAGE_TYPES),
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_moving_mask(
self,
option_string="--moving-mask",
type=str,
help="Path to moving image mask %s." % (IMAGE_TYPES),
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_metric(
self,
option_string="--metric",
type=str,
help="Metric for image registration method.",
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_metric_radius(
self,
option_string="--metric-radius",
type=int,
help="Radius in case metric 'ANTSNeighborhoodCorrelation' is chosen.",
default=10,
required=False,
):
self._add_argument(dict(locals()))
def add_labels(
self,
option_string="--labels",
nargs="+",
help="Labels for images given by filenames.",
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_image_selection(
self,
option_string="--image-selection",
nargs="+",
help="Specify image filenames without filename extension which will "
"be used only. ",
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_reference(
self,
option_string="--reference",
type=str,
help="Path to reference image file %s." % (IMAGE_TYPES),
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_reference_mask(
self,
option_string="--reference-mask",
type=str,
help="Path to reference mask image file %s." % (IMAGE_TYPES),
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_output(
self,
option_string="--output",
type=str,
help="Path to output image file %s." % (IMAGE_TYPES),
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_dir_output(
self,
option_string="--dir-output",
type=str,
help="Output directory.",
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_suffix_mask(
self,
option_string="--suffix-mask",
type=str,
help="Suffix used to associate a mask with an image. "
"E.g. suffix_mask='_mask' means an existing "
"image_i_mask.nii.gz represents the mask to "
"image_i.nii.gz for all images image_i in the input "
"directory.",
default="_mask",
required=False,
):
self._add_argument(dict(locals()))
def add_use_masks_srr(
self,
option_string="--use-masks-srr",
type=int,
help="Use masks in SRR step to confine volumetric reconstruction only "
"to the masked slice regions.",
default=1,
required=False,
):
self._add_argument(dict(locals()))
def add_outlier_rejection(
self,
option_string="--outlier-rejection",
type=int,
help="Turn on/off use of outlier rejection mechanism to eliminate "
"misregistered slices.",
default=0,
required=False,
):
self._add_argument(dict(locals()))
def add_boundary_stacks(
self,
option_string="--boundary-stacks",
type=int,
nargs="+",
help="Specify boundary in i-, j- and k-direction in mm "
"for cropping the given input stacks. "
"Stack will be cropped to bounding box encompassing the mask plus "
"the added boundary.",
default=[0, 0, 0],
):
self._add_argument(dict(locals()))
def add_prefix_output(
self,
option_string="--prefix-output",
type=str,
help="Prefix for SRR output file name.",
default="SRR_",
required=False,
):
self._add_argument(dict(locals()))
def add_target_stack_index(
self,
option_string="--target-stack-index",
type=int,
help="Index of input image stack that defines physical space for SRR. "
"First index is 0.",
default=0,
required=False,
):
self._add_argument(dict(locals()))
def add_target_stack(
self,
option_string="--target-stack",
type=str,
help="Choose target stack for reconstruction/pre-processing %s." % (
IMAGE_TYPES),
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_reconstruction(
self,
option_string="--reconstruction",
type=str,
help="Path to NIfTI file %s of the obtained volumetric reconstruction "
% (IMAGE_TYPES),
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_reconstruction_space(
self,
option_string="--reconstruction-space",
type=str,
help="Path to NIfTI file %s which defines the physical space "
"for the volumetric reconstruction/SRR." % (IMAGE_TYPES),
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_gestational_age(
self,
option_string="--gestational-age",
type=int,
help="Gestational age in weeks of the fetal brain to "
"be reconstructed.",
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_search_angle(
self,
option_string="--search-angle",
type=int,
help="Maximum search angle to be used to find correct orientation.",
default=180,
):
self._add_argument(dict(locals()))
def add_multiresolution(
self,
option_string="--multiresolution",
type=int,
help="Turn on/off multiresolution approach for motion correction.",
default=0,
):
self._add_argument(dict(locals()))
def add_shrink_factors(
self,
option_string="--shrink-factors",
type=int,
nargs="+",
help="Specify shrink factors for multiresolution approach.",
default=[2, 1],
):
self._add_argument(dict(locals()))
def add_smoothing_sigmas(
self,
option_string="--smoothing-sigmas",
type=float,
nargs="+",
help="Specify smoothing sigmas for multiresolution approach.",
default=[1, 0],
):
self._add_argument(dict(locals()))
def add_two_step_cycles(
self,
option_string="--two-step-cycles",
type=int,
help="Number of two-step-cycles, i.e. number of "
"Slice-to-Volume Registration and Super-Resolution Reconstruction "
"cycles",
default=3,
):
self._add_argument(dict(locals()))
def add_sigma(
self,
option_string="--sigma",
type=float,
help="Standard deviation for Scattered Data Approximation approach "
"to reconstruct first estimate of HR volume from all 3D input stacks.",
default=0.9,
):
self._add_argument(dict(locals()))
def add_minimizer(
self,
option_string="--minimizer",
type=str,
help="Choice of minimizer used for the inverse problem associated to "
"the SRR. Possible choices are 'lsmr' or any solver in "
"scipy.optimize.minimize like 'L-BFGS-B'. Note, in case of a chosen "
"non-linear data loss only non-linear solvers like 'L-BFGS-B' are "
"viable.",
default="lsmr",
):
self._add_argument(dict(locals()))
def add_alpha(
self,
option_string="--alpha",
type=float,
help="Regularization parameter alpha to solve the Super-Resolution "
"Reconstruction problem: SRR = argmin_x "
"[0.5 * sum_k ||y_k - A_k x||^2 + alpha * R(x)].",
default=0.03,
):
self._add_argument(dict(locals()))
def add_alpha_first(
self,
option_string="--alpha-first",
type=float,
help="Regularization parameter like 'alpha' but used for the first"
"SRR step.",
default=0.1,
):
self._add_argument(dict(locals()))
def add_threshold(
self,
option_string="--threshold",
type=float,
help="Threshold between 0 and 1 to detect misregistered slices based "
"on NCC in final cycle.",
default=0.8,
):
self._add_argument(dict(locals()))
def add_threshold_first(
self,
option_string="--threshold-first",
type=float,
help="Threshold between 0 and 1 to detect misregistered slices based "
"on NCC in first cycle.",
default=0.5,
):
self._add_argument(dict(locals()))
def add_s2v_smoothing(
self,
option_string="--s2v-smoothing",
type=float,
help="Value for Gaussian process parameter smoothing.",
default=0.5,
):
self._add_argument(dict(locals()))
def add_interleave(
self,
option_string="--interleave",
type=int,
help="Interleave used for slice acquisition",
default=2,
):
self._add_argument(dict(locals()))
def add_iter_max(
self,
option_string="--iter-max",
type=int,
help="Number of maximum iterations for the numerical solver.",
default=10,
):
self._add_argument(dict(locals()))
def add_iter_max_first(
self,
option_string="--iter-max-first",
type=int,
help="Number of maximum iterations for the numerical solver like "
"'iter-max' but used for the first SRR step",
default=5,
):
self._add_argument(dict(locals()))
def add_rho(
self,
option_string="--rho",
type=float,
help="Regularization parameter for augmented Lagrangian term required "
"by ADMM approach for TV regularization",
default=0.5,
):
self._add_argument(dict(locals()))
def add_iterations(
self,
option_string="--iterations",
type=int,
help="Number of ADMM/Primal Dual iterations.",
default=10,
):
self._add_argument(dict(locals()))
def add_tv_solver(
self,
option_string="--tv-solver",
type=str,
help="Type of TV solver. Either 'ADMM' or 'PD'.",
default="PD",
):
self._add_argument(dict(locals()))
def add_data_loss(
self,
option_string="--data-loss",
type=str,
help="Loss function rho used for data term, i.e. rho((y_k - A_k x)^2) "
"Possible choices are 'linear', 'soft_l1, 'huber', 'arctan' and "
"'cauchy'.",
default="linear",
):
self._add_argument(dict(locals()))
def add_data_loss_scale(
self,
option_string="--data-loss-scale",
type=float,
help="Value of soft margin between inlier and outlier residuals, "
"default is 1.0. The loss function is evaluated as "
"rho_(f2) = C**2 * rho(f2 / C**2), where C is data_loss_scale. "
"This parameter has no effect with data_loss='linear', but for other "
"loss values it is of crucial importance.",
default=1,
):
self._add_argument(dict(locals()))
def add_pd_alg_type(
self,
option_string="--pd-alg-type",
type=str,
help="Algorithm used to dynamically update parameters for each "
"iteration of the dual algorithm. "
"Possible choices are 'ALG2', 'ALG2_AHMOD' and 'ALG3' as "
"described in Chambolle et al., 2011",
default="ALG2",
):
self._add_argument(dict(locals()))
def add_dilation_radius(
self,
option_string="--dilation-radius",
type=int,
help="Dilation radius in number of voxels used for segmentation "
"propagation from target stack in case masks are not provided for all "
"images.",
default=3,
):
self._add_argument(dict(locals()))
def add_extra_frame_target(
self,
option_string="--extra-frame-target",
type=float,
help="Increase chosen target space uniformly in each direction by "
"extra frame given in mm.",
default=0,
):
self._add_argument(dict(locals()))
def add_bias_field_correction(
self,
option_string="--bias-field-correction",
type=int,
help="Turn on/off bias field correction step during data "
"preprocessing.",
default=0,
):
self._add_argument(dict(locals()))
def add_intensity_correction(
self,
option_string="--intensity-correction",
type=int,
help="Turn on/off linear intensity correction step during data "
"preprocessing.",
default=0,
):
self._add_argument(dict(locals()))
def add_isotropic_resolution(
self,
option_string="--isotropic-resolution",
type=float,
help="Specify isotropic resolution for obtained SRR volume. Default "
"resolution is specified by in-plane resolution of chosen target "
"stack.",
default=None,
):
self._add_argument(dict(locals()))
def add_log_config(
self,
option_string="--log-config",
type=int,
help="Turn on/off configuration log of executed script.",
default=0,
):
self._add_argument(dict(locals()))
def add_write_motion_correction(
self,
option_string="--write-motion-correction",
type=int,
help="Turn on/off functionality to write final result of motion "
"correction. This includes the rigidly aligned stacks with their "
"respective motion corrected individual slices and the overall "
"transform applied to each individual slice.",
default=0,
):
self._add_argument(dict(locals()))
def add_provide_comparison(
self,
option_string="--provide-comparison",
type=int,
help="Turn on/off functionality to create files "
"allowing for a visual comparison between original "
"data and the obtained SRR. A folder 'comparison' "
"will be created in the output directory containing "
"the obtained SRR along with the linearly resampled "
"original data. An additional script "
"'show_comparison.py' will be provided whose "
"execution will open all images in ITK-Snap "
"(http://www.itksnap.org/).",
default=0,
):
self._add_argument(dict(locals()))
def add_verbose(
self,
option_string="--verbose",
type=int,
help="Turn on/off verbose output.",
default=1,
):
self._add_argument(dict(locals()))
def add_option(
self,
option_string="--option",
nargs=None,
type=float,
help="Add option.",
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_argument(
self,
*a,
**k
):
self._parser.add_argument(*a, **k)
def add_psf_aware(
self,
option_string='--psf-aware',
type=int,
help="Turn on/off use of PSF-aware registration.",
default=0,
):
self._add_argument(dict(locals()))
def add_stack_recon_range(
self,
option_string="--stack-recon-range",
type=int,
help="Number of components used for SRR.",
default=15,
):
self._add_argument(dict(locals()))
def add_alphas(
self,
option_string="--alphas",
nargs="+",
type=float,
help="Specify regularization parameters to be looped through.",
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_data_losses(
self,
option_string="--data-losses",
nargs="+",
help="Specify data losses to be looped through.",
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_data_loss_scales(
self,
option_string="--data-loss-scales",
nargs="+",
type=float,
help="Specify data loss scales to be looped through.",
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_study_name(
self,
option_string="--study-name",
type=str,
help="Name of parameter study (no white spaces).",
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_measures(
self,
option_string="--measures",
type=str,
nargs="+",
help="Measures to be evaluated between reference (if given) and "
"reconstruction %s. " % ("(" + (", ").join(
SimilarityMeasures.similarity_measures.keys()) + ")"),
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_reconstruction_type(
self,
option_string="--reconstruction-type",
type=str,
help="Define reconstruction type. Allowed values are "
"'TK0L2', 'TK1L2', 'TVL2', and 'HuberL2'.",
default="TVL1",
required=False,
):
self._add_argument(dict(locals()))
def add_interpolator(
self,
option_string="--interpolator",
type=str,
help="Choose type of interpolator %s." % (INTERPOLATOR_TYPES),
default="Linear",
required=False,
):
self._add_argument(dict(locals()))
def add_slice_thicknesses(
self,
option_string="--slice-thicknesses",
nargs="+",
type=float,
help="Manually specify slice thicknesses of input image stacks. "
"If not provided, the slice thicknesses of each acquired stack "
"is assumed to be the image spacing in through-plane direction.",
default=None,
required=False,
):
self._add_argument(dict(locals()))
def add_viewer(
self,
option_string="--viewer",
type=str,
help="Viewer to be used for visualizations during verbose output "
"(%s)." % ", ".join(VIEWER_OPTIONS),
default="itksnap",
required=False,
):
self._add_argument(dict(locals()))
def add_v2v_method(
self,
option_string="--v2v-method",
type=str,
help="Registration method used for first rigid volume-to-volume "
"registration step "
"(%s)." % ", ".join(V2V_METHOD_OPTIONS),
default="FLIRT",
required=False,
):
self._add_argument(dict(locals()))
##
# Parse the provided configuration file
#
# Additional arguments provided in the commandline will be preferred. E.g.
# 'script.py --config config.json --dir-output path-to-output-dir' will set
# dir-output to path-to-output-dir regardless the setting in config.json.
# \date 2018-01-16 15:56:38+0000
#
# \param self The object
# \post sys.argv extended by the arguments provided in the config
# file
#
def _parse_config_file(self):
# Read path to config file
path_to_config_file = sys.argv[sys.argv.index(self._config_arg) + 1]
# Read config file and insert all config entries into sys.argv (read by
# argparse later)
dic = ph.read_dictionary_from_json(path_to_config_file)
# Insert all config entries into sys.argv
for k, v in six.iteritems(dic):
# ignore log info in config files
if k in ["version", "user", "date"]:
continue
# A 'None' entry should be ignored
if v is None:
continue
# Insert values as string right at the beginning of arguments
# Rationale: Later options, outside of the config file, will
# overwrite the config values
if type(v) is list:
for vi in reversed(v):
sys.argv.insert(1, str(vi))
# 'store_true' values are converted to True/False; ignore the value
# of this key as the existence of the key indicates 'True'
elif type(v) is bool:
if v == True:
sys.argv.insert(1, "--%s" % k)
continue
else:
sys.argv.insert(1, str(v))
sys.argv.insert(1, "--%s" % k)
##
# Adds an argument to argument parser.
#
# Rationale: Make interface as generic as possible so that function call
# works regardless the name of the desired option
# \date 2017-08-06 21:54:51+0100
#
# \param self The object
# \param allvars all variables set at respective function call as
# dictionary
#
def _add_argument(self, allvars):
# Skip variable 'self'
allvars.pop('self')
# Get name of argument to add
option_string = allvars.pop('option_string')
# Build dictionary for additional, optional parameters
kwargs = {}
for key, value in six.iteritems(allvars):
kwargs[key] = value
# Add information on default value in case provided
if 'default' in kwargs.keys():
if type(kwargs['default']) == list:
txt = " ".join([str(i) for i in kwargs['default']])
else:
txt = str(kwargs['default'])
txt_default = " [default: %s]" % txt
# Case where 'required' key is given:
if 'required' in kwargs.keys():
# Only add information in case argument is not mandatory to
# parse
if kwargs['default'] is not None and not kwargs['required']:
kwargs['help'] += txt_default
# Case where no such field was provided
else:
if kwargs['default'] is not None:
kwargs['help'] += txt_default
# Add argument with its options
self._parser.add_argument(option_string, **kwargs)
================================================
FILE: niftymic/utilities/intensity_correction.py
================================================
##
# \file intensity_correction.py
# \brief Class containing functions to correct for intensities
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date Nov 2016
#
# Import libraries
import sys
import SimpleITK as sitk
import numpy as np
from scipy.optimize import least_squares
import time
import matplotlib.pyplot as plt
import pysitk.simple_itk_helper as sitkh
import pysitk.python_helper as ph
import niftymic.base.stack as st
##
# Class to correct intensities
# \date 2016-11-01 20:12:46+0000
#
class IntensityCorrection(object):
##
# Constructor
# \date 2016-11-01 21:57:13+0000
#
# \param self The object
# \param stack Stack object to be intensity
# corrected
# \param reference Stack object used as
# reference for intensities
# (needs to be in the physical
# space as stack)
# \param use_reference_mask Use reference mask (as given
# in \p reference) to reduce
# focus for intensity
# correction; bool
# \param use_individual_slice_correction State whether intensity
# correction is performed for
# each slice independently;
# bool
# \param use_verbose Verbose; bool
#
def __init__(self,
stack=None,
reference=None,
use_stack_mask=True,
use_reference_mask=True,
use_individual_slice_correction=False,
use_verbose=False,
additional_stack=None,
prefix_corrected="",
):
if stack is not None:
self._stack = st.Stack.from_stack(stack)
else:
self._stack = None
# Additional stack to correct alongside given stack
if additional_stack is not None:
self._additional_stack = st.Stack.from_stack(additional_stack)
else:
self._additional_stack = None
# Check that stack and reference are in the same space
if reference is not None:
try:
self._stack.sitk - reference.sitk
except:
raise ValueError(
"Reference and stack are not in the same space")
self._reference = st.Stack.from_stack(reference)
else:
self._reference = None
self._apply_intensity_correction = {
"linear": self._apply_linear_intensity_correction,
"affine": self._apply_affine_intensity_correction,
}
self._use_verbose = use_verbose
self._use_reference_mask = use_reference_mask
self._use_stack_mask = use_stack_mask
self._use_individual_slice_correction = use_individual_slice_correction
self._prefix_corrected = prefix_corrected
##
# Sets the stack.
# \date 2016-11-05 22:58:01+0000
#
# \param self The object
# \param stack The stack as Stack object
#
def set_stack(self, stack):
self._stack = st.Stack.from_stack(stack)
##
# Sets the reference.
# \date 2016-11-05 22:58:10+0000
#
# \param self The object
# \param reference The reference as Stack object
#
def set_reference(self, reference):
self._reference = st.Stack.from_stack(reference)
##
# Sets additional stack to correct alongside given stack
# \date 2016-12-05 12:19:25+0000
#
# \param self The object
# \param additional_stack The additional stack
#
# \return { description_of_the_return_value }
#
def set_additional_stack(self, additional_stack):
self._additional_stack = st.Stack.from_stack(additional_stack)
##
# Use verbose
# \date 2016-11-05 22:58:28+0000
#
# \param self The object
# \param verbose The verbose as boolean
#
def use_verbose(self, verbose):
self._use_verbose = verbose
def use_reference_mask(self, use_reference_mask):
self._use_reference_mask = use_reference_mask
def use_stack_mask(self, use_stack_mask):
self._use_stack_mask = use_stack_mask
##
# Sets the use individual slice correction.
# \date 2016-11-22 22:47:47+0000
#
# \param self The object
# \param flag The flag
#
def use_individual_slice_correction(self, flag):
self._use_individual_slice_correction = flag
##
# Gets the intensity corrected stack
# \date 2016-11-05 22:58:50+0000
#
# \param self The object
#
# \return The intensity corrected stack as Stack object
#
def get_intensity_corrected_stack(self):
s = st.Stack.from_stack(self._stack)
s.set_filename(self._prefix_corrected + self._stack.get_filename())
return s
def get_intensity_corrected_additional_stack(self):
return st.Stack.from_stack(self._additional_stack)
##
# Gets the intensity correction coefficients obtained for each
# slice of the stack.
# \date 2016-11-10 02:22:40+0000
#
# \param self The object
#
# \return The intensity correction coefficients as (N_slices x
# DOF)-array
#
def get_intensity_correction_coefficients(self):
return np.array(self._correction_coefficients)
##
# Clip lower intensities based on percentile threshold
# \date 2016-11-05 22:59:08+0000
#
# \param self The object
# \param percentile The percentile defining the threshold
#
def run_lower_percentile_capping_of_stack(self, percentile=10):
if self._use_verbose:
ph.print_info(
"Cap lower intensities at %d%%-percentile" % (percentile))
nda = sitk.GetArrayFromImage(self._stack.sitk)
# Clip lower intensity values
i0 = np.percentile(nda, percentile)
nda[np.where(nda < i0)] = 0
nda[np.where(nda >= i0)] -= i0
# Create Stack instance with correct image header information
self._stack = self._create_stack_from_corrected_intensity_array(
nda, self._stack)
if self._additional_stack is not None:
nda_additional_stack = sitk.GetArrayFromImage(
self._additional_stack.sitk)
nda_additional_stack[np.where(nda_additional_stack < i0)] = 0
nda_additional_stack[np.where(nda_additional_stack >= i0)] -= i0
# Create Stack instance with correct image header information
self._additional_stack = self._create_stack_from_corrected_intensity_array(
nda_additional_stack, self._additional_stack)
##
# Run linear intensity correction model.
# \date 2016-11-05 23:02:46+0000
#
# Perform linear intensity correction, i.e.
# minimize || reference - c1*stack || in ell^2-sense.
#
# \param self The object
#
def run_linear_intensity_correction(self):
self._stack, self._correction_coefficients, self._additional_stack = self._run_intensity_correction(
"linear")
##
# Run affine intensity correction model.
# \date 2016-11-05 23:05:48+0000
#
# Perform affine intensity correction, i.e.
# minimize || reference - (c1*stack + c0)|| in ell^2-sense.
#
# \param self The object
#
def run_affine_intensity_correction(self):
self._stack, self._correction_coefficients, self._additional_stack = self._run_intensity_correction(
"affine")
##
# Execute respective intensity correction model.
# \date 2016-11-05 23:06:37+0000
#
# \param self The object
# \param correction_model The correction model. Either 'linear' or
# 'affine'
#
def _run_intensity_correction(self, correction_model):
N_slices = self._stack.get_number_of_slices()
if correction_model in ["linear"]:
correction_coefficients = np.zeros((N_slices, 1))
elif correction_model in ["affine"]:
correction_coefficients = np.zeros((N_slices, 2))
# Gets the required data arrays to perform intensity correction
nda, nda_reference, nda_mask, nda_additional_stack = self._get_data_arrays_prior_to_intensity_correction()
if self._use_individual_slice_correction:
if self._use_verbose:
ph.print_info("Run " + correction_model +
" intensity correction for each slice individually")
for i in range(0, N_slices):
if self._use_verbose:
sys.stdout.write("Slice %2d/%d: " %
(i, self._stack.get_number_of_slices() - 1))
sys.stdout.flush()
if self._additional_stack is None:
nda[i, :, :], correction_coefficients[i, :] = self._apply_intensity_correction[
correction_model](nda[i, :, :], nda_reference[i, :, :], nda_mask[i, :, :])
else:
nda[i, :, :], correction_coefficients[i, :], nda_additional_stack[i, :, :] = self._apply_intensity_correction[
correction_model](nda[i, :, :], nda_reference[i, :, :], nda_mask[i, :, :], nda_additional_stack[i, :, :])
else:
if self._use_verbose:
ph.print_info("Run " + correction_model +
" intensity correction uniformly for entire stack")
if self._additional_stack is None:
nda, cc = \
self._apply_intensity_correction[
correction_model](nda, nda_reference, nda_mask)
else:
nda, cc, nda_additional_stack = self._apply_intensity_correction[
correction_model](nda, nda_reference, nda_mask, nda_additional_stack)
correction_coefficients = cc
# debug
# tmp_corr = sitk.GetImageFromArray(nda)
# tmp_corr.CopyInformation(self._stack.sitk)
# tmp_mask = sitk.GetImageFromArray(nda_mask)
# tmp_mask.CopyInformation(self._stack.sitk_mask)
# sitkh.show_sitk_image(
# [
# tmp_corr,
# self._stack.sitk,
# self._reference.sitk,
# ],
# segmentation=tmp_mask,
# label=["corr", "orig", "ref"]
# )
# Create Stack instance with correct image header information
if self._additional_stack is None:
return self._create_stack_from_corrected_intensity_array(nda, self._stack), correction_coefficients, None
else:
return self._create_stack_from_corrected_intensity_array(nda, self._stack), correction_coefficients, self._create_stack_from_corrected_intensity_array(nda_additional_stack, self._additional_stack)
##
# Perform affine intensity correction via normal equations
# \date 2016-11-05 23:10:49+0000
#
# \param self The object
# \param nda Data array to be corrected
# \param nda_mask Mask to be used
# \param nda_reference Masked reference data array used to
# compute coefficients
#
# \return intensity corrected data array as np.array
#
def _apply_affine_intensity_correction(self, nda, nda_reference, nda_mask, nda_additional_stack=None):
# Find masked indices
indices = np.where(nda_mask > 0)
# Model: y = x*c1 + c0 = [x, 1]*[c1, c0]' = A*[c1,c0]
x = nda[indices].flatten().astype('double')
y = nda_reference[indices].flatten().astype('double')
# Solve via normal equations: [c1, c0] = (A'A)^{-1}A'y
A = np.ones((x.size, 2))
A[:, 0] = x
B = np.linalg.pinv(A.transpose().dot(A)).dot(A.transpose())
c1, c0 = B.dot(y)
if np.isnan(c1) or np.isnan(c0):
raise RuntimeError(
"Invalid value encountered during affine intensity correction "
"(c1, c0) = (%f, %f)" % (c1, c0))
if self._use_verbose:
ph.print_info("(c1, c0) = (%.3f, %.3f)" % (c1, c0))
if nda_additional_stack is None:
return nda * c1 + c0, np.array([c1, c0])
else:
return nda * c1 + c0, np.array([c1, c0]), nda_additional_stack * c1 + c0
##
# Perform linear intensity correction via normal equations
# \date 2016-11-05 23:12:13+0000
#
# \param self The object
# \param nda Data array to be corrected
# \param nda_mask Mask to be used
# \param nda_reference Masked reference data array used to
# compute coefficients
#
# \return intensity corrected data array as np.array
#
def _apply_linear_intensity_correction(self, nda, nda_reference, nda_mask, nda_additional_stack=None):
# Find masked indices
indices = np.where(nda_mask > 0)
# Model: y = x*c1
x = nda[indices].flatten().astype('double')
y = nda_reference[indices].flatten().astype('double')
# ph.show_2D_array_list([nda, nda_reference])
# Solve via normal equations: c1 = x'y/(x'x)
c1 = x.dot(y) / x.dot(x)
if np.isnan(c1):
raise RuntimeError(
"Invalid value encountered during linear intensity correction "
"(c1 = %f)" % c1)
if self._use_verbose:
ph.print_info("c1 = %.3f" % (c1))
if nda_additional_stack is None:
return nda * c1, c1
else:
return nda * c1, c1, nda_additional_stack * c1
##
# Gets the data arrays prior to intensity correction.
# \date 2016-11-05 23:07:37+0000
#
# \param self The object
#
# \return The data arrays prior to intensity correction.
#
def _get_data_arrays_prior_to_intensity_correction(self):
# Get required data arrays for intensity correction
nda = sitk.GetArrayFromImage(self._stack.sitk)
nda_reference = sitk.GetArrayFromImage(self._reference.sitk)
if self._use_reference_mask:
nda_mask_ref = sitk.GetArrayFromImage(self._reference.sitk_mask)
else:
nda_mask_ref = np.ones_like(nda)
if self._use_stack_mask:
nda_mask_stack = sitk.GetArrayFromImage(self._stack.sitk_mask)
else:
nda_mask_stack = np.ones_like(nda)
nda_mask = nda_mask_ref * nda_mask_stack
if self._additional_stack is None:
nda_additional_stack = None
else:
nda_additional_stack = sitk.GetArrayFromImage(
self._additional_stack.sitk)
# debug
# tmp_mask = sitk.GetImageFromArray(nda_mask)
# tmp_mask.CopyInformation(self._stack.sitk_mask)
# sitkh.show_sitk_image(
# [
# self._stack.sitk_mask,
# self._reference.sitk_mask,
# ],
# segmentation=tmp_mask,
# label=["orig", "ref"]
# )
return nda, nda_reference, nda_mask, nda_additional_stack
##
# Creates a Stack object from corrected intensity array with
# same image header information as input \p stack.
# \date 2016-11-05 23:15:33+0000
#
# \param self The object
# \param nda The nda
#
# \return Stack object with image containing the given array
# information.
#
def _create_stack_from_corrected_intensity_array(self, nda, stack):
# Convert back to image with correct header
image_sitk = sitk.GetImageFromArray(nda)
image_sitk.CopyInformation(stack.sitk)
# Potentially, #slices < #slices_ic as some slices might have been
# deleted.
slices = stack.get_slices()
helper_slice_numbers = stack.get_deleted_slice_numbers()
helper_slice_numbers.append(slices[0].get_slice_number())
helper_slice_numbers.append(slices[-1].get_slice_number())
slice_numbers = np.arange(np.min(helper_slice_numbers),
np.max(helper_slice_numbers) + 1)
stack_ic = st.Stack.from_sitk_image(
image_sitk=image_sitk,
slice_thickness=stack.get_slice_thickness(),
filename=stack.get_filename(),
image_sitk_mask=stack.sitk_mask,
slice_numbers=slice_numbers,
)
# Update registration history of stack
stack_ic.set_registration_history(
stack.get_registration_history())
# Update registration history of (kept) slices
kept_slice_numbers = [s.get_slice_number() for s in slices]
slices_ic = stack_ic.get_slices()
for slice_ic in slices_ic:
slice_number = slice_ic.get_slice_number()
# Update registration of kept slice
if slice_number in kept_slice_numbers:
index = kept_slice_numbers.index(slice_number)
slice_ic.set_registration_history(
slices[index].get_registration_history())
# Otherwise, delete slices
else:
stack_ic.delete_slice(slice_ic)
return stack_ic
================================================
FILE: niftymic/utilities/joint_image_mask_builder.py
================================================
##
# \file joint_image_mask_builder.py
# \brief Build common mask from multiple, individual ones
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date November 2017
#
import SimpleITK as sitk
import numpy as np
import pysitk.simple_itk_helper as sitkh
import niftymic.base.stack as st
class JointImageMaskBuilder(object):
def __init__(self,
stacks,
target,
dilation_radius=1,
dilation_kernel="Ball",
max_distance=50):
self._stacks = stacks
self._target = target
self._dilation_radius = dilation_radius
self._dilation_kernel = dilation_kernel
self._max_distance = max_distance
self._joint_image_mask = None
def run(self):
recon_space = self._target.get_isotropically_resampled_stack(
extra_frame=self._max_distance,
)
mask_sitk = 0 * recon_space.sitk_mask
dim = mask_sitk.GetDimension()
for stack in self._stacks:
stack_mask_sitk = sitk.Resample(
stack.sitk_mask,
mask_sitk,
eval("sitk.Euler%dDTransform()" % dim),
sitk.sitkNearestNeighbor,
0,
mask_sitk.GetPixelIDValue())
mask_sitk += stack_mask_sitk
thresholder = sitk.BinaryThresholdImageFilter()
mask_sitk = thresholder.Execute(mask_sitk, 0, 0.5, 0, 1)
if self._dilation_radius > 0:
dilater = sitk.BinaryDilateImageFilter()
dilater.SetKernelType(eval("sitk.sitk" + self._dilation_kernel))
dilater.SetKernelRadius(self._dilation_radius)
mask_sitk = dilater.Execute(mask_sitk)
self._joint_image_mask = st.Stack.from_sitk_image(
image_sitk=recon_space.sitk,
image_sitk_mask=mask_sitk,
filename=self._target.get_filename(),
slice_thickness=recon_space.get_slice_thickness(),
)
def get_stack(self):
return st.Stack.from_stack(self._joint_image_mask)
================================================
FILE: niftymic/utilities/motion_updater.py
================================================
##
# \file motion_updater.py
# \brief Class to apply stack and individual slice motion transformations
# from a 'motion_correction' directory.
#
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date Nov 2018
#
import os
import re
import numpy as np
import SimpleITK as sitk
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
import niftymic.base.stack as st
import niftymic.base.slice as sl
import niftymic.base.exceptions as exceptions
##
# Class to update motion correction of both stacks and slices given a directory
# that contains the respective transformation files.
#
# Provided motion-correction directory must contain "*.tfm" files in the
# following format:
#
# -# filenameA.tfm: Transformation to be applied to stack with filename
# 'filenameA'
# -# filenameA_slice[0-9]+.tfm: Transformations to be applied to individual
# slices of stack with filename 'filenameA'. If a slice transformation file
# is not provided, the respective slice will be deleted from the stack
# \date 2018-11-11 16:21:00+0000
#
class MotionUpdater(object):
##
# { constructor_description }
# \date 2018-11-11 16:26:58+0000
#
# \param self The object
# \param stacks Stacks as list of Stack objects
# \param dir_motion_correction Path to motion-correction files
# [*.tfm]
# \param volume_motion_only Update only stack/volumetric motion
# (and ignore individual slice motion
# transforms)
# \param prefix_slice Prefix of slices to indicate slice
# transformations. E.g. "_slice" refers
# to filenameA_slice[0-9]+.tfm files as
# slice transformations to stack
# "filenameA"
#
def __init__(
self,
stacks,
dir_motion_correction,
volume_motion_only=False,
prefix_slice="_slice",
):
self._stacks = [st.Stack.from_stack(s) for s in stacks]
self._dir_motion_correction = dir_motion_correction
self._volume_motion_only = volume_motion_only
self._prefix_slice = prefix_slice
self._check_against_json = {
True: self._check_against_json_true,
False: self._check_against_json_false,
}
self._rejected_slices = None
def run(self, older_than_v3=False):
if not ph.directory_exists(self._dir_motion_correction):
raise exceptions.DirectoryNotExistent(
self._dir_motion_correction)
abs_path_to_directory = os.path.abspath(
self._dir_motion_correction)
path_to_rejected_slices = os.path.join(
abs_path_to_directory, "rejected_slices.json")
if ph.file_exists(path_to_rejected_slices):
self._rejected_slices = ph.read_dictionary_from_json(
path_to_rejected_slices)
bool_check = True
else:
self._rejected_slices = None
bool_check = False
for i in range(len(self._stacks)):
stack_name = self._stacks[i].get_filename()
if not older_than_v3:
# update stack position
path_to_stack_transform = os.path.join(
abs_path_to_directory, "%s.tfm" % stack_name)
if ph.file_exists(path_to_stack_transform):
transform_stack_sitk = sitkh.read_transform_sitk(
path_to_stack_transform)
transform_stack_sitk_inv = sitkh.read_transform_sitk(
path_to_stack_transform, inverse=True)
self._stacks[i].update_motion_correction(
transform_stack_sitk)
ph.print_info(
"Stack '%s': Stack position updated" % stack_name)
else:
transform_stack_sitk_inv = sitk.Euler3DTransform()
if self._volume_motion_only:
continue
# update slice positions
pattern_trafo_slices = stack_name + self._prefix_slice + \
"([0-9]+)[.]tfm"
p = re.compile(pattern_trafo_slices)
dic_slice_transforms = {
int(p.match(f).group(1)): os.path.join(
abs_path_to_directory, p.match(f).group(0))
for f in os.listdir(abs_path_to_directory) if p.match(f)
}
slices = self._stacks[i].get_slices()
for i_slice in range(self._stacks[i].get_number_of_slices()):
if i_slice in dic_slice_transforms.keys():
transform_slice_sitk = sitkh.read_transform_sitk(
dic_slice_transforms[i_slice])
transform_slice_sitk = \
sitkh.get_composite_sitk_affine_transform(
transform_slice_sitk, transform_stack_sitk_inv)
slices[i_slice].update_motion_correction(
transform_slice_sitk)
else:
self._stacks[i].delete_slice(slices[i_slice])
# ----------------------------- HACK -----------------------------
# 18 Jan 2019
# HACK to use results of a previous version where image slices were
# still exported.
# (There was a bug after stack intensity correction, which resulted
# in v2v-reg transforms not being part of in the final registration
# transforms; Thus, slice transformations (tfm's) were flawed and
# could not be used):
else:
# Recover suffix for mask
pattern = stack_name + self._prefix_slice + \
"[0-9]+[_]([a-zA-Z]+)[.]nii.gz"
pm = re.compile(pattern)
matches = list(set([pm.match(f).group(1) for f in os.listdir(
abs_path_to_directory) if pm.match(f)]))
if len(matches) > 1:
raise RuntimeError("Suffix mask cannot be determined")
suffix_mask = "_%s" % matches[0]
# Recover stack
path_to_stack = os.path.join(
abs_path_to_directory, "%s.nii.gz" % stack_name)
path_to_stack_mask = os.path.join(
abs_path_to_directory, "%s%s.nii.gz" % (
stack_name, suffix_mask))
stack = st.Stack.from_filename(
path_to_stack, path_to_stack_mask)
# Recover slices
pattern_trafo_slices = stack_name + self._prefix_slice + \
"([0-9]+)[.]tfm"
p = re.compile(pattern_trafo_slices)
dic_slice_transforms = {
int(p.match(f).group(1)): os.path.join(
abs_path_to_directory, p.match(f).group(0))
for f in os.listdir(abs_path_to_directory) if p.match(f)
}
slices = self._stacks[i].get_slices()
for i_slice in range(self._stacks[i].get_number_of_slices()):
if i_slice in dic_slice_transforms.keys():
path_to_slice = re.sub(
".tfm", ".nii.gz", dic_slice_transforms[i_slice])
path_to_slice_mask = re.sub(
".tfm", "%s.nii.gz" % suffix_mask,
dic_slice_transforms[i_slice])
slice_sitk = sitk.ReadImage(path_to_slice)
slice_sitk_mask = sitk.ReadImage(path_to_slice_mask)
hack = sl.Slice.from_sitk_image(
slice_sitk=slice_sitk,
# slice_sitk=slice_sitk_mask, # mask for Mask-SRR!
slice_sitk_mask=slice_sitk_mask,
slice_number=slices[i_slice].get_slice_number(),
slice_thickness=slices[
i_slice].get_slice_thickness(),
)
self._stacks[i]._slices[i_slice] = hack
else:
self._stacks[i].delete_slice(slices[i_slice])
self._stacks[i].sitk = stack.sitk
self._stacks[i].sitk_mask = stack.sitk_mask
self._stacks[i].itk = stack.itk
self._stacks[i].itk_mask = stack.itk_mask
# -----------------------------------------------------------------
# print update information
ph.print_info(
"Stack '%s': Slice positions updated "
"(%d/%d slices deleted)" % (
stack_name,
len(self._stacks[i].get_deleted_slice_numbers()),
self._stacks[i].sitk.GetSize()[-1],
)
)
# delete entire stack if all slices were rejected
if self._stacks[i].get_number_of_slices() == 0:
ph.print_info(
"Stack '%s' removed as all slices were deleted" %
stack_name)
self._stacks[i] = None
# only return maintained stacks
self._stacks = [s for s in self._stacks if s is not None]
if len(self._stacks) == 0:
raise RuntimeError(
"All stacks removed. "
"Did you check that the correct motion-correction directory "
"was provided? "
"Or, in case of 3D mask SRR, that --suffix-mask input is "
"correct?")
def get_data(self):
return self._stacks
##
# Check slice_number of stack_name with entries in rejected_slices.json
# file. If there is a match, reject the slice
#
# rejected_slices.json serves as additional means of checking whether a
# slice shall be kept.
# Rationale: Potentially "old" slice transformations that were not deleted
# in the motion_correction folder can be detected here.
# \date 2019-04-09 09:55:39+0100
#
# \param self The object
# \param stack_name The stack name; string
# \param slice_number The slice number; integer
#
# \return False (reject slice) or True (keep it)
#
def _check_against_json_true(self, stack_name, slice_number):
# if slice_number of stack_name is in rejected slices, discard it
if stack_name in self._rejected_slices.keys():
if slice_number in self._rejected_slices[stack_name]:
ph.print_warning(
"Reject slice '%s-slice%d' as detected by "
"rejected_slices.json" % (
stack_name, slice_number)
)
return False
return True
##
# Dummy function in case no rejected_slices.json is in directory
# \date 2019-04-09 09:58:02+0100
#
# \param self The object
# \param stack_name The stack name
# \param slice_number The slice number
#
def _check_against_json_false(self, stack_name, slice_number):
return True
================================================
FILE: niftymic/utilities/n4_bias_field_correction.py
================================================
##
# \file n4_bias_field_correction.py
# \brief N4ITK Bias-Field Correction interface
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date May 2017
#
# Import libraries
import os
import sys
import itk
import SimpleITK as sitk
import numpy as np
import pysitk.simple_itk_helper as sitkh
import pysitk.python_helper as ph
import niftymic.base.stack as st
##
# Class implementing the segmentation propagation from one image to another
# \date 2017-05-10 23:48:08+0100
#
class N4BiasFieldCorrection(object):
def __init__(self,
stack=None,
use_mask=True,
convergence_threshold=1e-6,
spline_order=3,
wiener_filter_noise=0.11,
bias_field_fwhm=0.15,
prefix_corrected="",
):
self._stack = stack
self._use_mask = use_mask
self._convergence_threshold = convergence_threshold
self._spline_order = spline_order
self._wiener_filter_noise = wiener_filter_noise
self._bias_field_fwhm = bias_field_fwhm
self._prefix_corrected = prefix_corrected
self._stack_corrected = None
self._computational_time = ph.get_zero_time()
def set_stack(self, stack):
self._stack = stack
def get_bias_field_corrected_stack(self):
return st.Stack.from_stack(self._stack_corrected)
def get_computational_time(self):
return self._computational_time
def run_bias_field_correction(self):
time_start = ph.start_timing()
bias_field_corrector = sitk.N4BiasFieldCorrectionImageFilter()
bias_field_corrector.SetBiasFieldFullWidthAtHalfMaximum(
self._bias_field_fwhm)
bias_field_corrector.SetConvergenceThreshold(
self._convergence_threshold)
bias_field_corrector.SetSplineOrder(self._spline_order)
bias_field_corrector.SetWienerFilterNoise(self._wiener_filter_noise)
if self._use_mask:
image_sitk = bias_field_corrector.Execute(
self._stack.sitk, self._stack.sitk_mask)
else:
image_sitk = bias_field_corrector.Execute(self._stack.sitk)
# Reading of image might lead to slight differences
stack_corrected_sitk_mask = sitk.Resample(
self._stack.sitk_mask,
image_sitk,
sitk.Euler3DTransform(),
sitk.sitkNearestNeighbor,
0,
self._stack.sitk_mask.GetPixelIDValue())
self._stack_corrected = st.Stack.from_sitk_image(
image_sitk=image_sitk,
image_sitk_mask=stack_corrected_sitk_mask,
filename=self._prefix_corrected + self._stack.get_filename(),
slice_thickness=self._stack.get_slice_thickness(),
)
# Get computational time
self._computational_time = ph.stop_timing(time_start)
# Debug
# sitkh.show_stacks([self._stack, self._stack_corrected], label=["orig", "corr"])
================================================
FILE: niftymic/utilities/outlier_rejector.py
================================================
##
# \file outlier_rejector.py
# \brief Class to identify and reject outliers.
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date Jan 2019
#
import os
import scipy
import numpy as np
import SimpleITK as sitk
import matplotlib.pyplot as plt
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
import niftymic.validation.residual_evaluator as re
##
# Class to identify and reject outliers
# \date 2019-01-28 19:24:52+0100
#
class OutlierRejector(object):
def __init__(self,
stacks,
reference,
threshold,
use_slice_masks=False,
use_reference_mask=True,
measure="NCC",
verbose=True,
):
self._stacks = stacks
self._reference = reference
self._threshold = threshold
self._measure = measure
self._use_slice_masks = use_slice_masks
self._use_reference_mask = use_reference_mask
self._verbose = verbose
def get_stacks(self):
return self._stacks
def run(self):
residual_evaluator = re.ResidualEvaluator(
stacks=self._stacks,
reference=self._reference,
use_slice_masks=self._use_slice_masks,
use_reference_mask=self._use_reference_mask,
verbose=False,
measures=[self._measure],
)
residual_evaluator.compute_slice_projections()
residual_evaluator.evaluate_slice_similarities()
slice_sim = residual_evaluator.get_slice_similarities()
# residual_evaluator.show_slice_similarities(
# threshold=self._threshold,
# measures=[self._measure],
# directory="/tmp/spina/figs%s" % self._print_prefix[0:7],
# )
remove_stacks = []
for i, stack in enumerate(self._stacks):
nda_sim = np.nan_to_num(
slice_sim[stack.get_filename()][self._measure])
indices = np.where(nda_sim < self._threshold)[0]
slices = stack.get_slices()
# only those indices that match the available slice numbers
rejections = [
j for j in [s.get_slice_number() for s in slices]
if j in indices
]
for slice in slices:
if slice.get_slice_number() in rejections:
stack.delete_slice(slice)
if self._verbose:
txt = "Stack %d/%d (%s): Slice rejections %d/%d [%s]" % (
i + 1,
len(self._stacks),
stack.get_filename(),
len(stack.get_deleted_slice_numbers()),
stack.sitk.GetSize()[-1],
ph.convert_numbers_to_hyphenated_ranges(
stack.get_deleted_slice_numbers()),
)
if len(rejections) > 0:
res_values = nda_sim[rejections]
txt += " | Latest rejections: " \
"%d [%s] (%s < %g): %s" % (
len(rejections),
ph.convert_numbers_to_hyphenated_ranges(
rejections),
self._measure,
self._threshold,
np.round(res_values, 2).tolist(),
)
ph.print_info(txt)
# Log stack where all slices were rejected
if stack.get_number_of_slices() == 0:
remove_stacks.append(stack)
# Remove stacks where all slices where rejected
for stack in remove_stacks:
self._stacks.remove(stack)
if self._verbose:
ph.print_info("Stack '%s' removed entirely." %
stack.get_filename())
================================================
FILE: niftymic/utilities/parameter_normalization.py
================================================
##
# \file parameter_normalization.py
# \brief Class containing functions to normalize parameters. This can be
# used to normalize parameters used for optimization e.g.
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date Nov 2016
#
## Import libraries
import sys
# import SimpleITK as sitk
# import itk
import numpy as np
## Import modules
import pysitk.simple_itk_helper as sitkh
##
# Class to normalize parameters
# \date 2016-11-10 17:05:30+0000
#
class ParameterNormalization(object):
##
# Constructor
# \date 2016-11-10 17:08:25+0000
#
# \param self The object
# \param parameters_array (N x N_param)-np.array. N_param
# different types of parameters and N amount
# of each of them.
#
def __init__(self, parameters_array):
## Create copy of parameters
self._parameters_array = np.array(parameters_array)
## Amount of different parameters
self._N_parameters = self._parameters_array.shape[1]
self._coefficients = 1.* np.concatenate((np.zeros((1,self._N_parameters)), np.ones((1,self._N_parameters))))
##
# Gets the normalization coefficients as (2 x
# N_param)-np.array.
# \date 2016-11-10 18:00:52+0000
#
# The first row denotes the mean and the second the computed standard
# deviation of the originally provided parameter array.
#
# \param self The object
#
# \return The normalization coefficients as (2 x N_param)-np.array.
#
def get_normalization_coefficients(self):
return np.array(self._coefficients)
##
# Calculates the normalization coefficients which will be used
# for normalization and denormalization routines.
# \date 2016-11-10 18:01:17+0000
#
# \param self The object
#
def compute_normalization_coefficients(self):
coefficients = np.zeros((2, self._N_parameters))
for i in range(0, self._N_parameters):
coefficients[0,i] = np.mean(self._parameters_array[:,i])
sigma = np.std(self._parameters_array[:,i])
if abs(sigma) < 1e-8:
coefficients[1,i] = 1.
else:
coefficients[1,i] = sigma
self._coefficients = coefficients
##
# Normalize parameters based on previously computed
# coefficients.
# \date 2016-11-10 18:04:05+0000
#
# \remark I would like to not make a copy of the parameters
#
# \param self The object
# \param parameters (N x N_params)-np.array to be normalized
#
# \return normalized parameter array
#
def normalize_parameters(self, parameters):
parameters = np.array(parameters)
## Compute p_norm = (p - mean)/std
for i in range(0, self._N_parameters):
parameters[:,i] = (parameters[:,i] - self._coefficients[0,i])/self._coefficients[1,i]
return parameters
##
# Denormalize parameters based on previously computed
# coefficients.
# \date 2016-11-10 18:05:49+0000
#
# \param self The object
# \param parameters (N x N_params)-np.array to be normalized
#
# \return denormalized parameter array
#
def denormalize_parameters(self, parameters):
parameters = np.array(parameters)
## Compute p = p_norm*std + mean
for i in range(0, self._N_parameters):
parameters[:,i] = parameters[:,i]*self._coefficients[1,i] + self._coefficients[0,i]
return parameters
================================================
FILE: niftymic/utilities/segmentation_propagation.py
================================================
##
# \file SegmentationPropagation.py
# \brief { item_description }
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date May 2017
#
# Import libraries
import sys
import itk
import SimpleITK as sitk
import numpy as np
import pysitk.simple_itk_helper as sitkh
import pysitk.python_helper as ph
import niftymic.base.stack as st
import niftymic.utilities.stack_mask_morphological_operations as stmorph
##
# Class implementing the segmentation propagation from one image to another
# \date 2017-05-10 23:48:08+0100
#
class SegmentationPropagation(object):
# Constructor
def __init__(self,
stack=None,
template=None,
registration_method=None,
use_template_mask=True,
dilation_radius=0,
dilation_kernel="Ball",
use_dilation_in_plane_only=True,
interpolator="NearestNeighbor"):
self._stack = stack
self._template = template
self._registration_method = registration_method
self._dilation_radius = dilation_radius
self._dilation_kernel = dilation_kernel
self._use_dilation_in_plane_only = use_dilation_in_plane_only
self._interpolator = interpolator
self._stack_sitk = None
self._stack_sitk_mask = None
self._registration_transform_sitk = None
self._use_template_mask = use_template_mask
def set_stack(self, stack):
self._stack = stack
def get_stack(self):
return self._stack
def set_template(self, template):
self._template = template
def get_template(self):
return self._template
def set_dilation_radius(self, dilation_radius):
self._dilation_radius = dilation_radius
def get_dilation_radius(self):
return self._dilation_radius
def set_dilation_kernel(self, dilation_kernel):
if dilation_kernel not in ['Ball', 'Box', 'Annulus', 'Cross']:
raise ValueError(
"Dilation kernel must be 'Ball', 'Box', 'Annulus' or 'Cross'.")
self._dilation_kernel = dilation_kernel
def get_dilation_kernel(self):
return self._dilation_kernel
def get_segmented_stack(self):
# Create new Stack instance
stack_aligned_masked = st.Stack.from_sitk_image(
image_sitk=self._stack_sitk,
filename=self._stack.get_filename(),
image_sitk_mask=self._stack_sitk_mask,
slice_thickness=self._stack.get_slice_thickness(),
)
return stack_aligned_masked
def get_registration_transform_sitk(self):
return self._registration_transform_sitk
def run_segmentation_propagation(self):
if self._stack is None or self._template is None:
raise ValueError("Specify stack and template first")
# Choose interpolator
try:
interpolator_str = self._interpolator
interpolator = eval("sitk.sitk" + interpolator_str)
except:
raise ValueError("Error: interpolator is not known")
self._stack_sitk = sitk.Image(self._stack.sitk)
# Register stack to template
if self._registration_method is not None:
self._registration_method.set_fixed(self._template)
self._registration_method.set_moving(self._stack)
self._registration_method.use_fixed_mask(self._use_template_mask)
self._registration_method.run()
self._registration_transform_sitk = self._registration_method.get_registration_transform_sitk()
self._registration_transform_sitk = eval(
"sitk." + self._registration_transform_sitk.GetName() + "(self._registration_transform_sitk.GetInverse())")
self._stack_sitk = sitkh.get_transformed_sitk_image(
self._stack_sitk, self._registration_transform_sitk)
# Propagate mask
self._stack_sitk_mask = sitk.Resample(self._template.sitk_mask, self._stack_sitk, sitk.Euler3DTransform(
), interpolator, 0, self._template.sitk_mask.GetPixelIDValue())
# Dilate mask
if self._dilation_radius > 0:
stack_mask_morpher = stmorph.StackMaskMorphologicalOperations.from_sitk_mask(
mask_sitk=self._stack_sitk_mask,
dilation_radius=self._dilation_radius,
dilation_kernel=self._dilation_kernel,
use_dilation_in_plane_only=self._use_dilation_in_plane_only,
)
stack_mask_morpher.run_dilation()
self._stack_sitk_mask = stack_mask_morpher.get_processed_mask_sitk()
# sitkh.show_sitk_image(self._stack_sitk_mask)
================================================
FILE: niftymic/utilities/siena.py
================================================
##
# \file siena.py
# \brief
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date Nov 2016
#
# Import libraries
import SimpleITK as sitk
import numpy as np
import sys
import os
import re
from skimage.measure import compare_ssim as ssim
# Import modules
import pysitk.simple_itk_helper as sitkh
import pysitk.python_helper as ph
import niftymic.base.stack as st
from niftymic.definitions import DIR_TMP
class Siena(object):
def __init__(self,
stack1,
stack2,
dir_output="./siena/",
options='-B "-B -f 0.1" -2',
dir_tmp=os.path.join(DIR_TMP, "siena/")):
self._stack1 = st.Stack.from_stack(stack1)
self._stack2 = st.Stack.from_stack(stack2)
self._dir_output = dir_output
self._dir_tmp = dir_tmp
self._options = options
def run(self):
ph.create_directory(dir_tmp, delete_files=True)
# Write images
sitkh.write_nifti_image_sitk(self._stack1.sitk, self._dir_tmp +
self._stack1.get_filename() + ".nii.gz")
sitkh.write_nifti_image_sitk(self._stack2.sitk, self._dir_tmp +
self._stack2.get_filename() + ".nii.gz")
cmd = "siena "
cmd += self._dir_tmp + self._stack1.get_filename() + ".nii.gz "
cmd += self._dir_tmp + self._stack2.get_filename() + ".nii.gz "
cmd += "-o " + self._dir_output + " "
cmd += self._options
time_start = ph.start_timing()
ph.execute_command(cmd)
self._elapsed_time = ph.stop_timing(time_start)
# Extract measures from report
self._extract_percentage_brain_volume_change()
def print_statistics(self):
print("\tElapsed time: %s" % (self._elapsed_time))
print("\tPercentage Brain Volume Change (PBVC): %.2f%%" %
(self._percentage_brain_volume_change))
##
# Percentage Brain Volume Change
# \date 2016-11-27 17:42:55+0000
#
# \param self The object
#
def _extract_percentage_brain_volume_change(self):
datafile = file(self._dir_output + "report.siena")
for line in datafile:
if "finalPBVC" in line:
parts = line.split(" ")
break
self._percentage_brain_volume_change = float(parts[1])
def get_percentage_brain_volume_change(self):
return self._percentage_brain_volume_change
================================================
FILE: niftymic/utilities/stack_mask_morphological_operations.py
================================================
##
# \file stack_mask_morphological_operations.py
# \brief { item_description }
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date May 2017
#
import sys
import itk
import numpy as np
import SimpleITK as sitk
import pysitk.simple_itk_helper as sitkh
import pysitk.python_helper as ph
import niftymic.base.stack as st
##
# Class implementing the segmentation propagation from one image to another
# \date 2017-05-10 23:48:08+0100
#
class StackMaskMorphologicalOperations(object):
##
# { constructor_description }
# \date 2017-05-18 16:58:23+0100
#
# \param self The object
# \param mask_sitk The mask sitk
# \param dilation_radius The dilation radius
# \param dilation_kernel The dilation kernel
# \param use_dilation_in_plane_only The use dilation in plane only
#
def __init__(self, dilation_radius, dilation_kernel, use_dilation_in_plane_only):
self._dilation_radius = dilation_radius
self._dilation_kernel = dilation_kernel
self._use_dilation_in_plane_only = use_dilation_in_plane_only
@classmethod
def from_sitk_mask(cls, mask_sitk=None, dilation_radius=0, dilation_kernel="Ball", use_dilation_in_plane_only=True):
self = cls(dilation_radius=dilation_radius, dilation_kernel=dilation_kernel,
use_dilation_in_plane_only=use_dilation_in_plane_only)
self._mask_sitk = mask_sitk
self._stack = None
return self
@classmethod
def from_stack(cls, stack=None, dilation_radius=0, dilation_kernel="Ball", use_dilation_in_plane_only=True):
self = cls(dilation_radius=dilation_radius, dilation_kernel=dilation_kernel,
use_dilation_in_plane_only=use_dilation_in_plane_only)
self._mask_sitk = stack.sitk_mask
self._stack = stack
return self
def set_mask_sitk(self, mask_sitk):
self._mask_sitk = mask_sitk
def get_mask_sitk(self):
return self._mask_sitk
def get_stack(self):
return st.Stack.from_stack(self._stack)
def set_dilation_radius(self, dilation_radius):
self._dilation_radius = dilation_radius
def get_dilation_radius(self):
return self._dilation_radius
def set_dilation_kernel(self, dilation_kernel):
if dilation_kernel not in ['Ball', 'Box', 'Annulus', 'Cross']:
raise ValueError(
"Dilation kernel must be 'Ball', 'Box', 'Annulus' or 'Cross'.")
self._dilation_kernel = dilation_kernel
def get_dilation_kernel(self):
return self._dilation_kernel
def get_processed_mask_sitk(self):
return sitk.Image(self._mask_sitk)
def get_processed_stack(self):
if self._stack is None:
raise ValueError("No Stack instance was provided")
else:
return st.Stack.from_sitk_image(self._stack.sitk, self._stack.get_filename(), self._mask_sitk)
def get_computational_time(self):
return self._computational_time
def run_dilation(self):
time_start = ph.start_timing()
dilater = sitk.BinaryDilateImageFilter()
dilater.SetKernelType(eval("sitk.sitk" + self._dilation_kernel))
dilater.SetKernelRadius(self._dilation_radius)
if self._use_dilation_in_plane_only:
shape = self._mask_sitk.GetSize()
N_slices = shape[2]
nda_mask = np.zeros(shape[::-1], dtype=np.uint8)
for i in range(0, N_slices):
slice_mask_sitk = self._mask_sitk[:, :, i:i + 1]
mask_sitk = dilater.Execute(slice_mask_sitk)
nda_mask[i, :, :] = sitk.GetArrayFromImage(mask_sitk)
mask_sitk = sitk.GetImageFromArray(nda_mask)
mask_sitk.CopyInformation(self._mask_sitk)
self._mask_sitk = mask_sitk
else:
self._mask_sitk = dilater.Execute(self._mask_sitk)
if self._stack is not None:
self._stack = st.Stack.from_sitk_image(
self._stack.sitk,
image_sitk_mask=self._mask_sitk,
filename=self._stack.get_filename(),
slice_thickness=self._stack.get_slice_thickness(),
)
self._computational_time = ph.stop_timing(time_start)
================================================
FILE: niftymic/utilities/target_stack_estimator.py
================================================
##
# \file target_stack_estimator.py
# \brief Class to estimate target stack automatically
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date January 2018
#
import os
import re
import numpy as np
import SimpleITK as sitk
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
import niftymic.base.stack as st
##
# Class to estimate target stack automatically
# \date 2018-01-26 16:32:11+0000
#
class TargetStackEstimator(object):
def __init__(self):
self._target_stack_index = None
self._compute_motion_score = {
# implementation according to github fetalReconstruction
"github": self._compute_motion_score_github,
# implementation according to TMI paper Kainz2015
"kainz2015": self._compute_motion_score_kainz2015,
}
# appears more meaningful to me (see comments below)
self._mode = "kainz2015"
self._computational_time = ph.get_zero_time()
def get_target_stack_index(self):
return self._target_stack_index
def get_computational_time(self):
return self._computational_time
@staticmethod
def _compute_volume(file_path):
mask_sitk = sitkh.read_nifti_image_sitk(str(file_path), sitk.sitkUInt8)
# Compute mask volume
mask_nda = sitk.GetArrayFromImage(mask_sitk)
spacing = np.array(mask_sitk.GetSpacing())
volume = np.sum(mask_nda) * spacing.prod()
return volume
@staticmethod
def _compute_singular_values(stack):
# (z,y,x) array
nda = sitk.GetArrayFromImage(stack.sitk)
# reshape to M x K, M number of slice pixels, K number of slices
A = nda.reshape(nda.shape[0], -1).transpose()
U, s, Vt = np.linalg.svd(A)
return s
##
# Calculates the motion score similar to TMI Kainz2015 paper. Instead, a
# relative rank is used in order to not penalize stacks with more slices.
# (Motion score: A lower score indicates less motion).
# @date 2019-08-31 16:01:12+0100
#
# @param sing_values singular values, vector
# @param threshold error threshold, float
#
# @return The motion score, float.
#
@staticmethod
def _compute_motion_score_kainz2015(
sing_values, threshold=np.sqrt(1 - 0.99**2)):
sing_values_2 = np.square(sing_values)
# compute Frobenius norm
A_norm = np.sum(sing_values_2)
# compute relative rank-approximation error
# the higher r, the smaller the delta_r
delta_r = np.sqrt(np.array([
np.sum(sing_values_2[r + 1:]) / A_norm
for r in range(len(sing_values_2))
]))
# find lowest rank approximation that leads to error < threshold
r = np.where(delta_r < threshold)[0][0] + 1
# use relative rank so as to not "penalise" stack with more slices
r_rel = r / float(len(sing_values))
# surrogate motion score, the lower the better
motion_score = r_rel * delta_r[r - 1]
return motion_score
##
# Calculates the motion score based on TMI Kainz2015 paper, but the version
# as found on the GitHub fetalReconstruction repo. (A lower motion score
# shall indicate less motion).
# @date 2019-08-31 16:01:12+0100
#
# @param sing_values singular values, vector
# @param threshold error threshold, float
#
# @return The motion score, float.
#
@staticmethod
def _compute_motion_score_github(sing_values, threshold=0.99):
sing_values_2 = np.square(sing_values)
# compute Frobenius norm
A_norm = np.sum(sing_values_2)
# compute relative rank-approximation quality
# the higher r, the higher the delta_r
delta_r = np.sqrt(np.array([
np.sum(sing_values_2[0:r + 1]) / A_norm
for r in range(len(sing_values_2))
]))
# find highest rank approximation that leads to error <
# threshold
r = np.where(delta_r < threshold)[0][-1] + 1
# r = np.where(delta_r > threshold)[0][0] + 1 # lowest
# surrogate motion score
# NOTE: that's weird to me, should be the lower the better,
# but delta_r refers to relative rank-approximation quality,
# thus, the higher the better!
motion_score = r * delta_r[r - 1]
return motion_score
##
# Use stack with largest mask volume as target stack
# \date 2018-01-26 16:52:39+0000
#
# \param cls The cls
# \param file_paths_masks paths to image masks as list of strings
#
@classmethod
def from_volume(cls, file_paths_masks):
t0 = ph.start_timing()
target_stack_estimator = cls()
volumes = np.array([
TargetStackEstimator._compute_volume(f) for f in file_paths_masks
])
# find index to smallest "valid" volume, i.e. volume > q * median
index = np.argmax(
volumes[np.argsort(volumes)] > 0.7 * np.median(volumes))
index = np.argsort(volumes)[index]
# Get index corresponding to maximum volume stack mask
# index = np.argmax(volumes)
# index = np.argmin(volumes)
# Get index corresponding to median volume stack mask
# index = np.argsort(volumes)[len(volumes)//2]
target_stack_estimator._target_stack_index = index
# computational time
target_stack_estimator._computational_time = ph.stop_timing(t0)
return target_stack_estimator
##
# Compute target stack based on method presented in TMI Kainz2015. However,
# only on masked anatomy (bounding box) is used in addition to a relative
# rank weighting.
# @date 2019-08-30 14:33:24+0100
#
# @param cls The cls
# @param file_paths The file paths
# @param file_paths_masks The file paths masks
#
# @return target_stack_estimator instance
#
@classmethod
def from_motion_score(cls, file_paths, file_paths_masks):
if len(file_paths) != len(file_paths_masks):
raise ValueError(
"Number of provided images and masks must match")
t0 = ph.start_timing()
tse = cls()
volumes = np.array([
TargetStackEstimator._compute_volume(f) for f in file_paths_masks
])
# only allow stacks with minimum volume, i.e. anatomical/brain coverage
vol_min = 0.7 * np.median(volumes)
indices = [i for i in range(len(volumes)) if volumes[i] > vol_min]
# read all eligible stacks
stacks = [
st.Stack.from_filename(
file_path=file_paths[i],
file_path_mask=file_paths_masks[i],
extract_slices=False,
) for i in indices
]
# crop stack to bounding box of mask
stacks = [s.get_cropped_stack_based_on_mask() for s in stacks]
# debug
# for i_stack, stack in enumerate(stacks):
# stack.show(label=str(indices[i_stack]))
# compute motion scores of eligible stacks
motion_scores = [None] * len(indices)
for i_stack in range(len(indices)):
# compute singular values
s = TargetStackEstimator._compute_singular_values(stacks[i_stack])
# compute motion score
motion_scores[i_stack] = tse._compute_motion_score[tse._mode](s)
# select stack with minimum motion (score)
selection_best = np.argmin(motion_scores)
# reference back to input file_paths index
target_stack_index = indices[selection_best]
tse._target_stack_index = target_stack_index
# computational time
tse._computational_time = ph.stop_timing(t0)
# debug
# print(indices, len(indices), len(file_paths))
# print("Best: %d" % target_stack_index)
# print(motion_scores)
# print(tse.get_computational_time())
# ph.killall_itksnap()
return tse
================================================
FILE: niftymic/utilities/template_stack_estimator.py
================================================
##
# \file template_stack_estimator.py
# \brief Class to estimate template stack automatically
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date January 2018
#
import os
import re
import json
import numpy as np
import skimage.measure
import SimpleITK as sitk
import pysitk.simple_itk_helper as sitkh
from niftymic.definitions import DIR_TEMPLATES, TEMPLATES_INFO
##
# Class to estimate template stack automatically
# \date 2018-01-26 16:32:11+0000
#
class TemplateStackEstimator(object):
def __init__(self):
self._template_path = None
self._estimated_gw = None
##
# Gets the path to estimated template.
# \date 2018-01-27 02:14:53+0000
#
# \param self The object
#
# \return The path to template.
#
def get_path_to_template(self):
return self._template_path
def get_estimated_gw(self):
return self._estimated_gw
##
# Select template with similar brain volume
# \date 2018-01-26 16:52:39+0000
#
# \param cls The cls
# \param file_paths_masks paths to image masks as list of strings
#
@classmethod
def from_mask(cls, file_path_mask):
template_stack_estimator = cls()
mask_sitk = sitkh.read_nifti_image_sitk(
file_path_mask, sitk.sitkUInt8)
mask_nda = sitk.GetArrayFromImage(mask_sitk)
# get largest connected region (if more than one connected region)
mask_nda = TemplateStackEstimator.get_largest_connected_region_mask(
mask_nda)
spacing = np.array(mask_sitk.GetSpacing())
volume = len(np.where(mask_nda > 0)[0]) * spacing.prod()
# Read in template info
path_to_template_info = os.path.join(DIR_TEMPLATES, TEMPLATES_INFO)
with open(path_to_template_info) as json_file:
dic = json.load(json_file)
# Get gestational ages as list of integers
gestational_ages = sorted([int(gw) for gw in dic.keys()])
# Get matching gestational age
template_volumes = np.array(
[dic[str(k)]["volume_mask"] for k in gestational_ages])
index = np.argmin(np.abs(template_volumes - volume))
template_stack_estimator._estimated_gw = int(gestational_ages[index])
template_stack_estimator._template_path = os.path.join(
DIR_TEMPLATES, dic[str(gestational_ages[index])]["image"])
return template_stack_estimator
# # Ensure valid index after correction
# index = np.max([0, index - 1])
# # index = np.min([index + 1, len(template_volumes)-1])
# # Matching gestational age/week
# gw_match = str(gestational_ages[index])
# template_stack_estimator._template_path = os.path.join(
# DIR_TEMPLATES, dic[gw_match]["image"])
# return template_stack_estimator
# # Find template which has slightly smaller mask volume
# for k in gestational_ages:
# if dic[str(k)]["volume_mask_dil"] > volume:
# key = str(np.max([gestational_ages[0], k - 1]))
# template_stack_estimator._estimated_gw = int(key)
# template_stack_estimator._template_path = os.path.join(
# DIR_TEMPLATES, dic[key]["image"])
# return template_stack_estimator
# # Otherwise, return path to oldest template image available
# template_stack_estimator._estimated_gw = int(gestational_ages[-1])
# template_stack_estimator._template_path = os.path.join(
# DIR_TEMPLATES, dic[str(gestational_ages[-1])]["image"])
# return template_stack_estimator
##
# Gets the label/mask representing the largest connected region.
# \date 2019-02-26 16:30:01+0000
#
# \param mask_nda The mask nda
#
# \return The largest connected region as np.array.
#
@staticmethod
def get_largest_connected_region_mask(mask_nda):
# get label for each connected component
labels_nda = skimage.measure.label(mask_nda)
# only pick largest connected region
if labels_nda.max() > 1:
volumes = [
labels_nda[np.where(labels_nda == i)].sum()
for i in range(1, labels_nda.max() + 1)
]
label_max = np.argmax(np.array(volumes)) + 1
mask_nda = np.zeros_like(mask_nda)
mask_nda[np.where(labels_nda == label_max)] = 1
return mask_nda
================================================
FILE: niftymic/utilities/toolkit_executor.py
================================================
##
# \file toolkit_executor.py
# \brief generates function calls to execute other reconstruction toolkits
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date Jan 2018
#
import os
import pysitk.python_helper as ph
EXE_IRTK = {
"workstation": "/home/mebner/Development/VolumetricReconstruction_ImperialCollege/source/bin/SVRreconstructionGPU"
}
##
# Class to generate functions calls to execute other reconstruction toolkits
# \date 2018-01-27 02:12:00+0000
#
class ToolkitExecuter(object):
def __init__(self, paths_to_images, paths_to_masks, dir_output):
self._paths_to_images = paths_to_images
self._paths_to_masks = paths_to_masks
self._dir_output = dir_output
# separator for command line export
self._sep = " \\\n"
self._subdir_temp = "temp"
##
# Gets the function call for fetalReconstruction toolkit provided by
# Bernhard Kainz.
# \date 2018-01-27 02:12:26+0000
#
# \param self The object
# \param option_args The option arguments
# \param exe The executable
# \param output_name The output name
#
# \return The function call irtk.
#
def get_function_call_irtk(
self,
option_args=['-d 0', '--useCPU', '--resolution 1'],
exe=None,
output_name="IRTK_SRR.nii.gz",
kernel_mask_dilation=None,
verbose=False,
):
if exe is None:
exe = EXE_IRTK["workstation"]
self._dir_temp = os.path.join("${DIR_OUT}", self._subdir_temp)
cmd_args = []
# store pwd
cmd_args.append("CWD=$(pwd)")
cmd_args.append("DIR_OUT=%s" % self._dir_output)
# change to output directory
cmd_args.append("printf 'Change to output directory: %s\n' ${DIR_OUT}")
# cmd_args.append("\necho 'Change to output directory'")
cmd_args.append("mkdir -p ${DIR_OUT}")
cmd_args.append("cd ${DIR_OUT}")
# create temp directory if required
cmd_args.append("\necho 'Create temp directory'")
cmd_args.append("mkdir -p %s" % self._dir_temp)
cmd_args.append("cd %s" % self._dir_temp)
# dilate masks
if kernel_mask_dilation is not None:
cmd_args.append("\necho 'Dilate masks'")
cmd_args.append(self._exe_dilate_masks(
kernel_mask_dilation, self._paths_to_masks))
# exe to determine slice thickness for toolkit
cmd_args.append("\necho 'Fetch slice thickness for all stacks'")
cmd_args.append(self._exe_to_fetch_slice_thickness(
self._paths_to_images))
# toolkit execution
cmd_args.append("\necho 'IRTK Toolkit Execution'")
exe_args = [exe]
exe_args.append("-o %s" % output_name)
exe_args.append("-i %s%s" %
(self._sep, self._sep.join(self._paths_to_images)))
# exe_args.append("--manualMask %s" % self._paths_to_masks[0]) #
# causes cuda sync error!?
exe_args.append("-m %s%s" %
(self._sep, self._sep.join(self._paths_to_masks)))
exe_args.append("--thickness `printf \"%s\" \"${thickness}\"`")
exe_args.extend(option_args)
toolkit_execution = "%s" % self._sep.join(exe_args)
cmd_args.append(toolkit_execution)
cmd_args.append("cp -p %s ${DIR_OUT}/" % (output_name))
cmd_args.append("cd ${DIR_OUT}")
if not verbose:
cmd_args.append("\necho 'Delete temp directory'")
cmd_args.append("rm -rf %s" % self._dir_temp)
# cmd_args.append("\necho 'Change back to original directory'")
cmd_args.append("printf 'Change back to original directory: %s\n' ${CWD}")
cmd_args.append("cd ${CWD}")
cmd_args.append("\n")
cmd = (" \n").join(cmd_args)
return cmd
@staticmethod
def write_function_call_to_file(function_call, path_to_file):
text = "#!/bin/zsh\n\n%s" % function_call
ph.write_to_file(path_to_file, text, verbose=False)
ph.execute_command("chmod +x %s" % path_to_file, verbose=False)
##
# Provide bash-commands to read out slice thickness on-the-fly
#
# Rationale: IRTK recon toolkit assumes otherwise a thickness of twice the
# voxel spacing by default
# \date 2018-01-27 02:12:52+0000
#
# \param paths_to_images The paths to images
#
# \return bash command as string
#
def _exe_to_fetch_slice_thickness(self, paths_to_images):
cmd_args = []
cmd_args.append("args=()")
cmd_args.append("for i in %s" % (self._sep.join(paths_to_images)))
cmd_args.append("do")
cmd_args.append(
"t=$(fslhd ${i} | grep pixdim3 | awk -F ' ' '{print $2}')")
cmd_args.append("args+=(\" ${t}\")")
cmd_args.append("done")
cmd_args.append("thickness=${args[@]}")
cmd_args.append("")
cmd = ("\n").join(cmd_args)
return cmd
##
# Provide bash-commands to read out slice thickness on-the-fly
#
# Rationale: IRTK recon toolkit assumes otherwise a thickness of twice the
# voxel spacing by default
# \date 2018-01-27 02:12:52+0000
#
# \param paths_to_images The paths to images
#
# \return bash command as string
# #
def _exe_dilate_masks(self, kernel, paths_to_masks, label=1):
cmd_loop = []
kernel_str = [str(k) for k in kernel]
# Export dilated mask to temp directory
for i_mask, path_to_mask in enumerate(paths_to_masks):
directory = os.path.dirname(path_to_mask)
mask_filename = os.path.basename(path_to_mask)
path_to_mask_dilated = os.path.join(
self._dir_temp, ph.append_to_filename(mask_filename, "_dil"))
cmd_args = ["c3d"]
cmd_args.append(path_to_mask)
cmd_args.append("-dilate %s %smm" % (label, "x".join(kernel_str)))
cmd_args.append("-o %s" % path_to_mask_dilated)
cmd = self._sep.join(cmd_args)
cmd_loop.append(cmd)
paths_to_masks[i_mask] = path_to_mask_dilated
return "\n".join(cmd_loop)
================================================
FILE: niftymic/utilities/volumetric_reconstruction_pipeline.py
================================================
##
# \file volumetric_reconstruction_pipeline.py
# \brief Collection of modules useful for registration and
# reconstruction tasks.
#
# E.g. Volume-to-Volume Registration, Slice-to-Volume registration,
# Multi-component Reconstruction.
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date Aug 2017
#
import six
import numpy as np
import SimpleITK as sitk
from abc import ABCMeta, abstractmethod
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
import niftymic.base.stack as st
import niftymic.validation.motion_evaluator as me
import niftymic.utilities.outlier_rejector as outre
import niftymic.registration.transform_initializer as tinit
import niftymic.reconstruction.scattered_data_approximation as sda
import niftymic.utilities.binary_mask_from_mask_srr_estimator as bm
from niftymic.definitions import VIEWER
##
# Class which holds basic interface for all modules
# \date 2017-08-08 02:20:40+0100
#
class Pipeline(object):
__metaclass__ = ABCMeta
def __init__(self, stacks, verbose, viewer):
self._stacks = stacks
self._verbose = verbose
self._viewer = viewer
self._computational_time = ph.get_zero_time()
def set_stacks(self, stacks):
self._stacks = stacks
def get_stacks(self):
return [st.Stack.from_stack(stack) for stack in self._stacks]
def set_verbose(self, verbose):
self._verbose = verbose
def get_verbose(self):
return self._verbose
def get_computational_time(self):
return self._computational_time
def run(self):
time_start = ph.start_timing()
self._run()
self._computational_time = ph.stop_timing(time_start)
if self._verbose:
ph.print_info("Required computational time: %s" %
(self.get_computational_time()))
@abstractmethod
def _run(self):
pass
##
# Class which holds basic interface for all registration associated modules
# \date 2017-08-08 02:21:17+0100
#
class RegistrationPipeline(Pipeline):
__metaclass__ = ABCMeta
##
# Store variables relevant to register stacks to a certain reference volume
# \date 2017-08-08 02:21:56+0100
#
# \param self The object
# \param verbose Verbose output, bool
# \param stacks List of Stack objects
# \param reference Reference as Stack object
# \param registration_method Registration method, e.g.
# SimpleItkRegistration
#
def __init__(self, verbose, stacks, reference, registration_method, viewer):
Pipeline.__init__(self, stacks=stacks, verbose=verbose, viewer=viewer)
self._reference = reference
self._registration_method = registration_method
def set_reference(self, reference):
self._reference = reference
def get_reference(self):
return st.Stack.from_stack(self._reference)
##
# Class to perform Volume-to-Volume registration
# \date 2017-08-08 02:28:56+0100
#
class VolumeToVolumeRegistration(RegistrationPipeline):
##
# Store relevant information to perform Volume-to-Volume registration
# \date 2017-08-08 02:29:13+0100
#
# \param self The object
# \param stacks The stacks
# \param reference The reference
# \param registration_method The registration method
# \param verbose The verbose
#
def __init__(self,
stacks,
reference,
registration_method,
verbose=1,
viewer=VIEWER,
robust=False,
):
RegistrationPipeline.__init__(
self,
stacks=stacks,
reference=reference,
registration_method=registration_method,
viewer=viewer,
verbose=verbose,
)
self._robust = robust
def _run(self):
ph.print_title("Volume-to-Volume Registration")
for i in range(0, len(self._stacks)):
txt = "Volume-to-Volume Registration -- " \
"Stack %d/%d" % (i + 1, len(self._stacks))
if self._verbose:
ph.print_subtitle(txt)
else:
ph.print_info(txt)
if self._robust:
transform_initializer = tinit.TransformInitializer(
fixed=self._reference,
moving=self._stacks[i],
similarity_measure="NCC",
refine_pca_initializations=True,
)
transform_initializer.run()
transform_sitk = transform_initializer.get_transform_sitk()
transform_sitk = sitk.AffineTransform(
transform_sitk.GetInverse())
else:
self._registration_method.set_moving(self._reference)
self._registration_method.set_fixed(self._stacks[i])
self._registration_method.run()
transform_sitk = self._registration_method.get_registration_transform_sitk()
# Update position of stack
self._stacks[i].update_motion_correction(transform_sitk)
##
# Class to perform Slice-To-Volume registration
# \date 2017-08-08 02:30:03+0100
#
class SliceToVolumeRegistration(RegistrationPipeline):
##
# { constructor_description }
# \date 2017-08-08 02:30:18+0100
#
# \param self The object
# \param stacks The stacks
# \param reference The reference
# \param registration_method Registration method, e.g.
# SimpleItkRegistration
# \param verbose The verbose
# \param print_prefix Print at each iteration at the
# beginning, string
#
def __init__(self,
stacks,
reference,
registration_method,
verbose=1,
print_prefix="",
interleave=2,
viewer=VIEWER,
):
RegistrationPipeline.__init__(
self,
stacks=stacks,
reference=reference,
registration_method=registration_method,
verbose=verbose,
viewer=viewer,
)
self._print_prefix = print_prefix
self._interleave = interleave
def set_print_prefix(self, print_prefix):
self._print_prefix = print_prefix
def _run(self):
ph.print_title("Slice-to-Volume Registration")
self._registration_method.set_moving(self._reference)
for i, stack in enumerate(self._stacks):
slices = stack.get_slices()
transforms_sitk = {}
for j, slice_j in enumerate(slices):
txt = "%sSlice-to-Volume Registration -- " \
"Stack %d/%d (%s) -- Slice %d/%d" % (
self._print_prefix,
i + 1, len(self._stacks), stack.get_filename(),
j + 1, len(slices))
if self._verbose:
ph.print_subtitle(txt)
else:
ph.print_info(txt)
self._registration_method.set_fixed(slice_j)
self._registration_method.run()
# Store information on registration transform
transform_sitk = \
self._registration_method.get_registration_transform_sitk()
transforms_sitk[slice_j.get_slice_number()] = transform_sitk
# Update position of slice
for slice in slices:
slice_number = slice.get_slice_number()
slice.update_motion_correction(transforms_sitk[slice_number])
##
# Class to perform registration for the stack based on a specified set of
# slices
# \date 2017-10-16 12:52:18+0100
#
class SliceSetToVolumeRegistration(RegistrationPipeline):
##
# { constructor_description }
# \date 2017-10-16 12:53:04+0100
#
# \param self The object
# \param stacks The stacks
# \param reference The reference
# \param registration_method The registration method
# \param slice_index_sets_of_stacks Dictionary specifying the slice
# index sets for all stacks
# \param verbose The verbose
# \param print_prefix The print prefix
#
def __init__(self,
stack,
reference,
registration_method,
slice_set_indices,
verbose=1,
print_prefix="",
viewer=VIEWER,
):
RegistrationPipeline.__init__(
self,
stacks=[stack],
reference=reference,
registration_method=registration_method,
verbose=verbose,
viewer=viewer,
)
self._print_prefix = print_prefix
self._slice_set_indices = slice_set_indices
def _run(self, debug=1):
stack = self._stacks[0]
slices = stack.get_slices()
for i, indices in enumerate(self._slice_set_indices):
txt = "%s Split %d/%d -- Slices %s" % (
self._print_prefix, i + 1,
len(self._slice_set_indices), str(indices))
if self._verbose:
ph.print_subtitle(txt)
else:
ph.print_info(txt)
image = self._get_stack_subgroup(indices)
if debug:
first = np.linalg.norm(
stack.get_slice(indices[0]).sitk.GetOrigin() -
np.array(image.sitk[:, :, 0:1].GetOrigin()))
last = np.linalg.norm(
stack.get_slice(indices[-1]).sitk.GetOrigin() -
np.array(image.sitk[:, :, -1:].GetOrigin()))
if first > 1e-6:
raise RuntimeError(
"Hierarchical S2V: first slice position flawed")
if last > 1e-6:
raise RuntimeError(
"Hierarchical S2V: last slice position flawed")
self._registration_method.set_fixed(image)
self._registration_method.run()
transform_sitk = self._registration_method.\
get_registration_transform_sitk()
for j in indices:
slices[j].update_motion_correction(transform_sitk)
# if debug:
# image_after = self._get_stack_subgroup(indices)
# ph.killall_itksnap()
# print(stack.get_filename())
# sitkh.show_stacks(
# [self._reference, image, image_after],
# label=["reference", "before", "after"]
# # segmentation=image,
# )
##
# Gets the bundled stack of selected slices.
# \date 2017-10-16 13:10:32+0100
#
# \param self The object
# \param stack Stack as Stack object
# \param indices Indices of slices as list
#
# \return Stack object holding image of selected slices.
#
def _get_stack_subgroup(self, indices):
stack = self._stacks[0]
# For some reason simple element indexing does not work for sitk
# Problem: indices = [ 8 10 12 14]; but only 3 (!) slices are indexed!
# But this happens quite irregularly!?
# print all_[indices[0]:indices[-1]+1:self._interleave]
# print all_
# image_sitk = stack.sitk[
# :,
# :,
# indices[0]:indices[-1]+self._interleave:self._interleave]
# image_sitk_mask = stack.sitk_mask[
# :,
# :,
# indices[0]:indices[-1]+self._interleave:self._interleave]
# Build image from selected slices
nda = sitk.GetArrayFromImage(stack.sitk)
nda_mask = sitk.GetArrayFromImage(stack.sitk_mask)
image_sitk = sitk.GetImageFromArray(nda[indices, :, :])
image_sitk_mask = sitk.GetImageFromArray(nda_mask[indices, :, :])
# Update stack/slice subgroup position in space according to first
# slice which has undergone same motion as all remaining slices in the
# list
slice_sitk = stack.get_slice(indices[0]).sitk
direction = slice_sitk.GetDirection()
origin = slice_sitk.GetOrigin()
spacing = np.array(slice_sitk.GetSpacing())
# Update slice spacing according to selected interleave
if len(indices) > 1:
spacing[2] *= (indices[1] - indices[0])
# Update information for image and its mask
image_sitk.SetSpacing(spacing)
image_sitk.SetDirection(direction)
image_sitk.SetOrigin(origin)
image_sitk_mask.CopyInformation(image_sitk)
filename = stack.get_filename() + "-"
filename += ("_").join([str(j) for j in indices])
image = st.Stack.from_sitk_image(
image_sitk=image_sitk,
filename=filename,
image_sitk_mask=image_sitk_mask,
extract_slices=False,
slice_thickness=stack.get_slice_thickness(),
)
return image
class ReconstructionRegistrationPipeline(RegistrationPipeline):
__metaclass__ = ABCMeta
##
# Store variables relevant for two-step registration-reconstruction
# pipeline.
# \date 2017-10-16 10:30:39+0100
#
# \param self The object
# \param verbose Verbose output, bool
# \param stacks List of Stack objects
# \param reference Reference as Stack object
# \param registration_method Registration method, e.g.
# SimpleItkRegistration
# \param reconstruction_method Reconstruction method, e.g. TK1
# \param alpha_range Specify regularization parameter
# range, i.e. list [alpha_min,
# alpha_max]
#
def __init__(self,
verbose,
stacks,
reference,
registration_method,
reconstruction_method,
alphas,
viewer,
):
RegistrationPipeline.__init__(
self,
verbose=verbose,
stacks=stacks,
reference=reference,
registration_method=registration_method,
viewer=viewer,
)
self._reconstruction_method = reconstruction_method
self._alphas = alphas
self._reconstructions = [st.Stack.from_stack(
self._reference,
filename="Iter0_" + self._reference.get_filename())]
self._computational_time_reconstruction = ph.get_zero_time()
self._computational_time_registration = ph.get_zero_time()
def get_iterative_reconstructions(self):
return self._reconstructions
def get_computational_time_reconstruction(self):
return self._computational_time_reconstruction
def get_computational_time_registration(self):
return self._computational_time_registration
##
# Class to perform the two-step Slice-to-Volume registration and volumetric
# reconstruction iteratively
# \date 2017-08-08 02:30:43+0100
#
class TwoStepSliceToVolumeRegistrationReconstruction(
ReconstructionRegistrationPipeline):
##
# Store information to perform the two-step S2V reg and recon
# \date 2017-08-08 02:31:24+0100
#
# \param self The object
# \param stacks The stacks
# \param reference The reference
# \param registration_method Registration method, e.g.
# SimpleItkRegistration
# \param reconstruction_method Reconstruction method, e.g.
# TK1
# \param alphas List of alphas
# array
# \param verbose The verbose
# \param cycles Number of cycles, int
# \param outlier_rejection The outlier rejection
# \param threshold_measure The threshold measure
# \param thresholds The threshold range
# \param use_hierarchical_registration The use hierarchical registration
# \param interleave The interleave
# \param viewer The viewer
# \param sigma_sda_mask The sigma sda mask
#
def __init__(self,
stacks,
reference,
registration_method,
reconstruction_method,
alphas,
verbose=1,
cycles=3,
outlier_rejection=False,
threshold_measure="NCC",
thresholds=[0.6, 0.7, 0.8],
use_hierarchical_registration=False,
interleave=3,
viewer=VIEWER,
sigma_sda_mask=1.,
):
# Last volumetric reconstruction step is performed outside
if len(alphas) != cycles - 1:
raise ValueError(
"Elements in alpha list must correspond to cycles-1")
if outlier_rejection and len(thresholds) != cycles:
raise ValueError(
"Elements in outlier rejection threshold list must "
"correspond to the number of cycles")
ReconstructionRegistrationPipeline.__init__(
self,
stacks=stacks,
reference=reference,
registration_method=registration_method,
reconstruction_method=reconstruction_method,
alphas=alphas,
viewer=viewer,
verbose=verbose,
)
self._sigma_sda_mask = sigma_sda_mask
self._cycles = cycles
self._outlier_rejection = outlier_rejection
self._threshold_measure = threshold_measure
self._thresholds = thresholds
self._use_hierarchical_registration = use_hierarchical_registration
self._interleave = interleave
def _run(self):
ph.print_title("Two-step S2V-Registration and SRR Reconstruction")
s2vreg = SliceToVolumeRegistration(
stacks=self._stacks,
reference=self._reference,
registration_method=self._registration_method,
verbose=False,
interleave=self._interleave,
)
reference = self._reference
for cycle in range(0, self._cycles):
if cycle == 0 and self._use_hierarchical_registration:
hs2vreg = HieararchicalSliceSetRegistration(
stacks=self._stacks,
reference=reference,
registration_method=self._registration_method,
interleave=self._interleave,
viewer=self._viewer,
min_slices=1,
verbose=False,
)
hs2vreg.run()
self._computational_time_registration += \
hs2vreg.get_computational_time()
else:
# Slice-to-volume registration step
s2vreg.set_reference(reference)
s2vreg.set_print_prefix("Cycle %d/%d: " %
(cycle + 1, self._cycles))
s2vreg.run()
self._computational_time_registration += \
s2vreg.get_computational_time()
# Reject misregistered slices
if self._outlier_rejection:
ph.print_subtitle("Slice Outlier Rejection (%s < %g)" % (
self._threshold_measure, self._thresholds[cycle]))
outlier_rejector = outre.OutlierRejector(
stacks=self._stacks,
reference=self._reference,
threshold=self._thresholds[cycle],
measure=self._threshold_measure,
verbose=True,
)
outlier_rejector.run()
self._reconstruction_method.set_stacks(
outlier_rejector.get_stacks())
if len(self._stacks) == 0:
raise RuntimeError(
"All slices of all stacks were rejected "
"as outliers. Volumetric reconstruction is aborted.")
# SRR step
if cycle < self._cycles - 1:
# ---------------- Perform Image Reconstruction ---------------
ph.print_subtitle("Volumetric Image Reconstruction")
if isinstance(
self._reconstruction_method,
sda.ScatteredDataApproximation
):
self._reconstruction_method.set_sigma(self._alphas[cycle])
else:
self._reconstruction_method.set_alpha(self._alphas[cycle])
self._reconstruction_method.run()
self._computational_time_reconstruction += \
self._reconstruction_method.get_computational_time()
reference = self._reconstruction_method.get_reconstruction()
# ------------------ Perform Image Mask SDA -------------------
ph.print_subtitle("Volumetric Image Mask Reconstruction")
SDA = sda.ScatteredDataApproximation(
self._stacks,
reference,
sigma=self._sigma_sda_mask,
sda_mask=True,
)
SDA.run()
# reference contains updated mask based on SDA
reference = SDA.get_reconstruction()
# -------------------- Store Reconstruction -------------------
filename = "Iter%d_%s" % (
cycle + 1,
self._reconstruction_method.get_setting_specific_filename()
)
self._reconstructions.insert(0, st.Stack.from_stack(
reference, filename=filename))
if self._verbose:
sitkh.show_stacks(self._reconstructions,
segmentation=self._reference,
viewer=self._viewer)
##
# Class to perform hierarchical slice alignment.
#
# Given an interleave, associated subpackages with a decreasing number of
# slices are jointly registered to reference volume
# \date 2017-10-16 10:17:09+0100
#
class HieararchicalSliceSetRegistration(RegistrationPipeline):
##
# Store relevant variables
# \date 2017-10-16 10:18:58+0100
#
# \param self The object
# \param stacks List of stacks to be registered
# \param reference Reference image as Stack object.
# \param registration_method method, e.g. SimpleItkRegistration
# \param interleave Interleave of scans, integer
# \param min_slices The minimum slices
# \param verbose The verbose
# \param viewer The viewer
#
def __init__(self,
stacks,
reference,
registration_method,
interleave,
min_slices=1,
verbose=1,
viewer=VIEWER,
):
RegistrationPipeline.__init__(
self,
stacks=stacks,
reference=reference,
registration_method=registration_method,
verbose=verbose,
viewer=VIEWER,
)
self._interleave = interleave
self._min_slices = min_slices
def _run(self, debug=0):
ph.print_title(
"Hierarchical SliceSet2V-Registration")
N_stacks = len(self._stacks)
self._registration_method.set_moving(self._reference)
for i_stack, stack in enumerate(self._stacks):
n_slices = stack.get_number_of_slices()
for i in range(self._interleave):
package = list(np.arange(i, n_slices, self._interleave))
if len(package) / 2 >= self._min_slices:
indices_splits = self._recursive_split(
package, [], self._min_slices)
else:
indices_splits = [package]
prefix = "Hierarchical S2V-Reg: " \
"Stack %d/%d (%s) -- Interleave %d/%d --" % (
i_stack + 1, len(self._stacks), stack.get_filename(),
i + 1, self._interleave,
)
if debug:
ph.print_subtitle(
"%s %d splits: %s" % (
prefix, len(indices_splits), indices_splits),
)
ss2vreg = SliceSetToVolumeRegistration(
print_prefix=prefix,
stack=stack,
reference=self._reference,
registration_method=self._registration_method,
slice_set_indices=indices_splits,
verbose=self._verbose,
)
ss2vreg.run()
##
# Split list of arrays into halfs.
# \date 2017-10-16 13:16:50+0100
#
# \param self The object
# \param indices The indices
# \param indices_split The indices split
# \param N_min Minimum number of elements at which no further
# split shall be performed
#
# \return List of arrays holding slice indices.
#
def _recursive_split(self, indices, indices_split, N_min):
mid = int(len(indices) / 2)
a = indices[0:mid]
b = indices[mid:]
indices_split.append(a)
indices_split.append(b)
if len(a) / 2 >= N_min:
self._recursive_split(a, indices_split, N_min)
if len(b) / 2 >= N_min:
self._recursive_split(b, indices_split, N_min)
return indices_split
##
# Class to perform multi-component reconstruction
#
# Each stack is individually reconstructed at a given reconstruction space
# \date 2017-08-08 02:34:40+0100
#
class MultiComponentReconstruction(Pipeline):
##
# Store information relevant for multi-component reconstruction
# \date 2017-08-08 02:37:40+0100
#
# \param self The object
# \param stacks The stacks
# \param reconstruction_method The reconstruction method
# \param suffix Suffix added to filenames of each
# individual stack, string
# \param verbose The verbose
#
def __init__(self,
stacks,
reconstruction_method,
suffix="_recon",
verbose=0,
viewer=VIEWER,
):
Pipeline.__init__(self, stacks=stacks, verbose=verbose, viewer=viewer)
self._reconstruction_method = reconstruction_method
self._reconstructions = None
self._suffix = suffix
def set_reconstruction_method(self, reconstruction_method):
self._reconstruction_method = reconstruction_method
def get_reconstruction_method(self):
return self._reconstruction_method
def set_suffix(self, suffix):
self._suffix = suffix
def get_suffix(self):
return self._suffix
def get_reconstructions(self):
return [st.Stack.from_stack(stack) for stack in self._reconstructions]
def _run(self):
ph.print_title("Multi-Component Reconstruction")
self._reconstructions = [None] * len(self._stacks)
for i in range(0, len(self._stacks)):
ph.print_subtitle("Multi-Component Reconstruction -- "
"Stack %d/%d" % (i + 1, len(self._stacks)))
stack = self._stacks[i]
self._reconstruction_method.set_stacks([stack])
self._reconstruction_method.run()
self._reconstructions[i] = st.Stack.from_stack(
self._reconstruction_method.get_reconstruction())
self._reconstructions[i].set_filename(
stack.get_filename() + self._suffix)
================================================
FILE: niftymic/validation/__init__.py
================================================
================================================
FILE: niftymic/validation/evaluate_image_similarity.py
================================================
##
# \file evaluate_image_similarity.py
# \brief Evaluate similarity to a reference of one or more images
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date Feb 2018
#
import os
import numpy as np
import pandas as pd
import SimpleITK as sitk
import nsol.observer as obs
from nsol.similarity_measures import SimilarityMeasures as \
SimilarityMeasures
import niftymic.base.stack as st
import niftymic.base.data_reader as dr
import niftymic.registration.niftyreg as regniftyreg
from niftymic.utilities.input_arparser import InputArgparser
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
def main():
# Set print options
np.set_printoptions(precision=3)
pd.set_option('display.width', 1000)
input_parser = InputArgparser(
description=".",
)
input_parser.add_filenames(required=True)
input_parser.add_reference(required=True)
input_parser.add_reference_mask()
input_parser.add_dir_output(required=False)
input_parser.add_measures(
default=["PSNR", "RMSE", "MAE", "SSIM", "NCC", "NMI"])
input_parser.add_verbose(default=0)
args = input_parser.parse_args()
input_parser.print_arguments(args)
ph.print_title("Image similarity")
data_reader = dr.MultipleImagesReader(args.filenames)
data_reader.read_data()
stacks = data_reader.get_data()
reference = st.Stack.from_filename(args.reference, args.reference_mask)
for stack in stacks:
try:
stack.sitk - reference.sitk
except RuntimeError as e:
raise IOError(
"All provided images must be at the same image space")
x_ref = sitk.GetArrayFromImage(reference.sitk)
if args.reference_mask is None:
indices = np.where(x_ref != np.inf)
else:
x_ref_mask = sitk.GetArrayFromImage(reference.sitk_mask)
indices = np.where(x_ref_mask > 0)
measures_dic = {
m: lambda x, m=m:
SimilarityMeasures.similarity_measures[m](
x[indices], x_ref[indices])
# SimilarityMeasures.similarity_measures[m](x, x_ref)
for m in args.measures
}
observer = obs.Observer()
observer.set_measures(measures_dic)
for stack in stacks:
nda = sitk.GetArrayFromImage(stack.sitk)
observer.add_x(nda)
if args.verbose:
stacks_comparison = [s for s in stacks]
stacks_comparison.insert(0, reference)
sitkh.show_stacks(
stacks_comparison,
segmentation=reference,
)
observer.compute_measures()
measures = observer.get_measures()
# Store information in array
error = np.zeros((len(stacks), len(measures)))
cols = measures
rows = []
for i_stack, stack in enumerate(stacks):
error[i_stack, :] = np.array([measures[m][i_stack] for m in measures])
rows.append(stack.get_filename())
header = "# Ref: %s, Ref-Mask: %d, %s \n" % (
reference.get_filename(),
args.reference_mask is None,
ph.get_time_stamp(),
)
header += "# %s\n" % ("\t").join(measures)
path_to_file_filenames = os.path.join(
args.dir_output, "filenames.txt")
path_to_file_similarities = os.path.join(
args.dir_output, "similarities.txt")
# Write to files
ph.write_to_file(path_to_file_similarities, header)
ph.write_array_to_file(
path_to_file_similarities, error, verbose=False)
text = header
text += "%s\n" %"\n".join(rows)
ph.write_to_file(path_to_file_filenames, text)
# Print to screen
ph.print_subtitle("Computed Similarities")
df = pd.DataFrame(error, rows, cols)
print(df)
return 0
if __name__ == '__main__':
main()
================================================
FILE: niftymic/validation/evaluate_simulated_stack_similarity.py
================================================
##
# \file evaluate_simulated_stack_similarity.py
# \brief Script to evaluate the similarity of simulated stack from
# obtained reconstruction against the original stack.
#
# This function takes the result of simulate_stacks_from_reconstruction.py as
# input.
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date November 2017
#
# Import libraries
import SimpleITK as sitk
import numpy as np
import os
import pysitk.python_helper as ph
from nsol.similarity_measures import SimilarityMeasures as \
SimilarityMeasures
import niftymic.base.data_reader as dr
from niftymic.utilities.input_arparser import InputArgparser
def main():
# Read input
input_parser = InputArgparser(
description="Script to evaluate the similarity of simulated stack "
"from obtained reconstruction against the original stack. "
"This function takes the result of "
"simulate_stacks_from_reconstruction.py as input.",
)
input_parser.add_filenames(required=True)
input_parser.add_filenames_masks()
input_parser.add_dir_output(required=True)
input_parser.add_suffix_mask(default="_mask")
input_parser.add_measures(default=["NCC", "SSIM"])
input_parser.add_option(
option_string="--prefix-simulated",
type=str,
help="Specify the prefix of the simulated stacks to distinguish them "
"from the original data.",
default="Simulated_",
)
input_parser.add_option(
option_string="--dir-input-simulated",
type=str,
help="Specify the directory where the simulated stacks are. "
"If not given, it is assumed that they are in the same directory "
"as the original ones.",
default=None
)
input_parser.add_slice_thicknesses(default=None)
args = input_parser.parse_args()
input_parser.print_arguments(args)
# --------------------------------Read Data--------------------------------
ph.print_title("Read Data")
# Read original data
filenames_original = args.filenames
data_reader = dr.MultipleImagesReader(
file_paths=filenames_original,
file_paths_masks=args.filenames_masks,
suffix_mask=args.suffix_mask,
stacks_slice_thicknesses=args.slice_thicknesses,
)
data_reader.read_data()
stacks_original = data_reader.get_data()
# Read data simulated from obtained reconstruction
if args.dir_input_simulated is None:
dir_input_simulated = os.path.dirname(filenames_original[0])
else:
dir_input_simulated = args.dir_input_simulated
filenames_simulated = [
os.path.join("%s", "%s%s") %
(dir_input_simulated, args.prefix_simulated, os.path.basename(f))
for f in filenames_original
]
data_reader = dr.MultipleImagesReader(
filenames_simulated, suffix_mask=args.suffix_mask)
data_reader.read_data()
stacks_simulated = data_reader.get_data()
for i in range(len(stacks_original)):
try:
stacks_original[i].sitk - stacks_simulated[i].sitk
except:
raise IOError("Images '%s' and '%s' do not occupy the same space!"
% (filenames_original[i], filenames_simulated[i]))
similarity_measures = {
m: SimilarityMeasures.similarity_measures[m] for m in args.measures
}
similarities = np.zeros(len(args.measures))
for i in range(len(stacks_original)):
nda_3D_original = sitk.GetArrayFromImage(stacks_original[i].sitk)
nda_3D_simulated = sitk.GetArrayFromImage(stacks_simulated[i].sitk)
nda_3D_mask = sitk.GetArrayFromImage(stacks_original[i].sitk_mask)
path_to_file = os.path.join(
args.dir_output, "Similarity_%s.txt" %
stacks_original[i].get_filename())
text = "# Similarity: %s vs %s (%s)." % (
os.path.basename(filenames_original[i]),
os.path.basename(filenames_simulated[i]), ph.get_time_stamp())
text += "\n#\t" + ("\t").join(args.measures)
text += "\n"
ph.write_to_file(path_to_file, text, "w")
for k in range(nda_3D_original.shape[0]):
x_2D_original = nda_3D_original[k, :, :]
x_2D_simulated = nda_3D_simulated[k, :, :]
# zero slice, i.e. rejected during motion correction
if np.abs(x_2D_simulated).sum() < 1e-6:
x_2D_simulated[:] = np.nan
x_2D_mask = nda_3D_mask[k, :, :]
indices = np.where(x_2D_mask > 0)
for m, measure in enumerate(args.measures):
if len(indices[0]) > 0:
similarities[m] = similarity_measures[measure](
x_2D_original[indices], x_2D_simulated[indices])
else:
similarities[m] = np.nan
ph.write_array_to_file(path_to_file, similarities.reshape(1, -1))
return 0
if __name__ == '__main__':
main()
================================================
FILE: niftymic/validation/evaluate_slice_residual_similarity.py
================================================
##
# \file evaluate_slice_residual_similarity.py
# \brief Evaluate similarity to a reference of one or more images
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date Feb 2018
#
import os
import numpy as np
import pandas as pd
import SimpleITK as sitk
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
import niftymic.base.stack as st
import niftymic.base.data_reader as dr
import niftymic.utilities.intensity_correction as ic
import niftymic.registration.niftyreg as regniftyreg
import niftymic.validation.residual_evaluator as res_ev
from niftymic.utilities.input_arparser import InputArgparser
def main():
time_start = ph.start_timing()
# Set print options
np.set_printoptions(precision=3)
pd.set_option('display.width', 1000)
input_parser = InputArgparser(
description=".",
)
input_parser.add_filenames()
input_parser.add_filenames_masks()
input_parser.add_dir_input_mc()
input_parser.add_suffix_mask(default="_mask")
input_parser.add_reference(required=True)
input_parser.add_reference_mask()
input_parser.add_dir_output(required=False)
input_parser.add_log_config(default=1)
input_parser.add_measures(
default=["PSNR", "MAE", "RMSE", "SSIM", "NCC", "NMI"])
input_parser.add_verbose(default=0)
input_parser.add_target_stack(default=None)
input_parser.add_intensity_correction(default=1)
input_parser.add_slice_thicknesses(default=None)
input_parser.add_option(
option_string="--use-reference-mask", type=int, default=1)
input_parser.add_option(
option_string="--use-slice-masks", type=int, default=1)
args = input_parser.parse_args()
input_parser.print_arguments(args)
if args.log_config:
input_parser.log_config(os.path.abspath(__file__))
# --------------------------------Read Data--------------------------------
ph.print_title("Read Data")
data_reader = dr.MultipleImagesReader(
file_paths=args.filenames,
file_paths_masks=args.filenames_masks,
suffix_mask=args.suffix_mask,
dir_motion_correction=args.dir_input_mc,
stacks_slice_thicknesses=args.slice_thicknesses,
)
data_reader.read_data()
stacks = data_reader.get_data()
ph.print_info("%d input stacks read for further processing" % len(stacks))
# Specify target stack for intensity correction and reconstruction space
if args.target_stack is None:
target_stack_index = 0
else:
filenames = ["%s.nii.gz" % s.get_filename() for s in stacks]
filename_target_stack = os.path.basename(args.target_stack)
try:
target_stack_index = filenames.index(filename_target_stack)
except ValueError as e:
raise ValueError(
"--target-stack must correspond to an image as provided by "
"--filenames")
# ---------------------------Intensity Correction--------------------------
if args.intensity_correction:
ph.print_title("Intensity Correction")
intensity_corrector = ic.IntensityCorrection()
intensity_corrector.use_individual_slice_correction(False)
intensity_corrector.use_stack_mask(True)
intensity_corrector.use_reference_mask(True)
intensity_corrector.use_verbose(False)
for i, stack in enumerate(stacks):
if i == target_stack_index:
ph.print_info("Stack %d (%s): Reference image. Skipped." % (
i + 1, stack.get_filename()))
continue
else:
ph.print_info("Stack %d (%s): Intensity Correction ... " % (
i + 1, stack.get_filename()), newline=False)
intensity_corrector.set_stack(stack)
intensity_corrector.set_reference(
stacks[target_stack_index].get_resampled_stack(
resampling_grid=stack.sitk,
interpolator="NearestNeighbor",
))
intensity_corrector.run_linear_intensity_correction()
stacks[i] = intensity_corrector.get_intensity_corrected_stack()
print("done (c1 = %g) " %
intensity_corrector.get_intensity_correction_coefficients())
# ----------------------- Slice Residual Similarity -----------------------
reference = st.Stack.from_filename(args.reference, args.reference_mask)
ph.print_title("Slice Residual Similarity")
residual_evaluator = res_ev.ResidualEvaluator(
stacks=stacks,
reference=reference,
measures=args.measures,
use_reference_mask=args.use_reference_mask,
use_slice_masks=args.use_slice_masks,
)
residual_evaluator.compute_slice_projections()
residual_evaluator.evaluate_slice_similarities()
residual_evaluator.write_slice_similarities(args.dir_output)
elapsed_time = ph.stop_timing(time_start)
ph.print_title("Summary")
print("Computational Time for Slice Residual Evaluation: %s" %
(elapsed_time))
return 0
if __name__ == '__main__':
main()
================================================
FILE: niftymic/validation/export_side_by_side_simulated_vs_original_slice_comparison.py
================================================
##
# \file export_side_by_side_simulated_vs_original_slice_comparison.py
# \brief Script to generate a pdf holding all side-by-side comparisons.
#
# This function takes the result of simulate_stacks_from_reconstruction.py as
# input. The script relies on ImageMagick.
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date November 2017
#
# Import libraries
import SimpleITK as sitk
import numpy as np
import natsort
import os
import re
import pysitk.python_helper as ph
from nsol.similarity_measures import SimilarityMeasures as \
SimilarityMeasures
import niftymic.base.data_reader as dr
from niftymic.utilities.input_arparser import InputArgparser
from niftymic.definitions import DIR_TMP
##
# Export a side-by-side comparison to a (pdf) file
# \date 2017-11-28 23:28:12+0000
#
# \param nda_original numpy data 3D array of original data
# \param nda_projected numpy data 3D array of projected/simulated data
# \param path_to_file path to file, string
# \param resize factor to resize images (otherwise they
# might be very small depending on the FOV)
# \param extension extension of images produced for tmp results.
#
def export_comparison_to_file(nda_original,
nda_projected,
path_to_file,
resize,
extension="png"):
dir_tmp = os.path.join(DIR_TMP, "ImageMagick")
ph.clear_directory(dir_tmp, verbose=False)
for k in range(nda_original.shape[0]):
ctr = k + 1
# Export as individual image side-by-side
_export_image_side_by_side(
nda_left=nda_original[k, :, :],
nda_right=nda_projected[k, :, :],
label_left="original",
label_right="projected",
path_to_file=os.path.join(
dir_tmp, "%03d.%s" % (ctr, extension)),
ctr=ctr,
resize=resize,
extension=extension,
)
# Combine all side-by-side images to single pdf
_export_pdf_from_side_by_side_images(
dir_tmp, path_to_file, extension=extension)
ph.print_info("Side-by-side comparison exported to '%s'" % path_to_file)
# Delete tmp directory
ph.delete_directory(dir_tmp, verbose=False)
##
# Export a single side-by-side comparison of two images
# \date 2017-11-28 23:30:23+0000
#
def _export_image_side_by_side(
nda_left,
nda_right,
label_left,
label_right,
path_to_file,
ctr,
resize,
extension,
border=10,
background="black",
fill_ctr="orange",
fill_label="white",
pointsize=12,
):
dir_output = os.path.join(DIR_TMP, "ImageMagick", "side-by-side")
ph.clear_directory(dir_output, verbose=False)
path_to_left = os.path.join(dir_output, "left.%s" % extension)
path_to_right = os.path.join(dir_output, "right.%s" % extension)
nda_left = np.round(np.array(nda_left)).astype(np.uint8)
nda_right = np.round(np.array(nda_right)).astype(np.uint8)
ph.write_image(nda_left, path_to_left, verbose=False)
ph.write_image(nda_right, path_to_right, verbose=False)
_resize_image(path_to_left, resize=resize)
_resize_image(path_to_right, resize=resize)
cmd_args = []
cmd_args.append("-geometry +%d+%d" % (border, border))
cmd_args.append("-background %s" % background)
cmd_args.append("-pointsize %s" % pointsize)
cmd_args.append("-fill %s" % fill_ctr)
cmd_args.append("-gravity SouthWest -draw \"text 0,0 '%d'\"" % ctr)
cmd_args.append("-fill %s" % fill_label)
cmd_args.append("-label '%s' %s" % (label_left, path_to_left))
cmd_args.append("-label '%s' %s" % (label_right, path_to_right))
cmd_args.append("%s" % path_to_file)
cmd = "montage %s" % (" ").join(cmd_args)
ph.execute_command(cmd, verbose=False)
##
# Resize image
# \date 2017-11-28 23:31:07+0000
#
def _resize_image(path_to_file, resize):
factor = resize * 100
cmd_args = []
cmd_args.append("%s" % path_to_file)
cmd_args.append("-resize %dx%d%%\\!" % (factor, factor))
cmd = "convert %s %s" % ((" ").join(cmd_args), path_to_file)
ph.execute_command(cmd, verbose=False)
##
# Create single pdf from multiple side-by-side (png) images
# \date 2017-11-28 23:33:46+0000
#
# \param directory Path to directory with side-by-side png images
# \param path_to_file Path to combined pdf result
# \param extension The extension
#
def _export_pdf_from_side_by_side_images(directory, path_to_file, extension):
# Read all sidy-by-side (png) images in directory
pattern = "[a-zA-Z0-9_]+[.]%s" % extension
p = re.compile(pattern)
files = [os.path.join(directory, f)
for f in os.listdir(directory) if p.match(f)]
# Convert consecutive sequence of images into single pdf
files = natsort.natsorted(files, key=lambda y: y.lower())
cmd = "convert %s %s" % ((" ").join(files), path_to_file)
ph.execute_command(cmd, verbose=False)
def main():
input_parser = InputArgparser(
description="Script to export a side-by-side comparison of originally "
"acquired and simulated/projected slice given the estimated "
"volumetric reconstruction."
"This function takes the result of "
"simulate_stacks_from_reconstruction.py as input.",
)
input_parser.add_filenames(required=True)
input_parser.add_dir_output(required=True)
input_parser.add_option(
option_string="--prefix-simulated",
type=str,
help="Specify the prefix of the simulated stacks to distinguish them "
"from the original data.",
default="Simulated_",
)
input_parser.add_option(
option_string="--dir-input-simulated",
type=str,
help="Specify the directory where the simulated stacks are. "
"If not given, it is assumed that they are in the same directory "
"as the original ones.",
default=None
)
input_parser.add_option(
option_string="--resize",
type=float,
help="Factor to resize images (otherwise they might be very small "
"depending on the FOV)",
default=3)
args = input_parser.parse_args()
input_parser.print_arguments(args)
# --------------------------------Read Data--------------------------------
ph.print_title("Read Data")
# Read original data
filenames_original = args.filenames
data_reader = dr.MultipleImagesReader(filenames_original)
data_reader.read_data()
stacks_original = data_reader.get_data()
# Read data simulated from obtained reconstruction
if args.dir_input_simulated is None:
dir_input_simulated = os.path.dirname(filenames_original[0])
else:
dir_input_simulated = args.dir_input_simulated
filenames_simulated = [
os.path.join("%s", "%s%s") %
(dir_input_simulated, args.prefix_simulated, os.path.basename(f))
for f in filenames_original
]
data_reader = dr.MultipleImagesReader(filenames_simulated)
data_reader.read_data()
stacks_simulated = data_reader.get_data()
ph.create_directory(args.dir_output)
for i in range(len(stacks_original)):
try:
stacks_original[i].sitk - stacks_simulated[i].sitk
except:
raise IOError("Images '%s' and '%s' do not occupy the same space!"
% (filenames_original[i], filenames_simulated[i]))
# ---------------------Create side-by-side comparisons---------------------
ph.print_title("Create side-by-side comparisons")
intensity_max = 255
intensity_min = 0
for i in range(len(stacks_original)):
ph.print_subtitle("Stack %d/%d" % (i + 1, len(stacks_original)))
nda_3D_original = sitk.GetArrayFromImage(stacks_original[i].sitk)
nda_3D_simulated = sitk.GetArrayFromImage(stacks_simulated[i].sitk)
# Scale uniformly between 0 and 255 according to the simulated stack
# for export to png
scale = np.max(nda_3D_simulated)
nda_3D_original = intensity_max * nda_3D_original / scale
nda_3D_simulated = intensity_max * nda_3D_simulated / scale
nda_3D_simulated = np.clip(
nda_3D_simulated, intensity_min, intensity_max)
nda_3D_original = np.clip(
nda_3D_original, intensity_min, intensity_max)
filename = stacks_original[i].get_filename()
path_to_file = os.path.join(args.dir_output, "%s.pdf" % filename)
# Export side-by-side comparison of each stack to a pdf file
export_comparison_to_file(
nda_3D_original, nda_3D_simulated, path_to_file,
resize=args.resize)
if __name__ == '__main__':
main()
================================================
FILE: niftymic/validation/image_similarity_evaluator.py
================================================
##
# \file image_similarity_evaluator.py
# \brief Class to evaluate image similarity between stacks and a reference
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date February 2018
#
# Import libraries
import os
import re
import numpy as np
import SimpleITK as sitk
import nsol.observer as obs
from nsol.similarity_measures import SimilarityMeasures as \
SimilarityMeasures
import pysitk.python_helper as ph
import niftymic.reconstruction.linear_operators as lin_op
import niftymic.base.exceptions as exceptions
##
# Class to evaluate image similarity between stacks and a reference
# \date 2018-02-08 16:16:08+0000
#
class ImageSimilarityEvaluator(object):
##
# { constructor_description }
# \date 2018-02-08 14:13:19+0000
#
# \param self The object
# \param stacks List of Stack objects
# \param reference Reference as Stack object
# \param use_reference_mask The use reference mask
# \param measures Similarity measures as given in
# nsol.similarity_measures, list of strings
# \param verbose The verbose
#
def __init__(
self,
stacks=None,
reference=None,
use_reference_mask=True,
measures=["NCC", "NMI", "PSNR", "SSIM", "RMSE" ,"MAE"],
verbose=True,
):
self._stacks = stacks
self._reference = reference
self._measures = measures
self._use_reference_mask = use_reference_mask
self._verbose = verbose
self._similarities = None
self._filename_filenames = "filenames.txt"
self._filename_similarities = "similarities.txt"
##
# Sets the stacks.
# \date 2018-02-08 14:13:27+0000
#
# \param self The object
# \param stacks List of Stack objects
#
def set_stacks(self, stacks):
self._stacks = stacks
##
# Sets the reference from which the slices shall be simulated/projected.
# \date 2018-01-19 17:26:14+0000
#
# \param self The object
# \param reference The reference
#
def set_reference(self, reference):
self._reference = reference
##
# Gets the computed similarities.
# \date 2018-02-08 14:36:15+0000
#
# \param self The object
#
# \return The similarities as dictionary. E.g. { "NCC": np.array()}
#
def get_similarities(self):
return self._similarities
def get_measures(self):
return self._measures
##
# Calculates the similarities. Outcome can be fetched using
# 'get_similarities'
# \date 2018-02-08 16:16:41+0000
#
# \param self The object
#
# \post self._similarities updated.
#
def compute_similarities(self):
for stack in self._stacks:
try:
stack.sitk - self._reference.sitk
except RuntimeError as e:
raise IOError(
"All provided images must be at the same image space")
x_ref = sitk.GetArrayFromImage(self._reference.sitk)
x_ref_mask = np.ones_like(x_ref)
if self._use_reference_mask:
x_ref_mask *= sitk.GetArrayFromImage(self._reference.sitk_mask)
indices = np.where(x_ref_mask > 0)
if len(indices[0]) == 0:
raise RuntimeError(
"Support to evaluate similarity measures is zero")
# Define similarity measures as dic
measures_dic = {
m: lambda x, m=m:
SimilarityMeasures.similarity_measures[m](
x[indices], x_ref[indices])
# SimilarityMeasures.similarity_measures[m](x, x_ref)
for m in self._measures
}
# Compute similarities
observer = obs.Observer()
observer.set_measures(measures_dic)
for stack in self._stacks:
nda = sitk.GetArrayFromImage(stack.sitk)
observer.add_x(nda)
observer.compute_measures()
self._similarities = observer.get_measures()
# Add filenames to dictionary
image_names = [s.get_filename() for s in self._stacks]
self._similarities["filenames"] = image_names
##
# Writes the evaluated similarities to two files; one containing the
# similarity information, the other the filename information.
# \date 2018-02-08 14:58:29+0000
#
# \param self The object
# \param directory The directory
#
def write_similarities(self, directory):
# Store information in array
similarities_nda = np.zeros((len(self._stacks), len(self._measures)))
filenames = []
for i_stack, stack in enumerate(self._stacks):
similarities_nda[i_stack, :] = np.array(
[self._similarities[m][i_stack] for m in self._measures])
filenames.append(stack.get_filename())
# Build header of files
header = "# Ref: %s, Ref-Mask: %d, %s \n" % (
self._reference.get_filename(),
self._use_reference_mask,
ph.get_time_stamp(),
)
header += "# %s\n" % ("\t").join(self._measures)
# Get filename paths
path_to_file_filenames, path_to_file_similarities = self._get_filename_paths(
directory)
# Write similarities
ph.write_to_file(path_to_file_similarities, header)
ph.write_array_to_file(
path_to_file_similarities, similarities_nda, verbose=self._verbose)
# Write stack filenames
text = header
text += "%s\n" % "\n".join(filenames)
ph.write_to_file(path_to_file_filenames, text, verbose=self._verbose)
##
# Reads similarities.
# \date 2018-02-08 15:32:04+0000
#
# \param self The object
# \param directory The directory
#
def read_similarities(self, directory):
if not ph.directory_exists(directory):
raise IOError("Directory '%s' does not exist." % directory)
# Get filename paths
path_to_file_filenames, path_to_file_similarities = self._get_filename_paths(
directory)
for f in [path_to_file_filenames, path_to_file_similarities]:
if not ph.file_exists(path_to_file_filenames):
raise IOError("File '%s' does not exist" % f)
lines = ph.read_file_line_by_line(path_to_file_filenames)
# Get image filenames
image_names = [re.sub("\n", "", f) for f in lines[2:]]
# Get computed measures
measures = lines[1]
measures = re.sub("# ", "", measures)
measures = re.sub("\n", "", measures)
self._measures = measures.split("\t")
# Get computed similarities
similarities_nda = np.loadtxt(path_to_file_similarities, skiprows=2)
# Reconstruct similarity dictionary
self._similarities = {}
self._similarities["filenames"] = image_names
for i_m, m in enumerate(self._measures):
self._similarities[m] = similarities_nda[:, i_m]
def _get_filename_paths(self, directory):
# Define filename paths
path_to_file_filenames = os.path.join(
directory, self._filename_filenames)
path_to_file_similarities = os.path.join(
directory, self._filename_similarities)
return path_to_file_filenames, path_to_file_similarities
================================================
FILE: niftymic/validation/motion_evaluator.py
================================================
##
# \file motion_evaluator.py
# \brief Class to evaluate computed motions
#
# Should help to assess the registration accuracy.
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date January 2018
#
# Import libraries
import os
import re
import numpy as np
import pandas as pd
import SimpleITK as sitk
import matplotlib.pyplot as plt
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
import niftymic.base.exceptions as exceptions
##
# Class to evaluate computed motions
# \date 2018-01-25 22:53:37+0000
#
class MotionEvaluator(object):
def __init__(self, transforms_sitk):
self._transforms_sitk = transforms_sitk
self._transform_params = None
self._scale = {
# convert radiant to degree for angles
6: np.array([180. / np.pi, 180. / np.pi, 180. / np.pi, 1, 1, 1]),
}
self._labels_long = {
6: ["angle_x [deg]",
"angle_y [deg]",
"angle_z [deg]",
"t_x [mm]",
"t_y [mm]",
"t_z [mm]"],
}
self._labels_short = {
6: ["angle_x",
"angle_y",
"angle_z",
"t_x",
"t_y",
"t_z"],
}
def run(self):
# # Eliminate center information
# if self._transforms_sitk[0].GetName() in \
# ["Euler2DTransform", "Euler3DTransform"]:
# identity = eval("sitk.Euler%dDTransform()"
# transforms_sitk = [
# sitkh.get_composite_sitk_euler_transform()
# ]
# Create (#transforms x DOF) numpy array
self._transform_params = np.zeros((
len(self._transforms_sitk),
len(self._transforms_sitk[0].GetParameters())
))
for j in range(self._transform_params.shape[0]):
self._transform_params[j, :] = \
self._transforms_sitk[j].GetParameters()
def display(self, title=None, dir_output=None):
pd.set_option('display.width', 1000)
N_trafos, dof = self._transform_params.shape
if dof == 6:
params = self._get_scaled_params(self._transform_params)
# add mean value
params = np.concatenate(
(params,
np.mean(params, axis=0).reshape(1, -1),
np.std(params, axis=0).reshape(1, -1)
))
cols = self._labels_long[dof]
else:
params = self._transform_params
cols = ["a%d" % (d + 1) for d in range(0, dof)]
rows = ["Trafo %d" % (d + 1) for d in range(0, N_trafos)]
rows.append("Mean")
rows.append("Std")
df = pd.DataFrame(params, rows, cols)
print(df)
if dir_output is not None:
title = self._replace_string(title)
filename = "%s.csv" % title
ph.create_directory(dir_output)
df.to_csv(os.path.join(dir_output, filename))
##
# Plot figure to show parameter distribution.
# Only works for 3D rigid transforms for now.
# \date 2018-01-25 23:30:45+0000
#
# \param self The object
#
def show(self, title=None, dir_output=None):
params = self._get_scaled_params(self._transform_params)
N_trafos, dof = self._transform_params.shape
fig = plt.figure(title)
fig.clf()
x = range(1, N_trafos+1)
ax = plt.subplot(2, 1, 1)
for i_param in range(0, 3):
ax.plot(
x, params[:, i_param],
marker=ph.MARKERS[i_param],
color=ph.COLORS_TABLEAU20[i_param*2],
linestyle=":",
label=self._labels_short[dof][i_param],
markerfacecolor="w",
)
ax.set_xticks(x)
plt.ylabel('Rotation [deg]')
plt.legend(loc="best")
ax = plt.subplot(2, 1, 2)
for i_param in range(0, 3):
ax.plot(
x, params[:, 3+i_param],
marker=ph.MARKERS[i_param],
color=ph.COLORS_TABLEAU20[i_param*2],
linestyle=":",
label=self._labels_short[dof][3+i_param],
markerfacecolor="w",
)
ax.set_xticks(x)
plt.xlabel('Slice')
plt.ylabel('Translation [mm]')
plt.legend(loc="best")
plt.suptitle(title)
try:
# Open windows (and also save them) in full screen
manager = plt.get_current_fig_manager()
manager.full_screen_toggle()
except:
pass
plt.show(block=False)
if dir_output is not None:
title = self._replace_string(title)
filename = "%s.pdf" % title
ph.save_fig(fig, dir_output, filename)
def _get_scaled_params(self, transform_params):
dof = self._transform_params.shape[1]
return self._transform_params * self._scale[dof]
def _replace_string(self, string):
string = re.sub(" ", "_", string)
string = re.sub(":", "", string)
string = re.sub("/", "_", string)
return string
================================================
FILE: niftymic/validation/motion_simulator.py
================================================
##
# \file MotionSimulator.py
# \brief Abstract class to define interface for motion simulator
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date September 2017
#
# Import libraries
import os
import numpy as np
import SimpleITK as sitk
from abc import ABCMeta, abstractmethod
import pysitk.python_helper as ph
class MotionSimulator(object):
__metaclass__ = ABCMeta
def __init__(self, dimension, verbose):
self._transforms_sitk = None
self._dimension = dimension
self._verbose = verbose
@abstractmethod
def simulate_motion(self):
pass
def get_transforms_sitk(self):
# Return copy of transforms
transforms_sitk = [
eval("sitk." + self._transforms_sitk[0].GetName() +
"(t)") for t in self._transforms_sitk
]
return transforms_sitk
def write_transforms_sitk(self,
directory,
prefix_filename="Euler3dTransform_"):
ph.create_directory(directory)
for i, transform in enumerate(self._transforms_sitk):
path_to_file = os.path.join(
directory, "%s%d.tfm" % (prefix_filename, i))
sitk.WriteTransform(transform, path_to_file)
if self._verbose:
ph.print_info("Transform written to %s" % path_to_file)
class RigidMotionSimulator(MotionSimulator):
__metaclass__ = ABCMeta
def __init__(self, dimension, verbose):
MotionSimulator.__init__(self, dimension=dimension, verbose=verbose)
self._transform0_sitk = eval("sitk.Euler%dDTransform" % (dimension))
class RandomRigidMotionSimulator(RigidMotionSimulator):
def __init__(self,
dimension,
angle_max_deg=5,
translation_max=5,
verbose=False):
RigidMotionSimulator.__init__(
self, dimension=dimension, verbose=verbose)
self._angle_max_deg = angle_max_deg
self._translation_max = translation_max
def simulate_motion(self, seed=None, simulations=1):
np.random.seed(seed)
# Create random translation \f$\in\f$ [\p -translation_max, \p
# translation_max]
translation = 2. * np.random.rand(simulations, self._dimension) * \
self._translation_max - self._translation_max
# Create random rotation \f$\in\f$ [\p -angle_deg_max, \p
# angle_deg_max]
angle_rad = \
(2. * np.random.rand(simulations, self._dimension) * self._angle_max_deg -
self._angle_max_deg) * np.pi / 180.
# Set resulting rigid motion transform
self._transforms_sitk = [None] * simulations
for i in range(simulations):
self._transforms_sitk[i] = self._transform0_sitk()
parameters = list(angle_rad[i, :])
parameters.extend(translation[i, :])
self._transforms_sitk[i].SetParameters(parameters)
================================================
FILE: niftymic/validation/residual_evaluator.py
================================================
##
# \file residual_evaluator.py
# \brief Class to evaluate computed residuals between a
# simulated/projected and original/acquired slices of stacks
#
# Should help to assess the registration accuracy.
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date January 2018
#
# Import libraries
import os
import re
import numpy as np
import SimpleITK as sitk
import matplotlib.pyplot as plt
from nsol.similarity_measures import SimilarityMeasures as \
SimilarityMeasures
import pysitk.python_helper as ph
import pysitk.statistics_helper as sh
import niftymic.reconstruction.linear_operators as lin_op
import niftymic.base.exceptions as exceptions
##
# Class to evaluate computed residuals between a simulated/projected and
# original/acquired slices of stacks
#
# \date 2018-01-19 17:24:35+0000
#
class ResidualEvaluator(object):
##
# { constructor_description }
# \date 2018-01-19 17:24:46+0000
#
# \param self The object
# \param stacks List of Stack objects
# \param reference Reference as Stack object. Used to simulate slices
# at the position of the slices in stacks
# \param use_masks Turn on/off using masks for the residual
# evaluation
# \param measures Similarity measures as given in
# nsol.similarity_measures, list of strings
#
def __init__(
self,
stacks=None,
reference=None,
use_slice_masks=True,
use_reference_mask=True,
measures=["NCC", "NMI", "PSNR", "SSIM", "RMSE"],
verbose=True,
):
self._stacks = stacks
self._reference = reference
self._measures = measures
self._use_slice_masks = use_slice_masks
self._use_reference_mask = use_reference_mask
self._verbose = verbose
self._slice_projections = None
self._similarities = None
self._slice_similarities = None
self._init_value = np.nan
@staticmethod
def _get_original_number_of_slices(stack):
slices = stack.get_slices()
# Make sure that number of slices are either given by the stack-z-dim
# or, in case it was cropped at some point, at least comprises as
# many slices (whereby original slice number counts!)
N_slices = np.max([
stack.sitk.GetSize()[-1],
np.max([s.get_slice_number() + 1 for s in slices])
])
return N_slices
##
# Sets the stacks.
# \date 2018-01-19 17:26:04+0000
#
# \param self The object
# \param stacks List of Stack objects
#
def set_stacks(self, stacks):
self._stacks = stacks
##
# Sets the reference from which the slices shall be simulated/projected.
# \date 2018-01-19 17:26:14+0000
#
# \param self The object
# \param reference The reference
#
def set_reference(self, reference):
self._reference = reference
def get_measures(self):
return self._measures
##
# Gets the slice similarities computed between simulated/projected and
# original/acquired slices.
# \date 2018-01-19 17:26:44+0000
#
# \param self The object
#
# \return The slice similarities for all slices and measures as
# dictionary. E.g. {
# fetal_brain_1: {'NCC': 1D-array[...], 'NMI': 1D-array[..]},
# ...
# fetal_brain_N: {'NCC': 1D-array[...], 'NMI': 1D-array[..]}
# }
#
def get_slice_similarities(self):
return self._slice_similarities
##
# Gets the slice projections.
# \date 2018-01-19 17:27:41+0000
#
# \param self The object
#
# \return The slice projections as list of lists. E.g. [
# [stack1_slice1_sim, stack1_slice2_sim, ...], ...
# [stackN_slice1_sim, stackN_slice2_sim, ...]
# ]
#
def get_slice_projections(self):
return self._slice_projections
##
# Calculates the slice simulations/projections from the reference given the
# assumed slice acquisition protocol.
# \date 2018-01-19 17:29:20+0000
#
# \param self The object
#
# \return The slice projections.
#
def compute_slice_projections(self):
linear_operators = lin_op.LinearOperators()
self._slice_projections = [None] * len(self._stacks)
for i_stack, stack in enumerate(self._stacks):
slices = stack.get_slices()
N_slices = self._get_original_number_of_slices(stack)
self._slice_projections[i_stack] = [self._init_value] * N_slices
if self._verbose:
ph.print_info(
"Stack %d/%d: Compute slice projections ... " % (
i_stack + 1, len(self._stacks)),
newline=False)
# Compute slice projections based on assumed slice acquisition
# protocol
for slice in slices:
i_slice = slice.get_slice_number()
self._slice_projections[i_stack][i_slice] = linear_operators.A(
self._reference, slice)
if self._verbose:
print("done")
##
# Evaluate slice similarities for all simulated slices of all stacks for
# all similarity measures.
# \date 2018-01-19 17:30:37+0000
#
# \param self The object
#
def evaluate_slice_similarities(self):
if self._slice_projections is None:
raise exceptions.ObjectNotCreated("compute_slice_projections")
self._slice_similarities = {
stack.get_filename(): {} for stack in self._stacks
}
similarity_measures = {
m: SimilarityMeasures.similarity_measures[m]
for m in self._measures
}
for i_stack, stack in enumerate(self._stacks):
slices = stack.get_slices()
N_slices = self._get_original_number_of_slices(stack)
stack_name = stack.get_filename()
self._slice_similarities[stack_name] = {
m: np.ones(N_slices) * self._init_value for m in self._measures
}
if self._verbose:
ph.print_info(
"Stack %d/%d: Compute similarity measures ... " % (
i_stack + 1, len(self._stacks)),
newline=False)
for slice in slices:
i_slice = slice.get_slice_number()
slice_nda = np.squeeze(sitk.GetArrayFromImage(slice.sitk))
slice_proj_nda = np.squeeze(sitk.GetArrayFromImage(
self._slice_projections[i_stack][i_slice].sitk))
mask_nda = np.ones_like(slice_nda)
if self._use_slice_masks:
mask_nda *= np.squeeze(
sitk.GetArrayFromImage(slice.sitk_mask))
if self._use_reference_mask:
mask_nda *= np.squeeze(
sitk.GetArrayFromImage(
self._slice_projections[i_stack][i_slice].sitk_mask))
indices = np.where(mask_nda > 0)
if len(indices[0]) > 0:
for m in self._measures:
try:
self._slice_similarities[stack_name][m][i_slice] = \
similarity_measures[m](
slice_nda[indices], slice_proj_nda[indices])
except ValueError as e:
# Error in case only a few/to less non-zero entries
# exist
if m == "SSIM":
self._slice_similarities[
stack_name][m][i_slice] = \
SimilarityMeasures.UNDEF[m]
else:
raise ValueError(e.message)
else:
for m in self._measures:
self._slice_similarities[
stack_name][m][i_slice] = \
SimilarityMeasures.UNDEF[m]
if self._verbose:
print("done")
##
# Writes the computed slice similarities for all stacks to output directory
# \date 2018-01-19 17:42:27+0000
#
# \param self The object
# \param directory path to output directory, string
#
def write_slice_similarities(self, directory):
for i_stack, stack in enumerate(self._stacks):
stack_name = stack.get_filename()
path_to_file = os.path.join(
directory, "%s.txt" % stack_name)
# Write header info
header = "# %s, %s\n" % (stack.get_filename(), ph.get_time_stamp())
header += "# %s\n" % ("\t").join(self._measures)
ph.write_to_file(path_to_file, header, verbose=self._verbose)
# Write array information
N_slices = self._get_original_number_of_slices(stack)
array = np.ones(
(N_slices, len(self._measures))) * self._init_value
for i_m, m in enumerate(self._measures):
array[:, i_m] = self._slice_similarities[stack_name][m]
ph.write_array_to_file(path_to_file, array, verbose=self._verbose)
##
# Reads computed slice similarities for all files in directory.
# \date 2018-01-19 17:42:54+0000
#
# \param self The object
# \param directory The directory
# \param ext The extent
# \post self._slice_similarities updated
#
def read_slice_similarities(self, directory, ext="txt"):
if not ph.directory_exists(directory):
raise IOError("Given directory '%s' does not exist" % (
directory))
pattern = "([a-zA-Z0-9_\+\-]+)[.]%s" % ext
p = re.compile(pattern)
stack_names = [
p.match(f).group(1)
for f in os.listdir(directory) if p.match(f)
]
self._slice_similarities = {
stack_name: {} for stack_name in stack_names
}
for stack_name in stack_names:
path_to_file = os.path.join(directory, "%s.%s" % (stack_name, ext))
# Read computed measures
self._measures = ph.read_file_line_by_line(path_to_file)[1]
self._measures = re.sub("# ", "", self._measures)
self._measures = re.sub("\n", "", self._measures)
self._measures = self._measures.split("\t")
# Read array
array = np.loadtxt(path_to_file, skiprows=2)
# Ensure correct shape in case only a single slice available
array = array.reshape(-1, len(self._measures))
if array.ndim == 1:
array = array.reshape(len(array), 1)
for i_m, m in enumerate(self._measures):
self._slice_similarities[stack_name][m] = array[:, i_m]
##
# Shows the slice similarities in plots.
# \date 2018-02-09 18:28:45+0000
#
# \param self The object
# \param directory The directory
# \param title The title
# \param measures The measures
# \param threshold The threshold
#
def show_slice_similarities(
self,
directory=None,
title=None,
measures=["NCC"],
threshold=0.8,
):
for i_m, measure in enumerate(measures):
fig = plt.figure(measure)
fig.clf()
if title is not None:
title = "%s: %s" % (title, measure)
else:
title = measure
plt.suptitle(title)
stack_names = self._slice_similarities.keys()
for i_name, stack_name in enumerate(stack_names):
ax = plt.subplot(np.ceil(len(stack_names) / 2.), 2, i_name + 1)
nda = self._slice_similarities[stack_name][measure]
nda = np.nan_to_num(nda)
x = np.arange(nda.size)
# indices_in = np.where(nda >= threshold)
indices_out = np.where(nda < threshold)
plt.plot(x, nda,
color=ph.COLORS_TABLEAU20[0],
markerfacecolor="w",
marker=ph.MARKERS[0],
linestyle=":",
)
plt.plot(indices_out[0], nda[indices_out],
color=ph.COLORS_TABLEAU20[6],
# markerfacecolor="w",
marker=ph.MARKERS[0],
linestyle="",
)
plt.plot([x[0], x[-1]], np.ones(2) * threshold,
color=ph.COLORS_TABLEAU20[6],
linestyle="-.",
)
plt.xlabel("Slice")
# plt.ylabel(measure)
plt.title(stack_name)
ax.set_xticks(x)
# ax.set_xticklabels(x + 1)
ax.set_ylim([0, 1])
sh.make_figure_fullscreen()
plt.show(block=False)
if directory is not None:
filename = "slice_similarities_%s.pdf" % measure
ph.save_fig(fig, directory, filename)
================================================
FILE: niftymic/validation/show_evaluated_simulated_stack_similarity.py
================================================
##
# \file show_evaluated_simulated_stack_similarity.py
# \brief Script to show the evaluated similarity between simulated stack
# from obtained reconstruction and original stack.
#
# This function takes the result of evaluate_simulated_stack_similarity.py as
# input.
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date November 2017
#
# Import libraries
import SimpleITK as sitk
import numpy as np
import os
import re
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
from natsort import natsorted
import pysitk.python_helper as ph
from nsol.similarity_measures import SimilarityMeasures as \
SimilarityMeasures
from niftymic.utilities.input_arparser import InputArgparser
import niftymic.base.exceptions as exceptions
from niftymic.definitions import REGEX_FILENAMES
def main():
input_parser = InputArgparser(
description="Script to show the evaluated similarity between "
"simulated stack from obtained reconstruction and original stack. "
"This function takes the result of "
"evaluate_simulated_stack_similarity.py as input. "
"Provide --dir-output in order to save the results."
)
input_parser.add_dir_input(required=True)
input_parser.add_dir_output(required=False)
args = input_parser.parse_args()
input_parser.print_arguments(args)
if not ph.directory_exists(args.dir_input):
raise exceptions.DirectoryNotExistent(args.dir_input)
# --------------------------------Read Data--------------------------------
pattern = "Similarity_(" + REGEX_FILENAMES + ")[.]txt"
p = re.compile(pattern)
dic_filenames = {
p.match(f).group(1): p.match(f).group(0)
for f in os.listdir(args.dir_input) if p.match(f)
}
dic_stacks = {}
for filename in dic_filenames.keys():
path_to_file = os.path.join(args.dir_input, dic_filenames[filename])
# Extract evaluated measures written as header in second line
measures = open(path_to_file).readlines()[1]
measures = re.sub("#\t", "", measures)
measures = re.sub("\n", "", measures)
measures = measures.split("\t")
# Extract errors
similarities = np.loadtxt(path_to_file, skiprows=2)
# Build dictionary holding all similarity information for stack
dic_stack_similarity = {
measures[i]: similarities[:, i] for i in range(len(measures))
}
# dic_stack_similarity["measures"] = measures
# Store information of to dictionary
dic_stacks[filename] = dic_stack_similarity
# -----------Visualize stacks individually per similarity measure----------
ctr = [0]
N_stacks = len(dic_stacks)
N_measures = len(measures)
rows = 2 if N_measures < 6 else 3
filenames = natsorted(dic_stacks.keys(), key=lambda y: y.lower())
for i, filename in enumerate(filenames):
fig = plt.figure(ph.add_one(ctr))
fig.clf()
for m, measure in enumerate(measures):
ax = plt.subplot(rows, np.ceil(N_measures/float(rows)), m+1)
y = dic_stacks[filename][measure]
x = range(1, y.size+1)
lines = plt.plot(x, y)
line = lines[0]
line.set_linestyle("")
line.set_marker(ph.MARKERS[0])
# line.set_markerfacecolor("w")
plt.xlabel("Slice")
plt.ylabel(measure)
ax.set_xticks(x)
if measure in ["SSIM", "NCC"]:
ax.set_ylim([0, 1])
plt.suptitle(filename)
try:
# Open windows (and also save them) in full screen
manager = plt.get_current_fig_manager()
manager.full_screen_toggle()
except:
pass
plt.show(block=False)
if args.dir_output is not None:
filename = "Similarity_%s.pdf" % filename
ph.save_fig(fig, args.dir_output, filename)
# -----------All in one (meaningful in case of similar scaling)----------
fig = plt.figure(ph.add_one(ctr))
fig.clf()
data = {}
for m, measure in enumerate(measures):
for i, filename in enumerate(filenames):
similarities = dic_stacks[filename][measure]
labels = [filename] * similarities.size
if m == 0:
if "Stack" not in data.keys():
data["Stack"] = labels
else:
data["Stack"] = np.concatenate((data["Stack"], labels))
if measure not in data.keys():
data[measure] = similarities
else:
data[measure] = np.concatenate(
(data[measure], similarities))
df_melt = pd.DataFrame(data).melt(
id_vars="Stack",
var_name="",
value_name=" ",
value_vars=measures,
)
ax = plt.subplot(1, 1, 1)
b = sns.boxplot(
data=df_melt,
hue="Stack", # different colors for different "Stack"
x="",
y=" ",
order=measures,
)
ax.set_axisbelow(True)
try:
# Open windows (and also save them) in full screen
manager = plt.get_current_fig_manager()
manager.full_screen_toggle()
except:
pass
plt.show(block=False)
if args.dir_output is not None:
filename = "Boxplot.pdf"
ph.save_fig(fig, args.dir_output, filename)
# # -------------Boxplot: Plot individual similarity measures v1----------
# for m, measure in enumerate(measures):
# fig = plt.figure(ph.add_one(ctr))
# fig.clf()
# data = {}
# for i, filename in enumerate(filenames):
# similarities = dic_stacks[filename][measure]
# labels = [filename] * similarities.size
# if "Stack" not in data.keys():
# data["Stack"] = labels
# else:
# data["Stack"] = np.concatenate((data["Stack"], labels))
# if measure not in data.keys():
# data[measure] = similarities
# else:
# data[measure] = np.concatenate(
# (data[measure], similarities))
# df_melt = pd.DataFrame(data).melt(
# id_vars="Stack",
# var_name="",
# value_name=measure,
# )
# ax = plt.subplot(1, 1, 1)
# b = sns.boxplot(
# data=df_melt,
# hue="Stack", # different colors for different "Stack"
# x="",
# y=measure,
# )
# ax.set_axisbelow(True)
# plt.show(block=False)
# # -------------Boxplot: Plot individual similarity measures v2----------
# for m, measure in enumerate(measures):
# fig = plt.figure(ph.add_one(ctr))
# fig.clf()
# data = {}
# for i, filename in enumerate(filenames):
# similarities = dic_stacks[filename][measure]
# labels = [filename] * len(filenames)
# if filename not in data.keys():
# data[filename] = similarities
# else:
# data[filename] = np.concatenate(
# (data[filename], similarities))
# for filename in filenames:
# data[filename] = pd.Series(data[filename])
# df = pd.DataFrame(data)
# df_melt = df.melt(
# var_name="",
# value_name=measure,
# value_vars=filenames,
# )
# ax = plt.subplot(1, 1, 1)
# b = sns.boxplot(
# data=df_melt,
# x="",
# y=measure,
# order=filenames,
# )
# ax.set_axisbelow(True)
# plt.show(block=False)
return 0
if __name__ == '__main__':
main()
================================================
FILE: niftymic/validation/simulate_stacks_from_reconstruction.py
================================================
##
# \file simulate_stacks_from_reconstruction.py
# \brief Simulate stacks from obtained reconstruction
#
# Example call:
# python simulate_stacks_from_reconstruction.py \
# --dir-input dir-to-motion-correction \
# --reconstruction volumetric_reconstruction.nii.gz \
# --copy-data 1 \
# --dir-output dir-to-output
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date November 2017
#
# Import libraries
import os
import numpy as np
import SimpleITK as sitk
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
import niftymic.base.stack as st
import niftymic.base.data_reader as dr
import niftymic.base.data_writer as dw
import niftymic.reconstruction.linear_operators as lin_op
from niftymic.utilities.input_arparser import InputArgparser
from niftymic.definitions import ALLOWED_INTERPOLATORS
INTERPOLATOR_TYPES = "%s, or %s" % (
(", ").join(ALLOWED_INTERPOLATORS[0:-1]), ALLOWED_INTERPOLATORS[-1])
def main():
input_parser = InputArgparser(
description="Simulate stacks from obtained reconstruction. "
"Script simulates/projects the slices at estimated positions "
"within reconstructed volume. Ideally, if motion correction was "
"correct, the resulting stack of such obtained projected slices, "
"corresponds to the originally acquired (motion corrupted) data.",
)
input_parser.add_filenames(required=True)
input_parser.add_filenames_masks()
input_parser.add_dir_input_mc(required=True)
input_parser.add_reconstruction(required=True)
input_parser.add_dir_output(required=True)
input_parser.add_suffix_mask(default="_mask")
input_parser.add_prefix_output(default="Simulated_")
input_parser.add_option(
option_string="--copy-data",
type=int,
help="Turn on/off copying of original data (including masks) to "
"output folder.",
default=0)
input_parser.add_option(
option_string="--reconstruction-mask",
type=str,
help="If given, reconstruction image mask is propagated to "
"simulated stack(s) of slices as well",
default=None)
input_parser.add_interpolator(
option_string="--interpolator-mask",
help="Choose the interpolator type to propagate the reconstruction "
"mask (%s)." % (INTERPOLATOR_TYPES),
default="NearestNeighbor")
input_parser.add_log_config(default=0)
input_parser.add_verbose(default=0)
input_parser.add_slice_thicknesses(default=None)
args = input_parser.parse_args()
input_parser.print_arguments(args)
if args.interpolator_mask not in ALLOWED_INTERPOLATORS:
raise IOError(
"Unknown interpolator provided. Possible choices are %s" % (
INTERPOLATOR_TYPES))
if args.log_config:
input_parser.log_config(os.path.abspath(__file__))
# Read motion corrected data
data_reader = dr.MultipleImagesReader(
file_paths=args.filenames,
file_paths_masks=args.filenames_masks,
suffix_mask=args.suffix_mask,
dir_motion_correction=args.dir_input_mc,
stacks_slice_thicknesses=args.slice_thicknesses,
)
data_reader.read_data()
stacks = data_reader.get_data()
reconstruction = st.Stack.from_filename(
args.reconstruction, args.reconstruction_mask, extract_slices=False)
linear_operators = lin_op.LinearOperators()
for i, stack in enumerate(stacks):
# initialize image data array(s)
nda = np.zeros_like(sitk.GetArrayFromImage(stack.sitk))
nda[:] = np.nan
if args.reconstruction_mask:
nda_mask = np.zeros_like(sitk.GetArrayFromImage(stack.sitk_mask))
slices = stack.get_slices()
kept_indices = [s.get_slice_number() for s in slices]
# Fill stack information "as if slice was acquired consecutively"
# Therefore, simulated stack slices correspond to acquired slices
# (in case motion correction was correct)
for j in range(nda.shape[0]):
if j in kept_indices:
index = kept_indices.index(j)
simulated_slice = linear_operators.A(
reconstruction,
slices[index],
interpolator_mask=args.interpolator_mask
)
nda[j, :, :] = sitk.GetArrayFromImage(simulated_slice.sitk)
if args.reconstruction_mask:
nda_mask[j, :, :] = sitk.GetArrayFromImage(
simulated_slice.sitk_mask)
# Create nifti image with same image header as original stack
simulated_stack_sitk = sitk.GetImageFromArray(nda)
simulated_stack_sitk.CopyInformation(stack.sitk)
if args.reconstruction_mask:
simulated_stack_sitk_mask = sitk.GetImageFromArray(nda_mask)
simulated_stack_sitk_mask.CopyInformation(stack.sitk_mask)
else:
simulated_stack_sitk_mask = None
simulated_stack = st.Stack.from_sitk_image(
image_sitk=simulated_stack_sitk,
image_sitk_mask=simulated_stack_sitk_mask,
filename=args.prefix_output + stack.get_filename(),
extract_slices=False,
slice_thickness=stack.get_slice_thickness(),
)
if args.verbose:
sitkh.show_stacks([
stack, simulated_stack],
segmentation=stack)
simulated_stack.write(
args.dir_output,
write_mask=False,
write_slices=False,
suffix_mask=args.suffix_mask)
if args.copy_data:
stack.write(
args.dir_output,
write_mask=True,
write_slices=False,
suffix_mask="_mask")
return 0
if __name__ == '__main__':
main()
================================================
FILE: niftymic/validation/slice_acquisition.py
================================================
##
# \file slice_acquisition.py
# \brief Based on a given volume, this class aims to simulate the slice
# acquisition.
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date May 2016
#
# Import libraries
import itk
import SimpleITK as sitk
import numpy as np
import os
import sys
from abc import ABCMeta, abstractmethod
# Import modules from src-folder
import pysitk.simple_itk_helper as sitkh
import niftymic.base.stack as st
import niftymic.base.psf as psf
# Class simulating the slice acquisition
class SliceAcqusition(object):
__metaclass__ = ABCMeta
def __init__(self,
reference,
interpolator,
alpha_cut,
pixel_type=itk.D):
self._reference = reference
self._interpolator = interpolator
self._alpha_cut = alpha_cut
self._pixel_type = pixel_type
self._image_type = itk.Image[pixel_type, reference.sitk.GetDimension()]
self._output = None
def run(self):
self._run()
@abstractmethod
def _run(self):
pass
def _get_interpolator(self, stack_slice):
if self._interpolator == "OrientedGaussian":
# Get oriented PSF covariance matrix
cov = psf.PSF().get_covariance_matrix_in_reconstruction_space(
stack_slice, self._reference)
# Specify oriented Gaussian interpolator
interpolator_itk = itk.OrientedGaussianInterpolateImageFunction[
self._image_type, self._pixel_type].New()
interpolator_itk.SetCovariance(cov.flatten())
interpolator_itk.SetAlpha(self._alpha_cut)
else:
interpolator_itk = eval(
"itk.%sInterpolateImageFunction"
"[self._image_type, self._pixel_type].New()" %
self._interpolator)
return interpolator_itk
def get_output(self):
return st.Stack.from_stack(self._output)
class StaticSliceAcquisition(SliceAcqusition):
def __init__(self,
stack_slice,
reference,
interpolator="Linear",
alpha_cut=3):
SliceAcqusition.__init__(self,
reference=reference,
interpolator=interpolator,
alpha_cut=alpha_cut,
)
self._stack_slice = stack_slice
def set_stack_slice(self, stack_slice):
self._stack_slice = stack_slice
def _run(self):
resampler_itk = itk.ResampleImageFilter[
self._image_type, self._image_type].New()
resampler_itk.SetOutputParametersFromImage(self._stack_slice.itk)
resampler_itk.SetInterpolator(
self._get_interpolator(self._stack_slice))
resampler_itk.SetInput(self._reference.itk)
resampler_itk.UpdateLargestPossibleRegion()
resampler_itk.Update()
output_itk = resampler_itk.GetOutput()
output_itk.DisconnectPipeline()
output_sitk = sitkh.get_sitk_from_itk_image(output_itk)
self._output = st.Stack.from_sitk_image(
image_sitk=output_sitk,
image_sitk_mask=self._stack_slice.sitk_mask,
filename=self._stack_slice.get_filename()
)
================================================
FILE: niftymic/validation/slice_coverage.py
================================================
##
# \file slice_coverage.py
# \brief Class to visualize slice coverage over reconstruction space
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date Feb 2019
#
import numpy as np
import SimpleITK as sitk
##
# Class to visualize slice coverage over reconstruction space
# \date 2019-02-23 21:17:49+0000
#
class SliceCoverage(object):
def __init__(self, stacks, reconstruction_sitk):
self._stacks = stacks
self._reconstruction_sitk = reconstruction_sitk
self._coverage_sitk = None
##
# Gets the slice coverage as Image. The (integer) intensity values reflect
# the number of slices that have contributed to this particular voxel.
# \date 2019-02-23 21:18:10+0000
#
# \param self The object
#
# \return Slice coverage as sitk.Image uint8 image.
#
def get_coverage_sitk(self):
if self._coverage_sitk is None:
raise RuntimeError("Execute 'run' first")
return sitk.Image(self._coverage_sitk)
##
# Compute slice coverage
# \date 2019-02-23 21:19:55+0000
#
# \param self The object
#
def run(self):
# create zero image
coverage_sitk = sitk.Image(self._reconstruction_sitk) * 0
for i, stack in enumerate(self._stacks):
print("Slices of stack %d/%d ... " % (i + 1, len(self._stacks)))
# Add each individual slice contribution
for slice in stack.get_slices():
coverage_sitk = self._add_slice_contribution(
slice, coverage_sitk)
# Cast to unsigned integer
self._coverage_sitk = sitk.Cast(coverage_sitk, sitk.sitkUInt8)
##
# Adds a slice contribution.
# \date 2019-02-23 21:27:12+0000
#
# \param slice Slice as sl.Slice object
# \param coverage_sitk sitk.Image reflecting the current iteration of
# slice coverage
#
# \return Updated slice contribution, sitk.Image
#
@staticmethod
def _add_slice_contribution(slice, coverage_sitk):
#
slice_sitk = sitk.Image(slice.sitk)
spacing = np.array(slice_sitk.GetSpacing())
spacing[-1] = slice.get_slice_thickness()
slice_sitk.SetSpacing(spacing)
contrib_nda = sitk.GetArrayFromImage(slice_sitk)
contrib_nda[:] = 1
contrib_sitk = sitk.GetImageFromArray(contrib_nda)
contrib_sitk.CopyInformation(slice_sitk)
coverage_sitk += sitk.Resample(
contrib_sitk,
coverage_sitk,
sitk.Euler3DTransform(),
sitk.sitkNearestNeighbor,
0,
coverage_sitk.GetPixelIDValue(),
)
return coverage_sitk
================================================
FILE: niftymic/validation/write_random_motion_transforms.py
================================================
##
# \file write_random_motion_transforms.py
# \brief Create and write random motion transforms
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date November 2017
#
import numpy as np
from niftymic.utilities.input_arparser import InputArgparser
import niftymic.validation.motion_simulator as ms
def main():
input_parser = InputArgparser(
description="Create and write random rigid motion transformations. "
"Simulated transformations are exported as (Simple)ITK transforms. ",
)
input_parser.add_dir_output(required=True)
input_parser.add_option(
option_string="--simulations",
type=int,
required=True,
help="Number of simulated motion transformations."
)
input_parser.add_option(
option_string="--angle-max",
default=10,
help="random angles (in degree) are drawn such "
"that |angle| <= angle_max."
)
input_parser.add_option(
option_string="--translation-max",
default=10,
help="random translations (in millimetre) are drawn such "
"that |translation| <= translation_max."
)
input_parser.add_option(
option_string="--seed",
type=int,
default=1,
help="Seed for pseudo-random data generation"
)
input_parser.add_option(
option_string="--dimension",
type=int,
default=3,
help="Spatial dimension for transformations."
)
input_parser.add_prefix_output(default="EulerTransform_slice")
input_parser.add_verbose(default=1)
args = input_parser.parse_args()
input_parser.print_arguments(args)
motion_simulator = ms.RandomRigidMotionSimulator(
dimension=args.dimension,
angle_max_deg=args.angle_max,
translation_max=args.translation_max,
verbose=args.verbose)
motion_simulator.simulate_motion(
seed=args.seed,
simulations=args.simulations,
)
motion_simulator.write_transforms_sitk(
directory=args.dir_output,
prefix_filename=args.prefix_output,
)
return 0
if __name__ == '__main__':
main()
================================================
FILE: niftymic_correct_bias_field.py
================================================
# -*- coding: utf-8 -*-
import sys
from niftymic.application.correct_bias_field import main
if __name__ == "__main__":
sys.exit(main())
================================================
FILE: niftymic_multiply.py
================================================
# -*- coding: utf-8 -*-
import sys
from niftymic.application.multiply import main
if __name__ == "__main__":
sys.exit(main())
================================================
FILE: niftymic_nifti2dicom.py
================================================
# -*- coding: utf-8 -*-
import sys
from niftymic.application.nifti2dicom import main
if __name__ == "__main__":
sys.exit(main())
================================================
FILE: niftymic_reconstruct_volume.py
================================================
# -*- coding: utf-8 -*-
import sys
from niftymic.application.reconstruct_volume import main
if __name__ == "__main__":
sys.exit(main())
================================================
FILE: niftymic_reconstruct_volume_from_slices.py
================================================
# -*- coding: utf-8 -*-
import sys
from niftymic.application.reconstruct_volume_from_slices import main
if __name__ == "__main__":
sys.exit(main())
================================================
FILE: niftymic_register_image.py
================================================
# -*- coding: utf-8 -*-
import sys
from niftymic.application.register_image import main
if __name__ == "__main__":
sys.exit(main())
================================================
FILE: niftymic_rsfmri_estimate_motion.py
================================================
# -*- coding: utf-8 -*-
import sys
from niftymic.application.rsfmri_estimate_motion import main
if __name__ == "__main__":
sys.exit(main())
================================================
FILE: niftymic_rsfmri_reconstruct_volume_from_slices.py
================================================
# -*- coding: utf-8 -*-
import sys
from niftymic.application.rsfmri_reconstruct_volume_from_slices import main
if __name__ == "__main__":
sys.exit(main())
================================================
FILE: niftymic_run_reconstruction_parameter_study.py
================================================
# -*- coding: utf-8 -*-
import sys
from niftymic.application.run_reconstruction_parameter_study import main
if __name__ == "__main__":
sys.exit(main())
================================================
FILE: niftymic_run_reconstruction_pipeline.py
================================================
# -*- coding: utf-8 -*-
import sys
from niftymic.application.multiply_stack_with_mask import main
if __name__ == "__main__":
sys.exit(main())
================================================
FILE: niftymic_segment_fetal_brains.py
================================================
# -*- coding: utf-8 -*-
import sys
from niftymic.application.segment_fetal_brains import main
if __name__ == "__main__":
sys.exit(main())
================================================
FILE: niftymic_show_reconstruction_parameter_study.py
================================================
# -*- coding: utf-8 -*-
import sys
from nsol.application.show_parameter_study import main
if __name__ == "__main__":
sys.exit(main())
================================================
FILE: requirements-monaifbs.txt
================================================
torch>=1.4.0
torch-summary>=1.3.2
monai==0.3.0
pyyaml>=5.3.1
pytorch-ignite>=0.3.0
tensorboard>=2.2.1
================================================
FILE: requirements.txt
================================================
matplotlib>=2.2.2
natsort>=5.3.0
nibabel>=2.4.1
nipype>=1.0.3
nose>=1.3.7
nsol>=0.1.14
numpy>=1.14.2,!=1.16.0
pandas>=0.22.0
pydicom>=1.2.0
pysitk>=0.2.19
scikit_image>=0.14.1
scipy>=1.0.1
seaborn>=0.8.1
SimpleITK>=1.2.0
simplereg>=0.3.2
six>=1.11.0
================================================
FILE: setup.py
================================================
##
# \file setup.py
#
# Instructions:
# 1) Set environment variables with prefix NIFTYMIC_, e.g.
# `export NIFTYMIC_ITK_DIR=path-to-ITK_NIFTYMIC-build`
# to incorporate `-D ITK_DIR=path-to-ITK_NIFTYMIC-build` in `cmake` build.
# 2) `pip install -e .`
# All python packages and command line tools are then installed during
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date July 2017
#
import re
import os
import sys
from setuptools import setup, find_packages
about = {}
with open(os.path.join("niftymic", "__about__.py")) as fp:
exec(fp.read(), about)
with open("README.md", "r") as fh:
long_description = fh.read()
def install_requires(fname="requirements.txt"):
with open(fname) as f:
content = f.readlines()
content = [x.strip() for x in content]
return content
setup(name='NiftyMIC',
version=about["__version__"],
description=about["__summary__"],
long_description=long_description,
long_description_content_type="text/markdown",
url='https://github.com/gift-surg/NiftyMIC',
author=about["__author__"],
author_email=about["__email__"],
license=about["__license__"],
packages=find_packages(),
install_requires=install_requires(),
# data_files=[(d, [os.path.join(d, f) for f in files])
# for d, folders, files
# in os.walk(os.path.join("data", "demo"))],
zip_safe=False,
keywords='development numericalsolver convexoptimisation',
classifiers=[
'Intended Audience :: Developers',
'Intended Audience :: Healthcare Industry',
'Intended Audience :: Science/Research',
'License :: OSI Approved :: BSD License',
'Topic :: Software Development :: Build Tools',
'Topic :: Scientific/Engineering :: Medical Science Apps.',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 3',
],
entry_points={
'console_scripts': [
'niftymic_correct_bias_field = niftymic.application.correct_bias_field:main',
'niftymic_reconstruct_volume = niftymic.application.reconstruct_volume:main',
'niftymic_reconstruct_volume_from_slices = niftymic.application.reconstruct_volume_from_slices:main',
'niftymic_register_image = niftymic.application.register_image:main',
'niftymic_multiply = niftymic.application.multiply:main',
'niftymic_run_reconstruction_parameter_study = niftymic.application.run_reconstruction_parameter_study:main',
'niftymic_run_reconstruction_pipeline = niftymic.application.run_reconstruction_pipeline:main',
'niftymic_nifti2dicom = niftymic.application.nifti2dicom:main',
'niftymic_show_reconstruction_parameter_study = nsol.application.show_parameter_study:main',
'niftymic_segment_fetal_brains = niftymic.application.segment_fetal_brains:main',
# rs-fMRI
'niftymic_rsfmri_estimate_motion = niftymic.application.rsfmri_estimate_motion:main',
'niftymic_rsfmri_reconstruct_volume_from_slices = niftymic.application.rsfmri_reconstruct_volume_from_slices:main',
],
},
)
================================================
FILE: tests/__init__.py
================================================
================================================
FILE: tests/brain_stripping_test.py
================================================
# \file TestBrainStripping.py
# \brief Class containing unit tests for module BrainStripping
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date December 2015
import os
import unittest
import numpy as np
import SimpleITK as sitk
import pysitk.simple_itk_helper as sitkh
import niftymic.utilities.brain_stripping as bs
from niftymic.definitions import DIR_TEST
class BrainStrippingTest(unittest.TestCase):
# Specify input data
dir_test_data = DIR_TEST
accuracy = 7
def setUp(self):
self.precision = 7
self.dir_data = os.path.join(
DIR_TEST, "case-studies", "fetal-brain", "input-data")
self.filename = "axial"
def test_01_input_output(self):
brain_stripping = bs.BrainStripping.from_filename(
self.dir_data, self.filename)
brain_stripping.compute_brain_image(0)
brain_stripping.compute_brain_mask(0)
brain_stripping.compute_skull_image(0)
brain_stripping.run()
with self.assertRaises(ValueError) as ve:
brain_stripping.get_brain_image_sitk()
self.assertEqual(
"Brain was not asked for. Do not set option '-n' and run again.",
str(ve.exception))
with self.assertRaises(ValueError) as ve:
brain_stripping.get_brain_mask_sitk()
self.assertEqual(
"Brain mask was not asked for. Set option '-m' and run again.",
str(ve.exception))
with self.assertRaises(ValueError) as ve:
brain_stripping.get_skull_mask_sitk()
self.assertEqual(
"Skull mask was not asked for. Set option '-s' and run again.",
str(ve.exception))
def test_02_brain_mask(self):
path_to_reference = os.path.join(
DIR_TEST, "case-studies", "fetal-brain", "brain_stripping", "axial_seg.nii.gz")
brain_stripping = bs.BrainStripping.from_filename(
self.dir_data, self.filename)
brain_stripping.compute_brain_image(0)
brain_stripping.compute_brain_mask(1)
brain_stripping.compute_skull_image(0)
# brain_stripping.set_bet_options("-f 0.3")
brain_stripping.run()
original_sitk = brain_stripping.get_input_image_sitk()
res_sitk = brain_stripping.get_brain_mask_sitk()
ref_sitk = sitkh.read_nifti_image_sitk(path_to_reference)
diff_sitk = res_sitk - ref_sitk
error = np.linalg.norm(sitk.GetArrayFromImage(diff_sitk))
self.assertAlmostEqual(error, 0, places=self.precision)
================================================
FILE: tests/case_study_fetal_brain_test.py
================================================
##
# \file case_study_fetal_brain_test.py
# \brief Unit tests based on fetal brain case study
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date November 2017
import os
import unittest
import numpy as np
import re
import SimpleITK as sitk
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
from niftymic.definitions import DIR_TMP, DIR_TEST, REGEX_FILENAMES, DIR_TEMPLATES
class CaseStudyFetalBrainTest(unittest.TestCase):
def setUp(self):
self.precision = 7
self.dir_data = os.path.join(DIR_TEST, "case-studies", "fetal-brain")
self.filenames = [
os.path.join(self.dir_data,
"input-data",
"%s.nii.gz" % f)
for f in ["axial", "coronal", "sagittal"]]
self.dir_output = os.path.join(DIR_TMP, "case-studies", "fetal-brain")
self.suffix_mask = "_mask"
def test_reconstruct_volume(self):
filename = "SRR_stacks3_TK1_lsmr_alpha0p02_itermax5.nii.gz"
output = os.path.join(self.dir_output, filename)
dir_reference = os.path.join(self.dir_data, "reconstruct_volume")
dir_reference_mc = os.path.join(dir_reference, "motion_correction")
path_to_reference = os.path.join(dir_reference, filename)
path_to_reference_mask = ph.append_to_filename(
os.path.join(dir_reference, filename), self.suffix_mask)
two_step_cycles = 1
iter_max = 5
threshold = 0.75
alpha = 0.02
alpha_first = 0.2
sigma = 1
intensity_correction = 1
isotropic_resolution = 1.02
v2v_method = "RegAladin"
cmd_args = []
cmd_args.append("--filenames %s" % " ".join(self.filenames))
cmd_args.append("--output %s" % output)
cmd_args.append("--suffix-mask %s" % self.suffix_mask)
cmd_args.append("--two-step-cycles %s" % two_step_cycles)
cmd_args.append("--iter-max %d" % iter_max)
cmd_args.append("--threshold-first %f" % threshold)
cmd_args.append("--sigma %f" % sigma)
cmd_args.append("--threshold %f" % threshold)
cmd_args.append("--intensity-correction %d" % intensity_correction)
cmd_args.append("--isotropic-resolution %s" % isotropic_resolution)
cmd_args.append("--alpha %f" % alpha)
cmd_args.append("--alpha-first %f" % alpha_first)
cmd_args.append("--v2v-method %s" % v2v_method)
# cmd_args.append("--verbose 1")
cmd = "niftymic_reconstruct_volume %s" % (
" ").join(cmd_args)
self.assertEqual(ph.execute_command(cmd), 0)
# Check SRR volume
res_sitk = sitkh.read_nifti_image_sitk(output)
ref_sitk = sitkh.read_nifti_image_sitk(path_to_reference)
diff_sitk = res_sitk - ref_sitk
error = np.linalg.norm(sitk.GetArrayFromImage(diff_sitk))
self.assertAlmostEqual(error, 0, places=self.precision)
# Check SRR mask volume
res_sitk = sitkh.read_nifti_image_sitk(
ph.append_to_filename(output, "_mask"))
ref_sitk = sitkh.read_nifti_image_sitk(path_to_reference_mask)
diff_sitk = res_sitk - ref_sitk
error = np.linalg.norm(sitk.GetArrayFromImage(diff_sitk))
self.assertAlmostEqual(error, 0, places=self.precision)
# Check transforms
pattern = REGEX_FILENAMES + "[.]tfm"
p = re.compile(pattern)
dir_res_mc = os.path.join(self.dir_output, "motion_correction")
trafos_res = sorted(
[os.path.join(dir_res_mc, t)
for t in os.listdir(dir_res_mc) if p.match(t)])
trafos_ref = sorted(
[os.path.join(dir_reference_mc, t)
for t in os.listdir(dir_reference_mc) if p.match(t)])
self.assertEqual(len(trafos_res), len(trafos_ref))
for i in range(len(trafos_ref)):
nda_res = sitkh.read_transform_sitk(trafos_res[i]).GetParameters()
nda_ref = sitkh.read_transform_sitk(trafos_ref[i]).GetParameters()
nda_diff = np.linalg.norm(np.array(nda_res) - nda_ref)
self.assertAlmostEqual(nda_diff, 0, places=self.precision)
def test_reconstruct_volume_from_slices(self):
filename = "SRR_stacks3_TK1_lsmr_alpha0p02_itermax5.nii.gz"
output = os.path.join(self.dir_output, filename)
dir_reference = os.path.join(
self.dir_data, "reconstruct_volume_from_slices")
dir_input_mc = os.path.join(
self.dir_data, "reconstruct_volume_from_slices", "motion_correction")
path_to_reference = os.path.join(dir_reference, filename)
iter_max = 5
alpha = 0.02
intensity_correction = 1
cmd_args = []
cmd_args.append("--filenames %s" % " ".join(self.filenames))
cmd_args.append("--dir-input-mc %s" % dir_input_mc)
cmd_args.append("--output %s" % output)
cmd_args.append("--iter-max %d" % iter_max)
cmd_args.append("--intensity-correction %d" % intensity_correction)
cmd_args.append("--alpha %f" % alpha)
cmd_args.append("--reconstruction-space %s" % path_to_reference)
cmd = "niftymic_reconstruct_volume_from_slices %s" % (
" ").join(cmd_args)
self.assertEqual(ph.execute_command(cmd), 0)
# Check whether identical reconstruction has been created
reconstruction_sitk = sitkh.read_nifti_image_sitk(output)
reference_sitk = sitkh.read_nifti_image_sitk(path_to_reference)
difference_sitk = reconstruction_sitk - reference_sitk
error = np.linalg.norm(sitk.GetArrayFromImage(difference_sitk))
self.assertAlmostEqual(error, 0, places=self.precision)
def test_register_image(self):
filename = "registration_transform_sitk.txt"
gestational_age = 28
path_to_recon = os.path.join(
self.dir_data, "register_image",
"SRR_stacks3_TK1_lsmr_alpha0p02_itermax5.nii.gz")
dir_input_mc = os.path.join(
self.dir_data, "register_image", "motion_correction")
path_to_transform_res = os.path.join(self.dir_output, filename)
path_to_transform_ref = os.path.join(
self.dir_data, "register_image", filename)
dir_ref_mc = os.path.join(
self.dir_data, "register_image", "motion_correction_ref")
path_to_rejected_slices_ref = os.path.join(
dir_ref_mc, "rejected_slices.json")
template = os.path.join(
DIR_TEMPLATES,
"STA%d.nii.gz" % gestational_age)
template_mask = os.path.join(
DIR_TEMPLATES,
"STA%d_mask.nii.gz" % gestational_age)
cmd_args = ["niftymic_register_image"]
cmd_args.append("--fixed %s" % template)
cmd_args.append("--moving %s" % path_to_recon)
cmd_args.append("--fixed-mask %s" % template_mask)
cmd_args.append("--moving-mask %s" %
ph.append_to_filename(path_to_recon, self.suffix_mask))
cmd_args.append("--dir-input-mc %s" % dir_input_mc)
cmd_args.append("--output %s" % path_to_transform_res)
cmd_args.append("--init-pca")
# cmd_args.append("--verbose 1")
self.assertEqual(ph.execute_command(" ".join(cmd_args)), 0)
# Check registration transform
res_sitk = sitkh.read_transform_sitk(path_to_transform_res)
ref_sitk = sitkh.read_transform_sitk(path_to_transform_ref)
res_nda = res_sitk.GetParameters()
ref_nda = ref_sitk.GetParameters()
diff_nda = np.array(res_nda) - ref_nda
self.assertAlmostEqual(
np.linalg.norm(diff_nda), 0, places=self.precision)
# Check individual slice transforms
pattern = REGEX_FILENAMES + "[.]tfm"
p = re.compile(pattern)
dir_res_mc = os.path.join(self.dir_output, "motion_correction")
trafos_res = sorted(
[os.path.join(dir_res_mc, t)
for t in os.listdir(dir_res_mc) if p.match(t)])
trafos_ref = sorted(
[os.path.join(dir_ref_mc, t)
for t in os.listdir(dir_res_mc) if p.match(t)])
self.assertEqual(len(trafos_res), len(trafos_ref))
for i in range(len(trafos_ref)):
nda_res = sitkh.read_transform_sitk(trafos_res[i]).GetParameters()
nda_ref = sitkh.read_transform_sitk(trafos_ref[i]).GetParameters()
nda_diff = np.linalg.norm(np.array(nda_res) - nda_ref)
self.assertAlmostEqual(nda_diff, 0, places=self.precision)
# Check rejected_slices.json
path_to_rejected_slices_res = os.path.join(
dir_res_mc, "rejected_slices.json")
self.assertEqual(
ph.file_exists(path_to_rejected_slices_res), True)
rejected_slices_res = ph.read_dictionary_from_json(
path_to_rejected_slices_res)
rejected_slices_ref = ph.read_dictionary_from_json(
path_to_rejected_slices_ref)
self.assertEqual(rejected_slices_res == rejected_slices_ref, True)
================================================
FILE: tests/case_study_rsfmri_test.py
================================================
##
# \file case_study_fetal_brain_test.py
# \brief Unit tests based on fetal brain case study
#
# \author Michael Ebner (michael.ebner@kcl.ac.uk)
# \date July 2019
import re
import os
import unittest
import numpy as np
import SimpleITK as sitk
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
from niftymic.definitions import DIR_TMP, DIR_TEST, REGEX_FILENAMES
import niftymic.application.rsfmri_estimate_motion as rsfmri_estimate_motion
import niftymic.application.rsfmri_reconstruct_volume_from_slices as rsfmri_reconstruct_volume_from_slices
class CaseStudyRestingStateFMRITest(unittest.TestCase):
def setUp(self):
self.precision = 7
self.dir_data = os.path.join(DIR_TEST, "case-studies", "rsfmri")
self.filename = os.path.join(
self.dir_data, "data", "1000AB97_bold_3componentsonly.nii.gz")
self.dir_output = os.path.join(DIR_TMP, "case-studies", "rsfmri")
self.suffix_mask = "_mask"
def test_estimate_motion(self):
filename = "SRR_reference.nii.gz"
output = os.path.join(self.dir_output, filename)
dir_reference = os.path.join(self.dir_data, "estimate_motion")
dir_reference_mc = os.path.join(dir_reference, "motion_correction")
path_to_reference = os.path.join(dir_reference, filename)
path_to_reference_mask = ph.append_to_filename(
os.path.join(dir_reference, filename), self.suffix_mask)
two_step_cycles = 1
iter_max = 5
exe = os.path.abspath(rsfmri_estimate_motion.__file__)
cmd_args = ["python %s" % exe]
cmd_args.append("--filename %s" % self.filename)
cmd_args.append("--filename-mask %s" % ph.append_to_filename(
self.filename, self.suffix_mask))
cmd_args.append("--dir-output %s" % self.dir_output)
cmd_args.append("--two-step-cycles %s" % two_step_cycles)
cmd_args.append("--iter-max %d" % iter_max)
cmd = (" ").join(cmd_args)
self.assertEqual(ph.execute_command(cmd), 0)
# Check SRR volume
res_sitk = sitkh.read_nifti_image_sitk(output)
ref_sitk = sitkh.read_nifti_image_sitk(path_to_reference)
diff_sitk = res_sitk - ref_sitk
error = np.linalg.norm(sitk.GetArrayFromImage(diff_sitk))
self.assertAlmostEqual(error, 0, places=self.precision)
# Check SRR mask volume
res_sitk = sitkh.read_nifti_image_sitk(
ph.append_to_filename(output, self.suffix_mask))
ref_sitk = sitkh.read_nifti_image_sitk(path_to_reference_mask)
diff_sitk = res_sitk - ref_sitk
error = np.linalg.norm(sitk.GetArrayFromImage(diff_sitk))
self.assertAlmostEqual(error, 0, places=self.precision)
# Check transforms
pattern = REGEX_FILENAMES + "[.]tfm"
p = re.compile(pattern)
dir_res_mc = os.path.join(self.dir_output, "motion_correction")
trafos_res = sorted(
[os.path.join(dir_res_mc, t)
for t in os.listdir(dir_res_mc) if p.match(t)])
trafos_ref = sorted(
[os.path.join(dir_reference_mc, t)
for t in os.listdir(dir_reference_mc) if p.match(t)])
self.assertEqual(len(trafos_res), len(trafos_ref))
for i in range(len(trafos_ref)):
nda_res = sitkh.read_transform_sitk(trafos_res[i]).GetParameters()
nda_ref = sitkh.read_transform_sitk(trafos_ref[i]).GetParameters()
nda_diff = np.linalg.norm(np.array(nda_res) - nda_ref)
self.assertAlmostEqual(nda_diff, 0, places=self.precision)
def test_reconstruct_volume_from_slices(self):
filename = "bold_s2v.nii.gz"
output = os.path.join(self.dir_output, filename)
dir_reference = os.path.join(
self.dir_data, "reconstruct_volume_from_slices")
dir_input_mc = os.path.join(
self.dir_data, "reconstruct_volume_from_slices", "motion_correction")
path_to_reference = os.path.join(dir_reference, filename)
iter_max = 3
alpha = 0.05
beta = -1
cmd_args = []
exe = os.path.abspath(rsfmri_reconstruct_volume_from_slices.__file__)
cmd_args = ["python %s" % exe]
cmd_args.append("--filename %s" % self.filename)
cmd_args.append("--filename-mask %s" % ph.append_to_filename(
self.filename, self.suffix_mask))
cmd_args.append("--dir-input-mc %s" % dir_input_mc)
cmd_args.append("--output %s" % output)
cmd_args.append("--iter-max %d" % iter_max)
cmd_args.append("--alpha %f" % alpha)
cmd_args.append("--beta %f" % beta)
cmd = (" ").join(cmd_args)
self.assertEqual(ph.execute_command(cmd), 0)
# Check whether identical reconstruction has been created
reconstruction_sitk = sitkh.read_sitk_vector_image(output)
reference_sitk = sitkh.read_sitk_vector_image(path_to_reference)
difference_sitk = reconstruction_sitk - reference_sitk
error = np.linalg.norm(sitk.GetArrayFromImage(difference_sitk))
self.assertAlmostEqual(error, 0, places=self.precision)
def test_reconstruct_volume_from_slices_temporal_reg(self):
filename = "bold_s2v_alpha0p05_beta0p5.nii.gz"
output = os.path.join(self.dir_output, filename)
dir_reference = os.path.join(
self.dir_data, "reconstruct_volume_from_slices")
dir_input_mc = os.path.join(
self.dir_data, "reconstruct_volume_from_slices", "motion_correction")
path_to_reference = os.path.join(dir_reference, filename)
iter_max = 3
alpha = 0.05
beta = 0.5
cmd_args = []
exe = os.path.abspath(rsfmri_reconstruct_volume_from_slices.__file__)
cmd_args = ["python %s" % exe]
cmd_args.append("--filename %s" % self.filename)
cmd_args.append("--filename-mask %s" % ph.append_to_filename(
self.filename, self.suffix_mask))
cmd_args.append("--dir-input-mc %s" % dir_input_mc)
cmd_args.append("--output %s" % output)
cmd_args.append("--iter-max %d" % iter_max)
cmd_args.append("--alpha %f" % alpha)
cmd_args.append("--beta %f" % beta)
# cmd_args.append("--reconstruction-type TVL2")
cmd = (" ").join(cmd_args)
self.assertEqual(ph.execute_command(cmd), 0)
# Check whether identical reconstruction has been created
reconstruction_sitk = sitkh.read_sitk_vector_image(output)
reference_sitk = sitkh.read_sitk_vector_image(path_to_reference)
difference_sitk = reconstruction_sitk - reference_sitk
error = np.linalg.norm(sitk.GetArrayFromImage(difference_sitk))
self.assertAlmostEqual(error, 0, places=self.precision)
================================================
FILE: tests/data_reader_test.py
================================================
##
# \file data_reader_test.py
# \brief Unit tests for data reader
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date January 2018
import os
import unittest
import numpy as np
import re
import SimpleITK as sitk
import pysitk.python_helper as ph
import niftymic.base.data_reader as dr
from niftymic.definitions import DIR_TMP, DIR_TEST
class DataReaderTest(unittest.TestCase):
def setUp(self):
self.precision = 7
self.dir_data = os.path.join(DIR_TEST, "case-studies", "fetal-brain")
self.filenames = [
os.path.join(self.dir_data,
"input-data",
"%s.nii.gz" % f)
for f in ["axial", "coronal", "sagittal"]]
self.dir_output = os.path.join(DIR_TMP, "case-studies", "fetal-brain")
self.suffix_mask = "_mask"
##
# Check that the same number of stacks (and slices therein) are read
# \date 2018-01-31 23:03:52+0000
#
# \param self The object
#
def test_read_transformations(self):
directory_motion_correction = os.path.join(
DIR_TEST,
"case-studies",
"fetal-brain",
"reconstruct_volume",
"motion_correction",
)
data_reader = dr.MultipleImagesReader(
file_paths=self.filenames,
dir_motion_correction=directory_motion_correction)
data_reader.read_data()
stacks = data_reader.get_data()
data_reader = dr.SliceTransformationDirectoryReader(
directory_motion_correction)
data_reader.read_data()
transformations_dic = data_reader.get_data()
self.assertEqual(len(stacks) - len(transformations_dic.keys()), 0)
for stack in stacks:
N_slices = stack.get_number_of_slices()
N_slices2 = len(transformations_dic[stack.get_filename()].keys())
self.assertEqual(N_slices - N_slices2, 0)
================================================
FILE: tests/image_similarity_evaluator_test.py
================================================
##
# \file image_similarity_evaluator_test.py
# \brief Test ImageSimilarityEvaluator class
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date February 2018
import os
import unittest
import numpy as np
import re
import SimpleITK as sitk
import pysitk.python_helper as ph
import niftymic.base.stack as st
import niftymic.validation.image_similarity_evaluator as ise
import niftymic.base.exceptions as exceptions
from niftymic.definitions import DIR_TMP, DIR_TEST
class ImageSimilarityEvaluatorTest(unittest.TestCase):
def setUp(self):
self.precision = 7
def test_compute_write_read_similarities(self):
paths_to_stacks = [
os.path.join(
DIR_TEST, "fetal_brain_%d.nii.gz" % d) for d in range(0, 3)
]
path_to_reference = os.path.join(
DIR_TEST, "FetalBrain_reconstruction_3stacks_myAlg.nii.gz")
reference = st.Stack.from_filename(
path_to_reference, extract_slices=False)
stacks = [
st.Stack.from_filename(p, ph.append_to_filename(p, "_mask"))
for p in paths_to_stacks
]
stacks = [s.get_resampled_stack(reference.sitk) for s in stacks]
residual_evaluator = ise.ImageSimilarityEvaluator(stacks, reference)
residual_evaluator.compute_similarities()
residual_evaluator.write_similarities(DIR_TMP)
similarities = residual_evaluator.get_similarities()
similarities1 = ise.ImageSimilarityEvaluator()
similarities1.read_similarities(DIR_TMP)
similarities1 = similarities1.get_similarities()
for m in residual_evaluator.get_measures():
rho_res = similarities[m]
rho_res1 = similarities1[m]
error = np.linalg.norm(rho_res - rho_res1)
self.assertAlmostEqual(error, 0, places=self.precision)
def test_results_not_created(self):
residual_evaluator = ise.ImageSimilarityEvaluator()
# Directory does not exist
self.assertRaises(
IOError, lambda:
residual_evaluator.read_similarities(
os.path.join(DIR_TMP, "whatevertestasdfsfasdasf")))
# Directory does exist but does not contain similarity result files
self.assertRaises(IOError, lambda:
residual_evaluator.read_similarities(DIR_TEST))
================================================
FILE: tests/installation_test.py
================================================
##
# \file installation_test.py
# \brief Class to test installation
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date October 2017
# Import libraries
import unittest
from nipype.testing import example_data
import niftymic.base.stack as st
import niftymic.registration.flirt as flirt
import niftymic.registration.niftyreg as niftyreg
import niftymic.utilities.brain_stripping as bs
class InstallationTest(unittest.TestCase):
def setUp(self):
self.accuracy = 10
self.path_to_fixed = example_data("segmentation0.nii.gz")
self.path_to_moving = example_data("segmentation1.nii.gz")
self.fixed = st.Stack.from_filename(self.path_to_fixed)
self.moving = st.Stack.from_filename(self.path_to_moving)
##
# Test whether FSL installation was successful
# \date 2017-10-26 15:02:44+0100
#
def test_fsl(self):
# Run flirt registration
registration_method = flirt.FLIRT(
fixed=self.fixed, moving=self.moving)
registration_method.run()
# Run BET brain stripping
brain_stripper = bs.BrainStripping.from_sitk_image(self.fixed.sitk)
brain_stripper.run()
##
# Test whether NiftyReg installation was successful
# \date 2017-10-26 15:08:59+0100
#
def test_niftyreg(self):
# Run reg_aladin registration
registration_method = niftyreg.RegAladin(
fixed=self.fixed, moving=self.moving)
registration_method.run()
# Run reg_f3d registration
registration_method = niftyreg.RegF3D(
fixed=self.fixed, moving=self.moving)
registration_method.run()
##
# Test whether ITK_NiftyMIC installation was successful
# \date 2017-10-26 15:12:26+0100
#
def test_itk_niftymic(self):
import itk
image_itk = itk.Image.D3.New()
filter_itk = itk.OrientedGaussianInterpolateImageFilter.ID3ID3.New()
================================================
FILE: tests/installation_test_fetal_brain_seg.py
================================================
##
# \file installation_test_fetal_brain_seg.py
# \brief Class to test installation of fetal_brain_seg
# (https://github.com/gift-surg/fetal_brain_seg)
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date March 2019
#
import os
import unittest
import pysitk.python_helper as ph
from niftymic.definitions import DIR_TMP, DIR_TEMPLATES
class InstallationTest(unittest.TestCase):
def setUp(self):
self.accuracy = 10
self.path_to_image = os.path.join(DIR_TEMPLATES, "STA23.nii.gz")
##
# Test whether fetal_brain_seg can be called
# \date 2019-03-24 14:14:18+0000
#
def test_fetal_brain_seg(self):
dir_output = os.path.join(DIR_TMP, "seg")
cmd_args = ["niftymic_segment_fetal_brains"]
cmd_args.append("--filenames %s" % self.path_to_image)
cmd_args.append("--dir-output %s" % dir_output)
cmd_args.append("--neuroimage-legacy-seg 1")
# cmd_args.append("--verbose 1")
cmd = " ".join(cmd_args)
flag = ph.execute_command(cmd)
================================================
FILE: tests/installation_test_monaifbs.py
================================================
##
# \file installation_test_monaifbs.py
# \brief Class to test installation of MONAIfbs
# (https://github.com/gift-surg/MONAIfbs)
#
# \author Marta Ranzini (marta.ranzini@kcl.ac.uk)
# \date December 2020
#
import os
import unittest
import pysitk.python_helper as ph
from niftymic.definitions import DIR_TMP, DIR_TEMPLATES
class InstallationTestMonaiFbs(unittest.TestCase):
def setUp(self):
self.accuracy = 10
self.path_to_image = os.path.join(DIR_TEMPLATES, "STA23.nii.gz")
##
# Test whether monaifbs can be called
# \date 2019-03-24 14:14:18+0000
#
def test_fetal_brain_seg(self):
dir_output = os.path.join(DIR_TMP, "seg")
cmd_args = ["niftymic_segment_fetal_brains"]
cmd_args.append("--filenames %s" % self.path_to_image)
cmd_args.append("--dir-output %s" % dir_output)
# cmd_args.append("--verbose 1")
cmd = " ".join(cmd_args)
flag = ph.execute_command(cmd)
================================================
FILE: tests/intensity_correction_test.py
================================================
# \file TestIntensityCorrection.py
##
# \brief Class containing unit tests for module IntensityCorrection
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date October 2016
import os
import unittest
import numpy as np
import SimpleITK as sitk
import pysitk.python_helper as ph
import niftymic.base.stack as st
import niftymic.utilities.intensity_correction as ic
from niftymic.definitions import DIR_TEST
class IntensityCorrectionTest(unittest.TestCase):
# Specify input data
dir_test_data = DIR_TEST
accuracy = 6
use_verbose = False
def setUp(self):
pass
def test_linear_intensity_correction(self):
# Create stack of Lena slices
shape_z = 15
# Original stack
nda_2D = ph.read_image(
os.path.join(self.dir_test_data, "2D_Lena_256.png"))
nda_3D = np.tile(nda_2D, (shape_z, 1, 1)).astype('double')
stack_sitk = sitk.GetImageFromArray(nda_3D)
stack = st.Stack.from_sitk_image(
image_sitk=stack_sitk,
filename="Lena",
slice_thickness=stack_sitk.GetSpacing()[-1],
)
# 1) Create linearly corrupted intensity stack
nda_3D_corruped = np.zeros_like(nda_3D)
for i in range(0, shape_z):
nda_3D_corruped[i, :, :] = nda_3D[i, :, :] / (i + 1.)
stack_corrupted_sitk = sitk.GetImageFromArray(nda_3D_corruped)
stack_corrupted = st.Stack.from_sitk_image(
image_sitk=stack_corrupted_sitk,
filename="stack_corrupted",
slice_thickness=stack_corrupted_sitk.GetSpacing()[-1],
)
# stack_corrupted.show_slices()
# sitkh.show_stacks([stack, stack_corrupted])
# Ground truth-parameter:
ic_values = np.zeros((shape_z, 1))
for i in range(0, shape_z):
ic_values[i, :] = (i + 1.)
intensity_correction = ic.IntensityCorrection(
stack=stack_corrupted,
reference=stack,
use_individual_slice_correction=True,
use_verbose=self.use_verbose)
intensity_correction.run_linear_intensity_correction()
ic_values_est = intensity_correction.get_intensity_correction_coefficients()
nda_diff = ic_values - ic_values_est
self.assertEqual(np.round(
np.linalg.norm(nda_diff), decimals=self.accuracy), 0)
def test_affine_intensity_correction(self):
# Create stack of Lena slices
shape_z = 15
# Original stack
nda_2D = ph.read_image(
os.path.join(self.dir_test_data, "2D_Lena_256.png"))
nda_3D = np.tile(nda_2D, (shape_z, 1, 1)).astype('double')
stack_sitk = sitk.GetImageFromArray(nda_3D)
stack = st.Stack.from_sitk_image(
image_sitk=stack_sitk,
filename="Lena",
slice_thickness=stack_sitk.GetSpacing()[-1],
)
# 1) Create linearly corrupted intensity stack
nda_3D_corruped = np.zeros_like(nda_3D)
for i in range(0, shape_z):
nda_3D_corruped[i, :, :] = (nda_3D[i, :, :] - 10 * i) / (i + 1.)
stack_corrupted_sitk = sitk.GetImageFromArray(nda_3D_corruped)
stack_corrupted = st.Stack.from_sitk_image(
image_sitk=stack_corrupted_sitk,
filename="stack_corrupted",
slice_thickness=stack_corrupted_sitk.GetSpacing()[-1],
)
# stack_corrupted.show_slices()
# sitkh.show_stacks([stack, stack_corrupted])
# Ground truth-parameter:
ic_values = np.zeros((shape_z, 2))
for i in range(0, shape_z):
ic_values[i, :] = (i + 1, 10 * i)
intensity_correction = ic.IntensityCorrection(
stack=stack_corrupted,
reference=stack,
use_individual_slice_correction=True,
use_verbose=self.use_verbose)
intensity_correction.run_affine_intensity_correction()
ic_values_est = intensity_correction.get_intensity_correction_coefficients()
nda_diff = ic_values - ic_values_est
self.assertEqual(np.round(
np.linalg.norm(nda_diff), decimals=self.accuracy), 0)
================================================
FILE: tests/intra_stack_registration_test.py
================================================
##
# \file intra_stack_registration_test.py
# \brief Class containing unit tests for module IntraStackRegistration
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date October 2016
# Import libraries
import SimpleITK as sitk
import itk
import numpy as np
import unittest
import sys
import os
from scipy.ndimage import imread
import pysitk.simple_itk_helper as sitkh
import pysitk.python_helper as ph
# Import modules
import niftymic.base.stack as st
import niftymic.registration.intra_stack_registration as inplanereg
from niftymic.definitions import DIR_TEST
def get_inplane_corrupted_stack(stack,
angle_z,
center_2D,
translation_2D,
scale=1,
intensity_scale=1,
intensity_bias=0,
debug=0,
random=False):
# Convert to 3D:
translation_3D = np.zeros(3)
translation_3D[0:-1] = translation_2D
center_3D = np.zeros(3)
center_3D[0:-1] = center_2D
# Transform to align physical coordinate system with stack-coordinate
# system
affine_centering_sitk = sitk.AffineTransform(3)
affine_centering_sitk.SetMatrix(stack.sitk.GetDirection())
affine_centering_sitk.SetTranslation(stack.sitk.GetOrigin())
# Corrupt first stack towards positive direction
if random:
angle_z_1 = -angle_z*np.random.rand(1)[0]
else:
angle_z_1 = -angle_z
in_plane_motion_sitk = sitk.Euler3DTransform()
in_plane_motion_sitk.SetRotation(0, 0, angle_z_1)
in_plane_motion_sitk.SetCenter(center_3D)
in_plane_motion_sitk.SetTranslation(translation_3D)
motion_sitk = sitkh.get_composite_sitk_affine_transform(
in_plane_motion_sitk, sitk.AffineTransform(
affine_centering_sitk.GetInverse()))
motion_sitk = sitkh.get_composite_sitk_affine_transform(
affine_centering_sitk, motion_sitk)
stack_corrupted_resampled_sitk = sitk.Resample(
stack.sitk, motion_sitk, sitk.sitkLinear)
stack_corrupted_resampled_sitk_mask = sitk.Resample(
stack.sitk_mask, motion_sitk, sitk.sitkLinear)
# Corrupt first stack towards negative direction
if random:
angle_z_2 = -angle_z*np.random.rand(1)[0]
else:
angle_z_2 = -angle_z
in_plane_motion_2_sitk = sitk.Euler3DTransform()
in_plane_motion_2_sitk.SetRotation(0, 0, angle_z_2)
in_plane_motion_2_sitk.SetCenter(center_3D)
in_plane_motion_2_sitk.SetTranslation(-translation_3D)
motion_2_sitk = sitkh.get_composite_sitk_affine_transform(
in_plane_motion_2_sitk, sitk.AffineTransform(
affine_centering_sitk.GetInverse()))
motion_2_sitk = sitkh.get_composite_sitk_affine_transform(
affine_centering_sitk, motion_2_sitk)
stack_corrupted_2_resampled_sitk = sitk.Resample(
stack.sitk, motion_2_sitk, sitk.sitkLinear)
stack_corrupted_2_resampled_sitk_mask = sitk.Resample(
stack.sitk_mask, motion_2_sitk, sitk.sitkLinear)
# Create stack based on those two corrupted stacks
nda = sitk.GetArrayFromImage(stack_corrupted_resampled_sitk)
nda_mask = sitk.GetArrayFromImage(stack_corrupted_resampled_sitk_mask)
nda_neg = sitk.GetArrayFromImage(stack_corrupted_2_resampled_sitk)
nda_neg_mask = sitk.GetArrayFromImage(
stack_corrupted_2_resampled_sitk_mask)
for i in range(0, stack.sitk.GetDepth(), 2):
nda[i, :, :] = nda_neg[i, :, :]
nda_mask[i, :, :] = nda_neg_mask[i, :, :]
stack_corrupted_sitk = sitk.GetImageFromArray(
(nda-intensity_bias)/intensity_scale)
stack_corrupted_sitk_mask = sitk.GetImageFromArray(nda_mask)
stack_corrupted_sitk.CopyInformation(stack.sitk)
stack_corrupted_sitk_mask.CopyInformation(stack.sitk_mask)
# Debug: Show corrupted stacks (before scaling)
if debug:
sitkh.show_sitk_image(
[stack.sitk,
stack_corrupted_resampled_sitk,
stack_corrupted_2_resampled_sitk,
stack_corrupted_sitk],
title=["original",
"corrupted_1",
"corrupted_2",
"corrupted_final_from_1_and_2"])
# Update in-plane scaling
spacing = np.array(stack.sitk.GetSpacing())
spacing[0:-1] /= scale
stack_corrupted_sitk.SetSpacing(spacing)
stack_corrupted_sitk_mask.SetSpacing(spacing)
# Create Stack object
stack_corrupted = st.Stack.from_sitk_image(
stack_corrupted_sitk, "stack_corrupted", stack_corrupted_sitk_mask)
# Debug: Show corrupted stacks (after scaling)
if debug:
stack_corrupted_resampled_sitk = sitk.Resample(
stack_corrupted.sitk, stack.sitk)
sitkh.show_sitk_image(
[stack.sitk,
stack_corrupted_resampled_sitk],
title=["original", "corrupted"])
return stack_corrupted, motion_sitk, motion_2_sitk
class IntraStackRegistrationTest(unittest.TestCase):
# Specify input data
dir_test_data = DIR_TEST
accuracy = 6
def setUp(self):
pass
##
# Test whether the function
# _get_initial_transforms_and_parameters_geometry_moments
# works.
# \date 2016-11-09 23:59:25+0000
#
# \param self The object
#
def test_initial_transform_computation_1(self):
# Create stack of slice with only a dot in the middle
shape_xy = 15
shape_z = 15
# Original stack
nda_3D = np.zeros((shape_z, shape_xy, shape_xy))
nda_3D[:, 0, 0] = 1
stack_sitk = sitk.GetImageFromArray(nda_3D)
stack = st.Stack.from_sitk_image(stack_sitk, "stack")
# Create 'motion corrupted stack', i.e. point moves diagonally with
# step one
nda_3D_corruped = np.zeros_like(nda_3D)
for i in range(0, shape_z):
nda_3D_corruped[i, i, i] = 1
stack_corrupted_sitk = sitk.GetImageFromArray(nda_3D_corruped)
stack_corrupted = st.Stack.from_sitk_image(
stack_corrupted_sitk, "stack_corrupted")
# stack_corrupted.show_slices()
# sitkh.show_stacks([stack, stack_corrupted])
# Ground truth-parameter: zero angle but translation = (1, 1) from one
# slice to the next
parameters = np.ones((shape_z, 3))
parameters[:, 0] = 0
for i in range(0, shape_z):
parameters[i, :] *= i
# 1) Get initial transform in case no reference is given
inplane_registration = inplanereg.IntraStackRegistration(
stack_corrupted)
inplane_registration.set_transform_initializer_type("moments")
# inplane_registration.set_transform_initializer_type("identity")
inplane_registration._run_registration_pipeline_initialization()
parameters_est = inplane_registration.get_parameters()
nda_diff = parameters - parameters_est
self.assertEqual(np.round(
np.linalg.norm(nda_diff), decimals=self.accuracy), 0)
# 2) Get initial transform in case reference is given
inplane_registration = inplanereg.IntraStackRegistration(
stack_corrupted, stack)
inplane_registration.set_transform_initializer_type("moments")
# inplane_registration.set_image_transform_reference_fit_term("gradient_magnitude")
# inplane_registration.set_transform_initializer_type("identity")
inplane_registration._run_registration_pipeline_initialization()
inplane_registration._apply_motion_correction()
# stack_corrected = inplane_registration.get_corrected_stack()
# sitkh.show_stacks([stack, stack_corrupted, stack_corrected.get_resampled_stack_from_slices(interpolator="Linear")])
parameters_est = inplane_registration.get_parameters()
nda_diff = parameters - parameters_est
# print(nda_diff)
# print(parameters)
self.assertEqual(np.round(
np.linalg.norm(nda_diff), decimals=self.accuracy), 0)
##
# Test whether the function
# _get_initial_transforms_and_parameters_geometry_moments
# works.
# \date 2016-11-09 23:59:25+0000
#
# \param self The object
#
def test_initial_transform_computation_2(self):
# Create stack of slice with a pyramid in the middle
shape_xy = 250
shape_z = 15
intensity_mask = 10
length = 50
nda_2D = ph.read_image(os.path.join(
DIR_TEST, "2D_Pyramid_Midpoint_" + str(length) + ".png"))
# Original stack
nda_3D = np.zeros((shape_z, shape_xy, shape_xy))
i0 = (shape_xy - length) / 2
for i in range(0, shape_z):
nda_3D[i, i0:-i0, i0:-i0] = nda_2D
stack_sitk = sitk.GetImageFromArray(nda_3D)
stack = st.Stack.from_sitk_image(stack_sitk, "stack")
# Create 'motion corrupted stack', i.e. in-plane translation, and
# associated ground-truth parameters
parameters = np.zeros((shape_z, 3))
parameters[:, 0] = 0
nda_3D_corrupted = np.zeros_like(nda_3D)
nda_3D_corrupted[0, :, :] = nda_3D[0, :, :]
for i in range(1, shape_z):
# Get random translation
[tx, ty] = np.random.randint(0, 50, 2)
# Get image based on corruption
inew = i0 + tx
jnew = i0 + ty
nda_3D_corrupted[i, inew:, jnew:] = \
nda_3D[i, i0:2*i0+length-tx, i0:2*i0+length-ty]
# Get ground-truth parameters
parameters[i, 1] = ty
parameters[i, 2] = tx
stack_corrupted_sitk = sitk.GetImageFromArray(nda_3D_corrupted)
stack_corrupted = st.Stack.from_sitk_image(
stack_corrupted_sitk, "stack_corrupted")
# stack_corrupted.show_slices()
# sitkh.show_stacks([stack, stack_corrupted])
# 1) Get initial transform in case no reference is given
inplane_registration = inplanereg.IntraStackRegistration(
stack_corrupted)
inplane_registration.set_transform_initializer_type("moments")
# inplane_registration.set_transform_initializer_type("identity")
# inplane_registration.set_transform_initializer_type("geometry")
inplane_registration._run_registration_pipeline_initialization()
# Debug:
# inplane_registration._apply_motion_correction()
# stack_corrected = inplane_registration.get_corrected_stack()
# sitkh.show_stacks(
# [stack,
# stack_corrupted,
# stack_corrected.get_resampled_stack_from_slices(
# interpolator="Linear", filename="stack_corrected")])
parameters_est = inplane_registration.get_parameters()
nda_diff = parameters - parameters_est
self.assertEqual(np.round(
np.linalg.norm(nda_diff), decimals=self.accuracy), 0)
# 2) Get initial transform in case reference is given
inplane_registration = inplanereg.IntraStackRegistration(
stack_corrupted, stack)
inplane_registration.set_transform_initializer_type("moments")
# inplane_registration.set_transform_initializer_type("identity")
inplane_registration._run_registration_pipeline_initialization()
# Debug:
# inplane_registration._apply_motion_correction()
# stack_corrected = inplane_registration.get_corrected_stack()
# sitkh.show_stacks(
# [stack,
# stack_corrupted,
# stack_corrected.get_resampled_stack_from_slices(
# interpolator="Linear", filename="stack_corrected")])
parameters_est = inplane_registration.get_parameters()
nda_diff = parameters - parameters_est
# print(nda_diff)
# print(parameters)
self.assertEqual(np.round(
np.linalg.norm(nda_diff), decimals=self.accuracy), 0)
##
# Test whether the function
# _get_initial_transforms_and_parameters_geometry_moments
# works.
# \date 2016-11-09 23:59:25+0000
#
# \param self The object
#
def test_initial_transform_computation_3(self):
# Create stack of slice with a pyramid in the middle
shape_xy = 250
shape_z = 15
intensity_mask = 10
length = 50
nda_2D = ph.read_image(os.path.join(
DIR_TEST, "2D_Pyramid_Midpoint_" + str(length) + ".png"))
# Original stack
nda_3D = np.zeros((shape_z, shape_xy, shape_xy))
i0 = (shape_xy - length) / 2
for i in range(0, shape_z):
nda_3D[i, i0:-i0, i0:-i0] = nda_2D
nda_3D_mask = np.array(nda_3D).astype(np.uint8)
nda_3D_mask[np.where(nda_3D_mask <= intensity_mask)] = 0
nda_3D_mask[np.where(nda_3D_mask > intensity_mask)] = 1
# Add additional weight s.t. initialization without mask fails
for i in range(0, shape_z):
nda_3D[i, -i0:, -i0:] = 10
stack_sitk = sitk.GetImageFromArray(nda_3D)
stack_sitk_mask = sitk.GetImageFromArray(nda_3D_mask)
stack = st.Stack.from_sitk_image(stack_sitk, "stack", stack_sitk_mask)
# Create 'motion corrupted stack', i.e. in-plane translation, and
# associated ground-truth parameters
parameters = np.zeros((shape_z, 3))
parameters[:, 0] = 0
nda_3D_corrupted = np.zeros_like(nda_3D)
nda_3D_corrupted[0, :, :] = nda_3D[0, :, :]
nda_3D_corrupted_mask = np.zeros_like(nda_3D_mask)
nda_3D_corrupted_mask[0, :, :] = nda_3D_mask[0, :, :]
for i in range(1, shape_z):
# Get random translation
[tx, ty] = np.random.randint(0, 50, 2)
# Get image based on corruption
inew = i0 + tx
jnew = i0 + ty
nda_3D_corrupted[i, inew:, jnew:] = \
nda_3D[i, i0:2*i0+length-tx, i0:2*i0+length-ty]
nda_3D_corrupted_mask[i, inew:, jnew:] = \
nda_3D_mask[i, i0:2*i0+length-tx, i0:2*i0+length-ty]
# Get ground-truth parameters
parameters[i, 1] = ty
parameters[i, 2] = tx
# nda_3D_corrupted = np.zeros_like(nda_3D)
# nda_3D_corrupted[0, i0:-i0, i0:-i0] = nda_2D
# for i in range(1, shape_z):
# # Get random translation
# [tx, ty] = np.random.randint(0, 50, 2)
# # Get image based on corruption
# inew = i0 + tx
# jnew = i0 + ty
# nda_3D_corrupted[i, inew:inew+length, jnew:jnew+length] = nda_2D
# # Get ground-truth parameters
# parameters[i, 1] = ty
# parameters[i, 2] = tx
stack_corrupted_sitk = sitk.GetImageFromArray(nda_3D_corrupted)
stack_corrupted_sitk_mask = sitk.GetImageFromArray(
nda_3D_corrupted_mask)
stack_corrupted = st.Stack.from_sitk_image(
stack_corrupted_sitk, "stack_corrupted", stack_corrupted_sitk_mask)
# stack_corrupted.show(1)
# stack_corrupted.show_slices()
# sitkh.show_stacks([stack, stack_corrupted],
# segmentation=stack)
# 1) Get initial transform in case no reference is given
inplane_registration = inplanereg.IntraStackRegistration(
stack_corrupted,
use_stack_mask=True,
)
inplane_registration.set_transform_initializer_type("moments")
# inplane_registration.set_transform_initializer_type("identity")
# inplane_registration.set_transform_initializer_type("geometry")
inplane_registration._run_registration_pipeline_initialization()
# Debug:
# inplane_registration._apply_motion_correction()
# stack_corrected = inplane_registration.get_corrected_stack()
# sitkh.show_stacks(
# [stack,
# stack_corrupted,
# stack_corrected.get_resampled_stack_from_slices(
# interpolator="Linear", filename="stack_corrected")])
parameters_est = inplane_registration.get_parameters()
nda_diff = parameters - parameters_est
self.assertEqual(np.round(
np.linalg.norm(nda_diff), decimals=self.accuracy), 0)
# 2) Get initial transform in case reference is given
inplane_registration = inplanereg.IntraStackRegistration(
stack_corrupted, stack)
inplane_registration.set_transform_initializer_type("moments")
# inplane_registration.set_transform_initializer_type("identity")
inplane_registration.use_reference_mask(True)
inplane_registration.use_stack_mask_reference_fit_term(True)
inplane_registration._run_registration_pipeline_initialization()
# Debug:
# inplane_registration._apply_motion_correction()
# stack_corrected = inplane_registration.get_corrected_stack()
# sitkh.show_stacks(
# [stack,
# stack_corrupted,
# stack_corrected.get_resampled_stack_from_slices(
# interpolator="Linear", filename="stack_corrected")])
parameters_est = inplane_registration.get_parameters()
nda_diff = parameters - parameters_est
# print(nda_diff)
# print(parameters)
self.assertEqual(np.round(
np.linalg.norm(nda_diff), decimals=self.accuracy), 0)
##
# Test that initial intensity coefficients are computed
# correctly
# \date 2016-11-10 04:28:06+0000
#
# \param self The object
#
def test_initial_intensity_coefficient_computation(self):
# Create stack
shape_z = 15
nda_2D = imread(self.dir_test_data + "2D_Lena_256.png", flatten=True)
nda_3D = np.tile(nda_2D, (shape_z, 1, 1)).astype('double')
stack_sitk = sitk.GetImageFromArray(nda_3D)
stack = st.Stack.from_sitk_image(stack_sitk, "Lena")
# 1) Create linearly corrupted intensity stack
nda_3D_corruped = np.zeros_like(nda_3D)
for i in range(0, shape_z):
nda_3D_corruped[i, :, :] = nda_3D[i, :, :]/(i+1.)
stack_corrupted_sitk = sitk.GetImageFromArray(nda_3D_corruped)
stack_corrupted = st.Stack.from_sitk_image(
stack_corrupted_sitk, "stack_corrupted")
# stack_corrupted.show_slices()
# sitkh.show_stacks([stack, stack_corrupted])
# Ground truth-parameter: zero angle but translation = (1, 1) from one
# slice to the next
parameters = np.zeros((shape_z, 4))
parameters[:, 0] = 0
for i in range(0, shape_z):
parameters[i, 3:] = 1*(i+1.) # intensity
# Get initial transform in case no reference is given
inplane_registration = inplanereg.IntraStackRegistration(
stack_corrupted, stack)
# inplane_registration.set_transform_initializer_type("moments")
inplane_registration.set_intensity_correction_type_slice_neighbour_fit(
"linear")
inplane_registration.set_intensity_correction_initializer_type(
"linear")
inplane_registration._run_registration_pipeline_initialization()
parameters_est = inplane_registration.get_parameters()
nda_diff = parameters - parameters_est
self.assertEqual(np.round(
np.linalg.norm(nda_diff), decimals=self.accuracy), 0)
# 2) Create affinely corrupted intensity stack
# HINT: In case of individual slice correction is active!!
nda_3D_corruped = np.zeros_like(nda_3D)
for i in range(0, shape_z):
nda_3D_corruped[i, :, :] = (nda_3D[i, :, :]-10*i)/(i+1.)
stack_corrupted_sitk = sitk.GetImageFromArray(nda_3D_corruped)
stack_corrupted = st.Stack.from_sitk_image(
stack_corrupted_sitk, "stack_corrupted")
# stack_corrupted.show_slices()
# sitkh.show_stacks([stack, stack_corrupted])
# Ground truth-parameter: zero angle but translation = (1, 1) from one
# slice to the next
parameters = np.zeros((shape_z, 5))
parameters[:, 0] = 0
for i in range(0, shape_z):
parameters[i, 3:] = (i+1, 10*i) # intensity
# Get initial transform in case no reference is given
inplane_registration = inplanereg.IntraStackRegistration(
stack_corrupted, stack)
# inplane_registration.set_transform_initializer_type("moments")
inplane_registration.set_intensity_correction_type_slice_neighbour_fit(
"affine")
inplane_registration.set_intensity_correction_initializer_type(
"affine")
inplane_registration._run_registration_pipeline_initialization()
parameters_est = inplane_registration.get_parameters()
nda_diff = parameters - parameters_est
self.assertEqual(np.round(
np.linalg.norm(nda_diff), decimals=self.accuracy), 0)
##
# Verify that in-plane rigid registration works
# \date 2016-11-02 21:56:19+0000
#
# Verify that in-plane rigid registration works, i.e. test
# 1) registration parameters are close to ground truth (up to zero dp)
# 2) affine transformations for each slice correctly describes the
# registration
#
# \param self The object
#
def test_inplane_rigid_alignment_to_neighbour(self):
filename_stack = "fetal_brain_0"
# filename_recon = "FetalBrain_reconstruction_3stacks_myAlg"
# stack_sitk = sitk.ReadImage(self.dir_test_data + filename_stack + ".nii.gz")
# recon_sitk = sitk.ReadImage(self.dir_test_data + filename_recon + ".nii.gz")
# recon_resampled_sitk = sitk.Resample(recon_sitk, stack_sitk)
# stack = st.Stack.from_sitk_image(recon_resampled_sitk, "original")
stack = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename_stack + ".nii.gz"),
os.path.join(self.dir_test_data, filename_stack + "_mask.nii.gz")
)
nda = sitk.GetArrayFromImage(stack.sitk)
nda_mask = sitk.GetArrayFromImage(stack.sitk_mask)
i = 5
nda_slice = np.array(nda[i, :, :])
nda_mask_slice = np.array(nda_mask[i, :, :])
for i in range(0, nda.shape[0]):
nda[i, :, :] = nda_slice
nda_mask[i, :, :] = nda_mask_slice
stack_sitk = sitk.GetImageFromArray(nda)
stack_sitk_mask = sitk.GetImageFromArray(nda_mask)
stack_sitk.CopyInformation(stack.sitk)
stack_sitk_mask.CopyInformation(stack.sitk_mask)
stack = st.Stack.from_sitk_image(
stack_sitk, stack.get_filename(), stack_sitk_mask)
# Create in-plane motion corruption
angle_z = 0.1
center_2D = (0, 0)
translation_2D = np.array([1, -2])
# Get corrupted stack and corresponding motions
stack_corrupted, motion_sitk, motion_2_sitk = get_inplane_corrupted_stack(
stack, angle_z, center_2D, translation_2D, random=True)
# stack.show(1)
# stack_corrupted.show(1)
# Perform in-plane rigid registration
inplane_registration = inplanereg.IntraStackRegistration(
stack_corrupted, stack)
# inplane_registration = inplanereg.IntraStackRegistration(stack_corrupted)
inplane_registration.set_transform_initializer_type("moments")
inplane_registration.set_optimizer_iter_max(20)
inplane_registration.set_alpha_neighbour(1)
inplane_registration.set_alpha_reference(2)
# inplane_registration.use_parameter_normalization(True)
inplane_registration.use_stack_mask(1)
inplane_registration.use_reference_mask(0)
# inplane_registration.set_optimizer_loss("linear") # linear, soft_l1,
# huber
inplane_registration.set_optimizer_method("trf") # trf, lm, dogbox
# inplane_registration._run_registration_pipeline_initialization()
# inplane_registration._apply_motion_correction()
inplane_registration.use_verbose(True)
inplane_registration.run()
inplane_registration.print_statistics()
stack_registered = inplane_registration.get_corrected_stack()
parameters = inplane_registration.get_parameters()
sitkh.show_stacks([stack, stack_corrupted, stack_registered.get_resampled_stack_from_slices(
interpolator="Linear")])
# self.assertEqual(np.round(
# np.linalg.norm(nda_diff)
# , decimals = self.accuracy), 0)
# 2) Test slice transforms
slice_transforms_sitk = inplane_registration.get_slice_transforms_sitk()
stack_tmp = st.Stack.from_stack(stack_corrupted)
stack_tmp.update_motion_correction_of_slices(slice_transforms_sitk)
stack_diff_sitk = stack_tmp.get_resampled_stack_from_slices(
resampling_grid=stack.sitk).sitk - stack_registered.get_resampled_stack_from_slices(resampling_grid=stack.sitk).sitk
stack_diff_nda = sitk.GetArrayFromImage(stack_diff_sitk)
self.assertEqual(np.round(
np.linalg.norm(stack_diff_nda), decimals=8), 0)
def test_inplane_rigid_alignment_to_reference(self):
filename_stack = "fetal_brain_0"
# filename_recon = "FetalBrain_reconstruction_3stacks_myAlg"
# stack_sitk = sitk.ReadImage(self.dir_test_data + filename_stack + ".nii.gz")
# recon_sitk = sitk.ReadImage(self.dir_test_data + filename_recon + ".nii.gz")
# recon_resampled_sitk = sitk.Resample(recon_sitk, stack_sitk)
# stack = st.Stack.from_sitk_image(recon_resampled_sitk, "original")
stack = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename_stack + ".nii.gz"),
os.path.join(self.dir_test_data, filename_stack + "_mask.nii.gz")
)
# Create in-plane motion corruption
angle_z = 0.1
center_2D = (0, 0)
translation_2D = np.array([1, -2])
# Get corrupted stack and corresponding motions
stack_corrupted, motion_sitk, motion_2_sitk = get_inplane_corrupted_stack(
stack, angle_z, center_2D, translation_2D)
# stack.show(1)
# stack_corrupted.show(1)
# Perform in-plane rigid registration
inplane_registration = inplanereg.IntraStackRegistration(
stack_corrupted, stack)
# inplane_registration = inplanereg.IntraStackRegistration(stack_corrupted)
inplane_registration.set_transform_initializer_type("moments")
inplane_registration.set_optimizer_iter_max(10)
inplane_registration.set_alpha_neighbour(0)
inplane_registration.set_alpha_parameter(0)
inplane_registration.use_stack_mask(1)
inplane_registration.use_reference_mask(0)
inplane_registration.set_optimizer_loss("linear")
# inplane_registration.set_optimizer_method("trf")
# inplane_registration._run_registration_pipeline_initialization()
# inplane_registration._apply_motion_correction()
# inplane_registration.use_verbose(True)
inplane_registration.run()
inplane_registration.print_statistics()
stack_registered = inplane_registration.get_corrected_stack()
parameters = inplane_registration.get_parameters()
sitkh.show_stacks([stack, stack_corrupted, stack_registered.get_resampled_stack_from_slices(
interpolator="Linear", resampling_grid=stack.sitk)])
print(parameters)
# self.assertEqual(np.round(
# np.linalg.norm(nda_diff)
# , decimals = self.accuracy), 0)
# 2) Test slice transforms
slice_transforms_sitk = inplane_registration.get_slice_transforms_sitk()
stack_tmp = st.Stack.from_stack(stack_corrupted)
stack_tmp.update_motion_correction_of_slices(slice_transforms_sitk)
stack_diff_sitk = stack_tmp.get_resampled_stack_from_slices(
resampling_grid=stack.sitk).sitk - stack_registered.get_resampled_stack_from_slices(resampling_grid=stack.sitk).sitk
stack_diff_nda = sitk.GetArrayFromImage(stack_diff_sitk)
self.assertEqual(np.round(
np.linalg.norm(stack_diff_nda), decimals=8), 0)
def test_inplane_rigid_alignment_to_reference_with_intensity_correction_linear(self):
filename_stack = "fetal_brain_0"
filename_recon = "FetalBrain_reconstruction_3stacks_myAlg"
stack_sitk = sitk.ReadImage(
self.dir_test_data + filename_stack + ".nii.gz")
recon_sitk = sitk.ReadImage(
self.dir_test_data + filename_recon + ".nii.gz")
recon_resampled_sitk = sitk.Resample(recon_sitk, stack_sitk)
stack = st.Stack.from_sitk_image(recon_resampled_sitk, "original")
# Create in-plane motion corruption
angle_z = 0.05
center_2D = (0, 0)
translation_2D = np.array([1, -2])
intensity_scale = 10
intensity_bias = 0
# Get corrupted stack and corresponding motions
stack_corrupted, motion_sitk, motion_2_sitk = get_inplane_corrupted_stack(
stack, angle_z, center_2D, translation_2D, intensity_scale=intensity_scale, intensity_bias=intensity_bias)
# Perform in-plane rigid registration
inplane_registration = inplanereg.IntraStackRegistration(
stack_corrupted, stack)
# inplane_registration = inplanereg.IntraStackRegistration(stack_corrupted)
inplane_registration.set_transform_initializer_type("moments")
inplane_registration.set_transform_type("rigid")
inplane_registration.set_intensity_correction_initializer_type(
"linear")
inplane_registration.set_intensity_correction_type_slice_neighbour_fit(
"linear")
inplane_registration.set_intensity_correction_type_reference_fit(
"linear")
inplane_registration.set_optimizer_loss(
"linear") # linear, soft_l1, huber
inplane_registration.use_parameter_normalization(True)
inplane_registration.use_verbose(True)
inplane_registration.set_alpha_reference(1)
inplane_registration.set_alpha_neighbour(0)
inplane_registration.set_alpha_parameter(0)
inplane_registration.set_optimizer_iter_max(30)
inplane_registration.use_verbose(True)
inplane_registration.run()
inplane_registration.print_statistics()
stack_registered = inplane_registration.get_corrected_stack()
parameters = inplane_registration.get_parameters()
sitkh.show_stacks([stack, stack_corrupted, stack_registered.get_resampled_stack_from_slices(
resampling_grid=None, interpolator="Linear")])
print("Final parameters:")
print(parameters)
self.assertEqual(np.round(
np.linalg.norm(parameters[:, -1] - intensity_scale), decimals=0), 0)
# 2) Test slice transforms
slice_transforms_sitk = inplane_registration.get_slice_transforms_sitk()
stack_tmp = st.Stack.from_stack(stack_corrupted)
stack_tmp.update_motion_correction_of_slices(slice_transforms_sitk)
stack_diff_sitk = stack_tmp.get_resampled_stack_from_slices(
resampling_grid=stack.sitk).sitk - stack_registered.get_resampled_stack_from_slices(resampling_grid=stack.sitk).sitk
stack_diff_nda = sitk.GetArrayFromImage(stack_diff_sitk)
self.assertEqual(np.round(
np.linalg.norm(stack_diff_nda), decimals=8), 0)
##
# \bug There is some issue with slice based and uniform intensity correction.
# Unit test needs to be fixed at some point
# \date 2017-07-12 12:40:01+0100
#
# \param self The object
#
def test_inplane_rigid_alignment_to_reference_with_intensity_correction_affine(self):
filename_stack = "fetal_brain_0"
filename_recon = "FetalBrain_reconstruction_3stacks_myAlg"
stack_sitk = sitk.ReadImage(
self.dir_test_data + filename_stack + ".nii.gz")
recon_sitk = sitk.ReadImage(
self.dir_test_data + filename_recon + ".nii.gz")
recon_resampled_sitk = sitk.Resample(recon_sitk, stack_sitk)
stack = st.Stack.from_sitk_image(recon_resampled_sitk, "original")
# Create in-plane motion corruption
angle_z = 0.01
center_2D = (0, 0)
translation_2D = np.array([1, 0])
intensity_scale = 5
intensity_bias = 5
# Get corrupted stack and corresponding motions
stack_corrupted, motion_sitk, motion_2_sitk = get_inplane_corrupted_stack(
stack, angle_z, center_2D, translation_2D, intensity_scale=intensity_scale, intensity_bias=intensity_bias)
# Perform in-plane rigid registration
inplane_registration = inplanereg.IntraStackRegistration(
stack_corrupted, stack)
# inplane_registration = inplanereg.IntraStackRegistration(stack_corrupted)
inplane_registration.set_transform_type("rigid")
inplane_registration.set_transform_initializer_type("identity")
inplane_registration.set_optimizer_loss("linear")
inplane_registration.set_intensity_correction_initializer_type(
"affine")
inplane_registration.set_intensity_correction_type_slice_neighbour_fit(
"affine")
inplane_registration.use_parameter_normalization(True)
inplane_registration.use_verbose(True)
inplane_registration.use_stack_mask(True)
inplane_registration.set_prior_intensity_coefficients(
(intensity_scale-0.4, intensity_bias+0.7))
inplane_registration.set_alpha_reference(1)
inplane_registration.set_alpha_neighbour(1)
inplane_registration.set_alpha_parameter(1e3)
inplane_registration.set_optimizer_iter_max(15)
inplane_registration.use_verbose(True)
inplane_registration.run()
inplane_registration.print_statistics()
stack_registered = inplane_registration.get_corrected_stack()
parameters = inplane_registration.get_parameters()
sitkh.show_stacks([stack, stack_corrupted, stack_registered.get_resampled_stack_from_slices(
resampling_grid=None, interpolator="Linear")])
self.assertEqual(np.round(
np.linalg.norm(parameters[:, -2:] - np.array([intensity_scale, intensity_bias])), decimals=0), 0)
# 2) Test slice transforms
slice_transforms_sitk = inplane_registration.get_slice_transforms_sitk()
stack_tmp = st.Stack.from_stack(stack_corrupted)
stack_tmp.update_motion_correction_of_slices(slice_transforms_sitk)
stack_diff_sitk = stack_tmp.get_resampled_stack_from_slices(
resampling_grid=stack.sitk).sitk - stack_registered.get_resampled_stack_from_slices(resampling_grid=stack.sitk).sitk
stack_diff_nda = sitk.GetArrayFromImage(stack_diff_sitk)
self.assertEqual(np.round(
np.linalg.norm(stack_diff_nda), decimals=8), 0)
def test_inplane_similarity_alignment_to_reference(self):
filename_stack = "fetal_brain_0"
# filename_stack = "3D_SheppLoganPhantom_64"
stack = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename_stack + ".nii.gz"),
os.path.join(self.dir_test_data, filename_stack + "_mask.nii.gz")
)
# stack.show(1)
nda = sitk.GetArrayFromImage(stack.sitk)
nda_mask = sitk.GetArrayFromImage(stack.sitk_mask)
i = 5
nda_slice = np.array(nda[i, :, :])
nda_mask_slice = np.array(nda_mask[i, :, :])
for i in range(0, nda.shape[0]):
nda[i, :, :] = nda_slice
nda_mask[i, :, :] = nda_mask_slice
stack_sitk = sitk.GetImageFromArray(nda)
stack_sitk_mask = sitk.GetImageFromArray(nda_mask)
stack_sitk.CopyInformation(stack.sitk)
stack_sitk_mask.CopyInformation(stack.sitk_mask)
stack = st.Stack.from_sitk_image(
stack_sitk, stack.get_filename(), stack_sitk_mask)
# Create in-plane motion corruption
scale = 1.2
angle_z = 0.05
center_2D = (0, 0)
# translation_2D = np.array([0,0])
translation_2D = np.array([1, -1])
intensity_scale = 10
intensity_bias = 50
# Get corrupted stack and corresponding motions
stack_corrupted, motion_sitk, motion_2_sitk = get_inplane_corrupted_stack(
stack, angle_z, center_2D, translation_2D, scale=scale, intensity_scale=intensity_scale, intensity_bias=intensity_bias, debug=0)
# stack_corrupted.show(1)
# stack.show(1)
# Perform in-plane rigid registrations
inplane_registration = inplanereg.IntraStackRegistration(
stack=stack_corrupted, reference=stack)
# inplane_registration = inplanereg.IntraStackRegistration(stack_corrupted)
inplane_registration.set_transform_initializer_type("geometry")
# inplane_registration.set_transform_initializer_type("identity")
inplane_registration.set_intensity_correction_initializer_type(
"affine")
inplane_registration.set_transform_type("similarity")
inplane_registration.set_interpolator("Linear")
inplane_registration.set_optimizer_loss("linear")
# inplane_registration.use_reference_mask(True)
inplane_registration.use_stack_mask(True)
inplane_registration.use_parameter_normalization(True)
inplane_registration.set_prior_scale(1/scale)
inplane_registration.set_prior_intensity_coefficients(
(intensity_scale, intensity_bias))
inplane_registration.set_intensity_correction_type_slice_neighbour_fit(
"affine")
inplane_registration.set_intensity_correction_type_reference_fit(
"affine")
inplane_registration.use_verbose(True)
inplane_registration.set_alpha_reference(1)
inplane_registration.set_alpha_neighbour(0)
inplane_registration.set_alpha_parameter(1e10)
inplane_registration.set_optimizer_iter_max(20)
inplane_registration.use_verbose(True)
inplane_registration.run()
inplane_registration.print_statistics()
# inplane_registration._run_registration_pipeline_initialization()
# inplane_registration._apply_motion_correction()
stack_registered = inplane_registration.get_corrected_stack()
parameters = inplane_registration.get_parameters()
sitkh.show_sitk_image([stack.sitk, stack_corrupted.get_resampled_stack_from_slices(interpolator="Linear", resampling_grid=stack.sitk).sitk,
stack_registered.get_resampled_stack_from_slices(interpolator="Linear", resampling_grid=stack.sitk).sitk], label=["original", "corrupted", "recovered"])
# self.assertEqual(np.round(
# np.linalg.norm(nda_diff)
# , decimals = self.accuracy), 0)
# 2) Test slice transforms
slice_transforms_sitk = inplane_registration.get_slice_transforms_sitk()
stack_tmp = st.Stack.from_stack(stack_corrupted)
stack_tmp.update_motion_correction_of_slices(slice_transforms_sitk)
stack_diff_sitk = stack_tmp.get_resampled_stack_from_slices(
resampling_grid=stack.sitk).sitk - stack_registered.get_resampled_stack_from_slices(resampling_grid=stack.sitk).sitk
stack_diff_nda = sitk.GetArrayFromImage(stack_diff_sitk)
self.assertEqual(np.round(
np.linalg.norm(stack_diff_nda), decimals=8), 0)
def test_inplane_rigid_alignment_to_reference_multimodal(self):
filename_stack = "fetal_brain_0"
filename_recon = "FetalBrain_reconstruction_3stacks_myAlg"
stack_tmp = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename_stack + ".nii.gz"),
os.path.join(self.dir_test_data, filename_stack + "_mask.nii.gz")
)
recon = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename_recon)
)
recon_sitk = recon.get_resampled_stack_from_slices(
resampling_grid=stack_tmp.sitk, interpolator="Linear").sitk
stack = st.Stack.from_sitk_image(
recon_sitk, "original", stack_tmp.sitk_mask)
# recon_resampled_sitk = sitk.Resample(recon_sitk, stack_sitk)
# stack = st.Stack.from_sitk_image(recon_resampled_sitk, "original")
# Create in-plane motion corruption
scale = 1.05
angle_z = 0.05
center_2D = (0, 0)
translation_2D = np.array([1, -2])
intensity_scale = 1
intensity_bias = 0
# Get corrupted stack and corresponding motions
stack_corrupted, motion_sitk, motion_2_sitk = get_inplane_corrupted_stack(
stack, angle_z, center_2D, translation_2D, intensity_scale=intensity_scale, scale=scale, intensity_bias=intensity_bias)
# stack_corrupted.show(1)
# stack.show(1)
# Perform in-plane rigid registration
inplane_registration = inplanereg.IntraStackRegistration(
stack_corrupted, stack)
# inplane_registration = inplanereg.IntraStackRegistration(stack_corrupted)
# inplane_registration.set_image_transform_reference_fit_term("gradient_magnitude")
inplane_registration.set_image_transform_reference_fit_term(
"partial_derivative")
inplane_registration.set_transform_initializer_type("moments")
# inplane_registration.set_transform_type("similarity")
inplane_registration.set_intensity_correction_initializer_type(None)
inplane_registration.set_intensity_correction_type_slice_neighbour_fit(
None)
inplane_registration.set_intensity_correction_type_reference_fit(None)
inplane_registration.use_parameter_normalization(True)
inplane_registration.use_verbose(True)
inplane_registration.set_optimizer_loss(
"linear") # linear, soft_l1, huber
inplane_registration.set_alpha_reference(100)
inplane_registration.set_alpha_neighbour(0)
inplane_registration.set_alpha_parameter(1)
# inplane_registration.use_stack_mask(True)
# inplane_registration.use_reference_mask(True)
inplane_registration.set_optimizer_iter_max(10)
inplane_registration.run()
inplane_registration.print_statistics()
stack_registered = inplane_registration.get_corrected_stack()
parameters = inplane_registration.get_parameters()
sitkh.show_stacks([stack, stack_corrupted, stack_registered.get_resampled_stack_from_slices(
resampling_grid=None, interpolator="Linear")])
# print("Final parameters:")
# print(parameters)
# self.assertEqual(np.round(
# np.linalg.norm(parameters[:,-1] - intensity_scale)
# , decimals = 0), 0)
# 2) Test slice transforms
slice_transforms_sitk = inplane_registration.get_slice_transforms_sitk()
stack_tmp = st.Stack.from_stack(stack_corrupted)
stack_tmp.update_motion_correction_of_slices(slice_transforms_sitk)
stack_diff_sitk = stack_tmp.get_resampled_stack_from_slices(
resampling_grid=stack.sitk).sitk - stack_registered.get_resampled_stack_from_slices(resampling_grid=stack.sitk).sitk
stack_diff_nda = sitk.GetArrayFromImage(stack_diff_sitk)
self.assertEqual(np.round(
np.linalg.norm(stack_diff_nda), decimals=8), 0)
def test_inplane_uniform_scale_similarity_alignment_to_reference(self):
filename_stack = "fetal_brain_0"
# filename_stack = "3D_SheppLoganPhantom_64"
stack = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename_stack + ".nii.gz"),
os.path.join(self.dir_test_data, filename_stack + "_mask.nii.gz")
)
# stack.show(1)
nda = sitk.GetArrayFromImage(stack.sitk)
nda_mask = sitk.GetArrayFromImage(stack.sitk_mask)
i = 5
nda_slice = np.array(nda[i, :, :])
nda_mask_slice = np.array(nda_mask[i, :, :])
for i in range(0, nda.shape[0]): # 23 slices
nda[i, :, :] = nda_slice
nda_mask[i, :, :] = nda_mask_slice
stack_sitk = sitk.GetImageFromArray(nda)
stack_sitk_mask = sitk.GetImageFromArray(nda_mask)
stack_sitk.CopyInformation(stack.sitk)
stack_sitk_mask.CopyInformation(stack.sitk_mask)
stack = st.Stack.from_sitk_image(
stack_sitk, stack.get_filename(), stack_sitk_mask)
# Create in-plane motion corruption
# scale = 1.2
scale = 1
angle_z = 0.05
center_2D = (0, 0)
# translation_2D = np.array([0,0])
translation_2D = np.array([1, -1])
intensity_scale = 1
intensity_bias = 0
# Get corrupted stack and corresponding motions
stack_corrupted, motion_sitk, motion_2_sitk = get_inplane_corrupted_stack(
stack, angle_z, center_2D, translation_2D, scale=scale, intensity_scale=intensity_scale, intensity_bias=intensity_bias, debug=0)
# stack_corrupted.show(1)
# stack.show(1)
# Perform in-plane rigid registrations
inplane_registration = inplanereg.IntraStackRegistration(
stack=stack_corrupted,
reference=stack,
use_stack_mask=True,
use_reference_mask=True,
interpolator="Linear",
use_verbose=True,
)
# inplane_registration = inplanereg.IntraStackRegistration(stack_corrupted)
inplane_registration.set_transform_initializer_type("geometry")
# inplane_registration.set_transform_initializer_type("identity")
inplane_registration.set_intensity_correction_initializer_type(
"affine")
# inplane_registration.set_transform_type("similarity")
inplane_registration.set_transform_type("rigid")
# inplane_registration.set_optimizer("least_squares")
# inplane_registration.set_optimizer("BFGS")
# inplane_registration.set_optimizer("L-BFGS-B")
inplane_registration.set_optimizer("TNC")
# inplane_registration.set_optimizer("Powell")
# inplane_registration.set_optimizer("CG")
# inplane_registration.set_optimizer("Newton-CG")
inplane_registration.set_optimizer_loss("linear")
# inplane_registration.set_optimizer_loss("soft_l1")
# inplane_registration.set_optimizer_loss("arctan")
# inplane_registration.use_parameter_normalization(True)
inplane_registration.set_prior_scale(1/scale)
inplane_registration.set_prior_intensity_coefficients(
(intensity_scale, intensity_bias))
# inplane_registration.set_intensity_correction_type_slice_neighbour_fit(
# "affine")
# inplane_registration.set_intensity_correction_type_reference_fit(
# "affine")
inplane_registration.set_alpha_reference(1)
inplane_registration.set_alpha_neighbour(0)
inplane_registration.set_alpha_parameter(0)
inplane_registration.set_optimizer_iter_max(30)
inplane_registration.run()
inplane_registration.print_statistics()
# inplane_registration._run_registration_pipeline_initialization()
# inplane_registration._apply_motion_correction()
stack_registered = inplane_registration.get_corrected_stack()
parameters = inplane_registration.get_parameters()
sitkh.show_sitk_image([stack.sitk, stack_corrupted.get_resampled_stack_from_slices(interpolator="Linear", resampling_grid=stack.sitk).sitk,
stack_registered.get_resampled_stack_from_slices(interpolator="Linear", resampling_grid=stack.sitk).sitk], label=["original", "corrupted", "recovered"])
# self.assertEqual(np.round(
# np.linalg.norm(nda_diff)
# , decimals = self.accuracy), 0)
# 2) Test slice transforms
slice_transforms_sitk = inplane_registration.get_slice_transforms_sitk()
stack_tmp = st.Stack.from_stack(stack_corrupted)
stack_tmp.update_motion_correction_of_slices(slice_transforms_sitk)
stack_diff_sitk = stack_tmp.get_resampled_stack_from_slices(
resampling_grid=stack.sitk).sitk - stack_registered.get_resampled_stack_from_slices(resampling_grid=stack.sitk).sitk
stack_diff_nda = sitk.GetArrayFromImage(stack_diff_sitk)
self.assertEqual(np.round(
np.linalg.norm(stack_diff_nda), decimals=8), 0)
================================================
FILE: tests/linear_image_quality_transfer_test.py
================================================
# \file TestLinearImageQualityTransfer.py
# \brief Class containing unit tests for module DifferentialOperations
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date July 2016
import unittest
# Import libraries
import numpy as np
from scipy import ndimage
from niftymic.definitions import DIR_TEST
# Import modules from src-folder
class LinearImageQualityTransferTest(unittest.TestCase):
# Specify input data
dir_test_data = DIR_TEST
accuracy = 6
def setUp(self):
pass
##
# Test whether interpretation of a 2D kernel in 3D is correct
# \date 2016-11-06 15:26:19+0000
#
# \param self The object
#
def test_kernel_2D_as_kernel_3D(self):
## Shape in (z,y,x)-coordinates
nda_shape = (40, 200, 200)
# Define size of kernel
N = 6
# Create random data array
nda = 255 * np.random.rand(nda_shape[0], nda_shape[1], nda_shape[2])
# Create kernel with elements 1:N^2
kernel = np.arange(1, N*N+1)
# Define 2D- and equivalent 3D-kernel
kernel_2D = kernel.reshape(N, N)
kernel_3D = kernel.reshape(1, N, N)
# Create data array copies
nda_2D = np.array(nda)
nda_3D = np.array(nda)
# 2D
for i in range(0, nda.shape[0]):
nda_2D[i, :, :] = ndimage.convolve(nda_2D[i, :, :], kernel_2D)
# 3D
nda_3D = ndimage.convolve(nda_3D, kernel_3D)
self.assertEqual(np.around(
np.linalg.norm(nda_2D-nda_3D), decimals=self.accuracy), 0)
================================================
FILE: tests/linear_operators_test.py
================================================
##
# \file linear_operators_test.py
# \brief unit tests of linear operators
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date November 2017
import os
import unittest
import numpy as np
import re
import SimpleITK as sitk
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
import niftymic.base.stack as st
import niftymic.base.data_reader as dr
import niftymic.reconstruction.linear_operators as lin_op
import niftymic.validation.simulate_stacks_from_reconstruction as \
simulate_stacks_from_reconstruction
from niftymic.definitions import DIR_TMP, DIR_TEST
class LinearOperatorsTest(unittest.TestCase):
def setUp(self):
self.precision = 7
self.dir_output = os.path.join(DIR_TMP, "reconstruction")
self.dir_data = os.path.join(DIR_TEST, "case-studies", "fetal-brain")
self.filename = "axial"
self.suffix_mask = "_mask"
self.path_to_file = os.path.join(
self.dir_data, "input-data", "%s.nii.gz" % self.filename)
self.filename_recon = "SRR_stacks3_TK1_lsmr_alpha0p02_itermax5.nii.gz"
self.path_to_recon = os.path.join(
self.dir_data, "recon_projections", self.filename_recon)
self.path_to_recon_mask = ph.append_to_filename(
self.path_to_recon, self.suffix_mask)
##
# Test forward simulation of stack and associated propagation of
# (potentially existing) mask
# \date 2017-11-28 22:37:54+0000
#
def test_forward_operator_stack(self):
stack = st.Stack.from_filename(self.path_to_file)
reconstruction = st.Stack.from_filename(
self.path_to_recon, self.path_to_recon_mask)
linear_operators = lin_op.LinearOperators()
simulated_stack = linear_operators.A(
reconstruction, stack, interpolator_mask="Linear")
simulated_stack.set_filename(stack.get_filename() + "_sim")
# sitkh.show_stacks(
# [stack, simulated_stack], segmentation=simulated_stack)
filename_reference = os.path.join(
self.dir_data,
"recon_projections",
"stack",
"%s_sim.nii.gz" % self.filename)
filename_reference_mask = os.path.join(
self.dir_data,
"recon_projections",
"stack",
"%s_sim%s.nii.gz" % (self.filename, self.suffix_mask))
reference_simulated_stack = st.Stack.from_filename(
filename_reference, filename_reference_mask)
# Error simulated stack
difference_sitk = simulated_stack.sitk - \
reference_simulated_stack.sitk
error = np.linalg.norm(
sitk.GetArrayFromImage(difference_sitk))
self.assertAlmostEqual(error, 0, places=self.precision)
# Error propagated mask
difference_sitk = simulated_stack.sitk_mask - \
reference_simulated_stack.sitk_mask
error = np.linalg.norm(
sitk.GetArrayFromImage(difference_sitk))
self.assertAlmostEqual(error, 0, places=self.precision)
##
# Test script to simulate stacks from slices
# \date 2017-11-28 23:13:02+0000
#
def test_simulate_stacks_from_slices(self):
cmd_args = []
cmd_args.append("--filenames %s" % self.path_to_file)
cmd_args.append("--dir-input-mc %s" %
os.path.join(
self.dir_data,
"recon_projections",
"motion_correction"))
cmd_args.append("--reconstruction %s" % self.path_to_recon)
cmd_args.append("--reconstruction-mask %s" % self.path_to_recon_mask)
cmd_args.append("--copy-data 1")
cmd_args.append("--suffix-mask %s" % self.suffix_mask)
cmd_args.append("--dir-output %s" % self.dir_output)
exe = os.path.abspath(simulate_stacks_from_reconstruction.__file__)
cmd = "python %s %s" % (exe, (" ").join(cmd_args))
self.assertEqual(ph.execute_command(cmd), 0)
path_orig = os.path.join(self.dir_output, "%s.nii.gz" % self.filename)
path_sim = os.path.join(
self.dir_output, "Simulated_%s.nii.gz" % self.filename)
path_orig_ref = os.path.join(self.dir_data,
"recon_projections",
"slices",
"%s.nii.gz" % self.filename)
path_sim_ref = os.path.join(self.dir_data,
"recon_projections",
"slices",
"Simulated_%s.nii.gz" % self.filename)
for res, ref in zip(
[path_orig, path_sim], [path_orig_ref, path_sim_ref]):
res_sitk = sitk.ReadImage(res)
ref_sitk = sitk.ReadImage(ref)
nda_diff = np.nan_to_num(
sitk.GetArrayFromImage(res_sitk - ref_sitk))
self.assertAlmostEqual(np.linalg.norm(
nda_diff), 0, places=self.precision)
================================================
FILE: tests/niftyreg_test.py
================================================
# \file TestNiftyReg.py
# \brief Class containing unit tests for module Stack
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date May 2016
# Import libraries
import SimpleITK as sitk
import numpy as np
import unittest
import sys
import os
import pysitk.python_helper as ph
import pysitk.simple_itk_helper as sitkh
import niftymic.registration.niftyreg as nreg
import niftymic.base.stack as st
from niftymic.definitions import DIR_TEST
class NiftyRegTest(unittest.TestCase):
# Specify input data
dir_test_data = DIR_TEST
accuracy = 7
def setUp(self):
pass
def test_affine_transform_reg_aladin(self):
# Read data
filename_fixed = "stack1_rotated_angle_z_is_pi_over_10.nii.gz"
filename_moving = "FetalBrain_reconstruction_3stacks_myAlg.nii.gz"
diff_ref = os.path.join(
DIR_TEST, "stack1_rotated_angle_z_is_pi_over_10_nreg_diff.nii.gz")
moving = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename_moving),
)
fixed = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename_fixed)
)
# Set up NiftyReg
nifty_reg = nreg.RegAladin()
nifty_reg.set_fixed(fixed)
nifty_reg.set_moving(moving)
nifty_reg.set_registration_type("Rigid")
nifty_reg.use_verbose(False)
# Register via NiftyReg
nifty_reg.run()
# Get associated results
affine_transform_sitk = nifty_reg.get_registration_transform_sitk()
moving_warped = nifty_reg.get_warped_moving()
# Get SimpleITK result with "similar" interpolator (NiftyReg does not
# state what interpolator is used but it seems to be BSpline)
moving_warped_sitk = sitk.Resample(
moving.sitk, fixed.sitk, affine_transform_sitk, sitk.sitkBSpline, 0.0, moving.sitk.GetPixelIDValue())
diff_res_sitk = moving_warped.sitk - moving_warped_sitk
sitkh.write_nifti_image_sitk(diff_res_sitk, diff_ref)
diff_ref_sitk = sitk.ReadImage(diff_ref)
res_diff_nda = sitk.GetArrayFromImage(diff_res_sitk - diff_ref_sitk)
self.assertAlmostEqual(
np.linalg.norm(res_diff_nda), 0, places=self.accuracy)
================================================
FILE: tests/parameter_normalization_test.py
================================================
##
# \file parameter_normalization_test.py
# \brief Class containing unit tests for module ParameterNormalization
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date Nov 2016
import os
import sys
import unittest
import numpy as np
import SimpleITK as sitk
# Import modules
import pysitk.simple_itk_helper as sitkh
import niftymic.base.stack as st
import niftymic.utilities.parameter_normalization as pn
import niftymic.registration.intra_stack_registration as inplanereg
from niftymic.definitions import DIR_TEST
#
class ParameterNormalizationTest(unittest.TestCase):
# Specify input data
dir_test_data = DIR_TEST
accuracy = 6
def setUp(self):
pass
def test_parameter_normalization(self):
use_verbose = 0
filename_stack = "FetalBrain_reconstruction_3stacks_myAlg"
filename_stack_corrupted = "FetalBrain_reconstruction_3stacks_myAlg_corrupted_inplane"
stack_sitk = sitk.ReadImage(
os.path.join(self.dir_test_data, filename_stack + ".nii.gz"))
stack_corrupted_sitk = sitk.ReadImage(
os.path.join(self.dir_test_data, filename_stack_corrupted + ".nii.gz"))
stack_corrupted = st.Stack.from_sitk_image(
image_sitk=stack_corrupted_sitk,
filename="stack_corrupted",
slice_thickness=stack_corrupted_sitk.GetSpacing()[-1],
)
stack = st.Stack.from_sitk_image(
image_sitk=sitk.Resample(stack_sitk, stack_corrupted.sitk),
filename="stack",
slice_thickness=stack_corrupted.get_slice_thickness(),
)
# sitkh.show_stacks([stack, stack_corrupted])
inplane_registration = inplanereg.IntraStackRegistration(
stack_corrupted, stack)
inplane_registration.set_transform_initializer_type("moments")
inplane_registration.set_intensity_correction_type_slice_neighbour_fit(
"affine")
inplane_registration.set_transform_type("rigid")
inplane_registration._run_registration_pipeline_initialization()
parameters = inplane_registration.get_parameters()
# Normalization routine
parameters_tmp = np.array(parameters)
parameter_normalization = pn.ParameterNormalization(parameters_tmp)
parameter_normalization.compute_normalization_coefficients()
coefficients = parameter_normalization.get_normalization_coefficients()
# Check correct normalization
parameters_tmp = parameter_normalization.normalize_parameters(
parameters_tmp)
if use_verbose:
print("Normalization:")
for i in range(0, parameters_tmp.shape[1]):
mean = np.mean(parameters_tmp[:, i])
std = np.std(parameters_tmp[:, i])
if use_verbose:
print("\tmean = %.4f" % (mean))
print("\tstd = %.4f" % (std))
# Check mean
self.assertEqual(np.round(
abs(mean), decimals=self.accuracy), 0)
# Check standard deviation
if abs(std) > 1e-8:
self.assertEqual(np.round(
abs(std - 1), decimals=self.accuracy), 0)
# Check correct normalization
parameters_tmp = parameter_normalization.denormalize_parameters(
parameters_tmp)
if use_verbose:
print("\nDenormalization:")
for i in range(0, parameters_tmp.shape[1]):
mean = np.mean(parameters_tmp[:, i])
std = np.std(parameters_tmp[:, i])
if use_verbose:
print("\tmean = %.4f" % (mean))
print("\tstd = %.4f" % (std))
# Check mean
self.assertEqual(np.round(
abs(mean - coefficients[0, i]), decimals=self.accuracy), 0)
# Check standard deviation
if abs(std) > 1e-8:
self.assertEqual(np.round(
abs(std - coefficients[1, i]), decimals=self.accuracy), 0)
# Check parameter values
self.assertEqual(np.round(
np.linalg.norm(parameters_tmp - parameters), decimals=self.accuracy), 0)
================================================
FILE: tests/registration_test.py
================================================
# \file TestRegistration.py
# \brief Class containing unit tests for module Stack
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date September 2016
import unittest
# Import libraries
import SimpleITK as sitk
import itk
import numpy as np
import os
# Import modules
import niftymic.base.stack as st
import niftymic.prototyping.registration as myreg
import pysitk.python_helper as ph
from niftymic.definitions import DIR_TEST
# Pixel type of used 3D ITK image
PIXEL_TYPE = itk.D
# ITK image type
IMAGE_TYPE = itk.Image[PIXEL_TYPE, 3]
IMAGE_TYPE_CV33 = itk.Image.CVD33
IMAGE_TYPE_CV183 = itk.Image.CVD183
IMAGE_TYPE_CV363 = itk.Image.CVD363
class RegistrationTest(unittest.TestCase):
# Specify input data
dir_test_data = os.path.join(DIR_TEST, "registration/")
accuracy = 2
def setUp(self):
# Set print options for numpy
np.set_printoptions(precision=3)
"""
def test_GradientEuler3DTransformImageFilter(self):
filename_HRVolume = "HRVolume"
HR_volume = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename_HRVolume + ".nii.gz")
)
DOF_transform = 6
parameters = np.random.rand(
DOF_transform)*(2*np.pi, 2*np.pi, 2*np.pi, 10, 10, 10)
itk2np = itk.PyBuffer[IMAGE_TYPE]
itk2np_CVD33 = itk.PyBuffer[IMAGE_TYPE_CV33]
itk2np_CVD183 = itk.PyBuffer[IMAGE_TYPE_CV183]
# Create Euler3DTransform and update with random parameters
transform_itk = itk.Euler3DTransform.New()
parameters_itk = transform_itk.GetParameters()
sitkh.update_itk_parameters(parameters_itk, parameters)
transform_itk.SetParameters(parameters_itk)
# sitkh.print_itk_array(parameters_itk)
# ---------------------------------------------------------------------
filter_gradient_transform = itk.GradientEuler3DTransformImageFilter[
IMAGE_TYPE, PIXEL_TYPE, PIXEL_TYPE].New()
filter_gradient_transform.SetInput(HR_volume.itk)
filter_gradient_transform.SetTransform(transform_itk)
time_start = ph.start_timing() # Above is required only once in Registration
filter_gradient_transform.Update()
gradient_transform_itk = filter_gradient_transform.GetOutput()
# Get data array of Jacobian of transform w.r.t. parameters and
# reshape to N_HR_volume_voxels x DIMENSION x DOF
nda_gradient_transform_1 = itk2np_CVD183.GetArrayFromImage(
gradient_transform_itk).reshape(-1, 3, DOF_transform)
print("GradientEuler3DTransformImageFilter: " +
str(ph.stop_timing(time_start)))
# ---------------------------------------------------------------------
time_start = ph.start_timing()
nda_gradient_transform_2 = sitkh.get_numpy_array_of_jacobian_itk_transform_applied_on_sitk_image(
transform_itk, HR_volume.sitk)
print("get_numpy_array_of_jacobian_itk_transform_applied_on_sitk_image: " +
str(ph.stop_timing(time_start)))
# ---------------------------------------------------------------------
self.assertEqual(np.round(
np.linalg.norm(nda_gradient_transform_2-nda_gradient_transform_1), decimals=6), 0)
def test_GradientAffine3DTransformImageFilter(self):
filename_HRVolume = "HRVolume"
HR_volume = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename_HRVolume + ".nii.gz")
)
DOF_transform = 12
parameters = np.random.rand(DOF_transform)*10
itk2np = itk.PyBuffer[IMAGE_TYPE]
itk2np_CVD33 = itk.PyBuffer[IMAGE_TYPE_CV33]
itk2np_CVD363 = itk.PyBuffer[IMAGE_TYPE_CV363]
# Create Euler3DTransform and update with random parameters
transform_itk = itk.AffineTransform[PIXEL_TYPE, 3].New()
parameters_itk = transform_itk.GetParameters()
sitkh.update_itk_parameters(parameters_itk, parameters)
transform_itk.SetParameters(parameters_itk)
# sitkh.print_itk_array(parameters_itk)
# ---------------------------------------------------------------------
filter_gradient_transform = itk.GradientAffine3DTransformImageFilter[
IMAGE_TYPE, PIXEL_TYPE, PIXEL_TYPE].New()
filter_gradient_transform.SetInput(HR_volume.itk)
filter_gradient_transform.SetTransform(transform_itk)
time_start = ph.start_timing() # Above is required only once in Registration
filter_gradient_transform.Update()
gradient_transform_itk = filter_gradient_transform.GetOutput()
# Get data array of Jacobian of transform w.r.t. parameters and
# reshape to N_HR_volume_voxels x DIMENSION x DOF
nda_gradient_transform_1 = itk2np_CVD363.GetArrayFromImage(
gradient_transform_itk).reshape(-1, 3, DOF_transform)
print("GradientAffine3DTransformImageFilter: " +
str(ph.stop_timing(time_start)))
# ---------------------------------------------------------------------
time_start = ph.start_timing()
nda_gradient_transform_2 = sitkh.get_numpy_array_of_jacobian_itk_transform_applied_on_sitk_image(
transform_itk, HR_volume.sitk)
print("get_numpy_array_of_jacobian_itk_transform_applied_on_sitk_image: " +
str(ph.stop_timing(time_start)))
# ---------------------------------------------------------------------
self.assertEqual(np.round(
np.linalg.norm(nda_gradient_transform_2-nda_gradient_transform_1), decimals=6), 0)
def test_reshaping_of_structures(self):
# filename_prefix = "RigidTransform_"
filename_prefix = "TranslationOnly_"
filename_HRVolume = "HRVolume"
filename_StackSim = filename_prefix + "StackSimulated"
filename_transforms_prefix = filename_prefix + "TransformGroundTruth_slice"
stack_sim = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename_StackSim + ".nii.gz")
)
HR_volume = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename_HRVolume + ".nii.gz")
)
slices_sim = stack_sim.get_slices()
N_slices = len(slices_sim)
itk2np = itk.PyBuffer[itk.Image.D3]
itk2np_CVD33 = itk.PyBuffer[itk.Image.CVD33]
filter_OrientedGaussian_3D = itk.OrientedGaussianInterpolateImageFilter[
IMAGE_TYPE, IMAGE_TYPE].New()
filter_OrientedGaussian_3D.SetInput(HR_volume.itk)
filter_OrientedGaussian_3D.SetUseJacobian(True)
for j in range(0, N_slices):
slice = slices_sim[j]
filter_OrientedGaussian_3D.SetOutputParametersFromImage(slice.itk)
filter_OrientedGaussian_3D.UpdateLargestPossibleRegion()
filter_OrientedGaussian_3D.Update()
slice_simulated_nda = itk2np.GetArrayFromImage(
filter_OrientedGaussian_3D.GetOutput())
dslice_simulated_nda = itk2np_CVD33.GetArrayFromImage(
filter_OrientedGaussian_3D.GetJacobian())
shape = slice_simulated_nda.shape
slice_simulated_nda_flat = slice_simulated_nda.flatten()
dslice_simulated_nda_flat = dslice_simulated_nda.reshape(-1, 3)
array0 = np.zeros(3)
array1 = np.zeros(3)
abs_diff = 0
iter = 0
for i in range(0, shape[0]):
for j in range(0, shape[1]):
for k in range(0, shape[2]):
array0 = slice_simulated_nda[
i, j, k] - dslice_simulated_nda[i, j, k, :]
array1 = slice_simulated_nda_flat[
iter] - dslice_simulated_nda_flat[iter, :]
abs_diff += np.linalg.norm(array0-array1)
iter += 1
self.assertEqual(np.round(
abs_diff, decimals=self.accuracy), 0)
def test_vectorization_of_dImage_times_dT(self):
shape = np.array([200, 200])
slice_nda = np.random.rand(shape[0], shape[1])*255
slice_sitk = sitk.GetImageFromArray(slice_nda)
N_voxels = shape.prod()
gradient_image_filter_sitk = sitk.GradientImageFilter()
dslice_sitk = gradient_image_filter_sitk.Execute(slice_sitk)
# Reshape to (N_slice_voxels x dim)-array
dslice_nda = sitk.GetArrayFromImage(dslice_sitk).reshape(-1, 2)
euler_sitk = sitk.Euler2DTransform()
euler_itk = itk.Euler2DTransform.New()
parameters_sitk = (0.5, -4, 10)
euler_sitk.SetParameters(parameters_sitk)
euler_itk.SetParameters(itk.OptimizerParameters[
itk.D](parameters_sitk))
# Get d[T(theta, x)]/dtheta as (N_slice_voxels x dim x
# transform_type_dofs)
dT_nda = sitkh.get_numpy_array_of_jacobian_itk_transform_applied_on_sitk_image(
euler_itk, slice_sitk)
jacobian = np.zeros((N_voxels, euler_itk.GetNumberOfParameters()))
time0 = ph.start_timing()
for i in range(0, N_voxels):
jacobian[i, :] = dslice_nda[i, :].dot(dT_nda[i, :, :])
print("For-loop: " + str(ph.stop_timing(time0)))
time0 = ph.start_timing()
jacobian_2 = np.sum((dslice_nda[:, :, np.newaxis]*dT_nda), axis=1)
print("Vectorized: " + str(ph.stop_timing(time0)))
self.assertEqual(np.round(
np.linalg.norm(jacobian - jacobian_2), decimals=8), 0)
"""
def test_translation_registration_of_slices(self):
filename_prefix = "TranslationOnly_"
# filename_prefix = "RigidTransform_"
filename_HRVolume = "HRVolume"
filename_StackSim = filename_prefix + "StackSimulated"
filename_transforms_prefix = filename_prefix + "TransformGroundTruth_slice"
stack_sim = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename_StackSim + ".nii.gz"))
HR_volume = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename_HRVolume + ".nii.gz"))
slices_sim = stack_sim.get_slices()
N_slices = len(slices_sim)
scale = np.array(
[180. / np.pi, 180. / np.pi, 180. / np.pi, 1., 1., 1.])
time_start = ph.start_timing()
for j in range(0, N_slices):
# for j in range(20, N_slices):
rigid_transform_groundtruth_sitk = sitk.ReadTransform(
self.dir_test_data + filename_transforms_prefix + str(j) + ".tfm")
parameters_gd = np.array(
rigid_transform_groundtruth_sitk.GetParameters())
angle_max = 5. * np.pi / 180.
t_max = 5.
registration = myreg.Registration(
fixed=slices_sim[j], moving=HR_volume,
# initializer_type="SelfGEOMETRY",
use_verbose=0,
# data_loss="soft_l1",
# x_scale=[angle_max, angle_max, angle_max, t_max, t_max, t_max],
)
registration.run_registration()
# registration.print_statistics()
# Check parameters
transform_sitk = registration.get_registration_transform_sitk()
parameters = np.array(transform_sitk.GetParameters())
norm_diff = np.linalg.norm(parameters-parameters_gd)
params = parameters * scale
params_gt = parameters_gd * scale
print("Slice %s/%s: |parameters-parameters_gd| = %s" %
(j, N_slices-1, str(norm_diff)))
print("\tEst: " + str(params) + " (deg/mmm)")
print("\tGT: " + str(params_gt) + " (deg/mmm)")
print("\tDiff: " + str(params - params_gt) + " (deg/mmm)")
self.assertEqual(np.round(
norm_diff, decimals=self.accuracy), 0)
# Set elapsed time
print("Translation: " + str(ph.stop_timing(time_start)))
"""
def test_rigid_registration_of_slices(self):
filename_prefix = "RigidTransform_"
filename_HRVolume = "HRVolume"
filename_StackSim = filename_prefix + "StackSimulated"
filename_transforms_prefix = filename_prefix + "TransformGroundTruth_slice"
stack_sim = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename_StackSim + ".nii.gz"))
HR_volume = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename_HRVolume + ".nii.gz"))
slices_sim = stack_sim.get_slices()
N_slices = len(slices_sim)
time_start = time.time()
for j in range(0, N_slices):
rigid_transform_groundtruth_sitk = sitk.ReadTransform(
self.dir_test_data + filename_transforms_prefix + str(j) + ".tfm")
parameters_gd = np.array(
rigid_transform_groundtruth_sitk.GetParameters())
registration = myreg.Registration(
fixed=slices_sim[j], moving=HR_volume)
registration.run_registration()
# registration.print_statistics()
# Check parameters
parameters = registration.get_parameters()
norm_diff = np.linalg.norm(parameters-parameters_gd)
# print("Slice %s/%s: |parameters-parameters_gd| = %s" %(j, N_slices-1, str(norm_diff)) )
self.assertEqual(np.round(
norm_diff, decimals=self.accuracy), 0)
# Set elapsed time
time_end = time.time()
# print("Rigid registration test: Elapsed time = %s" %(timedelta(seconds=time_end-time_start)))
def test_rigid_registration_of_stack(self):
filename_prefix = "NoMotion_"
parameters_gd = (0.1, 0.1, 0.2, -1, 3, 2)
filename_HRVolume = "HRVolume"
filename_StackSim = filename_prefix + "StackSimulated"
stack_sim = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename_StackSim + ".nii.gz"))
HR_volume = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename_HRVolume + ".nii.gz"))
# Apply motion
transform_sitk = sitk.Euler3DTransform()
transform_sitk.SetParameters(parameters_gd)
stack_sitk = sitkh.get_transformed_sitk_image(
stack_sim.sitk, transform_sitk)
stack_sitk_mask = sitkh.get_transformed_sitk_image(
stack_sim.sitk_mask, transform_sitk)
stack_sim = st.Stack.from_sitk_image(
stack_sitk, filename=stack_sim.get_filename(), image_sitk_mask=stack_sitk_mask)
# PSF-aware Registration algorithm
time_start = time.time()
registration = myreg.Registration(
fixed=stack_sim,
moving=HR_volume,
use_verbose=True,
# data_loss="huber",
# minimizer="L-BFGS-B",
# minimizer="Newton-CG",
)
# registration.use_fixed_mask(True)
registration.use_verbose(True)
registration.run_registration()
# Check parameters (should be the negative of parameters_gd)
parameters = registration.get_parameters()
norm_diff = np.linalg.norm(parameters+parameters_gd)
print("parameters = " + str(parameters))
print("|parameters-parameters_gd| = %s" % (str(norm_diff)))
self.assertEqual(np.round(
norm_diff*0.1, decimals=0), 0)
# Set elapsed time
time_end = time.time()
print("Rigid registration test: Elapsed time = %s" %
(registration.get_computational_time()))
# Comparison with ITK registrations
scales_estimator = "PhysicalShift"
use_verbose = 1
# ----------------SimpleITK registration for comparison----------------
ph.print_title("SimpleITK registration for comparison:")
registration = regsitk.SimpleItkRegistration(
fixed=stack_sim,
moving=HR_volume,
optimizer="RegularStepGradientDescent",
optimizer_params={
"minStep": 1e-6,
"numberOfIterations": 500,
"gradientMagnitudeTolerance": 1e-6,
"learningRate": 1,
},
)
registration.set_metric("MeanSquares")
registration.use_verbose(use_verbose)
registration.set_scales_estimator(scales_estimator)
registration.run_registration()
parameters = np.array(
registration.get_registration_transform_sitk().GetParameters())
norm_diff = np.linalg.norm(parameters+parameters_gd)
print("\tparameters = " + str(parameters))
print("\t|parameters-parameters_gd| = %s" % (str(norm_diff)))
print("\tRigid registration test: Elapsed time = %s" %
(registration.get_computational_time()))
"""
================================================
FILE: tests/residual_evaluator_test.py
================================================
##
# \file residual_evaluator_test.py
# \brief Test ResidualEvaluator class
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date November 2017
import os
import unittest
import numpy as np
import re
import SimpleITK as sitk
import pysitk.python_helper as ph
import niftymic.base.stack as st
import niftymic.validation.residual_evaluator as res_ev
import niftymic.base.exceptions as exceptions
from niftymic.definitions import DIR_TMP, DIR_TEST
class ResidualEvaluatorTest(unittest.TestCase):
def setUp(self):
self.precision = 7
self.dir_tmp = os.path.join(DIR_TMP, "residual_evaluator")
def test_compute_write_read_slice_similarities(self):
paths_to_stacks = [
os.path.join(
DIR_TEST, "fetal_brain_%d.nii.gz" % d) for d in range(0, 3)
]
path_to_reference = os.path.join(
DIR_TEST, "FetalBrain_reconstruction_3stacks_myAlg.nii.gz")
stacks = [
st.Stack.from_filename(p, ph.append_to_filename(p, "_mask"))
for p in paths_to_stacks
]
reference = st.Stack.from_filename(
path_to_reference, extract_slices=False)
residual_evaluator = res_ev.ResidualEvaluator(stacks, reference)
residual_evaluator.compute_slice_projections()
residual_evaluator.evaluate_slice_similarities()
residual_evaluator.write_slice_similarities(self.dir_tmp)
slice_similarities = residual_evaluator.get_slice_similarities()
residual_evaluator1 = res_ev.ResidualEvaluator()
residual_evaluator1.read_slice_similarities(self.dir_tmp)
slice_similarities1 = residual_evaluator1.get_slice_similarities()
for stack_name in slice_similarities.keys():
for m in slice_similarities[stack_name].keys():
rho_res = slice_similarities[stack_name][m]
rho_res1 = slice_similarities1[stack_name][m]
error = np.linalg.norm(rho_res - rho_res1)
self.assertAlmostEqual(error, 0, places=self.precision)
def test_slice_projections_not_created(self):
paths_to_stacks = [
os.path.join(
DIR_TEST, "fetal_brain_%d.nii.gz" % d) for d in range(0, 1)
]
path_to_reference = os.path.join(
DIR_TEST, "FetalBrain_reconstruction_3stacks_myAlg.nii.gz")
stacks = [
st.Stack.from_filename(p, ph.append_to_filename(p, "_mask"))
for p in paths_to_stacks
]
reference = st.Stack.from_filename(
path_to_reference, extract_slices=False)
residual_evaluator = res_ev.ResidualEvaluator(stacks, reference)
self.assertRaises(exceptions.ObjectNotCreated, lambda:
residual_evaluator.evaluate_slice_similarities())
================================================
FILE: tests/run_tests.py
================================================
#!/usr/bin/python
##
# \file run_tests.py
# \brief main-file to run specified unit tests
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date September 2015
#
import unittest
from brain_stripping_test import *
from case_study_fetal_brain_test import *
from case_study_rsfmri_test import *
from data_reader_test import *
from image_similarity_evaluator_test import *
from intensity_correction_test import *
from linear_operators_test import *
from niftyreg_test import *
from residual_evaluator_test import *
from segmentation_propagation_test import *
# from simulator_slice_acquisition_test import * # only in dev branch
from stack_test import *
# from parameter_normalization_test import *
# from registration_test import * # TBC
# from intra_stack_registration_test import * # TBC
if __name__ == '__main__':
print("\nUnit tests:\n--------------")
unittest.main()
================================================
FILE: tests/segmentation_propagation_test.py
================================================
# \file TestSegmentationPropagation.py
# \brief Class containing unit tests for module SegmentationPropagation
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date May 2017
import unittest
# Import libraries
import SimpleITK as sitk
import numpy as np
import os
import pysitk.simple_itk_helper as sitkh
import niftymic.base.stack as st
import niftymic.registration.simple_itk_registration as regsitk
import niftymic.registration.niftyreg as nreg
import niftymic.utilities.segmentation_propagation as segprop
from niftymic.definitions import DIR_TEST
class SegmentationPropagationTest(unittest.TestCase):
# Specify input data
dir_test_data = DIR_TEST
accuracy = 6
def setUp(self):
pass
def test_registration(self):
filename = "fetal_brain_0"
parameters_gd = (0.1, 0.2, -0.3, 0, 0, 0)
# parameters_gd = np.zeros(6)
template = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename + ".nii.gz"),
os.path.join(self.dir_test_data, filename + "_mask.nii.gz"),
extract_slices=False,
)
transform_sitk_gd = sitk.Euler3DTransform()
transform_sitk_gd.SetParameters(parameters_gd)
stack_sitk = sitkh.get_transformed_sitk_image(
template.sitk, transform_sitk_gd)
stack = st.Stack.from_sitk_image(
image_sitk=stack_sitk,
filename="stack",
slice_thickness=template.get_slice_thickness(),
)
optimizer = "RegularStepGradientDescent"
optimizer_params = {
'learningRate': 1,
'minStep': 1e-6,
'numberOfIterations': 300
}
registration = regsitk.SimpleItkRegistration(
initializer_type="SelfGEOMETRY",
use_verbose=True,
metric="MeanSquares",
optimizer=optimizer,
optimizer_params=optimizer_params,
use_multiresolution_framework=False,
)
segmentation_propagation = segprop.SegmentationPropagation(
stack=stack,
template=template,
registration_method=registration,
dilation_radius=10,
)
segmentation_propagation.run_segmentation_propagation()
foo = segmentation_propagation.get_segmented_stack()
# Get transform and force center = 0
transform = segmentation_propagation.get_registration_transform_sitk()
transform = sitkh.get_composite_sitk_euler_transform(
transform, sitk.Euler3DTransform())
parameters = sitk.Euler3DTransform(
transform.GetInverse()).GetParameters()
self.assertEqual(np.round(
np.linalg.norm(np.array(parameters) - parameters_gd), decimals=4), 0)
================================================
FILE: tests/simulator_slice_acquisition_test.py
================================================
# \file simulator_slice_acquisition_test.py
# \brief Class containing unit tests for module SimulatorSliceAcqusition
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date May 2016
import os
import itk
import unittest
import numpy as np
import SimpleITK as sitk
import pysitk.simple_itk_helper as sitkh
import niftymic.base.psf as psf
import niftymic.base.stack as st
import niftymic.prototyping.simulator_slice_acqusition as sa
from niftymic.definitions import DIR_TEST
# Pixel type of used 3D ITK image
pixel_type = itk.D
# ITK image type
image_type = itk.Image[pixel_type, 3]
class SimulatorSliceAcqusitionTest(unittest.TestCase):
# Specify input data
dir_test_data = DIR_TEST
accuracy = 7
def setUp(self):
pass
def test_conversion_image_direction(self):
filename_HR_volume = "HR_volume_postmortem"
HR_volume = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename_HR_volume + ".nii.gz"),
os.path.join(self.dir_test_data,
filename_HR_volume + "_mask.nii.gz")
)
# Get unit vectors defining image grid in physical space and construct
# direction matrix
origin_HR_volume = np.array(HR_volume.sitk.GetOrigin())
a_x = HR_volume.sitk.TransformIndexToPhysicalPoint(
(1, 0, 0)) - origin_HR_volume
a_y = HR_volume.sitk.TransformIndexToPhysicalPoint(
(0, 1, 0)) - origin_HR_volume
a_z = HR_volume.sitk.TransformIndexToPhysicalPoint(
(0, 0, 1)) - origin_HR_volume
e_x = a_x/np.linalg.norm(a_x)
e_y = a_y/np.linalg.norm(a_y)
e_z = a_z/np.linalg.norm(a_z)
direction_matrix_test = np.array([e_x, e_y, e_z]).transpose()
direction_test = direction_matrix_test.flatten()
# Get respective vectors from image direction
direction = np.array(HR_volume.sitk.GetDirection())
e_x_test = direction[0::3]
e_y_test = direction[1::3]
e_z_test = direction[2::3]
# Check correspondences
self.assertEqual(np.round(np.linalg.norm(
e_x_test - e_x), decimals=self.accuracy), 0)
self.assertEqual(np.round(np.linalg.norm(
e_y_test - e_y), decimals=self.accuracy), 0)
self.assertEqual(np.round(np.linalg.norm(
e_z_test - e_z), decimals=self.accuracy), 0)
self.assertEqual(np.round(np.linalg.norm(
direction - direction_test), decimals=self.accuracy), 0)
# Test whether the slices, and hence stacks, are correctly simulated
# in each orthogonal direction by assuming no subject motion
def test_run_simulation_view(self):
filename_HR_volume = "HR_volume_postmortem"
HR_volume = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename_HR_volume + ".nii.gz"),
os.path.join(self.dir_test_data,
filename_HR_volume + "_mask.nii.gz")
)
# 1) Test for Nearest Neighbor Interpolator
slice_acquistion = sa.SliceAcqusition(HR_volume)
slice_acquistion.set_interpolator_type("NearestNeighbor")
slice_acquistion.set_motion_type("NoMotion")
slice_acquistion.run_simulation_view_1()
slice_acquistion.run_simulation_view_2()
slice_acquistion.run_simulation_view_3()
stacks_simulated = slice_acquistion.get_simulated_stacks()
for i in range(0, len(stacks_simulated)):
HR_volume_resampled_sitk = sitk.Resample(
HR_volume.sitk, stacks_simulated[i].sitk, sitk.Euler3DTransform(
), sitk.sitkNearestNeighbor, 0.0, stacks_simulated[i].sitk.GetPixelIDValue()
)
self.assertEqual(np.round(
np.linalg.norm(sitk.GetArrayFromImage(stacks_simulated[i].sitk - HR_volume_resampled_sitk)), decimals=self.accuracy), 0)
# 2) Test for Linear Interpolator
slice_acquistion = sa.SliceAcqusition(HR_volume)
slice_acquistion.set_interpolator_type("Linear")
slice_acquistion.set_motion_type("NoMotion")
slice_acquistion.run_simulation_view_1()
slice_acquistion.run_simulation_view_2()
slice_acquistion.run_simulation_view_3()
stacks_simulated = slice_acquistion.get_simulated_stacks()
for i in range(0, len(stacks_simulated)):
HR_volume_resampled_sitk = sitk.Resample(
HR_volume.sitk, stacks_simulated[i].sitk, sitk.Euler3DTransform(
), sitk.sitkLinear, 0.0, stacks_simulated[i].sitk.GetPixelIDValue()
)
self.assertEqual(np.round(
np.linalg.norm(sitk.GetArrayFromImage(stacks_simulated[i].sitk - HR_volume_resampled_sitk)), decimals=self.accuracy), 0)
# 3) Test for Oriented Gaussian Interpolator
alpha_cut = 3
slice_acquistion = sa.SliceAcqusition(HR_volume)
slice_acquistion.set_interpolator_type("OrientedGaussian")
slice_acquistion.set_motion_type("NoMotion")
slice_acquistion.set_alpha_cut(alpha_cut)
slice_acquistion.run_simulation_view_1()
slice_acquistion.run_simulation_view_2()
slice_acquistion.run_simulation_view_3()
stacks_simulated = slice_acquistion.get_simulated_stacks()
resampler = itk.ResampleImageFilter[image_type, image_type].New()
resampler.SetDefaultPixelValue(0.0)
resampler.SetInput(HR_volume.itk)
interpolator = itk.OrientedGaussianInterpolateImageFunction[
image_type, pixel_type].New()
interpolator.SetAlpha(alpha_cut)
resampler.SetInterpolator(interpolator)
PSF = psf.PSF()
for i in range(0, len(stacks_simulated)):
resampler.SetOutputParametersFromImage(stacks_simulated[i].itk)
# Set covariance based on oblique PSF
Cov_HR_coord = PSF.get_covariance_matrix_in_reconstruction_space(
stacks_simulated[i], HR_volume)
interpolator.SetCovariance(Cov_HR_coord.flatten())
resampler.UpdateLargestPossibleRegion()
resampler.Update()
HR_volume_resampled_itk = resampler.GetOutput()
HR_volume_resampled_sitk = sitkh.get_sitk_from_itk_image(
HR_volume_resampled_itk)
self.assertEqual(np.round(
np.linalg.norm(sitk.GetArrayFromImage(stacks_simulated[i].sitk - HR_volume_resampled_sitk)), decimals=self.accuracy), 0)
# Test whether the ground truth affine transforms set during the
# simulation correspond to the actually acquired positions within the
# sliced volume whereby no motion is applied to the HR volume
def test_ground_truth_affine_transforms_no_motion(self):
filename_HR_volume = "HR_volume_postmortem"
HR_volume = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename_HR_volume + ".nii.gz"),
os.path.join(self.dir_test_data,
filename_HR_volume + "_mask.nii.gz")
)
# Run simulation for Nearest Neighbor interpolation (shouldn't not
# matter anyway and is quicker)
slice_acquistion = sa.SliceAcqusition(HR_volume)
slice_acquistion.set_interpolator_type("NearestNeighbor")
slice_acquistion.set_motion_type("NoMotion")
slice_acquistion.run_simulation_view_1()
slice_acquistion.run_simulation_view_2()
slice_acquistion.run_simulation_view_3()
# Get simulated stack of slices + corresponding ground truth affine
# transforms indicating the correct acquisition of the slice
# within the volume
stacks_simulated = slice_acquistion.get_simulated_stacks()
affine_transforms_ground_truth, rigid_motion_transforms_ground_truth = slice_acquistion.get_ground_truth_data()
for i in range(0, len(stacks_simulated)):
stack = st.Stack.from_stack(stacks_simulated[i])
slices = stack.get_slices()
N_slices = stack.get_number_of_slices()
for j in range(0, N_slices):
# print("Stack %s: Slice %s/%s" %(i,j,N_slices-1))
slice = slices[j]
# slice.update_motion_correction(rigid_motion_transforms_ground_truth[i][j])
HR_volume_resampled_slice_sitk = sitk.Resample(
HR_volume.sitk, slice.sitk, sitk.Euler3DTransform(
), sitk.sitkNearestNeighbor, 0.0, slice.sitk.GetPixelIDValue()
)
self.assertEqual(np.round(
np.linalg.norm(sitk.GetArrayFromImage(slice.sitk - HR_volume_resampled_slice_sitk)), decimals=self.accuracy), 0)
# Test whether the ground truth affine transforms set during the
# simulation correspond to the actually acquired positions within the
# sliced volume whereby motion is applied to the HR volume
def test_ground_truth_affine_transforms_with_motion_NearestNeighbor(self):
filename_HR_volume = "HR_volume_postmortem"
HR_volume = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename_HR_volume + ".nii.gz"),
os.path.join(self.dir_test_data,
filename_HR_volume + "_mask.nii.gz")
)
# Run simulation for Nearest Neighbor interpolation (shouldn't not
# matter anyway and is quicker)
slice_acquistion = sa.SliceAcqusition(HR_volume)
slice_acquistion.set_interpolator_type("NearestNeighbor")
slice_acquistion.set_motion_type("Random")
slice_acquistion.run_simulation_view_1()
slice_acquistion.run_simulation_view_2()
slice_acquistion.run_simulation_view_3()
# Get simulated stack of slices + corresponding ground truth affine
# transforms indicating the correct acquisition of the slice
# within the volume
stacks_simulated = slice_acquistion.get_simulated_stacks()
affine_transforms_ground_truth, rigid_motion_transforms_ground_truth = slice_acquistion.get_ground_truth_data()
for i in range(0, len(stacks_simulated)):
stack = stacks_simulated[i]
slices = stack.get_slices()
N_slices = stack.get_number_of_slices()
for j in range(0, N_slices):
# print("Stack %s: Slice %s/%s" %(i,j,N_slices-1))
slice = slices[j]
slice.update_motion_correction(
rigid_motion_transforms_ground_truth[i][j])
HR_volume_resampled_slice_sitk = sitk.Resample(
HR_volume.sitk, slice.sitk, sitk.Euler3DTransform(
), sitk.sitkNearestNeighbor, 0.0, slice.sitk.GetPixelIDValue()
)
self.assertEqual(np.round(
np.linalg.norm(sitk.GetArrayFromImage(slice.sitk - HR_volume_resampled_slice_sitk)), decimals=self.accuracy), 0)
# Test whether the ground truth affine transforms set during the
# simulation correspond to the actually acquired positions within the
# sliced volume whereby motion is applied to the HR volume
def test_ground_truth_affine_transforms_with_motion_OrientedGaussian(self):
filename_HR_volume = "HR_volume_postmortem"
HR_volume = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename_HR_volume + ".nii.gz"),
os.path.join(self.dir_test_data,
filename_HR_volume + "_mask.nii.gz")
)
alpha_cut = 3
# Run simulation for Oriented Gaussian interpolation, hence more
# "realistic" case
slice_acquistion = sa.SliceAcqusition(HR_volume)
slice_acquistion.set_interpolator_type("OrientedGaussian")
# slice_acquistion.set_interpolator_type("NearestNeighbor")
# slice_acquistion.set_interpolator_type("Linear")
slice_acquistion.set_motion_type("Random")
# slice_acquistion.set_motion_type("NoMotion")
slice_acquistion.set_alpha_cut(alpha_cut)
slice_acquistion.run_simulation_view_1()
slice_acquistion.run_simulation_view_2()
slice_acquistion.run_simulation_view_3()
# Get simulated stack of slices + corresponding ground truth affine
# transforms indicating the correct acquisition of the slice
# within the volume
stacks_simulated = slice_acquistion.get_simulated_stacks()
affine_transforms_ground_truth, rigid_motion_transforms_ground_truth = slice_acquistion.get_ground_truth_data()
resampler = itk.ResampleImageFilter[image_type, image_type].New()
resampler.SetDefaultPixelValue(0.0)
resampler.SetInput(HR_volume.itk)
interpolator = itk.OrientedGaussianInterpolateImageFunction[
image_type, pixel_type].New()
interpolator.SetAlpha(alpha_cut)
# interpolator = itk.LinearInterpolateImageFunction[image_type, pixel_type].New()
# interpolator = itk.NearestNeighborInterpolateImageFunction[image_type, pixel_type].New()
resampler.SetInterpolator(interpolator)
PSF = psf.PSF()
for i in range(0, len(stacks_simulated)):
stack = stacks_simulated[i]
slices = stack.get_slices()
N_slices = stack.get_number_of_slices()
for j in range(0, N_slices):
# print("Stack %s: Slice %s/%s" %(i,j,N_slices-1))
slice = slices[j]
slice.update_motion_correction(
rigid_motion_transforms_ground_truth[i][j])
# Get covariance based on oblique PSF
Cov_HR_coord = PSF.get_covariance_matrix_in_reconstruction_space(
slice, HR_volume)
# Update resampler
interpolator.SetCovariance(Cov_HR_coord.flatten())
resampler.SetOutputParametersFromImage(slice.itk)
resampler.UpdateLargestPossibleRegion()
resampler.Update()
HR_volume_resampled_slice_itk = resampler.GetOutput()
HR_volume_resampled_slice_sitk = sitkh.get_sitk_from_itk_image(
HR_volume_resampled_slice_itk)
# HR_volume_resampled_slice_sitk = sitk.Resample(
# HR_volume.sitk, slice.sitk, sitk.Euler3DTransform(), sitk.sitkNearestNeighbor, 0.0, slice.sitk.GetPixelIDValue()
# )
norm_diff = np.linalg.norm(sitk.GetArrayFromImage(
slice.sitk - HR_volume_resampled_slice_sitk))
# try:
self.assertEqual(
np.round(norm_diff, decimals=self.accuracy), 0)
# except:
# print("Stack %s: Slice %s/%s" %(i,j,N_slices-1))
# print(norm_diff)
================================================
FILE: tests/stack_test.py
================================================
# \file TestStack.py
# \brief Class containing unit tests for module Stack
#
# \author Michael Ebner (michael.ebner.14@ucl.ac.uk)
# \date December 2015
import SimpleITK as sitk
import numpy as np
import unittest
import random
import os
import niftymic.base.stack as st
import niftymic.base.data_reader as dr
import niftymic.base.exceptions as exceptions
import niftymic.validation.motion_simulator as ms
from niftymic.definitions import DIR_TEST, DIR_TMP
class StackTest(unittest.TestCase):
# Specify input data
dir_test_data = DIR_TEST
dir_test_data_io = os.path.join(DIR_TEST, "IO")
accuracy = 7
def setUp(self):
pass
def test_get_resampled_stack_from_slices(self):
filename = "stack0"
stack = st.Stack.from_filename(
os.path.join(self.dir_test_data, filename + ".nii.gz"),
os.path.join(self.dir_test_data, filename + "_mask.nii.gz")
)
nda_stack = sitk.GetArrayFromImage(stack.sitk)
nda_stack_mask = sitk.GetArrayFromImage(stack.sitk_mask)
# Resample stack based on slices
stack_resampled_from_slice = stack.get_resampled_stack_from_slices()
# Check alignment of image
nda_stack_resampled = sitk.GetArrayFromImage(
stack_resampled_from_slice.sitk)
self.assertEqual(np.round(
np.linalg.norm(nda_stack - nda_stack_resampled), decimals=self.accuracy), 0)
# Check alignment of image mask
nda_stack_resampled_mask = sitk.GetArrayFromImage(
stack_resampled_from_slice.sitk_mask)
self.assertEqual(np.round(
np.linalg.norm(nda_stack_mask - nda_stack_resampled_mask), decimals=self.accuracy), 0)
def test_io_image_not_existent(self):
# Neither fetal_brain_2.nii.gz nor fetal_brain_2.nii exists
filename = "fetal_brain_2"
self.assertRaises(exceptions.FileNotExistent, lambda:
st.Stack.from_filename(
os.path.join(self.dir_test_data_io,
filename + ".nii.gz"))
)
self.assertRaises(exceptions.FileNotExistent, lambda:
st.Stack.from_filename(
os.path.join(self.dir_test_data_io,
filename + ".nii"))
)
def test_io_image_and_mask_1(self):
# Read *.nii + *_mask.nii
filename = "fetal_brain_3"
stack = st.Stack.from_filename(
os.path.join(self.dir_test_data_io, filename + ".nii"),
os.path.join(self.dir_test_data_io, filename + "_mask.nii")
)
# If everything was correctly read the mask will have zeros and ones
# in mask
nda_mask = sitk.GetArrayFromImage(stack.sitk_mask)
self.assertEqual(nda_mask.prod(), 0)
def test_io_image_and_mask_2(self):
# Read *.nii + *_mask.nii.gz
filename = "fetal_brain_4"
stack = st.Stack.from_filename(
os.path.join(self.dir_test_data_io, filename + ".nii"),
os.path.join(self.dir_test_data_io, filename + "_mask.nii.gz")
)
# If everything was correctly read the mask will have zero and ones
nda_mask = sitk.GetArrayFromImage(stack.sitk_mask)
self.assertEqual(nda_mask.prod(), 0)
def test_io_image_and_mask_3(self):
# Read *.nii.gz + *_mask.nii
filename = "fetal_brain_5"
stack = st.Stack.from_filename(
os.path.join(self.dir_test_data_io, filename + ".nii.gz"),
os.path.join(self.dir_test_data_io, filename + "_mask.nii")
)
# If everything was correctly read the mask will have zero and ones
nda_mask = sitk.GetArrayFromImage(stack.sitk_mask)
self.assertEqual(nda_mask.prod(), 0)
def test_io_image_and_mask_4(self):
# Read *.nii.gz + *_mask.nii.gz
filename = "fetal_brain_6"
stack = st.Stack.from_filename(
os.path.join(self.dir_test_data_io, filename + ".nii.gz"),
os.path.join(self.dir_test_data_io, filename + "_mask.nii.gz")
)
# If everything was correctly read the mask will have zero and ones
nda_mask = sitk.GetArrayFromImage(stack.sitk_mask)
self.assertEqual(nda_mask.prod(), 0)
def test_update_write_transform(self):
motion_simulator = ms.RandomRigidMotionSimulator(
dimension=3,
angle_max_deg=20,
translation_max=30)
filenames = ["fetal_brain_%d" % d for d in range(3)]
stacks = [st.Stack.from_filename(
os.path.join(self.dir_test_data, "%s.nii.gz" % f))
for f in filenames]
# Generate random motions for all slices of each stack
motions_sitk = {f: {} for f in filenames}
for i, stack in enumerate(stacks):
motion_simulator.simulate_motion(
seed=i, simulations=stack.get_number_of_slices())
motions_sitk[stack.get_filename()] = \
motion_simulator.get_transforms_sitk()
# Apply random motion to all slices of all stacks
dir_output = os.path.join(DIR_TMP, "test_update_write_transform")
for i, stack in enumerate(stacks):
for j, slice in enumerate(stack.get_slices()):
slice.update_motion_correction(
motions_sitk[stack.get_filename()][j])
# Write stacks to directory
stack.write(dir_output, write_slices=True, write_transforms=True)
# Read written stacks/slices/transformations
data_reader = dr.ImageSlicesDirectoryReader(
dir_output)
data_reader.read_data()
stacks_2 = data_reader.get_data()
data_reader = dr.SliceTransformationDirectoryReader(
dir_output)
data_reader.read_data()
transformations_dic = data_reader.get_data()
filenames_2 = [s.get_filename() for s in stacks_2]
for i, stack in enumerate(stacks):
stack_2 = stacks_2[filenames_2.index(stack.get_filename())]
slices = stack.get_slices()
slices_2 = stack_2.get_slices()
# test number of slices match
self.assertEqual(len(slices), len(slices_2))
# Test whether header of written slice coincides with transformed
# slice
for j in range(stack.get_number_of_slices()):
# Check Spacing
self.assertAlmostEqual(
np.max(np.abs(np.array(slices[j].sitk.GetSpacing()) -
np.array(slices_2[j].sitk.GetSpacing()))),
0, places=10)
# Check Origin
self.assertAlmostEqual(
np.max(np.abs(np.array(slices[j].sitk.GetOrigin()) -
np.array(slices_2[j].sitk.GetOrigin()))),
0, places=4)
# Check Direction
self.assertAlmostEqual(
np.max(np.abs(np.array(slices[j].sitk.GetDirection()) -
np.array(slices_2[j].sitk.GetDirection()))),
0, places=4)
# Test whether parameters of written slice transforms match
params = np.array(
motions_sitk[stack.get_filename()][j].GetParameters())
params_2 = np.array(
transformations_dic[stack.get_filename()][j].GetParameters())
self.assertAlmostEqual(
np.max(np.abs(params - params_2)), 0, places=16)