From ca9e5fa4bd62cb0881e6faef28516b4eabe1cbff Mon Sep 17 00:00:00 2001 From: Arman Date: Tue, 24 Sep 2024 20:04:06 -0400 Subject: [PATCH] Updated project files --- .dockerignore | 1 + .gitignore | 1 + Dockerfile | 4 +- archives/Trash/README.md | 177 +++++++++++++++ archives/Trash/app.py | 70 ++++++ archives/Trash/qodana.yaml | 29 +++ archives/Trash/requirements.txt | 1 + archives/Trash/setup.py | 48 ++++ archives/Trash/tests/test_example.py | 21 ++ archives/bootstrap.sh | 267 +++++++++++++++++++++++ archives/dcm2nii.py | 31 +++ archives/file_template.py | 26 +++ archives/fsl_docker/Dockerfile | 46 ++++ archives/fsl_docker/draft_Dockerfile.txt | 17 ++ archives/progress.txt | 41 ++++ archives/register_archive1.py | 38 ++++ archives/registration_tools_archive1.py | 95 ++++++++ archives/requirements_archive1.txt | 10 + images_register.py | 74 ++++--- registration_tools.py | 92 ++++++++ requirements.txt | 11 + setup.py | 45 +++- visualization_tools.py | 117 ++++++++++ 23 files changed, 1226 insertions(+), 36 deletions(-) create mode 100644 archives/Trash/README.md create mode 100755 archives/Trash/app.py create mode 100644 archives/Trash/qodana.yaml create mode 100644 archives/Trash/requirements.txt create mode 100644 archives/Trash/setup.py create mode 100644 archives/Trash/tests/test_example.py create mode 100755 archives/bootstrap.sh create mode 100644 archives/dcm2nii.py create mode 100644 archives/file_template.py create mode 100644 archives/fsl_docker/Dockerfile create mode 100644 archives/fsl_docker/draft_Dockerfile.txt create mode 100644 archives/progress.txt create mode 100644 archives/register_archive1.py create mode 100644 archives/registration_tools_archive1.py create mode 100644 archives/requirements_archive1.txt create mode 100644 registration_tools.py create mode 100644 visualization_tools.py diff --git a/.dockerignore b/.dockerignore index 63df785..04cebe3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -16,3 +16,4 @@ __pycache__/ examples/ venv/ +data/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index ffd9fdb..9f0b933 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ __pycache__/ .vscode/ venv/ +data \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index c163046..f6f721c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,8 +4,8 @@ FROM docker.io/python:3.12.1-slim-bookworm LABEL org.opencontainers.image.authors="FNNDSC " \ - org.opencontainers.image.title="Multiple image registration" \ - org.opencontainers.image.description="A ChRIS plugin to do multiple image registration" + org.opencontainers.image.title="Images registration" \ + org.opencontainers.image.description="A ChRIS plugin to do image registration" ARG SRCDIR=/usr/local/src/pl-images-register WORKDIR ${SRCDIR} diff --git a/archives/Trash/README.md b/archives/Trash/README.md new file mode 100644 index 0000000..a8ff3ec --- /dev/null +++ b/archives/Trash/README.md @@ -0,0 +1,177 @@ +# _ChRIS_ Plugin Template + +[![test status](https://github.com/FNNDSC/python-chrisapp-template/actions/workflows/src.yml/badge.svg)](https://github.com/FNNDSC/python-chrisapp-template/actions/workflows/src.yml) +[![MIT License](https://img.shields.io/github/license/FNNDSC/python-chrisapp-template)](LICENSE) + +This is a minimal template repository for _ChRIS_ plugin applications in Python. + +## About _ChRIS_ Plugins + +A _ChRIS_ plugin is a scientific data-processing software which can run anywhere all-the-same: +in the cloud via a [web app](https://github.com/FNNDSC/ChRIS_ui/), or on your own laptop +from the terminal. They are easy to build and easy to understand: most simply, a +_ChRIS_ plugin is a command-line program which processes data from an input directory +and creates data to an output directory with the usage +`commandname [options...] inputdir/ outputdir/`. + +For more information, visit our website https://chrisproject.org + +## How to Use This Template + +Go to https://github.com/FNNDSC/python-chrisapp-template and click "Use this template". +The newly created repository is ready to use right away. + +A script `bootstrap.sh` is provided to help fill in and rename values for your new project. +It is optional to use. + +1. Edit the variables in `bootstrap.sh` +2. Run `./bootstrap.sh` +3. Follow the instructions it will print out + +## Example Plugins + +Here are some good, complete examples of _ChRIS_ plugins created from this template. + +- https://github.com/FNNDSC/pl-dcm2niix (basic command wrapper example) +- (parallelizes a command) +- https://github.com/FNNDSC/pl-mri-preview (uses [NiBabel](https://nipy.org/nibabel/)) +- https://github.com/FNNDSC/pl-pyvista-volume (example using Python package project structure and pytest) +- https://github.com/FNNDSC/pl-fetal-cp-surface-extract (has a good README.md) + +## What's Inside + +| Path | Purpose | +|----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `app.py` | Main script: start editing here! | +| `tests/` | Unit tests | +| `setup.py` | [Python project metadata and installation script](https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/#setup-py) | +| `requirements.txt` | List of Python dependencies | +| `Dockerfile` | [Container image build recipe](https://docs.docker.com/engine/reference/builder/) | +| `.github/workflows/ci.yml` | "continuous integration" using [Github Actions](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions): automatic testing, building, and uploads to https://chrisstore.co | + +## Contributing + +The source code for the `main` branch of this repository is on the +[src](https://github.com/fnndsc/python-chrisapp-template/tree/src) +branch, which has an additional file +[`.github/workflows/src.yml`](https://github.com/FNNDSC/python-chrisapp-template/blob/src/.github/workflows/src.yml) +When tests pass, changes are automatically merged into `main`. +Developers should commit to or make pull requests targeting `src`. +Do not push directly to `main`. + +This is a workaround in order to do automatic testing of this template +without including the `.github/workflows/src.yml` file in the template itself. + + diff --git a/archives/Trash/app.py b/archives/Trash/app.py new file mode 100755 index 0000000..68e8e68 --- /dev/null +++ b/archives/Trash/app.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +from pathlib import Path +from argparse import ArgumentParser, Namespace, ArgumentDefaultsHelpFormatter + +from chris_plugin import chris_plugin, PathMapper + +__version__ = '1.0.0' + +DISPLAY_TITLE = r""" +ChRIS Plugin Template Title +""" + + +parser = ArgumentParser(description='!!!CHANGE ME!!! An example ChRIS plugin which ' + 'counts the number of occurrences of a given ' + 'word in text files.', + formatter_class=ArgumentDefaultsHelpFormatter) +parser.add_argument('-w', '--word', required=True, type=str, + help='word to count') +parser.add_argument('-p', '--pattern', default='**/*.txt', type=str, + help='input file filter glob') +parser.add_argument('-V', '--version', action='version', + version=f'%(prog)s {__version__}') + + +# The main function of this *ChRIS* plugin is denoted by this ``@chris_plugin`` "decorator." +# Some metadata about the plugin is specified here. There is more metadata specified in setup.py. +# +# documentation: https://fnndsc.github.io/chris_plugin/chris_plugin.html#chris_plugin +@chris_plugin( + parser=parser, + title='My ChRIS plugin', + category='', # ref. https://chrisstore.co/plugins + min_memory_limit='100Mi', # supported units: Mi, Gi + min_cpu_limit='1000m', # millicores, e.g. "1000m" = 1 CPU core + min_gpu_limit=0 # set min_gpu_limit=1 to enable GPU +) +def main(options: Namespace, inputdir: Path, outputdir: Path): + """ + *ChRIS* plugins usually have two positional arguments: an **input directory** containing + input files and an **output directory** where to write output files. Command-line arguments + are passed to this main method implicitly when ``main()`` is called below without parameters. + + :param options: non-positional arguments parsed by the parser given to @chris_plugin + :param inputdir: directory containing (read-only) input files + :param outputdir: directory where to write output files + """ + + print(DISPLAY_TITLE) + + # Typically it's easier to think of programs as operating on individual files + # rather than directories. The helper functions provided by a ``PathMapper`` + # object make it easy to discover input files and write to output files inside + # the given paths. + # + # Refer to the documentation for more options, examples, and advanced uses e.g. + # adding a progress bar and parallelism. + mapper = PathMapper.file_mapper(inputdir, outputdir, glob=options.pattern, suffix='.count.txt') + for input_file, output_file in mapper: + # The code block below is a small and easy example of how to use a ``PathMapper``. + # It is recommended that you put your functionality in a helper function, so that + # it is more legible and can be unit tested. + data = input_file.read_text() + frequency = data.count(options.word) + output_file.write_text(str(frequency)) + + +if __name__ == '__main__': + main() diff --git a/archives/Trash/qodana.yaml b/archives/Trash/qodana.yaml new file mode 100644 index 0000000..84e3e49 --- /dev/null +++ b/archives/Trash/qodana.yaml @@ -0,0 +1,29 @@ +#-------------------------------------------------------------------------------# +# Qodana analysis is configured by qodana.yaml file # +# https://www.jetbrains.com/help/qodana/qodana-yaml.html # +#-------------------------------------------------------------------------------# +version: "1.0" + +#Specify inspection profile for code analysis +profile: + name: qodana.starter + +#Enable inspections +#include: +# - name: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +#Execute shell command before Qodana execution (Applied in CI/CD pipeline) +#bootstrap: sh ./prepare-qodana.sh + +#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) +#plugins: +# - id: #(plugin id can be found at https://plugins.jetbrains.com) + +#Specify Qodana linter for analysis (Applied in CI/CD pipeline) +linter: jetbrains/qodana-python:latest diff --git a/archives/Trash/requirements.txt b/archives/Trash/requirements.txt new file mode 100644 index 0000000..645d77e --- /dev/null +++ b/archives/Trash/requirements.txt @@ -0,0 +1 @@ +chris_plugin==0.4.0 diff --git a/archives/Trash/setup.py b/archives/Trash/setup.py new file mode 100644 index 0000000..09a65b6 --- /dev/null +++ b/archives/Trash/setup.py @@ -0,0 +1,48 @@ +from setuptools import setup +import re + +_version_re = re.compile(r"(?<=^__version__ = (\"|'))(.+)(?=\"|')") + +def get_version(rel_path: str) -> str: + """ + Searches for the ``__version__ = `` line in a source code file. + + https://packaging.python.org/en/latest/guides/single-sourcing-package-version/ + """ + with open(rel_path, 'r') as f: + matches = map(_version_re.search, f) + filtered = filter(lambda m: m is not None, matches) + version = next(filtered, None) + if version is None: + raise RuntimeError(f'Could not find __version__ in {rel_path}') + return version.group(0) + + +setup( + name='chris-plugin-template', + version=get_version('archives/Trash/app.py'), + description='A ChRIS DS plugin template', + author='FNNDSC', + author_email='dev@babyMRI.org', + url='https://github.com/FNNDSC/python-chrisapp-template', + py_modules=['app'], + install_requires=['chris_plugin'], + license='MIT', + entry_points={ + 'console_scripts': [ + 'commandname = app:main' + ] + }, + classifiers=[ + 'License :: OSI Approved :: MIT License', + 'Topic :: Scientific/Engineering', + 'Topic :: Scientific/Engineering :: Bio-Informatics', + 'Topic :: Scientific/Engineering :: Medical Science Apps.' + ], + extras_require={ + 'none': [], + 'dev': [ + 'pytest~=7.1' + ] + } +) diff --git a/archives/Trash/tests/test_example.py b/archives/Trash/tests/test_example.py new file mode 100644 index 0000000..63003dc --- /dev/null +++ b/archives/Trash/tests/test_example.py @@ -0,0 +1,21 @@ +from pathlib import Path + +from archives.Trash.app import parser, main + + +def test_main(tmp_path: Path): + # setup example data + inputdir = tmp_path / 'incoming' + outputdir = tmp_path / 'outgoing' + inputdir.mkdir() + outputdir.mkdir() + (inputdir / 'plaintext.txt').write_text('hello ChRIS, I am a ChRIS plugin') + + # simulate run of main function + options = parser.parse_args(['--word', 'ChRIS', '--pattern', '*.txt']) + main(options, inputdir, outputdir) + + # assert behavior is expected + expected_output_file = outputdir / 'plaintext.count.txt' + assert expected_output_file.exists() + assert expected_output_file.read_text() == '2' diff --git a/archives/bootstrap.sh b/archives/bootstrap.sh new file mode 100755 index 0000000..d34ad24 --- /dev/null +++ b/archives/bootstrap.sh @@ -0,0 +1,267 @@ +#!/usr/bin/env bash +# bootstrap.sh: customize python-chrisapp-template with project details +# +# WARNING: This script is for advanced users only! Do not proceed +# unless you understand what this does. New developers would find +# it easier to use python-chrisapp-template as is. Simply ignore +# and optionally delete this file. + +# ======================================== +# CONFIGURATION +# ======================================== + +# ---------------------------------------- +# STEP 1. Change these values to your liking. +# ---------------------------------------- + +PLUGIN_NAME="pl-image-register" # name of current directory +PLUGIN_TITLE='My ChRIS Plugin' +SCRIPT_NAME='image_register' +DESCRIPTION='A ChRIS plugin to do something awesome' +ORGANIZATION='FNNDSC' +EMAIL='dev@babyMRI.org' + +# Github Actions: automatically test and build your code. +# https://github.com/FNNDSC/python-chrisapp-template/wiki/Continuous-Integration +# +# These options will fail unless your Github settings are preconfigured. +# Repositories under github.com/FNNDSC are preconfigured, so these defaults might work. +# Please review the file .github/workflows/ci.yml before you push it. + +# Automatically test on Github Actions each time you run `git push` +# If the value is "no" then tests are not performed. There are no side effects. +ENABLE_ACTIONS_TEST=yes +# Automatically build images on Github Actions each time you run `git push`, +# and also publish to https://chrisstore.co each time you run `git push --tags` +# If the value is "no" then builds will not be automated. +ENABLE_ACTIONS_BUILD=yes + +# WARNING: the default configuration in .github/workflows/ci.yml is to allow for +# the build to proceed regardless of whether tests pass. To modify this behavior +# and other advanced features (such as multi-architecture builds such as arm64, ppc64le) +# you must edit .github/workflows/ci.yml by hand. + + +# ---------------------------------------- +# STEP 2. Uncomment the line where it says READY=yes +# ---------------------------------------- + +READY=yes + +# ---------------------------------------- +# STEP 3. Run: ./bootstrap.sh +# ---------------------------------------- + + +if [ "$(uname -o 2> /dev/null)" != 'GNU/Linux' ]; then + >&2 echo "error: this script only works on GNU/Linux." +fi + +if ! [ "$READY" = 'yes' ]; then + >&2 echo "error: you are not READY." + exit 1 +fi + +cd $(dirname "$0") + + +# ======================================== +# VALIDATE INPUT +# ======================================== + +function contains_invalid_characters () { + [[ "$1" = *"/"* ]] +} + +# given a variable name, exit if the variable's value contains invalid characters. +function check_variable_value_valid () { + local varname="$1" + local varvalue="${!varname}" + if contains_invalid_characters "$varvalue"; then + >&2 echo "error: invalid characters in $varname=$varvalue" + exit 1 + fi +} + +# may not contain '/' +check_variable_value_valid PLUGIN_NAME +check_variable_value_valid SCRIPT_NAME +check_variable_value_valid ORGANIZATION +check_variable_value_valid EMAIL + + +# ======================================== +# COMMIT THE USER-SET CONFIG +# ======================================== + +# print command to run before running it +function verb () { + set -x + "$@" + { set +x; } 2> /dev/null +} + +# fail on error +set -e +set -o pipefail + +verb git commit -m 'Configure python-chrisapp-template/bootstrap.sh' -- "$0" + + +# ======================================== +# REPLACE VALUES +# ======================================== + +# execute sed on all files in project, excluding hidden paths and venv/ +function replace_in_all () { + if [ -z "$2" ]; then + return + fi + find . -type f \ + -not -path '*/\.*/*' -not -path '*/\venv/*' -not -name 'bootstrap.sh' \ + -exec sed -i -e "s/$1/$2/g" '{}' \; +} + +replace_in_all commandname "$SCRIPT_NAME" +replace_in_all pl-appname "$PLUGIN_NAME" +replace_in_all 'dev@babyMRI.org' "$EMAIL" +replace_in_all FNNDSC "$ORGANIZATION" + +# .github/ +if [ "${ENABLE_ACTIONS_TEST,,}" = 'yes' ]; then + sed -i -e '/delete this line to enable automatic testing/d' .github/workflows/ci.yml +fi + +if [ "${ENABLE_ACTIONS_BUILD,,}" = 'yes' ]; then + sed -i -e '/delete this line and uncomment the line below to enable automatic builds/d' .github/workflows/ci.yml + sed -i -e 's/# *if: github\.event_name/if: github\.event_name/' .github/workflows/ci.yml +fi + +# replace "/" with "\/" in string +function escape_slashes () { + sed 's/\//\\&/g' <<< "$@" +} + +escaped_description="$(escape_slashes "$DESCRIPTION")" +escaped_title="$(escape_slashes "$PLUGIN_TITLE")" + +# README.md +temp_file=$(mktemp) +sed -e'/^# ChRIS Plugin Title$/'\{ -e:1 -en\;b1 -e\} -ed README.md \ + | sed "s/^# ChRIS Plugin Title\$/# $escaped_title/" \ + | sed '/^END README TEMPLATE -->$/d' \ + | sed "s/fnndsc/${ORGANIZATION,,}/g" \ + | sed "s/app\\.py/$SCRIPT_NAME.py/g" \ + > $temp_file +mv $temp_file README.md + +# Dockerfile +sed "s#ARG SRCDIR=/usr/local/src/app#ARG SRCDIR=/usr/local/src/$PLUGIN_NAME#" Dockerfile \ + | sed "s/org\.opencontainers\.image\.title=\"ChRIS Plugin Title\"/org.opencontainers.image.title=\"$escaped_title\"/" \ + | sed "s/org\.opencontainers\.image\.description=\"A ChRIS plugin that\.\.\.\"/org.opencontainers.image.description=\"$escaped_description\"/" \ + > $temp_file +mv $temp_file Dockerfile + +# setup.py + +function guess_https_url () { + local origin="$(git remote get-url origin)" + local https_url="$origin" + if [[ "$https_url" = "git@"* ]]; then + # convert SSH url to HTTPS url by + # 1. change last ':' to '/' + # 2. replace leading 'git@' with 'https://' + https_url="$( + echo "$https_url" \ + | sed 's#\(.*\):#\1/#' \ + | sed 's#^git@#https://#' + )" + fi + echo "${https_url:0:-4}" # remove trailing ".git" +} + +appname_without_prefix="$(sed -E 's/(pl|dbg|ep)-//' <<< "$PLUGIN_NAME")" +sed "s/name='.*'/name='$appname_without_prefix'/" setup.py \ + | sed "s/description='.*'/description='$escaped_description'/" \ + | sed "s/py_modules=\['app'\]/py_modules=['$SCRIPT_NAME']/" \ + | sed "s/app:main/$SCRIPT_NAME:main/" \ + | sed "s#url='.*'#url='$(guess_https_url)'#" \ + | sed "s/app\.py/$SCRIPT_NAME.py/" \ + > $temp_file +mv $temp_file setup.py + +# app.py + +# FIGlet over HTTPS, since it's probably not installed locally +function figlet_wrapper () { + curl -fsSG 'https://figlet.chrisproject.org/' --data-urlencode "message=$*" \ + | grep -v '^[[:space:]]*$' +} + +function inject_figleted_title () { + python << EOF +for line in open('app.py'): + if line == 'ChRIS Plugin Template Title\n': + print(r"""$1""") + else: + print(line, end='') +EOF +} + +figleted_title="$(figlet_wrapper "$PLUGIN_NAME")" +echo "$figleted_title" +inject_figleted_title "$figleted_title" \ + | sed "s/title='My ChRIS plugin'/title='$escaped_title'/" \ + | sed "s/description='cli description'/description='$escaped_description'/" \ + > "$SCRIPT_NAME.py" +rm app.py + +# tests/ +for test_file in tests/*.py; do + sed "s/from app import/from $SCRIPT_NAME import/" $test_file > $temp_file + mv $temp_file $test_file +done + +# ======================================== +# SETUP +# ======================================== + +if ! [ -e venv ]; then + verb python -m venv venv +fi + +>&2 echo + source venv/bin/activate +source venv/bin/activate +verb pip install -r requirements.txt +verb pip install -e '.[dev]' + + +if [ -z "$TERM" ]; then + tput=tput +else + tput=true +fi + +$tput bold +>&2 printf '\n%s\n\n' '✨Done!✨' +$tput sgr0 + +$tput setaf 3 +>&2 echo 'To undo these actions and start over, run:' +>&2 printf '\n\t%s\n\t%s\n\t%s\n\t%s\n\n' \ + 'git reset --hard' \ + 'git clean -df' \ + 'rm -rf venv *.egg-info' \ + "git reset 'HEAD^'" +$tput setaf 6 +>&2 echo 'Activate the Python virtual environment by running:' +>&2 printf '\n\t%s\n\n' 'source venv/bin/activate' +>&2 echo 'Save these changes by running:' +>&2 printf '\n\t%s\n\n' 'git add -A && git commit -m "Run bootstrap.sh"' +$tput setaf 2 +echo 'For more information on how to get started, see README.md' +$tput sgr0 + +verb rm -v "$0" + +# Note to self: consider rewriting this in Python? diff --git a/archives/dcm2nii.py b/archives/dcm2nii.py new file mode 100644 index 0000000..8866d12 --- /dev/null +++ b/archives/dcm2nii.py @@ -0,0 +1,31 @@ +import os +import subprocess + + +def convert_dicom_to_nifti(dicom_folder, output_folder): + # Ensure the output folder exists + if not os.path.exists(output_folder): + os.makedirs(output_folder) + + # Command to run dcm2niix + command = [ + 'dcm2niix', # dcm2niix executable + '-z', 'y', # Enable compression + '-o', output_folder, # Output folder + dicom_folder # Input DICOM folder + ] + + # Run the command + subprocess.run(command, check=True) + print(f"Conversion complete. NIfTI files are saved in {output_folder}") + + + +if __name__ == '__main__': + # Define paths + dicom_folder = '/Users/arman/projects/pl-image-register/data/ALD_dicom' # Path to folder containing DICOMs + output_folder = '/Users/arman/projects/pl-image-register/data/nifti' # Path to folder to save NIfTI file + + # Convert DICOM to NIfTI + convert_dicom_to_nifti(dicom_folder, output_folder) + diff --git a/archives/file_template.py b/archives/file_template.py new file mode 100644 index 0000000..89650e7 --- /dev/null +++ b/archives/file_template.py @@ -0,0 +1,26 @@ +""" +Developed by Arman Avesta, MD, PhD +FNNDSC | Boston Children's Hospital | Harvard Medical School + +This module contains ???. +""" + +# ----------------------------------------------- ENVIRONMENT SETUP --------------------------------------------------- +# Project imports: + +# System imports: + + +# ---------------------------------------------- HELPER FUNCTIONS ----------------------------------------------------- + + + +# ----------------------------------------------- MAIN FUNCTIONS ------------------------------------------------------ + + + +# -------------------------------------------------- CODE TESTING ----------------------------------------------------- + +if __name__ == '__main__': + pass + diff --git a/archives/fsl_docker/Dockerfile b/archives/fsl_docker/Dockerfile new file mode 100644 index 0000000..472758f --- /dev/null +++ b/archives/fsl_docker/Dockerfile @@ -0,0 +1,46 @@ +# Python version can be changed, e.g. +# FROM python:3.8 +# FROM ghcr.io/mamba-org/micromamba:1.5.1-focal-cuda-11.3.1 +FROM docker.io/python:3.12.1-slim-bookworm + +LABEL org.opencontainers.image.authors="FNNDSC " \ + org.opencontainers.image.title="register_image" \ + org.opencontainers.image.description="This plugin re-orients a 3D scan (CT, MRI, PET, etc) into standard ' \ + ' planes, re-slices the re-oriented image, and saves the re-oriented images as a NIfTI file." + +ARG SRCDIR=/usr/local/src/app +WORKDIR ${SRCDIR} + +COPY requirements.txt . +RUN --mount=type=cache,sharing=private,target=/root/.cache/pip pip install -r requirements.txt + +# ---------------------------------------------- +# Install FSL: + +python3 -m pip install --upgrade certifi +python3 -m certifi + +echo "export SSL_CERT_FILE=$(python3 -m certifi)" >> ~/.bash_profile +source ~/.bash_profile + + +wget https://fsl.fmrib.ox.ac.uk/fsldownloads/fslconda/releases/fslinstaller.py + +chmod +x fslinstaller.py +python3 fslinstaller.py -q + +echo "export FSLDIR=/usr/local/fsl" >> ~/.bash_profile +echo ". ${FSLDIR}/etc/fslconf/fsl.sh" >> ~/.bash_profile +echo "PATH=${FSLDIR}/bin:${PATH}" >> ~/.bash_profile +source ~/.bash_profile + +# ---------------------------------------------- + + +COPY . . +ARG extras_require=none +RUN pip install ".[${extras_require}]" \ + && cd / && rm -rf ${SRCDIR} +WORKDIR / + +CMD ["commandname"] diff --git a/archives/fsl_docker/draft_Dockerfile.txt b/archives/fsl_docker/draft_Dockerfile.txt new file mode 100644 index 0000000..a295931 --- /dev/null +++ b/archives/fsl_docker/draft_Dockerfile.txt @@ -0,0 +1,17 @@ +python3 -m pip install --upgrade certifi +python3 -m certifi + +echo "export SSL_CERT_FILE=$(python3 -m certifi)" >> ~/.bash_profile +source ~/.bash_profile + + +wget https://fsl.fmrib.ox.ac.uk/fsldownloads/fslconda/releases/fslinstaller.py + +chmod +x fslinstaller.py +python3 fslinstaller.py -q + +echo "export FSLDIR=/usr/local/fsl" >> ~/.bash_profile +echo ". ${FSLDIR}/etc/fslconf/fsl.sh" >> ~/.bash_profile +echo "PATH=${FSLDIR}/bin:${PATH}" >> ~/.bash_profile +source ~/.bash_profile + diff --git a/archives/progress.txt b/archives/progress.txt new file mode 100644 index 0000000..a841d46 --- /dev/null +++ b/archives/progress.txt @@ -0,0 +1,41 @@ + +1. I replaced FSL's FLIRT registration with SimpleITK-based multi-resolution rigid registration. As a result, we don't +need to deal with installing FSL on our container. Also it's much faster than FSL's FLIRT (10-15 s vs 2-3 mins). + +2. I updated the resampling strategy to use B-splines instead of tri-linear interpolation --> the resultant registered +images now how a high quality (it wasn't the case with FLIRT registration). + +3. I didn't use any unsupported required packages, so that the plugin can be built. +While I temporarily dodged the bullet by avoiding these "unsupported" packages, I'm sure this issue will come up, +especially given that my tests show that even widely-used and necessary packages such as torch and monai lead to +errors in building the plugin. + + + + + +Jennings Zhang +Hi @arman.avesta:matrix.org. My name is Jennings, I am usually the one who you call if you have build problems. However, I'm currently on vacation. I will still peek at these chats every now and then. + +I strongly recommend building (and testing) locally before you push to GitHub. The builds on GitHub are relatively slow. (The fact that GitHub builds our projects for us automatically is a pretty advanced thing in the first place.) + +To build locally, run + + +docker build -t localhost/fnndsc/pl-image-register:latest . + +Jennings Zhang +In your GitHub build logs, I see the error message is gcc: command not found. This error message has to do with how Python's ecosystem of community libraries, including monai, torch, and nipype, is somewhat messy and fragmented. But fear not, you can definitely use these libraries. + +In the first line of Dockerfile, where you see the FROM statement, you are declaring the version of Python to use as version 3.12.1. Python version 3.12 is pretty new and many libraries don't fully support it yet. + +As I said, this is a messy issue. Here are some things you can try: + +Try changing the version of python to FROM docker.io/library/python:3.11.9-slim-bookworm. +If that doesn't work, try docker.io/library/python:3.11.9-bookworm (no more -slim) +Here is what's happening: some Python packages, such as pytorch, are comprised of a mix of C code and Python code. In order to install pytorch as a dependency, the C code needs to be compiled. Some pre-compiled pytorch packages are available, but they are more likely to be available for Python version 3.11 and unavailable for Python 3.12. + +If downgrading to Python version 3.11 isn't enough to fix things, then you will need to try the non-"slim" image. The non-slim image contains a C compiler (gcc) so it is likely that it will solve all of your problems. However, I recommend using non-slim images as a last resort. It is a nuclear option. The cost of using the non-slim image is that the container image for your ChRIS plugin will be an extra 300MB in compressed size, and the time it takes to build will increase from 1 minute to 10-60 minutes. + +There are more advanced alternatives you can explore (such as using conda-forge instead of pypi). + diff --git a/archives/register_archive1.py b/archives/register_archive1.py new file mode 100644 index 0000000..2f038a0 --- /dev/null +++ b/archives/register_archive1.py @@ -0,0 +1,38 @@ + +from nipype.interfaces.fsl import FLIRT # This requires FSL to be installed on the computer for it to work + + + +def register_images_rigid(fixed_image_path, moving_image_path, registered_moving_image_path, transform_matrix_path, + dof=6, cost='mutualinfo'): + + flirt = FLIRT() # https://fsl.fmrib.ox.ac.uk/fsl/docs/#/registration/flirt/index + flirt.inputs.reference = fixed_image_path + flirt.inputs.in_file = moving_image_path + flirt.inputs.out_file = registered_moving_image_path + flirt.inputs.out_matrix_file = transform_matrix_path + flirt.inputs.dof = dof # Degrees of freedom should be 6 for rigid registration + flirt.inputs.cost = cost # mutual information or normalized mutual information are best options + + result = flirt.run() + print(f' >>>>> results: {result}') + print(f"Registered image saved to {registered_moving_image_path}") + print(f"Transform matrix saved to {transform_matrix_path}") + + + + +# CODE TESTING: + +if __name__ == '__main__': + fixed_image_path = '/Users/arman/projects/register_image/data/nifti/fixed.nii.gz' + moving_image_path = '/Users/arman/projects/register_image/data/nifti/moving.nii.gz' + registered_moving_image_path = '/Users/arman/projects/register_image/data/nifti/moving_registered.nii.gz' + transform_matrix_path = '/Users/arman/projects/register_image/data/nifti/transform.mat' + + register_images_rigid(fixed_image_path, moving_image_path, registered_moving_image_path, transform_matrix_path) + + # 1. FLIRT takes ~2 minutes to complete. Will explore faster rigid registration strategies. It would be + # great if we can figure out what registration strategy Visage uses, because that takes ~10 seconds. + # 2. the registered image is a bit blurrier than the original image. Will explore better resampling strategies + # such as B-spline interpolation. \ No newline at end of file diff --git a/archives/registration_tools_archive1.py b/archives/registration_tools_archive1.py new file mode 100644 index 0000000..a47d6a8 --- /dev/null +++ b/archives/registration_tools_archive1.py @@ -0,0 +1,95 @@ + +import subprocess +from datetime import datetime +import SimpleITK as sitk +import numpy as np +import nibabel as nib + +from visualization_tools import imgshow + + +def register_images_rigid(fixed_image_path, moving_image_path, registered_moving_image_path, transform_matrix_path, + dof='6', cost='mutualinfo'): + """ + This function needs FSL's FLIRT to be able to run: https://fsl.fmrib.ox.ac.uk/fsl/docs/#/registration/flirt/index + """ + flirt_command = [ + 'flirt', + '-ref', fixed_image_path, + '-in', moving_image_path, + '-out', registered_moving_image_path, + '-omat', transform_matrix_path, + '-dof', dof, # Degrees of freedom should be '6' for rigid body transformation + '-cost', cost # multual information or normalized mutual information are best options + ] + subprocess.run(flirt_command, check=True) + print(f"Registered image saved to {registered_moving_image_path}") + print(f"Transform matrix saved to {transform_matrix_path}") + + +def resample_image(fixed_image_path, moving_image_path, registered_moving_image_path, transform_matrix_path): + """ + Resample a moving 3D image using a transform matrix to match the space of a fixed 3D image. + + Parameters: + moving_image_path (str): Path to the moving image file (e.g., 'moving.nii.gz'). + fixed_image_path (str): Path to the fixed image file (e.g., 'fixed.nii.gz'). + transform_matrix_path (str): Path to the transform matrix file (e.g., 'transform.mat'). + registered_image_path (str): Path to save the resampled moving image (e.g., 'moving_registered.nii.gz'). + + Returns: + None + """ + # Read the transform matrix from the file + transform_matrix = np.loadtxt(transform_matrix_path) + # print(f'transform_matrix: {transform_matrix}') + + # Create an affine transform + transform = sitk.AffineTransform(3) + transform.SetMatrix(transform_matrix[:3, :3].flatten()) + transform.SetTranslation(transform_matrix[:3, 3]) + + # Load the moving and fixed images + fixed_image = sitk.ReadImage(fixed_image_path, sitk.sitkFloat32) + moving_image = sitk.ReadImage(moving_image_path, sitk.sitkFloat32) + + # Resample the moving image + resampler = sitk.ResampleImageFilter() + resampler.SetReferenceImage(fixed_image) + resampler.SetTransform(transform) + resampler.SetInterpolator(sitk.sitkLinear) + + # Apply the transformation + moving_resampled = resampler.Execute(moving_image) + + # Save the resampled moving image + sitk.WriteImage(moving_resampled, registered_moving_image_path) + + print(f"Resampling completed and saved as '{registered_moving_image_path}'") + +# Example usage: +# resample_image('moving.nii.gz', 'fixed.nii.gz', 'transform.mat', 'moving_registered.nii.gz') + + + + +# CODE TESTING: + +if __name__ == '__main__': + fixed_image_path = '/Users/arman/projects/register_image/data/nifti/fixed.nii.gz' + moving_image_path = '/Users/arman/projects/register_image/data/nifti/moving.nii.gz' + registered_moving_image_path = '/Users/arman/projects/register_image/data/nifti/moving_registered.nii.gz' + transform_matrix_path = '/Users/arman/projects/register_image/data/nifti/transform.mat' + + t1 = datetime.now() + register_images_rigid(fixed_image_path, moving_image_path, registered_moving_image_path, transform_matrix_path) + print(f'Registration computation time: {datetime.now() - t1}') + + nifti = nib.load(registered_moving_image_path) + imgshow(nifti) + + # Test results show that: + # 1. FLIRT takes ~2 minutes to complete. Will explore faster rigid registration strategies. It would be + # great if we can figure out what registration strategy Visage uses, because that takes ~10 seconds. + # 2. the registered image is a bit blurrier than the original image. Will explore better resampling strategies + # such as B-spline interpolation. diff --git a/archives/requirements_archive1.txt b/archives/requirements_archive1.txt new file mode 100644 index 0000000..6a6b195 --- /dev/null +++ b/archives/requirements_archive1.txt @@ -0,0 +1,10 @@ +argparse~=1.4.0 +setuptools~=72.1.0 +SimpleITK~=2.3.1 +numpy~=2.0.1 +matplotlib~=3.9.1 +nibabel~=5.2.1 + +monai~=1.3.2 # build doesn't work +torch~=2.4.0 # build doesn't work +nipype~=1.8.6 # build doesn't work diff --git a/images_register.py b/images_register.py index 9b69c22..9edf1a9 100644 --- a/images_register.py +++ b/images_register.py @@ -1,12 +1,25 @@ #!/usr/bin/env python +""" +Developed by Arman Avesta, MD, PhD +FNNDSC | Boston Children's Hospital | Harvard Medical School + +This module contains argument parsing and main function for the image_register plugin. +""" +# --------------------------------------------- ENVIRONMENT SETUP ----------------------------------------------------- +# Project imports: +from registration_tools import rigid_registration +# System imports: +from os.path import join from pathlib import Path from argparse import ArgumentParser, Namespace, ArgumentDefaultsHelpFormatter +from chris_plugin import chris_plugin -from chris_plugin import chris_plugin, PathMapper - +# Version: __version__ = '1.0.1' +# ---------------------------------------------- ARGUMENT PARSING ----------------------------------------------------- + DISPLAY_TITLE = r""" _ _ _ _ | | (_) (_) | | @@ -18,31 +31,39 @@ |_| |___/ |___/ """ - -parser = ArgumentParser(description='!!!CHANGE ME!!! An example ChRIS plugin which ' - 'counts the number of occurrences of a given ' - 'word in text files.', +parser = ArgumentParser(description='This plugin registers a moving 3D image (CT, MRI, PET, etc) onto another' + 'fixed image and saves the registered moving image as well as ' + 'the transformation matrix. The fixed, moving, and registered moving images ' + 'are all in NIfTI format.', formatter_class=ArgumentDefaultsHelpFormatter) -parser.add_argument('-w', '--word', required=True, type=str, - help='word to count') -parser.add_argument('-p', '--pattern', default='**/*.txt', type=str, - help='input file filter glob') parser.add_argument('-V', '--version', action='version', version=f'%(prog)s {__version__}') +parser.add_argument('--fixed_image', type=str, default='fixed_image.nii.gz', + help='relative path to the fixed image in relation to input folder') +parser.add_argument('--moving_image', type=str, default='moving_image.nii.gz', + help='relative path to the moving image in relation to input folder') +parser.add_argument('--registered_image', type=str, default='registered_image.nii.gz', + help='relative path to the registered image in relation to output folder') +parser.add_argument('--transform_matrix', type=str, default='transform.mat', + help='relative path to the transformation matrix in relation to output folder') # The main function of this *ChRIS* plugin is denoted by this ``@chris_plugin`` "decorator." # Some metadata about the plugin is specified here. There is more metadata specified in setup.py. # # documentation: https://fnndsc.github.io/chris_plugin/chris_plugin.html#chris_plugin + +# ------------------------------------------- ChRIS PLUGIN WRAPPER ---------------------------------------------------- @chris_plugin( parser=parser, - title='Multiple image registration', - category='', # ref. https://chrisstore.co/plugins - min_memory_limit='100Mi', # supported units: Mi, Gi - min_cpu_limit='1000m', # millicores, e.g. "1000m" = 1 CPU core - min_gpu_limit=0 # set min_gpu_limit=1 to enable GPU + title='Images registration', + category='3D image processing', # ref. https://chrisstore.com/plugins + min_memory_limit='1Gi', # supported units: Mi, Gi + min_cpu_limit='1000m', # millicores, e.g. "1000m" = 1 CPU core + min_gpu_limit=0 # set min_gpu_limit=1 to enable GPU ) +# ----------------------------------------------- MAIN FUNCTION ------------------------------------------------------- + def main(options: Namespace, inputdir: Path, outputdir: Path): """ *ChRIS* plugins usually have two positional arguments: an **input directory** containing @@ -53,25 +74,16 @@ def main(options: Namespace, inputdir: Path, outputdir: Path): :param inputdir: directory containing (read-only) input files :param outputdir: directory where to write output files """ - print(DISPLAY_TITLE) - # Typically it's easier to think of programs as operating on individual files - # rather than directories. The helper functions provided by a ``PathMapper`` - # object make it easy to discover input files and write to output files inside - # the given paths. - # - # Refer to the documentation for more options, examples, and advanced uses e.g. - # adding a progress bar and parallelism. - mapper = PathMapper.file_mapper(inputdir, outputdir, glob=options.pattern, suffix='.count.txt') - for input_file, output_file in mapper: - # The code block below is a small and easy example of how to use a ``PathMapper``. - # It is recommended that you put your functionality in a helper function, so that - # it is more legible and can be unit tested. - data = input_file.read_text() - frequency = data.count(options.word) - output_file.write_text(str(frequency)) + fixed_image_path = join(inputdir, options.fixed_image) + moving_image_path = join(inputdir, options.moving_image) + registered_image_path = join(outputdir, options.registered_image) + transform_matrix_path = join(outputdir, options.transform_matrix) + + rigid_registration(fixed_image_path, moving_image_path, registered_image_path, transform_matrix_path) +# ------------------------------------------------ EXECUTE MAIN ------------------------------------------------------- if __name__ == '__main__': main() diff --git a/registration_tools.py b/registration_tools.py new file mode 100644 index 0000000..797ec25 --- /dev/null +++ b/registration_tools.py @@ -0,0 +1,92 @@ +""" +Developed by Arman Avesta, MD, PhD +FNNDSC | Boston Children's Hospital | Harvard Medical School + +This module contains image registration functions. +""" + +# ----------------------------------------------- ENVIRONMENT SETUP --------------------------------------------------- +# Project imports: +from visualization_tools import imgshow + +# System imports: +import SimpleITK as sitk +import nibabel as nib +import os +from datetime import datetime + +# ---------------------------------------------- HELPER FUNCTIONS ----------------------------------------------------- + + +# ----------------------------------------------- MAIN FUNCTIONS ------------------------------------------------------ + +def rigid_registration(fixed_image_path, moving_image_path, registered_image_path, transform_matrix_path): + # Read the images + fixed_image = sitk.ReadImage(fixed_image_path, sitk.sitkFloat32) + moving_image = sitk.ReadImage(moving_image_path, sitk.sitkFloat32) + + # Initialize the transform + initial_transform = sitk.CenteredTransformInitializer(fixed_image, + moving_image, + sitk.Euler3DTransform(), + sitk.CenteredTransformInitializerFilter.GEOMETRY) + + # Set up the multi-resolution framework + registration_method = sitk.ImageRegistrationMethod() + registration_method.SetShrinkFactorsPerLevel(shrinkFactors=[4, 2, 1]) + registration_method.SetSmoothingSigmasPerLevel(smoothingSigmas=[2, 1, 0]) + registration_method.SmoothingSigmasAreSpecifiedInPhysicalUnitsOn() + + # Set up the registration components + registration_method.SetMetricAsMattesMutualInformation(numberOfHistogramBins=50) + registration_method.SetMetricSamplingStrategy(registration_method.RANDOM) + registration_method.SetMetricSamplingPercentage(0.01) + registration_method.SetInterpolator(sitk.sitkLinear) + + registration_method.SetOptimizerAsGradientDescent(learningRate=1.0, numberOfIterations=100, + convergenceMinimumValue=1e-6, convergenceWindowSize=10) + registration_method.SetOptimizerScalesFromPhysicalShift() + + registration_method.SetInitialTransform(initial_transform, inPlace=False) + registration_method.SetOptimizerScalesFromJacobian() + + # Execute the registration + final_transform = registration_method.Execute(fixed_image, moving_image) + + # print(f"Final metric value: {registration_method.GetMetricValue()}") + # print(f"Optimizer's stopping condition: {registration_method.GetOptimizerStopConditionDescription()}") + + # Apply the transform to the moving image + resampled_moving_image = sitk.Resample(moving_image, fixed_image, final_transform, sitk.sitkBSpline, 0.0, + moving_image.GetPixelID()) + + # Save the registered image + if os.path.exists(registered_image_path): + os.remove(registered_image_path) + sitk.WriteImage(resampled_moving_image, registered_image_path) + + # Save the transform matrix + if os.path.exists(transform_matrix_path): + os.remove(transform_matrix_path) + sitk.WriteTransform(final_transform, transform_matrix_path) + + +# -------------------------------------------------- CODE TESTING ----------------------------------------------------- + +if __name__ == '__main__': + fixed_image_path = '/Users/arman/projects/pl-images-register/data/nifti/fixed.nii.gz' + moving_image_path = '/Users/arman/projects/pl-images-register/data/nifti/moving.nii.gz' + registered_image_path = '/Users/arman/projects/pl-images-register/data/nifti/moving_registered.nii.gz' + transform_matrix_path = '/Users/arman/projects/pl-images-register/data/nifti/transform.mat' + + t1 = datetime.now() + rigid_registration(fixed_image_path, moving_image_path, registered_image_path, transform_matrix_path) + print(f'Registration computation time: {datetime.now() - t1}') + + fixed_image = nib.load(fixed_image_path) + moving_image = nib.load(moving_image_path) + registered_image = nib.load(registered_image_path) + imgshow(fixed_image) + imgshow(moving_image) + imgshow(registered_image) + diff --git a/requirements.txt b/requirements.txt index 645d77e..98f8294 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,12 @@ chris_plugin==0.4.0 +argparse~=1.4.0 +setuptools~=72.1.0 +SimpleITK~=2.3.1 +numpy~=2.0.1 +matplotlib~=3.9.1 +nibabel~=5.2.1 + +# Not requird anymore. Build doesn't work with these: +# monai~=1.3.2 +# torch~=2.4.0 +# nipype~=1.8.6 diff --git a/setup.py b/setup.py index 664ed20..a64a73e 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,43 @@ +""" +Developed by Arman Avesta, MD, PhD +FNNDSC | Boston Children's Hospital | Harvard Medical School + +This module sets up the image registration plugin. +""" + +# ----------------------------------------------- ENVIRONMENT SETUP --------------------------------------------------- +# Project imports: + + +# System imports: from setuptools import setup import re -_version_re = re.compile(r"(?<=^__version__ = (\"|'))(.+)(?=\"|')") +# ------------------------------------------------ HELPER FUNCTIONS --------------------------------------------------- + +def parse_requirements(file_path): + """ + Parse the requirements.txt file and return a list of requirements, ignoring comments. + + Parameters: + file_path (str): Path to the requirements.txt file. + + Returns: + list: List of package names. + """ + requirements = [] + with open(file_path, 'r') as file: + for line in file: + # Strip whitespace and newline characters + line = line.strip() + # Skip empty lines and comments + if not line or line.startswith('#'): + continue + # Split the line on the first occurrence of '~=' + package = line.split('~=')[0] + requirements.append(package) + + return requirements def get_version(rel_path: str) -> str: """ @@ -9,6 +45,7 @@ def get_version(rel_path: str) -> str: https://packaging.python.org/en/latest/guides/single-sourcing-package-version/ """ + _version_re = re.compile(r"(?<=^__version__ = (\"|'))(.+)(?=\"|')") with open(rel_path, 'r') as f: matches = map(_version_re.search, f) filtered = filter(lambda m: m is not None, matches) @@ -18,13 +55,15 @@ def get_version(rel_path: str) -> str: return version.group(0) +# ------------------------------------------------- MAIN FUNCTIONS ---------------------------------------------------- + setup( name='images-register', - version=get_version('images_register.py'), + version=get_version('images_register.py'), # version='1.0.0 description='A ChRIS plugin to do multiple image registration', author='FNNDSC', author_email='arman.avasta@childrens.harvard.edu', - url='https://github.com/FNNDSC/pl-images-regi', + url='https://github.com/FNNDSC/pl-images-register', py_modules=['images_register'], install_requires=['chris_plugin'], license='MIT', diff --git a/visualization_tools.py b/visualization_tools.py new file mode 100644 index 0000000..5b0366c --- /dev/null +++ b/visualization_tools.py @@ -0,0 +1,117 @@ +""" +Developed by Arman Avesta, MD, PhD +FNNDSC | Boston Children's Hospital | Harvard Medical School + +This module contains image re-orientation and visualization functions. +""" + +# ----------------------------------------------- ENVIRONMENT SETUP --------------------------------------------------- +# Project imports: + + +# System imports: +import nibabel as nib +import matplotlib.pyplot as plt + +# # Print configs: +# np.set_printoptions(precision=1, suppress=True) + +# ---------------------------------------------- HELPER FUNCTIONS ----------------------------------------------------- + +def reorient_nifti(nifti): + """ + Re-orients NIfTI to LAS+ system = standard radiology system. + Note that the affine transform of NIfTI file (from the MRI volume space to the scanner space) is also corrected. + + :param nifti: input NIfTI file. + :return: re-oriented NIfTI in LAS+ system. + + Notes: + ------ + nib.io_orientation compares the orientation of nifti with RAS+ system. So if nifti is already in + RAS+ system, the return from nib.io_orientation(nifti.affine) will be: + [[0, 1], + [1, 1], + [2, 1]] + If nifti is in LAS+ system, the return would be: + [[0, -1], # -1 means that the first axis is flipped compared to RAS+ system. + [1, 1], + [2, 1]] + If nifti is in PIL+ system, the return would be: + [[1, -1], # P is the 2nd axis in RAS+ hence 1 (not 0), and is also flipped hence -1. + [2, -1], # I is the 3rd axis in RAS+ hence 2, and is also flipped hence -1. + [0, -1]] # L is the 1st axis in RAS+ hence 0, and is also flipped hence -1. + Because we want to save images in LAS+ orientation rather than RAS+, in the code below we find axis 0 and + negate the 2nd column, hence going from RAS+ to LAS+. For instance, for PIL+, the orientation will be: + [[1, -1], + [2, -1], + [0, -1]] + This is PIL+ compared to RAS+. To compare it to LAS+, we should change it to: + [[1, -1], + [2, -1], + [0, 1]] + That is what this part of the code does: + orientation[orientation[:, 0] == 0, 1] = - orientation[orientation[:, 0] == 0, 1] + Another inefficient way of implementing this function is: + ################################################################################ + original_orientation = nib.io_orientation(nifti.affine) + target_orientation = nib.axcodes2ornt(('L', 'A', 'S')) + orientation_transform = nib.ornt_transform(original_orientation, target_orientation) + return nifti.as_reoriented(orientation_transform) + ################################################################################ + """ + orientation = nib.io_orientation(nifti.affine) + orientation[orientation[:, 0] == 0, 1] = - orientation[orientation[:, 0] == 0, 1] + return nifti.as_reoriented(orientation) + + + +def imgshow(nifti): + """ + This function shows a nifti image in LAS+ system. + """ + kwargs = dict(cmap='gray', origin='lower') + ndim = nifti.ndim + assert ndim in (2, 3), f'image shape: {nifti.shape}; imshow can only show 2D and 3D images.' + + if ndim == 2: + img = nifti.get_fdata() + plt.imshow(img.T, **kwargs) + plt.show() + + elif ndim == 3: + nifti = reorient_nifti(nifti) + img = nifti.get_fdata() + voxsize = tuple(float(dim) for dim in nifti.header.get_zooms()) + + midaxial = img.shape[2] // 2 + midcoronal = img.shape[1] // 2 + midsagittal = img.shape[0] // 2 + axial_aspect_ratio = voxsize[1] / voxsize[0] + coronal_aspect_ratio = voxsize[2] / voxsize[0] + sagittal_aspect_ratio = voxsize[2] / voxsize[1] + + axial = plt.subplot(1, 3, 1) + plt.imshow(img[:, :, midaxial].T, **kwargs) + axial.set_aspect(axial_aspect_ratio) + axial.set_title('axial') + + coronal = plt.subplot(1, 3, 2) + plt.imshow(img[:, midcoronal, :].T, **kwargs) + coronal.set_aspect(coronal_aspect_ratio) + coronal.set_title('coronal') + + sagittal = plt.subplot(1, 3, 3) + plt.imshow(img[midsagittal, :, :].T, **kwargs) + sagittal.set_aspect(sagittal_aspect_ratio) + sagittal.set_title('sagittal') + + plt.show() + + + +# -------------------------------------------------- CODE TESTING ----------------------------------------------------- + +if __name__ == '__main__': + nifti = nib.load('/Users/arman/projects/pl-image-register/data/nifti/fixed.nii.gz') + imgshow(nifti) \ No newline at end of file