├── .coveragerc ├── .github └── workflows │ ├── python-package.yml │ └── python-publish.yml ├── .gitignore ├── AUTHOR ├── CONTRIBUTE.md ├── LICENCE.txt ├── README.md ├── coverage.svg ├── examples ├── __init__.py ├── a_generate_phantoms_for_examples.py ├── caliber_usage_distances_example.py ├── caliber_usage_example.py ├── cleaning_before_after.png ├── manipulator_example.py ├── prototype_check_imperfections.py ├── prototype_clean_a_segmentation.py ├── prototype_open_segmentations_in_freeview.py ├── prototype_segment_an_image.py ├── prototype_symmetrise_a_segmentation.py ├── simple_label_fusion.py └── simple_relabelling_example.py ├── logo_low.png ├── nilabels ├── __init__.py ├── agents │ ├── __init__.py │ ├── agents_controller.py │ ├── checker.py │ ├── fuser.py │ ├── header_controller.py │ ├── intensities_manipulator.py │ ├── labels_manipulator.py │ ├── math.py │ ├── measurer.py │ ├── segmenter.py │ ├── shape_manipulator.py │ └── symmetrizer.py ├── definitions.py └── tools │ ├── __init__.py │ ├── aux_methods │ ├── __init__.py │ ├── label_descriptor_manager.py │ ├── morpological_operations.py │ ├── sanity_checks.py │ ├── utils.py │ ├── utils_nib.py │ ├── utils_path.py │ └── utils_rotations.py │ ├── caliber │ ├── __init__.py │ ├── distances.py │ └── volumes_and_values.py │ ├── cleaning │ ├── __init__.py │ └── labels_cleaner.py │ ├── detections │ ├── __init__.py │ ├── check_imperfections.py │ ├── contours.py │ ├── get_segmentation.py │ └── island_detection.py │ ├── image_colors_manipulations │ ├── __init__.py │ ├── cutter.py │ ├── normaliser.py │ ├── relabeller.py │ └── segmentation_to_rgb.py │ ├── image_shape_manipulations │ ├── __init__.py │ ├── apply_passepartout.py │ ├── merger.py │ └── splitter.py │ └── visualiser │ ├── __init__.py │ ├── graphs_and_stats.py │ ├── see_volume.py │ └── volume_manipulations_for_visualisation.py ├── poetry.lock ├── pyproject.toml ├── requirements.txt └── tests ├── __init__.py ├── agents ├── __init__.py └── test_main_app_agent.py └── tools ├── __init__.py ├── decorators_tools.py ├── test_aux_methods_labels_descriptor_manager.py ├── test_aux_methods_morphological_operations.py ├── test_aux_methods_permutations.py ├── test_aux_methods_sanity_checks.py ├── test_aux_methods_utils.py ├── test_aux_methods_utils_nib.py ├── test_aux_methods_utils_path.py ├── test_aux_methods_utils_rotations.py ├── test_caliber_distances.py ├── test_caliber_volumes_and_values.py ├── test_cleaning_labels_cleaner.py ├── test_detections_check_imperfections.py ├── test_detections_contours.py ├── test_detections_get_segmentation.py ├── test_detections_island_detection.py ├── test_image_colors_manip_cutter.py ├── test_image_colors_manip_normaliser.py ├── test_image_colors_manip_relabeller.py ├── test_image_colors_manip_segm_to_rgb.py ├── test_image_shape_manip_apply_passepartout.py ├── test_image_shape_manip_merger.py ├── test_image_shape_manip_splitter.py └── test_labels_checker.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = nilabels 3 | include = */nilabels/*, 4 | omit = tools/visualiser/* 5 | nilabels/agents/* 6 | 7 | [report] 8 | include = */nilabels/*, 9 | omit = nilabels/tools/visualiser/* 10 | nilabels/agents/* 11 | */__init__.py -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.9", "3.10", "3.11"] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install ruff pytest 31 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 32 | - name: Lint with ruff 33 | run: | 34 | ruff check 35 | - name: Test with pytest 36 | run: | 37 | pytest 38 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | experiments 4 | dist 5 | MANIFEST 6 | *.egg-info 7 | build 8 | data_examples 9 | data_output 10 | fusing_labels 11 | *.pyc 12 | todo_list.txt 13 | z_* 14 | a_experiments 15 | .coverage 16 | .coverage.sie* 17 | paper 18 | .pytest_cache 19 | htmlcov 20 | -------------------------------------------------------------------------------- /AUTHOR: -------------------------------------------------------------------------------- 1 | sebastiano ferraris 2 | Dzhoshkun Ismail Shakir -------------------------------------------------------------------------------- /CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | # Contributing to NiLabels 2 | 3 | Thank you your help! 4 | 5 | NiLabels (ex LABelsToolkit) started as a python package containing a range of heterogeneous imaging tools to perform 6 | quick manipulations and measurement on segmentations from ipython or jupyter notebook. 7 | Initially planned to support several projects undertook by the initial author, after some development and refactoring 8 | it is now intended to be part of the Nipy ecosystem, to provide the neuroimaging developer community with another tool. 9 | 10 | ## Code of Conduct 11 | 12 | This project adopts the [Covenant Code of Conduct](https://contributor-covenant.org/). 13 | By participating, you are expected to uphold this code. 14 | 15 | ## Before starting 16 | 17 | Please familiarise with the design pattern and the nomenclature employed. 18 | 19 | + **tools:** core methods are all there, divivded by final intended aim. A tool acts on the numpy arrays or on 20 | instances of nibabel images. 21 | + **agents** are facades collecting all the tools, and make them act directly on the paths to the nifti images. 22 | + **main:** is facade of the facades under agents folder package. This collects all the methods under 23 | the agents facades, therefore accessing to all the tools. 24 | 25 | Typical usage in an ipython session involves importing the main facade, and then some tab completition to browse 26 | the provided methods. 27 | 28 | ## Contributions: Questions, bugs, issues and new features 29 | 30 | + For any issue bugs or question related to the code, please raise an issue in the 31 | [nilabels issue page](https://github.com/SebastianoF/nilabels/issues). 32 | 33 | + Propose here as well improvements suggestions and new features. 34 | 35 | + **Please use a new issue for each thread:** make your issue re-usable and reachable by other users that may have 36 | encountered a similar problem. 37 | 38 | + If you forked the repository and made some contributions that you would like to integrate in the git master branch, 39 | you can do a [git pull request](https://yangsu.github.io/pull-request-tutorial/). Please **check tests are all passed** 40 | before this. 41 | 42 | ## To update the coverage badge 43 | 44 | + `pip install coverage-badge` 45 | + `coverage-badge > coverage.svg` in the root folder 46 | 47 | ## Styleguides 48 | 49 | + The code follows the [PEP-8](https://www.python.org/dev/peps/pep-0008/) style convention. 50 | + Please follow the [ITK standard prefix commit message convention](https://itk.org/Wiki/ITK/Git/Develop) for commit messages. 51 | + Please use the prefix `pfi_` and `pfo_` for the variable names containing path to files and path to folders respectively 52 | 53 | ## To-Do list and work in progress 54 | 55 | Please see under [todo wiki-page](https://github.com/SebastianoF/nilabel/wiki/Work-in-Progress) 56 | for the future intended future work and directions. 57 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sebastiano Ferraris 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![coverage](https://github.com/nipy/nilabels/blob/master/coverage.svg)](https://github.com/SebastianoF/nilabels/blob/master/coverage.svg) 3 | [![example workflow](https://github.com/nipy/nilabels/actions/workflows/python-package.yml/badge.svg?style=svg)](https://github.com/nipy/nilabels/actions/workflows/python-package.yml/badge.svg) 4 | [![PyPI version](https://badge.fury.io/py/nilabels.svg)](https://badge.fury.io/py/nilabels) 5 | [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) 6 | 7 | 8 |

9 | 10 |

11 | 12 | # NiLabels 13 | 14 | NiLabels is a cacophony of tools to automate simple manipulations and measurements of medical image 15 | segmentations in nifti format. It is strongly based on and influenced by the library [NiBabel](http://nipy.org/nibabel/) 16 | 17 | + Written in [Python 3](https://docs.python-guide.org/) 18 | + [Motivations](https://github.com/SebastianoF/nilabels/wiki/Motivations) 19 | + [Features](https://github.com/SebastianoF/nilabels/wiki/What-you-can-do-with-nilabels) 20 | + [Design pattern](https://github.com/SebastianoF/nilabels/wiki/Design-Pattern) 21 | + [Work in progress](https://github.com/SebastianoF/nilabels/wiki/Work-in-Progress) 22 | 23 | ## Introductory examples 24 | 25 | ### 1 Manipulate labels: relabel 26 | 27 | Given a segmentation, imagine you want to change the labels values from [1, 2, 3, 4, 5, 6] to [2, 12, 4, 7, 5, 6] 28 | and save the result in `my_new_segm.nii.gz`. Then: 29 | 30 | ```python 31 | import nilabels as nil 32 | 33 | 34 | nil_app = nil.App() 35 | nil_app.manipulate_labels.relabel('my_segm.nii.gz', 'my_new_segm.nii.gz', [1, 2, 3, 4, 5, 6], [2, 12, 4, 7, 5, 6]) 36 | 37 | ``` 38 | 39 | ### 2 Manipulate labels: clean a segmentation 40 | 41 | Given a parcellation for which we expect a single connected component per label, we want to have it cleaned from all the 42 | extra components, merging them with the closest labels. 43 | 44 | ```python 45 | import nilabels as nil 46 | 47 | 48 | nil_app = nil.App() 49 | 50 | nil_app.check.number_connected_components_per_label('noisy_segm.nii.gz', where_to_save_the_log_file='before_cleaning.txt') 51 | nil_app.manipulate_labels.clean_segmentation('noisy_segm.nii.gz', 'cleaned_segm.nii.gz', force_overwriting=True) 52 | nil_app.check.number_connected_components_per_label('cleaned_segm.nii.gz', where_to_save_the_log_file='after_cleaning.txt') 53 | 54 | ``` 55 |

56 | 57 |

58 | 59 | Before cleaning `check.number_connected_components_per_label` would return: 60 | 61 | ```text 62 | 63 | Label 0 has 1 connected components 64 | Label 1 has 13761 connected components 65 | Label 2 has 14175 connected components 66 | Label 3 has 14373 connected components 67 | Label 4 has 1016 connected components 68 | Label 5 has 806 connected components 69 | Label 6 has 816 connected components 70 | Label 7 has 1281 connected components 71 | Label 8 has 977 connected components 72 | Label 9 has 746 connected components 73 | ``` 74 | 75 | The same command after cleaning: 76 | 77 | ```text 78 | Label 0 has 1 connected components 79 | Label 1 has 1 connected components 80 | Label 2 has 1 connected components 81 | Label 3 has 1 connected components 82 | Label 4 has 1 connected components 83 | Label 5 has 1 connected components 84 | Label 6 has 1 connected components 85 | Label 7 has 1 connected components 86 | Label 8 has 1 connected components 87 | Label 9 has 1 connected components 88 | ``` 89 | 90 | More tools are introduced in the [documentation](https://github.com/SebastianoF/nilabels/wiki/What-you-can-do-with-nilabels). 91 | 92 | ## Instructions 93 | 94 | + [Documentation](https://github.com/SebastianoF/nilabels/wiki) 95 | + [How to install](https://github.com/SebastianoF/nilabels/wiki/Instructions) 96 | + [How to run the tests](https://github.com/SebastianoF/nilabels/wiki/Testing) 97 | 98 | ## Development 99 | 100 | `nilabel` is a python package managed with [poetry](https://python-poetry.org/) and linted with [ruff](https://docs.astral.sh/ruff/), tested with [pytest](https://docs.pytest.org/en/8.0.x/) 101 | 102 | ## TODO 103 | 104 | Other than the many TODOs around the code, there are two more things: 105 | 106 | + typechecking with mypy 107 | + migrate from cicleCI to github workflows 108 | 109 | ## Licencing and Copyright 110 | 111 | Copyright (c) 2017, Sebastiano Ferraris. NiLabels (ex. [LABelsToolkit](https://github.com/SebastianoF/LABelsToolkit)) 112 | is provided as it is and it is available as free open-source software under 113 | [MIT License](https://github.com/SebastianoF/nilabels/blob/master/LICENCE.txt) 114 | 115 | ## Acknowledgements 116 | 117 | + This repository had begun within the [GIFT-surg research project](http://www.gift-surg.ac.uk). 118 | + This work was supported by Wellcome / Engineering and Physical Sciences Research Council (EPSRC) [WT101957; NS/A000027/1; 203145Z/16/Z]. 119 | Sebastiano Ferraris is supported by the EPSRC-funded UCL Centre for Doctoral Training in Medical Imaging (EP/L016478/1) and Doctoral Training Grant (EP/M506448/1). 120 | -------------------------------------------------------------------------------- /coverage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | coverage 17 | coverage 18 | 92% 19 | 92% 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/nilabels/4a61cd95e6bfb3644a3724461502d7cf69f5615d/examples/__init__.py -------------------------------------------------------------------------------- /examples/caliber_usage_distances_example.py: -------------------------------------------------------------------------------- 1 | from os.path import join as jph 2 | 3 | from nilabels.agents.agents_controller import AgentsController as LT 4 | from nilabels.definitions import root_dir 5 | from nilabels.tools.caliber.distances import covariance_distance, dice_score, hausdorff_distance 6 | 7 | if __name__ == "__main__": 8 | 9 | # Paths to input 10 | pfo_examples = jph(root_dir, "data_examples") 11 | pfi_seg1 = jph(pfo_examples, "fourfolds_one.nii.gz") 12 | pfi_seg2 = jph(pfo_examples, "fourfolds_two.nii.gz") 13 | where_to_save = None 14 | 15 | # Instantiate a Labels Manager class 16 | m = LT() 17 | m.measure.return_mm3 = False 18 | 19 | # get the measure 20 | d = m.measure.dist(pfi_seg1, pfi_seg2, 21 | metrics=(dice_score, hausdorff_distance, covariance_distance), 22 | where_to_save=where_to_save) 23 | 24 | print(d) 25 | -------------------------------------------------------------------------------- /examples/caliber_usage_example.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os.path import join as jph 3 | 4 | import numpy as np 5 | 6 | from nilabels.agents.agents_controller import AgentsController as NiL 7 | from nilabels.definitions import root_dir 8 | 9 | if __name__ == "__main__": 10 | 11 | examples_folder = jph(root_dir, "data_examples") 12 | 13 | pfi_im = jph(examples_folder, "cubes_in_space.nii.gz") 14 | pfi_im_bin = jph(examples_folder, "cubes_in_space_bin.nii.gz") 15 | 16 | for p in [pfi_im, pfi_im_bin, examples_folder]: 17 | if not os.path.exists(p): 18 | raise OSError("Run lm.tools.benchmarking.generate_images_examples.py to create the images examples before this, please.") 19 | 20 | # total volume: 21 | m = NiL() 22 | print("The image contains 4 cubes of sides 11, 17, 19 and 9:\n") 23 | print(f"11**3 + 17**3 + 19**3 + 9**3 = {11**3 + 17**3 + 19**3 + 9**3} ") 24 | print("sa.get_total_volume() = {} ".format(m.measure.get_total_volume(pfi_im)["Volume"].values)) 25 | # Get volumes per label: 26 | print("The 4 cubes of sides 11, 17, 19 and 9 are labelled 1, 2, 3 and 4 resp.:") 27 | print("Volume measured label 1 = {}".format(m.measure.volume(pfi_im, labels=1)["Volume"].values)) 28 | print(f"11**3 = {11 ** 3}") 29 | print("Volume measured label 2 = {}".format(m.measure.volume(pfi_im, labels=2)["Volume"].values)) 30 | print(f"17**3 = {17 ** 3}") 31 | print("Volume measured label 3 = {}".format(m.measure.volume(pfi_im, labels=3)["Volume"].values)) 32 | print(f"19**3 = {19 ** 3}") 33 | print("Volume measured label 4 = {}".format(m.measure.volume(pfi_im, labels=4)["Volume"].values)) 34 | print(f"9**3 = {9 ** 3}") 35 | print("Volume measured labels ([1, 3]) = {}".format(m.measure.volume(pfi_im, labels=[[1, 3]])["Volume"].values)) 36 | print(f"11**3 + 19**3 = {11**3 + 19**3}") 37 | print("\nTo sum up: \n") 38 | print(f"Volume measured labels ([1, 2, 3, 4, [1, 3]]) = \n{m.measure.volume(pfi_im, labels=[1, 2, 3, 4, [1, 3], [1, 2, 3, 4]])}\n") 39 | print(f"Total volume = {m.measure.get_total_volume(pfi_im)} \n") 40 | print("------------") 41 | # Get volumes under each label, given the image weight, corresponding to the label itself: 42 | vals_below_labels = m.measure.values_below_labels(pfi_im, pfi_im, labels=[1, 2, 3, 4, 5, [1, 3]]) 43 | print(f"average below labels [1, 2, 3, 4, [1, 3]] = \n{vals_below_labels}") 44 | print("mu, std below label 1 = {} {}".format(np.mean(vals_below_labels["1"]), np.std(vals_below_labels["1"]))) 45 | print("mu, std below label 2 = {} {}".format(np.mean(vals_below_labels["2"]), np.std(vals_below_labels["2"]))) 46 | print("mu, std below label 3 = {} {}".format(np.mean(vals_below_labels["3"]), np.std(vals_below_labels["3"]))) 47 | print("mu, std below label 4 = {} {}".format(np.mean(vals_below_labels["4"]), np.std(vals_below_labels["4"]))) 48 | # print('mu, std below label 5 = {} {}'.format(np.mean(vals_below_labels['5']), np.std(vals_below_labels['5']))) 49 | print("mu, std below label [1, 3] = {} {}".format(np.mean(vals_below_labels["[1, 3]"]), np.std(vals_below_labels["[1, 3]"]))) 50 | 51 | print(f"\nValues as they are reported: {vals_below_labels}") 52 | 53 | -------------------------------------------------------------------------------- /examples/cleaning_before_after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/nilabels/4a61cd95e6bfb3644a3724461502d7cf69f5615d/examples/cleaning_before_after.png -------------------------------------------------------------------------------- /examples/prototype_check_imperfections.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os.path import join as jph 3 | 4 | import a_generate_phantoms_for_examples as gen 5 | 6 | from nilabels.agents.agents_controller import AgentsController as NiL 7 | from nilabels.definitions import root_dir 8 | from nilabels.tools.aux_methods.label_descriptor_manager import generate_dummy_label_descriptor 9 | 10 | # ---- GENERATE DATA ---- 11 | 12 | 13 | if not os.path.exists(jph(root_dir, "data_examples", "ellipsoids_seg.nii.gz")): 14 | 15 | creation_list = {"Examples folder" : True, 16 | "Punt e mes" : False, 17 | "C" : False, 18 | "Planetaruim" : False, 19 | "Buckle ellipsoids" : True, 20 | "Ellipsoids family" : False, 21 | "Cubes in the sky" : False, 22 | "Sandwich" : False, 23 | "Four-folds" : False} 24 | 25 | gen.generate_figures(creation_list) 26 | 27 | # ---- PATH MANAGER ---- 28 | 29 | pfi_input_segm = jph(root_dir, "data_examples", "ellipsoids_seg.nii.gz") 30 | 31 | # ---- CREATE LABELS DESCRIPTOR FOR PHANTOM ellipsoids with 0 to 6 labels ---- 32 | 33 | pfi_labels_descriptor = jph(root_dir, "data_examples", "labels_descriptor_ellipsoids.txt") 34 | generate_dummy_label_descriptor(pfi_labels_descriptor, list_labels=[0, 1, 4, 5, 6, 7, 8]) # add extra labels to test 35 | 36 | # ---- PERFORM the check ---- 37 | 38 | la = NiL() 39 | in_descriptor_not_delineated, delineated_not_in_descriptor = la.check.missing_labels(pfi_input_segm, pfi_labels_descriptor, pfi_where_to_save_the_log_file=None) 40 | 41 | # Print expected to be seen in the terminal: set([8, 7]) set([2, 3]) 42 | -------------------------------------------------------------------------------- /examples/prototype_clean_a_segmentation.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os.path import join as jph 3 | 4 | import a_generate_phantoms_for_examples as gen 5 | import nibabel as nib 6 | import numpy as np 7 | 8 | import nilabels as nil 9 | from nilabels.definitions import root_dir 10 | 11 | # ---- GENERATE DATA ---- 12 | 13 | 14 | if not os.path.exists(jph(root_dir, "data_examples", "ellipsoids.nii.gz")): 15 | creation_list = { 16 | "Examples folder": True, 17 | "Punt e mes": False, 18 | "C": False, 19 | "Planetaruim": False, 20 | "Buckle ellipsoids": True, 21 | "Ellipsoids family": False, 22 | "Cubes in the sky": False, 23 | "Sandwich": False, 24 | "Four-folds": False, 25 | } 26 | 27 | gen.generate_figures(creation_list) 28 | 29 | # ---- PATH MANAGER ---- 30 | 31 | # input 32 | pfi_input_anatomy = jph(root_dir, "data_examples", "ellipsoids.nii.gz") 33 | pfi_input_segmentation_noisy = jph(root_dir, "data_examples", "ellipsoids_seg_noisy.nii.gz") 34 | pfo_output_folder = jph(root_dir, "data_output") 35 | 36 | assert os.path.exists(pfi_input_anatomy), pfi_input_anatomy 37 | assert os.path.exists(pfi_input_segmentation_noisy), pfi_input_segmentation_noisy 38 | assert os.path.exists(pfo_output_folder), pfo_output_folder 39 | 40 | # Output 41 | log_file_before_cleaning = jph(pfo_output_folder, "log_before_cleaning.txt") 42 | pfi_output_cleaned_segmentation = jph(pfo_output_folder, "ellipsoids_segm_cleaned.nii.gz") 43 | log_file_after_cleaning = jph(pfo_output_folder, "log_after_cleaning.txt") 44 | pfi_differece_cleaned_non_cleaned = jph(pfo_output_folder, "difference_half_cleaned_uncleaned.nii.gz") 45 | 46 | 47 | # ---- PROCESS ---- 48 | 49 | nl = nil.App() 50 | 51 | # get the report before cleaning 52 | nl.check.number_connected_components_per_label( 53 | pfi_input_segmentation_noisy, where_to_save_the_log_file=log_file_before_cleaning, 54 | ) 55 | 56 | print("Wanted final number of components per label:") 57 | im_input_segmentation_noisy = nib.load(pfi_input_segmentation_noisy) 58 | correspondences_labels_components = [[k, 1] for k in range(np.max(im_input_segmentation_noisy.get_fdata()) + 1)] 59 | print(correspondences_labels_components) 60 | 61 | # get the cleaned segmentation 62 | nl.manipulate_labels.clean_segmentation( 63 | pfi_input_segmentation_noisy, 64 | pfi_output_cleaned_segmentation, 65 | labels_to_clean=correspondences_labels_components, 66 | force_overwriting=True, 67 | ) 68 | 69 | # get the report of the connected components afterwards 70 | nl.check.number_connected_components_per_label( 71 | pfi_output_cleaned_segmentation, where_to_save_the_log_file=log_file_after_cleaning, 72 | ) 73 | 74 | # ---- GET DIFFERENCE ---- 75 | 76 | cmd = f"seg_maths {pfi_input_segmentation_noisy} -sub {pfi_output_cleaned_segmentation} {pfi_differece_cleaned_non_cleaned}" 77 | os.system(cmd) 78 | cmd = f"seg_maths {pfi_differece_cleaned_non_cleaned} -bin {pfi_differece_cleaned_non_cleaned}" 79 | os.system(cmd) 80 | 81 | # ---- VISUALISE OUTPUT ---- 82 | 83 | opener1 = f"itksnap -g {pfi_input_anatomy} -s {pfi_input_segmentation_noisy}" 84 | opener2 = f"itksnap -g {pfi_input_anatomy} -s {pfi_output_cleaned_segmentation}" 85 | opener3 = f"itksnap -g {pfi_input_anatomy} -s {pfi_differece_cleaned_non_cleaned}" 86 | 87 | os.system(opener1) 88 | os.system(opener2) 89 | os.system(opener3) 90 | -------------------------------------------------------------------------------- /examples/prototype_open_segmentations_in_freeview.py: -------------------------------------------------------------------------------- 1 | """Use ITK-snap, its labels_descriptor.txt and freeview to get the surfaces and overlay the surface to the main image 2 | directly in freeview with correct naming convention. 3 | """ 4 | 5 | import os 6 | 7 | from nilabels.tools.aux_methods.label_descriptor_manager import LabelsDescriptorManager 8 | 9 | 10 | def freesurfer_surface_overlayed(pfi_anatomy, pfo_stl_surfaces, pfi_descriptor, convention_descriptor="itk-snap", 11 | suffix_surf="surf", add_colors=True, labels_to_delineate="all"): 12 | """Manual step: from a segmentation export all the labels in stand-alone .stl files with ITK-snap, in a folder 13 | pfo_stl_surfaces, with suffix suffix_surf 14 | :param pfi_anatomy: 15 | :param pfo_stl_surfaces: 16 | :param pfi_descriptor: 17 | :param convention_descriptor: 18 | :param suffix_surf: 19 | :param add_colors: 20 | :param labels_to_delineate: 21 | :return: 22 | """ 23 | ldm = LabelsDescriptorManager(pfi_descriptor, labels_descriptor_convention=convention_descriptor) 24 | 25 | cmd = f"source $FREESURFER_HOME/SetUpFreeSurfer.sh; freeview -v {pfi_anatomy} -f " 26 | 27 | if labels_to_delineate: 28 | labels_to_delineate = ldm.dict_label_descriptor.keys()[1:-1] 29 | 30 | for k in labels_to_delineate: 31 | 32 | pfi_surface = os.path.join(pfo_stl_surfaces, f"{suffix_surf}{k:05d}.stl") 33 | assert os.path.exists(pfi_surface), pfi_surface 34 | if add_colors: 35 | triplet_rgb = f"{ldm.dict_label_descriptor[k][0][0]},{ldm.dict_label_descriptor[k][0][1]},{ldm.dict_label_descriptor[k][0][2]}" 36 | 37 | cmd += f" {pfi_surface}:edgecolor={triplet_rgb}:color={triplet_rgb} " 38 | else: 39 | cmd += f" {pfi_surface} " 40 | os.system(cmd) 41 | 42 | 43 | if __name__ == "__main__": 44 | print("Step 0: create segmented atlas with phantom generator.") 45 | print("Step 1: Manual step - open the segmentation in ITK-Snap and export all the surfaces in .stl in the " 46 | "specified folder") 47 | print("Step 2: run freesurfer_surface_overlayed") 48 | -------------------------------------------------------------------------------- /examples/prototype_segment_an_image.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os.path import join as jph 3 | 4 | import a_generate_phantoms_for_examples as gen 5 | 6 | from nilabels.agents.agents_controller import AgentsController as NiL 7 | from nilabels.definitions import root_dir 8 | 9 | # ---- GENERATE DATA ---- 10 | 11 | 12 | if not os.path.exists(jph(root_dir, "data_examples", "ellipsoids.nii.gz")): 13 | 14 | creation_list = {"Examples folder" : True, 15 | "Punt e mes" : False, 16 | "C" : False, 17 | "Planetaruim" : False, 18 | "Buckle ellipsoids" : True, 19 | "Ellipsoids family" : False, 20 | "Cubes in the sky" : False, 21 | "Sandwich" : False, 22 | "Four-folds" : False} 23 | 24 | gen.generate_figures(creation_list) 25 | 26 | 27 | # ---- PATH MANAGER ---- 28 | 29 | # input: 30 | pfi_input_anatomy = jph(root_dir, "data_examples", "ellipsoids.nii.gz") 31 | pfo_output_folder = jph(root_dir, "data_output") 32 | 33 | assert os.path.exists(pfi_input_anatomy), pfi_input_anatomy 34 | assert os.path.exists(pfo_output_folder) 35 | 36 | # output: 37 | pfi_intensities_segmentation = jph(pfo_output_folder, "ellipsoids_segm_int.nii.gz") 38 | pfi_otsu_segmentation = jph(pfo_output_folder, "ellipsoids_segm_otsu.nii.gz") 39 | pfi_mog_segmentation_crisp = jph(pfo_output_folder, "ellipsoids_segm_mog_crisp.nii.gz") 40 | pfi_mog_segmentation_prob = jph(pfo_output_folder, "ellipsoids_segm_mog_prob.nii.gz") 41 | 42 | print("---- PROCESS 1: intensities segmentation ----") 43 | 44 | la = NiL() 45 | la.segment.simple_intensities_thresholding(pfi_input_anatomy, pfi_intensities_segmentation, number_of_levels=5) 46 | 47 | print("---- PROCESS 2: Otsu ----") 48 | 49 | la = NiL() 50 | la.segment.otsu_thresholding(pfi_input_anatomy, pfi_otsu_segmentation, side="above", return_as_mask=False) 51 | 52 | print("---- PROCESS 2: MoG ----") 53 | 54 | la = NiL() 55 | la.segment.mixture_of_gaussians(pfi_input_anatomy, pfi_mog_segmentation_crisp, pfi_mog_segmentation_prob, 56 | K=5, see_histogram=True) 57 | 58 | print("---- VIEW ----") 59 | 60 | opener1 = f"itksnap -g {pfi_input_anatomy} -s {pfi_intensities_segmentation}" 61 | opener2 = f"itksnap -g {pfi_input_anatomy} -s {pfi_otsu_segmentation}" 62 | opener3 = f"itksnap -g {pfi_input_anatomy} -s {pfi_mog_segmentation_crisp}" 63 | opener4 = f"itksnap -g {pfi_input_anatomy} -o {pfi_mog_segmentation_prob}" 64 | 65 | os.system(opener1) 66 | os.system(opener2) 67 | os.system(opener3) 68 | os.system(opener4) 69 | -------------------------------------------------------------------------------- /examples/prototype_symmetrise_a_segmentation.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os.path import join as jph 3 | 4 | import a_generate_phantoms_for_examples as gen 5 | 6 | from nilabels.agents.agents_controller import AgentsController as NiL 7 | from nilabels.definitions import root_dir 8 | 9 | # ---- GENERATE DATA ---- 10 | 11 | 12 | if not os.path.exists(jph(root_dir, "data_examples", "ellipsoids.nii.gz")): 13 | 14 | creation_list = {"Examples folder" : False, 15 | "Punt e mes" : False, 16 | "C" : False, 17 | "Planetaruim" : False, 18 | "Buckle ellipsoids" : True, 19 | "Ellipsoids family" : False, 20 | "Cubes in the sky" : False, 21 | "Sandwich" : False, 22 | "Four-folds" : False} 23 | 24 | gen.generate_figures(creation_list) 25 | 26 | 27 | # ---- PATH MANAGER ---- 28 | 29 | 30 | # input 31 | pfi_input_anatomy = jph(root_dir, "data_examples", "ellipsoids.nii.gz") 32 | pfi_input_segmentation = jph(root_dir, "data_examples", "ellipsoids_seg_half.nii.gz") 33 | pfo_output_folder = jph(root_dir, "data_output") 34 | 35 | assert os.path.exists(pfi_input_anatomy), pfi_input_anatomy 36 | assert os.path.exists(pfi_input_segmentation), pfi_input_segmentation 37 | assert os.path.exists(pfo_output_folder), pfo_output_folder 38 | 39 | # output 40 | pfi_output_segmentation = jph(root_dir, "data_examples", "ellipsoids_seg_symmetrised.nii.gz") 41 | 42 | 43 | # ---- LABELS LIST ---- 44 | 45 | 46 | labels_central = [] 47 | labels_left = [1, 2, 3, 4, 5, 6] 48 | labels_right = [a + 10 for a in labels_left] 49 | 50 | labels_sym_left = labels_left + labels_central 51 | labels_sym_right = labels_right + labels_central 52 | 53 | 54 | # --- EXECUTE ---- 55 | 56 | 57 | lt = NiL() 58 | lt.symmetrize.symmetrise_with_registration(pfi_input_anatomy, 59 | pfi_input_segmentation, 60 | labels_sym_left, 61 | pfi_output_segmentation, 62 | results_folder_path=pfo_output_folder, 63 | list_labels_transformed=labels_sym_right, 64 | coord="z", 65 | reuse_registration=False) 66 | 67 | # --- SEE RESULTS ---- 68 | 69 | opener1 = f"itksnap -g {pfi_input_anatomy} -s {pfi_input_segmentation}" 70 | opener2 = f"itksnap -g {pfi_input_anatomy} -s {pfi_output_segmentation}" 71 | 72 | os.system(opener1) 73 | os.system(opener2) 74 | -------------------------------------------------------------------------------- /examples/simple_label_fusion.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os.path import join as jph 3 | 4 | from nilabels.agents.agents_controller import AgentsController as NiL 5 | from nilabels.definitions import root_dir 6 | 7 | if __name__ == "__main__": 8 | 9 | run_steps = {"Generate results folders": False, 10 | "Register and propagate": False, 11 | "Fuse seg_LabFusion": True} 12 | 13 | pfo_input_dataset = jph(root_dir, "data_examples", "ellipsoids_family") 14 | pfo_results_propagation = jph(root_dir, "data_output", "results_propagation") 15 | pfo_results_label_fusion = jph(root_dir, "data_output", "results_label_fusion") 16 | 17 | if run_steps["Generate results folders"]: 18 | cmd0 = f"mkdir -p {pfo_results_propagation}" 19 | cmd1 = f"mkdir -p {pfo_results_label_fusion}" 20 | os.system(cmd0) 21 | os.system(cmd1) 22 | 23 | if run_steps["Register and propagate"]: 24 | fin_target = "target.nii.gz" 25 | fin_target_seg = "target_seg.nii.gz" 26 | 27 | for k in range(1, 11): 28 | # affine registration 29 | cmd_aff = "reg_aladin -ref {0} -flo {1} -aff {2} -res {3}".format(jph(pfo_input_dataset, "target.nii.gz"), 30 | jph(pfo_input_dataset, "ellipsoid" + str(k) + ".nii.gz"), 31 | jph(pfo_results_propagation, "aff_ellipsoid" + str(k) + "_on_target.txt"), 32 | jph(pfo_results_propagation, "aff_ellipsoid" + str(k) + "_on_target.nii.gz")) 33 | os.system(cmd_aff) 34 | # non-rigid registration 35 | cmd_nrig = "reg_f3d -ref {0} -flo {1} -cpp {2} -res {3}".format(jph(pfo_input_dataset, "target.nii.gz"), 36 | jph(pfo_results_propagation, "aff_ellipsoid" + str(k) + "_on_target.nii.gz"), 37 | jph(pfo_results_propagation, "cpp_ellipsoid" + str(k) + "_on_target.nii.gz"), 38 | jph(pfo_results_propagation, "nrig_ellipsoid" + str(k) + "_on_target.nii.gz")) 39 | os.system(cmd_nrig) 40 | # affine propagation 41 | cmd_resample_aff = "reg_resample -ref {0} -flo {1} -trans {2} -res {3} -inter 0".format( 42 | jph(pfo_input_dataset, "target.nii.gz"), 43 | jph(pfo_input_dataset, "ellipsoid" + str(k) + "_seg.nii.gz"), 44 | jph(pfo_results_propagation, "aff_ellipsoid" + str(k) + "_on_target.txt"), 45 | jph(pfo_results_propagation, "seg_aff_ellipsoid" + str(k) + "_on_target.nii.gz"), 46 | ) 47 | os.system(cmd_resample_aff) 48 | # non-rigid propagation 49 | cmd_resample_nrig = "reg_resample -ref {0} -flo {1} -trans {2} -res {3} -inter 0".format( 50 | jph(pfo_input_dataset, "target.nii.gz"), 51 | jph(pfo_results_propagation, "seg_aff_ellipsoid" + str(k) + "_on_target.nii.gz"), 52 | jph(pfo_results_propagation, "cpp_ellipsoid" + str(k) + "_on_target.nii.gz"), 53 | jph(pfo_results_propagation, "seg_nrig_ellipsoid" + str(k) + "_on_target.nii.gz"), 54 | ) 55 | os.system(cmd_resample_nrig) 56 | 57 | if run_steps["Fuse seg_LabFusion"]: 58 | 59 | # instantiate a label manager: 60 | lm = NiL(jph(root_dir, "data_examples", "ellipsoids_family"), jph(root_dir, "data_output")) 61 | 62 | # With majority voting 63 | options_seg = "-MV" 64 | 65 | list_pfi_segmentations = [jph(pfo_results_propagation, "seg_nrig_ellipsoid" + str(k) + "_on_target.nii.gz") for k in range(1, 11)] 66 | list_pfi_warped = [jph(pfo_results_propagation, "seg_nrig_ellipsoid" + str(k) + "_on_target.nii.gz") for k in range(1, 11)] 67 | 68 | lm.fuse.create_stack_for_labels_fusion("target.nii.gz", jph("results_label_fusion", "output" + options_seg + ".nii.gz"), 69 | list_pfi_segmentations, options=options_seg) 70 | 71 | # If something more sophisticated needs to be done, it returns the paths to the stacks of images: 72 | options_seg = "_test2" 73 | list_paths = lm.fuse.create_stack_for_labels_fusion("target.nii.gz", jph("results_label_fusion", "output" + options_seg + ".nii.gz"), 74 | list_pfi_segmentations, list_pfi_warped, options=options_seg, prepare_data_only=True) 75 | 76 | print(list_paths) 77 | -------------------------------------------------------------------------------- /examples/simple_relabelling_example.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os.path import join as jph 3 | 4 | from nilabels.agents.agents_controller import AgentsController as NiL 5 | from nilabels.definitions import root_dir 6 | 7 | if __name__ == "__main__": 8 | 9 | # generate output folder for examples: 10 | cmd = "mkdir -p {}".format(jph(root_dir, "data_output")) 11 | os.system(cmd) 12 | 13 | # instantiate a label manager: 14 | lt = NiL(jph(root_dir, "data_examples"), jph(root_dir, "data_output")) 15 | 16 | # data: 17 | fin_punt_seg_original = "mes_seg.nii.gz" 18 | fin_punt_seg_new = "mes_seg_relabelled.nii.gz" 19 | 20 | list_old_labels = [1, 2, 3, 4, 5, 6] 21 | list_new_labels = [2, 3, 4, 5, 6, 7] 22 | 23 | # Using the manager to relabel the data: 24 | lt.manipulate_labels.relabel(fin_punt_seg_original, fin_punt_seg_new, 25 | list_old_labels, list_new_labels) 26 | 27 | # figure before: 28 | cmd = "itksnap -g {0} -s {1}".format( 29 | jph(root_dir, "data_examples", "mes.nii.gz"), 30 | jph(root_dir, "data_examples", fin_punt_seg_original)) 31 | os.system(cmd) 32 | # figure after 33 | cmd = "itksnap -g {0} -s {1}".format( 34 | jph(root_dir, "data_examples", "mes.nii.gz"), 35 | jph(root_dir, "data_output", fin_punt_seg_new)) 36 | os.system(cmd) 37 | -------------------------------------------------------------------------------- /logo_low.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/nilabels/4a61cd95e6bfb3644a3724461502d7cf69f5615d/logo_low.png -------------------------------------------------------------------------------- /nilabels/__init__.py: -------------------------------------------------------------------------------- 1 | from .agents.agents_controller import AgentsController as App # noqa: F401 2 | -------------------------------------------------------------------------------- /nilabels/agents/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/nilabels/4a61cd95e6bfb3644a3724461502d7cf69f5615d/nilabels/agents/__init__.py -------------------------------------------------------------------------------- /nilabels/agents/agents_controller.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from nilabels.agents.checker import LabelsChecker 4 | from nilabels.agents.fuser import LabelsFuser 5 | from nilabels.agents.header_controller import HeaderController 6 | from nilabels.agents.intensities_manipulator import IntensitiesManipulator 7 | from nilabels.agents.labels_manipulator import LabelsManipulator 8 | from nilabels.agents.math import Math 9 | from nilabels.agents.measurer import LabelsMeasure 10 | from nilabels.agents.segmenter import LabelsSegmenter 11 | from nilabels.agents.shape_manipulator import ShapeManipulator 12 | from nilabels.agents.symmetrizer import SegmentationSymmetrizer 13 | 14 | 15 | class AgentsController: 16 | def __init__(self, input_data_folder=None, output_data_folder=None): 17 | """Main agent-class that access all the tools methods given input paths through agents. 18 | Each agent has a different semantic task, so that recover the tool to be applied to a path will be easier. 19 | The nomenclature and functionality of each tool is decoupled from the agents that are using them. 20 | > The input of a method in tools is typically a nibabel image. 21 | > The input of a method in the agents it typically a path to a nifti image. 22 | """ 23 | if input_data_folder is not None and not os.path.isdir(input_data_folder): 24 | raise OSError("Selected path must be None or must point to an existing folder.") 25 | 26 | self._pfo_in = input_data_folder 27 | 28 | if output_data_folder is None: 29 | self._pfo_out = input_data_folder 30 | else: 31 | self._pfo_out = output_data_folder 32 | self._set_attribute_agents() 33 | 34 | def set_input_data_folder(self, input_data_folder): 35 | self._pfo_in = input_data_folder 36 | self._set_attribute_agents() 37 | 38 | def set_output_data_folder(self, output_data_folder): 39 | self._pfo_out = output_data_folder 40 | self._set_attribute_agents() 41 | 42 | def _set_attribute_agents(self): 43 | self.manipulate_labels = LabelsManipulator(self._pfo_in, self._pfo_out) 44 | self.manipulate_intensities = IntensitiesManipulator(self._pfo_in, self._pfo_out) 45 | self.manipulate_shape = ShapeManipulator(self._pfo_in, self._pfo_out) 46 | self.measure = LabelsMeasure(self._pfo_in, self._pfo_out) 47 | self.fuse = LabelsFuser(self._pfo_in, self._pfo_out) 48 | self.symmetrize = SegmentationSymmetrizer(self._pfo_in, self._pfo_out) 49 | self.check = LabelsChecker(self._pfo_in, self._pfo_out) 50 | self.header = HeaderController(self._pfo_in, self._pfo_out) 51 | self.segment = LabelsSegmenter(self._pfo_in, self._pfo_out) 52 | self.math = Math(self._pfo_in, self._pfo_out) 53 | -------------------------------------------------------------------------------- /nilabels/agents/checker.py: -------------------------------------------------------------------------------- 1 | import nibabel as nib 2 | from scipy import ndimage 3 | 4 | from nilabels.tools.aux_methods.label_descriptor_manager import LabelsDescriptorManager 5 | from nilabels.tools.aux_methods.utils_nib import compare_two_nib 6 | from nilabels.tools.aux_methods.utils_path import connect_path_tail_head 7 | from nilabels.tools.detections.check_imperfections import check_missing_labels 8 | 9 | 10 | class LabelsChecker: 11 | """Facade of the methods in tools, for work with paths to images rather than 12 | with data. Methods under LabelsManagerFuse access label fusion methods. 13 | """ 14 | 15 | def __init__(self, input_data_folder=None, output_data_folder=None): 16 | self.pfo_in = input_data_folder 17 | self.pfo_out = output_data_folder 18 | 19 | def missing_labels( 20 | self, 21 | path_input_segmentation, 22 | path_input_labels_descriptor, 23 | pfi_where_to_save_the_log_file=None, 24 | ): 25 | pfi_segm = connect_path_tail_head(self.pfo_in, path_input_segmentation) 26 | pfi_ld = connect_path_tail_head(self.pfo_in, path_input_labels_descriptor) 27 | ldm = LabelsDescriptorManager(pfi_ld) 28 | im_se = nib.load(pfi_segm) 29 | in_descriptor_not_delineated, delineated_not_in_descriptor = check_missing_labels( 30 | im_se, 31 | ldm, 32 | pfi_where_log=pfi_where_to_save_the_log_file, 33 | ) 34 | return in_descriptor_not_delineated, delineated_not_in_descriptor 35 | 36 | def number_connected_components_per_label(self, input_segmentation, where_to_save_the_log_file=None): 37 | pfi_segm = connect_path_tail_head(self.pfo_in, input_segmentation) 38 | im = nib.load(pfi_segm) 39 | msg = f"Labels check number of connected components for segmentation {pfi_segm} \n\n" 40 | labs_list, cc_list = [], [] 41 | for lab in sorted(set(im.get_fdata().flat)): 42 | cc = ndimage.label(im.get_fdata() == lab)[1] 43 | msg_l = f"Label {lab} has {cc} connected components" 44 | print(msg_l) 45 | msg += msg_l + "\n" 46 | labs_list.append(lab) 47 | cc_list.append(cc) 48 | if where_to_save_the_log_file is not None: 49 | f = open(where_to_save_the_log_file, "w") 50 | f.write(msg) 51 | f.close() 52 | return labs_list, cc_list 53 | 54 | def compare_two_nifti_images(self, path_first_image, path_second_image): 55 | pfi_first = connect_path_tail_head(self.pfo_in, path_first_image) 56 | pfi_second = connect_path_tail_head(self.pfo_in, path_second_image) 57 | im1 = nib.load(pfi_first) 58 | im2 = nib.load(pfi_second) 59 | return compare_two_nib(im1, im2) 60 | -------------------------------------------------------------------------------- /nilabels/agents/fuser.py: -------------------------------------------------------------------------------- 1 | import nibabel as nib 2 | import numpy as np 3 | 4 | from nilabels.tools.aux_methods.utils_nib import set_new_data 5 | from nilabels.tools.aux_methods.utils_path import connect_path_tail_head 6 | 7 | 8 | class LabelsFuser: 9 | """Facade of the methods in tools, for work with paths to images rather than 10 | with data. 11 | """ 12 | 13 | def __init__(self, input_data_folder=None, output_data_folder=None): 14 | self.pfo_in = input_data_folder 15 | self.pfo_out = output_data_folder 16 | 17 | def create_stack_for_labels_fusion( 18 | self, 19 | pfi_target, 20 | pfi_result, 21 | list_pfi_segmentations, 22 | list_pfi_warped=None, 23 | seg_output_name="res_4d_seg", 24 | warp_output_name="res_4d_warp", 25 | output_tag="", 26 | ): 27 | """Stack and fuse anatomical images and segmentations in a single command. 28 | :param pfi_target: path to file to the target of the segmentation 29 | :param pfi_result: path to file where to store the result. 30 | :param list_pfi_segmentations: list of the segmentations to fuse 31 | :param list_pfi_warped: list of the warped images to fuse 32 | :param seg_output_name: 33 | :param warp_output_name: 34 | :param output_tag: additional tag output. 35 | :return: if prepare_data_only is True it returns the path to files prepared to be provided to nifty_seg, 36 | in the following order 37 | [pfi_target, pfi_result, pfi_4d_seg, pfi_4d_warp] 38 | 39 | """ 40 | pfi_target = connect_path_tail_head(self.pfo_in, pfi_target) 41 | pfi_result = connect_path_tail_head(self.pfo_out, pfi_result) 42 | # save 4d segmentations in stack_seg 43 | list_pfi_segmentations = [connect_path_tail_head(self.pfo_in, j) for j in list_pfi_segmentations] 44 | # 45 | list_stack_seg = [nib.load(pfi).get_fdata() for pfi in list_pfi_segmentations] 46 | stack_seg = np.stack(list_stack_seg, axis=3) 47 | del list_stack_seg 48 | im_4d_seg = set_new_data(nib.load(list_pfi_segmentations[0]), stack_seg) 49 | pfi_4d_seg = connect_path_tail_head(self.pfo_out, f"{seg_output_name}_{output_tag}.nii.gz") 50 | nib.save(im_4d_seg, pfi_4d_seg) 51 | 52 | # save 4d warped if available 53 | if list_pfi_warped is None: 54 | pfi_4d_warp = None 55 | else: 56 | list_pfi_warped = [connect_path_tail_head(self.pfo_in, j) for j in list_pfi_warped] 57 | # 58 | list_stack_warp = [nib.load(pfi).get_fdata() for pfi in list_pfi_warped] 59 | stack_warp = np.stack(list_stack_warp, axis=3) 60 | del list_stack_warp 61 | im_4d_warp = set_new_data(nib.load(list_pfi_warped[0]), stack_warp) 62 | pfi_4d_warp = connect_path_tail_head(self.pfo_out, f"{warp_output_name}_{output_tag}.nii.gz") 63 | nib.save(im_4d_warp, pfi_4d_warp) 64 | 65 | return pfi_target, pfi_result, pfi_4d_seg, pfi_4d_warp 66 | -------------------------------------------------------------------------------- /nilabels/agents/header_controller.py: -------------------------------------------------------------------------------- 1 | import nibabel as nib 2 | import numpy as np 3 | 4 | from nilabels.tools.aux_methods.utils_nib import ( 5 | modify_affine_transformation, 6 | modify_image_data_type, 7 | replace_translational_part, 8 | ) 9 | from nilabels.tools.aux_methods.utils_path import connect_path_tail_head, get_pfi_in_pfi_out 10 | from nilabels.tools.aux_methods.utils_rotations import get_small_orthogonal_rotation 11 | 12 | 13 | class HeaderController: 14 | """Facade of the methods in tools. symmetrizer, for work with paths to images rather than 15 | with data. Methods under LabelsManagerManipulate are taking in general 16 | one or more input manipulate them according to some rule and save the 17 | output in the output_data_folder or in the specified paths. 18 | """ 19 | 20 | def __init__(self, input_data_folder=None, output_data_folder=None): 21 | self.pfo_in = input_data_folder 22 | self.pfo_out = output_data_folder 23 | 24 | def modify_image_type(self, filename_in, filename_out, new_dtype, update_description=None, verbose=1): 25 | """Change data type and optionally update the nifti field descriptor. 26 | :param filename_in: path to filename input 27 | :param filename_out: path to filename output 28 | :param new_dtype: numpy data type compatible input 29 | :param update_description: string with the new 'descrip' nifti header value. 30 | :param verbose: 31 | :return: image with new dtype and descriptor updated. 32 | """ 33 | pfi_in, pfi_out = get_pfi_in_pfi_out(filename_in, filename_out, self.pfo_in, self.pfo_out) 34 | 35 | im = nib.load(pfi_in) 36 | new_im = modify_image_data_type(im, new_dtype=new_dtype, update_descrip_field_header=update_description, verbose=verbose) 37 | nib.save(new_im, pfi_out) 38 | 39 | def modify_affine(self, filename_in, affine_in, filename_out, q_form=True, s_form=True, 40 | multiplication_side="left"): 41 | """Modify the affine transformation by substitution or by left or right multiplication 42 | :param filename_in: path to filename input 43 | :param affine_in: path to affine matrix input, or nd.array or .npy array 44 | :param filename_out: path to filename output 45 | :param q_form: affect the q_form (True) 46 | :param s_form: affect the s_form (True) 47 | :param multiplication_side: multiplication_side: can be lef, right, or replace. 48 | :return: save new image with the updated affine transformation 49 | 50 | NOTE: please see the documentation http://nipy.org/nibabel/nifti_images.html#choosing-image-affine for more on the 51 | relationships between s_form affine, q_form affine and fall-back header affine. 52 | """ 53 | pfi_in, pfi_out = get_pfi_in_pfi_out(filename_in, filename_out, self.pfo_in, self.pfo_out) 54 | 55 | if isinstance(affine_in, str): 56 | 57 | if affine_in.endswith(".txt"): 58 | aff = np.loadtxt(connect_path_tail_head(self.pfo_in, affine_in)) 59 | else: 60 | aff = np.load(connect_path_tail_head(self.pfo_in, affine_in)) 61 | 62 | elif isinstance(affine_in, np.ndarray): 63 | aff = affine_in 64 | else: 65 | raise OSError("parameter affine_in can be path to an affine matrix .txt or .npy or the numpy array" 66 | "corresponding to the affine transformation.") 67 | 68 | im = nib.load(pfi_in) 69 | new_im = modify_affine_transformation(im, aff, q_form=q_form, s_form=s_form, 70 | multiplication_side=multiplication_side) 71 | nib.save(new_im, pfi_out) 72 | 73 | def apply_small_rotation(self, filename_in, filename_out, angle=np.pi/6, principal_axis="pitch", 74 | respect_to_centre=True): 75 | """:param filename_in: path to filename input 76 | :param filename_out: path to filename output 77 | :param angle: rotation angle in radiants 78 | :param principal_axis: 'yaw', 'pitch' or 'roll' 79 | :param respect_to_centre: by default is True. If False, respect to the origin. 80 | :return: 81 | """ 82 | if isinstance(angle, list): 83 | assert isinstance(principal_axis, list) 84 | assert len(principal_axis) == len(angle) 85 | rot = np.identity(4) 86 | for pa, an in zip(principal_axis, angle): 87 | aff = get_small_orthogonal_rotation(theta=an, principal_axis=pa) 88 | rot = rot.dot(aff) 89 | else: 90 | rot = get_small_orthogonal_rotation(theta=angle, principal_axis=principal_axis) 91 | 92 | pfi_in, pfi_out = get_pfi_in_pfi_out(filename_in, filename_out, self.pfo_in, self.pfo_out) 93 | im = nib.load(pfi_in) 94 | 95 | if respect_to_centre: 96 | fov_centre = im.affine.dot(np.array(list(np.array(im.shape[:3]) / float(2)) + [1])) 97 | 98 | transl = np.eye(4) 99 | transl[:3, 3] = fov_centre[:3] 100 | 101 | transl_inv = np.eye(4) 102 | transl_inv[:3, 3] = -1 * fov_centre[:3] 103 | 104 | rt = transl.dot(rot.dot(transl_inv)) 105 | 106 | new_aff = rt.dot(im.affine) 107 | else: 108 | new_aff = im.affine[:] 109 | new_aff[:3, :3] = rot[:3, :3].dot(new_aff[:3, :3]) 110 | 111 | new_im = modify_affine_transformation(im_input=im, new_aff=new_aff, q_form=True, s_form=True, 112 | multiplication_side="replace") 113 | 114 | nib.save(new_im, pfi_out) 115 | 116 | def modify_translational_part(self, filename_in, filename_out, new_translation): 117 | """:param filename_in: path to filename input 118 | :param filename_out: path to filename output 119 | :param new_translation: translation that will replace the existing one. 120 | :return: 121 | """ 122 | pfi_in, pfi_out = get_pfi_in_pfi_out(filename_in, filename_out, self.pfo_in, self.pfo_out) 123 | im = nib.load(pfi_in) 124 | 125 | if isinstance(new_translation, str): 126 | 127 | if new_translation.endswith(".txt"): 128 | tr = np.loadtxt(connect_path_tail_head(self.pfo_in, new_translation)) 129 | else: 130 | tr = np.load(connect_path_tail_head(self.pfo_in, new_translation)) 131 | 132 | elif isinstance(new_translation, np.ndarray): 133 | tr = new_translation 134 | elif isinstance(new_translation, list): 135 | tr = np.array(new_translation) 136 | else: 137 | raise OSError("parameter new_translation can be path to an affine matrix .txt or .npy or the numpy array" 138 | "corresponding to the new intended translational part.") 139 | 140 | new_im = replace_translational_part(im, tr) 141 | nib.save(new_im, pfi_out) 142 | -------------------------------------------------------------------------------- /nilabels/agents/intensities_manipulator.py: -------------------------------------------------------------------------------- 1 | import nibabel as nib 2 | import numpy as np 3 | 4 | from nilabels.tools.aux_methods.utils import labels_query 5 | from nilabels.tools.aux_methods.utils_path import connect_path_tail_head, get_pfi_in_pfi_out 6 | from nilabels.tools.detections.contours import contour_from_segmentation 7 | from nilabels.tools.image_colors_manipulations.cutter import apply_a_mask_nib 8 | from nilabels.tools.image_colors_manipulations.normaliser import normalise_below_labels 9 | from nilabels.tools.image_shape_manipulations.merger import grafting 10 | 11 | 12 | class IntensitiesManipulator: 13 | """Facade of the methods in tools, for work with paths to images rather than 14 | with data. Methods under LabelsManagerManipulate are taking in general 15 | one or more input manipulate them according to some rule and save the 16 | output in the output_data_folder or in the specified paths. 17 | """ 18 | 19 | def __init__(self, input_data_folder=None, output_data_folder=None, path_label_descriptor=None): 20 | self.pfo_in = input_data_folder 21 | self.pfo_out = output_data_folder 22 | self.path_label_descriptor = path_label_descriptor 23 | 24 | def normalise_below_label(self, filename_image_in, filename_image_out, filename_segm, labels, stats=np.median): 25 | """:param filename_image_in: path to image input 26 | :param filename_image_out: path to image output 27 | :param filename_segm: path to segmentation 28 | :param labels: list of labels below which the voxels are collected 29 | :param stats: a statistics (by default the median). 30 | :return: a new image with the intensites normalised according to the proposed statistics computed on the 31 | intensities below the provided labels. 32 | """ 33 | pfi_in, pfi_out = get_pfi_in_pfi_out(filename_image_in, filename_image_out, self.pfo_in, self.pfo_out) 34 | pfi_segm = connect_path_tail_head(self.pfo_in, filename_segm) 35 | 36 | im_input = nib.load(pfi_in) 37 | im_segm = nib.load(pfi_segm) 38 | 39 | labels_list, labels_names = labels_query(labels, im_segm.get_fdata()) 40 | im_out = normalise_below_labels(im_input, labels_list, labels, stats=stats, exclude_first_label=True) 41 | 42 | nib.save(im_out, pfi_out) 43 | 44 | def get_contour_from_segmentation( 45 | self, 46 | filename_input_segmentation, 47 | filename_output_contour, 48 | omit_axis=None, 49 | verbose=0, 50 | ): 51 | """Get the contour from a segmentation. 52 | :param filename_input_segmentation: input segmentation 53 | :param filename_output_contour: output contour 54 | :param omit_axis: meant to avoid "walls" in the output segmentation 55 | :param verbose: 56 | :return: 57 | """ 58 | pfi_in, pfi_out = get_pfi_in_pfi_out( 59 | filename_input_segmentation, 60 | filename_output_contour, 61 | self.pfo_in, 62 | self.pfo_out, 63 | ) 64 | im_segm = nib.load(pfi_in) 65 | 66 | im_contour = contour_from_segmentation(im_segm, omit_axis=omit_axis, verbose=verbose) 67 | 68 | nib.save(im_contour, filename_output_contour) 69 | 70 | def get_grafting(self, pfi_input_hosting_mould, pfi_input_patch, pfi_output_grafted, pfi_input_patch_mask=None): 71 | """:param pfi_input_hosting_mould: base image where the grafting should happen 72 | :param pfi_input_patch: patch to be grafted in the hosting_mould 73 | :param pfi_output_grafted: output image with the grafting 74 | :param pfi_input_patch_mask: optional additional mask, where the grafting will take place. 75 | :return: 76 | """ 77 | pfi_hosting = connect_path_tail_head(self.pfo_in, pfi_input_hosting_mould) 78 | pfi_patch = connect_path_tail_head(self.pfo_in, pfi_input_patch) 79 | 80 | im_hosting = nib.load(pfi_hosting) 81 | im_patch = nib.load(pfi_patch) 82 | im_mask = None 83 | 84 | if pfi_input_patch_mask is not None: 85 | pfi_mask = connect_path_tail_head(self.pfo_in, pfi_input_patch_mask) 86 | im_mask = nib.load(pfi_mask) 87 | 88 | im_grafted = grafting(im_hosting, im_patch, im_patch_mask=im_mask) 89 | 90 | pfi_output = connect_path_tail_head(self.pfo_out, pfi_output_grafted) 91 | nib.save(im_grafted, pfi_output) 92 | 93 | def crop_outside_mask(self, filename_input_image, filename_mask, filename_output_image_masked): 94 | """Set to zero all the values outside the mask. 95 | Adaptative - if the mask is 3D and the image is 4D, will create a temporary mask, 96 | generate the stack of masks, and apply the stacks to the image. 97 | :param filename_input_image: path to file 3d x T image 98 | :param filename_mask: 3d mask same dimension as the 3d of the pfi_input 99 | :param filename_output_image_masked: apply the mask to each time point T in the fourth dimension if any. 100 | :return: None, it saves the output in pfi_output. 101 | """ 102 | pfi_in, pfi_out = get_pfi_in_pfi_out( 103 | filename_input_image, 104 | filename_output_image_masked, 105 | self.pfo_in, 106 | self.pfo_out, 107 | ) 108 | 109 | pfi_mask = connect_path_tail_head(self.pfo_in, filename_mask) 110 | 111 | im_in, im_mask = nib.load(pfi_in), nib.load(pfi_mask) 112 | im_masked = apply_a_mask_nib(im_in, im_mask) 113 | 114 | pfi_output = connect_path_tail_head(self.pfo_out, filename_output_image_masked) 115 | nib.save(im_masked, pfi_output) 116 | -------------------------------------------------------------------------------- /nilabels/agents/math.py: -------------------------------------------------------------------------------- 1 | import nibabel as nib 2 | 3 | from nilabels.tools.aux_methods.utils_nib import set_new_data 4 | from nilabels.tools.aux_methods.utils_path import connect_path_tail_head, get_pfi_in_pfi_out 5 | 6 | 7 | class Math: 8 | """Facade of no external methods. Simple class for quick algebraic manipulations of images with the same grid""" 9 | 10 | def __init__(self, input_data_folder=None, output_data_folder=None): 11 | self.pfo_in = input_data_folder 12 | self.pfo_out = output_data_folder 13 | 14 | def sum(self, path_first_image, path_second_image, path_resulting_image): 15 | pfi_im1, pfi_im2 = get_pfi_in_pfi_out(path_first_image, path_second_image, self.pfo_in, self.pfo_in) 16 | pfi_result = connect_path_tail_head(self.pfo_out, path_resulting_image) 17 | 18 | im1 = nib.load(pfi_im1) 19 | im2 = nib.load(pfi_im2) 20 | 21 | if not im1.shape == im2.shape: 22 | raise OSError("Input images must have the same dimensions.") 23 | 24 | im_result = set_new_data(im1, new_data=im1.get_fdata() + im2.get_fdata()) 25 | 26 | nib.save(im_result, pfi_result) 27 | print(f"Image sum of {pfi_im1} {pfi_im2} saved under {pfi_result}.") 28 | return pfi_result 29 | 30 | def sub(self, path_first_image, path_second_image, path_resulting_image): 31 | pfi_im1, pfi_im2 = get_pfi_in_pfi_out(path_first_image, path_second_image, self.pfo_in, self.pfo_in) 32 | pfi_result = connect_path_tail_head(self.pfo_out, path_resulting_image) 33 | 34 | im1 = nib.load(pfi_im1) 35 | im2 = nib.load(pfi_im2) 36 | 37 | if not im1.shape == im2.shape: 38 | raise OSError("Input images must have the same dimensions.") 39 | 40 | im_result = set_new_data(im1, new_data=im1.get_fdata() - im2.get_fdata()) 41 | 42 | nib.save(im_result, pfi_result) 43 | print(f"Image difference of {pfi_im1} {pfi_im2} saved under {pfi_result}.") 44 | return pfi_result 45 | 46 | def prod(self, path_first_image, path_second_image, path_resulting_image): 47 | pfi_im1, pfi_im2 = get_pfi_in_pfi_out(path_first_image, path_second_image, self.pfo_in, self.pfo_in) 48 | pfi_result = connect_path_tail_head(self.pfo_out, path_resulting_image) 49 | 50 | im1 = nib.load(pfi_im1) 51 | im2 = nib.load(pfi_im2) 52 | 53 | if not im1.shape == im2.shape: 54 | raise OSError("Input images must have the same dimensions.") 55 | 56 | im_result = set_new_data(im1, new_data=im1.get_fdata() * im2.get_fdata()) 57 | 58 | nib.save(im_result, pfi_result) 59 | print(f"Image product of {pfi_im1} {pfi_im2} saved under {pfi_result}.") 60 | return pfi_result 61 | 62 | def scalar_prod(self, scalar, path_image, path_resulting_image): 63 | pfi_image = connect_path_tail_head(self.pfo_in, path_image) 64 | pfi_result = connect_path_tail_head(self.pfo_out, path_resulting_image) 65 | im = nib.load(pfi_image) 66 | 67 | im_result = set_new_data(im, new_data=scalar * im.get_fdata()) 68 | 69 | nib.save(im_result, pfi_result) 70 | print(f"Image {pfi_image} times {scalar} saved under {pfi_result}.") 71 | return pfi_result 72 | -------------------------------------------------------------------------------- /nilabels/agents/segmenter.py: -------------------------------------------------------------------------------- 1 | import nibabel as nib 2 | import numpy as np 3 | 4 | from nilabels.tools.aux_methods.utils_nib import set_new_data 5 | from nilabels.tools.aux_methods.utils_path import connect_path_tail_head 6 | from nilabels.tools.detections.get_segmentation import MoG_array, intensity_segmentation, otsu_threshold 7 | 8 | 9 | class LabelsSegmenter: 10 | """Facade for the simple segmentation methods based on intensities, Otsu thresholding and 11 | mixture of gaussians. 12 | """ 13 | 14 | def __init__(self, input_data_folder=None, output_data_folder=None): 15 | self.pfo_in = input_data_folder 16 | self.pfo_out = output_data_folder 17 | 18 | def simple_intensities_thresholding( 19 | self, path_to_input_image, path_to_output_segmentation, number_of_levels=5, output_dtype=np.uint16, 20 | ): 21 | """Simple level intensity-based segmentation. 22 | :param path_to_input_image: 23 | :param path_to_output_segmentation: 24 | :param number_of_levels: number of levels in the output segmentations 25 | :param output_dtype: data type output crisp segmentation (uint16) 26 | :return: the method saves crisp segmentation based on intensities at the specified path. 27 | """ 28 | pfi_input_image = connect_path_tail_head(self.pfo_in, path_to_input_image) 29 | 30 | input_im = nib.load(pfi_input_image) 31 | output_array = intensity_segmentation(input_im.get_fdata(), num_levels=number_of_levels) 32 | output_im = set_new_data(input_im, output_array, new_dtype=output_dtype) 33 | 34 | pfi_output_segm = connect_path_tail_head(self.pfo_out, path_to_output_segmentation) 35 | nib.save(output_im, pfi_output_segm) 36 | 37 | def otsu_thresholding(self, path_to_input_image, path_to_output_segmentation, side="above", return_as_mask=True): 38 | """Binary segmentation with Otsu thresholding parameters from skimage filters. 39 | :param path_to_input_image: 40 | :param path_to_output_segmentation: 41 | :param side: can be 'above' or 'below' if the user requires to mask the values above the Otsu threshold or 42 | below. 43 | :param return_as_mask: if False it returns the thresholded input image. 44 | :return: the method saves the crisp binary segmentation (or the thresholded input image) according to Otsu 45 | threshold. 46 | """ 47 | pfi_input_image = connect_path_tail_head(self.pfo_in, path_to_input_image) 48 | 49 | input_im = nib.load(pfi_input_image) 50 | output_array = otsu_threshold(input_im.get_fdata(), side=side, return_as_mask=return_as_mask) 51 | output_im = set_new_data(input_im, output_array, new_dtype=output_array.dtype) 52 | 53 | pfi_output_segm = connect_path_tail_head(self.pfo_out, path_to_output_segmentation) 54 | nib.save(output_im, pfi_output_segm) 55 | 56 | def mixture_of_gaussians( 57 | self, 58 | path_to_input_image, 59 | path_to_output_segmentation_crisp, 60 | path_to_output_segmentation_prob, 61 | K=None, 62 | mask_im=None, 63 | pre_process_median_filter=False, 64 | pre_process_only_interquartile=False, 65 | see_histogram=None, 66 | reorder_mus=True, 67 | output_dtype_crisp=np.uint16, 68 | output_dtype_prob=np.float32, 69 | ): 70 | """Wrap of MoG_array for nibabel images. 71 | ----- 72 | :param path_to_input_image: path to input image format to be segmented with a MOG method. 73 | :param path_to_output_segmentation_crisp: path to output crisp segmentation 74 | :param path_to_output_segmentation_prob: path to probabilistic output segmentation 75 | :param K: number of classes, if None, it is estimated with a BIC criterion (may take a while) 76 | :param mask_im: nibabel mask if you want to consider only a subset of the masked data. 77 | :param pre_process_median_filter: apply a median filter before pre-processing (reduce salt and pepper noise). 78 | :param pre_process_only_interquartile: set to zero above and below interquartile in the data. 79 | :param see_histogram: can be True, False (or None) or a string (with a path where to save the plotted 80 | histogram). 81 | :param reorder_mus: only if output_gmm_class=False, reorder labels from smallest to bigger means. 82 | :param output_dtype_crisp: data type output crisp segmentation (uint16) 83 | :param output_dtype_prob: data type output probabilistic segmentation (float32) 84 | :return: save crisp and probabilistic segmentation at the specified files after sklearn.mixture.GaussianMixture 85 | """ 86 | pfi_input_image = connect_path_tail_head(self.pfo_in, path_to_input_image) 87 | 88 | input_im = nib.load(pfi_input_image) 89 | if mask_im is not None: 90 | mask_array = mask_im.get_fdata() 91 | else: 92 | mask_array = None 93 | 94 | ans = MoG_array( 95 | input_im.get_fdata(), 96 | K=K, 97 | mask_array=mask_array, 98 | pre_process_median_filter=pre_process_median_filter, 99 | pre_process_only_interquartile=pre_process_only_interquartile, 100 | output_gmm_class=False, 101 | see_histogram=see_histogram, 102 | reorder_mus=reorder_mus, 103 | ) 104 | 105 | crisp, prob = ans[0], ans[1] 106 | 107 | im_crisp = set_new_data(input_im, crisp, new_dtype=output_dtype_crisp) 108 | im_prob = set_new_data(input_im, prob, new_dtype=output_dtype_prob) 109 | 110 | pfi_im_crisp = connect_path_tail_head(self.pfo_out, path_to_output_segmentation_crisp) 111 | pfi_im_prob = connect_path_tail_head(self.pfo_out, path_to_output_segmentation_prob) 112 | 113 | nib.save(im_crisp, pfi_im_crisp) 114 | nib.save(im_prob, pfi_im_prob) 115 | -------------------------------------------------------------------------------- /nilabels/agents/shape_manipulator.py: -------------------------------------------------------------------------------- 1 | import nibabel as nib 2 | import numpy as np 3 | 4 | from nilabels.tools.aux_methods.utils_nib import set_new_data 5 | from nilabels.tools.aux_methods.utils_path import connect_path_tail_head, get_pfi_in_pfi_out 6 | from nilabels.tools.image_colors_manipulations.cutter import cut_4d_volume_with_a_1_slice_mask_nib 7 | from nilabels.tools.image_shape_manipulations.merger import merge_labels_from_4d, stack_images 8 | from nilabels.tools.image_shape_manipulations.splitter import split_labels_to_4d 9 | 10 | 11 | class ShapeManipulator: 12 | def __init__(self, input_data_folder=None, output_data_folder=None): 13 | self.pfo_in = input_data_folder 14 | self.pfo_out = output_data_folder 15 | 16 | def extend_slice_new_dimension(self, pfi_input, pfi_output=None, new_axis=3, num_slices=10): 17 | pfi_in, pfi_out = get_pfi_in_pfi_out(pfi_input, pfi_output, self.pfo_in, self.pfo_out) 18 | 19 | im_slice = nib.load(pfi_in) 20 | data_slice = im_slice.get_fdata() 21 | 22 | data_extended = np.stack([data_slice] * num_slices, axis=new_axis) 23 | 24 | im_extended = set_new_data(im_slice, data_extended) 25 | nib.save(im_extended, pfi_out) 26 | print(f"Extended image of {pfi_in} saved in {pfi_out}.") 27 | return pfi_out 28 | 29 | def split_in_4d(self, pfi_input, pfi_output=None, list_labels=None, keep_original_values=True): 30 | pfi_in, pfi_out = get_pfi_in_pfi_out(pfi_input, pfi_output, self.pfo_in, self.pfo_out) 31 | 32 | im_labels_3d = nib.load(pfi_in) 33 | data_labels_3d = im_labels_3d.get_fdata() 34 | assert len(data_labels_3d.shape) == 3 35 | if list_labels is None: 36 | list_labels = list(np.sort(list(set(data_labels_3d.flat)))) 37 | data_split_in_4d = split_labels_to_4d( 38 | data_labels_3d, list_labels=list_labels, keep_original_values=keep_original_values, 39 | ) 40 | 41 | im_split_in_4d = set_new_data(im_labels_3d, data_split_in_4d) 42 | nib.save(im_split_in_4d, pfi_out) 43 | print(f"Split labels from image {pfi_in} saved in {pfi_out}.") 44 | return pfi_out 45 | 46 | def merge_from_4d(self, pfi_input, pfi_output=None): 47 | pfi_in, pfi_out = get_pfi_in_pfi_out(pfi_input, pfi_output, self.pfo_in, self.pfo_out) 48 | 49 | im_labels_4d = nib.load(pfi_in) 50 | data_labels_4d = im_labels_4d.get_fdata() 51 | assert len(data_labels_4d.shape) == 4 52 | data_merged_in_3d = merge_labels_from_4d(data_labels_4d) 53 | 54 | im_merged_in_3d = set_new_data(im_labels_4d, data_merged_in_3d) 55 | nib.save(im_merged_in_3d, pfi_out) 56 | print(f"Merged labels from 4d image {pfi_in} saved in {pfi_out}.") 57 | return pfi_out 58 | 59 | def cut_4d_volume_with_a_1_slice_mask(self, pfi_input, filename_mask, pfi_output=None): 60 | pfi_in, pfi_out = get_pfi_in_pfi_out(pfi_input, pfi_output, self.pfo_in, self.pfo_out) 61 | pfi_mask = connect_path_tail_head(self.pfo_in, filename_mask) 62 | 63 | im_dwi = nib.load(pfi_in) 64 | im_mask = nib.load(pfi_mask) 65 | 66 | im_masked = cut_4d_volume_with_a_1_slice_mask_nib(im_dwi, im_mask) 67 | 68 | nib.save(im_masked, pfi_out) 69 | 70 | def stack_list_pfi_images(self, list_pfi_input, pfi_output): 71 | list_pfi_in = [connect_path_tail_head(self.pfo_in, p) for p in list_pfi_input] 72 | pfi_out = connect_path_tail_head(self.pfo_out, pfi_output) 73 | 74 | list_im = [nib.load(p) for p in list_pfi_in] 75 | stack_im = stack_images(list_im) 76 | 77 | nib.save(stack_im, pfi_out) 78 | -------------------------------------------------------------------------------- /nilabels/definitions.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | __version__ = "v0.0.8" # update also in setup.py 4 | root_dir = os.path.dirname(os.path.abspath(os.path.dirname(__file__))) 5 | 6 | info = { 7 | "name": "NiLabels", 8 | "version": __version__, 9 | "description": "", 10 | "repository": { 11 | "type": "git", 12 | "url": "", 13 | }, 14 | "author": "Sebastiano Ferraris", 15 | "dependencies": { 16 | # requirements.txt automatically generated using pipreqs 17 | "python requirements" : f"{root_dir}/requirements.txt", 18 | }, 19 | } 20 | 21 | 22 | definition_template = """ A template is the average, computed with a chose protocol, of a series of images acquisition 23 | of the same anatomy, or in genreral of different objects that share common features. 24 | """ 25 | 26 | definition_atlas = """ An atlas is the segmentation of the template, obtained averaging with a chosen protocol, 27 | the series of segmentations corresponding to the series of images acquisition that generates the template. 28 | """ 29 | 30 | definition_label = """ A segmentation assigns each region a label, and labels 31 | are represented as subset of voxel with the same positive integer value. 32 | """ 33 | 34 | nomenclature_conventions = """ pfi_xxx = path to file xxx, \npfo_xxx = path to folder xxx, 35 | \nin_xxx = input data structure xxx, \nout_xxx = output data structure xxx, \nz_ : prefix to temporary files and folders, 36 | \nfin_ : file name. 37 | """ 38 | -------------------------------------------------------------------------------- /nilabels/tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/nilabels/4a61cd95e6bfb3644a3724461502d7cf69f5615d/nilabels/tools/__init__.py -------------------------------------------------------------------------------- /nilabels/tools/aux_methods/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/nilabels/4a61cd95e6bfb3644a3724461502d7cf69f5615d/nilabels/tools/aux_methods/__init__.py -------------------------------------------------------------------------------- /nilabels/tools/aux_methods/morpological_operations.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy import ndimage 3 | 4 | 5 | def get_morphological_patch(dimension, shape): 6 | """:param dimension: dimension of the image (NOT the shape). 7 | :param shape: circle or square. 8 | :return: morphological patch as ndimage 9 | """ 10 | if shape == "circle": 11 | morpho_patch = ndimage.generate_binary_structure(dimension, 1) 12 | elif shape == "square": 13 | morpho_patch = ndimage.generate_binary_structure(dimension, 3) 14 | else: 15 | raise OSError 16 | 17 | return morpho_patch 18 | 19 | 20 | def get_morphological_mask(point, omega, radius=5, shape="circle", morpho_patch=None): 21 | """Helper to obtain a morphological mask based on get_morphological_patch 22 | :param point: centre of the mask 23 | :param omega: grid dimension of the image domain. E.g. [256, 256, 128]. 24 | :param radius: radius of the mask if morpho_patch not given. 25 | :param shape: 'circle' shape of the mask if morpho_patch not given. 26 | :param morpho_patch:To avoid computing the morphological mask at each iteration if this method, 27 | this mask can be provided as input. This bypasses the input radius and shape. 28 | """ 29 | if morpho_patch is None: 30 | d = len(omega) 31 | morpho_patch = get_morphological_patch(d, shape=shape) 32 | 33 | array_mask = np.zeros(omega, dtype=bool) 34 | array_mask.itemset(tuple(point), 1) 35 | for _ in range(radius): 36 | array_mask = ndimage.binary_dilation(array_mask, structure=morpho_patch).astype(array_mask.dtype) 37 | return array_mask 38 | 39 | 40 | def get_values_below_patch(point, target_image, radius=5, shape="circle", morpho_mask=None): 41 | """To obtain the list of the values below a maks. 42 | :param point: central point of the patch 43 | :param target_image: array image whose values we are interested into. 44 | :param radius: patch radius if morpho_patch is not given. 45 | :param shape: shape patch if morpho_patch is not given. 46 | :param morpho_mask: To avoid computing the morphological mask at each iteration if this method, 47 | this mask can be provided as input. This bypasses the input radius and shape. 48 | :return: 49 | """ 50 | if morpho_mask is None: 51 | morpho_mask = get_morphological_mask(point, target_image.shape, radius=radius, shape=shape) 52 | coord = np.nonzero(morpho_mask.flatten())[0] 53 | return np.take(target_image.flatten(), coord) 54 | 55 | 56 | def get_circle_shell_for_given_radius(radius, dimension=3): 57 | """:param radius: radius of the circle. 58 | :param dimension: must be 2 or 3. 59 | :return: matrix coordinate values for a circle of given input radius and dimension centered at the origin. 60 | E.G. 61 | >> get_circle_shell_for_given_radius(3,2) 62 | [(-3, 0), (-2, -2), (-2, -1), (-2, 1), (-2, 2), (-1, -2), (-1, 2), (0, -3), (0, 3), (1, -2), (1, 2), (2, -2), 63 | (2, -1), (2, 1), (2, 2), (3, 0)] 64 | Note: implementation not optimal. 65 | """ 66 | # Generalise midpoint circle algorithm for future version. 67 | circle = [] 68 | if dimension == 3: 69 | for xi in range(-radius, radius + 1): 70 | for yi in range(-radius, radius + 1): 71 | for zi in range(-radius, radius + 1): 72 | if (radius - 1) ** 2 < xi ** 2 + yi ** 2 + zi ** 2 <= radius ** 2: 73 | circle.append((xi, yi, zi)) 74 | elif dimension == 2: 75 | for xi in range(-radius, radius + 1): 76 | for yi in range(-radius, radius + 1): 77 | if (radius - 1) ** 2 < xi ** 2 + yi ** 2 <= radius ** 2: 78 | circle.append((xi, yi)) 79 | else: 80 | raise OSError("Dimensions allowed are 2 or 3.") 81 | return circle 82 | -------------------------------------------------------------------------------- /nilabels/tools/aux_methods/sanity_checks.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import os 3 | import subprocess 4 | import time 5 | 6 | 7 | def check_pfi_io(pfi_input, pfi_output): 8 | """Check if input and output files exist 9 | :param pfi_input: path to file input 10 | :param pfi_output: path to file output. Can be None. 11 | :return: True if pfi_input exists and if output is provided and exists. 12 | """ 13 | if not os.path.exists(pfi_input): 14 | msg = f"Input file {pfi_input} does not exists." 15 | raise OSError(msg) 16 | if pfi_output is not None and not os.path.exists(os.path.dirname(pfi_output)): 17 | msg = f"Output file {pfi_output} is located in a non-existing folder." 18 | raise OSError(msg) 19 | return True 20 | 21 | 22 | def check_path_validity(pfi, interval=1, timeout=100): 23 | """Workaround function to cope with delayed operations in the cluster. 24 | Boringly asking if something exists, until timeout or appearance of the sought file happen. 25 | :param pfi: path to file to assess 26 | :param interval: seconds 27 | :param timeout: number of intervals before timeout. 28 | :return: 29 | """ 30 | if os.path.exists(pfi): 31 | if pfi.endswith(".nii.gz"): 32 | mustend = time.time() + timeout 33 | while time.time() < mustend: 34 | try: 35 | subprocess.check_output(f"gunzip -t {pfi}", shell=True) 36 | except subprocess.CalledProcessError: 37 | print("Caught CalledProcessError") 38 | else: 39 | return True 40 | time.sleep(interval) 41 | msg = f"File {pfi} corrupted after 100 tests. \n" 42 | raise OSError(msg) 43 | return True 44 | msg = f"{pfi} does not exist!" 45 | raise OSError(msg) 46 | 47 | 48 | def is_valid_permutation(in_perm, for_labels=True): 49 | """A permutation in generalised Cauchy notation is a list of 2 lists of same size: 50 | a = [[1,2,3], [2,3,1]] 51 | means permute 1 with 2, 2 with 3, 3 with 1. 52 | :param for_labels: if True the permutation elements must be int. 53 | :param in_perm: input permutation. 54 | """ 55 | if len(in_perm) != 2: 56 | return False 57 | if not len(in_perm[0]) == len(in_perm[1]) == len(set(in_perm[0])) == len(set(in_perm[1])): 58 | return False 59 | if (sorted(set(in_perm[0])) > sorted(set(in_perm[1]))) - (sorted(set(in_perm[0])) < sorted(set(in_perm[1]))) != 0: 60 | return False 61 | if for_labels and not all(isinstance(x, int) for x in list(itertools.chain(*in_perm))): 62 | # as dealing with labels, all the elements must be int 63 | return False 64 | return True 65 | -------------------------------------------------------------------------------- /nilabels/tools/aux_methods/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | 4 | import numpy as np 5 | 6 | from nilabels.tools.aux_methods.sanity_checks import is_valid_permutation 7 | 8 | # ---- List utils ---- 9 | 10 | 11 | def lift_list(input_list): 12 | """List of nested lists becomes a list with the element exposed in the main list. 13 | :param input_list: a list of lists. 14 | :return: eliminates the first nesting levels of lists. 15 | E.G. 16 | >> lift_list([1, 2, [1,2,3], [1,2], [4,5, 6], [3,4]]) 17 | [1, 2, 1, 2, 3, 1, 2, 4, 5, 6, 3, 4] 18 | """ 19 | if input_list == []: 20 | return [] 21 | 22 | return ( 23 | lift_list(input_list[0]) + (lift_list(input_list[1:]) if len(input_list) > 1 else []) 24 | if isinstance(input_list, list) 25 | else [input_list] 26 | ) 27 | 28 | 29 | def eliminates_consecutive_duplicates(input_list): 30 | """:param input_list: a list 31 | :return: the same list with no consecutive duplicates. 32 | """ 33 | output_list = [input_list[0]] 34 | for i in range(1, len(input_list)): 35 | if input_list[i] != input_list[i - 1]: 36 | output_list.append(input_list[i]) 37 | return output_list 38 | 39 | 40 | # ---- Command executions utils ---- 41 | 42 | 43 | def print_and_run(cmd, msg=None, safety_on=False, short_path_output=True): 44 | """Run the command to console and print the message. 45 | if msg is None print the command itself. 46 | :param cmd: command for the terminal 47 | :param msg: message to show before running the command 48 | on the top of the command itself. 49 | :param short_path_output: the message provided at the prompt has only the filenames without the paths. 50 | :param safety_on: safety, in case you want to see the messages at a first run. 51 | :return: 52 | """ 53 | 54 | def scan_and_remove_path(msg): 55 | """Take a string with a series of paths separated by a space and keeps only the base-names of each path.""" 56 | a = [os.path.basename(p) for p in msg.split(" ")] 57 | return " ".join(a) 58 | 59 | if short_path_output: 60 | output_msg = scan_and_remove_path(cmd) 61 | else: 62 | output_msg = cmd 63 | 64 | if msg is not None: 65 | output_msg += f"{msg}\n{output_msg}" 66 | 67 | print(f"\n-> {output_msg}\n") 68 | 69 | if not safety_on: 70 | subprocess.call(cmd, shell=True) 71 | 72 | return output_msg 73 | 74 | 75 | # ---------- Labels processors --------------- 76 | 77 | 78 | def labels_query(labels, segmentation_array=None, remove_zero=True): 79 | """labels_list can be a list or a list of lists in case some labels have to be considered together. labels_names 80 | :param labels: can be int, list, string as 'all' or 'tot', or a string containing a path to a .txt or a numpy array 81 | :param segmentation_array: optional segmentation image data (array) 82 | :param remove_zero: do not return zero 83 | :return: labels_list, labels_names 84 | """ 85 | labels_names = [] 86 | if labels is None: 87 | labels = "all" 88 | 89 | if isinstance(labels, int): 90 | if segmentation_array is not None: 91 | assert labels in segmentation_array 92 | labels_list = [labels] 93 | elif isinstance(labels, list): 94 | labels_list = labels 95 | elif isinstance(labels, str): 96 | if labels == "all" and segmentation_array is not None: 97 | labels_list = list(np.sort(list(set(segmentation_array.astype(int).flat)))) 98 | elif labels == "tot" and segmentation_array is not None: 99 | labels_list = [list(np.sort(list(set(segmentation_array.astype(int).flat))))] 100 | if labels_list[0][0] == 0: 101 | if remove_zero: 102 | labels_list = labels_list[0][1:] # remove initial zero 103 | else: 104 | labels_list = labels_list[0] 105 | elif os.path.exists(labels): 106 | if labels.endswith(".txt"): 107 | labels_list = list(np.loadtxt(labels)) 108 | else: 109 | labels_list = list(np.load(labels)) 110 | else: 111 | raise OSError( 112 | "Input labels must be a list, a list of lists, or an int or the string 'all' (with " 113 | "segmentation array not set to none)) or the path to a file with the labels.", 114 | ) 115 | elif isinstance(labels, dict): 116 | # expected input is the output of manipulate_descriptor.get_multi_label_dict (keys are labels names id are 117 | # list of labels) 118 | labels_list = [] 119 | labels_names = labels.keys() 120 | for k in labels_names: 121 | if len(labels[k]) > 1: 122 | labels_list.append(labels[k]) 123 | else: 124 | labels_list.append(labels[k][0]) 125 | else: 126 | raise OSError( 127 | "Input labels must be a list, a list of lists, or an int or the string 'all' or the path to a" 128 | "file with the labels.", 129 | ) 130 | if not isinstance(labels, dict): 131 | labels_names = [str(l) for l in labels_list] 132 | 133 | return labels_list, labels_names 134 | 135 | 136 | # ------------ Permutations -------------- 137 | # Thanks to Martin R and Accumulation 138 | # https://codereview.stackexchange.com/questions/201725/disjoint-cycles-of-a-permutation 139 | 140 | 141 | def permutation_from_cauchy_to_disjoints_cycles(cauchy_perm): 142 | """From [[1, 2, 3, 4, 5], [3, 4, 5, 2, 1]] 143 | to [[1, 3, 5], [2, 4]] 144 | :param cauchy_perm: permutation in generalised Cauchy convention (any object, not necessarily numbers from 1 to n 145 | or from 0 ot n-1) where the objects that are not permuted do not appear. 146 | :return: input permutation written in disjoint cycles. 147 | """ 148 | if not is_valid_permutation(cauchy_perm): 149 | raise OSError("Input permutation is not valid") 150 | list_cycles = [] 151 | cycle = [cauchy_perm[0][0]] 152 | while len(cauchy_perm[0]) > 0: 153 | first_row_element, second_row_element = cycle[-1], cauchy_perm[1][cauchy_perm[0].index(cycle[-1])] 154 | cycle.append(second_row_element) 155 | cauchy_perm[0].remove(first_row_element) 156 | cauchy_perm[1].remove(second_row_element) 157 | if cycle[0] == cycle[-1]: 158 | if len(cycle) > 2: 159 | list_cycles += [cycle[:-1]] 160 | if len(cauchy_perm[0]) > 0: 161 | cycle = [cauchy_perm[0][0]] 162 | return list_cycles 163 | 164 | 165 | def permutation_from_disjoint_cycles_to_cauchy(cyclic_perm): 166 | """From [[1, 3, 5], [2, 4]] 167 | to [[1, 2, 3, 4, 5], [3, 4, 5, 2, 1]] 168 | """ 169 | pairs = sorted([(a, b) for cycle in cyclic_perm for a, b in zip(cycle, cycle[1:] + cycle[:1])]) 170 | return [list(i) for i in zip(*pairs)] 171 | -------------------------------------------------------------------------------- /nilabels/tools/aux_methods/utils_path.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def connect_path_tail_head(tail, head): 5 | """It is expected to find 6 | 1) the path to folder in tail and filename in head. 7 | 2) the full path in the head (with tail as sub-path). 8 | 3) the tail with a base path and head to have an additional path + filename. 9 | :param tail: 10 | :param head: 11 | :return: 12 | """ 13 | if tail is None or tail == "": 14 | return head 15 | if head.startswith(tail): # Case 2 16 | return head 17 | # case 1, 3 18 | return os.path.join(tail, head) 19 | 20 | 21 | def get_pfi_in_pfi_out(filename_in, filename_out, pfo_in, pfo_out): 22 | """Core method of every facade to connect working folder with input data files. 23 | :param filename_in: filename input 24 | :param filename_out: filename output 25 | :param pfo_in: path to folder input 26 | :param pfo_out: path to folder output 27 | :return: connection of the inupt and the output. 28 | """ 29 | pfi_in = connect_path_tail_head(pfo_in, filename_in) 30 | if filename_out is None: 31 | pfi_out = pfi_in 32 | elif pfo_out is None: 33 | pfi_out = connect_path_tail_head(pfo_in, filename_out) 34 | else: 35 | pfi_out = connect_path_tail_head(pfo_out, filename_out) 36 | 37 | return pfi_in, pfi_out 38 | -------------------------------------------------------------------------------- /nilabels/tools/caliber/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/nilabels/4a61cd95e6bfb3644a3724461502d7cf69f5615d/nilabels/tools/caliber/__init__.py -------------------------------------------------------------------------------- /nilabels/tools/caliber/volumes_and_values.py: -------------------------------------------------------------------------------- 1 | """This module is divided into two parts. 2 | First one -> essential functions, input are nibabel objects, output are reals or arrays. 3 | The first part refers to the number of voxels. 4 | Second one -> it uses the first part, to plot volumes, normalisation outputs values in pandas arrays or dataframes. 5 | """ 6 | 7 | import numpy as np 8 | import pandas as pd 9 | 10 | from nilabels.tools.aux_methods.utils_nib import one_voxel_volume 11 | 12 | 13 | def get_total_num_nonzero_voxels(im_segm, list_labels_to_exclude=None): 14 | """:param im_segm: 15 | :param list_labels_to_exclude: 16 | :return: 17 | """ 18 | seg = np.copy(im_segm.get_fdata()) 19 | if list_labels_to_exclude is not None: 20 | for label_k in list_labels_to_exclude: 21 | places = seg != label_k 22 | seg = seg * places 23 | num_voxels = np.count_nonzero(seg) 24 | else: 25 | num_voxels = int(np.count_nonzero(im_segm.get_fdata())) 26 | return num_voxels 27 | 28 | 29 | def get_num_voxels_from_labels_list(im_segm, labels_list): 30 | """:param im_segm: image segmentation 31 | :param labels_list: integer, list of labels [l1, l2, ..., ln], or list of list of labels if labels needs to be 32 | considered together. 33 | e.g. labels_list = [1,2,[3,4]] -> values below label 1, values below label 2, values below label 3 and 4. 34 | :return: np.arrays with the number of voxels below each input label or list input label. 35 | """ 36 | num_voxels_per_label = np.zeros(len(labels_list)).astype(np.int64) 37 | 38 | for k, label_k in enumerate(labels_list): 39 | if isinstance(label_k, int): 40 | all_places = im_segm.get_fdata() == label_k 41 | num_voxels_per_label[k] = np.count_nonzero(np.nan_to_num(all_places)) 42 | elif isinstance(label_k, list): 43 | all_places = np.zeros_like(im_segm.get_fdata(), dtype=bool) 44 | for label_k_j in label_k: 45 | all_places += im_segm.get_fdata() == label_k_j 46 | num_voxels_per_label[k] = np.count_nonzero(np.nan_to_num(all_places)) 47 | else: 48 | raise OSError("Labels list must be like [1,2,[3,4]], where [3, 4] are considered as a single label.") 49 | 50 | return num_voxels_per_label 51 | 52 | 53 | def get_values_below_labels_list(im_segm, im_anat, labels_list): 54 | """:param im_segm: image segmentation 55 | :param im_anat: anatomical image, corresponding to the segmentation. 56 | :param labels_list: integer, list of labels [l1, l2, ..., ln], or list of list of labels if labels needs to be 57 | considered together. 58 | e.g. labels_list = [1,2,[3,4]] -> values belows label 1, values below label 2, values below label 3 and 4. 59 | :return: list of np.arrays. Each containing all the values below the corresponding labels. 60 | """ 61 | assert im_segm.shape == im_anat.shape 62 | 63 | values_below_each_label = [] 64 | 65 | for label_k in labels_list: 66 | if isinstance(label_k, int): 67 | coords = np.where(im_segm.get_fdata() == label_k) 68 | values_below_each_label.append(im_anat.get_fdata()[coords].flatten()) 69 | elif isinstance(label_k, list): 70 | vals = np.array([]) 71 | for label_k_j in label_k: 72 | coords = np.where(im_segm.get_fdata() == label_k_j) 73 | vals = np.concatenate((vals, im_anat.get_fdata()[coords].flatten()), axis=0) 74 | values_below_each_label.append(vals) 75 | else: 76 | raise OSError("Labels list must be like [1,2,[3,4]], where [3, 4] are considered as a single label.") 77 | 78 | return values_below_each_label 79 | 80 | 81 | def get_volumes_per_label(im_segm, labels, labels_names, tot_volume_prior=None, verbose=0): 82 | """Get a separate volume for each label in a data-frame 83 | :param im_segm: nibabel segmentation 84 | :param labels: labels you want to measure, or 'all' if you want them all or 'tot' to have the total of the non zero 85 | labels. 86 | :param labels_names: list with the indexes of labels in the final dataframes, corresponding to labels list. 87 | :param tot_volume_prior: factor the volumes will be divided with. 88 | :param verbose: > 0 will provide the intermediate stepsp 89 | :return: 90 | """ 91 | num_non_zero_voxels = get_total_num_nonzero_voxels(im_segm) 92 | vol_non_zero_voxels_mm3 = num_non_zero_voxels * one_voxel_volume(im_segm) 93 | if tot_volume_prior is None: 94 | tot_volume_prior = vol_non_zero_voxels_mm3 95 | if labels_names not in [None, "all", "tot"] and len(labels) != len(labels_names): 96 | raise OSError("Inconsistent labels - labels_names input.") 97 | 98 | if labels_names == "all": 99 | labels_names = [f"reg {l}" for l in labels] 100 | 101 | if labels_names == "tot": 102 | labels_names = ["tot"] 103 | 104 | non_zero_voxels = np.count_nonzero(im_segm.get_fdata()) 105 | volumes = non_zero_voxels * one_voxel_volume(im_segm) 106 | vol_over_tot = volumes / float(tot_volume_prior) 107 | 108 | return pd.DataFrame( 109 | { 110 | "Num voxels": pd.Series([non_zero_voxels], index=labels_names), 111 | "Volume": pd.Series([volumes], index=labels_names), 112 | "Vol over Tot": pd.Series([vol_over_tot], index=labels_names), 113 | }, 114 | ) 115 | 116 | non_zero_voxels_list = [] 117 | volumes_list = [] 118 | vol_over_tot_list = [] 119 | 120 | for label_k in labels: 121 | all_places = np.zeros_like(im_segm.get_fdata(), dtype=bool) 122 | if isinstance(label_k, int): 123 | all_places += im_segm.get_fdata() == label_k 124 | else: 125 | for label_k_j in label_k: 126 | all_places += im_segm.get_fdata() == label_k_j 127 | 128 | flat_volume_voxel = np.nan_to_num((all_places.astype(np.float64)).flatten()) 129 | 130 | non_zero_voxels = np.count_nonzero(flat_volume_voxel) 131 | volumes = non_zero_voxels * one_voxel_volume(im_segm) 132 | 133 | vol_over_tot = volumes / float(tot_volume_prior) 134 | 135 | non_zero_voxels_list.append(non_zero_voxels) 136 | volumes_list.append(volumes) 137 | vol_over_tot_list.append(vol_over_tot) 138 | 139 | data_frame = pd.DataFrame( 140 | { 141 | "Label": pd.Series(labels, index=labels_names), 142 | "Num voxels": pd.Series(non_zero_voxels_list, index=labels_names), 143 | "Volume": pd.Series(volumes_list, index=labels_names), 144 | "Vol over Tot": pd.Series(vol_over_tot_list, index=labels_names), 145 | }, 146 | ) 147 | 148 | data_frame = data_frame.rename_axis("Region") 149 | data_frame = data_frame.reset_index() 150 | 151 | if verbose > 0: 152 | print(data_frame) 153 | 154 | return data_frame 155 | -------------------------------------------------------------------------------- /nilabels/tools/cleaning/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/nilabels/4a61cd95e6bfb3644a3724461502d7cf69f5615d/nilabels/tools/cleaning/__init__.py -------------------------------------------------------------------------------- /nilabels/tools/cleaning/labels_cleaner.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy import ndimage 3 | 4 | from nilabels.tools.detections.island_detection import island_for_label 5 | 6 | 7 | def multi_lab_segmentation_dilate_1_above_selected_label(arr_segm, selected_label=-1, labels_to_dilate=(), verbose=2): 8 | """The orders of labels to dilate counts. 9 | :param arr_segm: 10 | :param selected_label: 11 | :param labels_to_dilate: if None all labels are dilated, in ascending order (algorithm is NOT order invariant). 12 | :param verbose: 13 | :return: 14 | """ 15 | answer = np.copy(arr_segm) 16 | if labels_to_dilate == (): 17 | labels_to_dilate = sorted(set(arr_segm.flat) - {selected_label}) 18 | 19 | num_labels_dilated = 0 20 | for l in labels_to_dilate: 21 | if verbose > 1: 22 | print(f"Dilating label {l} over hole-label {selected_label}") 23 | selected_labels_mask = np.zeros_like(answer, dtype=bool) 24 | selected_labels_mask[answer == selected_label] = 1 25 | bin_label_l = np.zeros_like(answer, dtype=bool) 26 | bin_label_l[answer == l] = 1 27 | dilated_bin_label_l = ndimage.morphology.binary_dilation(bin_label_l) 28 | dilation_l_over_selected_label = dilated_bin_label_l * selected_labels_mask 29 | answer[dilation_l_over_selected_label > 0] = l 30 | num_labels_dilated += 1 31 | if verbose > 0: 32 | print(f"Number of labels_dilated: {num_labels_dilated}\n") 33 | 34 | return answer 35 | 36 | 37 | def holes_filler(arr_segm_with_holes, holes_label=-1, labels_sequence=(), verbose=1): 38 | """Given a segmentation with holes (holes are specified by a special labels called holes_label) 39 | the holes are filled with the closest labels around. 40 | It applies multi_lab_segmentation_dilate_1_above_selected_label until all the holes 41 | are filled. 42 | :param arr_segm_with_holes: 43 | :param holes_label: 44 | :param labels_sequence: As multi_lab_segmentation_dilate_1_above_selected_label is not invariant 45 | for the selected sequence. 46 | :param verbose: 47 | :return: 48 | """ 49 | num_rounds = 0 50 | arr_segm_no_holes = np.copy(arr_segm_with_holes) 51 | 52 | if verbose: 53 | print("Filling holes in the segmentation:") 54 | 55 | while holes_label in arr_segm_no_holes: 56 | arr_segm_no_holes = multi_lab_segmentation_dilate_1_above_selected_label( 57 | arr_segm_no_holes, selected_label=holes_label, labels_to_dilate=labels_sequence, 58 | ) 59 | num_rounds += 1 60 | 61 | if verbose: 62 | print(f"Number of dilations required to remove the holes: {num_rounds}") 63 | 64 | return arr_segm_no_holes 65 | 66 | 67 | def clean_semgentation(arr_segm, labels_to_clean=(), label_for_holes=-1, verbose=1): 68 | """Given an array representing a binary segmentation, the connected components of the segmentations. 69 | If an hole could be filled by 2 different labels, wins the label with lower value. 70 | This should be improved with a probabilistic framework [future work]. 71 | Only the largest connected component of each label will remain in the final 72 | segmentation. The smaller components will be filled by the surrounding labels. 73 | :param arr_segm: an array of a segmentation. 74 | :param labels_to_clean: list of binaries lists. [[z_1, zc_1], ... , [z_J, zc_J]] where z_j is the label you want to 75 | clean and zc_1 is the number of components you want to keep. If empty tuple, by default cleans all the labels 76 | keeping only one component. 77 | :prarm return_filled_holes: if True returns two arrayw, one with the holes filled, and the other with the 78 | binarised holes that had been filled. 79 | :param label_for_holes: internal variable for the dummy labels that will be used for the 'holes'. This must 80 | not be a label already present in the segmentation. 81 | :param verbose: 82 | :return: 83 | """ 84 | if labels_to_clean == (): 85 | labels_to_clean = sorted(set(arr_segm.flat)) 86 | labels_to_clean = [[z, 1] for z in labels_to_clean] 87 | 88 | segm_with_holes = np.copy(arr_segm) 89 | for lab, num_components in labels_to_clean: 90 | if verbose: 91 | print(f"Cleaning label {lab}, keeping {num_components} components") 92 | islands = island_for_label(arr_segm, lab, m=num_components, special_label=label_for_holes) 93 | segm_with_holes[islands == label_for_holes] = label_for_holes 94 | 95 | return holes_filler(segm_with_holes, holes_label=label_for_holes) 96 | -------------------------------------------------------------------------------- /nilabels/tools/detections/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/nilabels/4a61cd95e6bfb3644a3724461502d7cf69f5615d/nilabels/tools/detections/__init__.py -------------------------------------------------------------------------------- /nilabels/tools/detections/check_imperfections.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | from skimage import measure 4 | 5 | from nilabels.tools.aux_methods.label_descriptor_manager import LabelsDescriptorManager 6 | from nilabels.tools.aux_methods.utils_nib import one_voxel_volume 7 | 8 | 9 | def check_missing_labels(im_segm, labels_descriptor, pfi_where_log=None): 10 | """:param im_segm: nibabel image of a segmentation. 11 | :param labels_descriptor: instance of LabelsDescriptorManager 12 | :param pfi_where_log: path to file where to save a log of the output. 13 | :return: correspondences between labels in the segmentation and labels in the label descriptors, 14 | number of voxels, volume and number of connected components per label, all in a log file. 15 | """ 16 | assert isinstance(labels_descriptor, LabelsDescriptorManager) 17 | labels_dict = labels_descriptor.get_dict_itk_snap() 18 | labels_list_from_descriptor = labels_dict.keys() 19 | 20 | labels_in_the_image = set(im_segm.get_fdata().astype(int).flatten()) 21 | intersection = labels_in_the_image & set(labels_list_from_descriptor) 22 | 23 | in_descriptor_not_delineated = set(labels_list_from_descriptor) - intersection 24 | delineated_not_in_descriptor = labels_in_the_image - intersection 25 | 26 | msg = f"Labels in the descriptor not delineated: \n{in_descriptor_not_delineated}\n" 27 | msg += f"Labels delineated not in the descriptor: \n{delineated_not_in_descriptor}" 28 | print(msg) 29 | 30 | if pfi_where_log is not None: 31 | labels_names = [labels_dict[l][2] for l in labels_list_from_descriptor] 32 | 33 | num_voxels_per_label = [] 34 | num_connected_components_per_label = [] 35 | for label_k in labels_list_from_descriptor: 36 | all_places = im_segm.get_fdata() == label_k 37 | 38 | cc_l = len(set(measure.label(all_places, background=0).flatten())) - 1 39 | num_connected_components_per_label.append(cc_l) 40 | 41 | len_non_zero_places = len(all_places[np.where(all_places > 1e-6)]) 42 | if len_non_zero_places == 0: 43 | msg_l = ( 44 | f"\nLabel {label_k} present in label descriptor and not delineated in the " "given segmentation." 45 | ) 46 | msg += msg_l 47 | print(msg_l) 48 | num_voxels_per_label.append(len_non_zero_places) 49 | 50 | se_voxels = pd.Series(num_voxels_per_label, index=labels_names) 51 | se_volume = pd.Series(one_voxel_volume(im_segm) * np.array(num_voxels_per_label), index=labels_names) 52 | 53 | df = pd.DataFrame( 54 | {"Num voxels": se_voxels, "Volumes": se_volume, "Connected components": num_connected_components_per_label}, 55 | ) 56 | 57 | df.index = df.index.map("{:<30}".format) 58 | df["Num voxels"] = df["Num voxels"].map("{:<10}".format) 59 | df["Volumes"] = df["Volumes"].map("{:<10}".format) 60 | df["Connected components"] = df["Connected components"].map("{:<10}".format) 61 | 62 | f = open(pfi_where_log, "w+") 63 | df.to_string(f) 64 | f.close() 65 | 66 | f = open(pfi_where_log, "a+") 67 | f.write("\n\n" + msg) 68 | f.close() 69 | 70 | print(f"Log status saved in {pfi_where_log}") 71 | 72 | return in_descriptor_not_delineated, delineated_not_in_descriptor 73 | -------------------------------------------------------------------------------- /nilabels/tools/detections/contours.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy import ndimage as nd 3 | 4 | from nilabels.tools.aux_methods.utils_nib import set_new_data 5 | 6 | 7 | def contour_from_array_at_label(im_arr, lab, thr=0.3, omit_axis=None, verbose=0): 8 | """Get the contour of a single label 9 | :param im_arr: input array with segmentation 10 | :param lab: considered label 11 | :param thr: threshold (default 0.3) increase to increase the contour thickness. 12 | :param omit_axis: a directional axis preference for the contour creation, to avoid "walls" when scrolling 13 | the 3d image in a particular direction. None if no preference axis is expected. 14 | :param verbose: 15 | :return: boolean mask with the array labels. 16 | """ 17 | if verbose > 0: 18 | print(f"Getting contour for label {lab}") 19 | array_label_l = im_arr == lab 20 | assert isinstance(array_label_l, np.ndarray) 21 | gra = np.gradient(array_label_l.astype(bool).astype(np.float64)) 22 | if omit_axis is None: 23 | thresholded_gra = np.sqrt(gra[0] ** 2 + gra[1] ** 2 + gra[2] ** 2) > thr 24 | elif omit_axis == "x": 25 | thresholded_gra = np.sqrt(gra[1] ** 2 + gra[2] ** 2) > thr 26 | elif omit_axis == "y": 27 | thresholded_gra = np.sqrt(gra[0] ** 2 + gra[2] ** 2) > thr 28 | elif omit_axis == "z": 29 | thresholded_gra = np.sqrt(gra[0] ** 2 + gra[1] ** 2) > thr 30 | else: 31 | raise OSError 32 | return thresholded_gra 33 | 34 | 35 | def contour_from_segmentation(im_segm, omit_axis=None, verbose=0): 36 | """From an input nibabel image segmentation, returns the contour of each segmented region with the original 37 | label. 38 | :param im_segm: 39 | :param omit_axis: a directional axis preference for the contour creation, to avoid "walls" when scrolling 40 | the 3d image in a particular direction. None if no preference axis is expected. 41 | :param verbose: 0 no, 1 yes. 42 | :return: return the contour of the provided segmentation 43 | """ 44 | list_labels = sorted(set(im_segm.get_fdata().flat))[1:] 45 | output_arr = np.zeros_like(im_segm.get_fdata(), dtype=im_segm.get_data_dtype()) 46 | 47 | for la in list_labels: 48 | output_arr += contour_from_array_at_label(im_segm.get_fdata(), la, omit_axis=omit_axis, verbose=verbose) 49 | 50 | return set_new_data(im_segm, output_arr.astype(bool) * im_segm.get_fdata(), new_dtype=im_segm.get_data_dtype()) 51 | 52 | 53 | def get_xyz_borders_of_a_label(segm_arr, label): 54 | """:param segm_arr: array representing a segmentation 55 | :param label: a single integer label 56 | :return: box coordinates containing the given label in the segmentation, None if the label is not present. 57 | """ 58 | assert segm_arr.ndim == 3 59 | 60 | if label not in segm_arr: 61 | return None 62 | 63 | X, Y, Z = np.where(segm_arr == label) 64 | return [np.min(X), np.max(X), np.min(Y), np.max(Y), np.min(Z), np.max(Z)] 65 | 66 | 67 | def get_internal_contour_with_erosion_at_label(segm_arr, lab, thickness=1): 68 | """Get the internal contour for a given thickness. 69 | :param segm_arr: input segmentation where to extract the contour 70 | :param lab: label to extract the contour 71 | :param thickness: final thickness of the segmentation 72 | :return: image with only the contour of the given input image. 73 | """ 74 | im_lab = segm_arr == lab 75 | return (im_lab ^ nd.morphology.binary_erosion(im_lab, iterations=thickness).astype(bool)).astype( 76 | segm_arr.dtype, 77 | ) * lab 78 | -------------------------------------------------------------------------------- /nilabels/tools/detections/get_segmentation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from matplotlib import mlab 3 | from matplotlib import pyplot as plt 4 | from scipy.signal import medfilt 5 | from sklearn.mixture import GaussianMixture 6 | 7 | try: 8 | from skimage import filters 9 | except ImportError: 10 | from skimage import filter as filters 11 | 12 | from nilabels.tools.image_colors_manipulations.relabeller import relabeller 13 | 14 | 15 | def intensity_segmentation(in_array, num_levels=5): 16 | """Simplest way of getting an intensity based segmentation. 17 | :param in_array: image data in a numpy array. 18 | :param num_levels: maximum allowed 65535 - 1. 19 | :return: segmentation of the result in levels levels based on the intensities of the in_data. 20 | """ 21 | segm = np.zeros_like(in_array, dtype=np.uint16) 22 | min_data = np.min(in_array) 23 | max_data = np.max(in_array) 24 | h = (max_data - min_data) / float(int(num_levels)) 25 | for k in range(num_levels): 26 | places = (min_data + k * h <= in_array) * (in_array < min_data + (k + 1) * h) 27 | np.place(segm, places, k) 28 | places = in_array == max_data 29 | np.place(segm, places, num_levels - 1) 30 | return segm 31 | 32 | 33 | def otsu_threshold(in_array, side="above", return_as_mask=True): 34 | """Segmentation of an array with Otsu thresholding parameters from skimage filters. 35 | :param in_array: input array representing an rgb image. 36 | :param side: must be 'above' or 'below', representing the side of the image thresholded after Otsu response. 37 | :param return_as_mask: the output can be a boolean mask if True. 38 | :return: thresholded input image according to Otsu and input parameters. 39 | """ 40 | otsu_thr = filters.threshold_otsu(in_array) 41 | if side == "above": 42 | new_data = in_array * (in_array >= otsu_thr) 43 | elif side == "below": 44 | new_data = in_array * (in_array < otsu_thr) 45 | else: 46 | raise OSError("Parameter side must be 'above' or 'below'.") 47 | if return_as_mask: 48 | new_data = new_data.astype(bool) 49 | return new_data 50 | 51 | 52 | def MoG_array( 53 | in_array, 54 | K=None, 55 | mask_array=None, 56 | pre_process_median_filter=False, 57 | output_gmm_class=False, 58 | pre_process_only_interquartile=False, 59 | see_histogram=None, 60 | reorder_mus=True, 61 | ): 62 | """Mixture of gaussians for medical images. A simple wrap of 63 | sklearn.mixture.GaussianMixture to get a mog-based segmentation of an input 64 | nibabel image. 65 | :param in_array: input array format to be segmented with a MOG method. 66 | :param K: number of classes, if None, it is estimated with a BIC criterion (may take a while) 67 | :param mask_array: nibabel mask if you want to consider only a subset of the masked data. 68 | :param pre_process_median_filter: apply a median filter before pre-processing (reduce salt and pepper noise). 69 | :param pre_process_only_interquartile: set to zero above and below interquartile (below mask if any) in the data. 70 | :param output_gmm_class: return only the gmm sklearn class instance. 71 | :param see_histogram: can be True, False (or None) or a string (with a path where to save the plotted histogram). 72 | :param reorder_mus: only if output_gmm_class=False, reorder labels from smallest to bigger means. 73 | :return: [c, p] crisp and probabilistic segmentation OR gmm, instance of the class sklearn.mixture.GaussianMixture. 74 | """ 75 | if pre_process_median_filter: 76 | print("Pre-process with a median filter.") 77 | data = medfilt(in_array) 78 | else: 79 | data = in_array 80 | data = np.copy(data.flatten().reshape(-1, 1)) 81 | 82 | if mask_array is not None: 83 | mask_data = np.copy(mask_array.flatten().astype(np.bool_).reshape(-1, 1)) 84 | data = mask_data * data 85 | 86 | if pre_process_only_interquartile: 87 | print("Get only interquartile data.") 88 | non_zero_data = data[np.where(np.nan_to_num(data) > 1e-6)] 89 | low_p = np.percentile(non_zero_data, 25) 90 | high_p = np.percentile(non_zero_data, 75) 91 | data = (data > low_p) * (data < high_p) * data 92 | 93 | if K is None: 94 | print("Estimating numbers of components with BIC criterion... may take some minutes") 95 | n_components = range(3, 15) 96 | models = [GaussianMixture(n_components=k, random_state=0).fit(data) for k in n_components] 97 | K = np.min([m.bic(data) for m in models]) 98 | print(f"Estimated number of classes according to BIC: {K}") 99 | 100 | gmm = GaussianMixture(n_components=K).fit(data) 101 | 102 | if output_gmm_class: 103 | return gmm 104 | 105 | crisp = gmm.predict(data).reshape(in_array.shape) 106 | prob = gmm.predict_proba(data).reshape(list(in_array.shape) + [K]) 107 | 108 | if reorder_mus: 109 | mu = gmm.means_.reshape(-1) 110 | p = list(np.argsort(mu)) 111 | 112 | old_labels = list(range(K)) 113 | new_labels = [p.index(l) for l in old_labels] # the inverse of p 114 | 115 | crisp = np.copy(relabeller(crisp, old_labels, new_labels)) 116 | prob = np.stack([prob[..., t] for t in new_labels], axis=3) 117 | 118 | if see_histogram is not None and see_histogram is not False: 119 | fig = plt.figure() 120 | ax = fig.add_subplot(111) 121 | ax.set_aspect(1) 122 | ax.hist(crisp.flatten(), bins=50, normed=True) 123 | lx = ax.get_xlim() 124 | x = np.arange(lx[0], lx[1], (lx[1] - lx[0]) / 1000.0) 125 | for m, s in zip(gmm.means_, gmm.precisions_.reshape(-1)): 126 | ax.plot(x, mlab.normpdf(x, m, s)) 127 | 128 | if isinstance(see_histogram, str): 129 | plt.savefig(see_histogram) 130 | else: 131 | plt.show() 132 | 133 | return crisp, prob 134 | -------------------------------------------------------------------------------- /nilabels/tools/detections/island_detection.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import numpy as np 4 | from scipy import ndimage 5 | 6 | from nilabels.tools.image_colors_manipulations.relabeller import relabeller 7 | 8 | 9 | def island_for_label(array_segm, label, m=0, special_label=-1): 10 | """As ndimage.label, with output ordered by the size of the connected component. 11 | :param array_segm: 12 | :param label: 13 | :param m: integer. If m = 0 the n connected components will be numbered from 1 (biggest) to n (smallest). 14 | If m > 0, from the m-th largest components, all the components are set to the special 15 | special_label. 16 | :param special_label: value used if m > 0. 17 | :return: segmentation with the components sorted. 18 | ----- 19 | e.g. 20 | array_segm has 4 components of for the input label. 21 | If m = 0 it returns the components labelled from 1 to 4, where 1 is the biggest. 22 | if m = 2 the first two largest components are numbered 1 and 2, and the remaining 2 are labelled with special_label. 23 | """ 24 | if label not in array_segm: 25 | msg = f"Label {label} not in the provided array." 26 | logging.info(msg) 27 | return array_segm 28 | 29 | binary_segm_comp, num_comp = ndimage.label(array_segm == label) 30 | 31 | if num_comp == 1: 32 | return binary_segm_comp 33 | 34 | voxels_per_components = np.array([np.count_nonzero(binary_segm_comp == l + 1) for l in range(num_comp)]) 35 | scores = voxels_per_components.argsort()[::-1] + 1 36 | new_labels = np.arange(num_comp) + 1 37 | 38 | binary_segm_components_sorted = relabeller(binary_segm_comp, scores, new_labels, verbose=1) 39 | 40 | if m > 0: 41 | binary_segm_components_sorted[binary_segm_components_sorted > m] = special_label 42 | 43 | return binary_segm_components_sorted 44 | -------------------------------------------------------------------------------- /nilabels/tools/image_colors_manipulations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/nilabels/4a61cd95e6bfb3644a3724461502d7cf69f5615d/nilabels/tools/image_colors_manipulations/__init__.py -------------------------------------------------------------------------------- /nilabels/tools/image_colors_manipulations/cutter.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from nilabels.tools.aux_methods.utils_nib import set_new_data 4 | 5 | 6 | def cut_4d_volume_with_a_1_slice_mask(data_4d, data_mask): 7 | """Fist slice maks is applied to all the timepoints of the volume. 8 | :param data_4d: 9 | :param data_mask: 10 | :return: 11 | """ 12 | assert data_4d.shape[:3] == data_mask.shape 13 | 14 | if len(data_4d.shape) == 3: # 4d data is actually a 3d data 15 | data_masked_4d = np.multiply(data_mask, data_4d) 16 | else: 17 | data_masked_4d = np.zeros_like(data_4d) 18 | for t in range(data_4d.shape[-1]): 19 | data_masked_4d[..., t] = np.multiply(data_mask, data_4d[..., t]) 20 | 21 | return data_masked_4d 22 | 23 | 24 | def cut_4d_volume_with_a_1_slice_mask_nib(input_4d_nib, input_mask_nib): 25 | """Fist slice maks is applied to all the timepoints of the nibabel image. 26 | :param input_4d_nib: input 4d nifty image 27 | :param input_mask_nib: input mask 28 | :return: 29 | """ 30 | data_4d = input_4d_nib.get_fdata() 31 | data_mask = input_mask_nib.get_fdata() 32 | ans = cut_4d_volume_with_a_1_slice_mask(data_4d, data_mask) 33 | 34 | return set_new_data(input_4d_nib, ans) 35 | 36 | 37 | def apply_a_mask_nib(im_input, im_mask): 38 | """Set to zero all the values outside the mask. 39 | From nibabel input and output. 40 | Adaptative - if the mask is 3D and the image is 4D, will create a temporary mask, 41 | generate the stack of masks, and apply the stacks to the image. 42 | :param im_input: nibabel image to be masked 43 | :param im_mask: nibabel image with the mask 44 | :return: im_input with intensities cropped after im_mask. 45 | """ 46 | assert len(im_mask.shape) == 3 47 | 48 | # TODO correct this: merge the cut_4d_volume_with_a_1_slice_mask here 49 | if im_mask.shape != im_input.shape[:3]: 50 | msg = f"Provided mask and image does not have compatible dimension: {im_input.shape} and {im_mask.shape}" 51 | raise OSError(msg) 52 | 53 | if len(im_input.shape) == 3: 54 | new_data = im_input.get_fdata() * im_mask.get_fdata().astype(bool) 55 | else: 56 | new_data = np.zeros_like(im_input.get_fdata()) 57 | for t in range(im_input.shape[3]): 58 | new_data[..., t] = im_input.get_fdata()[..., t] * im_mask.get_fdata().astype(bool) 59 | 60 | return set_new_data(image=im_input, new_data=new_data) 61 | -------------------------------------------------------------------------------- /nilabels/tools/image_colors_manipulations/normaliser.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from nilabels.tools.aux_methods.utils_nib import set_new_data 4 | 5 | 6 | def normalise_below_labels(im_input, im_segm, labels_list=None, stats=np.median, exclude_first_label=True): 7 | """Normalise the intensities of im_input for the stats obtained of the values found under input labels. 8 | :param im_input: nibabel image 9 | :param im_segm: nibabel segmentation 10 | :param labels_list: list of labels you want to normalise below 11 | :param stats: required statistic e.g. np.median 12 | :param exclude_first_label: remove the first label from the labels list. 13 | :return: divide for the statistics required for only the non-zero values below. 14 | """ 15 | if labels_list is not None: 16 | if exclude_first_label: 17 | labels_list = labels_list[1:] 18 | mask_data = np.zeros_like(im_segm.get_fdata(), dtype=bool) 19 | for label_k in labels_list: 20 | mask_data += im_segm.get_fdata() == label_k 21 | else: 22 | mask_data = np.zeros_like(im_segm.get_fdata(), dtype=bool) 23 | mask_data[im_segm.get_fdata() > 0] = 1 24 | 25 | masked_im_data = np.nan_to_num((mask_data.astype(np.float64) * im_input.get_fdata().astype(np.float64)).flatten()) 26 | non_zero_masked_im_data = masked_im_data[np.where(masked_im_data > 1e-6)] 27 | s = stats(non_zero_masked_im_data) 28 | assert isinstance(s, float) 29 | return set_new_data(im_input, (1 / float(s)) * im_input.get_fdata()) 30 | 31 | 32 | def intensities_normalisation_linear( 33 | im_input, 34 | im_segm, 35 | im_mask_foreground=None, 36 | toll=1e-12, 37 | percentile_range=(1, 99), 38 | output_range=(0.1, 10), 39 | ): 40 | """Normalise the values below the binarised segmentation so that the normalised image 41 | will be between 0 and 1, based on a linear transformation whose parameters are learned 42 | from the values below the segmentation. 43 | E.G. 44 | 45 | min_intensities = 1% of the intensities below the mask 46 | max_intensities = 99% of the intensities above the mask 47 | 48 | compute the parameters a and b so that: 49 | 50 | a * min_intensities + b = 0.1 51 | a * max_intensities + b = 10 52 | 53 | To all the values in the image is then applied the linear function: 54 | f(x) = a * x + b in the foreground, 55 | f(x) = 0 in the background. 56 | :return: 57 | """ 58 | mask_data = np.zeros_like(im_segm.get_fdata(), dtype=bool) 59 | mask_data[im_segm.get_fdata() > 0] = 1 60 | 61 | non_zero_below_mask = im_input.get_fdata()[np.where(mask_data > toll)].flatten() 62 | 63 | min_intensities = np.percentile(non_zero_below_mask, percentile_range[0]) 64 | max_intensities = np.percentile(non_zero_below_mask, percentile_range[1]) 65 | 66 | a = (output_range[1] - output_range[0]) / (max_intensities - min_intensities) 67 | b = output_range[0] - a * min_intensities 68 | 69 | if im_mask_foreground is None: 70 | im_mask_foreground_data = np.ones_like(im_input.get_fdata()) 71 | else: 72 | im_mask_foreground_data = im_mask_foreground.get_fdata() 73 | 74 | return set_new_data(im_input, im_mask_foreground_data * (a * im_input.get_fdata() + b)) 75 | 76 | 77 | def mahalanobis_distance_map(im, im_mask=None, trim=False): 78 | """From an image to its Mahalanobis distance map 79 | :param im: input image acquired with some modality. 80 | :param im_mask: considering only the data below the given mask. 81 | :param trim: if mask is provided the output image is masked with zeros values outside the mask. 82 | :return: nibabel image same shape as im, with the corresponding Mahalanobis map 83 | """ 84 | if im_mask is None: 85 | mu = np.mean(im.get_fdata().flatten()) 86 | sigma2 = np.std(im.get_fdata().flatten()) 87 | return set_new_data(im, np.sqrt((im.get_fdata() - mu) * sigma2 * (im.get_fdata() - mu))) 88 | 89 | np.testing.assert_array_equal(im.affine, im_mask.affine) 90 | np.testing.assert_array_equal(im.shape, im_mask.shape) 91 | mu = np.mean(im.get_fdata().flatten() * im_mask.get_fdata().flatten()) 92 | sigma2 = np.std(im.get_fdata().flatten() * im_mask.get_fdata().flatten()) 93 | new_data = np.sqrt((im.get_fdata() - mu) * sigma2 ** (-1) * (im.get_fdata() - mu)) 94 | if trim: 95 | new_data = new_data * im_mask.get_fdata().astype(bool) 96 | return set_new_data(im, new_data) 97 | -------------------------------------------------------------------------------- /nilabels/tools/image_colors_manipulations/relabeller.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import logging 3 | 4 | import numpy as np 5 | 6 | from nilabels.tools.aux_methods.sanity_checks import is_valid_permutation 7 | 8 | 9 | def relabeller(in_data, list_old_labels, list_new_labels, verbose=True): 10 | """:param in_data: array corresponding to an image segmentation. 11 | :param list_old_labels: list or tuple of labels 12 | :param list_new_labels: list or tuple of labels of the same len as list_old_labels 13 | :param verbose: 14 | :return: array where all the labels in list_new_labels are substituted with list_new_label in the same order. 15 | """ 16 | if isinstance(list_new_labels, int): 17 | list_new_labels = [list_new_labels] 18 | if isinstance(list_old_labels, int): 19 | list_old_labels = [list_old_labels] 20 | 21 | # sanity check: old and new must have the same number of elements 22 | if not len(list_old_labels) == len(list_new_labels): 23 | raise OSError("Labels lists old and new do not have the same length.") 24 | 25 | new_data = copy.deepcopy(in_data) 26 | 27 | for k in range(len(list_new_labels)): 28 | places = in_data == list_old_labels[k] 29 | if np.any(places): 30 | np.place(new_data, places, list_new_labels[k]) 31 | if verbose: 32 | msg = f"Label {list_old_labels[k]} substituted with label {list_new_labels[k]}" 33 | logging.info(msg) 34 | elif verbose: 35 | msg = f"Label {list_old_labels[k]} not present in the array" 36 | logging.info(msg) 37 | 38 | return new_data 39 | 40 | 41 | def permute_labels(in_data, permutation): 42 | """Permute the values of the labels in an int image. 43 | :param in_data: array corresponding to an image segmentation. 44 | :param permutation: 45 | :return: 46 | """ 47 | if not is_valid_permutation(permutation): 48 | raise OSError("Input permutation not valid.") 49 | return relabeller(in_data, permutation[0], permutation[1]) 50 | 51 | 52 | def erase_labels(in_data, labels_to_erase): 53 | """:param in_data: array corresponding to an image segmentation. 54 | :param labels_to_erase: list or tuple of labels 55 | :return: all the labels in the list labels_to_erase will be assigned to zero. 56 | """ 57 | if isinstance(labels_to_erase, int): 58 | labels_to_erase = [labels_to_erase] 59 | return relabeller(in_data, list_old_labels=labels_to_erase, list_new_labels=[0] * len(labels_to_erase)) 60 | 61 | 62 | def assign_all_other_labels_the_same_value(in_data, labels_to_keep, same_value_label=255): 63 | """All the labels that are not in the list labels_to_keep will be given the value same_value_label 64 | :param in_data: array corresponding to an image segmentation. 65 | :param labels_to_keep: list or tuple of value in in_data 66 | :param same_value_label: a single label value. 67 | :return: segmentation of the same size where all the labels not in the list label_to_keep will be assigned to the 68 | value same_value_label. 69 | """ 70 | list_labels = sorted(set(in_data.flat)) 71 | if isinstance(labels_to_keep, int): 72 | labels_to_keep = [labels_to_keep] 73 | 74 | labels_that_will_have_the_same_value = list(set(list_labels) - set(labels_to_keep) - {0}) 75 | 76 | return relabeller( 77 | in_data, 78 | list_old_labels=labels_that_will_have_the_same_value, 79 | list_new_labels=[same_value_label] * len(labels_that_will_have_the_same_value), 80 | ) 81 | 82 | 83 | def keep_only_one_label(in_data, label_to_keep): 84 | """From a segmentation keeps only the values in the list labels_to_keep. 85 | :param in_data: a segmentation (only positive labels allowed). 86 | :param label_to_keep: the single label that will be kept. 87 | :return: 88 | """ 89 | list_labels = sorted(set(in_data.flat)) 90 | 91 | if label_to_keep not in list_labels: 92 | msg = f"the label {label_to_keep} you want to keep is not present in the segmentation" 93 | logging.info(msg) 94 | return in_data 95 | 96 | labels_not_to_keep = list(set(list_labels) - {label_to_keep}) 97 | return relabeller( 98 | in_data, 99 | list_old_labels=labels_not_to_keep, 100 | list_new_labels=[0] * len(labels_not_to_keep), 101 | verbose=False, 102 | ) 103 | 104 | 105 | def relabel_half_side_one_label(in_data, label_old, label_new, side_to_modify, axis, plane_intercept): 106 | """:param in_data: input data array (must be 3d) 107 | :param label_old: single label to be replaced 108 | :param label_new: single label to replace with 109 | :param side_to_modify: can be the string 'above' or 'below' 110 | :param axis: can be 'x', 'y', 'z' 111 | :param plane_intercept: voxel along the selected direction plane where to consider the symmetry. 112 | :return: 113 | """ 114 | if in_data.ndim != 3: 115 | msg = "Input array must be 3-dimensional." 116 | raise OSError(msg) 117 | 118 | if side_to_modify not in ["below", "above"]: 119 | msg = "side_to_copy must be one of the two {}.".format(["below", "above"]) 120 | raise OSError(msg) 121 | 122 | if axis not in ["x", "y", "z"]: 123 | msg = "axis variable must be one of the following: {}.".format(["x", "y", "z"]) 124 | raise OSError(msg) 125 | 126 | positions = in_data == label_old 127 | halfed_positions = np.zeros_like(positions) 128 | if axis == "x": 129 | if side_to_modify == "above": 130 | halfed_positions[plane_intercept:, :, :] = positions[plane_intercept:, :, :] 131 | if side_to_modify == "below": 132 | halfed_positions[:plane_intercept, :, :] = positions[:plane_intercept, :, :] 133 | if axis == "y": 134 | if side_to_modify == "above": 135 | halfed_positions[:, plane_intercept:, :] = positions[:, plane_intercept:, :] 136 | if side_to_modify == "below": 137 | halfed_positions[:, plane_intercept, :] = positions[:, plane_intercept, :] 138 | if axis == "z": 139 | if side_to_modify == "above": 140 | halfed_positions[:, :, plane_intercept:] = positions[:, :, plane_intercept:] 141 | if side_to_modify == "below": 142 | halfed_positions[:, :, :plane_intercept] = positions[:, :, :plane_intercept] 143 | 144 | return in_data * np.invert(halfed_positions) + label_new * halfed_positions.astype(int) 145 | -------------------------------------------------------------------------------- /nilabels/tools/image_colors_manipulations/segmentation_to_rgb.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from nilabels.tools.aux_methods.utils_nib import set_new_data 4 | 5 | 6 | def get_rgb_image_from_segmentation_and_label_descriptor(im_segm, ldm, invert_black_white=False, dtype_output=np.int32): 7 | """From the labels descriptor and a nibabel segmentation image. 8 | :param im_segm: nibabel segmentation whose labels corresponds to the input labels descriptor. 9 | :param ldm: instance of class label descriptor manager. 10 | :param dtype_output: data type of the output image. 11 | :param invert_black_white: to swap black with white (improving background visualisation). 12 | :return: a 4d image, where at each voxel there is the [r, g, b] vector in the fourth dimension. 13 | """ 14 | 15 | if not len(im_segm.shape) == 3: 16 | raise OSError("input segmentation must be 3D.") 17 | 18 | rgb_image_arr = np.ones(list(im_segm.shape) + [3]) 19 | 20 | for l in ldm.dict_label_descriptor: 21 | pl = im_segm.get_fdata() == l 22 | rgb_image_arr[pl, :] = ldm.dict_label_descriptor[l][0] 23 | 24 | if invert_black_white: 25 | pl = im_segm.get_fdata() == 0 26 | rgb_image_arr[pl, :] = np.array([255, 255, 255]) 27 | return set_new_data(im_segm, rgb_image_arr, new_dtype=dtype_output) 28 | -------------------------------------------------------------------------------- /nilabels/tools/image_shape_manipulations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/nilabels/4a61cd95e6bfb3644a3724461502d7cf69f5615d/nilabels/tools/image_shape_manipulations/__init__.py -------------------------------------------------------------------------------- /nilabels/tools/image_shape_manipulations/apply_passepartout.py: -------------------------------------------------------------------------------- 1 | from nilabels.tools.aux_methods.utils_nib import images_are_overlapping 2 | from nilabels.tools.detections.contours import get_xyz_borders_of_a_label, set_new_data 3 | 4 | 5 | def crop_with_passepartout(im_input, passepartout_values): 6 | """:param im_input: 7 | :param passepartout_values: in the form [x_min, -x_max, y_min, -y_max, z_min, -z_max]. where -x_max is the 8 | thickness of the border from the border. 9 | :return: 10 | """ 11 | x_min, x_max, y_min, y_max, z_min, z_max = passepartout_values 12 | cropped_data = im_input.get_fdata()[x_min:-x_max, y_min:-y_max, z_min:-z_max] 13 | return set_new_data(im_input, cropped_data) 14 | 15 | 16 | def crop_with_passepartout_based_on_label_segmentation(im_input_to_crop, im_segm, margins, label): 17 | """:param im_input_to_crop: 18 | :param im_segm: 19 | :param margins: in the form [x,y,z] space in voxel in each direction left as passepartout 20 | around the selected label of the segmentation. 21 | :param label: label around which we want to isolate the input image to crop 22 | :return: 23 | --- 24 | Note: if label is not in the segmentation, return the input image. 25 | """ 26 | assert images_are_overlapping(im_input_to_crop, im_segm) 27 | 28 | v = get_xyz_borders_of_a_label(im_segm.get_fdata(), label) 29 | x_min, x_max = v[0] - margins[0], v[1] + margins[0] + 1 30 | y_min, y_max = v[2] - margins[1], v[3] + margins[1] + 1 31 | z_min, z_max = v[4] - margins[2], v[5] + margins[2] + 1 32 | 33 | return crop_with_passepartout(im_input_to_crop, [x_min, -x_max, y_min, -y_max, z_min, -z_max]) 34 | -------------------------------------------------------------------------------- /nilabels/tools/image_shape_manipulations/merger.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from nilabels.tools.aux_methods.utils_nib import set_new_data 4 | 5 | 6 | def merge_labels_from_4d(in_data, keep_original_values=True): 7 | """Can be the inverse function of split label with default parameters. 8 | The labels are assuming to have no overlaps. 9 | :param in_data: 4d volume 10 | :param keep_original_values: merge the labels with their values, otherwise it uses the values of the slice 11 | numbering (including zero label!). 12 | :return: 13 | """ 14 | if in_data.ndim != 4: 15 | msg = "Input array must be 4-dimensional." 16 | raise OSError(msg) 17 | 18 | in_data_shape = in_data.shape 19 | out_data = np.zeros(in_data_shape[:3], dtype=in_data.dtype) 20 | 21 | for t in range(in_data.shape[3]): 22 | slice_t = in_data[..., t] 23 | if keep_original_values: 24 | out_data = out_data + slice_t 25 | else: 26 | out_data = out_data + ((t + 1) * slice_t.astype(bool)).astype(in_data.dtype) 27 | return out_data 28 | 29 | 30 | def stack_images(list_images): 31 | """From a list of images of the same shape, the stack of these images in the new dimension. 32 | :param list_images: 33 | :return: stack image of the input list 34 | """ 35 | msg = "input images shapes are not all of the same dimension" 36 | assert False not in [list_images[0].shape == im.shape for im in list_images[1:]], msg 37 | new_data = np.stack([nib_image.get_fdata() for nib_image in list_images], axis=len(list_images[0].shape)) 38 | return set_new_data(list_images[0], new_data) 39 | 40 | 41 | def reproduce_slice_fourth_dimension(nib_image, num_slices=10, repetition_axis=3): 42 | im_sh = nib_image.shape 43 | if not (len(im_sh) == 2 or len(im_sh) == 3): 44 | raise OSError("Methods can be used only for 2 or 3 dim images. No conflicts with existing multi, slices") 45 | 46 | new_data = np.stack([nib_image.get_fdata()] * num_slices, axis=repetition_axis) 47 | return set_new_data(nib_image, new_data) 48 | 49 | 50 | def grafting(im_hosting, im_patch, im_patch_mask=None): 51 | """Takes an hosting image, an image patch and a patch mask (optional) of the same dimension and 52 | in the same real space. 53 | It crops the patch (or patch mask if present) on the hosting image, and substitute the value from the patch. 54 | :param im_hosting: Mould or holder of the patch 55 | :param im_patch: patch to add. 56 | :param im_patch_mask: mask in case the mould is not zero in the region where the patch goes. 57 | :return: 58 | """ 59 | np.testing.assert_array_equal(im_hosting.affine, im_patch.affine) 60 | 61 | if im_patch_mask is None: 62 | patch_region = im_patch.get_fdata().astype(bool) 63 | else: 64 | np.testing.assert_array_equal(im_hosting.affine, im_patch_mask.affine) 65 | np.testing.assert_array_equal(im_hosting.shape, im_patch_mask.shape) 66 | 67 | patch_region = im_patch_mask.get_fdata().astype(bool) 68 | 69 | patch_inverted = np.invert(patch_region) 70 | new_data = im_hosting.get_fdata() * patch_inverted + im_patch.get_fdata() * patch_region 71 | 72 | return set_new_data(im_hosting, new_data) 73 | 74 | 75 | def from_segmentations_stack_to_probabilistic_segmentation(arr_labels_stack): 76 | """A probabilistic atlas has at each time-point a different label (a mapping is provided as well in 77 | the conversion with correspondence time-point<->label number). Each time point has the normalised average 78 | of each label. 79 | :param arr_labels_stack: stack of 1D arrays (segmentations x num_voxels) containing a different discrete 80 | segmentation. 81 | Values of labels needs to be consecutive, or there will be empty images in the result. 82 | :return: 83 | N number of voxels, J number of segmentations, K number of labels. 84 | """ 85 | J, K = arr_labels_stack.shape[0], np.max(arr_labels_stack) + 1 86 | return 1 / float(J) * np.stack([np.sum(arr_labels_stack == k, axis=0).astype(np.float64) for k in range(K)], axis=0) 87 | 88 | 89 | def substitute_volume_at_timepoint(im_input_4d, im_input_3d, timepoint): 90 | """Substitute the im_input_3d image at the time point timepoint of the im_input_4d. 91 | :param im_input_4d: 4d image 92 | :param im_input_3d: 3d image whose shape is compatible with the fist 3 dimensions of im_input_4d 93 | :param timepoint: a timepoint in the 4th dimension of the im_input_4d 94 | :return: im_input_4d whose at the timepoint-th time point the data of im_input_3d are stored. 95 | Handle base case: If the input 4d volume is actually a 3d and timepoint is 0, then just return the same volume. 96 | """ 97 | if len(im_input_4d.shape) == 3 and timepoint == 0: 98 | return im_input_3d 99 | if len(im_input_4d.shape) == 4 and timepoint < im_input_4d.shape[-1]: 100 | new_data = im_input_4d.get_fdata()[:] 101 | new_data[..., timepoint] = im_input_3d.get_fdata()[:] 102 | return set_new_data(im_input_4d, new_data) 103 | raise OSError("Incompatible shape input volume.") 104 | -------------------------------------------------------------------------------- /nilabels/tools/image_shape_manipulations/splitter.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def split_labels_to_4d(in_data, list_labels=(), keep_original_values=True): 5 | """Split labels of a 3d segmentation in a 4d segmentation, 6 | one label for each slice in ascending order. 7 | Labels can be relabelled in consecutive order or can keep the 8 | original labels value. 9 | :param in_data: segmentation (only positive labels allowed). 10 | :param list_labels: list of labels to split. 11 | :param keep_original_values: boolean otherwise keep the same 12 | value for all the 13 | :return: 14 | """ 15 | msg = "Input array must be 3-dimensional." 16 | assert in_data.ndim == 3, msg 17 | 18 | out_data = np.zeros(list(in_data.shape) + [len(list_labels)], dtype=in_data.dtype) 19 | 20 | for l_index, l in enumerate(list_labels): 21 | places_l = in_data == l 22 | if keep_original_values: 23 | out_data[..., l_index] = l * places_l # .astype(in_data.dtype) 24 | else: 25 | out_data[..., l_index] = places_l # .astype(in_data.dtype) 26 | 27 | return out_data 28 | -------------------------------------------------------------------------------- /nilabels/tools/visualiser/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/nilabels/4a61cd95e6bfb3644a3724461502d7cf69f5615d/nilabels/tools/visualiser/__init__.py -------------------------------------------------------------------------------- /nilabels/tools/visualiser/see_volume.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os.path import join as jph 3 | 4 | import matplotlib.pyplot as plt 5 | import nibabel as nib 6 | import numpy as np 7 | from matplotlib import rc 8 | 9 | from nilabels.tools.aux_methods.utils import print_and_run 10 | 11 | 12 | def see_array(in_array, pfo_tmp="./z_tmp", in_array_segm=None, pfi_label_descriptor=None, block=False): 13 | """Itk-snap based quick array visualiser. 14 | 15 | :param in_array: numpy array or list of numpy array same dimension (GIGO). 16 | :param pfo_tmp: path to file temporary folder. 17 | :param in_array_segm: if there is a single array representing a segmentation (in this case all images must 18 | have the same shape). 19 | :param pfi_label_descriptor: path to file to a label descriptor in ITK-snap standard format. 20 | :param block: if want to stop after each show. 21 | :return: 22 | """ 23 | if isinstance(in_array, list): 24 | assert len(in_array) > 0 25 | sh = in_array[0].shape 26 | for arr in in_array[1:]: 27 | assert sh == arr.shape 28 | print_and_run(f"mkdir {pfo_tmp}") 29 | cmd = "itksnap -g " 30 | for arr_id, arr in enumerate(in_array): 31 | im = nib.Nifti1Image(arr, affine=np.eye(4)) 32 | pfi_im = jph(pfo_tmp, f"im_{arr_id}.nii.gz") 33 | nib.save(im, pfi_im) 34 | if arr_id == 1: 35 | cmd += f" -o {pfi_im} " 36 | else: 37 | cmd += f" {pfi_im} " 38 | elif isinstance(in_array, np.ndarray): 39 | print_and_run(f"mkdir {pfo_tmp}") 40 | im = nib.Nifti1Image(in_array, affine=np.eye(4)) 41 | pfi_im = jph(pfo_tmp, "im_0.nii.gz") 42 | nib.save(im, pfi_im) 43 | cmd = f"itksnap -g {pfi_im}" 44 | else: 45 | raise OSError 46 | if in_array_segm is not None: 47 | im_segm = nib.Nifti1Image(in_array_segm, affine=np.eye(4)) 48 | pfi_im_segm = jph(pfo_tmp, "im_segm_0.nii.gz") 49 | nib.save(im_segm, pfi_im_segm) 50 | cmd += f" -s {pfi_im_segm} " 51 | if pfi_label_descriptor and os.path.exists(pfi_label_descriptor): 52 | cmd += f" -l {pfi_im_segm} " 53 | print_and_run(cmd) 54 | if block: 55 | _ = input("Press any key to continue.") 56 | 57 | 58 | def see_image_slice_with_a_grid( 59 | pfi_image, 60 | fig_num=1, 61 | axis_quote=("y", 230), 62 | vmin=None, 63 | vmax=None, 64 | cmap="gray", 65 | pfi_where_to_save=None, 66 | ): 67 | rc("text", usetex=True) 68 | fig = plt.figure(fig_num, figsize=(6, 6)) 69 | fig.canvas.set_window_title(f"canvas {fig_num}") 70 | ax = fig.add_subplot(111) 71 | 72 | im = nib.load(pfi_image) 73 | if axis_quote[0] == "x": 74 | data = im.get_fdata()[axis_quote[1], :, :].T 75 | shape = data.shape 76 | 77 | voxel_origin = np.array([axis_quote[1], 0, 0, 1]) 78 | voxel_x = np.array([axis_quote[1], shape[1], 0, 1]) 79 | voxel_y = np.array([axis_quote[1], 0, shape[0], 1]) 80 | 81 | affine = im.affine 82 | 83 | pt_origin = affine.dot(voxel_origin) 84 | pt_x = affine.dot(voxel_x) 85 | pt_y = affine.dot(voxel_y) 86 | 87 | horizontal_min = pt_origin[1] 88 | horizontal_max = pt_x[1] 89 | vertical_min = pt_origin[2] 90 | vertical_max = pt_y[2] 91 | 92 | extent = [horizontal_min, horizontal_max, vertical_min, vertical_max] 93 | 94 | elif axis_quote[0] == "y": 95 | data = im.get_fdata()[:, axis_quote[1], :].T 96 | shape = data.shape 97 | 98 | voxel_origin = np.array([0, axis_quote[1], 0, 1]) 99 | voxel_x = np.array([shape[1], axis_quote[1], 0, 1]) 100 | voxel_y = np.array([0, axis_quote[1], shape[0], 1]) 101 | 102 | affine = im.affine 103 | 104 | pt_origin = affine.dot(voxel_origin) 105 | pt_x = affine.dot(voxel_x) 106 | pt_y = affine.dot(voxel_y) 107 | 108 | horizontal_min = pt_origin[0] 109 | horizontal_max = pt_x[0] 110 | vertical_min = pt_origin[2] 111 | vertical_max = pt_y[2] 112 | 113 | extent = [horizontal_min, horizontal_max, vertical_min, vertical_max] 114 | 115 | elif axis_quote[0] == "z": 116 | data = im.get_fdata()[:, :, axis_quote[1]].T 117 | shape = data.shape 118 | 119 | voxel_origin = np.array([0, 0, axis_quote[1], 1]) 120 | voxel_x = np.array([shape[1], 0, axis_quote[1], 1]) 121 | voxel_y = np.array([0, shape[0], axis_quote[1], 1]) 122 | 123 | affine = im.affine 124 | 125 | pt_origin = affine.dot(voxel_origin) 126 | pt_x = affine.dot(voxel_x) 127 | pt_y = affine.dot(voxel_y) 128 | 129 | horizontal_min = pt_origin[0] 130 | horizontal_max = pt_x[0] 131 | vertical_min = pt_origin[1] 132 | vertical_max = pt_y[1] 133 | 134 | extent = [horizontal_min, horizontal_max, vertical_min, vertical_max] 135 | 136 | else: 137 | raise OSError 138 | 139 | ax.imshow(data, extent=extent, origin="lower", interpolation="nearest", cmap=cmap, vmin=vmin, vmax=vmax) 140 | 141 | ax.grid(color="grey", linestyle="-", linewidth=0.5) 142 | ax.set_aspect("equal") 143 | 144 | for tick in ax.xaxis.get_major_ticks(): 145 | tick.label.set_fontsize(8) 146 | for tick in ax.yaxis.get_major_ticks(): 147 | tick.label.set_fontsize(8) 148 | 149 | if pfi_where_to_save is not None: 150 | plt.savefig(pfi_where_to_save, format="pdf", dpi=200) 151 | -------------------------------------------------------------------------------- /nilabels/tools/visualiser/volume_manipulations_for_visualisation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from nilabels.tools.aux_methods.utils_nib import set_new_data 4 | 5 | 6 | def exploded_segmentation(im_segm, direction, intercepts, offset, dtype=int): 7 | """Damien Hirst-like sectioning of an anatomical segmentation. 8 | :param im_segm: nibabel image segmentation 9 | :param direction: sectioning direction, can be sagittal, axial or coronal 10 | (conventional names for images oriented to standard (diagonal affine transformation)) 11 | :param intercepts: list of values of the stack plane in the input segmentation. 12 | Needs to include the max plane and the min plane 13 | :param offset: voxel to leave empty between one slice and the other 14 | :return: nibabel image output as sectioning of the input one. 15 | """ 16 | if direction.lower() == "axial": 17 | block = np.zeros([im_segm.shape[0], im_segm.shape[1], offset]).astype(dtype) 18 | stack = [] 19 | for j in range(1, len(intercepts)): 20 | stack += [im_segm.get_fdata()[:, :, intercepts[j - 1] : intercepts[j]].astype(dtype)] + [block] 21 | return set_new_data(im_segm, np.concatenate(stack, axis=2)) 22 | 23 | if direction.lower() == "sagittal": 24 | block = np.zeros([offset, im_segm.shape[1], im_segm.shape[2]]).astype(dtype) 25 | stack = [] 26 | for j in range(1, len(intercepts)): 27 | stack += [im_segm.get_fdata()[intercepts[j - 1] : intercepts[j], :, :].astype(dtype)] + [block] 28 | return set_new_data(im_segm, np.concatenate(stack, axis=0)) 29 | 30 | if direction.lower() == "coronal": 31 | block = np.zeros([im_segm.shape[0], offset, im_segm.shape[2]]).astype(dtype) 32 | stack = [] 33 | for j in range(1, len(intercepts)): 34 | stack += [im_segm.get_fdata()[:, intercepts[j - 1] : intercepts[j], :].astype(dtype)] + [block] 35 | return set_new_data(im_segm, np.concatenate(stack, axis=1)) 36 | 37 | raise OSError 38 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "nilabels" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["SebastianoF "] 6 | license = "MIT License" 7 | readme = "README.md" 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.9" 11 | matplotlib = "^3.8.4" 12 | nibabel = ">=2.3.3" 13 | numpy = "^1.16.0" 14 | pandas = ">=0.23.4" 15 | scipy = ">=1.2.0" 16 | setuptools = ">=40.6.3" 17 | scikit-image = ">=0.14.2" 18 | sympy = ">=1.3" 19 | tabulate = ">=0.8.2" 20 | scikit-learn = ">=1.5.0" 21 | 22 | 23 | [tool.poetry.group.dev.dependencies] 24 | pytest = "^8.1.1" 25 | ruff = "^0.3.7" 26 | 27 | [build-system] 28 | requires = ["poetry-core"] 29 | build-backend = "poetry.core.masonry.api" 30 | 31 | [tool.ruff] 32 | line-length = 120 33 | 34 | [tool.ruff.lint] 35 | extend-select = ["ALL"] 36 | ignore = [ 37 | "FIX002", # TODOs related 38 | "TD002", 39 | "TD003", 40 | "TD004", 41 | "TD005", 42 | "D103", # docstring missing. Not all functions require doctsings 43 | "D100", # docstring missing. Not all modules require doctsings 44 | "D107", # missing docstrings 45 | "D", # TODO 46 | "ANN", # TODO 47 | "NPY", 48 | "PTH", 49 | "FBT", 50 | "S101", # we don't dislike assert statements 51 | "PLR0913", # too many arguments allowed 52 | "PLR2004", # magic values allowed 53 | "N806", # uppercase variable name 54 | "ERA001", # commented out code 55 | "EM101", 56 | "EM103", 57 | "RUF005", 58 | "PLR0915", 59 | "TCH002", # false positives 60 | "E741", 61 | "TRY003", 62 | "T201", # some print statements 63 | "N", 64 | "SIM115", # TODO 65 | "PD901", 66 | "ISC001", 67 | "B008", 68 | "RET504", 69 | "PLR0912", 70 | "SIM118", 71 | "PLR0912", 72 | "C901", 73 | "SIM108", 74 | "S602", 75 | "PERF401", 76 | "UP030", 77 | "E501", 78 | "S605", 79 | "S605", 80 | 81 | ] 82 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | contourpy==1.2.1 ; python_version >= "3.9" and python_version < "4.0" 2 | cycler==0.12.1 ; python_version >= "3.9" and python_version < "4.0" 3 | fonttools==4.53.0 ; python_version >= "3.9" and python_version < "4.0" 4 | imageio==2.34.1 ; python_version >= "3.9" and python_version < "4.0" 5 | importlib-resources==6.4.0 ; python_version >= "3.9" and python_version < "3.10" 6 | joblib==1.4.2 ; python_version >= "3.9" and python_version < "4.0" 7 | kiwisolver==1.4.5 ; python_version >= "3.9" and python_version < "4.0" 8 | lazy-loader==0.4 ; python_version >= "3.9" and python_version < "4.0" 9 | matplotlib==3.9.0 ; python_version >= "3.9" and python_version < "4.0" 10 | mpmath==1.3.0 ; python_version >= "3.9" and python_version < "4.0" 11 | networkx==3.2.1 ; python_version >= "3.9" and python_version < "4.0" 12 | nibabel==5.2.1 ; python_version >= "3.9" and python_version < "4.0" 13 | numpy==1.26.4 ; python_version >= "3.9" and python_version < "4.0" 14 | packaging==24.1 ; python_version >= "3.9" and python_version < "4.0" 15 | pandas==2.2.2 ; python_version >= "3.9" and python_version < "4.0" 16 | pillow==10.3.0 ; python_version >= "3.9" and python_version < "4.0" 17 | pyparsing==3.1.2 ; python_version >= "3.9" and python_version < "4.0" 18 | python-dateutil==2.9.0.post0 ; python_version >= "3.9" and python_version < "4.0" 19 | pytz==2024.1 ; python_version >= "3.9" and python_version < "4.0" 20 | scikit-image==0.22.0 ; python_version >= "3.9" and python_version < "4.0" 21 | scikit-learn==1.5.0 ; python_version >= "3.9" and python_version < "4.0" 22 | scipy==1.13.1 ; python_version >= "3.9" and python_version < "4.0" 23 | setuptools==70.0.0 ; python_version >= "3.9" and python_version < "4.0" 24 | six==1.16.0 ; python_version >= "3.9" and python_version < "4.0" 25 | sympy==1.12.1 ; python_version >= "3.9" and python_version < "4.0" 26 | tabulate==0.9.0 ; python_version >= "3.9" and python_version < "4.0" 27 | threadpoolctl==3.5.0 ; python_version >= "3.9" and python_version < "4.0" 28 | tifffile==2024.5.22 ; python_version >= "3.9" and python_version < "4.0" 29 | tzdata==2024.1 ; python_version >= "3.9" and python_version < "4.0" 30 | zipp==3.19.2 ; python_version >= "3.9" and python_version < "3.10" 31 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/nilabels/4a61cd95e6bfb3644a3724461502d7cf69f5615d/tests/__init__.py -------------------------------------------------------------------------------- /tests/agents/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/nilabels/4a61cd95e6bfb3644a3724461502d7cf69f5615d/tests/agents/__init__.py -------------------------------------------------------------------------------- /tests/agents/test_main_app_agent.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/nilabels/4a61cd95e6bfb3644a3724461502d7cf69f5615d/tests/agents/test_main_app_agent.py -------------------------------------------------------------------------------- /tests/tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/nilabels/4a61cd95e6bfb3644a3724461502d7cf69f5615d/tests/tools/__init__.py -------------------------------------------------------------------------------- /tests/tools/decorators_tools.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import os 3 | from pathlib import Path 4 | 5 | import nibabel as nib 6 | import numpy as np 7 | 8 | # PATH MANAGER 9 | 10 | 11 | test_dir = os.path.dirname(os.path.abspath(__file__)) 12 | pfo_tmp_test = Path(test_dir) / "z_tmp_test" 13 | 14 | # AUXILIARIES 15 | 16 | 17 | def is_a_string_number(s): 18 | try: 19 | float(s) 20 | except ValueError: 21 | return False 22 | else: 23 | return True 24 | 25 | 26 | # DECORATORS 27 | 28 | 29 | def create_and_erase_temporary_folder(test_func): 30 | def wrap(*args, **kwargs): 31 | # 1) Before: create folder 32 | pfo_tmp_test.mkdir(parents=True, exist_ok=True) 33 | # 2) Run test 34 | test_func(*args, **kwargs) 35 | 36 | return wrap 37 | 38 | 39 | def create_and_erase_temporary_folder_with_a_dummy_nifti_image(test_func): 40 | def wrap(*args, **kwargs): 41 | # 1) Before: create folder 42 | pfo_tmp_test.mkdir(parents=True, exist_ok=True) 43 | nib_im = nib.Nifti1Image(np.zeros((30, 30, 30)), affine=np.eye(4)) 44 | nib.save(nib_im, pfo_tmp_test / "dummy_image.nii.gz") 45 | # 2) Run test 46 | test_func(*args, **kwargs) 47 | 48 | return wrap 49 | 50 | 51 | def write_and_erase_temporary_folder(test_func): 52 | def wrap(*args, **kwargs): 53 | # 1) Before: create folder 54 | pfo_tmp_test.mkdir(parents=True, exist_ok=True) 55 | # 2) Run test 56 | test_func(*args, **kwargs) 57 | 58 | return wrap 59 | 60 | 61 | def write_and_erase_temporary_folder_with_dummy_labels_descriptor(test_func): 62 | def wrap(*args, **kwargs): 63 | # 1) Before: create folder 64 | pfo_tmp_test.mkdir(parents=True, exist_ok=True) 65 | # 1bis) Then, generate dummy descriptor in the generated folder 66 | descriptor_dummy = """################################################ 67 | # ITK-SnAP Label Description File 68 | # File format: 69 | # IDX -R- -G- -B- -A-- VIS MSH LABEL 70 | # Fields: 71 | # IDX: Zero-based index 72 | # -R-: Red color component (0..255) 73 | # -G-: Green color component (0..255) 74 | # -B-: Blue color component (0..255) 75 | # -A-: Label transparency (0.00 .. 1.00) 76 | # VIS: Label visibility (0 or 1) 77 | # IDX: Label mesh visibility (0 or 1) 78 | # LABEL: Label description 79 | ################################################ 80 | 0 0 0 0 0 0 0 "background" 81 | 1 255 0 0 1 1 1 "label one (l1)" 82 | 2 204 0 0 1 1 1 "label two (l2)" 83 | 3 51 51 255 1 1 1 "label three" 84 | 4 102 102 255 1 1 1 "label four" 85 | 5 0 204 51 1 1 1 "label five (l5)" 86 | 6 51 255 102 1 1 1 "label six" 87 | 7 255 255 0 1 1 1 "label seven" 88 | 8 255 50 50 1 1 1 "label eight" """ 89 | with open(pfo_tmp_test / "labels_descriptor.txt", "w+") as f: 90 | f.write(descriptor_dummy) 91 | # 2) Run test 92 | test_func(*args, **kwargs) 93 | 94 | return wrap 95 | 96 | 97 | def write_and_erase_temporary_folder_with_left_right_dummy_labels_descriptor(test_func): 98 | def wrap(*args, **kwargs): 99 | # 1) Before: create folder 100 | pfo_tmp_test.mkdir(parents=True, exist_ok=True) 101 | # 1bis) Then, generate summy descriptor left right in the generated folder 102 | d = collections.OrderedDict() 103 | d.update({0: [[0, 0, 0], [0, 0, 0], "background"]}) 104 | d.update({1: [[255, 0, 0], [1, 1, 1], "label A Left"]}) 105 | d.update({2: [[204, 0, 0], [1, 1, 1], "label A Right"]}) 106 | d.update({3: [[51, 51, 255], [1, 1, 1], "label B Left"]}) 107 | d.update({4: [[102, 102, 255], [1, 1, 1], "label B Right"]}) 108 | d.update({5: [[0, 204, 51], [1, 1, 1], "label C"]}) 109 | d.update({6: [[51, 255, 102], [1, 1, 1], "label D"]}) 110 | d.update({7: [[255, 255, 0], [1, 1, 1], "label E Left"]}) 111 | d.update({8: [[255, 50, 50], [1, 1, 1], "label E Right"]}) 112 | with open(pfo_tmp_test / "labels_descriptor_RL.txt", "w+") as f: 113 | for j in d: 114 | line = f'{j: >5}{d[j][0][0]: >6}{d[j][0][1]: >6}{d[j][0][2]: >6}{d[j][1][0]: >9}{d[j][1][1]: >6}{d[j][1][2]: >6} "{d[j][2]}"\n' 115 | f.write(line) 116 | # 2) Run test 117 | test_func(*args, **kwargs) 118 | 119 | return wrap 120 | 121 | 122 | def create_and_erase_temporary_folder_with_a_dummy_b_vectors_list(test_func): 123 | def wrap(*args, **kwargs): 124 | # 1) Before: create folder 125 | pfo_tmp_test.mkdir(parents=True, exist_ok=True) 126 | # noinspection PyTypeChecker 127 | np.savetxt(pfo_tmp_test / "b_vects_file.txt", np.random.randn(10, 3)) 128 | # 2) Run test 129 | test_func(*args, **kwargs) 130 | 131 | return wrap 132 | -------------------------------------------------------------------------------- /tests/tools/test_aux_methods_morphological_operations.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy.testing import assert_array_equal, assert_raises 3 | 4 | from nilabels.tools.aux_methods.morpological_operations import ( 5 | get_circle_shell_for_given_radius, 6 | get_morphological_mask, 7 | get_morphological_patch, 8 | get_values_below_patch, 9 | ) 10 | 11 | # TEST aux_methods.morphological.py 12 | 13 | 14 | def test_get_morpological_patch(): 15 | expected = np.ones([3, 3]).astype(bool) 16 | expected[0, 0] = False 17 | expected[0, 2] = False 18 | expected[2, 0] = False 19 | expected[2, 2] = False 20 | assert_array_equal(get_morphological_patch(2, "circle"), expected) 21 | assert_array_equal(get_morphological_patch(2, "square"), np.ones([3, 3]).astype(bool)) 22 | 23 | 24 | def test_get_morpological_patch_not_allowed_input(): 25 | with assert_raises(IOError): 26 | get_morphological_patch(2, "spam") 27 | 28 | 29 | def test_get_morphological_mask_not_allowed_input(): 30 | with assert_raises(IOError): 31 | get_morphological_mask((5, 5), (11, 11), radius=2, shape="spam") 32 | 33 | 34 | def test_get_morphological_mask_with_morpho_patch(): 35 | morpho_patch = np.array([[[False, True, False], 36 | [True, True, True], 37 | [False, True, False]], 38 | 39 | [[True, True, True], 40 | [True, False, True], 41 | [True, True, True]], 42 | 43 | [[False, True, False], 44 | [True, True, True], 45 | [False, True, False]]]) 46 | 47 | arr_mask = get_morphological_mask((2, 2, 2), (4, 4, 4), radius=1, shape="unused", morpho_patch=morpho_patch) 48 | 49 | expected_arr_mask = np.array([[[False, False, False, False], 50 | [False, False, False, False], 51 | [False, False, False, False], 52 | [False, False, False, False]], 53 | 54 | [[False, False, False, False], 55 | [False, False, True, False], 56 | [False, True, True, True], 57 | [False, False, True, False]], 58 | 59 | [[False, False, False, False], 60 | [False, True, True, True], 61 | [False, True, False, True], 62 | [False, True, True, True]], 63 | 64 | [[False, False, False, False], 65 | [False, False, True, False], 66 | [False, True, True, True], 67 | [False, False, True, False]]]) 68 | 69 | assert_array_equal(arr_mask, expected_arr_mask) 70 | 71 | 72 | def test_get_morphological_mask_with_zero_radius(): 73 | arr_mask = get_morphological_mask((2, 2, 2), (5, 5, 5), radius=0, shape="circle") 74 | 75 | expected_arr_mask = np.zeros((5, 5, 5), dtype=bool) 76 | expected_arr_mask[2, 2, 2] = 1 77 | 78 | assert_array_equal(arr_mask, expected_arr_mask) 79 | 80 | 81 | def test_get_morphological_mask_without_morpho_patch(): 82 | arr_mask = get_morphological_mask((2, 2), (5, 5), radius=2, shape="circle") 83 | expected_arr_mask = np.array([[False, False, True, False, False], 84 | [False, True, True, True, False], 85 | [True, True, True, True, True], 86 | [False, True, True, True, False], 87 | [False, False, True, False, False]]) 88 | assert_array_equal(arr_mask, expected_arr_mask) 89 | 90 | 91 | def test_get_patch_values_simple(): 92 | # toy mask on a simple image: 93 | image = np.random.randint(0, 10, (7, 7)) 94 | patch = np.zeros_like(image).astype(bool) 95 | patch[2, 2] = True 96 | patch[2, 3] = True 97 | patch[3, 2] = True 98 | patch[3, 3] = True 99 | 100 | vals = get_values_below_patch([2, 2, 2], image, morpho_mask=patch) 101 | assert_array_equal([image[2, 2], image[2, 3], image[3, 2], image[3, 3]], vals) 102 | 103 | 104 | def test_get_values_below_patch_no_morpho_mask(): 105 | image = np.ones((7, 7)) 106 | vals = get_values_below_patch([3, 3], image, radius=1, shape="square") 107 | 108 | assert_array_equal([1.0 ] * 9, vals) 109 | 110 | 111 | def test_get_shell_for_given_radius(): 112 | expected_ans = [(-2, 0, 0), (-1, -1, -1), (-1, -1, 0), (-1, -1, 1), (-1, 0, -1), (-1, 0, 1), (-1, 1, -1), 113 | (-1, 1, 0), (-1, 1, 1), (0, -2, 0), (0, -1, -1), (0, -1, 1), (0, 0, -2), (0, 0, 2), (0, 1, -1), 114 | (0, 1, 1), (0, 2, 0), (1, -1, -1), (1, -1, 0), (1, -1, 1), (1, 0, -1), (1, 0, 1), (1, 1, -1), 115 | (1, 1, 0), (1, 1, 1), (2, 0, 0)] 116 | computed_ans = get_circle_shell_for_given_radius(2) 117 | 118 | assert_array_equal(expected_ans, computed_ans) 119 | 120 | 121 | def get_circle_shell_for_given_radius_2d(): 122 | expected_ans = [(-2, 0), (-1, -1), (-1, 1), (0, -2), (0, 2), (1, -1), (1, 1), (2, 0)] 123 | computed_ans = get_circle_shell_for_given_radius(2, dimension=2) 124 | np.testing.assert_array_equal(expected_ans, computed_ans) 125 | 126 | 127 | def get_circle_shell_for_given_radius_3_2d(): 128 | expected_ans = [(-3, 0), (-2, -2), (-2, -1), (-2, 1), (-2, 2), (-1, -2), (-1, 2), (0, -3), (0, 3), (1, -2), 129 | (1, 2), (2, -2), (2, -1), (2, 1), (2, 2), (3, 0)] 130 | computed_ans = get_circle_shell_for_given_radius(3, dimension=2) 131 | assert_array_equal(expected_ans, computed_ans) 132 | 133 | 134 | def get_circle_shell_for_given_radius_wrong_input_nd(): 135 | with assert_raises(IOError): 136 | get_circle_shell_for_given_radius(2, dimension=4) 137 | with assert_raises(IOError): 138 | get_circle_shell_for_given_radius(2, dimension=1) 139 | 140 | 141 | if __name__ == "__main__": 142 | test_get_morpological_patch() 143 | test_get_morpological_patch_not_allowed_input() 144 | test_get_morphological_mask_not_allowed_input() 145 | test_get_morphological_mask_with_morpho_patch() 146 | test_get_morphological_mask_with_zero_radius() 147 | test_get_morphological_mask_without_morpho_patch() 148 | test_get_values_below_patch_no_morpho_mask() 149 | test_get_patch_values_simple() 150 | test_get_shell_for_given_radius() 151 | get_circle_shell_for_given_radius_2d() 152 | get_circle_shell_for_given_radius_3_2d() 153 | get_circle_shell_for_given_radius_wrong_input_nd() 154 | -------------------------------------------------------------------------------- /tests/tools/test_aux_methods_permutations.py: -------------------------------------------------------------------------------- 1 | from numpy.testing import assert_array_equal, assert_raises 2 | 3 | from nilabels.tools.aux_methods.utils import ( 4 | permutation_from_cauchy_to_disjoints_cycles, 5 | permutation_from_disjoint_cycles_to_cauchy, 6 | ) 7 | 8 | # Test permutations: 9 | 10 | 11 | def test_from_permutation_to_disjoints_cycles(): 12 | cauchy_perm = [[1, 2, 3, 4, 5], [3, 4, 5, 2, 1]] 13 | cycles_perm = permutation_from_cauchy_to_disjoints_cycles(cauchy_perm) 14 | expected_ans = [[1, 3, 5], [2, 4]] 15 | for c1, c2 in zip(expected_ans, cycles_perm): 16 | assert_array_equal(c1, c2) 17 | 18 | 19 | def test_from_disjoint_cycles_to_permutation(): 20 | cycles_perm = [[1, 3, 5], [2, 4]] 21 | cauchy_perm = permutation_from_disjoint_cycles_to_cauchy(cycles_perm) 22 | expected_ans = [[1, 2, 3, 4, 5], [3, 4, 5, 2, 1]] 23 | for c1, c2 in zip(cauchy_perm, expected_ans): 24 | assert_array_equal(c1, c2) 25 | 26 | 27 | def test_from_permutation_to_disjoints_cycles_single_cycle(): 28 | cauchy_perm = [[1, 2, 3, 4, 5, 6, 7], 29 | [3, 4, 5, 1, 2, 7, 6]] 30 | cycles_perm = permutation_from_cauchy_to_disjoints_cycles(cauchy_perm) 31 | expected_ans = [[1, 3, 5, 2, 4], [6, 7]] 32 | 33 | for c1, c2 in zip(expected_ans, cycles_perm): 34 | assert_array_equal(c1, c2) 35 | 36 | 37 | def test_from_permutation_to_disjoints_cycles_single_cycle_no_valid_permutation(): 38 | cauchy_perm = [[1, 2, 3, 4, 5, 6, 7], 39 | [3, 4, 5, 1, 2, 7]] 40 | with assert_raises(IOError): 41 | permutation_from_cauchy_to_disjoints_cycles(cauchy_perm) 42 | 43 | 44 | def test_from_disjoint_cycles_to_permutation_single_cycle(): 45 | cycles_perm = [[1, 3, 5, 2, 4]] 46 | cauchy_perm = permutation_from_disjoint_cycles_to_cauchy(cycles_perm) 47 | expected_ans = [[1, 2, 3, 4, 5], [3, 4, 5, 1, 2]] 48 | 49 | for c1, c2 in zip(cauchy_perm, expected_ans): 50 | assert_array_equal(c1, c2) 51 | 52 | 53 | if __name__ == "__main__": 54 | test_from_permutation_to_disjoints_cycles() 55 | test_from_permutation_to_disjoints_cycles_single_cycle_no_valid_permutation() 56 | test_from_disjoint_cycles_to_permutation() 57 | test_from_permutation_to_disjoints_cycles_single_cycle() 58 | test_from_disjoint_cycles_to_permutation_single_cycle() 59 | -------------------------------------------------------------------------------- /tests/tools/test_aux_methods_sanity_checks.py: -------------------------------------------------------------------------------- 1 | from os.path import join as jph 2 | 3 | from numpy.testing import assert_raises 4 | 5 | from nilabels.definitions import root_dir 6 | from nilabels.tools.aux_methods.sanity_checks import check_path_validity, check_pfi_io, is_valid_permutation 7 | from tests.tools.decorators_tools import create_and_erase_temporary_folder_with_a_dummy_nifti_image, pfo_tmp_test 8 | 9 | # TEST: methods sanity_checks 10 | 11 | 12 | def test_check_pfi_io(): 13 | assert check_pfi_io(root_dir, None) 14 | assert check_pfi_io(root_dir, root_dir) 15 | 16 | non_existing_file = jph(root_dir, "non_existing_file.txt") 17 | file_in_non_existing_folder = jph(root_dir, "non_existing_folder/non_existing_file.txt") 18 | 19 | with assert_raises(IOError): 20 | check_pfi_io(non_existing_file, None) 21 | with assert_raises(IOError): 22 | check_pfi_io(root_dir, file_in_non_existing_folder) 23 | 24 | 25 | def test_check_path_validity_not_existing_path(): 26 | with assert_raises(IOError): 27 | check_path_validity("/Spammer/path_to_spam") 28 | 29 | 30 | @create_and_erase_temporary_folder_with_a_dummy_nifti_image 31 | def test_check_path_validity_for_a_nifti_image(): 32 | assert check_path_validity(jph(pfo_tmp_test, "dummy_image.nii.gz")) 33 | 34 | 35 | def test_check_path_validity_root(): 36 | assert check_path_validity(root_dir) 37 | 38 | 39 | def test_is_valid_permutation(): 40 | assert not is_valid_permutation([1, 2, 3]) 41 | assert not is_valid_permutation([[1, 2, 3, 4], [3, 1, 2]]) 42 | assert not is_valid_permutation([[1, 2, 3], [4, 5, 6]]) 43 | assert not is_valid_permutation([[1, 1, 3], [1, 3, 1]]) 44 | assert not is_valid_permutation([[1.2, 2, 3], [2, 1.2, 3]]) 45 | assert is_valid_permutation([[1.2, 2, 3], [2, 1.2, 3]], for_labels=False) 46 | assert is_valid_permutation([[1, 2, 3], [3, 1, 2]]) 47 | 48 | 49 | if __name__ == "__main__": 50 | test_check_pfi_io() 51 | test_check_path_validity_not_existing_path() 52 | test_check_path_validity_for_a_nifti_image() 53 | test_check_path_validity_root() 54 | test_is_valid_permutation() 55 | -------------------------------------------------------------------------------- /tests/tools/test_aux_methods_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os.path import join as jph 3 | 4 | import numpy as np 5 | from numpy.testing import assert_array_equal 6 | 7 | from nilabels.tools.aux_methods.utils import eliminates_consecutive_duplicates, labels_query, lift_list, print_and_run 8 | from tests.tools.decorators_tools import create_and_erase_temporary_folder, test_dir 9 | 10 | # TEST tools.aux_methods.utils.py''' 11 | 12 | 13 | def test_lift_list_1(): 14 | l_in, l_out = [[0, 1], 2, 3, [4, [5, 6]], 7, [8, [9]]], range(10) 15 | assert_array_equal(lift_list(l_in), l_out) 16 | 17 | 18 | def test_lift_list_2(): 19 | l_in, l_out = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], range(10) 20 | assert_array_equal(lift_list(l_in), l_out) 21 | 22 | 23 | def test_lift_list_3(): 24 | l_in, l_out = [], [] 25 | assert_array_equal(lift_list(l_in), l_out) 26 | 27 | 28 | def test_eliminates_consecutive_duplicates(): 29 | l_in, l_out = [0, 0, 0, 1, 1, 2, 3, 4, 5, 5, 5, 6, 7, 8, 9], range(10) 30 | assert_array_equal(eliminates_consecutive_duplicates(l_in), l_out) 31 | 32 | 33 | @create_and_erase_temporary_folder 34 | def test_print_and_run_create_file(): 35 | cmd = "touch {}".format(jph(test_dir, "z_tmp_test", "tmp.txt")) 36 | output_msg = print_and_run(cmd) 37 | assert os.path.exists(jph(test_dir, "z_tmp_test", "tmp.txt")) 38 | assert output_msg == "touch tmp.txt" 39 | 40 | 41 | @create_and_erase_temporary_folder 42 | def test_print_and_run_create_file_safety_on(): 43 | cmd = "touch {}".format(jph(test_dir, "z_tmp_test", "tmp.txt")) 44 | output_msg = print_and_run(cmd, safety_on=True) 45 | assert output_msg == "touch tmp.txt" 46 | 47 | 48 | @create_and_erase_temporary_folder 49 | def test_print_and_run_create_file_safety_off(): 50 | cmd = "touch {}".format(jph(test_dir, "z_tmp_test", "tmp.txt")) 51 | output_msg = print_and_run(cmd, safety_on=False, short_path_output=False) 52 | assert os.path.exists(jph(test_dir, "z_tmp_test", "tmp.txt")) 53 | assert output_msg == "touch {}".format(jph(test_dir, "z_tmp_test", "tmp.txt")) 54 | 55 | 56 | def test_labels_query_int_input(): 57 | lab, lab_names = labels_query(1) 58 | assert_array_equal(lab, [1]) 59 | assert_array_equal(lab_names, ["1"]) 60 | 61 | 62 | def test_labels_query_list_input1(): 63 | lab, lab_names = labels_query([1, 2, 3]) 64 | assert_array_equal(lab, [1, 2, 3]) 65 | assert_array_equal(lab_names, ["1", "2", "3"]) 66 | 67 | 68 | def test_labels_query_list_input2(): 69 | lab, lab_names = labels_query([1, 2, 3, [4, 5, 6]]) 70 | assert_array_equal(lift_list(lab), lift_list([1, 2, 3, [4, 5, 6]])) 71 | assert_array_equal(lab_names, ["1", "2", "3", "[4, 5, 6]"]) 72 | 73 | 74 | def test_labels_query_all_or_tot_input(): 75 | v = np.arange(10).reshape(5, 2) 76 | lab, lab_names = labels_query("all", v, remove_zero=False) 77 | assert_array_equal(lab, np.arange(10)) 78 | lab, lab_names = labels_query("tot", v, remove_zero=False) 79 | assert_array_equal(lab, np.arange(10)) 80 | lab, lab_names = labels_query("tot", v, remove_zero=True) 81 | assert_array_equal(lab, np.arange(10)[1:]) 82 | 83 | 84 | if __name__ == "__main__": 85 | test_lift_list_1() 86 | test_lift_list_2() 87 | test_lift_list_3() 88 | test_eliminates_consecutive_duplicates() 89 | test_print_and_run_create_file() 90 | test_print_and_run_create_file_safety_on() 91 | test_print_and_run_create_file_safety_off() 92 | test_labels_query_int_input() 93 | test_labels_query_list_input1() 94 | test_labels_query_list_input2() 95 | test_labels_query_all_or_tot_input() 96 | -------------------------------------------------------------------------------- /tests/tools/test_aux_methods_utils_path.py: -------------------------------------------------------------------------------- 1 | from os.path import join as jph 2 | 3 | from numpy.testing import assert_array_equal 4 | 5 | from nilabels.definitions import root_dir 6 | from nilabels.tools.aux_methods.utils_path import connect_path_tail_head, get_pfi_in_pfi_out 7 | 8 | # TEST aux_methods.sanity_checks.py - NOTE - this is the core of the manager design ''' 9 | 10 | 11 | def test_connect_tail_head_path(): 12 | # Case 1: 13 | assert connect_path_tail_head("as/df/gh", "lm.txt") == "as/df/gh/lm.txt" 14 | # Case 2: 15 | assert connect_path_tail_head("as/df/gh", "as/df/gh/lm/nb.txt") == "as/df/gh/lm/nb.txt" 16 | # Case 3: 17 | assert connect_path_tail_head("as/df/gh", "lm/nb.txt") == "as/df/gh/lm/nb.txt" 18 | 19 | 20 | def test_get_pfi_in_pfi_out(): 21 | 22 | tail_a = jph(root_dir, "tests") 23 | tail_b = root_dir 24 | head_a = "test_auxiliary_methods.py" 25 | head_b = "head_b.txt" 26 | 27 | assert_array_equal(get_pfi_in_pfi_out(head_a, None, tail_a, None), (jph(tail_a, head_a), jph(tail_a, head_a))) 28 | assert_array_equal(get_pfi_in_pfi_out(head_a, head_b, tail_a, None), (jph(tail_a, head_a), jph(tail_a, head_b))) 29 | assert_array_equal(get_pfi_in_pfi_out(head_a, head_b, tail_a, tail_b), (jph(tail_a, head_a), jph(tail_b, head_b))) 30 | 31 | 32 | if __name__ == "__main__": 33 | test_connect_tail_head_path() 34 | test_get_pfi_in_pfi_out() 35 | -------------------------------------------------------------------------------- /tests/tools/test_detections_check_imperfections.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import nibabel as nib 4 | import numpy as np 5 | 6 | from nilabels.tools.aux_methods.label_descriptor_manager import LabelsDescriptorManager 7 | from nilabels.tools.detections.check_imperfections import check_missing_labels 8 | from tests.tools.decorators_tools import pfo_tmp_test, write_and_erase_temporary_folder_with_dummy_labels_descriptor 9 | 10 | 11 | @write_and_erase_temporary_folder_with_dummy_labels_descriptor 12 | def test_check_missing_labels(): 13 | # Instantiate a labels descriptor manager 14 | pfi_ld = os.path.join(pfo_tmp_test, "labels_descriptor.txt") 15 | ldm = LabelsDescriptorManager(pfi_ld, labels_descriptor_convention="itk-snap") 16 | 17 | # Create dummy image 18 | data = np.zeros([10, 10, 10]) 19 | data[:3, :3, :3] = 1 20 | data[3:5, 3:5, 3:5] = 12 21 | data[5:7, 5:7, 5:7] = 3 22 | data[7:10, 7:10, 7:10] = 7 23 | 24 | im_segm = nib.Nifti1Image(data, affine=np.eye(4)) 25 | 26 | # Apply check_missing_labels, then test the output 27 | pfi_log = os.path.join(pfo_tmp_test, "check_imperfections_log.txt") 28 | in_descriptor_not_delineated, delineated_not_in_descriptor = check_missing_labels(im_segm, ldm, pfi_log) 29 | 30 | np.testing.assert_equal(in_descriptor_not_delineated, {8, 2, 4, 5, 6}) # in label descriptor, not in image 31 | np.testing.assert_equal(delineated_not_in_descriptor, {12}) # in image not in label descriptor 32 | assert os.path.exists(pfi_log) 33 | 34 | 35 | if __name__ == "__main__": 36 | test_check_missing_labels() 37 | -------------------------------------------------------------------------------- /tests/tools/test_detections_contours.py: -------------------------------------------------------------------------------- 1 | import nibabel as nib 2 | import numpy as np 3 | 4 | from nilabels.tools.detections.contours import ( 5 | contour_from_array_at_label, 6 | contour_from_segmentation, 7 | get_internal_contour_with_erosion_at_label, 8 | get_xyz_borders_of_a_label, 9 | ) 10 | 11 | 12 | def test_contour_from_array_at_label_empty_image(): 13 | arr = np.zeros([50, 50, 50]) 14 | arr_contour = contour_from_array_at_label(arr, 1) 15 | 16 | np.testing.assert_array_equal(arr_contour, np.zeros_like(arr_contour)) 17 | 18 | 19 | def test_contour_from_array_at_label_simple_border(): 20 | arr = np.zeros([50, 50, 50]) 21 | arr[:, :, 25:] = 1 22 | 23 | arr_contour = contour_from_array_at_label(arr, 1) 24 | 25 | im_expected = np.zeros([50, 50, 50]) 26 | im_expected[:, :, 24:26] = 1 27 | 28 | np.testing.assert_array_equal(arr_contour, im_expected) 29 | 30 | 31 | def test_contour_from_array_at_label_simple_border_omit_axis_x(): 32 | arr = np.zeros([50, 50, 50]) 33 | arr[:, :, 25:] = 1 34 | 35 | arr_contour = contour_from_array_at_label(arr, 1, omit_axis="x") 36 | 37 | im_expected = np.zeros([50, 50, 50]) 38 | im_expected[:, :, 24:26] = 1 39 | 40 | np.testing.assert_array_equal(arr_contour, im_expected) 41 | 42 | 43 | def test_contour_from_array_at_label_simple_border_omit_axis_y(): 44 | arr = np.zeros([50, 50, 50]) 45 | arr[:, :, 25:] = 1 46 | 47 | arr_contour = contour_from_array_at_label(arr, 1, omit_axis="y") 48 | 49 | im_expected = np.zeros([50, 50, 50]) 50 | im_expected[:, :, 24:26] = 1 51 | 52 | np.testing.assert_array_equal(arr_contour, im_expected) 53 | 54 | 55 | def test_contour_from_array_at_label_simple_border_omit_axis_z(): 56 | arr = np.zeros([50, 50, 50]) 57 | arr[:, :, 25:] = 1 58 | 59 | arr_contour = contour_from_array_at_label(arr, 1, omit_axis="z") 60 | 61 | np.testing.assert_array_equal(arr_contour, np.zeros_like(arr_contour)) 62 | 63 | 64 | def test_contour_from_array_at_label_error(): 65 | arr = np.zeros([50, 50, 50]) 66 | 67 | with np.testing.assert_raises(IOError): 68 | contour_from_array_at_label(arr, 1, omit_axis="spam") 69 | 70 | 71 | def test_contour_from_segmentation(): 72 | im_data = np.zeros([50, 50, 50]) 73 | im_data[:, :, 20:] = 1 74 | im_data[:, :, 30:] = 2 75 | 76 | im = nib.Nifti1Image(im_data, affine=np.eye(4)) 77 | 78 | im_contour = contour_from_segmentation(im, omit_axis=None, verbose=1) 79 | 80 | im_data_expected = np.zeros([50, 50, 50]) 81 | im_data_expected[:, :, 20:21] = 1 82 | 83 | im_data_expected[:, :, 29:30] = 1 84 | im_data_expected[:, :, 30:31] = 2 85 | np.testing.assert_array_equal(im_contour.get_fdata(), im_data_expected) 86 | 87 | 88 | def test_get_xyz_borders_of_a_label(): 89 | arr = np.zeros([10, 10, 10]) 90 | 91 | arr[1, 1, 1] = 1 92 | arr[3, 3, 2] = 1 93 | out_coords = get_xyz_borders_of_a_label(arr, 1) 94 | np.testing.assert_equal(out_coords, [1, 3, 1, 3, 1, 2]) 95 | 96 | 97 | def test_get_xyz_borders_of_a_label_no_labels_found(): 98 | arr = np.zeros([10, 10, 10]) 99 | out_coords = get_xyz_borders_of_a_label(arr, 3) 100 | np.testing.assert_equal(out_coords is None, True) 101 | 102 | 103 | def test_get_internal_contour_with_erosion_at_label(): 104 | arr = np.zeros([10, 10, 10]) 105 | arr[2:-2, 2:-2, 2:-2] = 1 106 | 107 | expected_output = np.zeros([10, 10, 10]) 108 | expected_output[2:-2, 2:-2, 2:-2] = 1 109 | expected_output[3:-3, 3:-3, 3:-3] = 0 110 | 111 | im_cont = get_internal_contour_with_erosion_at_label(arr, 1) 112 | 113 | np.testing.assert_array_equal(im_cont, expected_output) 114 | 115 | 116 | if __name__ == "__main__": 117 | test_contour_from_array_at_label_empty_image() 118 | 119 | test_contour_from_array_at_label_simple_border() 120 | test_contour_from_array_at_label_simple_border_omit_axis_x() 121 | test_contour_from_array_at_label_simple_border_omit_axis_y() 122 | test_contour_from_array_at_label_simple_border_omit_axis_z() 123 | 124 | test_contour_from_array_at_label_error() 125 | 126 | test_contour_from_segmentation() 127 | 128 | test_get_xyz_borders_of_a_label() 129 | test_get_xyz_borders_of_a_label_no_labels_found() 130 | test_get_internal_contour_with_erosion_at_label() 131 | -------------------------------------------------------------------------------- /tests/tools/test_detections_get_segmentation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from nilabels.tools.detections.get_segmentation import MoG_array, intensity_segmentation, otsu_threshold 4 | 5 | # ----- Test get segmentation ---- 6 | 7 | 8 | def test_intensity_segmentation_1(): 9 | im_array = np.random.randint(0, 5, [10, 10], np.uint8) 10 | output_segm = intensity_segmentation(im_array) 11 | # if the input is a segmentation with 5 labels, the segmentation is the input. 12 | np.testing.assert_array_equal(im_array, output_segm) 13 | 14 | 15 | def test_intensity_segmentation_2(): 16 | seed_segm = np.array([0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5]) 17 | seed_image = np.linspace(0, 5, len(seed_segm)) 18 | 19 | segm = np.stack([seed_segm] * 6) 20 | image = np.stack([seed_image] * 6) 21 | 22 | output_segm = intensity_segmentation(image, num_levels=6) 23 | np.testing.assert_array_equal(segm, output_segm) 24 | 25 | segm_transposed = segm.T 26 | image_transposed = image.T 27 | 28 | output_segm_transposed = intensity_segmentation(image_transposed, num_levels=6) 29 | np.testing.assert_array_equal(segm_transposed, output_segm_transposed) 30 | 31 | 32 | def test_otsu_threshold_bad_input(): 33 | with np.testing.assert_raises(IOError): 34 | otsu_threshold(np.random.rand(40, 40), side="spam") 35 | 36 | 37 | def test_otsu_threshold_side_above(): 38 | arr = np.zeros([20, 20]) 39 | arr[:10, :] = 1 40 | arr[10:, :] = 2 41 | arr_thr = otsu_threshold(arr, side="above", return_as_mask=False) 42 | 43 | expected_arr_thr = np.zeros([20, 20]) 44 | expected_arr_thr[10:, :] = 2 45 | 46 | np.testing.assert_array_equal(arr_thr, expected_arr_thr) 47 | 48 | 49 | def test_otsu_threshold_side_below(): 50 | arr = np.zeros([20, 20]) 51 | arr[:10, :] = 1 52 | arr[10:, :] = 2 53 | arr_thr = otsu_threshold(arr, side="below", return_as_mask=False) 54 | 55 | expected_arr_thr = np.zeros([20, 20]) 56 | expected_arr_thr[:10, :] = 1 57 | 58 | np.testing.assert_array_equal(arr_thr, expected_arr_thr) 59 | 60 | 61 | def test_otsu_threshold_as_mask(): 62 | arr = np.zeros([20, 20]) 63 | arr[:10, :] = 1 64 | arr[10:, :] = 2 65 | arr_thr = otsu_threshold(arr, side="above", return_as_mask=True) 66 | 67 | expected_arr_thr = np.zeros([20, 20]) 68 | expected_arr_thr[10:, :] = 1 69 | 70 | np.testing.assert_array_equal(arr_thr, expected_arr_thr) 71 | 72 | 73 | def test_mog_array_1(): 74 | arr = np.zeros([20, 20, 20]) 75 | arr[:10, ...] = 1 76 | arr[10:, ...] = 2 77 | crisp, prob = MoG_array(arr, K=2) 78 | 79 | expected_crisp = np.zeros([20, 20, 20]) 80 | expected_crisp[:10, ...] = 0 81 | expected_crisp[10:, ...] = 1 82 | 83 | expected_prob = np.zeros([20, 20, 20, 2]) 84 | expected_prob[:10, ..., 0] = 1 85 | expected_prob[10:, ..., 1] = 1 86 | 87 | np.testing.assert_array_equal(crisp, expected_crisp) 88 | np.testing.assert_array_equal(prob, expected_prob) 89 | 90 | 91 | if __name__ == "__main__": 92 | test_intensity_segmentation_1() 93 | test_intensity_segmentation_2() 94 | 95 | test_otsu_threshold_bad_input() 96 | test_otsu_threshold_side_above() 97 | test_otsu_threshold_side_below() 98 | test_otsu_threshold_as_mask() 99 | 100 | test_mog_array_1() 101 | -------------------------------------------------------------------------------- /tests/tools/test_detections_island_detection.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy.testing import assert_array_equal 3 | 4 | from nilabels.tools.detections.island_detection import island_for_label 5 | 6 | 7 | def test_island_for_label_ok_input(): 8 | in_data = np.array( 9 | [ 10 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 11 | [0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], 12 | [0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0], 13 | [0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0], 14 | [0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0], 15 | [0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0], 16 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 17 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 18 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 19 | ], 20 | ) 21 | 22 | expected_ans_false = [ 23 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 24 | [0, 3, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0], 25 | [0, 3, 0, 0, 1, 1, 0, 0, 2, 0, 0, 0], 26 | [0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0], 27 | [0, 5, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0], 28 | [0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0], 29 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 30 | [0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 31 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 32 | ] 33 | 34 | expected_ans_true = [ 35 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 36 | [0, -1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0], 37 | [0, -1, 0, 0, 1, 1, 0, 0, -1, 0, 0, 0], 38 | [0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0], 39 | [0, -1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0], 40 | [0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0], 41 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 42 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 43 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 44 | ] 45 | 46 | ans_false = island_for_label(in_data, 1) 47 | 48 | ans_true = island_for_label(in_data, 1, m=1) 49 | 50 | assert_array_equal(expected_ans_false, ans_false) 51 | assert_array_equal(expected_ans_true, ans_true) 52 | 53 | 54 | def test_island_for_label_no_label_in_input(): 55 | in_data = np.array( 56 | [ 57 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 58 | [0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], 59 | [0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0], 60 | [0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0], 61 | [0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0], 62 | [0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0], 63 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 64 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 65 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 66 | ], 67 | ) 68 | 69 | bypassed_ans = island_for_label(in_data, 2) 70 | assert_array_equal(bypassed_ans, in_data) 71 | 72 | 73 | def test_island_for_label_multiple_components_for_more_than_one_m(): 74 | in_data = np.array( 75 | [ 76 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], 77 | [0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], 78 | [0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1], 79 | [0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1], 80 | [0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1], 81 | [0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1], 82 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 83 | [0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0], 84 | [0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0], 85 | ], 86 | ) 87 | 88 | expected_output = np.array( 89 | [ 90 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0], 91 | [0, 2, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0], 92 | [0, 2, 0, 0, 1, 1, 0, 0, -1, 0, 0, 3], 93 | [0, 2, 0, 1, 1, 1, 0, 0, 0, 0, 0, 3], 94 | [0, 2, 0, 1, 0, 1, 0, 0, 0, 0, 0, 3], 95 | [0, 2, 0, 1, 1, 1, 1, 0, 0, 0, 0, 3], 96 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 97 | [0, -1, 0, 0, 0, 0, 0, 0, -1, -1, -1, 0], 98 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 99 | ], 100 | ) 101 | 102 | ans = island_for_label(in_data, 1, m=3, special_label=-1) 103 | 104 | assert_array_equal(expected_output, ans) 105 | 106 | 107 | def test_island_for_label_multiple_components_for_more_than_one_m_again(): 108 | in_data = np.array( 109 | [ 110 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], 111 | [0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], 112 | [0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1], 113 | [0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1], 114 | [0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1], 115 | [0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1], 116 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 117 | [0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0], 118 | [0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0], 119 | ], 120 | ) 121 | 122 | expected_output = np.array( 123 | [ 124 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0], 125 | [0, 2, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0], 126 | [0, 2, 0, 0, 1, 1, 0, 0, -1, 0, 0, 3], 127 | [0, 2, 0, 1, 1, 1, 0, 0, 0, 0, 0, 3], 128 | [0, 2, 0, 1, 0, 1, 0, 0, 0, 0, 0, 3], 129 | [0, 2, 0, 1, 1, 1, 1, 0, 0, 0, 0, 3], 130 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 131 | [0, -1, 0, 0, 0, 0, 0, 0, 4, 4, 4, 0], 132 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 133 | ], 134 | ) 135 | 136 | ans = island_for_label(in_data, 1, m=4, special_label=-1) 137 | 138 | assert_array_equal(expected_output, ans) 139 | -------------------------------------------------------------------------------- /tests/tools/test_image_colors_manip_cutter.py: -------------------------------------------------------------------------------- 1 | import nibabel as nib 2 | import numpy as np 3 | from numpy.testing import assert_array_equal 4 | 5 | from nilabels.tools.image_colors_manipulations.cutter import ( 6 | apply_a_mask_nib, 7 | cut_4d_volume_with_a_1_slice_mask, 8 | cut_4d_volume_with_a_1_slice_mask_nib, 9 | ) 10 | 11 | 12 | def test_cut_4d_volume_with_a_1_slice_mask(): 13 | data_0 = np.stack([np.array(range(5 * 5 * 5)).reshape(5, 5, 5)] * 4, axis=3) 14 | mask = np.zeros([5, 5, 5]) 15 | for k in range(5): 16 | mask[k, k, k] = 1 17 | expected_answer_for_each_slice = np.zeros([5, 5, 5]) 18 | for k in range(5): 19 | expected_answer_for_each_slice[k, k, k] = 30 * k + k 20 | ans = cut_4d_volume_with_a_1_slice_mask(data_0, mask) 21 | 22 | for k in range(4): 23 | assert_array_equal(ans[..., k], expected_answer_for_each_slice) 24 | 25 | 26 | def test_cut_4d_volume_with_a_1_slice_mask_if_input_3d(): 27 | data_0 = np.array(range(5 * 5 * 5)).reshape(5, 5, 5) 28 | mask = np.zeros([5, 5, 5]) 29 | mask[:3, :3, :] = 1 30 | 31 | ans = cut_4d_volume_with_a_1_slice_mask(data_0, mask) 32 | 33 | assert_array_equal(ans, mask * data_0) 34 | 35 | 36 | def test_cut_4d_volume_with_a_1_slice_mask_nib(): 37 | data_0 = np.stack([np.array(range(5 * 5 * 5)).reshape(5, 5, 5)] * 4, axis=3) 38 | mask = np.zeros([5, 5, 5]) 39 | for k in range(5): 40 | mask[k, k, k] = 1 41 | expected_answer_for_each_slice = np.zeros([5, 5, 5]) 42 | for k in range(5): 43 | expected_answer_for_each_slice[k, k, k] = 30 * k + k 44 | 45 | im_data0 = nib.Nifti1Image(data_0, affine=np.eye(4), dtype=np.int64) 46 | im_mask = nib.Nifti1Image(mask, affine=np.eye(4), dtype=np.int64) 47 | 48 | im_ans = cut_4d_volume_with_a_1_slice_mask_nib(im_data0, im_mask) 49 | 50 | for k in range(4): 51 | assert_array_equal(im_ans.get_fdata()[..., k], expected_answer_for_each_slice) 52 | 53 | 54 | def test_apply_a_mask_nib_wrong_input(): 55 | data_0 = np.array(range(5 * 5 * 5)).reshape(5, 5, 5) 56 | mask = np.zeros([5, 5, 4]) 57 | mask[:3, :3, :] = 1 58 | 59 | im_data0 = nib.Nifti1Image(data_0, affine=np.eye(4), dtype=np.int64) 60 | im_mask = nib.Nifti1Image(mask, affine=np.eye(4), dtype=np.int64) 61 | with np.testing.assert_raises(IOError): 62 | apply_a_mask_nib(im_data0, im_mask) 63 | 64 | 65 | def test_apply_a_mask_nib_ok_input(): 66 | data_0 = np.array(range(5 * 5 * 5)).reshape(5, 5, 5) 67 | mask = np.zeros([5, 5, 5]) 68 | mask[:3, :3, :] = 1 69 | 70 | expected_data = data_0 * mask 71 | 72 | im_data0 = nib.Nifti1Image(data_0, affine=np.eye(4), dtype=np.int64) 73 | im_mask = nib.Nifti1Image(mask, affine=np.eye(4), dtype=np.int64) 74 | 75 | im_masked = apply_a_mask_nib(im_data0, im_mask) 76 | 77 | np.testing.assert_array_equal(im_masked.get_fdata(), expected_data) 78 | 79 | 80 | def test_apply_a_mask_nib_4d_input(): 81 | data_0 = np.stack([np.array(range(5 * 5 * 5)).reshape(5, 5, 5)] * 4, axis=3) 82 | mask = np.zeros([5, 5, 5]) 83 | for k in range(5): 84 | mask[k, k, k] = 1 85 | expected_answer_for_each_slice = np.zeros([5, 5, 5]) 86 | for k in range(5): 87 | expected_answer_for_each_slice[k, k, k] = 30 * k + k 88 | 89 | im_data0 = nib.Nifti1Image(data_0, affine=np.eye(4), dtype=np.int64) 90 | im_mask = nib.Nifti1Image(mask, affine=np.eye(4), dtype=np.int64) 91 | 92 | im_ans = apply_a_mask_nib(im_data0, im_mask) 93 | 94 | for k in range(4): 95 | assert_array_equal(im_ans.get_fdata()[..., k], expected_answer_for_each_slice) 96 | 97 | 98 | if __name__ == "__main__": 99 | test_cut_4d_volume_with_a_1_slice_mask() 100 | test_cut_4d_volume_with_a_1_slice_mask_if_input_3d() 101 | 102 | test_cut_4d_volume_with_a_1_slice_mask_nib() 103 | 104 | test_apply_a_mask_nib_wrong_input() 105 | test_apply_a_mask_nib_ok_input() 106 | test_apply_a_mask_nib_4d_input() 107 | -------------------------------------------------------------------------------- /tests/tools/test_image_colors_manip_normaliser.py: -------------------------------------------------------------------------------- 1 | import nibabel as nib 2 | import numpy as np 3 | 4 | from nilabels.tools.image_colors_manipulations.normaliser import ( 5 | intensities_normalisation_linear, 6 | mahalanobis_distance_map, 7 | normalise_below_labels, 8 | ) 9 | 10 | 11 | def test_normalise_below_labels(): 12 | arr_data = np.ones([20, 21, 22]) 13 | arr_segm = np.zeros([20, 21, 22]) 14 | 15 | arr_data[5:10, 1, 1] = np.array([1, 2, 3, 4, 5]) 16 | 17 | arr_segm[5:10, 1, 1] = np.ones([5]) 18 | factor = np.median(np.array([1, 2, 3, 4, 5])) 19 | 20 | im_data = nib.Nifti1Image(arr_data, affine=np.eye(4)) 21 | im_segm = nib.Nifti1Image(arr_segm, affine=np.eye(4)) 22 | 23 | expected_array_normalised = arr_data / factor 24 | 25 | im_normalised_below = normalise_below_labels(im_data, im_segm) 26 | 27 | np.testing.assert_array_almost_equal(im_normalised_below.get_fdata(), expected_array_normalised) 28 | 29 | 30 | def test_normalise_below_labels_specified_list(): 31 | arr_data = np.ones([20, 21, 22]) 32 | arr_segm = np.zeros([20, 21, 22]) 33 | 34 | arr_data[5:10, 1, 1] = np.array([1, 2, 3, 4, 5]) 35 | arr_data[5:10, 2, 1] = np.array([3, 6, 9, 12, 15]) 36 | 37 | arr_segm[5:10, 1, 1] = np.ones([5]) 38 | arr_segm[5:10, 2, 1] = 2 * np.ones([5]) 39 | 40 | factor_1 = np.median(np.array([1, 2, 3, 4, 5])) 41 | factor_2 = np.median(np.array([3, 6, 9, 12, 15])) 42 | factor_1_2 = np.median(np.array([1, 2, 3, 4, 5, 3, 6, 9, 12, 15])) 43 | 44 | im_data = nib.Nifti1Image(arr_data, affine=np.eye(4)) 45 | im_segm = nib.Nifti1Image(arr_segm, affine=np.eye(4)) 46 | 47 | # No labels indicated: 48 | expected_array_normalised = arr_data / factor_1_2 49 | im_normalised_below = normalise_below_labels(im_data, im_segm, labels_list=None, exclude_first_label=False) 50 | np.testing.assert_array_almost_equal(im_normalised_below.get_fdata(), expected_array_normalised) 51 | 52 | # asking only for label 2 53 | expected_array_normalised = arr_data / factor_2 54 | im_normalised_below = normalise_below_labels(im_data, im_segm, labels_list=[2], exclude_first_label=False) 55 | np.testing.assert_array_almost_equal(im_normalised_below.get_fdata(), expected_array_normalised) 56 | 57 | # asking only for label 1 58 | expected_array_normalised = arr_data / factor_1 59 | im_normalised_below = normalise_below_labels(im_data, im_segm, labels_list=[1], exclude_first_label=False) 60 | np.testing.assert_array_almost_equal(im_normalised_below.get_fdata(), expected_array_normalised) 61 | 62 | 63 | def test_normalise_below_labels_specified_list_exclude_first() -> None: 64 | arr_data = np.ones([20, 21, 22]) 65 | arr_segm = np.zeros([20, 21, 22]) 66 | 67 | arr_data[5:10, 1, 1] = np.array([1, 2, 3, 4, 5]) 68 | arr_data[5:10, 2, 1] = np.array([3, 6, 9, 12, 15]) 69 | 70 | arr_segm[5:10, 1, 1] = np.ones([5]) 71 | arr_segm[5:10, 2, 1] = 2 * np.ones([5]) 72 | 73 | factor_2 = np.median(np.array([3, 6, 9, 12, 15])) 74 | 75 | im_data = nib.Nifti1Image(arr_data, affine=np.eye(4)) 76 | im_segm = nib.Nifti1Image(arr_segm, affine=np.eye(4)) 77 | 78 | expected_array_normalised = arr_data / factor_2 79 | im_normalised_below = normalise_below_labels(im_data, im_segm, labels_list=[1, 2], exclude_first_label=True) 80 | np.testing.assert_array_almost_equal(im_normalised_below.get_fdata(), expected_array_normalised) 81 | 82 | 83 | def test_intensities_normalisation(): 84 | arr_data = np.zeros([20, 20, 20]) 85 | arr_segm = np.zeros([20, 20, 20]) 86 | 87 | arr_data[:5, :5, :5] = 2 88 | arr_data[5:10, 5:10, 5:10] = 4 89 | arr_data[10:15, 10:15, 10:15] = 6 90 | arr_data[15:, 15:, 15:] = 8 91 | 92 | arr_segm[arr_data > 1] = 1 93 | 94 | im_data = nib.Nifti1Image(arr_data, affine=np.eye(4)) 95 | im_segm = nib.Nifti1Image(arr_segm, affine=np.eye(4)) 96 | 97 | im_normalised = intensities_normalisation_linear(im_data, im_segm, im_mask_foreground=im_segm) 98 | np.testing.assert_almost_equal(np.min(im_normalised.get_fdata()), 0.0) 99 | np.testing.assert_almost_equal(np.max(im_normalised.get_fdata()), 10.0) 100 | 101 | im_normalised = intensities_normalisation_linear(im_data, im_segm) 102 | np.testing.assert_almost_equal(np.min(im_normalised.get_fdata()), -3.2) 103 | np.testing.assert_almost_equal(np.max(im_normalised.get_fdata()), 10.0) 104 | 105 | 106 | def test_mahalanobis_distance_map(): 107 | data = np.zeros([10, 10, 10]) 108 | im = nib.Nifti1Image(data, affine=np.eye(4)) 109 | md_im = mahalanobis_distance_map(im) 110 | np.testing.assert_array_equal(md_im.get_fdata(), np.zeros_like(md_im.get_fdata())) 111 | 112 | data = np.ones([10, 10, 10]) 113 | im = nib.Nifti1Image(data, affine=np.eye(4)) 114 | md_im = mahalanobis_distance_map(im) 115 | np.testing.assert_array_equal(md_im.get_fdata(), np.zeros_like(md_im.get_fdata())) 116 | 117 | 118 | def test_mahalanobis_distance_map_with_mask(): 119 | data = np.random.randn(10, 10, 10) 120 | mask = np.zeros_like(data) 121 | mask[2:-2, 2:-2, 2:-2] = 1 122 | 123 | mu = np.mean(data.flatten() * mask.flatten()) 124 | sigma2 = np.std(data.flatten() * mask.flatten()) 125 | mn_data = np.sqrt((data - mu) * sigma2 ** (-1) * (data - mu)) 126 | 127 | im = nib.Nifti1Image(data, affine=np.eye(4)) 128 | im_mask = nib.Nifti1Image(mask, affine=np.eye(4)) 129 | 130 | md_im = mahalanobis_distance_map(im, im_mask) 131 | np.testing.assert_array_equal(md_im.get_fdata(), mn_data) 132 | 133 | mn_data_trimmed = mn_data * mask.astype(bool) 134 | md_im = mahalanobis_distance_map(im, im_mask, trim=True) 135 | np.testing.assert_array_equal(md_im.get_fdata(), mn_data_trimmed) 136 | 137 | 138 | if __name__ == "__main__": 139 | test_normalise_below_labels() 140 | test_normalise_below_labels_specified_list() 141 | test_normalise_below_labels_specified_list_exclude_first() 142 | 143 | test_intensities_normalisation() 144 | 145 | test_mahalanobis_distance_map() 146 | test_mahalanobis_distance_map_with_mask() 147 | -------------------------------------------------------------------------------- /tests/tools/test_image_colors_manip_relabeller.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from nilabels.tools.image_colors_manipulations.relabeller import ( 4 | assign_all_other_labels_the_same_value, 5 | erase_labels, 6 | keep_only_one_label, 7 | permute_labels, 8 | relabel_half_side_one_label, 9 | relabeller, 10 | ) 11 | 12 | 13 | def test_relabeller_basic(): 14 | data = np.array(range(10)).reshape(2, 5) 15 | relabelled_data = relabeller(data, range(10), range(10)[::-1]) 16 | np.testing.assert_array_equal(relabelled_data, np.array(range(10)[::-1]).reshape(2, 5)) 17 | 18 | 19 | def test_relabeller_one_element(): 20 | data = np.array(range(10)).reshape(2, 5) 21 | relabelled_data = relabeller(data, 0, 1, verbose=1) 22 | expected_output = data[:] 23 | expected_output[0, 0] = 1 24 | np.testing.assert_array_equal(relabelled_data, expected_output) 25 | 26 | 27 | def test_relabeller_one_element_not_in_array(): 28 | data = np.array(range(10)).reshape(2, 5) 29 | relabelled_data = relabeller(data, 15, 1, verbose=1) 30 | np.testing.assert_array_equal(relabelled_data, data) 31 | 32 | 33 | def test_relabeller_wrong_input(): 34 | data = np.array(range(10)).reshape(2, 5) 35 | with np.testing.assert_raises(IOError): 36 | relabeller(data, [1, 2], [3, 4, 4]) 37 | 38 | 39 | def test_permute_labels_invalid_permutation(): 40 | invalid_permutation = [[3, 3, 3], [1, 1, 1]] 41 | with np.testing.assert_raises(IOError): 42 | permute_labels(np.zeros([3, 3]), invalid_permutation) 43 | 44 | 45 | def test_permute_labels_valid_permutation(): 46 | data = np.array([[1, 2, 3], [1, 2, 3], [1, 2, 3]]) 47 | valid_permutation = [[1, 2, 3], [1, 3, 2]] 48 | perm_data = permute_labels(data, valid_permutation) 49 | expected_data = np.array([[1, 3, 2], [1, 3, 2], [1, 3, 2]]) 50 | np.testing.assert_equal(perm_data, expected_data) 51 | 52 | 53 | def test_erase_label_simple(): 54 | data = np.array(range(10)).reshape(2, 5) 55 | data_erased_1 = erase_labels(data, 1) 56 | expected_output = data[:] 57 | expected_output[0, 1] = 0 58 | np.testing.assert_array_equal(data_erased_1, expected_output) 59 | 60 | 61 | def test_assign_all_other_labels_the_same_values_simple(): 62 | data = np.array(range(10)).reshape(2, 5) 63 | data_erased_1 = erase_labels(data, 1) 64 | data_labels_to_keep = assign_all_other_labels_the_same_value(data, range(2, 10), same_value_label=0) 65 | np.testing.assert_array_equal(data_erased_1, data_labels_to_keep) 66 | 67 | 68 | def test_assign_all_other_labels_the_same_values_single_value(): 69 | data = np.array(range(10)).reshape(2, 5) 70 | data_erased_1 = np.zeros_like(data) 71 | data_erased_1[0, 1] = 1 72 | data_labels_to_keep = assign_all_other_labels_the_same_value(data, 1, same_value_label=0) 73 | np.testing.assert_array_equal(data_erased_1, data_labels_to_keep) 74 | 75 | 76 | def test_keep_only_one_label_label_simple(): 77 | data = np.array(range(10)).reshape(2, 5) 78 | new_data = keep_only_one_label(data, 1) 79 | expected_data = np.zeros([2, 5]) 80 | expected_data[0, 1] = 1 81 | np.testing.assert_array_equal(new_data, expected_data) 82 | 83 | 84 | def test_keep_only_one_label_label_not_present(): 85 | data = np.array(range(10)).reshape(2, 5) 86 | new_data = keep_only_one_label(data, 120) 87 | np.testing.assert_array_equal(new_data, data) 88 | 89 | 90 | def test_relabel_half_side_one_label_wrong_input_shape(): 91 | data = np.array(range(10)).reshape(2, 5) 92 | with np.testing.assert_raises(IOError): 93 | relabel_half_side_one_label( 94 | data, 95 | label_old=[1, 2], 96 | label_new=[2, 1], 97 | side_to_modify="above", 98 | axis="x", 99 | plane_intercept=2, 100 | ) 101 | 102 | 103 | def test_relabel_half_side_one_label_wrong_input_side(): 104 | data = np.array(range(27)).reshape(3, 3, 3) 105 | with np.testing.assert_raises(IOError): 106 | relabel_half_side_one_label( 107 | data, 108 | label_old=[1, 2], 109 | label_new=[2, 1], 110 | side_to_modify="spam", 111 | axis="x", 112 | plane_intercept=2, 113 | ) 114 | 115 | 116 | def test_relabel_half_side_one_label_wrong_input_axis(): 117 | data = np.array(range(27)).reshape(3, 3, 3) 118 | with np.testing.assert_raises(IOError): 119 | relabel_half_side_one_label( 120 | data, 121 | label_old=[1, 2], 122 | label_new=[2, 1], 123 | side_to_modify="above", 124 | axis="spam", 125 | plane_intercept=2, 126 | ) 127 | 128 | 129 | def test_relabel_half_side_one_label_wrong_input_simple(): 130 | data = np.array(range(3**3)).reshape(3, 3, 3) 131 | # Z above 132 | new_data = relabel_half_side_one_label( 133 | data, 134 | label_old=1, 135 | label_new=100, 136 | side_to_modify="above", 137 | axis="z", 138 | plane_intercept=1, 139 | ) 140 | expected_data = data[:] 141 | expected_data[0, 0, 1] = 100 142 | 143 | np.testing.assert_array_equal(new_data, expected_data) 144 | 145 | # Z below 146 | new_data = relabel_half_side_one_label( 147 | data, 148 | label_old=3, 149 | label_new=300, 150 | side_to_modify="below", 151 | axis="z", 152 | plane_intercept=2, 153 | ) 154 | expected_data = data[:] 155 | expected_data[0, 1, 0] = 300 156 | 157 | np.testing.assert_array_equal(new_data, expected_data) 158 | 159 | # Y above 160 | new_data = relabel_half_side_one_label( 161 | data, 162 | label_old=8, 163 | label_new=800, 164 | side_to_modify="above", 165 | axis="y", 166 | plane_intercept=1, 167 | ) 168 | expected_data = data[:] 169 | expected_data[0, 2, 2] = 800 170 | 171 | np.testing.assert_array_equal(new_data, expected_data) 172 | 173 | # Y below 174 | new_data = relabel_half_side_one_label( 175 | data, 176 | label_old=6, 177 | label_new=600, 178 | side_to_modify="below", 179 | axis="y", 180 | plane_intercept=2, 181 | ) 182 | expected_data = data[:] 183 | expected_data[0, 2, 0] = 600 184 | np.testing.assert_array_equal(new_data, expected_data) 185 | 186 | # X above 187 | new_data = relabel_half_side_one_label( 188 | data, 189 | label_old=18, 190 | label_new=180, 191 | side_to_modify="above", 192 | axis="x", 193 | plane_intercept=1, 194 | ) 195 | expected_data = data[:] 196 | expected_data[2, 0, 0] = 180 197 | np.testing.assert_array_equal(new_data, expected_data) 198 | 199 | # X below 200 | new_data = relabel_half_side_one_label( 201 | data, 202 | label_old=4, 203 | label_new=400, 204 | side_to_modify="below", 205 | axis="x", 206 | plane_intercept=2, 207 | ) 208 | expected_data = data[:] 209 | expected_data[0, 1, 1] = 400 210 | np.testing.assert_array_equal(new_data, expected_data) 211 | 212 | 213 | if __name__ == "__main__": 214 | test_relabeller_basic() 215 | test_relabeller_one_element() 216 | test_relabeller_one_element_not_in_array() 217 | test_relabeller_wrong_input() 218 | 219 | test_permute_labels_invalid_permutation() 220 | test_permute_labels_valid_permutation() 221 | 222 | test_erase_label_simple() 223 | 224 | test_assign_all_other_labels_the_same_values_simple() 225 | test_assign_all_other_labels_the_same_values_single_value() 226 | 227 | test_keep_only_one_label_label_simple() 228 | test_keep_only_one_label_label_not_present() 229 | 230 | test_relabel_half_side_one_label_wrong_input_shape() 231 | test_relabel_half_side_one_label_wrong_input_side() 232 | test_relabel_half_side_one_label_wrong_input_axis() 233 | 234 | test_relabel_half_side_one_label_wrong_input_simple() 235 | -------------------------------------------------------------------------------- /tests/tools/test_image_colors_manip_segm_to_rgb.py: -------------------------------------------------------------------------------- 1 | from os.path import join as jph 2 | 3 | import nibabel as nib 4 | import numpy as np 5 | 6 | from nilabels.tools.aux_methods.label_descriptor_manager import LabelsDescriptorManager 7 | from nilabels.tools.image_colors_manipulations.segmentation_to_rgb import ( 8 | get_rgb_image_from_segmentation_and_label_descriptor, 9 | ) 10 | from tests.tools.decorators_tools import pfo_tmp_test, write_and_erase_temporary_folder_with_dummy_labels_descriptor 11 | 12 | 13 | @write_and_erase_temporary_folder_with_dummy_labels_descriptor 14 | def test_get_rgb_image_from_segmentation_and_label_descriptor_simple(): 15 | ldm = LabelsDescriptorManager(jph(pfo_tmp_test, "labels_descriptor.txt")) 16 | 17 | segm_data = np.zeros([20, 20, 20]) # block diagonal dummy segmentation 18 | segm_data[:5, :5, :5] = 1 19 | segm_data[5:10, 5:10, 5:10] = 2 20 | segm_data[10:15, 10:15, 10:15] = 3 21 | segm_data[15:, 15:, 15:] = 4 22 | 23 | im_segm = nib.Nifti1Image(segm_data, affine=np.eye(4)) 24 | 25 | im_segm_rgb = get_rgb_image_from_segmentation_and_label_descriptor(im_segm, ldm) 26 | 27 | segm_rgb_expected = np.zeros([20, 20, 20, 3]) 28 | segm_rgb_expected[:5, :5, :5, :] = np.array([255, 0, 0]) 29 | segm_rgb_expected[5:10, 5:10, 5:10, :] = np.array([204, 0, 0]) 30 | segm_rgb_expected[10:15, 10:15, 10:15, :] = np.array([51, 51, 255]) 31 | segm_rgb_expected[15:, 15:, 15:, :] = np.array([102, 102, 255]) 32 | 33 | np.testing.assert_equal(im_segm_rgb.get_fdata(), segm_rgb_expected) 34 | 35 | 36 | @write_and_erase_temporary_folder_with_dummy_labels_descriptor 37 | def test_get_rgb_image_from_segmentation_and_label_descriptor_simple_invert_bl_wh(): 38 | ldm = LabelsDescriptorManager(jph(pfo_tmp_test, "labels_descriptor.txt")) 39 | 40 | segm_data = np.zeros([20, 20, 20]) # block diagonal dummy segmentation 41 | segm_data[:5, :5, :5] = 1 42 | segm_data[5:10, 5:10, 5:10] = 2 43 | segm_data[10:15, 10:15, 10:15] = 3 44 | segm_data[15:, 15:, 15:] = 4 45 | 46 | im_segm = nib.Nifti1Image(segm_data, affine=np.eye(4)) 47 | 48 | im_segm_rgb = get_rgb_image_from_segmentation_and_label_descriptor(im_segm, ldm, invert_black_white=True) 49 | 50 | segm_rgb_expected = np.zeros([20, 20, 20, 3]) 51 | segm_rgb_expected[..., :] = np.array([255, 255, 255]) 52 | segm_rgb_expected[:5, :5, :5, :] = np.array([255, 0, 0]) 53 | segm_rgb_expected[5:10, 5:10, 5:10, :] = np.array([204, 0, 0]) 54 | segm_rgb_expected[10:15, 10:15, 10:15, :] = np.array([51, 51, 255]) 55 | segm_rgb_expected[15:, 15:, 15:, :] = np.array([102, 102, 255]) 56 | 57 | np.testing.assert_equal(im_segm_rgb.get_fdata(), segm_rgb_expected) 58 | 59 | 60 | @write_and_erase_temporary_folder_with_dummy_labels_descriptor 61 | def test_get_rgb_image_from_segmentation_and_label_descriptor_wrong_input_dimension(): 62 | ldm = LabelsDescriptorManager(jph(pfo_tmp_test, "labels_descriptor.txt")) 63 | im_segm = nib.Nifti1Image(np.zeros([4, 4, 4, 4]), affine=np.eye(4)) 64 | with np.testing.assert_raises(IOError): 65 | get_rgb_image_from_segmentation_and_label_descriptor(im_segm, ldm) 66 | 67 | 68 | if __name__ == "__main__": 69 | test_get_rgb_image_from_segmentation_and_label_descriptor_simple() 70 | test_get_rgb_image_from_segmentation_and_label_descriptor_simple_invert_bl_wh() 71 | test_get_rgb_image_from_segmentation_and_label_descriptor_wrong_input_dimension() 72 | -------------------------------------------------------------------------------- /tests/tools/test_image_shape_manip_apply_passepartout.py: -------------------------------------------------------------------------------- 1 | import nibabel as nib 2 | import numpy as np 3 | from numpy.testing import assert_array_equal 4 | 5 | from nilabels.tools.image_shape_manipulations.apply_passepartout import ( 6 | crop_with_passepartout, 7 | crop_with_passepartout_based_on_label_segmentation, 8 | ) 9 | 10 | 11 | def test_crop_with_passepartout_simple() -> None: 12 | data = np.random.randint(0, 100, [10, 10, 10]) 13 | im = nib.Nifti1Image(data, np.eye(4), dtype=np.int64) 14 | 15 | x_min, x_max = 2, 2 16 | y_min, y_max = 3, 1 17 | z_min, z_max = 4, 5 18 | 19 | new_im = crop_with_passepartout(im, [x_min, x_max, y_min, y_max, z_min, z_max]) 20 | 21 | assert_array_equal(data[x_min:-x_max, y_min:-y_max, z_min:-z_max], new_im.get_fdata()) 22 | 23 | 24 | def test_crop_with_passepartout_based_on_label_segmentation_simple() -> None: 25 | arr = np.array( 26 | [ 27 | [ 28 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 29 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 30 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 31 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 32 | [0, 0, 0, 0, 0, 0, 2, 0, 0], 33 | [0, 0, 0, 0, 0, 0, 2, 0, 0], 34 | [0, 0, 0, 0, 0, 0, 2, 0, 0], 35 | [0, 0, 0, 0, 0, 0, 2, 0, 0], 36 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 37 | ], 38 | [ 39 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 40 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 41 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 42 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 43 | [0, 0, 0, 0, 0, 2, 2, 0, 0], 44 | [0, 0, 0, 0, 2, 2, 2, 0, 0], 45 | [0, 0, 0, 0, 0, 2, 2, 0, 0], 46 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 47 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 48 | ], 49 | ], 50 | ) 51 | 52 | arr_expected = np.array( 53 | [[[0, 0, 2], [0, 0, 2], [0, 0, 2], [0, 0, 2]], [[0, 2, 2], [2, 2, 2], [0, 2, 2], [0, 0, 0]]], 54 | ) 55 | 56 | im_intput = nib.Nifti1Image(arr, np.eye(4), dtype=np.int64) 57 | 58 | im_cropped = crop_with_passepartout_based_on_label_segmentation(im_intput, im_intput, [0, 0, 0], 2) 59 | 60 | assert_array_equal(arr_expected, im_cropped.get_fdata()) 61 | 62 | 63 | def test_crop_with_passepartout_based_on_label_segmentation_with_im() -> None: 64 | dat = np.array( 65 | [ 66 | [ 67 | [1, 1, 1, 1, 1, 1, 1, 1, 1], 68 | [2, 2, 2, 2, 2, 2, 2, 2, 2], 69 | [3, 3, 3, 3, 3, 3, 3, 3, 3], 70 | [4, 4, 4, 4, 4, 4, 4, 4, 4], 71 | [5, 5, 5, 5, 5, 5, 5, 5, 5], 72 | [6, 6, 6, 6, 6, 6, 6, 6, 6], 73 | [7, 7, 7, 7, 7, 7, 7, 7, 7], 74 | [8, 8, 8, 8, 8, 8, 8, 8, 8], 75 | [9, 9, 9, 9, 9, 9, 9, 9, 9], 76 | ], 77 | [ 78 | [9, 9, 9, 9, 9, 9, 9, 9, 9], 79 | [8, 8, 8, 8, 8, 8, 8, 8, 8], 80 | [7, 7, 7, 7, 7, 7, 7, 7, 7], 81 | [6, 6, 6, 6, 6, 6, 6, 6, 6], 82 | [5, 5, 5, 5, 5, 5, 5, 5, 5], 83 | [4, 4, 4, 4, 4, 4, 4, 4, 4], 84 | [3, 3, 3, 3, 3, 3, 3, 3, 3], 85 | [2, 2, 2, 2, 2, 2, 2, 2, 2], 86 | [1, 1, 1, 1, 1, 1, 1, 1, 1], 87 | ], 88 | ], 89 | ) 90 | 91 | sgm = np.array( 92 | [ 93 | [ 94 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 95 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 96 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 97 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 98 | [0, 0, 0, 0, 0, 0, 2, 0, 0], 99 | [0, 0, 0, 0, 0, 0, 2, 0, 0], 100 | [0, 0, 0, 0, 0, 0, 2, 0, 0], 101 | [0, 0, 0, 0, 0, 0, 2, 0, 0], 102 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 103 | ], 104 | [ 105 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 106 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 107 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 108 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 109 | [0, 0, 0, 0, 0, 2, 2, 0, 0], 110 | [0, 0, 0, 0, 2, 2, 2, 0, 0], 111 | [0, 0, 0, 0, 0, 2, 2, 0, 0], 112 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 113 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 114 | ], 115 | ], 116 | ) 117 | 118 | arr_expected = np.array( 119 | [[[5, 5, 5], [6, 6, 6], [7, 7, 7], [8, 8, 8]], [[5, 5, 5], [4, 4, 4], [3, 3, 3], [2, 2, 2]]], 120 | ) 121 | 122 | img_input = nib.Nifti1Image(dat, np.eye(4), dtype=np.int64) 123 | segm_intput = nib.Nifti1Image(sgm, np.eye(4), dtype=np.int64) 124 | 125 | im_cropped = crop_with_passepartout_based_on_label_segmentation(img_input, segm_intput, [0, 0, 0], 2) 126 | 127 | assert_array_equal(arr_expected, im_cropped.get_fdata()) 128 | 129 | 130 | if __name__ == "__main__": 131 | test_crop_with_passepartout_simple() 132 | test_crop_with_passepartout_based_on_label_segmentation_simple() 133 | test_crop_with_passepartout_based_on_label_segmentation_with_im() 134 | -------------------------------------------------------------------------------- /tests/tools/test_image_shape_manip_merger.py: -------------------------------------------------------------------------------- 1 | import nibabel as nib 2 | import numpy as np 3 | 4 | from nilabels.tools.image_shape_manipulations.merger import ( 5 | from_segmentations_stack_to_probabilistic_segmentation, 6 | grafting, 7 | merge_labels_from_4d, 8 | reproduce_slice_fourth_dimension, 9 | stack_images, 10 | substitute_volume_at_timepoint, 11 | ) 12 | 13 | 14 | def test_merge_labels_from_4d_fake_input() -> None: 15 | data = np.zeros([3, 3, 3]) 16 | with np.testing.assert_raises(IOError): 17 | merge_labels_from_4d(data) 18 | 19 | 20 | def test_merge_labels_from_4d_shape_output() -> None: 21 | data000 = np.zeros([3, 3, 3]) 22 | data111 = np.zeros([3, 3, 3]) 23 | data222 = np.zeros([3, 3, 3]) 24 | data000[0, 0, 0] = 1 25 | data111[1, 1, 1] = 2 26 | data222[2, 2, 2] = 4 27 | data = np.stack([data000, data111, data222], axis=3) 28 | 29 | out = merge_labels_from_4d(data) 30 | np.testing.assert_array_equal([out[0, 0, 0], out[1, 1, 1], out[2, 2, 2]], [1, 2, 4]) 31 | 32 | out = merge_labels_from_4d(data, keep_original_values=False) 33 | np.testing.assert_array_equal([out[0, 0, 0], out[1, 1, 1], out[2, 2, 2]], [1, 2, 3]) 34 | 35 | 36 | def test_stack_images_cascade() -> None: 37 | d = 2 38 | im1 = nib.Nifti1Image(np.zeros([d, d]), affine=np.eye(4), dtype=np.int64) 39 | np.testing.assert_array_equal(im1.shape, (d, d)) 40 | 41 | list_images1 = [im1] * d 42 | im2 = stack_images(list_images1) 43 | np.testing.assert_array_equal(im2.shape, (d, d, d)) 44 | 45 | list_images2 = [im2] * d 46 | im3 = stack_images(list_images2) 47 | np.testing.assert_array_equal(im3.shape, (d, d, d, d)) 48 | 49 | list_images3 = [im3] * d 50 | im4 = stack_images(list_images3) 51 | np.testing.assert_array_equal(im4.shape, (d, d, d, d, d)) 52 | 53 | 54 | def test_reproduce_slice_fourth_dimension_wrong_input() -> None: 55 | im_test = nib.Nifti1Image(np.zeros([5, 5, 5, 5]), affine=np.eye(4), dtype=np.int64) 56 | with np.testing.assert_raises(IOError): 57 | reproduce_slice_fourth_dimension(im_test) 58 | 59 | 60 | def test_reproduce_slice_fourth_dimension_simple() -> None: 61 | data_test = np.arange(16).reshape(4, 4) 62 | num_slices = 4 63 | repetition_axis = 2 64 | 65 | im_reproduced = reproduce_slice_fourth_dimension( 66 | nib.Nifti1Image(data_test, affine=np.eye(4), dtype=np.int64), 67 | num_slices=4, 68 | repetition_axis=repetition_axis, 69 | ) 70 | 71 | data_expected = np.stack([data_test] * num_slices, axis=repetition_axis) 72 | 73 | np.testing.assert_array_equal(im_reproduced.get_fdata(), data_expected) 74 | 75 | 76 | def test_grafting_simple() -> None: 77 | data_hosting = 3 * np.ones([5, 5, 5]) 78 | data_patch = np.zeros([5, 5, 5]) 79 | data_patch[2:4, 2:4, 2:4] = 7 80 | 81 | data_expected = 3 * np.ones([5, 5, 5]) 82 | data_expected[2:4, 2:4, 2:4] = 7 83 | 84 | im_hosting = nib.Nifti1Image(data_hosting, affine=np.eye(4)) 85 | im_patch = nib.Nifti1Image(data_patch, affine=np.eye(4)) 86 | 87 | im_grafted = grafting(im_hosting, im_patch) 88 | 89 | np.testing.assert_array_equal(im_grafted.get_fdata(), data_expected) 90 | 91 | 92 | def test_from_segmentations_stack_to_probabilistic_segmentation_simple() -> None: 93 | # Generate initial 1D segmentations: 94 | # 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 95 | a1 = [0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4] 96 | a2 = [0, 0, 1, 1, 1, 1, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4] 97 | a3 = [0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4] 98 | a4 = [0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4] 99 | a5 = [0, 0, 1, 1, 1, 1, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4] 100 | a6 = [0, 0, 1, 1, 1, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4] 101 | 102 | stack = np.stack([np.array(a) for a in [a1, a2, a3, a4, a5, a6]]) 103 | 104 | prob = from_segmentations_stack_to_probabilistic_segmentation(stack) 105 | 106 | # expected output probability for each class, computed manually(!) 107 | k0 = [6, 5, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 108 | k1 = [0, 1, 4, 6, 5, 5, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 109 | k2 = [0, 0, 0, 0, 1, 1, 4, 6, 3, 3, 1, 0, 0, 0, 0, 0, 0, 0, 0] 110 | k3 = [0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 5, 6, 5, 5, 3, 3, 2, 1, 0] 111 | k4 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 3, 3, 4, 5, 6] 112 | 113 | k0 = 1 / 6.0 * np.array(k0) 114 | k1 = 1 / 6.0 * np.array(k1) 115 | k2 = 1 / 6.0 * np.array(k2) 116 | k3 = 1 / 6.0 * np.array(k3) 117 | k4 = 1 / 6.0 * np.array(k4) 118 | 119 | prob_expected = np.stack([k0, k1, k2, k3, k4], axis=0) 120 | np.testing.assert_array_equal(prob, prob_expected) 121 | 122 | 123 | def test_from_segmentations_stack_to_probabilistic_segmentation_random_sum_rows_to_get_one() -> None: 124 | j = 12 125 | n = 120 126 | k = 7 127 | rng = np.random.default_rng(2021) 128 | stack = np.stack([rng.choice(range(k), n) for _ in range(j)]) 129 | prob = from_segmentations_stack_to_probabilistic_segmentation(stack) 130 | s = np.sum(prob, axis=0) 131 | np.testing.assert_array_almost_equal(s, np.ones(n)) 132 | 133 | 134 | def test_substitute_volume_at_timepoint_wrong_input() -> None: 135 | im_4d = nib.Nifti1Image(np.zeros([5, 5, 5, 3]), affine=np.eye(4)) 136 | im_3d = nib.Nifti1Image(np.ones([5, 5, 5]), affine=np.eye(4)) 137 | tp = 7 138 | with np.testing.assert_raises(IOError): 139 | substitute_volume_at_timepoint(im_4d, im_3d, tp) 140 | 141 | 142 | def test_substitute_volume_at_timepoint_simple() -> None: 143 | im_4d = nib.Nifti1Image(np.zeros([5, 5, 5, 4]), affine=np.eye(4)) 144 | im_3d = nib.Nifti1Image(np.ones([5, 5, 5]), affine=np.eye(4)) 145 | tp = 2 146 | expected_data = np.stack( 147 | [np.zeros([5, 5, 5]), np.zeros([5, 5, 5]), np.ones([5, 5, 5]), np.zeros([5, 5, 5])], 148 | axis=3, 149 | ) 150 | im_subs = substitute_volume_at_timepoint(im_4d, im_3d, tp) 151 | 152 | np.testing.assert_array_equal(im_subs.get_fdata(), expected_data) 153 | 154 | 155 | if __name__ == "__main__": 156 | test_merge_labels_from_4d_fake_input() 157 | test_merge_labels_from_4d_shape_output() 158 | 159 | test_stack_images_cascade() 160 | 161 | test_reproduce_slice_fourth_dimension_wrong_input() 162 | test_reproduce_slice_fourth_dimension_simple() 163 | 164 | test_grafting_simple() 165 | 166 | test_substitute_volume_at_timepoint_wrong_input() 167 | test_substitute_volume_at_timepoint_simple() 168 | -------------------------------------------------------------------------------- /tests/tools/test_image_shape_manip_splitter.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from nilabels.tools.image_shape_manipulations.splitter import split_labels_to_4d 4 | 5 | 6 | def test_split_labels_to_4d_true_false() -> None: 7 | data = np.array(range(8)).reshape(2, 2, 2) 8 | splitted_4d = split_labels_to_4d(data, list_labels=range(8)) 9 | for t in range(8): 10 | expected_slice = np.zeros(8) 11 | expected_slice[t] = t 12 | np.testing.assert_array_equal(splitted_4d[..., t], expected_slice.reshape(2, 2, 2)) 13 | 14 | splitted_4d = split_labels_to_4d(data, list_labels=range(8), keep_original_values=False) 15 | expected_ans = [ 16 | [[[1, 0, 0, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0, 0, 0]], [[0, 0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0, 0]]], 17 | [[[0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0]], [[0, 0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 0, 0, 1]]], 18 | ] 19 | np.testing.assert_array_equal(splitted_4d, expected_ans) 20 | 21 | 22 | if __name__ == "__main__": 23 | test_split_labels_to_4d_true_false() 24 | -------------------------------------------------------------------------------- /tests/tools/test_labels_checker.py: -------------------------------------------------------------------------------- 1 | import nibabel as nib 2 | import numpy as np 3 | 4 | 5 | def test_check_missing_labels_paired() -> None: 6 | array = np.array( 7 | [ 8 | [ 9 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 10 | [0, 0, 0, 0, 7, 0, 0, 0, 0], 11 | [0, 1, 0, 0, 7, 0, 0, 0, 0], 12 | [0, 1, 0, 6, 7, 0, 0, 0, 0], 13 | [0, 1, 0, 6, 0, 0, 2, 0, 0], 14 | [0, 1, 0, 6, 0, 0, 2, 0, 0], 15 | [0, 1, 0, 0, 0, 0, 2, 0, 0], 16 | [0, 0, 0, 0, 0, 0, 2, 0, 0], 17 | [0, 0, 0, 0, 0, 0, 2, 0, 0], 18 | ], 19 | [ 20 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 21 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 22 | [0, 1, 0, 0, 0, 0, 0, 0, 0], 23 | [0, 1, 0, 0, 0, 0, 0, 0, 0], 24 | [0, 1, 0, 0, 0, 0, 2, 0, 0], 25 | [0, 1, 0, 5, 0, 0, 2, 0, 0], 26 | [0, 1, 0, 5, 0, 0, 2, 0, 0], 27 | [0, 0, 0, 5, 0, 0, 2, 0, 0], 28 | [0, 0, 0, 0, 0, 0, 2, 0, 0], 29 | ], 30 | ], 31 | ) 32 | im = nib.Nifti1Image(array, np.eye(4), dtype=np.int64) 33 | del im 34 | # TODO 35 | 36 | 37 | def test_check_missing_labels_unpaired() -> None: 38 | # TODO 39 | pass 40 | 41 | 42 | def test_check_number_connected_components() -> None: 43 | # TODO 44 | pass 45 | --------------------------------------------------------------------------------