diff --git a/.gitignore b/.gitignore index 0eacff49..778dbad4 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,8 @@ testing_data* *.swp data/brain_parcellation/OASIS data/u-net +data/csv_data +data/._csv_data dist build *.egg-info diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7300db82..2db3d041 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -36,12 +36,11 @@ testjob: # !!kill coverage in case of hanging processes - if pgrep coverage; then pkill -f coverage; fi + - export TF_CPP_MIN_LOG_LEVEL=2 # print system info - which nvidia-smi - nvidia-smi - pwd - - python -c "import tensorflow as tf; print tf.__version__" - - python -c "import tensorflow as tf; from tensorflow.python.client import device_lib; print device_lib.list_local_devices()" - ls -la /dev | grep nvidia # removing existing testing data @@ -57,14 +56,16 @@ testjob: - mkdir -p testing_data - tar -xzvf testing_data_v0_3.tar.gz -C testing_data - tar -xzvf testing_code_v0_3.tar.gz -C testing_data + - wget -N https://www.dropbox.com/s/gt0hm6o61rlsfcc/csv_data.tar.gz + - tar -C data -xzvf csv_data.tar.gz - #### python 3 tests ################################### + #### python 2 tests ################################### # save NiftyNet folder path just in case - export niftynet_dir=$(pwd) # create a virtual env to dev-test - - venv="niftynet-dev-test-py3" - - mypython=$(which python3) + - venv="niftynet-dev-test-py2" + - mypython=$(which python2) - virtualenv -p $mypython $venv - cd $venv - venv_dir=$(pwd) @@ -75,6 +76,8 @@ testjob: - cd $niftynet_dir - pip install -r requirements-gpu.txt + - python -c "import tensorflow as tf; print(tf.__version__)" + - python -c "import tensorflow as tf; from tensorflow.python.client import device_lib; print(device_lib.list_local_devices())" - echo $(python tests/get_gpu_index.py) - export CUDA_VISIBLE_DEVICES=$(python tests/get_gpu_index.py) @@ -125,7 +128,7 @@ testjob: - python net_classify.py train -c testing_data/test_classification.ini - python net_classify.py inference -c testing_data/test_classification.ini - - python net_classify.py evaluation -c testing_data/test_classification.ini + #- python net_classify.py evaluation -c testing_data/test_classification.ini - python net_regress.py train -c config/default_monomodal_regression.ini --batch_size=1 --name toynet --max_iter 10 - python net_regress.py inference -c config/default_monomodal_regression.ini --batch_size=7 --name toynet --spatial_window_size 84,84,84 @@ -140,20 +143,20 @@ testjob: - python net_autoencoder.py inference -c config/vae_config.ini --inference_type encode-decode - python -m tests.test_model_zoo - - python -m unittest discover -s "tests" -p "*_test.py" + # - python -m unittest discover -s "tests" -p "*_test.py" # deactivate virtual environment - deactivate - cd $niftynet_dir - ###############end of python3 + ###############end of python2 - ######### Python 2 ###################### run python2 code with coverage wrapper + ######### Python 3 ###################### run python3 code with coverage wrapper # save NiftyNet folder path just in case - export niftynet_dir=$(pwd) # create a virtual env to dev-test - - venv="niftynet-dev-test-py2" - - mypython=$(which python2) + - venv="niftynet-dev-test-py3" + - mypython=$(which python3) - virtualenv -p $mypython $venv - cd $venv - venv_dir=$(pwd) @@ -197,7 +200,7 @@ testjob: - coverage run -a --source . net_classify.py train -c testing_data/test_classification.ini - coverage run -a --source . net_classify.py inference -c testing_data/test_classification.ini - - coverage run -a --source . net_classify.py evaluation -c testing_data/test_classification.ini + # - coverage run -a --source . net_classify.py evaluation -c testing_data/test_classification.ini - coverage run -a --source . net_regress.py train -c config/default_monomodal_regression.ini --max_iter 10 --name toynet --batch_size=2 - coverage run -a --source . net_run.py train -a net_regress -c config/default_monomodal_regression.ini --max_iter 10 --name toynet --batch_size=2 @@ -222,7 +225,7 @@ testjob: # deactivate virtual environment - deactivate - cd $niftynet_dir - ###############end of python2 + ###############end of python3 - echo 'finished test' tags: - gift-little @@ -232,6 +235,7 @@ quicktest: except: - master - dev + - fixes-unit-tests - tags - 147-revise-contribution-guidelines-to-include-github - 150-properly-format-the-bibtex-entry-to-the-ipmi-2017-paper-on-the-main-readme @@ -265,6 +269,8 @@ quicktest: - wget -q https://www.dropbox.com/s/5p5fdgy053tgmdj/testing_data_v0_3.tar.gz - mkdir -p testing_data - tar -xzvf testing_data_v0_3.tar.gz -C testing_data + - wget -N https://www.dropbox.com/s/gt0hm6o61rlsfcc/csv_data.tar.gz + - tar -C data -xzvf csv_data.tar.gz ######### Python 2 ###################### run python2 code with coverage wrapper # save NiftyNet folder path just in case @@ -312,15 +318,18 @@ pip-installer: only: - master - dev - - release-v0.5.0 - tags + - fixes-dependencies script: + - export TF_CPP_MIN_LOG_LEVEL=2 # get the shortened version of last commit's hash - LAST_COMMIT=$(git rev-parse --short HEAD) # source utils - source ci/utils.sh # following three lines copied over from dev script: - ls -la /dev | grep nvidia + # install TF + - pip install tensorflow-gpu==1.13.2 - echo $(python tests/get_gpu_index.py) - export CUDA_VISIBLE_DEVICES=$(python tests/get_gpu_index.py) # create a Python file that will import all available packages from the pip installer @@ -357,7 +366,7 @@ pip-installer: - set -e - cd $venv_dir # install TF - - pip install tensorflow-gpu==1.12 + - pip install tensorflow-gpu==1.13.2 # install using built NiftyNet wheel - pip install $niftynet_wheel # install SimpleITK for package importer test to work properly @@ -408,10 +417,10 @@ pip-installer: - net_classify train -c extensions/testing/test_classification.ini - net_classify inference -c extensions/testing/test_classification.ini - - net_classify evaluation -c extensions/testing/test_classification.ini + #- net_classify evaluation -c extensions/testing/test_classification.ini - net_run --app net_classify train -c extensions/testing/test_classification.ini - net_run --app net_classify inference -c extensions/testing/test_classification.ini - - net_run --app net_classify evaluation -c extensions/testing/test_classification.ini + #- net_run --app net_classify evaluation -c extensions/testing/test_classification.ini - net_regress train -c $niftynet_dir/config/default_monomodal_regression.ini --max_iter 10 --name toynet --batch_size=2 - net_regress inference -c $niftynet_dir/config/default_monomodal_regression.ini --name toynet --spatial_window_size 84,84,84 --batch_size 7 @@ -461,7 +470,7 @@ pip-installer: - set -e - cd $venv_dir # install TF - - pip install tensorflow-gpu==1.12 + - pip install tensorflow-gpu==1.13.2 # install using built NiftyNet wheel - pip install $niftynet_wheel # install SimpleITK for package importer test to work properly @@ -482,10 +491,10 @@ pip-installer: - net_classify train -c extensions/testing/test_classification.ini - net_classify inference -c extensions/testing/test_classification.ini - - net_classify evaluation -c extensions/testing/test_classification.ini + #- net_classify evaluation -c extensions/testing/test_classification.ini - net_run --app net_classify train -c extensions/testing/test_classification.ini - net_run --app net_classify inference -c extensions/testing/test_classification.ini - - net_run --app net_classify evaluation -c extensions/testing/test_classification.ini + #- net_run --app net_classify evaluation -c extensions/testing/test_classification.ini - net_regress train -c $niftynet_dir/config/default_monomodal_regression.ini --max_iter 10 --name toynet --batch_size=2 - net_regress inference -c $niftynet_dir/config/default_monomodal_regression.ini --name toynet --spatial_window_size 84,84,84 --batch_size 7 @@ -537,6 +546,8 @@ pip-camera-ready: - export check_version_venv=check_version_venv - virtualenv $check_version_venv - source $check_version_venv/bin/activate + # install TF + - pip install tensorflow-gpu==1.13.2 - pip install -r requirements-gpu.txt - pip install $niftynet_wheel - net_segment --version 2>&1 | grep -E 'NiftyNet.*version.*[0-9]+\.[0-9]+\.[0-9]+' | grep -v + diff --git a/CHANGELOG.md b/CHANGELOG.md index e4b6d88b..07d71dab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,38 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +## [0.6.0] - 2019-10-09 +### Added +* isotropic random scaling option +* volume padding with user-specified constant +* subpixel layer for superresolution +* various loss functions for regression (smooth L1 loss, cosine loss etc.) +* handler for early stopping mechanism +* aggregator with multiple outputs including labels in CSV +* nnUNet, an improved version of UNet3D +* data augmentation with mixup and mixmatch +* documentation contents +* demo for learning rate scheduling +* demo for deep boosted regression +* initial integration of NiftyReg Resampler +* initial integration of CSV reader + +### Fixed +* issue of loading binary values of NIfTI file +* various fixes in CI tests +* prefix name for aggregators +* various improvements in error messages +* issue of batch indices in the conditional random field +* issue of location selection in the weighted sampler +* model zoo: compatibility upgrade +* model zoo: new decathlon hippocampus dataset + +### Changed +* feature normalisation types options: instance norm, group norm, batch norm +* convolution with padding option +* various documentation and docstrings +* defaulting to remove length one dimensions when saving a 5D volume + ## [0.5.0] - 2019-02-04 ### Added * Version controlled model zoo with git-lfs @@ -115,7 +147,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Fixed * Bugs in data augmentation, I/O, sampler -[Unreleased]: https://github.com/NifTK/NiftyNet/compare/v0.5.0...HEAD +[Unreleased]: https://github.com/NifTK/NiftyNet/compare/v0.6.0...HEAD +[0.6.0]: https://github.com/NifTK/NiftyNet/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/NifTK/NiftyNet/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/NifTK/NiftyNet/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/NifTK/NiftyNet/compare/v0.2.2...v0.3.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index df13dbba..39c2bea9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -284,7 +284,7 @@ Steps to release a new version: 1. Push the release note changes to a new branch `releasing-x`; 1. Send a pull request from `releasing-x` to `dev`; 1. Check CI tests outcome, check changelog, accept the pull request; -1. Tag the latest commit of `dev`; +1. Tag the latest commit of `dev` (make sure that commit is not [skip][gitlab-ci-skip]ped, as this will subsequently [skip the tag build][tag-ci-skip-issue]); 1. Once the tag has been pushed to GitHub, run [chandler][chandler] to synchronise the changelog with the published release on GitHub 1. the `pip stage` will be triggered in CI, there should be a wheel ready; 1. Publish the pip wheel on [PyPI test server][pypi-test]; @@ -292,6 +292,8 @@ Steps to release a new version: 1. Push pip wheel to release (warning: not revertible); 1. Merge `dev` to `master` (archiving the new version). +[tag-ci-skip-issue]: https://gitlab.com/gitlab-org/gitlab/issues/18798 +[gitlab-ci-skip]: https://docs.gitlab.com/ee/ci/yaml/README.html#skipping-jobs [chandler]: https://github.com/mattbrictson/chandler [pep440]: https://www.python.org/dev/peps/pep-0440/ [changelog]: CHANGELOG.md diff --git a/README.md b/README.md index 4108fc07..ad5fb964 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ HIG -- High-dimensional Imaging Group, UCL), where BMEIS acts as the consortium ### Installation 1. Please install the appropriate [TensorFlow][tf] package*: - * [`pip install tensorflow-gpu==1.12`][tf-pypi-gpu] for TensorFlow with GPU support - * [`pip install tensorflow==1.12`][tf-pypi] for CPU-only TensorFlow + * [`pip install "tensorflow-gpu>=1.13.2, <=1.14"`][tf-pypi-gpu] for TensorFlow with GPU support + * [`pip install "tensorflow>=1.13.2, <=1.14"`][tf-pypi] for CPU-only TensorFlow 1. [`pip install niftynet`](https://pypi.org/project/NiftyNet/) All other NiftyNet dependencies are installed automatically as part of the pip installation process. diff --git a/demos/Learning_Rate_Decay/Demo_applications/decay_lr_comparison_application.py b/demos/Learning_Rate_Decay/Demo_applications/decay_lr_comparison_application.py new file mode 100644 index 00000000..48b69850 --- /dev/null +++ b/demos/Learning_Rate_Decay/Demo_applications/decay_lr_comparison_application.py @@ -0,0 +1,87 @@ +import tensorflow as tf + +from niftynet.application.segmentation_application import \ + SegmentationApplication +from niftynet.engine.application_factory import OptimiserFactory +from niftynet.engine.application_variables import CONSOLE +from niftynet.engine.application_variables import TF_SUMMARIES +from niftynet.layer.loss_segmentation import LossFunction + +SUPPORTED_INPUT = set(['image', 'label', 'weight']) + + +class DecayLearningRateApplication(SegmentationApplication): + REQUIRED_CONFIG_SECTION = "SEGMENTATION" + + def __init__(self, net_param, action_param, is_training): + SegmentationApplication.__init__( + self, net_param, action_param, is_training) + tf.logging.info('starting decay learning segmentation application') + self.learning_rate = None + self.current_lr = action_param.lr + if self.action_param.validation_every_n > 0: + raise NotImplementedError("validation process is not implemented " + "in this demo.") + + def connect_data_and_network(self, + outputs_collector=None, + gradients_collector=None): + data_dict = self.get_sampler()[0][0].pop_batch_op() + image = tf.cast(data_dict['image'], tf.float32) + net_out = self.net(image, self.is_training) + + if self.is_training: + with tf.name_scope('Optimiser'): + self.learning_rate = tf.placeholder(tf.float32, shape=[]) + optimiser_class = OptimiserFactory.create( + name=self.action_param.optimiser) + self.optimiser = optimiser_class.get_instance( + learning_rate=self.learning_rate) + loss_func = LossFunction( + n_class=self.segmentation_param.num_classes, + loss_type=self.action_param.loss_type) + data_loss = loss_func( + prediction=net_out, + ground_truth=data_dict.get('label', None), + weight_map=data_dict.get('weight', None)) + + loss = data_loss + reg_losses = tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES) + + if self.net_param.decay > 0.0 and reg_losses: + reg_loss = tf.reduce_mean( + [tf.reduce_mean(reg_loss) for reg_loss in reg_losses]) + loss = data_loss + reg_loss + grads = self.optimiser.compute_gradients(loss) + # collecting gradients variables + gradients_collector.add_to_collection([grads]) + # collecting output variables + outputs_collector.add_to_collection( + var=data_loss, name='loss', + average_over_devices=False, collection=CONSOLE) + outputs_collector.add_to_collection( + var=self.learning_rate, name='lr', + average_over_devices=False, collection=CONSOLE) + outputs_collector.add_to_collection( + var=data_loss, name='loss', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + else: + # converting logits into final output for + # classification probabilities or argmax classification labels + SegmentationApplication.connect_data_and_network( + self, outputs_collector, gradients_collector) + + def set_iteration_update(self, iteration_message): + """ + This function will be called by the application engine at each + iteration. + """ + current_iter = iteration_message.current_iter + if iteration_message.is_training: + if current_iter > 0 and current_iter % 5 == 0: + self.current_lr = self.current_lr / 1.02 + iteration_message.data_feed_dict[self.is_validation] = False + elif iteration_message.is_validation: + iteration_message.data_feed_dict[self.is_validation] = True + iteration_message.data_feed_dict[self.learning_rate] = self.current_lr diff --git a/demos/Learning_Rate_Decay/Demo_applications/no_decay_lr_comparison_application.py b/demos/Learning_Rate_Decay/Demo_applications/no_decay_lr_comparison_application.py new file mode 100644 index 00000000..70800fe4 --- /dev/null +++ b/demos/Learning_Rate_Decay/Demo_applications/no_decay_lr_comparison_application.py @@ -0,0 +1,85 @@ +import tensorflow as tf + +from niftynet.application.segmentation_application import \ + SegmentationApplication +from niftynet.engine.application_factory import OptimiserFactory +from niftynet.engine.application_variables import CONSOLE +from niftynet.engine.application_variables import TF_SUMMARIES +from niftynet.layer.loss_segmentation import LossFunction + +SUPPORTED_INPUT = set(['image', 'label', 'weight']) + + +class DecayLearningRateApplication(SegmentationApplication): + REQUIRED_CONFIG_SECTION = "SEGMENTATION" + + def __init__(self, net_param, action_param, is_training): + SegmentationApplication.__init__( + self, net_param, action_param, is_training) + tf.logging.info('starting decay learning segmentation application') + self.learning_rate = None + self.current_lr = action_param.lr + if self.action_param.validation_every_n > 0: + raise NotImplementedError("validation process is not implemented " + "in this demo.") + + def connect_data_and_network(self, + outputs_collector=None, + gradients_collector=None): + data_dict = self.get_sampler()[0][0].pop_batch_op() + image = tf.cast(data_dict['image'], tf.float32) + net_out = self.net(image, self.is_training) + + if self.is_training: + with tf.name_scope('Optimiser'): + self.learning_rate = tf.placeholder(tf.float32, shape=[]) + optimiser_class = OptimiserFactory.create( + name=self.action_param.optimiser) + self.optimiser = optimiser_class.get_instance( + learning_rate=self.learning_rate) + loss_func = LossFunction( + n_class=self.segmentation_param.num_classes, + loss_type=self.action_param.loss_type) + data_loss = loss_func( + prediction=net_out, + ground_truth=data_dict.get('label', None), + weight_map=data_dict.get('weight', None)) + + loss = data_loss + reg_losses = tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES) + + if self.net_param.decay > 0.0 and reg_losses: + reg_loss = tf.reduce_mean( + [tf.reduce_mean(reg_loss) for reg_loss in reg_losses]) + loss = data_loss + reg_loss + grads = self.optimiser.compute_gradients(loss) + # collecting gradients variables + gradients_collector.add_to_collection([grads]) + # collecting output variables + outputs_collector.add_to_collection( + var=data_loss, name='dice_loss', + average_over_devices=False, collection=CONSOLE) + outputs_collector.add_to_collection( + var=self.learning_rate, name='lr', + average_over_devices=False, collection=CONSOLE) + outputs_collector.add_to_collection( + var=data_loss, name='dice_loss', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + else: + # converting logits into final output for + # classification probabilities or argmax classification labels + SegmentationApplication.connect_data_and_network( + self, outputs_collector, gradients_collector) + + def set_iteration_update(self, iteration_message): + """ + This function will be called by the application engine at each + iteration. + """ + current_iter = iteration_message.current_iter + if iteration_message.is_training: + iteration_message.data_feed_dict[self.is_validation] = False + elif iteration_message.is_validation: + iteration_message.data_feed_dict[self.is_validation] = True + iteration_message.data_feed_dict[self.learning_rate] = self.current_lr diff --git a/demos/Learning_Rate_Decay/Demo_for_learning_rate_decay_application.ipynb b/demos/Learning_Rate_Decay/Demo_for_learning_rate_decay_application.ipynb new file mode 100644 index 00000000..e847a7b8 --- /dev/null +++ b/demos/Learning_Rate_Decay/Demo_for_learning_rate_decay_application.ipynb @@ -0,0 +1,334 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Demo for Learning Rate Decay Application" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This demo will address how to make use of the learning rate scheduler in the context of a segmentation task. The scheduler allows for the learning rate to evolve predictably with training iterations (this will typically be a decay over time). This demo assumes you have a working installation of NiftyNet." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Preparation:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "1) Ensure you are in the NiftyNet root, and set this root as an environment variable:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os,sys\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "niftynet_path='your/niftynet/path' # Set your NiftyNet root path here\n", + "os.environ['niftynet_config_home'] = niftynet_path\n", + "os.chdir(niftynet_path)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "2) Acquire the data. We will make use of a publicly available hippocampus segmentation dataset in this demo. Prior to this you will need to create a file named 'config.ini' in your NiftyNet root folder with the following format:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```ini\n", + "[global]\n", + "home = /your/niftynet/path\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The following code snippet will create the file for you:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "f = open(\"config.ini\", \"w+\")\n", + "f.write(\"[global]\\n\")\n", + "f.write(\"home = {}\".format(niftynet_path))\n", + "f.close()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now you are ready to download the data which you can do by running the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "%run net_download.py decathlon_hippocampus -r" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The training data, labels, and test data will be downloaded into the */data/decathlon_hippocampus/* folder in your NiftyNet root directory." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The configuration file:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The configuration file in NiftyNet specifies all the parameters pertinent to the training/ inference task at hand including but not limited to: Training data location, network of choice, learning rate, etc.
\n", + "\n", + "In this instance a configuration file has been provided (learning_rate_demo_train_config.ini) with default settings for a segmentation training task. Note that these settings can be overriden in the command line or by simply editing the configuration file. For further information regarding the configuration file and the individual variables refer to the relevant documentation [here](https://niftynet.readthedocs.io/en/latest/config_spec.html)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Training a network from the command line:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To run this application from the command line using the variables specified in the configuration file:\n", + "\n", + "```python net_run.py train -a niftynet.contrib.learning_rate_schedule.decay_lr_application.DecayLearningRateApplication \n", + " -c /demos/Learning_Rate_Decay/learning_rate_demo_train_config.ini --max_iter 90```\n", + "\n", + "With the current setup, the learning rate will halve every three iterations. NiftyNet will create *logs* and *models* folders to store training logs and models, respectively.
\n", + "\n", + "The following cells exemplify the potential benefit of having a decaying learning rate: Two networks are trained, one lacks any learning rate scheduling, the other has a learning rate schedule that decays the learning rate by 2% every 5 iterations. By looking at the loss curves we can see that the latter converges towards a lower cross entropy despite the exact same initialisation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import sys\n", + "import niftynet\n", + "sys.argv=['','train','-a','demos.Learning_Rate_Decay.Demo_applications.decay_lr_comparison_application.DecayLearningRateApplication','-c',os.path.join('demos','Learning_Rate_Decay','learning_rate_demo_train_config.ini'),'--max_iter','500','--lr','25.0','--model_dir','./models/decay']\n", + "niftynet.main()\n", + "sys.argv=['','train','-a','demos.Learning_Rate_Decay.Demo_applications.no_decay_lr_comparison_application.DecayLearningRateApplication','-c',os.path.join('demos','Learning_Rate_Decay','learning_rate_demo_train_config.ini'),'--max_iter','500','--lr','25.0','--model_dir','./models/no_decay']\n", + "niftynet.main()" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAApkAAAFICAYAAAAbJeVFAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAMTQAADE0B0s6tTgAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4yLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvOIA7rQAAIABJREFUeJzsnXl8FPX9/1+fECAgyBnlDiQhCSaARFA5VEBC0Xqh9aKKR/ttre2vHm1tbb/9evXbWmv9WrWtVato8USrRRqQSxAQBEHwBBIOIeE+wxUCyfz+eM2nOyx7zOzO7Ozxfj4e+5hkzs/uzs685n0qwzAgCIIgCIIgCG6S5fcABEEQBEEQhPRDRKYgCIIgCILgOiIyBUEQBEEQBNcRkSkIgiAIgiC4johMQRAEQRAEwXVEZAqCIAiCIAiuIyJTEARBEARBcB0RmYLgI0qpvkqpp5RSXyqlDiml6pVSNUqpZeb8q/weox8opUYqpQyl1Dy/xwIASqmbzfFMcrBNb3Objd6NTBAEIXkRkSkIPqGUuhLAZwB+COA0AIsAvAXgUwDdzfl/822AHqKU2mgKsN5+j0UQBEHwhmy/ByAImYhS6nQALwJoCeCPAP7bMIz6oHXOAvAtH4YnCIIgCHEjIlMQ/OESAG0AbDEM46ehVjAMYzmA5QkdlSAIgiC4hLjLBcEfTjenO51uaHU1K6UuUkrNU0rtV0rtVUpNU0r1t6w7QSm1WCl1QCm1Tyn1T6VUQYR9n62UekMptUUp1aCU2qGUelcpVRFhm2yl1G1KqQ/NcdQrpaqUUk8opboHrXuzUsoAkGfO2mC+F/0aGWL/zZVSP1dKfaGUOqKU2m2+j34RxtRBKfWAUmql+d4PK6U+U0r9t1KqdYT3cae5Xr1SaqdS6i3r5+kmSqkeSqknzc+q3vzsFimlvq+UahZmm6uVUrPNz+CYOf1SKfWsUmpA0LrtlFK/Md/PIaXUUfN7XaSUelAp1dzheFubn89C81w7qpT62jw/JgStGzEcQik1yVx+c7j5SqkypdTrSqmtSqlGpdT9SqnfmcufjjDOMnOd7cHvUSnVTSn1mFLqK/OcOKAY//wjpdRJRhelVEul1M+UUsvNdRuUUtvMbR5RSnV08BEKQuZhGIa85CWvBL8A3ADAAHAcwIUOt91obvs7AE0AFgJ4HcAac/5eAAUAHgFwDMAcAFMAbDKX1wLoEGK//wWg0VxnBYBXwDhRw3zdF2KblgBmmcuPAKgE8JrlWDsBlFvWHwFgEoCD5vI3zf/1q8Rcb6S5fJG5/0MAppvr633vBdA7xJjOsKyzxdxuKoBt5rxPALQL2iYLwNvm8qMA3jPfxwbzff3ZXDbJwffU29xmY4hlQwDsNpd/bR5runksA8AMAC2Ctvkfc9kxAPPN7+ffYFxvE4A7Leu2NucbAHaY7/9VAO8D2GrOb+/gvfQE8IW53SEAM839fQBgX/B7ROAcPen7MZdPMpffHGb+MwDqzc//dXP8PwFQZPnuc8Ls+4/mOn8Mmn8+gD3msg0A/mV+znreewCaB50Ts81l+8Fz+xXzfNTv70y/ryXyklcyv3wfgLzklYkv0FVeY96omsyb/38DuBhAbpRt9Q2uHhaBCqAZgDfMZZ8B2AVgoGV5awRE46+C9tnfFC9NAG4MWnYRKLwMABVByx4251dbBQWA5gCeM5etx8mCKZoIGYmAuF0BoItlWY4pDgwAfwvarpU5FgPAQ9bjmu//FXPZ80Hb/dCcvw1AP8v8bAB/sYxlkoPvuDdCiExQmOv3/9cgYZNvCiADwP8GbXMYwAEAxSGOlQdToJv/TzT3UWndv7ksC8AFwd9JhPeRBWAZAkIsN2h5DoCLHX6/kxBZZOqHqKwQ2y40l18XYlk2gO3m8jLL/C7g76EJwA+s+wXQCXwQMwD8j2X++Zbzr22IYw0G0MnN64K85JVuL98HIC95ZeoLQDGAJZabqvX1CYDbADQLsZ2+gT8SYtkgyz5uD7H8SnPZ3KD5WhC+FWasT5rLZ1rm5ZiixwBwaYhtWiNgPZwQ5j30DnO8kQgI8IEhlp9jLl8XNP82c/67YfbbxhQhx2Cx5gKoMre7LcQ2OQhY/yY5+H57I7TI1FbsWgAtQ2x3lbm8Dqa1DkCuOW+VzWP/zFz/LhfO08sRsAq3sblNtO93EiKLzDWhzn1znVvNdd6LMNZlQfP1w9CTYfbZHUADaPVV5ryrzW3+FO9nKC95ZepLYjIFwScMw1hjGMa5oGB6ELQS6RjNM0Er1wylVIswu6gMMa/K5vJuQfNHmtNJYY71d3N6niVecDAo2vYYhvFu8AaGYRwG3cAAMCrMfqOxyTCMVSHmf2VOuwfN/6Y5fT3UzgzDOAjgY9DiNQQAzLjRQnOVySG2qQctxG4x0py+ZhjG0RDL/wm6g9sCOMscw05QuA1QSv1RKXVGlGMsM6f3KKUmxhk7OM6cvmJ+fongHcMwGsMsewN02Y9RSvUIWnaLOX0+aH6086IW/G3kAuhrzl4Bho/cqpT6oVKqq4PxC4IASfwRBN8xDGOpYRj3GYYxDkwIOgsBcTYGwB1hNt0UYl8HIy0HLY8ArXNWtFjbEOZY6yzbdbK5jXW7YDFol1DvAYZh1Jl/tgxalG9O/xGUUPSfFxiSAFBQAIAWKrsiiKhI79EpET83wzAMyzLr5zYRtLTdDeALM+mnUil1l1Kqc9A+5gH4PVh/9UUAu5RSa5RSzyulLldKObn255nT1Q62iZeN4RaY39EU8P41Uc9XSp0Gisl6MF7Uij4vFkQ4L7RwzzWPsw7AXWDox1MAtpgJTa8qpb4d4eFPEAQTKWEkCEmEKTBWALjezIK+DMAVAP4QYvWmKPuKuDxFcPoetHiaAbrFI/G18+H4h2EYC8xs7W+CMZXDAHwDjJl9QCk13jCMOZb1f2FmYV8KJlwNBy19twBYppQaZRjGocS+i/8QTeQeibL8eQA3A7gJwG/NeTeA97Q3DcPYF+Z4b4JW0Ejs1n8YhvGkUuoN8Hc4wnxdZ74eUEqdZxjG1ij7E4SMRUSmICQvM8GbW+doK7pALZiRng/g8xDLtSWoHszG1dsAQJ8I+9Xb1UZYx002AygB8HfDMN60uY0eW2elVJsw1szebgwu6Hj5EdbRn+kJn5thGEdAofQmACilcgH8BsD3QOGVF7T+RjCe9klz/SFgSMAQAPcAuM/GeLU1ucTGupoGc9o2zPK8MPNtYQruagBFSqnhhmEsAkUncLKrHOB50RfA7w3D+NjhsbYDeNZ8QSlVYh5jKBjreVNMb0IQMgBxlwuCDyillI3VepnTGi/HYjLPnN4cZvmt5nSBYRjHzb8/BksRdVRKXRa8gVKqFWjxAZg9b0WLELcfdKeb02vsbmAYRg2YAQ8AE4KXK6VagkkgbjHPnF6rlAoOW4BSajyADmBoQ8Ri/Gas5j3mv72UUh2irL8MzJYHGPdrhxnm9Hql1Ck2t9Hi+KRapkqpLgDKbe4nEi+Y05sVu2P1B8XknBDrOj4vwmEYxmowFAGw/xkKQkYiIlMQ/OF2pdSLSqlhwQsUuRLAj8xZrwWv4wF/Amt2XqGUuiFoPGMBfN/891E930yI+bP57x+VUnmWbZqb++wCxhcGWxW1cC516w2YPAO6wa9WSv1eKXWSJU0p1UUp9V9Bsx83p/ebliq9bjPwPQcnSsWDrlnaDcBj1iLgSqk+YJ1HgJnQ9eb8PKXUd5VSp4bY36XmdC+YkQ6l1Hil1PnBsZfm96ITeeyGC0wFqx10AzBFKdXJulAplaOUuihom9nm9OdKqfaWdXMBvAQmjMXLi2A4xTVgCSoAeDFMmMgfwHqedyulfhIqnlIp1cd67iulRiulLg5R0F2BHbuAFAu5EISE43d6u7zklYkvAHciUGpoB5hZ/jJYXHuDZdk/EFQrENHLwxgwwztDLOuN8AXCv4dAMfbl5ngWgjdyA+GLseuC1YfN8b8G3nwNsDbhWSG203UpDwB4Cyyh9BzMGpAIlDCaF+EzDPk+QeGqP8O9YOHyl8Fi61+Y72db0DZZoJgywJqgM8DkkfVgfKCulTnJwXcc6bO2FmPfaH5m/0aYYuygxcwALcBLwSzp18H4XV3q6TuW9R835+8Ewy4mg8XHdQ3JGgA9HLyXPDDxxwBjGt8Da47OR+hi7O0t5+l2AO+ARcz3AfgUgcL3NwdtNynU/Ajjmo7Ab6UJQH6Edc83Pw89pjnm5/IuArVVl4T4je4HLfEvg5n/+n3tgxRjl5e8Ir58H4C85JWJLzBW7XIATwD4CHTzNYBCrdq8gY8Ls62+yfUOszwmkWkuPwe0tG0Fa0nuAjANQUXYg7bJBgtcLwYtaUfN9/AEgO5htskC8Asw/lMLKwPASHP5SMQoMi2f788AfAgKzQawzuNSsBPS0DDv425QiNab7/0dAAPBMALXRKa5vCeYtbzO/MzqzPHeBiA7xPu5wxQ5a0FxfhCsJ/kigoQ8KEp/B2ABKCiPgg8zHwO4FzEUEQetj/eYn2Gd+RltBMXrtSHW726Obbt5/PXmZ98G0etk3mxzTLqWZcRzxbL+aWC5sOWWc3Uz2KTgfgD9LesWgDGrs8GHpiNgPPIq87O1LdLlJa9Mfemis4IgCIIgCILgGhKTKQiCIAiCILiOiExBEARBEATBdURkCoIgCIIgCK4jIlMQBEEQBEFwHRGZgiAIgiAIguskTVvJli1bGrm5uX4PQxAEQRAEQQhDbW1tg2EYLe2smzQiMzc3FzU1ieieJwiCIAiCIMSCUmqn3XXFXS4IgiAIgiC4johMQRAEQRAEwXVEZAqCIAiCIAiuIyJTEARBEARBcB0RmYIgCIIgCILriMgUBEEQBEEQXEdEpiAIgiAIguA6IjIFQRAEQRAE1xGRKQiCIAiCILiOiExBEARBEATBdURkCoIgCIKQWJqagAEDgAce8HskgoeIyBQEQRAEIbHs2AF89hkwd67fIxE8RESmIAiCIAiJpaaG06+/9nccgqeIyBQEQRAEIbFs3sxpTQ1w/Li/YxE8Q0SmIAiCIAiJRVsyGxuBLVv8HYvgGSIyBUEQBEFILNqSCYjLPI0RkSkIgiAIQmLRlkxARGYaIyJTEARBEITEIiIzIxCRKQiCIAhCYtm8GejZk3+LyExbRGQKgiAIgpA4mpqA2lrgzDOB1q2BjRv9HpHgESIyBUEQBEFIHDt2AMeO0ZKZlyeWzDRGRKYgCIIgCIlDx2P26EGRuWkTYBj+jknwBBGZgiAIgiAkDl2+SIvM+npaN4W0Q0SmIAiCIAiJQ1sye/YEevfm3+IyT0tEZAqCIAiCkDiC3eWAiMw0JdvvAQiCIAiCkEFY3eXaTS4iMy0RS6YgCIIgCImjpgbo3BnIyRFLZpojIlMQBEEQhMRhLcTetSvQvLnUykxTRGQKgiAIgpAYdCH2Hj34f1YWBadYMtMSEZmCIAiCICQGXYhdi0xACrKnMSIyBUEQBEFIDNbyRZq8PKCuDti3z58xCZ4hIjOVOHKErga/qaoCXnvN71EIgiAIqYa1fJFGamWmLSIyU4UdOxgg/fjjfo8E+N3vgOuvlwuCIAiC4AxdvijYkgnIPSUNEZGZKkyfDuzfDyxY4PdIGLQNAMuW+TsOQRAEIbUIZckUkZm2iMhMFWbM4HT1an/HAQDbtnEqIlMQBEFwgrUQu0ZEZtoiIjMVaGwEZs7k39XVzMzzk+3bORWRKQiCIDjBWohd06MHoJTUykxDRGSmAh9/DOzZA7RsCRw/DmzY4N9YGhuBnTv59/LlyZGIJAiCIKQGmzefaMUEgBYtgG7d0tOS+eWXwJ13Ag0Nfo/EF0RkpgLaVT5hAqd+usx37gwIy7o6YO1a/8YiCIIgpA66ELs16UeTrrUyJ00C/vQn4P33/R6JL4jITAWmTwfatQNuvpn/r1nj31i0q7xfP04//ti/sQgnc+AA0L8/8Mwzfo9EEAThRHbuPLkQu6Z3by4/fDjhw/IUnei0eLG/4/AJEZnJzu7dwNKlwJgxQGkp5/lpydRJP5dcwqnEZSYXb74JfP458O67fo9EEAThREKVL9Lo5J9NmxI3nkSgq7GIyMwQ9u0Dnn8+db7wWbMAwwDGjQM6dWLAtJ8iU1syhw8HOnQQkZlsvPQSp8lQhUAQBMFKqPJFmnTNMNcic8mSjMxhsCUylVI5Sql3lFJrlVKrlFKzlFKFYda9RCm1WilVpZT6p1LqVHeHHCf79gHf+Q4wZYrfI7GHjsf8xjc4LSnx112uLZldugCDBwOffOJ/trtAvv4amDePf69fDxw96utwBEEQTiBU+SJNOopMwwiIzLo6JgFlGE4smc8AKDYMYyCAfwF4LngFpVQbAH8HcIVhGH0BbAHwazcG6hrdu7NUQiqY5JuaKDJLSwPuheJiutB37fJnTMEis74+I384ScnLL3M6cCDPnepqf8cjCIJgJVTfck06isw9e3iPPO00/p8qHlQXsSUyDcOoNwyj0jAMw5y1BEDvEKteBOATwzC0r+4vAK6Pe5Ru0rw52zPqJ6pk5tNP6Z4eNy4wr6SEU7+smdpdfvrpwJAh/Ftc5v5jGHSV5+YCP/4x54nLXBCEZELfd7t3P3lZr16cplOtTG3FvPJKTkVk2uYO0JoZTC8A1seQjQC6KqWyYzyON/TqlRqWTO0qt4rM4mJO/RIQ27Yx0z0nR0RmPBw+7G58zrJlfPCYMAEoK+M8EZmCICQTuhB7q1YnLzvlFC5LJ0umFpnl5UCfPsCHH/o7Hh9wLDKVUr8EUAjg3ngOrJS6WylVo18HDx6MZ3fO6NWLYinZY9ZmzABatwbOOy8wT1sy/RSZXbrw7+7d+beITGfU1fGz++Uv3dunTviZONH/BxFBEIRQ1NSEjsfUpFutTGui09ChNATs2ePvmBKMI5GplPopgCsBXGQYRqhiVpsA5Fn+7w1gq2EYx4NXNAzjMcMweuhXmzZtnAwlPnQ8iH7KSEbq6oBFi4DRo9npR9OnD13+frrLTz+dfyvFuMzPPmPciWCPFSuYgPbEE4HuSfHQ0AC8+ipjdwcNoqW5a1cRmUJ4jh8HbrwR+Oc//R6JkCk0NVF0hYrH1PTuDWzZkj7JpFpjdO8ODBvGv5cs8W88PmBbZCql7gbjKysMw9gXZrUZAMqVUqa5DbcDeC2+IXqAjv1IZpf53Lm8EVhd5QCQnQ0UFvojII4dY9KRtmQCdJkfPw6sWpX48aQq+rM6coRCM14qK/l0PHEihT9Ai/fq1YzVFIRg3nsPmDwZePZZv0ciZAqRCrFr8vICYjQdsIrMoUP5d4bFZdotYdQDwB8BtAfwvlJqpVLqI3PZg0qp2wDAMIwDAL4L4B2lVDWAHgAe8mTk8ZAKIjNUPKampIQlahLdC3XHDk6DRSYgLnMnrFzJaY8ewFNPsUtPPLz0EsXlt78dmFdSAhw8CGzdGt++hfTk+ec5/fxzf8chZA6Ryhdp0i3DvLaW+QsdOwIDBjD8LcPiMu1ml9cYhqEMwygwDONM83WOuex/DMN42rLuVMMwSgzDKDQM4wrDMPZ7NfhY2HFoBx468h5mFiB5M8wNgyKzsBAoKDh5eXEx0NgIrFuX2HHp8kXaXQ6IyIyFVauAbt2A++6j2zyeFpC7dwPTprEjlDVj0+/YXSF52bkz0BGqpgbYn1SXaMFrPvgA+Na3Ep+TEKl8kSbdRGZNTaBsYnY275dLl/L+nSFkXMefhsYG/M/ap/FuEZLXkrlmDX9koayYgH8CwlojU9O5M+NoRGTa49gx4IsvgDPPZExc167AY4/FfsF//XXuc+LEE+eLyBTC8fLLPGfOPJP/f/GFv+MREstvfgO89Vbi6xtnqiXT+vA/bBg9TBnkQcg4kdmtbTfkZOdgXees5BWZkVzlgH+1MnWNTKvIBJj8s3p1/G7fTGD1aoY5DBzIhK6772ag+z/+Edv+XnqJpT/Gjz9xvohMIRSGQVd5u3bAr37FeRl0w8t4duwA5szh324kHTrBiSUzHWplHjnCWHmryMzAuMyME5lZKgsFHQpQndssed3lM2YALVoAI0eGXu5XiZpQ7nKALgDDYNa0EBmd9DNwIKff/z7Qvj3wyCPOXShr1gAffQRcdRWFppUePRj/IyJTsLJiBatBXH89Hw4BsWRmEm++GajP65fIDFWIXdO+PdC2bXpYMrds4dRquT33XE4zKC4z40QmABR2LMSGNsdxfNPG5Mu+PXIEmD8fOP/8k4WDpn17Cr1kcJcDEpfpBJ30o12VbdsCP/oRUFUFvP22s31NnsxpsKscALKy+DAiIlOwohN+br2VCZBt2oglM5N4zVLsJdEic/Pm8IXYNUqlT63MUKI6N5e5FmLJTG8KOxbieJaBTdmHki/off581pwM5yrXlJTQkpVIkazd5boPq+asszgVkRmdVat4kS0sDMz78Y857+GH7X+fTU10sffoEd7iXVLCC3siGx0IyUt9PfDKK+wINXgwH0RKS0VkZgo1NcCCBQEvih+WzEjxmJrevXndcrMjmh9YyxdZGTYMqK5O/OfvExkrMgGguiOSz2U+fTqn0URmcTEzk3VZoUSwbRvQqROLwVs59VSOR0RmZAyDlswBA4BmzQLzc3OB73wHWL4cmD3b3r4WLODT/g03nLgvKzouc+3a+MYtpAfvvMNrxi23BOqplpbyGpIhN7yM5o03OP1//4/TRH7nuvalHZGZl8e4de05S1XCicwMi8sUkZlsyT8zZvCHeMYZkdfzI/nH2lIymCFDgA0bWFJHCM3WrcCuXQFLgpWf/IRi8eGH7e1Lt5G88cbw60jyj2DlhRdYRuWGGwLzdJ97ictMf157jQaBCRN4rUmkyNSF2CMl/WjSJcNci8xgYS0iM/0p6MDak0knMtevp9Vp3LiApSEcfiT/WFtKBqPjMj/+OHHjSTWC4zGt9O7Ni//cuayjFonDh4EpU+jyjPQwIiJT0GzaBMyaBVx66YnhLlpkiss8vVm3jp6m8eMZmtOpU2JFpp3yRZp0EZk1NbyPBxtmysoYCy0iM33p2a4nmmc1Tz53+XvvcRrNVQ4kXkDU1zN+NZIlExCXeSSCM8uDueceTn//+8j7+de/WC4qVMKPlb59eZETkSm8+CLDNW655cT5paWciiUzvXn9dU6vu47T3NzEikw75Ys06SIya2tplAkOL2vWDDjnHN4r06VHewQyUmRmZ2WjT7veyWfJnDGDJ+CFF0ZfNy+PdRYT5S4PVyNTc+aZHLuIzPCsXEnR179/6OVlZbQ0vf12ZGH40kt0e+obRjhataKF1E2RWV+f+gH5bnLgABOvXnjB75GEp6kJmDSJv92LLjpxWdeuQIcOqWXJPHgQOH7c71GkFq+9Ruulvrf4JTKdWDJTvVZmcCF2K0OH0iP16aeJHZMPZKTIBIDCzn2xriPQuMnFpyXDYEyddos6oaGBRXKHDmWJomg0a0ZLVaKsVOFqZGpataJISid3+Z497t7MVq1im9C2bcOv84tf8Dx65JHQy7duBWbOpFjIzY1+zJIShmC40casro43if/93/j3lS489BArQsTTGtRrPviAoTg33siHEytK8Xf7+efJV84tFAcPAvn5wH//t98jSR2++IK1Ua+6KmBVy80F9u5NnCVNewztWDJPO40GlFS2ZDY28lodTlRnUFxm5orMDoVoaAbU7tno3k6/+gq4915g9Gj+qJ2waBFw6JA9V7mmpIRPe/X1zo4VC+FqZFoZMoQFaHUR2lTls8+Aa65hTbeHHnJnn4cOUeyFise0MmwYcN55rIEZKpTjlVdomYrmKteUlLBlpRsX7CVLmNg1dWr8+0oHVq8G/u//+PeyZclbKkpbWYNd5ZrSUmadb92auDHFyvz5tMAtWuT3SFKHYFc5EHhATVSipp1C7JqsrNSvlbljBw0U4d6vLsouIjN9+U+G+dEt7jWr16Vi9u4Fxo6l9cAu0VpJhqK4mIKjutr+NrESzV0OpH5c5qpVfNofMICJNc2auSeotKUoXDymlXvvpYVBCxgrL71ES/cll9g7rpsJYvqC+MknFM2ZjGGwFMzx48C11/IasnCh36M6mbo6nstDhwL9+oVeJ5WSf2bO5DTRLXVTFcOgq7xLFzb40GiRmSiX+ebNdNdHKsRuRYvMVLCuhyJc+SJNx440AGRA55+MF5nrTm1yrx5XVRWnd9/NJ5kxY+xb9WbMoJtg0CD7x0tk8k80dzmQuiLzk0+YdXnmmcA//wlcdhnd/ldeSeG5Z0/8x4iW9GNl3DgK3WeeOdHSsGoVY3iuvRbIybF3XDdLXWmR2diYXmERsfD226xpevPNDHEAgHnz/BxRaF5/nV3EwlkxgdQUmTt38mFeiMwnn/C+dM01J9fmBRInMmtq7LnKNXl5fJB149rrB9FEJsAHv40bU78eaBQyXmS6mmGuRebPfw48+yzrRo4dG90lsWULxcM3vkFXgV0SWSvTjru8rIyxNKkiMpcvp6AsL2eh6iuuYG/nf/2LXYxGjuST9IIF8R8rUvmiYJSicDl0CHjqqcB8XRvTrqsccO9BpKmJ7vI2bfh/JrsrDx8G7rqLNQcffpgPBB06JKfIfOEFWo+uvTb8OqmSYb5pE89jHVcqTQaio9tIBicJJlJkOinErkn1DHM7iU7DhnGa5i7zjBWZee3z0AxZ7maYV1XxxpOby97Ajz7KC/fFFzMLNRxOShdZKSriNBGWzO3bKYA7dw6/TvPmFFEff5zcbo6lS+luHjwYePddWixXrqR1ympJ1u0a3RAPq1ZRiNi90F59NdCnD/DEExSbx48zHrOgIBA0bofTTqN7Pd5zZM0alrCaOJHfcwa4ecLy8MO8Zjz4IC37WVl0RX78ceTfeaL56ivewK6+mtelcHTuzPeR7JZMbcW88kpORWRGpqmJluy8vEAMoCaRItNJIXZNqotMu5ZMQERmutKiWQvkteriviVT1yYE2MXlV7+iqLniivAJOjNmcJuKCmfHO/VUoFu3xFkyc3PDtzDUDBlCF8eGDd6PySkbNzIr+5xzgMpK3nw//RR4663QbuySEoq0eEVmUxNF5plnRi+yr8nOBn72M36Wzz1H1+zSMqdWAAAgAElEQVS2bRR5dvcBcN2SkvhFpr4QjhpFy+/ixZlZymjdOmb+l5UBP/xhYP7IkQwjSCYL76RJnEZylWvKyvhAnMzf6cyZPJ9vv53/S1xmZJYs4cPQtdeefM1IpMh0Ur5Ikwkis18/oF27tH9gz1iRCdBlXt0RMNwoY3ToEE+svn1PnP/QQ7wozp0LXH/9ySVxjh9nJ47Bg+2VpAmmuJgCwmvLYaSWklaSOS7zpz+loL/2WmaQv/FG+JqVAC/MI0fGH5e5fj3PDzvxmFZuuYUWpj/+kUITOLEloF1KShgjHM970CJz6FBg+HDuKxMtSXfdxWz9J588sRyQm1ZvNzh2jAXY8/NPTPgIR2kpz9FkqhtspbGRD1qDBwNnn83fZiaef04I5yoHEisynXT70aR6rczaWpaqi1SuLiuLBo+PP2YJwzQlo0Vmwen9cLgFsG1LVfw70xnewSJTKd6QJkxg3N93v3uitWDZMgawO3WVa0pK6KLzuvxIpJaSVpJVZB47RkvIuefy4qvj0KLhRlymk3hMKzk5wJ138iL91lssbZSf7/z4bsTuLl5Md1f37oFYomSy2iWCf/+b4RXXXRcQlZpki8ucMYO/2VtusRfnnezJPx9/zOvkN77BGNO8PLFkRqKxkQ/RRUWhrzudOnGaSEumE3d59+70mqWqJdNuDOqwYXxojaW2doqQ0SKzsDNjGqv3Oyg1FA6d9BMsMgFe5CdNYhzgiy8y+1xbHmMpXWQlEck/Bw/SymHHkllUxOSQZMs+XrSIYvzii51tp8XE/PmxH9tJZnkwP/hBIJ7uxhtjO368yT/79wNffhmIIdIiM83dPCdQXw/ccQdwyinAH/5w8vJki8t84QU+4N50k731k11k6njMsWM5LSriNTeZ3ft+Mn8+HzKuuy50eE12NsvoJKslMzub66eqyIzU7cdKBsRlZrbI1Bnm9S5YASOJTIDJEm+8AVxwAfCnPwWKfM+YwcSMs8+O7bhu1kEMh50amZpmzZiZvXy5e/VH3WD6dE6D2+pFw424zJUr+f2fcYbzbdu1A375S7aHvPrq2I4fr8hcupQPRTp5oGtXJiVlksh87DHGY/7P/4S/WY4alRxxmTt20OJaUWHfeqTPzWTNMJ85kw+v+hwsLmZpJm0lE05Eu8ojVRVIVGvJWGIygdQtyF5XR8OMHZF5zjl8CEjja6mITADVzQ/wghUP0UQmQDfP1KlMnLjvPuD+++lWrqg4ud2bXRJhybRTI9PKkCH8kSWTO6uykmKxvNzZdkrxwWDlytjr8q1axSDvFi1i2/7nP2cilZ12o6HIz+f5FavItMZjaoYN4/4S1THETzZtAn7zG1rP7rwz/HrJEpf58suM9baT8KNp146CNBktmXV1PAdHjw60RdSVNSQu82QaGhheM2BA5AfbRIlMp4XYNXl5jP1O1k5a4bCT9KNp147fkVgy05P8DvlQhlkrM94n4qoqxmTpWJdwnHoqrZclJcADD9BCFKurHOCNoVUrby2ZdmpkWkm2uMzNm3nzvOgiZ3VINfHEZe7Zw+M7jcd0k+bNgcLC+ERmixYnlnfKkBpvAJgwduQIY6sjPSj0789rwPvvJ25swRgG8PzzfCC54gpn25aVsexRMnkgACZNNjYyHlOjPTjJ9CCbLMyezetOqIQfK7m5fEj0OuTAaSF2TapmmDu13A4bxntEmlrlM1pk5mTnoEezDu7UytTli+yQm0v3T69etJRZL55OycriU32yuMsBZoACySMyY3WVa+KxUMUTj+kmJSV09zrNYtRF2M86i4X2NZkSlzl7Ntsyjh8fiAcMR1YWrd7Ll9P65gfLl/OB6tvftt8VSlNayiSEdeu8GVusBMdjAmLJjIQdVznA+1BTk7dddZqaaNlz6ioHUldkOrFkAmkfl5nRIhMAClv3YBmjeE7kujoKMbsiE+CT3eLFvInZPRnDUVJCkXz4cHz7CYdTd3mfPrToJkvyT2UlBUA0kRCOfv14QU51kdnY6FxArF0L7Nt3cjHn/v0ZI5fOIrOhgf3Jc3IYk2kHv+tlPv88p05c5ZpkTf6ZOZPXlIKCwLyePfm9iCXzRI4cYRWTs8+OXolClzHatcu78ezcyd+RiMzwiMhMbwo79UVdDrB7cxwXq3Dli6LRrRvjjOKluJhusioXSjGFwqm7XClaM1eu9L/+19GjFPJDh9KVGQu6XmYscZm6NEUyiEzAucU7VDwmwASvc89lUtCxY/GPLxl58kl+Xvfey8QrO/gZl3nkCLtCDRjgPPYYSE6RuW4dX2PHnpglnZXF661YMk9k+nRWN4jmKgcSUyszlvJFmlStlalFpl1hXVTETH8RmelJYXcW467e/lXsO7GT9OMlbvWnDsf27UwccSLShgyhwPP7hrVwIcsvOS1dFIyOy1y40Nl2q1bxiTZSO85E4LbIBOgyP3IkPWu8bd3KxLw+fdh5yS46LtMPkfnOOyw3deutzrpCafr143bJlGGuXeWhQoqKiylAwnVS85qFC4Hvfc+/44fitdf4HV5zTfR1EyEyYylfpOnVi9NUs2TW1DAO3m5zlawsPrCvWJFc55JLiMjUInN/HG0Q/RaZXgfBb9sW6NFsl2RJ/qms5DRekXnBBZw6EQ8NDbxh+5n0o4m11NXixbxBhLpJpHNc5j33MKv18cedZcX6GZc5ZQqPP2FCbNu3bk0Xq98PhlZmzqTVfNSok5cVFfHBz48Y0l27WFLs2WdZ+zgZOHAAmDaNTRvsuGqT3ZKZk0PvWaqJzNpalnlzcr8cOpT3ixUrvBuXT2S8yCzQZYwa4qiV6bfI1EHwXlky7baUtKKTf/yOy5w+nT/4eN3VZ5xBa6QTkbl6NV3JfrvKAWYbd+ni7EFk/36K5FBWTIBP3+lY423hQmDyZCaKXXqp8+39iMusr6cgGzYstva0mrIyuqD9DnMB+NuZO5e1BEOV79IPTol2mRsGcNttvC42bw48+mhyZOS/+y49C3Zc5UDAu5KslkwgNWtl2i3EbiWN4zJFZHZkMHm12hd7/++qKv5gY61jGC9t2vBJ0QtLpmHYbylppVs3vvy0ZG7YwJIsF10Um/vQio7L/OQTJsLYIdZ2kl5RUuKsz/2yZScWYQ+mXTuKkkWLYv/tJCNPPMHv+/HHYztvtNUtkS7z+fMZFhKLKLZSWsoam8kQ67h0Ka3B4RL29MN1opN/Jk9mHcqrrmIN2+pqhir4zeuv0+p71VX21k+kJTMekbl1K0OvUoGGBjZDcPp+zz6blk8RmelHmxZt0KWxFarbNcZeysFJ+SKvKC6mgHC75tn+/fyBO7VkAnSZf/65d1nv0dCli+J1lWuc1stMlsxyTUkJv09dkioakeIxNcOG8cldWyxSnaNHWcf23HMDIsYpZWUM5E+kyJw2jdNLLolvP8mU/BMpHhPwp4zRpk3Aj37Eh+6nnw5UH/j97/190Nq7l9e7Cy9k0wk7JEpkxlKIXaOTf1Ll+rJ1K88Dp5bMtm0Zz714cXo9sENEJgCgsFkua2XGciLv28f4HL9FZkkJxZzObHMLpzUyrQwZQjeSX4kh06czYWnMGHf25zRzeNUq9rq2ll7xE6dxmboIe6RM5eHDOU0Xl/n8+Yxtu+yy2PeR6LhMw6DI7NOHyTvxkEwi87336B3SoTfBdOxID1KiLJlNTcDNN/M7ff55Hvu001guatkynjt+8c47DC+w6yoHWPe2bVvv3eWxWjGB1Ctj5LR8kZWhQ4EtW+Kv2Z1kiMgEUHhKL+xuDexd/6XzjXU8ZqxWD7fwKvnHaY1MK34WZa+vB+bMoQhq186dfTqJyzQMiuv+/enCSgacZJjrIuzl5ScWYQ9GJ//43a/bLaZO5TRet7OOy3RajSAWvviCWdaXXBJ/WEhREc9XvzPM9+zhdePCCyO33C0qSpwl84kn2M3p+98/0Tvyk5/wweKRRxIzjlC8+irjQ8ePd7adl60ldSH2WJJ+NJkkMtO0i5qITACFnWmFXLcpBoub30k/Gq/KGDmtkWnFz+Sf+fMZBO+Wqxw4sV5mtLjMLVvYsi1Z4jEBZ+fI2rV0wYWLx9Tk59Oakw6WTMOgyMzPj9zz2Q6JrJepXeXxCmOADxRFRfFZMg0DePjh2NqwaubOpUiJ1kChuJieJC+71gAU3b/4Bb0Sjz564rKCAuBb36Ln5NNPvR1HMGvXAt/8JjBrFqdO8wK8FJnxFGLXpFqtzHhiUNM0+UdEJoDCHgMAANU7YhBoySYy3bZkand5LJbMTp14w/bDkhlvK8lwjBzJm180C1WyFGG30qsX48fsiMwlSziNFI8JUHgPG8bQgIMH4x+jn6xaRffeZZfFbxFMZFzmtGlM/jv/fHf2V1bGskCxxlIvXcoC9ldfbT9JLphQrSRDkYi4zIYG4MYb6Y7+xz/4WQeja6n+4Q/ejcNKXR2PWVbGMm0TJjBG1ClaZHoRBxhP+SJNJlkyCwroKUuHB3YLIjIBFBawpmN13UbnG2uRWVjo3oBioXt3xv8lkyUTYFzmmjVMOEkklZV8mtQxZm5ht15msiX9AHTp6QSxaNhJ+tEMH07XsN81UeNFu8rjicfUJCouc9cufldjx0YOa3BCaSlFR6zXEt3acvt2ik2nGAbjMYuKonda8rpGMAA8+CCrSvziF+F/D4MHs3vbq696K4iamoAXXuBn8+ijDMdZuBB4+eXYDAG5uRTPXpyj8ZYvAoBTT2Vzg1QTmd26Od9WKZ5fK1fSC5cmiMgEUNC1FACwrmGb842rqijA2rZ1eVQOUcq+gHCCGyIT4M02UVRX83txo3RRMHbjMleu5LH793f3+PFSUsILdjQr1eLFfHCxY4VIl7jMd9+lu3HECHf2Z9fqHQ/Tp/MY8WaVW4kn+efwYXadGTiQ7//pp527/9auZfJDNCsm4L0lc/Fi4He/AwYNAu67L/K6P/85H7Yef9ybsSxZwvCVW2/ld/7ss7Qa6+S7WPAyw9wNSyaQWrUya2vpwcvJiW37YcNYQszv+tIuIiITQPuc9uh8NBvVzWKwtiVD+SJNcTF/2G66Lbdvp4Xk1FNj296P5B+3SxdZ0RaqaPUyV62idTuUa81PdFhFpJtyXR0Fhh0rJsDkoBYtUtvNU1vLC/vFFzOBwg0SEZc5bRofZtw81+MRmW+/zfPn1luBv/6V58X3vuesv71dVznA35hS3lgyDx4EJk7k+TB5Mt9LJCoqKK6ffdbdGNEtWziOoUN53bn7bv5+v/vd+JMKEyEy47FkAhSZNTXJUfA+GjU18b3fNIzLFJFpUnC8LapPaeBThF1272ZyRLKITDsCwim620+sFsHycm6byHZZlZW8MVx4oTf7j2ahOnSIDx/JlPSjsZP8E60IezA5OcBZZ/HC6Had1kShk2fccJVryspo1fBKZB47xpqeZ58dm6s0HAUFFFSxZJg//zy3/fa3ea7dey/F6mOP2d/HzJnMKNciPRItW9Kl7oUl86c/pVfkd7+zlwimFNuRHjoE/OUv8R//6FEmUBUVMRZ03Djgs8+AP/7RvcYfXopM7S6PJT7RSl4e78tbtsQ/Ji8xDI4xnvc7ZAh/Py+8wFJqaYCITJPC7NOwrS1w8Osq+xslS9KPxovkn1haSlpp25YXyUS5yw8f5k39vPO8C2GIZqH67DNecJIpHlNjR2Q6icfUDB9Oy65XrU29ZupUCptx49zbp9dxmQsXcr9uusoBfg79+jm3ZG7cyKzwyy6juAYYx1hUBDzwALB+ffR9NDSwTNDw4fZ/v8XFvBa7+YBTWQn87W/s3nTHHfa3u+YaiqInnogvrm7JEsbG3nsv2+K++y7HpH+/buG1JbNTJ6B16/j2kyrJP7t388EgHpHZujVjgFevZk3WNCjMLiLTpLBNLwDAuqqP7G+UbCLTabHtaDQ1sUVWvFaS8nJmq8aaaeqEefNYI9MLV7lGx2WGK76cbO0krdjpc794MS3BkYqwB5PKcZkHD7Km6siR7tVU1XgZl/nuu5y6LTIBWmE3bXImjidN4vSWWwLzcnIYl3nkCHD77dFvmh9+SEugHVe5pqiIv3m3usLs2gV85zsMEZo0iQ8LdsnOZt3MnTsDn4dTPvqI73/rVnYS+vxzd2qghsJrS2a8rnIgdURmPJnlVu65hw8r//wn8Nvfxj8unxGRaVKYS4FWvWmV/Y2STWT27csLkVsic88euinisWQCAbGSiM4/lZWceikytYVqxYrQWfPJmFmuad2aF+1w54hhBIqwOwle1yIzFeMyZ82iBcJNV7nGy7jMadN4E/fiPCtlMiS+tNmgoqmJoqpbt5MF4qhRwE03MWP8jTci78dJPKbGzQxzwwBuu40enKeeYtkvp9x6K8tXPfqo8zjCFSvYRrOxkbHl99zjXtWAUHglMt0oxK5JlVqZbsWgKsWwkwEDgF//Gvj3v+Mfm4+IyDQp7MkLdfVOBwItWcoXaVq35kXRLXd5PC0lrZx1Fqdex2UaBi/MeXnuu5WCueCC8BaqVavoJor3idYrSkp4joRyL65dy4cLJ65ygNbugoLUFJludfkJRWkpz4X333d3v2vX8vrjlYXLafLPvHm0NE2cGLpDz6OP8nO4447IHo2ZM7neoEH2x+pmhvnkycBbbwFXXQXccENs+zjlFPY3X7+e1ii7fPopk4eOHuU56Vbd00h4JTLdKMSu0WWsMsWSCfAceucdlm+aMCFxrVM9QESmSWERkxyqDzjoG1pVxRMq3pgTNykp4cXWjfikeFpKWtE3DK9F5tq1vLBffLE3N14r4SxUTU28WQwc6P0YYqWkhO7FUD1ydRF2u0k/VoYN43cQ6w3rq68SH+ze2EiLYP/+0WsyxkI0q3es6EQlL1zlgHORqWtjWl3lVjp3ptCMVDtz505+ThUVzrKm3bJk1tcDP/4xr3dPPx3f7/dHPwJataK7205c3ZdfAmPGMHTj7be9S1oM5pRTOE63RaZb5YuAQFxnJolMAOjTB3j9dZ4TV1zhbb1dDxGRadKpVwna1QPVx7fb28Awkqt8kaakhPFPbsQnxVsjU9O+PTv/eJ3842XpomC0hSpYZK5bx5iyZIzH1ERKEIsl6UcTT+/dBQv4mf7qV863jYclSxiD54WrXONFXOa0aRQHo0e7t08reXm8sdvJMN+/n9a/4cMDVsVQ3HQTBXe42pmzZ/O66sRVDvCm3qpV/JbMRYtoZb3zTorieMjNpdt8+fLoVuy1aykq9+4F3nzT3eQzO3jRWtIt1zFAsZ8KtTLdFpkAHzweeYThTRMnpmT1DhGZJio7G4UHW6C6mc2nhR07+GSRbCLTzeSfeFpKBlNeTlHjZevBykqWfxg1yrtjaMJZqJKxnWQwkTLMFy9mXF0sFohY4zIPHKAAMYzEFyHWyTNei0zAvbjMffsoyi+8kOLKC7KyKPrtWDJff51WwHBWTI1SFJjhamfqeMyKCudjLSqK35I5e3Zsxw/H3XdzbI88En6ddev4oLBzJz9HL0I2ouGFyHSj24+VvDx6XpI527qmhr/HDh3c3e/dd9Nl/q9/AQ895O6+E4CITAuFje1Q0/oYjhyzUXoi2ZJ+NHZK1NjFLUsmQJFpGIGkGLc5eJDZ3iNH0gWUCEJZqJI56UcT7hw5cCBQhD0WV2FpKTNynYrMu+8GNmzgBfqLLxJ7I5k6lee3bhrgBeGs3rHy3ntMyPPKVa4pK+M1YPfuyOs9/zytntdcE32f4WpnGgZF5hlnxCZMioooQuIpGzR7NhN23PJC5OfzM3nvvdDXva+/psDcupWxoFde6c5xneKlJdMNdzlAkXnkiDdZ8G5RW0srptthUkqxwP+gQcD99wdiyFMEEZkWCpufBgBYv8XG03uyikw3My3diskEvE/+ef99BppfdJE3+w+FtlBZSxmtXMnyP/36JW4cTjn9dJbqCRaZy5ZRNMcSjwkwju7cc7mfhgZ720ybBjz3HK1Ht9xC74C+QXlNVRXjQC+91FmZGqdkZfFccSsu0+t4TI3OMI/kMv/qK5bcufpq+3UtQ9XO/OILFrL+xjdiG2txMYVqdXVs2+/ZQ9f26NHxd9Gx8rOfcRpszayp4bE2b2bh7euuc++YTsnNpYA7dMi9fbpViF2TCmWMtMj0gtatGavbuTMT0lKoHrGtK6tS6gml1EallKGUCvmYp5QaqZQ6opRaaXl55MvxhsK2vQEA66pttEBMVpHZtSsv9m65y085xZ3WiF4n/ySidFEwoSxUq1bRGhOtBZ2fKEWLUvA5Ek88pmbYMLpOP/kk+rq7drE1Xrt2tIbpPu+xtDOMhUS4yjVuxWU2NvJcHzTI++oFdpJ/XniB02iuciuhamfGUrrISrwZ5vPmcRxjxsS2fTjKy7nP118PlODZupWhDuvXA888w1g7P/Eiw9ytQuyaZBeZhw8zptbL32ReHkuAHT4MXH65u4mEHmL38f1NACMARPuG1xiGcablFYfvIvEUnGbWytxsw6VbVcWbdUGBx6NySDgBEQvxdvuxkptL14kXIlOXLiooSKzoD+7osns3L67JnPSjKSnh92stJ6OLsGurcyzYjcvU9Qi3bwf+/Ge6SOPpmR0LU6fSRZ+ITF634jIXL6bVzWsrJhD9+zh2DHjpJf7unJbbCa6dOXMmH8xiLdsTrwdHx2N6cS78/Od8OPi//6OQGzOGYvjPf+ZDlt9okblrl3v7dKsQu0ZXfkjWWpk66cfN9xyKUaPYVnTtWlo0UyARyJbINAzjA8MwEuTD8o/CnhQH1btsPA1XVVE0OSlYnSiKi/m0HG/JAzdFJsCn+i++iC9uKhRffcUn3IsuSnzZIGu9zFSIx9QE35R1EfZBg+I7p885h+I7msh8+WVmJF99NYPaAXvuWbfYvZvf2dix3iXPWNFdouIVmYlylQNMAGvXLvz3MWMGHxJuvjm23521dub8+WwFG6vlK15L5uzZtBR5YTS48EL+rp57jn9/+SUF5+23u3+sWHDbkqkLsbspuJLdkulFZnk4fvxjWr+nTWOMZpLjdiBSgVJqhVJqmVIq4i9IKXW3UqpGvw56mXVsky4FA9G6Aag+GKVWpo79STZXucaNHuaNjXyydSMeU1Nezv1+9pl7+wT8cZVrrBaqZG4nGUxw8k9VFYVXPK5ygIk//fuzHEy4BJ7Nm1lHsEsX4C9/CQiUDh0obBJhyZw+nediIlzlgHv1MqdN42/Sy0QljVK0Zn7+eejv8oUXuM5NN8W2f2vtzPr62OMxAZZJO+202K55mzbx/B8zxpuHVKXYuefwYV77Hn6YZZKSBbdF5q5djMl2K+kHYBhY8+beicymJhpmYiWRIlNXaTjrLGabOyn47wNuiswVAHoYhlEOYDyA25RSYdMNDcN4zDCMHvrVxo24vzhReXko3ANUH98RecWtWxkknawi043kn507+cNz05LpVfLP9Om0vmnBl0jKypiROm9ealkyg0VmPEXYgxk2jL+RUDeEpibG7+3fT8tOcD3CsjJaerx2A02dyov1N7/p7XGs6LjMBQti237DBloVv/lNbxOVrJSV0T2vkwA1O3YwprWiIj4xoWtnAvGJTIDWzFgsmXPmcOp2PKaVb32L7s3HH6f7PJlwW2S6Xb4I4Pnes6d3IvPRR2ktjTVxLFHuck2rVkwEys3lbygR3p8Yce1KZRhGnWEY+82/awC8CuA8t/afENq1Q2FdNr7OPoiGxgjZscma9KNxo4yRWy0lrege5m6KzLo63rRHjUqM2zMYa1zmwoW8EHbsmPhxOKWggFm0+hxxI+lHM3w4p6Fc5n/+M2/q//VfoQVeWRnDKTZsiH8c4Th6lK7ec85x11IfjXjjMhPpKteEC2F4+WWWUXKS8BMKpYApU1gDcMCA+PZVXExB7DS2UMdjelXYHmCrzX/8g6EByYbbItPt8kUaLwuyT5rEGONY+4Tr95zIVsI9e7J4/8CBDGtJUlwTmUqprkqpLPPvtgAuAWAjxTS5KGxqhyYFbNy3MfxKyS4yCwspfuIRmW6WL9J07UrR6qbInDOHFwc/XOUabaFavz41rJgAkywKCk4UmV27Ar16xb9vnfyzaNGJ81evptswP//EGolWtKjx0mU+fz5rgibKVa6JNy5z2jR+b15a3IIJlfxjGHSVt2/PdnfxkpvrzncRS1ymYVBkDhhAd3smkgqWTIAic/9+97Oqv/yScf0AMGtWbPuoreU9102jjB3OP59GlkRZUGPAbgmjvymlagD0APCeUqranP+cUkpfHa4C8JlSahWAJQBmAXjBgzF7SmGLrgCA6t1V4VdKdpGZk8NsvHjc5W4WYrdSXs64JLt1FKORyFaS4bC66VNFZAK0eFdXs/TGZ5/FXoQ9mN69ed5YLZnHjzNY/ehR4MUXw5fFSkSGeSJLF1nRVu9PPnHeevPAAYrTkSPt16N0g1Dfx/LlPF8mTEiuxMdYwoQ+/5yu/0QK92Tj1FMZ75jslkx9v3W7PfGUKZy2b8/fWCz3ptpaGmSys10dmi0SnezqELvZ5d83YyezDcM43TCMQnP+dw3DmGr+/ZRhGKWGYQw0p/cbRjL3gApN4am9AQDVm1aGX6mqijeM/PzEDCoWSkr4RN/YGNv2braUtFJezh+xGzEkhsGkn6Iif78LHZcJpEbSj6akhOLvtdfiK8IejFK0Zn76KcURAPzudyzS/rOfASNGhN/2jDM49SrGyDAYj5mfHzhWIrnzTgqz0aMDNzc7zJ7N302i2w7m5vJl/T5iqY2ZCGKxZCYiHjPZUYoWdrctmW67jseP5/SVV9zd75QpfP933MFcC6cPgACFdRJbE/1EOv4EUXg64xmrayNkQFdV0XSfzAW3+/ePT8x5Zcl0M/nns8/4BJnILj+h0BYqIPUsmQDjkQB34jE1w4dTuC5dSsvDgw9SjD/4YOTt2rShJdQrS+annzKb+LLL/AHM1MgAACAASURBVLEAjBgBfPABrSbXXMNOMHaexbX1NZGJShprhnl9PW/yZWXx1VP1goIC/hadWDJnz6b16bzUSh9wHTdbS9bU8KHbrULsmn79eM5NmeJeGbyvvuI9cvz4wH3Eqcu8sZH3y0TGY6YQIjKD6N6rDC2PR3CXNzUld/kijU6+CI6Ls4sXMZmAu8k/OkjbjxtvMA89BDz5JONhUwUtMpcu5Y3WTdGg4zLnzAFuvJGCbvJkoGXL6NuWlTF+89gx98aj0X1/E+0qt3LWWczmLy1lpvEPfkCLcjiamniul5YCffokbpyasjLg4EGK83feYQH/W29NPjddixb8fOxaMo8do3t06FB3upqlMm6LTLdd5ZobbmCyp06CixftTbj6apYFa9/eucjcvp1CU0RmSERkBpHVKw/5e4HqQ5tDr1Bby6d57ZpJVsIlX9hl+3ZmrLkdc9WzJwswuyEyKyvZ9jLWLiFuUlrK2o/JduONhI5hA1gs2s3s/EGDKCgfeYTWggcftG/lLS2lAIi1nEgkpk7ljSSSyz4R5OXxtzlmDPC3v9ENrkMLgvn4Y8YNJjKr3Io1w/yFF/hAcsMN/owlGkVFPG/shAl99BHdo5nsKtfk5lK8HT0a336amrx1HV93Ha3Vkye7s78pU3g/GjWK1TZGj2ZYz5499veRyBqZKYiIzGB69ULhHmBj424cbwphXUj2pB9Np060VMVjyfQiU04pWjNXrYpsvYnGnj1MLKmosGcdE06mY8dARq2brnKA38ngwbzZDxvGWEy7eJX8U1tLwXbxxUx08Jt27figdOutLKl03nmBpAkr2mqT6HhMjf4+Kitp5bnkkkBGcrJRXEyhtClKQw1A4jGtuNVa0otC7Fa6dOE1v7Iy/rGuXs1rzPjxgYSdsWMZFjJ3rv396N+sxGSGRERmMN27o3APcEw1YfP+ENbMVBGZAF3mGzcGnrScsH27d+UYyssZUxNPiaX33uNTs59Z5emAdpm7lfRjZfx4Puy8+CKtBHbxSmRqseanqzyY5s1ZlP43v+GD1znnBDpHad59lw8EXnxHdtCWzGee4Q341lv9GYcdtIfJTlzm7NnM1B8yxNsxpQJulTHyqnyRlRtuoIHCSeJcKN58k9Orrw7Mq6jg1InLXCyZERGRGUzLlig8fioAoHpPCHddqolMwLk1s6GBLQa9KlTtRvKPjscUkRkfAwbQuqzPFTf5yU/4sOI0TrWkhC4xt0Xm1Km0WIwb5+5+40Up4Fe/YkLNrl20aOrSXDU1FJ0XX+xMqLtJ+/a8gR47xmuC34l2kdAhINHiMg8eZFzsBRckh1Xbb9wSmV6VL7JyxRVMKvrHP+Lbz5QpfHgbNSowLz+fr5kz7SXkASIyoyAiMwSFLc1ameFEZnY2M2CTnVhF5g6zraaXlkwgdpHZ2Ej34plnyg87Xu6/n9nObhRhD0Uswignh8LUzTJGhw7RPXrBBcnbHeP662lda9GCrvGnnw48TPkVj6nR1uUbb/SnFqBd7FoyP/iA1jBxlZNUsmS2aUMvyeLFwLp1se1j7VpWmrjiipMfMsaOpQfQ7r5FZEZERGYI/lMrc1eIC1VVFTMYk/lCq+nblxcPpyLTi5aSVvLzeaOPVWQuXUpLazJklac6nTr5nwQTirIy/tbq693Z36xZjNVLJld5KM47jzfP3r2Zdf6rX/FaE29f73g55xw+MCSzqxzgjb516+iWTN1KUkQmcUtk6raPXloygUDi2csvx7a9Nas8GKcu85oa3s8yvUJBGERkhqBX1xJkNwLVW4IsKY2NfLpJBVc5ECiKvXIl3UN28ap8kXVcgwax80lTk/PtxVWe/pSW8tyIJ27Xii5d5FfyjBOKiig0hw3jw9R559Fl7Se/+AWrBPTr5+84oqEUP79olszZs/kQ7UdB/mTELZG5dm1iPH1jxvD+NHmyfbe2lTffBDp0AC688ORlo0czXMeuyKytFStmBERkhiC7V2/02RfCXb55M+MVU0VkAnSZNzbS+mcXrwqxWykvp/DVMa5OqKykBe6cc9wfl5AcaPesGy7zxkYm/fTv70+dyVjIzaV7/3//F3j0Ub9Hw/JWqXLdKy7mtfrw4dDLt29nI4cLL0ytkmNe4pbIXLOGRfG9jnPNzmZ4SVUVSw45obqahpdQrnKAD3Rnn83fX7QKKIYhIjMKIjJD0bMnCvcA647UosmwWNpSKelHE0tcplctJa3EmvyzZQstoOPG+ZcIIXiPmxnmq1bx5plq4RU5OcAvfxmIYRbsoeMywz3A6vI04ioP0LEjrXfxiMxjx+jpS1QNae0yd1ozM5KrXFNRwbqh0QRsXR3jvUVkhkVEZijMWplHjWOorbOU/0lFkXnWWaxZ6ERkJsqSCTgXmZWVnIqrPL3p25dWBjdEphYVo0fHvy8h+YmWYa7jMUO5SjOVrCx6h+IRmRs30vJnbfLgJeXlrETx2mvOuoNNmUJrZaTv325cptTIjIqIzFCYIhMIcpmnoshs2ZJ14BYvttcFAwiITF2o2wv69mW3nlhEZlZW8pWhEdyleXPerNxwl8+dy/15UaZJSD4iZZgbBoVDcbH3ySmpRrytJfXnnSiRqRStmTt3suSQHdatoyfs8stZxSEc557LRJ5oIlMyy6MiIjMUubkoqGP2+Ekis0UL78q9eMXw4TTr271hb9/Op1ov42qaNWMJohUr7AduHz3KH/3QoXTvCOlNWRmwYYOzpLVgjh0DFizgTaN1a/fGJiQvWmSGsmRWVzNeU6yYJ+OWyExky+UJEzi16zK34yoHeO8bNYrGmbq68OuJyIyKiMxQZGWhMCdErcyqKpbfSbVYQKdxmV61lAymvBzYt49Cwg4LFlBwpFpsnRAbutPMl1/Gvo+PP+Y5I67yzKFdO8aTh7JkSivJ8OTmsl1vrO1+tahPlCUTYCLfiBHAO+9EFoOaN9/k+aHd4ZGoqKD3b9688OuIyIyKiMww9O6Qj6wmYN1esyDr8ePA+vWp5SrXDBvGqV2R6WVLSStOk3+kdFFm4UaG+fvvcyoiM7MoLqboCfaSzJ7NcJuRI30ZVlKjM8x3745t+zVrKOC8DLMKxY03sp7u229HXm/9emD58uiuco2duEyJyYyKiMwwtOyRh177gepd5tOZDmpORZHZqRMDpO2IzCNHgP37vc0s1zhN/qms5I95wADvxiQkD25kmM+dyyxtKXeVWRQV0Utidf82NvJ8GDyYNRKFE4m3jNGaNfzcE10W6uqrKRqjtZnUvcq/9S17+9Vxu5HiPWtr6Vrv3NnePjMQEZnhMJN/qvdUwzCM1Ez6sTJ8OIVybW3k9bzu9mOlXz8KADsis7qalomLL5badplCnz48P2IVmUeP8sFqxAgmwAmZQ6gM808+AfbulXjMcMQjMuvqGGaVSFe5pkMHhlDNnRv5/jZlCnDqqWwbaQelaM1cuxbYtCn0OrW1QLdutI4LIZFPJhymyDzUeATbD21PD5EJRLdmJqJGpiY7m1ZJO8k/4irPPJo1Y0eWWN3lS5bQjTZqlLvjEpKfUBnmEo8ZGS0yd+1yvq0f8ZhWbriB95BXXw29fMMGxmdfdpmzB85oLvOaGonHjIKIzHCYBdkBM/knU0RmImpkWikv55Ozjm0JR2UlXSJihcgsyspoLdi71/m2Uh8zcwllyZw9m5ZxHaMunEg8lkw/MsutXHwxa1+GyzLXrvJoWeXBjBlDi2YokXn0KD8riceMiIjMcATXyqyq4gUqVU+ovn15EUk2kWkn+efgQWb4jRzJ2mVC5qAzzGOxZr7/PtC2LWPwhMyiTx9awrX4OXKE1SnOO4/XceFk4hGZflsyc3IoIFetYsvQYN58k9cCu65yTefOwKBBfEBpajpx2datnIolMyIiMsMRypJZUJC6sRdK8Ql+5crIdQcT6S4H7CX/zJnDnvFSuijziDXD/NAhusvPO49hGUJm0aIFy81p8fPhh7Q8ias8PG5YMv309Ok2ky+/fOL8r78Gli4FLr00tgeMigpm3H/yyYnzpXyRLVJUMSWAtm2R39QOgJlhvnFj6rrKNcOHM8Ny6dLw6yTakllayuy8SCJT4jEzl1gzzBctYiF2cZVnLkVFTBg8fjwQjynhNuHp1InTWEVmr17+NjwYMYJjePnlE62OsbrKNeHiMqV8kS1EZEagVfc8dD/UDNXbvuRJm+oic8QITiO5zLdto7U2USUZWrakkAgnMg2D8ZhFRUBhYWLGJCQPPXvSzeVUZEp9TKG4mA8aX39Nd2fHjuwyJoSmeXNmajsVmYZBi7Ff8ZiarCxaM2tqgPnzA/OnTGGY1Te+Edt+hw8HWrU6WWSKJdMWIjIj0asXCnc1obpuIwwg9UVmeTlFXSSRuX073SaJ7GpUXg5s2RKwolr59FP+mMVVnpkoRWu3U3f53Lm8YQ4c6M24hORHi54lS5hZPHp06nVrSzSxtJasrQUOH/YvHtPKt7/NqU4A2rQJ+OgjuspbtYptnzk5wPnnAwsX8n1qRGTaQkRmJHr2RMlOA/sbD+Hr9kh9kdmyJTBkCPuxNjaGXidRLSWtREr+EVe5UFrKG9+OHfbW37+fomLkyNSNoRbiR4uev/2N1jaJx4xOLCJTx2Mmg8g84wwaLd58k8leb73F+bG6yjUVFcwL+OCDwDwtMrt1i2/faY5cgSPRqxdGmW21Z+Uj9UUmQNN/XV14y1CiWkpaiZT8U1lJV8f55yd2TELy4DQuc8EChrdIfczMRlsyFyzgVOIxo5ObyzqZwZnUkdDJVX67yzU33MB73LRpdJWfcgowblx8+9RZ6VaXeU0NPy9p9BAREZmR6NkTF24AlAHMKmqWHk8skeplHjzIrNxEZZZrBgygGytYZO7eTatrRYW9XrNCeuI0w1zqYwoA0LVroORZXh6rgwiR6dyZXq59++xvk0yWTAC47jp6MP7wB94/Lrkkdle5pqyMxhdri8naWnGV20BEZiR69ULnw0D5VmBOPtBoOHi6S1Z0IeJQIjPRmeWaVq3YYjJYZL73Hp+oJR4zs9G1Mu1aMt9/HzjtNLrOhMxFqYB1TRfVFiITSxmjNWtozevVy5sxOaVrV37fy5bx/3hd5QDPnTFjeA3aupXhF1u2iMi0gYjMSJg/mrHrgD0tGrFiq40e28lOp05ASUlokZnoGplWysuZBbp7d2BeZSWnF12U+PEIyUOXLswMtiMyd+9mLdhRo0RUCAHrmsRj2iMWkbl2LUPJkin+WdfMbN3avfuHLmU0ezZDChoaRGTaIInOiiSkWzdAKVSs47+z1ofpX5pqDB/Oup86cFnjlyUTODn5p7ERmDGD3RbSIUxBiB2l6K764ovoPe516RJxlQsAy9bk5QUEghAZpyLz6FHeS5LFVa4ZP56u/2uvda92p7VeptTItI2IzEg0bw5064Zhm4HWqgVmrpsZfZtUIFxcpp8iMzj556OPaJUSV7kA0GW+f//JD0bB6HhMSfoRAOCmmyiCdKFxITJORWZ1NUOakk1ktmnDLn1//at7++zalQ+7VpEplsyoiMiMRq9eaNkIXNCxHB9u/hAHGyK0ZEwVwolMP93lAwfSYqVFppQuEqzYzTCfO5fWBSncLwjOcSoyddJPsmSWW2nf3v3M74oKGmN0ApCIzKiIyIxGXh4AoKLoIhxrOob5G+dH2SAF6NuXF5NksmS2bcsLlRaZlZV0d5x9duLHIiQfdjLMt20DvvqKrnKJxxQE5zgVmbp8UbJZMr1ClzJ65RVORWRGRURmNH79a+DFFzH2zKsApElcplLMMl+5kmWLNNu2BVqL+UF5Od0vX37JsY0bJx06BGInw1y3khRXuSDERjpZMr3g/PNZTm/PHv4vMZlREZEZjTPOACZOxBm5Z6Bb227pITIB9jFvbASWLg3M276dpV/8yhLUyT+//S2nEo8paDp1ooVdRKYgeEdODuMZnYjMzp1Z/SETaN06EG7WujXQrp2/40kBRGTaRCmFivwKfLnzS9TU1fg9nPgJFZfpR0tJKzr559VXKXS1a0IQALrMv/wyfDeSuXOB/Pz/hLgIghADTlpLrl2bOa5yjb4vde8uYTk2EJHpgIp8ljCYvX62zyNxgfJyBkVrkWkY/rSUtDJoEKdNTXTnZ8rTsWCP0lLg8GFmCwezaROwbp2ULhKEeLErMnfv5ivTRKYuZSTxmLYQkemAMfks6JsWpYxatgSGDGHbrcZGloc5etSfzHJN+/a0RAHiKhdOJlKGuXaVi8gUhPjQIjNaTdpMi8fUDBoEjBzJdpVCVERkOuD0Nqdj4OkDMXv9bDSlQ4vJ4cOBujpm7PqZWW5Fx2VK6SIhmEgZ5ro+5siRCRuOIKQlubnsZnPgQOT1Mi2zXJOVxYfan/zE75GkBCIyHTK2YCx2Ht6JVdtWJfS49cfrccf0OzCjeoZ7O7XGZfpZI9PKffcBf/kL0L+/v+MQkg/dizzYkmkYvOj368eCyYIgxI7dDHNtycw0kSk4QkSmQ3RcZqKzzH+/8Pd4YukTuPTVS/Gv1f9yZ6fDhnG6aFHyWDJLS4Ef/EACqoWTOfVUoFevk0XmunXA5s3iKhcEN3AiMrOyAiFOghACEZkOGdFrBFo2a5lQkVm1uwq/XfhbFHUqQoecDrh6ytV4d8278e+4UyegpCS5RKYgRKKsDFi9Gjh+PDBPShcJgnvYFZlr1wJ9+rjfVUdIK0RkOqRV81Y4P+98LPh6AY4cO+L58QzDwA8rf4iGxgb8/bK/Y+5Nc9E+pz2ueuMqTFs7Lf4DDB/ObF3dacdvd7kgRKK0lPFi1dWBeRKPKQjuoUXmrl3h12ls5G9QXOVCFERkxkBFfgWONh7Fgk0LPD/WG1+8gVnrZ+GWM2/BiF4jUHZaGeZMnIN2Oe1w1RtXobKqMr4D6LjMqVM5FUumkMwEZ5jreMyBA2mZFwQhPuxYMr/+mtVIMi2zXHCMiMwYGFvAYqxelzKqO1qHu967Cx1bdcQjFY/8Z37/0/tjzsQ5aNuiLca/Ph7Tq6bHfhAtMvftY7eHU0+Nc9SC4CHBGeZffcWkNYnHFAR3sCMyMzWzXHCMiMwY6H96f5x2ymmex2X+eu6vsfXgVjwy5hF0bt35hGUDTh+AORPnoE2LNhj/+vjYs8779g1cVE4/XRJuhOSmpITnqLZkale5iExBcAc7IlMyywWb2BKZSqknlFIblVKGUurMCOt9RylVpZRap5R6VinV3L2hJg9ZKgsV+RX4dPun2HZwmyfHWLF1BZ5a9hSG9RyGWwbdEnKdgV0GYs7EOWjdvDWueO2K2CyrSgWyzMVVLiQ7rVsDBQUBkfn++8xwPe88f8clCOnCKafQqyUiU3ABu5bMNwGMAPB1uBWUUn0APATgPACFAE4H8L14B5iseNlisrGpEbdNuw0KCn/95l+RpcJ/TWd2OROzJ85G6+atcflrl2PWuhisqyNGcCoiU0gFSkuBqirgyBGKzMGDgXbt/B6VIKQHSkVvLbl2LdCmjdSlFaJiS2QahvGBYRg1UVb7FoCphmFsMwzDAPA0gOvjHWCyUlHgXb3MZ5Y/g2VbluHOc+/EgNMHRF2/vGs5Zt04CznZObjstcswZ/0cZwfUcZmSWS6kAmVlzG594w1g714pXSQIbhNNZK5Zw6QfCa8SouBmTGYvnGjp3GjOS0u6te2G0txSzFo3C0a0Hq8O2H5wO+6dcy96nNoD94+83/Z2Z3U76z9C89JXL8XcDXPtH3TIEOCuu4DvfMf5gAUh0ejknyef5FTiMQXBXSKJzEOHgJoacZULtvAt8UcpdbdSqka/Dh486NdQYqYivwJbD27FFztD9FKOkZ/O+in2H92PP437E9q0aONo28HdBmPmDTPRolkLXPLKJXh/w/v2NszOBh57DDj77BhGLAgJprSU0+XLgebNA5Z4QRDcITeXYvJIiFrQVVWcSvkiwQZuisxNAPIs//c254XEMIzHDMPooV9t2jgTVMmA26WM5m6Yi8mfTsbFfS/G+JLxMe1jSPchmHnjTDRv1hyXvnopth7Y6srYBCFpKC7mgxEAnHMOExUEQXCPSBnmkvQjOMBNkfkWgMuUUl2UUgrAbQBec3H/Scf5eeejRbMWrsRlHj1+FLf/+3bkZOfgqYuegooj1uXs7mdj0uWTcOjYITy88OG4xyYISUWLFgErirjKBcF9RGQKLmG3hNHflFI1AHoAeE8pVW3Of04pdRkAGIaxHsB9ABYBqAawE8DfPBl1knBKi1MwvOdwzN84H/XH6+Pa16MfPoo1u9fg1+f/Gn069Il7bFeUXIHB3Qbj6eVPo6YuWs6WIKQY2mUuST+C4D52RGbfvokbj5Cy2M0u/77p1s42DON0wzAKzfnfNQxjqmW9Zw3DKDBf3zEM45hXA08WKvIrcOT4EXy4+cOY97F+73r8ZsFvUNK5BD8d9lNXxqWUwoMjH0RDYwN+u+C3ruxTEJKG734X+Pa3AzVeBUFwj85m849QInPtWqBbN6Bt28SOSUhJpONPnOi4zJjqUwIwDAM/qvwR6o/X4y8X/wUtmrVwbWzjCsdhaI+heG7Fc/h6X9gSp4KQeowdC0yeTNe5IAjuEs6SaRi0ZIqrXLCJiMw4GdR1EDq16oSZ62NL/vnnV//E9OrpuGHADRjVx13Xn1IKD456EMeajuGhDx5ydd+CIAhCmhJOZG7bBhw4IJnlgm1EZMZJlsrCmPwx+GTrJ9h5KELx2hAcOHoAd8y4A+1z2uPRikc9Gd+FfS7E+XnnY9LKSajeU+3JMQRBEIQ0IpzIXLuWU7FkCjYRkekCFfkVMGBgzgb7nXZ2HtqJ69+6HrUHavHb0b/F6W286bajYzMbjUaxZgqCIAjRad+eZcKCRaZklgsOEZHpAv9pMWkzLnPqmqko+2sZ/l31b0zoPwHfO8vbFu8X9L4AF/a5EJM/nYzVu1Z7eixBEAQhxVGKyT/hRKa4ywWbiMh0gV7teqG4UzFmrp8ZscXk/vr9uOVft+Dy1y7HscZjePWqV/HylS+jWVYzz8f40KiH0GQ04YH5D3h+LEEQBCHFCdVacu1adtnq3duXIQmph4hMl6jIr0BNXQ3W7F4Tcvn7G97HgKcHYNLKSRhXOA6f3/45riu7LmHjG9pzKC4qvAivf/46Pt/xecKOKwiCIKQgoUTmmjVAYWGg45YgREFEpkuEK2V05NgR3DXjLox+aTR2H96Np7/5NConVKJb224JH+MDIx+AAQP3z7s/4ccWBEEQUojcXGD/fqChgf83NADr14urXHCEiEyXGNl7JLKzsk8oZbSsdhnKnynH4x89juE9h2PVbavw/cHfj6tlZDwM6T4ElxVfhre+egsrt630ZQyCIAhCCqAzzHft4nTDBqCxUZJ+BEeIyHSJti3bYmiPoZi3cR4OHzuM++fdj6F/H4r1e9fj9/+/vfuOq7r6Hzj+OmwQRJYTcIIDNRVxQmruclTmKi3NlZWZDRtayvfb+JZ+s/z2c2WOxDJzZbkX7r0H7gFuRQGRfe/5/XHlJoIKeFn6fj4e98G9n3s+55x7P58PvDmfM1p9w/o+66nsXrmgq0loc1OfzFHhowq4JkIIIQqte6cxkpHlIhekY4UFta7Umo2RGwmYEMDZmLM8VeopZr0wi1qlahV01czqlK5Dl+pdmB8xn50XdhJULqigqySEEKKwuV+QKbfLRQ5IS6YFpffLjIyN5NPgT9kxYEehCjDThTYPRaH4PPzzgq6KEEKIwujeIFMmYhe5IEGmBTUo14BJz01ia7+tfNnyS4uuQ25JASUD6FGzB8tPLmdL1JY8Lcuojfy440d2XdyVp+UIIYSwoHv7ZB47Bm5upvkzhcgmCTItSCnFoPqDaFCuQUFX5aFGNRuFlbLi83V525r5fzv+jyHLhtDk5yb8uOPHB84jKoQQopDI6na5v79ponYhskmCzCdUVc+q9KrdizVn1rD+7Po8KeP0zdN8vOZjfF198XX1ZciyIbyy4BXiU+LzpDwhhBAWcneQGRMDV6/KrXKRYxJkPsE+f/pzrJU1n637zOItjEZtpP/i/iSkJjCt0zR2DdxF56qd+e3QbzSc2lCWtxRCiMLM3d3UanntmvTHFLkmQeYTrLJ7ZfrU6cPGyI2sObPGonlP2T2FdWfXMbDeQFpWakkJhxIs7L6Qb1p9w9HrRwn6KYi5h+datEwhhBAWYm0NHh6mIFOmLxK5JEHmE+6zpz/D1srWoq2Z52LO8eGqD/Ep7sOYNmPM25VSDG86nDWvrqGYbTG6z+vOsOXDSDWkWqRcIYQQFpS+tGR6S6ZMXyRySILMJ1z5EuXpX68/285v46/jfz1yflprBvw1gPiUeH7q+BPF7YtnStO8QnP2DNpDsG8w32//nuYzm3Mh7sIjly2EEMKC0oPMY8dMt86rVCnoGokiRoJMwachn+Js58wrC15hc+TmR8pr2t5prDq9ir51+tK2Stv7pivrUpa1r67l/cbvsyVqC/Wm1GPtmbWPVLYQQggL8vKC6GiIiIDy5cHRsaBrJIoYCTIF3sW9WfryUrTWtJvdjk2Rm3KVz/m487y38j3KupTlu7bfPTS9rbUtY9uM5Y+uf5CYmkjrWa35euPXGLUxV+ULIYSwIC8v0BoOH5Zb5SJXJMgUAISUD2F5r+UAtAvLeaCptWbQ34OIS45jcofJlHAoke19X6rxErsG7qKGVw0+XfspL/z+ArdTbueofCGEEBaWPo2R1jLoR+SKBJnCLNg3mOWvLEcpRbuwdmw8tzHb+846MIulJ5bSq3YvOvh3yHHZ/h7+bOu3jZdrvcziY4tpG9aW2KTYHOcjhBDCQtKDTJAgU+SKBJkig6a+Tc2BZvvZ7dlwbsND97l06xJDlw+lVLFS/NDuh1yXXcyuGGEvhPFeo/fYHLWZZ355hmu3r+U6PyGEEI/g7iBTbpeLXJAgcU3GVgAAIABJREFUU2TS1LcpK3qtwEpZ8ezsZx8YaGqtGbxkMDFJMUx8biLuju6PVLZSirFtxhLaPJQ9l/bQbEYzGXkuhBAFQVoyxSOSIFNkqYlPE3Og2X52+/suPTnn0Bz+PPYn3QO680L1FyxStlKKz5t9zndtviPiegQh00M4ffO0RfIWQgiRTelBpqMjeHsXbF1EkSRBprivxj6NWdFrBdbKmmd/fZbws+EZ3r8Sf4Uhy4bg5eTF/9r/z+LlD2s8jJ86/sTZmLOETA/hyLUjFi9DCCHEfXh6mn76+YGVhAsi5+SsEQ/U2KcxK3uvxMbKhud+fS5DoPn2sreJTozmx2d/xKuY1/0zeQT96/Xnty6/cfX2VZrNaMaeS3vypBwhhBD38PICZ2eoW7egayKKKGWppQQflbe3tz5//nxBV0Pcx/bz22kT1oZUQypLXl5CdGI0Xf/oyovVX2Re13kopfK0/CXHl9BlbhfsbexZ8vISgn2D87Q8IYQQmCZiL1nStI65EIBS6oLWOlv9JyTIFNl2d6DpZOuERnPkzSOUci6VL+WvO7OOTnM6YTAaWNRjEW0qt8mXcoUQQghhkpMgU26Xi2xr6N2QVb1XYWdtR3RiNOPbjc+3ABOgRcUWrHl1DQ42DnT8rSMLIxbmW9lCCCGEyBlpyRQ5duTaEfZc2sMrtV7J89vkWTl45SCtZ7XmesJ1pneeTu+neud7HYQQQognkdwuF4+9E9EnaDWrFZGxkczpMofuNbsXdJWEEEKIx57cLhePPT8PPzb23Yibgxuj14+msPyzJIQQQggTCTJFkeXr6kvfOn05ev0oa8+sLejqCCGEEOIuEmSKIm1w0GAAJuyaUMA1EUIIIcTdJMgURVoV9yq0q9KOP4/+yfk46dMrhBBCFBYSZIoi7836b2LQBibvmlzQVRFCCCHEHUVydLnW2vwQwmA0UGtiLZLSkjj29jFsrW3zrCyllPkhhBBCPGke2ymMEhMTiY6OJj4+XgJMkUFsUiwxSTF4OnlSzK5YnpallMLZ2RkPDw8cHR3ztCwhhBCiMMlJkGmT15WxlMTERCIjIylRogQVKlTA1jbvWqtE0ZNmSOPQ1UM42Tnh7+Gfp2WlpqYSGxtLZGQkvr6+EmgKIYQQWSgyQWZ0dDQlSpSgVKn8W8ZQFB3W1ta4F3MnOjGaZGMyTrZOeVqWg4MDYDovvb2z9Q+dEEII8UQpEgN/tNbEx8fj6upa0FURhZhXMS8Art2+li/lubq6StcNIYQQ4j6KTJCptZZb5OKBitkWw8nWiejEaNKMaXlenq2trQxAE0IIIe6jyASZQjyMUoqSxUpi1EaiE6LzrVw5P4UQQojMikSQKUR2uTm4Ya2suZZwTYI/IYQQogBJkCkeK9ZW1ng6eZKUlsStlFsFXR0hhBDiiZXtIFMp5aeU2qKUOq6U2qmUCsgiTXOlVKJSat9dD5nfReSr9AFAV29fNW9TStGnT58CqpEQQgjx5MlJS+ZkYIrW2h/4Bphxn3THtNZ17nokPmoln0Tx8fGMGTOGJk2a4Obmhp2dHWXLluWFF15gwYIFheZW8KJFixg9enS+lhkeHs7o0aOJiYnJ8n0HGweK2xcnJimGFENKvtZNCCGEECbZCjKVUiWB+kDYnU3zAR+lVJW8qtiT7Pjx49SpU4fhw4fj6urKyJEjmTRpEm+99RbXrl2jS5cuTJw4saCrCZiCzNDQ0HwtMzw8nNDQ0PsGmQAli5UE8m86IyGEEEJklN3J2H2AS1rrNACttVZKRQK+wMl70lZWSu0BDMB0rfUEi9X2CZCQkEDHjh2JjIxk8eLFdOzYMcP7I0aMYNWqVURH59/o6aLI1d4VO2s7ridcp4xLmWzvFx8fj7Ozcx7WTAghhHgyWHrgzx7AW2tdD3gBeEMp1S2rhEqp95RS59Mf8fHxFq5K0fTTTz9x/PhxPvzww0wBZrrWrVvTo0ePDNumTJlC3bp1cXR0pESJEjz77LPs2rUrQ5qzZ8+ilGL06NEsXryYevXq4eDggI+PD1999VWmcjZt2kTbtm0pWbIkDg4OeHt706lTJw4ePAhA8+bNmTlzJmDq85j+CA8PB2DHjh306dMHPz8/nJyccHV15ZlnnmHt2rWZymrevDkVKlTgwoULdOvWDVdXV1xcXHjhhRe4ePGiOV2fPn3MLacVK1Y0lzljxowM+Sml8HLyItWYSkxS5hbPu7+L2bNnU6dOHRwcHBg5cmSW37kQQgghcia7LZlRQBmllI3WOk0ppTC1YkbenUhrHXfX8/NKqd+AEGDuvRlqrb8Dvkt/7e3tXTg6GRawBQsWANC/f/9s7/PRRx/x7bff0rhxY77++mtiYmKYOHEiwcHBrFq1ipCQkAzply5dyuTJk3njjTfo168fv/76KyNGjMDHx4fevXsDcPToUdq0aYOfnx/Dhw/Hzc2NS5cusW7dOo4ePUqtWrUYMWIERqORjRs3MmvWLHP+1atXB2DhwoUcP36cnj174uPjw+XLl5k6dSqtW7dm7dq1NGvWLEO9bt++TbNmzQgJCeGbb77h0KFDTJw4kbi4ONasWQPAoEGDiIuLY+HChYwbNw5PT08AmjRpkul78XTy5OKtixkGAN1r4cKFXLhwgTfffJO33noLLy+vbH/vQgghhLg/ld0BJEqpcGCG1nqGUuol4GOtdf170pQBrmitjUopF2A58LPWetrD8vf29tbnz5/P8j2DwcDx48fx9/fH2to6c4JOneDUqWx9jnxRuTIsXpyrXT08PDAYDA/sb3i3Y8eOUb16dUJCQli9erV5VaQzZ84QEBBA5cqVzS2PZ8+epWLFihQrVowjR47g6+sLQGJiIuXLl6dSpUps27YNgPHjxzN06FCuXLlCyZIl71t+nz59mDlzZpYDkW7fvk2xYsUybLt27Ro1atQgMDCQ5cuXm7c3b96c9evX89///pf33nvPvH3IkCH8+OOPREREUK1aNQBGjx5NaGgoZ86coUKFCg/8fs7cPEN0YjRB5YJ47bXXzC2e6d+Fra0thw4dwt/f/4H5ZOWh56UQQgjxmFFKXdBae2cnbU5ulw8CBimljgMfA33vFDZVKdXpTpouwEGl1H5gG7AKmJ6DMp54cXFxuLi4ZDv9n3/+idaajz76KMOymxUrVuSVV17h0KFDnDyZsdvs888/bw4wARwdHWnUqFGGdOnrxM+fP5+0tNwt0Xh3gHn79m2io6NRStGgQQN27NiRKb21tTVvvfVWhm0tWrQAyPQZsit9OqP76dChQ64CTCGEEEI8WHZvl6O1PgY0zmJ7/7ue/wj8aJmq5UAuWw0Lo+LFi3PrVvYnET9z5gwAAQGZpi01bzt9+jRVqvwzEUDFihUzpXV3d88wmKhHjx6EhYXx5ptv8tFHH9G0aVPatWtHjx49KFWqVLbqduXKFT799FMWL17M9evXM7xn6nGRUZkyZbC3t89ULyDXA53S1zMHMGpjpvfv/l6EEEIIYTmy4k8hExAQQGxsrDl4zAvZubVrb2/PqlWr2LZtG++//z4pKSl88MEH+Pv7mwf2PIjRaKRNmzbMnj2b/v37M3fuXFasWMGqVat45plnsry9/qB65XZe0PT1zAGS05Izve/k5JSrfB/F+bjzfL/te45dP5bvZQshhBD5RYLMQqZLly4A/Pzzz9lKX6lSJQCOHDmS6b30belpcqNhw4aMGjWKNWvWcOTIEQwGQ4bJ17NqkQQ4ePAgBw4c4OOPP+brr7+ma9eutGnThlatWpGQkJDr+jyozPtxc3ADIMmQlO1gNdWQyq3kW6SkWWYy91RDKgsjFvLcr89R/vvyDFsxjHaz23Ez8aZF8hdCCCEKGwkyC5kBAwbg5+fHmDFjWLp0aZZp1qxZw++//w5Ap06dUEoxduzYDH0nz507x+zZs6lZs2aubgnfe3sbTMGqq6trhlvX6XNK3ryZMVhKb5U0GjPeog4PD2f79u05rs/d7lfm/VhbmepiMBoyrWeutSYhNYHohGiiYqM4Hn2c/Zf3s//Kfo5FH+PA1QMcvnqYqNgo4pLjsrzl/iAnok/w8eqP8Rnnw4tzX2TFyRV08O/AsEbDOBtzltcXv15oVm8SQgghLCnbfTJF/nBycuLvv/+mffv2dOjQgfbt29OyZUvc3Ny4fPkyy5cvZ8OGDeYVf6pWrcoHH3zAmDFjaN68OV27djVPYWQwGJgwIXdz4X/xxResXLmSjh07UrFiRdLS0liwYAEXL15k6NCh5nQNGzbkxx9/5O2336Zdu3bY2tryzDPPUK1aNapXr863335LYmIi/v7+HDx4kBkzZlCzZk3ziPfcaNiwIQCffPIJPXv2xN7enoYNG2bZ1/Rel25dIiE1gchY0+xbl+IvceTaP63ASikcbRxxdXDFwcaBxNRE4pLjuHL7ClduX8FKWVHcvjjF7YvjYpP1AK3E1ETmR8xn6p6prD+3HoBKbpX4quFXvFbnNcq6lEVrzaX4S8w5NIfx28cztNHQLPMSQgghiioJMgshf39/9u/fz8SJE1mwYAH/+te/uH37Nl5eXjRq1IhFixbRuXNnc/pvv/2WypUrM3HiRD766CPs7e1p2rQpoaGhBAUF5aoOnTt35uLFi8yZM4erV6/i5ORE1apVmTVrFr169TKn69mzJ7t372bOnDnMmTMHo9HIunXraN68OUuWLOH9999n2rRpJCcnU69ePf766y+mT5/+SEFmcHAwX375JZMnT6Zfv34YDAamT5/+0CDTztqOWym3uJVyyzxBu4ONA2Wcy+Bo64iTjRP2NvaZbsent3bGJccRmxxLTFKMaX8jxN6KZdrqabSp0gZ3R3em75tO2IEwYpNjsbO2o2fNnvSv15/mFZpjpf65caCUYnKHyey+uJsPV31IY5/GNCjXINffiRBCCFHYZHuezLz2SPNkCpENKWkpxCbH4mjjiIOtAzZWufsfK82YZgo4E2M5eeIknVZ1IsmQZH4/wCuAAfUG0Kt2LzycPB6Y1/7L+2k4tSGlnUuzd9Be3BzdclUnIYQQIj/kZJ5MackUTww7Gzu8bB59RR8bKxvcHd1xtXMlqXgS2/ptY/mp5VyOv0yPmj1oUK5BtgcnPVX6Kca3H8+gvwfR98++LOy+MMcDm4QQQojCSIJMIR5RzZI1earMU7nef0C9AYSfDee3Q7/xw/YfeLfRuxasnRBCCFEwZHS5EAUsvX+mv4c/w1cNZ8eFzKshCSGEEEWNBJlCFAIu9i780fUPrK2s6fZHN24k3ijoKgkhhBCPRIJMIQqJ2qVqM77deM7FnqPvn31l/kwhhBBFmgSZQhQi/ev155Var7D42GK+3/Z9QVdHCCGEyDUJMoUoRJRSTOowiaoeVRm+ejjbzz/a6khCCCFEQZEgU4hCxtnOmbld52JjZUO3edI/UwghRNEkQaYQhVDtUrX5X/v/ERkbSZ9FfaR/phBCiCJHgkwhCql+dfvRq3Yv/jr+F+O2jcuTMqJio1hyfIkEsUIIISxOgkwhCimlFBOfm0hVj6p8sPIDhq8aTlJa0sN3zAatNTP2zSBgQgAdfutAt3ndiEuOs0jeQgghBEiQKfLZ6NGjUUpx9uzZAqvDjBkzUEoRHh5eYHXILmc7Z5a+spSgckGM2TKGwCmB7Lq465HyvHb7Gl3mdqHvn31xtnPmWb9nmXdkHkE/BXHo6iEL1VwIIcSTToLMQiY8PByllPlhY2ODu7s7Tz31FAMGDGDDhg0FXUWRzyq5VWLz65v56pmvOBF9gkZTG/HZ2s9IMaTkOK+/j/9NrYm1WHh0Id0CunFw8EH+7vk349qO4/TN0zSc2pCwA2F58CmEEEI8aSTILKR69erFrFmzmD59Ov/+979p2rQpf/31F82aNaN79+4kJycXdBVzZeTIkSQmJlK+fPmCrkqRYmNlwychn7Br4C5qlarFFxu/oMFPDThw5UC29o9PiWfQX4Po+FtHktKSmP3ibOZ0mYOHkwdKKd5t9C7hr4VTwqEEvRf2ZvDfg0lOK5rnmBBCiMLBpqArILIWGBhIr169Mmz77rvvGDBgAGFhYZQoUYLJkycXUO1yz8bGBhubonHaJScno5TCzs6uoKtiVrtUbbb3386XG77ky41fUn9KfUY1G8VHwR9hY5X197olaguvLnyVUzdP0bJiS6Z3no6Pq0+mdE19m7Jn4B5eXvAyk3ZPYtelXfzR9Q8qlKiQx59KCCHE40haMosQBwcHpk2bRpUqVfj55585c+ZMhvePHj1Kz549KVWqFPb29lSpUoXQ0FBSU1Mz5XX06FF69epF2bJlsbe3x9vbm+7du3Pq1Clzmjlz5tCxY0d8fHywt7enVKlS9OzZk9OnT5vTpKamUrp0aZo3b55lnYcOHYqVlZU536z6ZPbp0welFDExMQwcOBBPT08cHR1p2bIlERERmfI8e/YsXbp0oXjx4hQvXpzOnTtz5swZKlSocN96PEx6vQ4ePMg777xD2bJlcXR05MiRI7nKLy/ZWdsR2iKUbf234efhx8h1I2nycxMirmX8rlIMKYxYM4KQ6SFcuHWB79t+z8reK7MMMNOVci7Fyl4r+TT4U3Zd3EXglECWnViWo/qlGdPYErWF0eGjeXvp2xy8cjBXn1MIIUTRVjSalISZra0tvXr1YvTo0axcuZJBgwYBsHPnTlq2bImXlxdvv/02JUuWZMeOHfzrX/9i//79LFiwwJzH1q1badOmDVprBgwYQPXq1bl69SorVqzg0KFDVK5cGYAJEybg6enJ4MGD8fLy4siRI0ydOpV169Zx6NAhPD09sbW15dVXX2Xs2LGcPn2aSpUqmctJSUlh9uzZNGvWzJzng7Rt25ayZcvyr3/9i6ioKMaNG0fnzp2JiIjA2toagOjoaEJCQrhy5QqDBw+mWrVqbNiwgRYtWnD79u1H/n579epF8eLFGT58OEajEXd390fOM6/UL1uf3QN3M2rdKMZuHUvdyXX54pkvGNZoGMeij9FrQS/2Xt5LvTL1mPXCLGp41chWvtZW1nzZ8ksaeTfi1UWv8tyvzzHy6ZGMajYKayvrTOm11py6eYpVp1ax8vRK1p5Zm2Gk+v/t/D+er/Y8I0NGElg20GKfP7vSjGn8ffxvPJ08aeLTBCsl/1sLIUS+0FoXike5cuX0/aSlpekjR47otLS0+6Z5XKxbt04Dety4cfdNs2DBAg3o9957z7ytVq1aOiAgQMfHx2dI+8MPP2hAr127VmuttdFo1NWqVdOOjo46IiIiU94Gg8H8/N68tNZ67dq1GtBff/21eduxY8c0oEeOHJkh7R9//KEB/csvv5i3jRo1SgP6zJkz5m2vvfaaBvSQIUMy7D927FgN6GXLlpm3ffjhhxrQc+bMyZA2fXuzZs0y1fle06dP14Bet25dpno988wz2T7PCtN5uSVyi/Yb76cZja4zqY62/7e9tgq10iPWjNDJacm5zvfUjVO63uR6mtHo1r+01lfjr2qttb6RcEPPOzxPD1w8UFf8vqJmNJrRaKtQK914amP9+drP9aZzm/S2qG2682+dze+3D2uvN0duttTHfiCj0agXRizUVf9X1Vy+93feetjyYXpr1FZtNBpznbfBaNDbz2/Xo9eN1i1nttT/2fgfbTAaHr6jEEIUccB5nc3Y7rFoyez0WydO3Tz18IT5pLJbZRb3XJxn+bu4uAAQF2dqLTp48CAHDx7kyy+/JDExkcTERHPadu3aAbBq1SpatGjB3r17OXr0KEOGDKFatWqZ8ray+qeVp1ixYoDpH5Fbt26RkpJCrVq1cHV1ZceOHeZ0/v7+hISEMHPmTEJDQ815TJ8+HVdXV1566aVsfa533nknw+sWLVoAcPLkSfO2v/76C29vb7p165Yh7QcffMCYMWOyVc6DDB061NxqWpQ09mnMvjf28cnqTxi/YzyV3Soz64VZNPZp/Ej5po9sf2fZO/y05yfqTq6Ld3Fvdl7ciVEbzWneCHyDNpXb0KJiC0o4lMiQx6Ieizhw5QBfbfyKuYfnsuzkMlpUaMHIp0fSokILlFKPVMesbI3ayoerPmRz1GaK2RZjZMhI0oxp/H74d8ZtG8e4beMo71qebgHd6BbQjcAygQ+tx/WE66w4uYJlJ5ex4tQKridcB8BKWbHmzBrWn1vPrBdm4eHkYfHPI4QQRdFjEWQ+aW7dugVA8eLFAcz9FkeMGMGIESOy3OfKlSsAnDhxAoA6deo8tJxdu3bx2WefsWHDBhISEjK8FxMTk+H1gAEDePXVV1mzZg2tW7fm0qVLrFixgv79++Po6Jitz1WxYsUMr9NvVUdHR5u3nTlzhiZNmmQKCEqWLEmJEhmDm9yoUqXKI+dRUJxsnfih/Q+80/AdyrqUxdE2e9/7wzjYODCl4xSa+jTlzaVvEp8ST+eqnWlTuQ2tK7WmsvvDu0LULlWbOS/NIbR5KF9v+pqwA2G0/KUljb0bM/LpkbSv0t4iwebx6ON8suYTFkQswFpZM7j+YD5v9jmlnUsD8FXLr9h1cRdzD89l7pG5jNkyhjFbxlDJrRLdanSje83uPFXqKZRSGLWRXRd3sezEMpaeXMrOCzvRmFZGqlO6DgPqDaB9lfY8Vfophi4fyox9MwicEsi8bvOoX7b+I3+WwuTglYPsv7KfuqXrUt2runQ5EEJky2MRZOZlq2FhdOCAadoaf39/AIxGU4vS8OHDad26dZb7lC1bNsPrh/1BP3fuHM2bN8fd3Z3Q0FD8/PxwcnJCKUWPHj3MZaZ76aWXeOedd5g2bRqtW7dm5syZGAwG+vXrl+3Pdb8WRJ2PSx46OTnlW1l5JTtBX268Vuc1etTsgbWV9X1Hsj9MVc+qzHh+BqOajeKbzd8wfd90nvv1OeqWrsvIp0fSqWqnXOV9Jf4KoetDmbJ7CgZt4IVqL/B1y6+p6lk1QzqlFEHlgggqF8S3rb9l+4Xt/H7od/448gf/2fwf/rP5P/h7+FO7VG3Cz4abWytd7V3pUqML7au0p12VdpR1yXg9Tes0jaY+TXl76ds0ndaUH9r9wKDAQY8cOKcZ0wBy/X0/ipikGH47+BvT9k3LsACAq70rDb0b0ti7MY29G9PQu2Gm1mshhIDHJMh8kqSmphIWFoa1tTVt27YFwM/PDzANCmrVqtUD908PTPft2/fAdIsWLeL27dv8/fffGUZsJyYmcvPmzUzpHR0defnll5k2bRo3b95kxowZ1KpVi6CgoJx8vIeqUKECJ06cQGud4Q/41atXM7WuCsuzt7G3SD4V3SoyqcMkRj49krFbxjJl9xS6zO2CnbUdVT2qElAygACvAGqWrEmAVwCV3CplOegoPiWe/275L2O2jOF26m2a+DRhTOsxNPFp8tA6KKVo5N2IRt6N+G/b/7Ilagu/H/qdeRHzmHdkXobWysY+jR8Y6Cml6F+vP/XK1OOluS8xeMlgNkdtZtJzkyhmVyzH38+pG6cYv3080/ZNIz4lHld7V9wc3XB3dMfd0R03h8zP3RzdKO1cmlola+Fi75LjMgGM2si6M+uYtm8aCyIWkJSWRDHbYrxe53VaVGzB/sv72Xp+K+vPrmflqZXm/ap7VjcFnT6mwFNaO4UQIEFmkZKcnEz//v05efIkAwcOpEKFCgDUq1ePGjVqMGHCBAYNGoSPT8YpapKSkkhNTcXFxYU6depQvXp1pk6dyltvvWUOOtOlB2/prYr3tlh+++23mbal69+/PxMmTGDIkCEcO3aMcePGWeiT/6Njx46MHTuWuXPn0r17d/P2sWPHWrwskfe8i3vzfbvv+ST4EybtmsSOizs4fPUwcw7NyZDOwcaBap7VCPAyBZ8BJQO4EHeB0PWhXLl9BX8Pf75p9Q2dq3bOVeuhlbIi2DeYYN9gvm/3PbdSbuWqda5emXrsHrib1xa9RtiBMPZd3sf8bvPx9/B/6L5aazZFbmLctnEsOroIjaZ2qdpU9ajKzaSb3Ei8wY3EG5y8cfKB68wrFH4eftQrU496pesRWDaQuqXr4ubodt99zsWcY8a+GUzfN51zsecACPYN5vU6r9M1oCvOds4A9Kptmrs3xZDCvsv72Bq1la3nTY9p+6Yxbd80wNTa2blaZ96s/yYNyjXIk363QojCT4LMQmr37t2EhYWZB90cPnyYBQsWcPnyZbp168b48ePNaZVS/PLLL7Rs2ZKaNWvSr18/qlWrRlxcHMeOHWP+/PksWLCA5s2bo5Ri2rRptGrVisDAQPMURtevX2flypW8++67dO7cmfbt2+Po6Ejv3r0ZMmQIxYsXZ+3atezatQsPj6wHNtStW5fAwEBmz56NnZ1dpsnkLeGjjz7i119/pXfv3mzdupWqVauyceNGtmzZgqenp/wxK6JKOZdiVPNR5te3km8RcT2Cw1cPc/jaYQ5dPcTha4eZfXB2xv2KlWLicxPpV7cftta2FqmLtZX1I93+dXN0Y1GPRXyz6RtGrhtJ/Sn1md55Ol1qdMkyfaohlbmH5zJu2zh2X9oNQAf/DgxrNOy+A6PSjGnEJMVwI/EGNxP/CUCj4qLYe3kvey7tYc6hORmC9YolKpoCzzuPmiVrsvHcRqbtm8aa02vQaMo4l+GT4E/oU6fPAwNjO2s7GpRrQINyDRjKUAAu3rpoDjo3nNvAL/t/4Zf9v1CvTD3eCnqLHjV74GRb9LujgOkfgt8O/camyE10C+hGs/LN5HePEFmQILOQCgsLIywsDCsrK1xcXPD19eW5556jd+/eNGvWLFP6wMBA9u7dy5dffsm8efO4fPkyJUqUoFKlSgwbNozatWub0zZq1Mg8h2ZYWBixsbGULFmSkJAQatWqBUDlypVZsmQJI0aM4IsvvsDe3p6WLVuyfv36LMtP179/f3bv3k2nTp3w9PS0+Pfi6enJpk2beP/99/n5559RStGiRQvWrVtHUFBQtgcZicJVxbexAAAO7klEQVTNxd7FHMTcLTYpliPXjnD42mFSDan0fqq3uZWtMLFSVnwS8gkNvRvSc35PXvrjJd5r9B7/afUfczB8I/EGU3ZP4ccdP3Lh1gUcbRwZXH8wQxsOzdSX9F42VjZ4Onni6XT/ayw2KZZ9l/ex59Iec+C58OhC5kfMz5DO1sqWF6u/yOt1X6dN5Ta57v9Z1qUsXWp0MQfTB64cYOLOicw6MIt+i/vx/sr36VunL4PrD8bPwy9XZWTX7ZTbbL+wnc2Rm7mReIOBgQOp7lXdInnvu7yPIcuGsClyEwATd02kTuk6DGs0jB41e2BnXXhWCLub1prtF7Yz9/Bc9l7eS3nX8vi5++Hn4Yefux9V3KvkupuFEPej8nNQxYN4e3vr8+fPZ/mewWDg+PHj+Pv7F8npZZ4k06dP5/XXX2fZsmXm6ZPyQ3R0NJ6engwaNIhJkyblS5lyXorsuBB3ge7zurM5ajPBvsF83fJrfjv4GzP2zyAhNYGyLmUZ0mAIAwMH4u6Yt5P/J6QmcODKAfZc2sOBKweo5lmNV2q9glcxrzwrMzYpllkHZjFh5wQirptmwmhTuQ1v1n+TDv4dsuxrm1MX4i6wOWozmyM3szlqM/su78OgDeb3FYqetXry2dOfUc0z89Rt2XEj8Qafrf2MSbtNv1/eCHyDvnX7MnPfTKbvm87t1NuUdi7NW0Fv8Ub9Nx74D8D9pBpS2XlxJ6tPr+b0zdM0KNeAp8s/TQ2vGrnq46q1Zvel3fx+6HfmHplLZGwkAMVsi3E7NfPiFaWdS1PFvYop+LwrAK3hVcNidwpE0aeUuqC19s5WWgkyhSUFBQVx7do1Tp8+nWHOTUtKTEzM1GL54YcfMnbsWObNm0eXLlnflrQ0OS9FdqUaUvlo9UeM2/ZPP+XAMoEMazSMrgFdC23rlyVprQk/G86EXRNYGLEQgzbg6+rLoMBBvFLrFVzsXbBSVigUVsrK/FDqn9cKhUZz6Oohc0C5OWqzOXgC8HLyoqlvU5p4N6Gpb1OM2sgXG75gxakVuQo2DUYDU/dMZcTaEUQnRhPsG8z/2v+POqX/mQbuZuJNpu6Zyv92/I+ouCgcbBzoXbs37zZ694ErbWmtibgewerTq1l9ejXhZ8O5lXIrUzp3R3eCfYN52vdpni7/NHXL1L1vi7PWmn2X95mn6Tp907QMsK+rr3marsAygcQmx3Ii+gQnbpz45+ed5zeTMg7udHNw4/lqz9O1RldaVmr5RJyv4v4kyBT56urVq6xZs4a1a9cydepUJkyYwODBg/OsvJCQECpXrkxgYCAGg4HVq1ezZMkSmjZtyvr16/PtHJHzUuTUgogF/HX8L16v8zrBvsFPbD++C3EX+GnPT0zZPYVL8ZceKa/qntVp6tOUpr5NaerTlCruVbL8XrdGbSV0fWiOgs2tUVt5e9nb7Lm0hzLOZRjTegwv13r5vsct1ZDKgogFjNs2ju0XtgOmVtthjYbRtnJblFJciLvAmjNrzIFl+ue3sbKhkXcjWlVsRatKrfDz8GP7+e1sOLeBDZEb2H1xt7l11tnOmSY+TcxBZ1C5IE5En+D3w78z9/BcTtwwzYdczqWcecGBhuUaZvt8u5F4wxx4RlyLYOnJpey7bJqRJH1QV9caXWldqbXFZpy4H601UXFRRFyL4GbSTZr4NMHX1TdPy3yQFEMKh64eItWQiquDKyUcSuBq74qDjcMTcz1LkCnyVXh4OC1atMDNzY1XX32V7777Ls9aMcE0wj0sLIxz586RmJiIj48PL774IqNGjcLZOf/658l5KcSjSTWksujoItadXYfBaECjMWojRm3M+Fxn3F6pRCWa+jalsXfjHK+wlJ1g83L8ZT5e/TEz98/E1sqWdxu9y2dPf5ajPotbo7Yybts45kfMx6iNVPOshkKZuwwA1CxZ0xxUPl3+6QfmH58Sz7bz20xB57kNbDu/jWRDMgDWytocgJZ2Lk3XGl3pHtCdxj6NLTaV1InoE8yPmM+8I/PMA9SK2xeno39HutboSpvKbR5pAYg0Yxqnb54m4loER64dIeJ6hOlxLSLTrX1/D39aVWxFy0otaVGhxQNnTngUBqOBiOsR7Lywk50Xd7Lr4i72X9lPiiElU1o7aztc7e8EnQ6u/zy/89PDyQNPJ088HD0yPS9qLcMSZAqRD+S8FKLo2hK1hdD1oaw8tdIcbH7c9GNWn17NqPBR3Eq5RZvKbRjfbvxDB2I9yLmYc/y440em7p1KMdtitK7cmlYVW/FMxWco41Im1/kmpyWz8+JONpzbwNbzW/Ep7kP3gO4E+wZbpJ/rg5y+eZr5R+YzL2IeOy6Ylhh2tnOmg38H2lZui42VDWnGNFINqaafxtQMz9PfSzGkcC72HBHXIzgefTxT8FbGuQzVvapTw7MG1b2q42Lnwvpz61l1epW5i4SVsiKwTCCtKpmC9SY+TXCwccjxZ9Jac+rmqQwB5Z5LezIEuF5OXgSVC6J+mfq42LsQkxRDbFIsscmxpufpP5NMP+OS48yrhD2Is51zhqCzumd1QnxDCPYNppRzqRx/lrwmQaYQ+UDOSyGKvruDzXQVSlRgXNtxuZ53NSvpf2sft1uq52LOmVs4t57fmuP9FYqKbhWp7lnd9PCqTg2vGlTzrHbfqcTSA8L0Lgdrz6w19yN1sHEgxDeElhVbUsq5FPEp8dl6XLl9hZikfxb0KG5fnPpl6xNUNsj0KBeET3GfHB0/ozZyK/kWMUkxRCdGcz3hOtEJ0Vk/T4wmOiGaawnXSEj9Zxlnfw9/gn2CCSkfQohvCJXcKhX4OSRBphD5QM5LIR4fW6K2MHHXRKp5VOO9xu890q3fJ9X5uPPsuLADa2VaetbW2tb008o2w/P092ytbCntXPqRv2uD0cDey3vNQeemyE3mrgT342DjgLOds/nh7uhO3dJ1CSobRP2y9fHz8CuQVau01hyLPsamyE1sjNzIpshN5sFbYGrdDfYNNrd01i5VO89bru8lQaYQ+UDOSyGEKHwSUxPZdn4bCakJGQLJ9Ecxu2K5ng+2IFyIu5Ah6Dxw5YD5NryLnQurX12daU7hvJSTILNIfMsF3TQsxIPI+SmEEIWHo60jLSq2KOhqWEy54uXoXrM73WuallKOSYpha9RWc9CZnWVrC0qRCTJtbGxISEjAxUVWJBCFQ0JCAjY2NhJkCiGEyDclHErQ3q897f3aF3RVHqrIBJkeHh5cvHgRDw8PXFxcsLEpElUXj6G0tDRu3bpFdHQ0Xl5eEmQKIYQQWSgykZq7uzsODg5cvXqV6OhojEZjQVdJPKGsrKywt7fHx8cHJyengq6OEEIIUSgVmSATwMnJiQoVKqC1Nj+EyE9KKfNDCCGEEPdXpILMdPJHXgghhBCicMv/SaCEEEIIIcRjT4JMIYQQQghhcRJkCiGEEEIIi8t2kKmU8lNKbVFKHVdK7VRKBdwnXT+l1Aml1Cml1E9KKVvLVVcIIYQQQhQFOWnJnAxM0Vr7A98AM+5NoJSqCPwbCAGqAKWAgY9eTSGEEEIIUZRkK8hUSpUE6gNhdzbNB3yUUlXuSfoSsFhrfVmb5heaBPS0VGWFEEIIIUTRkN0pjHyAS1rrNACttVZKRQK+wMm70vkC5+56ffbOtkyUUu8B7921yaCUupzN+liCMxCfj+WJvCPH8vEhx/LxIcfy8SHH8vFhiWPpld2EBTZPptb6O+C7gipfKXVea+1dUOULy5Fj+fiQY/n4kGP5+JBj+fjI72OZ3T6ZUUAZpZQNgDLNhO4LRN6TLhIof9frClmkEUIIIYQQj7lsBZla66vAHqDXnU1dgPNa65P3JJ0PdFJKlb4TiL4BzLFUZYUQQgghRNGQk9Hlg4BBSqnjwMdAXwCl1FSlVCcArfVpYBSwGVNfzWuYRqUXRgV2q15YnBzLx4ccy8eHHMvHhxzLx0e+HktlGgQuhBBCCCGE5ciKP0IIIYQQwuIkyBRCCCGEEBb3xAWZ2V0eUxQ+SqnxSqmzSimtlKpz13Y5pkWMUspBKbXozjHbr5Ralb64g1KqpFJq+Z3laQ8ppZ4u6PqKB1NKrVRKHVBK7VNKbVRK1b2zXa7NIkgp1ffO79nn77yWa7IIuvP38tid63KfUqr7ne35dl0+cUEm2VgeUxRa84BgMk74D3JMi6opQFWt9VPAn8DUO9v/A2zTWvthGmD4q1LKtoDqKLKnm9a6tta6DqaBBTPubJdrs4hRSlUABgDb7tos12TR1V1rXefO4/c72/LtunyigswcLI8pCiGt9Qat9fm7t8kxLZq01kla66X6n5GH2zDNqwvQDdOStGitdwIXgWb5XkmRbVrrmLteugJars2iRyllhemfvSFA8l1vyTX5mMjv6/KJCjLJYnlMTJPFZ7n0pSgS5Jg+HoYCfyqlPABbrfXdS8yeRY5noaeU+kUpFQX8G+iNXJtF0XvAZq317vQNck0Web8opQ4qpX5WSnmRz9flkxZkCiEKGaXUp0AV4JOCrovIPa31q1prH2AkpltwoghRStXEtNDKFwVdF2ExT2utawP1gOvAzPyuwJMWZGZ3eUxRdMgxLcKUUh8ALwLttdYJWutoIE0pVfquZBWQ41lkaK1nAi2A88i1WZSEYLrWTiilzgKNMPWb7oZck0WS1jryzs9U4HtMxzhf/2Y+UUFmDpbHFEWEHNOiSyn1HtATaH1Pn74/MC1Ji1IqCCgHrM//GorsUEqVUEqVvev180A0INdmEaK1nqi1LqO1rqC1roCpn/RArfVE5JoscpRSxZRSJe7a1BPYm99/M5+4FX+UUlUxjaTyAOKAvlrrgwVaKZEtSqnJwHNAaUx/xG5pravIMS16lFLemP6jPg3curM5WWvdUClVCpgFVARSgLe11usKpqbiYZRS5TEFIY6AEdNywh9orffJtVl0KaXCge+11ovkmix6lFKVMA3qsQYUpt+1Q7XWZ/PzunzigkwhhBBCCJH3nqjb5UIIIYQQIn9IkCmEEEIIISxOgkwhhBBCCGFxEmQKIYQQQgiLkyBTCCGEEEJYnASZQgghhBDC4iTIFEIIIYQQFidBphBCCCGEsDgJMoUQQgghhMX9P4AZl5kiRZFaAAAAAElFTkSuQmCC\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "decay_model_dir = niftynet_path + '/models/decay'\n", + "no_decay_model_dir = niftynet_path + '/models/no_decay'\n", + "\n", + "with open(os.path.join(decay_model_dir, 'training_niftynet_log'), 'r') as f:\n", + " lines = f.readlines()\n", + " data = ' '.join(lines)\n", + " last_run = data.rpartition('Parameters from random initialisations')[-1]\n", + " last_run_lines = last_run.split('\\n')\n", + " raw_lines = [l.split(',')[1:] for l in last_run_lines if 'loss' in l]\n", + " iterations = [int(l[0].split(':')[1].split(' ')[-1]) for l in raw_lines]\n", + " decay_CE_losses = [float(l[1].split('=')[1]) for l in raw_lines]\n", + "\n", + "with open(os.path.join(no_decay_model_dir, 'training_niftynet_log'), 'r') as f:\n", + " lines = f.readlines()\n", + " data = ' '.join(lines)\n", + " last_run = data.rpartition('Parameters from random initialisations')[-1]\n", + " last_run_lines = last_run.split('\\n')\n", + " raw_lines = [l.split(',')[1:] for l in last_run_lines if 'loss' in l]\n", + " iterations = [int(l[0].split(':')[1].split(' ')[-1]) for l in raw_lines]\n", + " no_decay_CE_losses = [float(l[1].split('=')[1]) for l in raw_lines]\n", + "\n", + "fig = plt.figure(figsize=(10, 4.5), dpi=80)\n", + "plt.plot([np.mean(no_decay_CE_losses[l:l+10]) for l in range(0,len(no_decay_CE_losses), 10)],\n", + " color='red', label='Constant lr')\n", + "plt.plot([np.mean(decay_CE_losses[l:l+10]) for l in range(0,len(decay_CE_losses), 10)],\n", + " color='green', label='Decaying lr')\n", + "plt.title(\"Smoothed loss curves\", fontsize=20)\n", + "plt.legend(fontsize=16)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Customising the learning rate scheduler:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Currently the application is set up such that the learning rate is halved every third training iteration. This is an arbitrary default setup and it is likely you'll want to alter this to suit your purposes. Let's look at the *set_iteration_update* method of the *DecayLearningRateApplication* class in the application (*decay_lr_application.py*):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def set_iteration_update(self, iteration_message):\n", + " \"\"\"\n", + " This function will be called by the application engine at each\n", + " iteration.\n", + " \"\"\"\n", + " current_iter = iteration_message.current_iter\n", + " if iteration_message.is_training:\n", + " if current_iter > 0 and current_iter % 3 == 0:\n", + " self.current_lr = self.current_lr / 2.0\n", + " iteration_message.data_feed_dict[self.is_validation] = False\n", + " elif iteration_message.is_validation:\n", + " iteration_message.data_feed_dict[self.is_validation] = True\n", + " iteration_message.data_feed_dict[self.learning_rate] = self.current_lr" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The relevant subsection we will want to focus on is contained in two lines:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if current_iter > 0 and current_iter % 3 == 0:\n", + " self.current_lr = self.current_lr / 2.0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The second line contains the logic that changes the learning rate wheras the first line stipulates the condition under which this will occur. As such only these two lines need to be changed if the scheduling is to be changed. For example, if we'd like to reduce the learning rate by 1% every 5 iterations then this snippet of code would look like:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if current_iter > 0 and current_iter % 5 == 0:\n", + " self.current_lr = self.current_lr * 0.99" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Similarly, if we'd like for the learning rate to decay exponentially every iteration modulated by some factor *k*:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if current_iter > 0:\n", + " self.current_lr = self.current_lr * np.exp(-k * current_iter)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this demo we have covered the following:
\n", + "1. How to run NiftyNet using the learning rate decay application from the command line and from python code directly\n", + "2. How to edit the learning rate application to schedule the learning rate to suit one's needs" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/demos/Learning_Rate_Decay/README.md b/demos/Learning_Rate_Decay/README.md new file mode 100644 index 00000000..794d0a1d --- /dev/null +++ b/demos/Learning_Rate_Decay/README.md @@ -0,0 +1,29 @@ +# Learning rate decay application + +This application implements a simple learning rate schedule of +"halving the learning rate every 3 iterations" for segmentation applications. + +The concept is general and could be used for other types of application. A brief demo is provide which can be fully run from a jupyter notebook provided a a working installation of NiftyNet exists on your system. + +The core function is implemented by: + +1) Adding a `self.learning_rate` placeholder, and connect it to the network +in `connect_data_and_network` function + +2) Adding a `self.current_lr` variable to keep track of the current learning rate + +3) Overriding the default `set_iteration_update` function provided in `BaseApplication` +so that `self.current_lr` is changed according to the `current_iter`. + +4) To feed the `self.current_lr` value to the network, the data feeding dictionary +is updated within the customised `set_iteration_update` function, by +``` +iteration_message.data_feed_dict[self.learning_rate] = self.current_lr +``` +`iteration_message.data_feed_dict` will be used in +`tf.Session.run(..., feed_dict=iteration_message.data_feed_dict)` by the engine +at each iteration. + + +*This demo only supports NiftyNet cloned from [GitHub](https://github.com/NifTK/NiftyNet).* +Further demos/ trained models can be found at [NiftyNet model zoo](https://github.com/NifTK/NiftyNetModelZoo/blob/master/dense_vnet_abdominal_ct_model_zoo.md). diff --git a/demos/Learning_Rate_Decay/decay_lr_application.py b/demos/Learning_Rate_Decay/decay_lr_application.py new file mode 100644 index 00000000..48b69850 --- /dev/null +++ b/demos/Learning_Rate_Decay/decay_lr_application.py @@ -0,0 +1,87 @@ +import tensorflow as tf + +from niftynet.application.segmentation_application import \ + SegmentationApplication +from niftynet.engine.application_factory import OptimiserFactory +from niftynet.engine.application_variables import CONSOLE +from niftynet.engine.application_variables import TF_SUMMARIES +from niftynet.layer.loss_segmentation import LossFunction + +SUPPORTED_INPUT = set(['image', 'label', 'weight']) + + +class DecayLearningRateApplication(SegmentationApplication): + REQUIRED_CONFIG_SECTION = "SEGMENTATION" + + def __init__(self, net_param, action_param, is_training): + SegmentationApplication.__init__( + self, net_param, action_param, is_training) + tf.logging.info('starting decay learning segmentation application') + self.learning_rate = None + self.current_lr = action_param.lr + if self.action_param.validation_every_n > 0: + raise NotImplementedError("validation process is not implemented " + "in this demo.") + + def connect_data_and_network(self, + outputs_collector=None, + gradients_collector=None): + data_dict = self.get_sampler()[0][0].pop_batch_op() + image = tf.cast(data_dict['image'], tf.float32) + net_out = self.net(image, self.is_training) + + if self.is_training: + with tf.name_scope('Optimiser'): + self.learning_rate = tf.placeholder(tf.float32, shape=[]) + optimiser_class = OptimiserFactory.create( + name=self.action_param.optimiser) + self.optimiser = optimiser_class.get_instance( + learning_rate=self.learning_rate) + loss_func = LossFunction( + n_class=self.segmentation_param.num_classes, + loss_type=self.action_param.loss_type) + data_loss = loss_func( + prediction=net_out, + ground_truth=data_dict.get('label', None), + weight_map=data_dict.get('weight', None)) + + loss = data_loss + reg_losses = tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES) + + if self.net_param.decay > 0.0 and reg_losses: + reg_loss = tf.reduce_mean( + [tf.reduce_mean(reg_loss) for reg_loss in reg_losses]) + loss = data_loss + reg_loss + grads = self.optimiser.compute_gradients(loss) + # collecting gradients variables + gradients_collector.add_to_collection([grads]) + # collecting output variables + outputs_collector.add_to_collection( + var=data_loss, name='loss', + average_over_devices=False, collection=CONSOLE) + outputs_collector.add_to_collection( + var=self.learning_rate, name='lr', + average_over_devices=False, collection=CONSOLE) + outputs_collector.add_to_collection( + var=data_loss, name='loss', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + else: + # converting logits into final output for + # classification probabilities or argmax classification labels + SegmentationApplication.connect_data_and_network( + self, outputs_collector, gradients_collector) + + def set_iteration_update(self, iteration_message): + """ + This function will be called by the application engine at each + iteration. + """ + current_iter = iteration_message.current_iter + if iteration_message.is_training: + if current_iter > 0 and current_iter % 5 == 0: + self.current_lr = self.current_lr / 1.02 + iteration_message.data_feed_dict[self.is_validation] = False + elif iteration_message.is_validation: + iteration_message.data_feed_dict[self.is_validation] = True + iteration_message.data_feed_dict[self.learning_rate] = self.current_lr diff --git a/demos/Learning_Rate_Decay/learning_rate_demo_train_config.ini b/demos/Learning_Rate_Decay/learning_rate_demo_train_config.ini new file mode 100644 index 00000000..def8ce05 --- /dev/null +++ b/demos/Learning_Rate_Decay/learning_rate_demo_train_config.ini @@ -0,0 +1,63 @@ +############################ input configuration sections +[images] # Name this as you see fit +path_to_search = ./data/decathlon_hippocampus +filename_contains = img_hippocampus_ +filename_not_contains = ._ +spatial_window_size = (24, 24, 24) +interp_order = 3 + +[label] +path_to_search = ./data/decathlon_hippocampus +filename_contains = label_hippocampus_ +filename_not_contains = ._ +spatial_window_size = (24, 24, 24) +interp_order = 0 + +############################## system configuration sections +[SYSTEM] +cuda_devices = "" +num_threads = 6 +num_gpus = 1 +model_dir = ./models/model_multimodal_toy +queue_length = 20 + +[NETWORK] +name = highres3dnet +activation_function = prelu +batch_size = 1 +decay = 0 +reg_type = L2 + +# Volume level pre-processing +volume_padding_size = 0 +# Normalisation +whitening = True +normalise_foreground_only = False + +[TRAINING] +sample_per_volume = 1 +optimiser = gradientdescent +# rotation_angle = (-10.0, 10.0) +# scaling_percentage = (-10.0, 10.0) +# random_flipping_axes= 1 +lr = 0.0001 +loss_type = CrossEntropy +starting_iter = 0 +save_every_n = 100 +max_iter = 500 +max_checkpoints = 20 + +[INFERENCE] +border = 5 +#inference_iter = 10 +save_seg_dir = ./output/toy +output_interp_order = 0 +spatial_window_size = (64, 64, 64) + +############################ custom configuration sections +[SEGMENTATION] +image = images +label = label +output_prob = False +num_classes = 3 +label_normalisation = False diff --git a/demos/PyTorchNiftyNet/segmentation.py b/demos/PyTorchNiftyNet/segmentation.py index 9f14b8c5..c3c680f8 100644 --- a/demos/PyTorchNiftyNet/segmentation.py +++ b/demos/PyTorchNiftyNet/segmentation.py @@ -171,7 +171,7 @@ def inference(sampler, model, device, pred_path, cp_path): outputs = outputs.cpu().numpy() outputs = np.transpose(outputs, (0, 2, 3, 4, 1)) - output.decode_batch(outputs.astype(np.float32), + output.decode_batch({'window_image': outputs.astype(np.float32)}, batch_output['image_location']) diff --git a/demos/module_examples/FullCSVReaderDemo.ipynb b/demos/module_examples/FullCSVReaderDemo.ipynb new file mode 100644 index 00000000..4d23b273 --- /dev/null +++ b/demos/module_examples/FullCSVReaderDemo.ipynb @@ -0,0 +1,744 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "import os\n", + "niftynet_path = '/home/tom/phd/NiftyNet-Generator-PR/NiftyNet'\n", + "sys.path.append(niftynet_path)\n", + "os.environ['CUDA_VISIBLE_DEVICES'] = ''\n", + "import pandas as pd\n", + "import numpy as np\n", + "import tensorflow as tf\n", + "import matplotlib.pyplot as plt\n", + "from niftynet.io.image_reader import ImageReader\n", + "from niftynet.io.image_sets_partitioner import ImageSetsPartitioner\n", + "from collections import namedtuple\n", + "\n", + "from niftynet.contrib.preprocessors.preprocessing import Preprocessing\n", + "from niftynet.contrib.csv_reader.sampler_csv_rows import ImageWindowDatasetCSV\n", + "from niftynet.contrib.csv_reader.sampler_resize_v2_csv import ResizeSamplerCSV as ResizeSampler\n", + "from niftynet.contrib.csv_reader.csv_reader import CSVReader" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "#### Some setup\n", + "NetParam = namedtuple('NetParam', 'normalise_foreground_only foreground_type multimod_foreground_type histogram_ref_file norm_type cutoff normalisation whitening')\n", + "ActionParam = namedtuple('ActionParam', 'random_flipping_axes scaling_percentage rotation_angle rotation_angle_x rotation_angle_y rotation_angle_z do_elastic_deformation num_ctrl_points deformation_sigma proportion_to_deform')\n", + "class TaskParam:\n", + " def __init__(self, classes):\n", + " self.image = classes\n", + "net_param = NetParam(normalise_foreground_only=False, foreground_type='threshold_plus', multimod_foreground_type = 'and', histogram_ref_file='mapping.txt', norm_type='percentile', cutoff=(0.05, 0.95), normalisation=False, whitening=True)\n", + "action_param = ActionParam(random_flipping_axes=[], scaling_percentage=[], rotation_angle=None, rotation_angle_x=None, rotation_angle_y=None, rotation_angle_z=None, do_elastic_deformation=False, num_ctrl_points=6, deformation_sigma=50, proportion_to_deform=0.9)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1) Create a csv of labels and show how it can be returned by the CSV Reader\n" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Accessing: https://github.com/NifTK/NiftyNetModelZoo\n", + "mr_ct_regression_model_zoo_data: FAIL. \n", + "No NiftyNet example was found for mr_ct_regression_model_zoo_data.\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
subject_idlabel
0RANRAN
1HALHAL
2MILMIL
3CHACHA
4GRAGRA
5PLAPLA
6NARNAR
7WEBWEB
8PARPAR
9HONHON
10HAFHAF
11LEWLEW
12SOUSOU
13SPESPE
14CRICRI
\n", + "
" + ], + "text/plain": [ + " subject_id label\n", + "0 RAN RAN\n", + "1 HAL HAL\n", + "2 MIL MIL\n", + "3 CHA CHA\n", + "4 GRA GRA\n", + "5 PLA PLA\n", + "6 NAR NAR\n", + "7 WEB WEB\n", + "8 PAR PAR\n", + "9 HON HON\n", + "10 HAF HAF\n", + "11 LEW LEW\n", + "12 SOU SOU\n", + "13 SPE SPE\n", + "14 CRI CRI" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from niftynet.utilities.download import download\n", + "download('mr_ct_regression_model_zoo_data')\n", + "labels_location = 'ct.csv'\n", + "files = [file for file in os.listdir('/home/tom/niftynet/data/mr_ct_regression/CT_zero_mean') if file.endswith('.nii.gz')]\n", + "pd.DataFrame(data=[(file.replace('.nii.gz', ''), file.replace('.nii.gz', '')) for file in files]).to_csv('label.csv', index=None, header=['subject_id', 'label'])\n", + "pd.read_csv('label.csv')" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1mINFO:niftynet:\u001b[0m \n", + "\n", + "Number of subjects 15, input section names: ['subject_id', 'CT']\n", + "-- using all subjects (without data partitioning).\n", + "\n", + "\u001b[1mINFO:niftynet:\u001b[0m Image reader: loading 15 subjects from sections ['CT'] as input [image]\n", + "\u001b[1mWARNING:niftynet:\u001b[0m This method will read your entire csv into memory\n", + "One sample from the csv_reader: [0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]\n", + "\u001b[1mINFO:niftynet:\u001b[0m reading size of preprocessed images\n", + "\u001b[1mWARNING:niftynet:\u001b[0m sampler queue_length should be larger than batch_size, defaulting to batch_size * 5.0 (10).\n", + "(1, 100, 100, 1, 1, 1)\n", + "(1, 15, 1, 1, 1, 1)\n" + ] + } + ], + "source": [ + "#### Testing the CSV Reader on labels\n", + "# Make sure we accept 'Label', 'label', 'LABEL'\n", + "task_param = TaskParam(['image'])\n", + "image_data_param = {'CT': {'path_to_search': '~/niftynet/data/mr_ct_regression/CT_zero_mean', 'filename_contains': 'nii'}}\n", + "#csv_data_file is a csv with data\n", + "csv_data_param = {'label': {'csv_data_file': 'label.csv', 'to_ohe': True}}\n", + "grouping_param = {'image': (['CT'])}\n", + "\n", + "image_sets_partitioner = ImageSetsPartitioner().initialise(image_data_param)\n", + "image_reader = ImageReader().initialise(image_data_param, grouping_param, file_list=image_sets_partitioner.all_files)\n", + "preprocessing = Preprocessing(net_param, action_param, task_param)\n", + "normalisation_layers = preprocessing.prepare_normalisation_layers()\n", + "augmentation_layers = preprocessing.prepare_augmentation_layers()\n", + "image_reader.add_preprocessing_layers(normalisation_layers + augmentation_layers)\n", + "csv_reader = CSVReader(('label',)).initialise(csv_data_param, {'label': (['label'])}, file_list=image_sets_partitioner.all_files)\n", + "print('One sample from the csv_reader:', np.squeeze(csv_reader(idx=13)[1]['label']))\n", + "window_sizes = {'image': (100, 100, 1), 'label': (1, 1, 1)}\n", + "sampler = ResizeSampler(reader=image_reader,\n", + " csv_reader=csv_reader,\n", + " window_sizes=window_sizes,\n", + " num_threads=2,\n", + " smaller_final_batch_mode='drop',\n", + " batch_size=2,\n", + " queue_length=2)\n", + "sample = next(sampler())\n", + "print(sample['image'].shape)\n", + "print(sample['label'].shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2) Create a csv of features and show how it can be returned by the CSV Reader\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Accessing: https://github.com/NifTK/NiftyNetModelZoo\n", + "mr_ct_regression_model_zoo_data: FAIL. \n", + "No NiftyNet example was found for mr_ct_regression_model_zoo_data.\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
subject_id0123456789
0RAN-2.0189050.0167000.7535000.225668-0.4531370.0077400.405050-0.038025-1.144459-0.437604
1HAL0.7506081.3392360.3931030.4039520.0349921.7439240.2225660.411477-0.5572210.407324
2MIL-0.581589-0.988355-1.5367940.344513-0.161370-0.118697-0.3222090.063637-1.028091-0.392706
3CHA2.234084-0.264239-0.923163-0.8176170.3883250.560078-0.4771641.966860-0.0185100.442490
4GRA0.6531140.4774930.6508121.3530490.734319-1.5393320.653350-0.752011-0.634769-0.231503
5PLA-0.983351-1.6005700.223609-1.100547-0.4302551.315413-0.702730-1.838355-0.8100462.419473
6NAR0.810422-2.304446-0.0792810.4564760.6433750.2688230.3667550.5307361.3453580.515989
7WEB0.710464-0.665521-1.130775-0.437872-0.359845-0.128057-0.472871-0.0607461.754929-1.009953
8PAR1.380408-0.825570-0.630116-2.312509-0.335770-1.176994-0.455428-0.757432-0.1711520.803127
9HON-0.312656-0.7071390.5718080.6828540.9078170.067288-0.384104-1.0307020.015677-0.808565
10HAF2.413476-0.2588320.351502-1.447595-0.8611271.074561-0.5500001.0824760.487512-0.926261
11LEW0.466895-0.3750610.6579981.2034851.3471310.552526-0.7050731.992426-0.8164160.532992
12SOU-0.009581-0.436214-0.6002872.1471110.839317-0.4445720.1975500.5486110.334053-1.498843
13SPE0.238420-0.966039-0.0731831.595601-0.093269-1.048532-0.657099-1.1789050.795620-0.974147
14CRI-0.1887911.6119990.423356-1.644961-0.961844-1.4469890.869527-1.843371-2.446698-1.428567
\n", + "
" + ], + "text/plain": [ + " subject_id 0 1 2 3 4 5 \\\n", + "0 RAN -2.018905 0.016700 0.753500 0.225668 -0.453137 0.007740 \n", + "1 HAL 0.750608 1.339236 0.393103 0.403952 0.034992 1.743924 \n", + "2 MIL -0.581589 -0.988355 -1.536794 0.344513 -0.161370 -0.118697 \n", + "3 CHA 2.234084 -0.264239 -0.923163 -0.817617 0.388325 0.560078 \n", + "4 GRA 0.653114 0.477493 0.650812 1.353049 0.734319 -1.539332 \n", + "5 PLA -0.983351 -1.600570 0.223609 -1.100547 -0.430255 1.315413 \n", + "6 NAR 0.810422 -2.304446 -0.079281 0.456476 0.643375 0.268823 \n", + "7 WEB 0.710464 -0.665521 -1.130775 -0.437872 -0.359845 -0.128057 \n", + "8 PAR 1.380408 -0.825570 -0.630116 -2.312509 -0.335770 -1.176994 \n", + "9 HON -0.312656 -0.707139 0.571808 0.682854 0.907817 0.067288 \n", + "10 HAF 2.413476 -0.258832 0.351502 -1.447595 -0.861127 1.074561 \n", + "11 LEW 0.466895 -0.375061 0.657998 1.203485 1.347131 0.552526 \n", + "12 SOU -0.009581 -0.436214 -0.600287 2.147111 0.839317 -0.444572 \n", + "13 SPE 0.238420 -0.966039 -0.073183 1.595601 -0.093269 -1.048532 \n", + "14 CRI -0.188791 1.611999 0.423356 -1.644961 -0.961844 -1.446989 \n", + "\n", + " 6 7 8 9 \n", + "0 0.405050 -0.038025 -1.144459 -0.437604 \n", + "1 0.222566 0.411477 -0.557221 0.407324 \n", + "2 -0.322209 0.063637 -1.028091 -0.392706 \n", + "3 -0.477164 1.966860 -0.018510 0.442490 \n", + "4 0.653350 -0.752011 -0.634769 -0.231503 \n", + "5 -0.702730 -1.838355 -0.810046 2.419473 \n", + "6 0.366755 0.530736 1.345358 0.515989 \n", + "7 -0.472871 -0.060746 1.754929 -1.009953 \n", + "8 -0.455428 -0.757432 -0.171152 0.803127 \n", + "9 -0.384104 -1.030702 0.015677 -0.808565 \n", + "10 -0.550000 1.082476 0.487512 -0.926261 \n", + "11 -0.705073 1.992426 -0.816416 0.532992 \n", + "12 0.197550 0.548611 0.334053 -1.498843 \n", + "13 -0.657099 -1.178905 0.795620 -0.974147 \n", + "14 0.869527 -1.843371 -2.446698 -1.428567 " + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from niftynet.utilities.download import download\n", + "download('mr_ct_regression_model_zoo_data')\n", + "labels_location = 'ct.csv'\n", + "files = [file.replace('.nii.gz', '') for file in os.listdir('/home/tom/niftynet/data/mr_ct_regression/CT_zero_mean') if file.endswith('.nii.gz')]\n", + "\n", + "pd.DataFrame(data=[tuple([file] + list(np.random.randn(10))) for file in files]).to_csv('features.csv', index=None, header=['subject_id'] + [str(x) for x in range(10)])\n", + "pd.read_csv('features.csv')" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1mINFO:niftynet:\u001b[0m \n", + "\n", + "Number of subjects 15, input section names: ['subject_id', 'CT']\n", + "-- using all subjects (without data partitioning).\n", + "\n", + "\u001b[1mINFO:niftynet:\u001b[0m Image reader: loading 15 subjects from sections ['CT'] as input [image]\n", + "\u001b[1mWARNING:niftynet:\u001b[0m This method will read your entire csv into memory\n", + "One sample from the csv_reader: [ 0.23841972 -0.96603888 -0.07318273 1.59560139 -0.09326917 -1.04853203\n", + " -0.65709902 -1.17890471 0.7956195 -0.97414747]\n", + "\u001b[1mINFO:niftynet:\u001b[0m reading size of preprocessed images\n", + "\u001b[1mWARNING:niftynet:\u001b[0m sampler queue_length should be larger than batch_size, defaulting to batch_size * 5.0 (10).\n", + "(1, 100, 100, 1, 1, 1)\n", + "(1, 10, 1, 1, 1, 1)\n", + "dict_keys(['image_location', 'image', 'features', 'features_location'])\n" + ] + } + ], + "source": [ + "task_param = TaskParam(['image'])\n", + "image_data_param = {'CT': {'path_to_search': '~/niftynet/data/mr_ct_regression/CT_zero_mean', 'filename_contains': 'nii'}}\n", + "csv_data_param = {'features': {'csv_data_file': 'features.csv', 'to_ohe': False}}\n", + "grouping_param = {'image': (['CT'])}\n", + "image_sets_partitioner = ImageSetsPartitioner().initialise(image_data_param)\n", + "image_reader = ImageReader().initialise(image_data_param, grouping_param, file_list=image_sets_partitioner.all_files)\n", + "preprocessing = Preprocessing(net_param, action_param, task_param)\n", + "normalisation_layers = preprocessing.prepare_normalisation_layers()\n", + "augmentation_layers = preprocessing.prepare_augmentation_layers()\n", + "image_reader.add_preprocessing_layers(normalisation_layers + augmentation_layers)\n", + "csv_reader = CSVReader(('features',)).initialise(csv_data_param, {'features': ['features']}, file_list=image_sets_partitioner.all_files)\n", + "print('One sample from the csv_reader:', np.squeeze(csv_reader(idx=13)[1]['features']))\n", + "window_sizes = {'image': (100, 100, 1), 'features': (1, 1, 1)}\n", + "sampler = ResizeSampler(reader=image_reader,\n", + " csv_reader=csv_reader,\n", + " window_sizes=window_sizes,\n", + " num_threads=2,\n", + " smaller_final_batch_mode='drop',\n", + " batch_size=2,\n", + " queue_length=2)\n", + "sample = next(sampler())\n", + "print(sample['image'].shape)\n", + "print(sample['features'].shape)\n", + "print(sample.keys())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Testing the CSV Reader on labels AND Features\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1mINFO:niftynet:\u001b[0m \n", + "\n", + "Number of subjects 15, input section names: ['subject_id', 'CT']\n", + "-- using all subjects (without data partitioning).\n", + "\n", + "\u001b[1mINFO:niftynet:\u001b[0m Image reader: loading 15 subjects from sections ['CT'] as input [image]\n", + "\u001b[1mWARNING:niftynet:\u001b[0m This method will read your entire csv into memory\n", + "\u001b[1mWARNING:niftynet:\u001b[0m This method will read your entire csv into memory\n", + "One sample from the csv_reader: [0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]\n", + "\u001b[1mINFO:niftynet:\u001b[0m reading size of preprocessed images\n", + "\u001b[1mWARNING:niftynet:\u001b[0m sampler queue_length should be larger than batch_size, defaulting to batch_size * 5.0 (10).\n", + "(1, 100, 100, 1, 1, 1)\n", + "(1, 15, 1, 1, 1, 1)\n", + "(1, 10, 1, 1, 1, 1)\n" + ] + } + ], + "source": [ + "# Make sure we accept 'Label', 'label', 'LABEL'\n", + "task_param = TaskParam(['image'])\n", + "image_data_param = {'CT': {'path_to_search': '~/niftynet/data/mr_ct_regression/CT_zero_mean', 'filename_contains': 'nii'}}\n", + "csv_data_param = {'label': {'csv_data_file': 'label.csv', 'to_ohe': True},\n", + " 'features': {'csv_data_file': 'features.csv', 'to_ohe': False}}\n", + "grouping_param = {'image': (['CT'])}\n", + "\n", + "image_sets_partitioner = ImageSetsPartitioner().initialise(image_data_param)\n", + "image_reader = ImageReader().initialise(image_data_param, grouping_param, file_list=image_sets_partitioner.all_files)\n", + "preprocessing = Preprocessing(net_param, action_param, task_param)\n", + "normalisation_layers = preprocessing.prepare_normalisation_layers()\n", + "augmentation_layers = preprocessing.prepare_augmentation_layers()\n", + "image_reader.add_preprocessing_layers(normalisation_layers + augmentation_layers)\n", + "\n", + "csv_reader = CSVReader(('label', 'features')).initialise(csv_data_param,\n", + " {'label': (['label']), 'features': (['features'])},\n", + " file_list=image_sets_partitioner.all_files)\n", + "\n", + "\n", + "print('One sample from the csv_reader:', np.squeeze(csv_reader(idx=13)[1]['label']))\n", + "window_sizes = {'image': (100, 100, 1), 'label': (1, 1, 1)}\n", + "sampler = ResizeSampler(reader=image_reader,\n", + " csv_reader=csv_reader,\n", + " window_sizes=window_sizes,\n", + " num_threads=2,\n", + " smaller_final_batch_mode='drop',\n", + " batch_size=2,\n", + " queue_length=2)\n", + "sample = next(sampler())\n", + "print(sample['image'].shape)\n", + "print(sample['label'].shape)\n", + "print(sample['features'].shape)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "modalities = ['t1ce.', 't1.', 'flair.', 't2.']\n", + "def get_modality(string):\n", + " return modalities[[True if mod in string else False for mod in modalities].index(True)][:-1]\n", + " \n", + "files = [(file.replace('.nii.gz', ''), get_modality(file)) \\\n", + " for file in os.listdir('/home/tom/data/BRATS_18_SPLITS/train') if 'seg' not in file]\n", + "pd.DataFrame(data=files, columns=['subject_id', 'label']).to_csv('/home/tom/phd/NiftyNet-Generator-PR/NiftyNet/modality_labels.csv', index=None)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.4" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/demos/module_examples/ImageReader.ipynb b/demos/module_examples/ImageReader.ipynb index bfafb938..3177bf90 100644 --- a/demos/module_examples/ImageReader.ipynb +++ b/demos/module_examples/ImageReader.ipynb @@ -41,18 +41,7 @@ "cell_type": "code", "execution_count": 3, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO:tensorflow:TensorFlow version 1.5.0\n", - "CRITICAL:tensorflow:Optional Python module cv2 not found, please install cv2 and retry if the application fails.\n", - "INFO:tensorflow:Available Image Loaders:\n", - "['nibabel', 'skimage', 'pillow', 'simpleitk', 'dummy'].\n" - ] - } - ], + "outputs": [], "source": [ "import sys\n", "niftynet_path = '/Users/bar/Documents/Niftynet/'\n", @@ -70,15 +59,15 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Accessing: https://cmiclab.cs.ucl.ac.uk/CMIC/NiftyNetExampleServer\n", - "anisotropic_nets_brats_challenge_model_zoo_data: OK. \n", + "Accessing: https://github.com/NifTK/NiftyNetModelZoo\n", + "anisotropic_nets_brats_challenge_model_zoo: OK. \n", "Already downloaded. Use the -r option to download again.\n" ] }, @@ -88,14 +77,14 @@ "True" ] }, - "execution_count": 6, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from niftynet.utilities.download import download\n", - "download('anisotropic_nets_brats_challenge_model_zoo_data')" + "download('anisotropic_nets_brats_challenge_model_zoo')" ] }, { @@ -107,22 +96,9 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1mINFO:niftynet:\u001b[0m \n", - "\n", - "Number of subjects 10, input section names: ['subject_id', 'MR']\n", - "-- using all subjects (without data partitioning).\n", - "\n", - "\u001b[1mINFO:niftynet:\u001b[0m Image reader: loading 10 subjects from sections ('MR',) as input [MR]\n" - ] - } - ], + "outputs": [], "source": [ "from niftynet.io.image_reader import ImageReader\n", "\n", @@ -132,30 +108,17 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "({'MR': (134, 167, 135, 1, 1)}, {'MR': tf.float32})" - ] - }, - "execution_count": 25, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "reader.shapes, reader.tf_dtypes" ] }, { "cell_type": "code", - "execution_count": 26, - "metadata": { - "collapsed": true - }, + "execution_count": null, + "metadata": {}, "outputs": [], "source": [ "# read data using the initialised reader\n", @@ -164,39 +127,18 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "((134, 167, 135, 1, 1), dtype('float32'))" - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "image_data['MR'].shape, image_data['MR'].dtype" ] }, { "cell_type": "code", - "execution_count": 30, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "3 image: (134, 167, 135, 1, 1)\n", - "6 image: (152, 169, 130, 1, 1)\n", - "8 image: (152, 169, 130, 1, 1)\n" - ] - } - ], + "outputs": [], "source": [ "# randomly sample the list of images\n", "for _ in range(3):\n", @@ -223,23 +165,9 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1mINFO:niftynet:\u001b[0m \n", - "\n", - "Number of subjects 2, input section names: ['subject_id', 'image', 'label']\n", - "-- using all subjects (without data partitioning).\n", - "\n", - "\u001b[1mINFO:niftynet:\u001b[0m Image reader: loading 2 subjects from sections ('image',) as input [image]\n", - "\u001b[1mINFO:niftynet:\u001b[0m Image reader: loading 2 subjects from sections ('label',) as input [label]\n" - ] - } - ], + "outputs": [], "source": [ "from niftynet.io.image_reader import ImageReader\n", "\n", @@ -262,20 +190,9 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "((134, 167, 135, 1, 1), (134, 167, 135, 1, 1))" - ] - }, - "execution_count": 34, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "idx, image_data, interp_order = reader(idx=0)\n", "\n", @@ -293,23 +210,9 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1mINFO:niftynet:\u001b[0m \n", - "\n", - "Number of subjects 2, input section names: ['subject_id', 'T1', 'T1c', 'T2', 'Flair', 'label']\n", - "-- using all subjects (without data partitioning).\n", - "\n", - "\u001b[1mINFO:niftynet:\u001b[0m Image reader: loading 2 subjects from sections ('T1', 'T1c', 'T2', 'Flair') as input [image]\n", - "\u001b[1mINFO:niftynet:\u001b[0m Image reader: loading 2 subjects from sections ('label',) as input [label]\n" - ] - } - ], + "outputs": [], "source": [ "from niftynet.io.image_reader import ImageReader\n", "\n", @@ -329,10 +232,8 @@ }, { "cell_type": "code", - "execution_count": 36, - "metadata": { - "collapsed": true - }, + "execution_count": null, + "metadata": {}, "outputs": [], "source": [ "_, image_data, _ = reader(idx=0)" @@ -340,20 +241,9 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "((134, 167, 135, 1, 4), (134, 167, 135, 1, 1))" - ] - }, - "execution_count": 37, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "image_data['image'].shape, image_data['label'].shape" ] @@ -382,32 +272,9 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1mINFO:niftynet:\u001b[0m \n", - "\n", - "Number of subjects 10, input section names: ['subject_id', 'MR']\n", - "-- using all subjects (without data partitioning).\n", - "\n", - "\u001b[1mINFO:niftynet:\u001b[0m Image reader: loading 10 subjects from sections ('MR',) as input [MR]\n" - ] - }, - { - "data": { - "text/plain": [ - "(134, 167, 135, 1, 1)" - ] - }, - "execution_count": 38, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "from niftynet.io.image_reader import ImageReader\n", "from niftynet.layer.rand_rotation import RandomRotationLayer as Rotate\n", @@ -436,29 +303,9 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1mINFO:niftynet:\u001b[0m \n", - "\n", - "Number of subjects 2, input section names: ['subject_id', 'T1', 'T1c', 'T2', 'Flair', 'label']\n", - "-- using all subjects (without data partitioning).\n", - "\n", - "\u001b[1mINFO:niftynet:\u001b[0m Image reader: loading 2 subjects from sections ('T1', 'T1c', 'T2', 'Flair') as input [image]\n", - "\u001b[1mINFO:niftynet:\u001b[0m Image reader: loading 2 subjects from sections ('label',) as input [label]\n", - "dict_keys(['image', 'label'])\n", - "image: (1, 134, 167, 135, 1, 4), label: (1, 134, 167, 135, 1, 1)\n", - "dict_keys(['image', 'label'])\n", - "image: (1, 134, 167, 135, 1, 4), label: (1, 134, 167, 135, 1, 1)\n", - "dict_keys(['image', 'label'])\n", - "image: (1, 152, 169, 130, 1, 4), label: (1, 152, 169, 130, 1, 1)\n" - ] - } - ], + "outputs": [], "source": [ "import tensorflow as tf\n", "from niftynet.io.image_reader import ImageReader\n", @@ -511,7 +358,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python [default]", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -525,7 +372,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.5" + "version": "3.6.4" } }, "nbformat": 4, diff --git a/demos/unet/README.md b/demos/unet/README.md index f77f4a10..a7a45f8c 100644 --- a/demos/unet/README.md +++ b/demos/unet/README.md @@ -1,8 +1,8 @@ This folder presents the IPython notebook document and scripts for tutorial: - > Zach Eaton-Rosen, "[Using NiftyNet to Train U-Net for Cell Segmentation](http://www.miccai.org/edu/finalists/U-Net_Demo.html)", 2018. + > Zach Eaton-Rosen, "[Using NiftyNet to Train U-Net for Cell Segmentation](https://miccai-sb.github.io/materials/U-Net_Demo.html)", 2018. The tutorial is the winner* -of [the MICCAI educational challenge 2018](http://www.miccai.org/edu/mec.html). +of [the MICCAI educational challenge 2018](https://miccai-sb.github.io/challenge). *Decided through expert panel judging followed by a popular vote. diff --git a/doc/requirements.txt b/doc/requirements.txt index adf1bf8e..57c7361f 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -6,10 +6,10 @@ recommonmark==0.4.0 six>=1.10 nibabel>=2.1.0 -numpy>=1.13.3, <= 1.14.5 +numpy>=1.13.3 scipy>=0.18 configparser -tensorflow==1.5 +tensorflow>=1.13.2, <=1.14 pandas pillow blinker diff --git a/doc/source/buffer_queue.md b/doc/source/buffer_queue.md new file mode 100644 index 00000000..b8c06df2 --- /dev/null +++ b/doc/source/buffer_queue.md @@ -0,0 +1,122 @@ + +Buffer queue in NiftyNet +==================== +This example ilustrates +how the `buffer_queue` is created +and modified after each iteration during the network training. + + +```python +from random import shuffle +import random +import string +``` + + +```python +num_volumes = 4 +windows_per_volume = 10 +num_iterations = 10 +batch_size = 5 +``` + +As mentioned in the [config file](.), image window samplers fill the `buffer_queue` +and networks read the `buffer_queue`. In the following two lines, +sampled windows in the `buffer_queue ` are randomly generated and written as +`volume_id-window_id`. + + + +```python +volumes_ids = [random.choice(string.ascii_uppercase) for _ in range(num_volumes)] +windows_ids = list(range(windows_per_volume)) +``` + + +```python +def generate_ids(volumes_ids, windows_ids): + return ''.join(random.choice(volumes_ids))\ + + '-' + ''.join(str(random.choice(windows_ids))) +``` + + +```python +# Defaut configuration +queue_length_default = 10 +queue_length = int(max(queue_length_default, round(batch_size * 2.5))) +buffer_queue = [generate_ids(volumes_ids, windows_ids) for _ in range(queue_length)] +print("[INFO] Initial buffer_queue: {}".format(buffer_queue)) +``` + + [INFO] Initial buffer_queue: ['K-2', 'P-5', 'A-4', 'K-8', 'A-3', 'J-9', 'K-6', 'A-4', 'K-1', 'J-9', 'A-6', 'K-6'] + + +Inside the `for` loop, a snippet code shows the `buffer_queue` being updated during the network training process. At first, the `buffer_queue` is shuffled, secondly `batch_size` image windows are read from the queue for the training computations. Lastly, the `buffer_queue` is updated with new `batch_size` image windows for the next iteration. + + +```python +for iterations in range(num_iterations): + shuffle(buffer_queue) + image_windows = [buffer_queue.pop(0) for ii in range(batch_size)] + print("[INFO] Iteration: {} Image windows: {}".format(iterations + 1, image_windows)) + + ''' + Do something ... + ''' + # Update the buffer queue with new image windows for the next iteration + buffer_queue += [generate_ids(volumes_ids, windows_ids) for _ in range(batch_size)] + print("[INFO] Updated buffer {}".format(buffer_queue, iterations + 1)) + print("\n\n") +``` + + [INFO] Iteration: 1 Image windows: ['J-9', 'K-6', 'A-3', 'A-6', 'A-4'] + [INFO] Updated buffer ['A-4', 'K-1', 'J-9', 'K-6', 'P-5', 'K-8', 'K-2', 'A-6', 'A-0', 'A-3', 'J-7', 'A-2'] + + + + [INFO] Iteration: 2 Image windows: ['K-1', 'A-6', 'A-3', 'J-7', 'K-2'] + [INFO] Updated buffer ['P-5', 'A-4', 'K-6', 'J-9', 'K-8', 'A-2', 'A-0', 'J-7', 'A-6', 'J-7', 'J-7', 'J-0'] + + + + [INFO] Iteration: 3 Image windows: ['A-0', 'K-8', 'J-7', 'A-6', 'A-2'] + [INFO] Updated buffer ['J-7', 'J-7', 'P-5', 'J-9', 'K-6', 'J-0', 'A-4', 'K-4', 'K-3', 'K-1', 'P-5', 'K-2'] + + + + [INFO] Iteration: 4 Image windows: ['J-7', 'K-2', 'K-6', 'K-1', 'P-5'] + [INFO] Updated buffer ['J-0', 'K-4', 'J-9', 'A-4', 'J-7', 'K-3', 'P-5', 'P-8', 'P-5', 'P-5', 'K-2', 'J-7'] + + + + [INFO] Iteration: 5 Image windows: ['P-5', 'P-5', 'J-9', 'J-7', 'J-0'] + [INFO] Updated buffer ['K-3', 'P-8', 'K-4', 'A-4', 'J-7', 'K-2', 'P-5', 'A-2', 'A-1', 'A-6', 'J-3', 'J-1'] + + + + [INFO] Iteration: 6 Image windows: ['P-8', 'J-3', 'K-4', 'J-7', 'A-4'] + [INFO] Updated buffer ['A-6', 'J-1', 'A-2', 'A-1', 'P-5', 'K-2', 'K-3', 'J-8', 'A-4', 'K-1', 'J-7', 'J-0'] + + + + [INFO] Iteration: 7 Image windows: ['K-2', 'A-2', 'J-7', 'P-5', 'J-1'] + [INFO] Updated buffer ['A-4', 'A-6', 'J-0', 'A-1', 'K-3', 'J-8', 'K-1', 'J-7', 'A-6', 'J-5', 'A-7', 'P-0'] + + + + [INFO] Iteration: 8 Image windows: ['J-0', 'J-8', 'J-5', 'P-0', 'K-1'] + [INFO] Updated buffer ['A-6', 'A-4', 'A-1', 'A-7', 'A-6', 'J-7', 'K-3', 'K-8', 'K-5', 'P-0', 'A-7', 'J-3'] + + + + [INFO] Iteration: 9 Image windows: ['K-5', 'A-7', 'J-3', 'A-4', 'A-1'] + [INFO] Updated buffer ['K-8', 'A-6', 'A-7', 'K-3', 'A-6', 'J-7', 'P-0', 'P-9', 'J-2', 'J-4', 'P-1', 'K-6'] + + + + [INFO] Iteration: 10 Image windows: ['P-0', 'A-7', 'P-1', 'K-8', 'A-6'] + [INFO] Updated buffer ['P-9', 'J-2', 'K-6', 'K-3', 'J-4', 'J-7', 'A-6', 'K-1', 'P-2', 'K-0', 'J-0', 'P-9'] + + + + diff --git a/doc/source/config_spec.md b/doc/source/config_spec.md index 730d67ba..366ff87b 100644 --- a/doc/source/config_spec.md +++ b/doc/source/config_spec.md @@ -38,7 +38,7 @@ or `inference`: NiftyNet will try to import the class named `MyApplication` implemented in `user/path/python/module.py`. A few applications are already included in NiftyNet, and can be passed as an -argument of `-a`. Aliases are also created for these application (full +argument of `-a`. Aliases are also created for these application (full specification can be found here: [`SUPPORTED_APP`](./niftynet.engine.application_factory.html)): The commands include: @@ -80,7 +80,7 @@ In the case of quickly adjusting only a few options in the configuration file, creating a separate file is sometimes tedious. To make it more accessible, `net_run` command also accepts parameters -specification in the form of `-- ` or `--=`. When +specification in the form of `-- ` or `--=`. When these are used, `value` will override the corresponding value of `name` defined both by system default and configuration file. @@ -92,23 +92,23 @@ The configuration file currently adopts the INI file format, and is parsed by [`configparser`](https://docs.python.org/3/library/configparser.html). The file consists of multiple sections of `name=value` elements. -All files should have two sections: +All files should have at least these two sections: - [`[SYSTEM]`](#system) - [`[NETWORK]`](#network) -If `train` action is specified, then a [`[TRAINING]`](#training) section is required. +If `train` action is specified, then a [`[TRAINING]`](#training) section is required. -If `inference` action is specified, then an [`[INFERENCE]`](#inference) section is required. +If `inference` action is specified, then an [`[INFERENCE]`](#inference) section is required. Additionally, an application specific section is required for each application -(Please find further comments on [creating customised parser here](https://github.com/NifTK/NiftyNet/blob/dev/niftynet/utilities/user_parameters_custom.py)): +(please find further comments on [creating customised parsers here](https://github.com/NifTK/NiftyNet/blob/dev/niftynet/utilities/user_parameters_custom.py)): - `[GAN]` for generative adversarial networks - `[SEGMENTATION]` for segmentation networks - `[REGRESSION]` for regression networks - `[AUTOENCODER]` for autoencoder networks The [user parameter parser](../niftynet/utilities/user_parameters_parser.py) -tries to match the section names listed above. All other section names will be +tries to match the section names listed above. All other section names will be treated as [`input data source specifications`](#input-data-source-section). The following sections specify parameters (` = ` pairs) available @@ -116,6 +116,42 @@ within each section. ### Input data source section +This section will be used by [ImageReader](./niftynet.io.image_reader.html) +to generate a list of [input images objects](./niftynet.io.image_type.html). +For example, the section + +```ini +[T1Image] +path_to_search = ./example_volumes/image_folder +filename_contains = T1, subject +filename_not_contains = T1c, T2 +spatial_window_size = 128, 128, 1 +pixdim = 1.0, 1.0, 1.0 +axcodes = A, R, S +interp_order = 3 +``` + +specifies a set of images +(currently supports NIfTI format via [NiBabel library](http://nipy.org/nibabel/nifti_images.html)) +from `./example_volumes/image_folder`, with filenames containing both `T1` and +`subject`, but not `T1c` and `T2`. These images will be read into +memory and transformed into "A, R, S" orientation +(using [NiBabel](http://nipy.org/nibabel/reference/nibabel.orientations.html)). +The images will also be transformed to have voxel size `(1.0, 1.0, 1.0)` +with an interpolation order of `3`. + +A CSV file with the matched filenames and extracted subject names will be +generated to `T1Image.csv` in [`model_dir`](#model-dir) (by default; the CSV +file location can be specified by setting [csv_file](#csv-file)). To exclude +particular images, the [csv_file](#csv-file) can be edited manually. + +This input source can be used alone, as a monomodal input to an application. +Additional modalities can be used, as shown in [this example][multimodal]. + +[multimodal]: https://github.com/NifTK/NiftyNet/blob/dev/config/default_multimodal_segmentation.ini + +The [**input filename matching guide**](./filename_matching.html) is useful to +understand how files are matched. Name | Type | Example | Default ---- | ---- | ------- | ------- @@ -140,6 +176,7 @@ be disabled; [path_to_search](#path-to-search), parameter is left blank or the file does not exist, input image search will be enabled, and the matched filenames will be written to this file path. + ###### `path_to_search` Single or multiple folders to search for input images. @@ -170,33 +207,53 @@ matched pattern will be removed from the file names to form the subject id. See also: [input filename matching guide](./filename_matching.html) ###### `interp_order` -Interpolation order of the input data. Note that only the following values -are supported. -- `0`: nearest neighbor with `sitk.sitkNearestNeighbor` +Interpolation order of the input data. Note that only the following values are +supported. +- `0`: nearest neighbour with `sitk.sitkNearestNeighbor` - `1`: linear interpolation with `sitk.sitkLinear` -- `2` and above: b-spline interpolation with `sitk.sitkBSpline` +- `2` and above: B-spline interpolation with `sitk.sitkBSpline` - negative values: returns original image +B-spline interpolation produces the best results, but it's slower than linear +or nearest neighbour. Linear interpolation is usually a good compromise between +speed and quality. + +[This SimpleITK notebook][simpleitk-interpolation] shows some interpolation examples. + +[simpleitk-interpolation]: https://simpleitk-prototype.readthedocs.io/en/latest/user_guide/transforms/plot_interpolation.html + ###### `pixdim` If specified, the input volume will be resampled to the voxel sizes before fed into the network. ###### `axcodes` If specified, the input volume will be reoriented to the axes codes -before fed into the network. +before fed into the network. This is useful if the input images have different +orientations. + +The impact on performance is minimal, so it's a good idea to set this +parameter in order to force the reorientation to, for example, `R, A, S`. +More information about NIfTI orientation can be found on [3D Slicer][slicer-docs] or [NiBabel][nibabel-docs] docs. + +[slicer-docs]: https://www.slicer.org/wiki/Coordinate_systems +[nibabel-docs]: https://nipy.org/nibabel/coordinate_systems.html ###### `spatial_window_size` -Array of three integers specifies the input window size. +Array of three integers specifying the input window size. Setting it to single slice, e.g., `spatial_window_size=64, 64, 1`, yields a 2-D slice window. -See also: [Patch-base analysis guide](./window_sizes.html) +See also: [Patch-based analysis guide](./window_sizes.html) +and [U-Net window shape tutorial][unet] + +[unet]: https://gist.github.com/fepegar/1fb865494cb44ac043c3189ec415d411 ###### `loader` Specify the loader to be used to load the files in the input section. Some loaders require additional Python packages. -Supported loaders: `nibabel`, `opencv`, `skimage`, `pillow`, `simpleitk`, `dummy` in prioriy order. +Supported loaders: `nibabel`, `opencv`, `skimage`, `pillow`, `simpleitk`, `dummy` in priority order. Default value `None` indicates trying all available loaders, in the above priority order. + This section will be used by [ImageReader](./niftynet.io.image_reader.html) to generate a list of [input images objects](./niftynet.io.image_type.html). For example: @@ -230,43 +287,54 @@ can be find [here](https://github.com/NifTK/NiftyNet/blob/dev/config/default_mul The following sections describe system parameters that can be specified in the configuration file. - ### SYSTEM Name | Type | Example | Default ---- | ---- | ------- | ------- -[cuda_devices](#cuda-devices) | `integer array` | `cuda_devices=0,1,2` | `''` -[num_threads](#num-threads) | `positive integer` | `num_threads=1` | `2` -[num_gpus](#num-gpus) | `integer` | `num_gpus=4` | `1` -[model_dir](#model-dir) | `string` | `model_dir=/User/test_dir` | The directory of the config. file -[dataset_split_file](#dataset-split-file) | `string` | `dataset_split_file=/User/my_test` | `./dataset_split_file.csv` -[event_handler](#event-handler) | `string` or a list of `string`s | `event_handler=model_restorer` | `model_saver, model_restorer, sampler_threading, apply_gradients, output_interpreter, console_logger, tensorboard_logger` +[`cuda_devices`](#cuda-devices) | `integer or integer array` | `cuda_devices=0,1,2` | `''` +[`num_threads`](#num-threads) | `positive integer` | `num_threads=1` | `2` +[`num_gpus`](#num-gpus) | `integer` | `num_gpus=4` | `1` +[`model_dir`](#model-dir) | `string` | `model_dir=/User/test_dir` | The directory of the config file +[`dataset_split_file`](#dataset-split-file) | `string` | `dataset_split_file=/User/my_test` | `./dataset_split_file.csv` +[`event_handler`](#event-handler) | `string` or a list of `string`s | `event_handler=model_restorer` | `model_saver, model_restorer, sampler_threading, apply_gradients, output_interpreter, console_logger, tensorboard_logger` ###### `cuda_devices` -Sets the environment variable `CUDA_VISIBLE_DEVICES` variable, -e.g. `0,2,3` uses devices 0, 2, 3 will be visible; device 1 is masked. +Sets the environment variable `CUDA_VISIBLE_DEVICES`, +e.g. `0,2,3` uses devices 0, 2, 3 and device 1 is masked. ###### `num_threads` Sets number of preprocessing threads for training. + -###### `num_gpus` +###### `num_gpus` Sets number of training GPUs. The value should be the number of available GPUs at most. This option is ignored if there's no GPU device. -###### `model_dir` -Directory to save/load intermediate training models and logs. NiftyNet tries +###### `model_dir` +Directory to save/load intermediate training models and logs. NiftyNet tries to interpret this parameter as an absolute system path or a path relative to -the current command. It's defaulting to the directory of the current -configuration file if left blank. +the current command. It defaults to the directory of the current +configuration file if left blank. + +If running inference, it is assumed that `model_dir` contains two folders +named `models` and `logs`. -It is assumed that `model_dir` contains two folders, `models` and `logs`. +###### `dataset_split_file` +Path to a CSV file assigning subjects to training/validation/inference subsets: + +``` +subject_001,training +subject_021,training +subject_027,training +subject_029,validation +subject_429,validation +subject_002,inference +``` -###### `dataset_split_file` -File assigning subjects to training/validation/inference subsets. -If the string is a relative path, NiftyNet interpret this as relative to `model_dir`. +If the string is a relative path, NiftyNet interprets this as relative to `model_dir`. -###### `event_handler` +###### `event_handler` Event handler functions registered to these signals will be called by the engine, along with NiftyNet application properties and iteration messages as function parameters. See [Signals and event handlers](extending_event_handler.html) for more details. @@ -276,102 +344,153 @@ function parameters. See [Signals and event handlers](extending_event_handler.ht Name | Type | Example | Default ---- | ---- | ------- | ------- -[name](#name) | `string` | `name=niftynet.network.toynet.ToyNet` | `''` -[activation_function](#activation-function) | `string` | `activation_function=prelu` | `relu` -[batch_size](#batch-size) | `integer` | `batch_size=10` | `2` -[smaller_final_batch_mode](#smaller-final-batch-mode) | `string` | | `pad` -[decay](#decay) | `non-negative float` | `decay=1e-5` | `0.0` -[reg_type](#reg-type) | `string` | `reg_type=L1` | `L2` -[volume_padding_size](#volume-padding-size) | `integer array` | `volume_padding_size=4, 4, 4` | `0,0,0` -[volume_padding_mode](#volume-padding-mode) | `string` | `volume_padding_mode=symmetric` | `minimum` -[window_sampling](#window-sampling) | `string` | `window_sampling=uniform` | `uniform` -[queue_length](#queue-length) | `integer` | `queue_length=10` | `5` -[keep_prob](#keep-prob) | `non-negative float` | `keep_prob=0.2` | `1.0` - -###### `name` -A network class from [niftynet/network](./niftynet.network.html) or from user -specified module string. NiftyNet tries to import this string as a module -specification. For example, setting it to `niftynet.network.toynet.ToyNet` +[`name`](#name) | `string` | `name=niftynet.network.toynet.ToyNet` | `''` +[`activation_function`](#activation-function) | `string` | `activation_function=prelu` | `relu` +[`batch_size`](#batch-size) | `integer` | `batch_size=10` | `2` +[`smaller_final_batch_mode`](#smaller-final-batch-mode) | `string` | | `pad` +[`decay`](#decay) | `non-negative float` | `decay=1e-5` | `0.0` +[`reg_type`](#reg-type) | `string` | `reg_type=L1` | `L2` +[`volume_padding_size`](#volume-padding-size) | `integer array` | `volume_padding_size=4, 4, 4` | `0,0,0` +[`volume_padding_mode`](#volume-padding-mode) | `string` | `volume_padding_mode=symmetric` | `minimum` +[`window_sampling`](#window-sampling) | `string` | `window_sampling=uniform` | `uniform` +[`force_output_identity_resizing`](#force-output-identity-resizing) | `boolean` | `force_output_identity_resizing=True` | `False` +[`queue_length`](#queue-length) | `integer` | `queue_length=10` | `5` +[`keep_prob`](#keep-prob) | `non-negative float` | `keep_prob=0.2` | `1.0` + +###### `name` +A network class from [niftynet/network](./niftynet.network.html) or from a +user-specified module string. NiftyNet tries to import this string as a module +specification. For example, setting it to `niftynet.network.toynet.ToyNet` will import the `ToyNet` class defined in [`niftynet/network/toynet.py`](./niftynet.network.toynet.html) -(The relevant module path must be a valid Python path). -There are also some shortcuts -([`SUPPORTED_NETWORK`](./niftynet.engine.application_factory.html#niftynet.engine.application_factory.ApplicationNetFactory)) -for NiftyNet's default network modules. - -###### `activation_function` -Sets the type of activation of the network. Available choices are listed in -`SUPPORTED_OP` in [activation layer](https://github.com/NifTK/NiftyNet/blob/dev/niftynet/layer/activation.py). -Depending on its implementation, the network might ignore this option . - -###### `batch_size` -Sets number of image windows to be processed at each iteration. -When `num_gpus` is greater than 1, `batch_size` is used for each computing device. +(the relevant module path must be a valid Python path). +There are also some shortcuts for NiftyNet's default network modules defined in [`SUPPORTED_NETWORK`]. + +[supported-network]: ./niftynet.engine.application_factory.html#niftynet.engine.application_factory.ApplicationNetFactory + +###### `activation_function` +Sets the type of activation function of the network layers. +Available choices are listed in `SUPPORTED_OP` in +[activation layer][activation]. +Depending on the implementation, the network might ignore this option. + +[activation]: https://github.com/NifTK/NiftyNet/blob/dev/niftynet/layer/activation.py#L27-L37 + +###### `batch_size` +Number of image windows to be processed at each iteration. +When `num_gpus` is greater than 1, `batch_size` is used for each GPU. That is, the effective inputs at each iteration become `batch_size` x `num_gpus`. -###### `smaller_final_batch_mode` -When total number of window samples are not divisible by batch_size +See the [interactive buffer animation](#queue-length) to simulate the effect +of modifying this parameter. + +###### `smaller_final_batch_mode` +When the total number of window samples is not divisible by `batch_size` the class supports different modes for the final batch: - `drop`: drop the remainder batch - `pad`: padding the final smaller batch with -1 -- `dynamic`: output the remainder directly ( - in this case the batch_size is undetermined at "compile time") +- `dynamic`: output the remainder directly (in this case the batch_size is undetermined at "compile time") + ###### `reg_type` -Type of trainable parameter regularisation; currently the available choices are "L1" and "L2". -The loss will be added to `tf.GraphKeys.REGULARIZATION_LOSSES` collection. -This option will be ignored if [decay](#decay) is `0.0`. +Type of regularisation for trainable parameters. +Currently the available choices are `L1` (Lasso regression) +and `L2` (ridge regression or weight decay). +The regularisation looks like this: + +``` +J' = J + λ * 1/2 * sum(w ** n) +``` + +where `J` is the loss, `J'` is the regularised loss, λ is the [decay](#decay) +parameter, `w` is an array containing all the trainable parameters and `n` defines the regularisation type (1 for `L1`, 2 for `L2`). + +This option will be ignored if [`decay`](#decay) is `0`. + +The loss will be added to the +[`tf.GraphKeys.REGULARIZATION_LOSSES`][tf-losses] collection. + +[tf-losses]: https://www.tensorflow.org/api_docs/python/tf/GraphKeys#REGULARIZATION_LOSSES ###### `decay` -Strength of regularisation, to help prevent overfitting. +Weight decay factor λ, see [`reg_type`](#reg_type). +A largest value means stronger regularisation, used to prevent overfitting. ###### `volume_padding_size` -Number of values padded at image volume level. -The padding effect is equivalent to `numpy.pad` with: +Number of voxels padded at image volume level (before window sampling). +The padding effect is equivalent to [`numpy.pad`][numpy-pad] with: + ```python -numpy.pad(input_volume, - (volume_padding_size[0], - volume_padding_size[1], - volume_padding_size[2], 0, 0), - mode='minimum') +i, j, k = volume_padding_size +numpy.pad( + input_volume, + (i, j, k, 0, 0), + mode='minimum', +) ``` -For 2-D inputs, the third dimension of `volume_padding_size` should be set to `0`, -e.g. `volume_padding_size=M,N,0`. -`volume_padding_size=M` is a shortcut for 3-D inputs, equivalent to `volume_padding_size=M,M,M`. -The same amount of padding will be removed when before writing the output volume. -See also: [Patch-base analysis guide](./window_sizes.html) +For 2D inputs, the third dimension of `volume_padding_size` should be set to `0`, e.g. `volume_padding_size=M,N,0`. + +For 3D inputs, setting `volume_padding_size=M` +is equivalent to `volume_padding_size=M,M,M`. +The same amount of padding will be removed before writing the output volume. + +See also: [Patch-based analysis guide](./window_sizes.html) ###### `volume_padding_mode` -Set which type of numpy padding to do, see -[https://docs.scipy.org/doc/numpy-1.14.0/reference/generated/numpy.pad.html](https://docs.scipy.org/doc/numpy-1.14.0/reference/generated/numpy.pad.html) for details. +Set which type of numpy padding to do, +see [`numpy.pad`][numpy-pad] for details. + +[numpy-pad]: https://docs.scipy.org/doc/numpy/reference/generated/numpy.pad.html ###### `window_sampling` Type of sampler used to generate image windows from each image volume: -- uniform: fixed size uniformly distributed, -- weighted: fixed size where the likelihood of sampling a voxel is proportional to the cumulative intensity histogram, -- balanced: fixed size where each label has the same probability of being sampled, -- resize: resize image to the window size. +- `uniform`: fixed size uniformly distributed, +- `weighted`: fixed size where the likelihood of sampling a voxel is proportional to the cumulative intensity histogram on the sampling prior, +- `balanced`: fixed size where each label in the sampling prior has the same probability of being sampled, +- `resize`: resize image to the window size. -For `weighted` and `balanced`, an input section is required to load sampling priors. -As an [example in the demo folder](https://github.com/NifTK/NiftyNet/blob/v0.3.0/demos/PROMISE12/promise12_balanced_train_config.ini#L61), -`sampler` parameter is set to `label`, indicating that the sampler uses `label` -section as the sampling prior. +For `weighted` and `balanced`, +an input section is required to load sampling priors. +In the [sampling demo][sampling-demo] +the `sampler` parameter is set to `label`, +indicating that the sampler uses the `label` section as the sampling prior. + +See also: [Patch-based analysis guide](./window_sizes.html) + +[sampling-demo]: https://github.com/NifTK/NiftyNet/blob/v0.3.0/demos/PROMISE12/promise12_balanced_train_config.ini#L61 + +###### `force_output_identity_resizing` +Boolean to prevent the inferred output from being resized up to input image shape during regression tasks when the resize sampler is used. +An example use case is regression of a single value from an input image, where the inferred output should not be resized to image shape. ###### `queue_length` -Integer specifies window buffer size used when sampling image windows from image volumes. +Size of the buffer used when sampling image windows from image volumes. Image window samplers fill the buffer and networks read the buffer. -Because the network reads [batch_size](#batch-size) windows at each iteration, -this value is set to at least `batch_size * 2.5` to allow for a possible randomised buffer, -i.e. `max(queue_length, round(batch_size * 2.5))`. +Because the network reads [`batch_size`](#batch-size) windows at each iteration, this value is set to at least `5 * batch_size` +to allow for a possible randomised buffer, i.e. + +```python +queue_length = max(queue_length, batch_size * 5) +``` + +A longer queue increases the probability of windows in a batch coming from +different input volumes, but it will take longer to fill and consume more +memory. + +You can use this interactive animation to simulate the effect +of modifying the parameters related to the buffer: + + ###### `keep_prob` -The probability that each element is kept if dropout is supported by the network. +The probability that each unit is kept if dropout is supported by the network. The default value is `0.5`, meaning randomly dropout at the ratio of 0.5. This is also used as a default value at inference stage. To achieve a deterministic inference, set `keep_prob=1`; -to draw stochastic samples at inferece, set `keep_prob` to a value in between 0 and 1. +to [draw stochastic samples at inferece][highres3dnet], +set `keep_prob` to a value between 0 and 1. In the case of drawing multiple Monte Carlo samples, the user can run the inference command mutiple times, with each time a different `save_seg_dir`, for @@ -379,61 +498,71 @@ example: `python net_segment.py inference ... --save_seg_dir run_2 --keep_prob 0.5`. - +[highres3dnet]: https://arxiv.org/pdf/1707.01992.pdf ##### Volume-normalisation Intensity based volume normalisation can be configured using a combination of parameters described below: (1) Setting `normalisation=True` enables the [histogram-based -normalisation](./niftynet.utilities.histogram_standardisation.html). The -relevant configuration parameters are: +standardisation](./niftynet.utilities.histogram_standardisation.html) as described by [Nyúl et al., 2000][nyul]. +The relevant configuration parameters are: > `histogram_ref_file`, `norm_type`, `cutoff`, `normalise_foreground_only`, `foreground_type`, `multimod_foreground_type`. -These parameters are ignored and histogram-based normalisation is disabled if `normalisation=False`. +If `normalisation=False`, these parameters are ignored and histogram-based normalisation is disabled. -(2) Setting `whitening=True` enables the volume level normalisation computed by `(I - mean(I))/std(I)`. +(2) Setting `whitening=True` enables the volume level normalisation computed by `(I - mean(I))/std(I)`, i.e. the volume is set to have zero-mean and unit variance. The relevant configuration parameters are: > `normalise_foreground_only`, `foreground_type`, `multimod_foreground_type`. -These parameters are ignored and whitening is disabled if `whitening=False`. +If `whitening=False`, these parameters are ignored and whitening is disabled. -More specifically: +(3) Setting `rgb_normalisation=True` enables RGB histogram equalisation. It requires OpenCV (opencv-python) and only supports 2D images. +Unlike `normalisation`, it does not use histogram landmarks or files. + +[nyul]: https://ieeexplore.ieee.org/document/836373 +More specifically: Name | Type | Example | Default ---- | ---- | ------- | ------- -[normalisation](#normalisation) | `boolean` | `normalisation=True` | `False` -[whitening](#whitening) | `boolean` | `whitening=True` | `False` -[histogram_ref_file](#histogram-ref-file) | `string` | `histogram_ref_file=./hist_ref.txt` | `''` -[norm_type](#norm-type) | `string` | `norm_type=percentile` | `percentile` -[cutoff](#cutoff) | `float array (two elements)` | `cutoff=0.1, 0.9` | `0.01, 0.99` -[normalise_foreground_only](#normalise-foreground-only) | `boolean` | `normalise_foreground_only=True` | `False` -[foreground_type](#foreground-type) | `string` | `foreground_type=otsu_plus` | `otsu_plus` -[multimod_foreground_type](#multimod-foreground-type) | `string` | `multimod_foreground_type=and` | `and` +[`normalisation`](#normalisation) | `boolean` | `normalisation=True` | `False` +[`whitening`](#whitening) | `boolean` | `whitening=True` | `False` +[`rgb_normalisation`](#rgb-normalisation) | `boolean` | `rgb_normalisation=True` | `False` +[`histogram_ref_file`](#histogram-ref-file) | `string` | `histogram_ref_file=./hist_ref.txt` | `''` +[`norm_type`](#norm-type) | `string` | `norm_type=percentile` | `percentile` +[`cutoff`](#cutoff) | `float array (two elements)` | `cutoff=0.1, 0.9` | `0.01, 0.99` +[`normalise_foreground_only`](#normalise-foreground-only) | `boolean` | `normalise_foreground_only=True` | `False` +[`foreground_type`](#foreground-type) | `string` | `foreground_type=otsu_plus` | `otsu_plus` +[`multimod_foreground_type`](#multimod-foreground-type) | `string` | `multimod_foreground_type=and` | `and` ###### `normalisation` -Boolean indicates if an histogram standardisation should be applied to the data. +Boolean indicates if histogram standardisation +(as described in [Nyúl et al., 2000][nyul]) should be applied to the data. ###### `whitening` -Boolean indicates if the loaded image should be whitened, -that is, given input image `I`, returns `(I - mean(I))/std(I)`. +Boolean to indicate if the loaded image should be whitened, +that is, given input image `I`, returns `(I - mean(I))/std(I)`. + +###### `rgb_normalisation` +Boolean to indicate if an RGB histogram equalisation should be applied to the data. ###### `histogram_ref_file` -Name of the file that contains the normalisation parameter if it has been trained before or where to save it. +Name of the file that contains the standardisation parameters if it has been trained before or where to save it. ###### `norm_type` -Type of histogram landmarks used in histogram-based normalisation (percentile or quartile). +Type of histogram landmarks used in histogram-based standardisation (percentile or quartile). ###### `cutoff` -Inferior and superior cutoff in histogram-based normalisation. +Inferior and superior cutoff in histogram-based standardisation. ###### `normalise_foreground_only` -Boolean indicates if a mask should be computed based on `foreground_type` and `multimod_foreground_type`. -If this parameter is set to `True`, all normalisation steps will be applied to the generated foreground -regions only. +Boolean to indicate if a mask should be computed based on `foreground_type` and `multimod_foreground_type`. +If this parameter is set to `True`, all normalisation steps will be applied to +the generated foreground regions only. ###### `foreground_type` -To generate a foreground mask and the normalisation will be applied to foreground only. +To generate a foreground mask and the normalisation will be applied to +foreground only. Available choices: > `otsu_plus`, `otsu_minus`, `thresh_plus`, `thresh_minus`, `mean_plus`. @@ -448,100 +577,109 @@ Strategies applied to combine foreground masks of multiple modalities, can take Name | Type | Example | Default ---- | ---- | ------- | ------- -[optimiser](#optimiser) | `string` | `optimiser=momentum` | `adam` -[sample_per_volume](#sample-per-volume) | `positive integer` | `sample_per_volume=5` | `1` -[lr](#lr) | `float` | `lr=0.001` | `0.1` -[loss_type](#loss-type) | `string` | `loss_type=CrossEntropy` | `Dice` -[starting_iter](#starting-iter) | `integer` | `starting_iter=0` | `0` -[save_every_n](#save-every-n) | `integer` | `save_every_n=5` | `500` -[tensorboard_every_n](#tensorboard-every-n) | `integer` | `tensorboard_every_n=5` | `20` -[max_iter](#max-iter) | `integer` | `max_iter=1000` | `10000` -[max_checkpoints](#max-checkpoints) | `integer` | `max_checkpoints=5` | `100` -[vars_to_restore](#vars-to-restore) | `string` | `vars_to_restore=^.*(conv_1|conv_2).*$` | `''` -[vars_to_freeze](#vars-to-freeze) | `string` | `vars_to_freeze=^.*(conv_3|conv_4).*$` | value of `vars_to_restore` +[`optimiser`](#optimiser) | `string` | `optimiser=momentum` | `adam` +[`sample_per_volume`](#sample-per-volume) | `positive integer` | `sample_per_volume=5` | `1` +[`lr`](#lr) | `float` | `lr=0.001` | `0.1` +[`loss_type`](#loss-type) | `string` | `loss_type=CrossEntropy` | `Dice` +[`starting_iter`](#starting-iter) | `integer` | `starting_iter=0` | `0` +[`save_every_n`](#save-every-n) | `integer` | `save_every_n=5` | `500` +[`tensorboard_every_n`](#tensorboard-every-n) | `integer` | `tensorboard_every_n=5` | `20` +[`max_iter`](#max-iter) | `integer` | `max_iter=1000` | `10000` +[`max_checkpoints`](#max-checkpoints) | `integer` | `max_checkpoints=5` | `100` +[`vars_to_restore`](#vars-to-restore) | `string` | `vars_to_restore=^.*(conv_1|conv_2).*$` | `''` +[`vars_to_freeze`](#vars-to-freeze) | `string` | `vars_to_freeze=^.*(conv_3|conv_4).*$` | value of `vars_to_restore` ###### `optimiser` -Type of optimiser for computing graph gradients. Current available options are -defined here: -[`SUPPORTED_OPTIMIZERS`](./niftynet.engine.application_factory.html#niftynet.engine.application_factory.OptimiserFactory). +Type of optimiser for computing graph gradients. Current available options are +defined here in [`SUPPORTED_OPTIMIZERS`][optimizers]. + +[optimizers]: ./niftynet.engine.application_factory.html#niftynet.engine.application_factory.OptimiserFactory ###### `sample_per_volume` -Set number of samples to take from each image volume. +Number of samples to take from each image volume when filling the queue. + +See the [interactive buffer animation](#queue-length) to simulate the effect +of modifying this parameter. ###### `lr` The learning rate for the optimiser. ###### `loss_type` Type of loss function. -Please see the relevant loss function layer for choices available: +Please see the relevant loss function layer for available choices: - [Segmentation](./niftynet.layer.loss_segmentation.html), - [Regression](./niftynet.layer.loss_regression.html), - [Autoencoder](./niftynet.layer.loss_autoencoder.html), - [GAN](./niftynet.layer.loss_gan.html). The corresponding loss function type names are defined in the -[`ApplicationFactory`](https://github.com/NifTK/NiftyNet/blob/dev/niftynet/engine/application_factory.py) +[`ApplicationFactory`][app-factory]. + +[app-factory]: https://github.com/NifTK/NiftyNet/blob/dev/niftynet/engine/application_factory.py ###### `starting_iter` -The iteration to resume training model. +The iteration from which to resume training the model. Setting `starting_iter=0` starts the network from random initialisations. Setting `starting_iter=-1` starts the network from the latest checkpoint if it exists. ###### `save_every_n` Frequency of saving the current training model saving. -Setting to a `0` to disable the saving schedule. -(A final model will always be saved when quitting the training loop.) +Setting it to `0` disables the saving schedule (the last model will always be saved when quitting the training loop). ###### `tensorboard_every_n` -Frequency of evaluating graph elements and write to tensorboard. -Setting to `0` to disable the tensorboard writing schedule. +Frequency of evaluating graph elements and writing to tensorboard. +Setting it to `0` disables the tensorboard writing schedule. ###### `max_iter` Maximum number of training iterations. -The value is total number of iterations. -Setting both `starting_iter` and `max_iter` to `0` to -save the random model initialisation. +Setting both `starting_iter` and `max_iter` to `0` can be used to save the +random model initialisation. ###### `max_checkpoints` -Maximum number of recent checkpoints to keep. +Maximum number of checkpoints to save. ###### `vars_to_restore` -Regular expression string to match variable names, -values of the matched variables will be initialised for a checkpoint file. +Regular expression string to match variable names that will be initialised from a checkpoint file. See also: [guide for finetuning pre-trained networks](./transfer_learning.html) ###### `vars_to_freeze` -Regular expression string to match variable names, -values of the matched variables will be updated during training. -Defaulting to the value of `vars_to_restore`. +Regular expression string to match variable names that will be updated during +training. +Defaults to the value of `vars_to_restore`. See also: [guide for finetuning pre-trained networks](./transfer_learning.html) ##### Validation during training Setting [`validation_every_n`](#validation-every-n) to a positive integer enables validation loops during training. -When validation is enabled, images list (defined by [input specifications](#input-data-source-section)) -will be treated as the whole dataset, and partitioned into subsets of training, validation, and inference -according to [`exclude_fraction_for_validation`](#exclude-fraction-for-validation) and +When validation is enabled, images list +(defined by [input specifications](#input-data-source-section)) +will be treated as the whole dataset, +and partitioned into subsets of training, validation, and inference according +to [`exclude_fraction_for_validation`](#exclude-fraction-for-validation) and [`exclude_fraction_for_inference`](#exclude-fraction-for-inference). A CSV table randomly mapping each file name to one of the stages `{'Training', 'Validation', 'Inference'}` will be generated and written to [dataset_split_file](#dataset-split-file). This file will be created at the -beginning of training (`starting_iter=0`) and only if the file does not exist. +beginning of training (`starting_iter=0`) only if the file does not exist. - If a new random partition is required, please remove the existing [dataset_split_file](#dataset-split-file). -- If no partition is required, please remove any existing [dataset_split_file](#dataset-split-file), -and make sure both [`exclude_fraction_for_validation`](#exclude-fraction-for-validation) -and [`exclude_fraction_for_inference`](#exclude-fraction-for-inference) are `0`. +- If no partition is required, +please remove any existing [dataset_split_file](#dataset-split-file), +and make sure +both [`exclude_fraction_for_validation`](#exclude-fraction-for-validation) +and [`exclude_fraction_for_inference`](#exclude-fraction-for-inference) +are `0`. -To exclude particular subjects or adjust the randomly generated partition, the -[`dataset_split_file`](#dataset-split-file) can be edited manually. Please note -duplicated rows are not removed. For example, if the content of +To exclude specific subjects or adjust the randomly generated partition, the +[`dataset_split_file`](#dataset-split-file) can be edited manually. +Please note duplicated rows are not removed. For example, if the content of [`dataset_split_file`](#dataset-split-file) is as follows: + ```text 1040,Training 1071,Inference @@ -550,30 +688,37 @@ duplicated rows are not removed. For example, if the content of 1065,Training 1065,Validation ``` -Each row will be treated as an independent subject. This means: ->subject `1065` will be used in both `Training` and `Validation` stages, and it'll be sampled more frequently than subject `1040` during training; ->subject `1071` will be used in `Inference` twice, the output of the second inference will overwrite the first. -Note that at each validation iteration, input will be sampled from the set of validation data, -and the network parameters will remain unchanged. The `is_training` parameter of the network -is set to `True` during validation, as a result layers with different behaviours in training and inference -(such as dropout and batch normalisation) uses the training behaviour. +Each row will be treated as an independent subject. This means that: + +> Subject `1065` will be used in both `Training` and `Validation` stages, and it will be sampled more frequently than subject `1040` during training. + +> Subject `1071` will be used twice in `Inference`, and the output of the second inference will overwrite the first. + +Note that at each validation iteration, +input will be sampled from the set of validation data, +and the network parameters will remain unchanged. + +The `is_training` parameter of the network is set to `True` during validation. +As a result, layers with different behaviours in training and inference +(such as dropout and batch normalisation) use the training behaviour. During inference, if a [dataset_split_file](#dataset-split-file) is available, -only image files in the `Inference` phase will be used, otherwise inference -will process all image files defined by [input specifications](#input-data-source-section). +only image files in the `Inference` phase will be used, +otherwise inference will process all image files +defined by [input specifications](#input-data-source-section). Name | Type | Example | Default ---- | ---- | ------- | ------- -[validation_every_n](#validation-every-n) | `integer` | `validation_every_n=10` | `-1` -[validation_max_iter](#validation-max-iter) | `integer` | `validation_max_iter=5` | `1` -[exclude_fraction_for_validation](#exclude-fraction-for-validation) | `float` | `exclude_fraction_for_validation=0.2` | `0.0` -[exclude_fraction_for_inference](#exclude-fraction-for-inference) | `float` | `exclude_fraction_for_inference=0.1` | `0.0` +[`validation_every_n`](#validation-every-n) | `integer` | `validation_every_n=10` | `-1` +[`validation_max_iter`](#validation-max-iter) | `integer` | `validation_max_iter=5` | `1` +[`exclude_fraction_for_validation`](#exclude-fraction-for-validation) | `float` | `exclude_fraction_for_validation=0.2` | `0.0` +[`exclude_fraction_for_inference`](#exclude-fraction-for-inference) | `float` | `exclude_fraction_for_inference=0.1` | `0.0` ###### `validation_every_n` Run validation iterations after every N training iterations. -Setting to `0` disables the validation. +Setting it to `0` disables the validation. ###### `validation_max_iter` Number of validation iterations to run. @@ -587,91 +732,113 @@ Value should be in `[0, 1]`. Fraction of dataset to use for inference. Value should be in `[0, 1]`. -##### Data augmentation during training +##### Data augmentation during training Name | Type | Example | Default ---- | ---- | ------- | ------- -[rotation_angle](#rotation-angle) | `float array` | `rotation_angle=-10.0,10.0` | `''` -[scaling_percentage](#scaling-percentage) | `float array` | `scaling_percentage=-20.0,20.0` | `''` -[antialiasing](#scaling-percentage) | `boolean` | `antialiasing=True` | `True` -[random_flipping_axes](#random-flipping-axes) | `integer array` | `random_flipping_axes=1,2` | `-1` -[do_elastic_deformation](#do-elastic-deformation) | `boolean` | `do_elastic_deformation=True` | `False` -[num_ctrl_points](#do-elastic-deformation) | `integer` | `num_ctrl_points=1` | `4` -[deformation_sigma](#do-elastic-deformation) | `float` | `deformation_sigma=1` | `15` -[proportion_to_deform](#do-elastic-deformation) | `float` | `proportion_to_deform=0.7` | `0.5` -[bias_field_range](#bias-field-range) | `float array` | `bias_field_range=-10.0,10.0` | `''` -[bf_order](#bias-field-range) | `integer` | `bf_order=1` | `3` +[`rotation_angle`](#rotation-angle) | `float array` | `rotation_angle=-10.0,10.0` | `''` +[`scaling_percentage`](#scaling-percentage) | `float array` | `scaling_percentage=-20.0,20.0` | `''` +[`antialiasing`](#scaling-percentage) | `boolean` | `antialiasing=True` | `True` +[`isotropic_scaling`](#scaling-percentage) | `boolean` | `isotropic_scaling=True` | `False` +[`random_flipping_axes`](#random-flipping-axes) | `integer array` | `random_flipping_axes=1,2` | `-1` +[`do_elastic_deformation`](#do-elastic-deformation) | `boolean` | `do_elastic_deformation=True` | `False` +[`num_ctrl_points`](#do-elastic-deformation) | `integer` | `num_ctrl_points=1` | `4` +[`deformation_sigma`](#do-elastic-deformation) | `float` | `deformation_sigma=1` | `15` +[`proportion_to_deform`](#do-elastic-deformation) | `float` | `proportion_to_deform=0.7` | `0.5` +[`bias_field_range`](#bias-field-range) | `float array` | `bias_field_range=-10.0,10.0` | `''` +[`bf_order`](#bias-field-range) | `integer` | `bf_order=1` | `3` ###### `rotation_angle` -Float array, indicates a random rotation operation should be applied to the -volumes (This can be slow depending on the input volume dimensionality). +Interval of rotation degrees to apply a random rotation to the volumes. +A different random value is compueted for each rotation axis. + +This processing can be slow +depending on the input volume size and dimensionality. ###### `scaling_percentage` -Float array indicates a random spatial scaling should be applied -(This can be slow depending on the input volume dimensionality). -The option accepts percentages relative to 100 (the original input size). -E.g, `(-50, 50)` indicates transforming -image (size `d`) to image with its size in between `0.5*d` and `1.5d`. +Interval of percentages relative to 100 to apply a random spatial scaling +to the volumes. +For example, setting this parameter to `(-50, 50)` might transform a volume +with size `100, 100, 100` to `140, 88, 109`. When random scaling is enabled, it is possible to further specify: -- `antialiasing` indicating if antialiasing should be performed + +- `antialiasing`: indicating if Gaussian filtering should be performed when randomly downsampling the input images. +- `isotropic_scaling`: indicating if the same amount of scaling +should be applied in each dimension. If this option is set to `False`, +a different random value will be computed for each volume axis. + +This processing can be slow +depending on the input volume size and dimensionality. ###### `random_flipping_axes` -The axes which can be flipped to augment the data. -Supply as comma-separated values within single quotes, e.g. '0,1'. -Note that these are 0-indexed, so choose some combination of 0, 1. +Axes which can be flipped to augment the data. + +For example, to randomly flip the first and third axes, use +`random_flipping_axes = 0, 2` ###### `do_elastic_deformation` -Boolean value indicates data augmentation using elastic deformations +Boolean value to indicate if data augmentation +using elastic deformations should be performed. When `do_elastic_deformation=True`, it is possible to further specify: -- `num_ctrl_points` -- number of control points for the elastic deformation, -- `deformation_sigma` -- the standard deviation for the elastic deformation, -- `proportion_to_deform` -- what fraction of samples to deform elastically. +- `num_ctrl_points`: number of control points for the elastic deformation, +- `deformation_sigma`: the standard deviation for the elastic deformation, +- `proportion_to_deform`: what fraction of samples to deform elastically. + +See an example of elastic deformations for data augmentation +on the [U-Net demo][unet-demo]. + +[unet-demo]: https://github.com/NifTK/NiftyNet/blob/dev/demos/unet/U-Net_Demo.ipynb ###### `bias_field_range` -Float array, indicates data augmentation with randomised bias field +Float array indicating whether to perform +data augmentation with randomised bias field. -When `bias_field_range` is not None, it is possible to further specify: -- `bf_order` -- maximal polynomial order to use for the bias field augmentation. +When `bias_field_range` is not `None`, it is possible to further specify: +- `bf_order`: maximal polynomial order to use for the bias field augmentation. ### INFERENCE Name | Type | Example | Default ---- | ---- | ------- | ------- -[spatial_window_size](#spatial-window-size) | `integer array` | `spatial_window_size=64,64,64` | `''` -[border](#border) | `integer array` | `border=5,5,5` | `0, 0, 0` -[inference_iter](#inference-iter) | `integer` | `inference_iter=1000` | `-1` -[save_seg_dir](#save-seg-dir) | `string` | `save_seg_dir=output/test` | `output` -[output_postfix](#output-postfix) | `string` | `output_postfix=_output` | `_niftynet_out` -[output_interp_order](#output-interp-order) | `non-negative integer` | `output_interp_order=0` | `0` -[dataset_to_infer](#dataset-to-infer) | `string` | `dataset_to_infer=training` | `''` +[`spatial_window_size`](#spatial-window-size) | `integer array` | `spatial_window_size=64,64,64` | `''` +[`border`](#border) | `integer array` | `border=5,5,5` | `0, 0, 0` +[`inference_iter`](#inference-iter) | `integer` | `inference_iter=1000` | `-1` +[`save_seg_dir`](#save-seg-dir) | `string` | `save_seg_dir=output/test` | `output` +[`output_postfix`](#output-postfix) | `string` | `output_postfix=_output` | `_niftynet_out` +[`output_interp_order`](#output-interp-order) | `non-negative integer` | `output_interp_order=0` | `0` +[`dataset_to_infer`](#dataset-to-infer) | `string` | `dataset_to_infer=training` | `''` +[`fill_constant`](#fill-constant) | `float` | `fill_constant=1.0` | `0.0` + ###### `spatial_window_size` -Array of integers indicating the size of input window. By default, the window -size at inference time is the same as the [input source specification](#input-data-source-section). -If this parameter is specified, it -overrides the `spatial_window_size` parameter in input source sections. +Array of integers indicating the size of input window. +By default, the window size at inference time is the same as +the [input source specification](#input-data-source-section). +If this parameter is specified, it overrides the `spatial_window_size` parameter in input source sections. -See also: [Patch-base analysis guide](./window_sizes.html) +See also: [Patch-based analysis guide](./window_sizes.html) ###### `border` -Tuple of integers specifying a border size used to crop (along both sides of each -dimension) the network output image window. E.g., `3, 3, 3` will crop a -`64x64x64` window to size `58x58x58`. +Tuple of integers specifying a border size used to crop +(along both sides of each dimension) the network output image window. +E.g., `3, 3, 3` will crop a `64x64x64` window to size `58x58x58`. -See also: [Patch-base analysis guide](./window_sizes.html) +See also: [Patch-based analysis guide](./window_sizes.html) ###### `inference_iter` Integer specifies the trained model to be used for inference. -`-1` or unspecified indicating to use the latest available trained model in `model_dir`. +If set to `-1` or unspecified, +the latest available trained model in `model_dir` will be used. ###### `save_seg_dir` -Prediction directory name. If it's a relative path, it is set to be relative to [`model_dir`](#model-dir). +Prediction directory name. +If it's a relative path, it is set to be relative to [`model_dir`](#model-dir). ###### `output_postfix` Postfix appended to every inference output filenames. @@ -680,9 +847,14 @@ Postfix appended to every inference output filenames. Interpolation order of the network outputs. ###### `dataset_to_infer` -String specifies which dataset ('all', 'training', 'validation', 'inference') to compute inference for. -By default 'inference' dataset is used. If no `dataset_split_file` is specified, then all data specified -in the csv or search path are used for inference. +String to specify which dataset +(`all`, `training`, `validation` or `inference`) to compute inference for. +By default `inference` dataset is used. +If no `dataset_split_file` is specified, then all data specified +in the CSV or search path are used for inference. + +##### `fill_constant` +Value used to fill borders of output images. ### EVALUATION @@ -725,6 +897,6 @@ in the application configuration section (such as `[SEGMENTATION]`). - `evaluation_units` -- `foreground`, `label` or `cc`. Describe how the evaluation should be performed in the case of segmentation mostly (`foreground` means only one label, `label` means metrics per label, `cc` - means metrics per connected component). More on this topic can be found at + means metrics per connected component). More on this topic can be found at [segmentation evaluations](./niftynet.evaluation.segmentation_evaluations.html). --> diff --git a/doc/source/filename_matching.md b/doc/source/filename_matching.md index 57e31bb0..df4f38b1 100644 --- a/doc/source/filename_matching.md +++ b/doc/source/filename_matching.md @@ -8,7 +8,7 @@ manner. To facilitate the cross-subject analysis, the user should specify lists of files to be used. For example, the relevant configurations could be: -``` +```ini [SYSTEM] dataset_split_file = '/mnt/data/cross_validation_fold_01.csv' @@ -26,19 +26,19 @@ where `[MRI_T1]` and `[segmentation_target]` are input source sections, with The csv files should be created beforehand by the user and share the same set of unique subject identifier (“subject ID”) among them, for example: -Content of t1_list.csv: +Contents of `t1_list.csv`: ``` subject_001,/mnt/data/t1/T1_001_img.nii.gz subject_002,/mnt/data/t1/T1_002_img.nii.gz ``` -Content of ground_truth.csv: +Contents of `ground_truth.csv`: ``` subject_001,/mnt/data/ground_truth/001_img_seg.nii.gz subject_002,/mnt/data/ground_truth/002_img_seg.nii.gz ``` -Content of cross_validation_fold_01.txt: +Contents of `cross_validation_fold_01.csv`: ``` subject_001,training subject_002,inference diff --git a/doc/source/installation.rst b/doc/source/installation.rst index 6a64f66a..4180077a 100644 --- a/doc/source/installation.rst +++ b/doc/source/installation.rst @@ -3,8 +3,8 @@ Installation 1. Installing the appropriate `TensorFlow`_ package: - - ``pip install tensorflow-gpu==1.12`` for TensorFlow with GPU support - - ``pip install tensorflow==1.12`` for CPU-only TensorFlow + - ``pip install "tensorflow-gpu>=1.13.2, <=1.14"`` for TensorFlow with GPU support + - ``pip install "tensorflow>=1.13.2, <=1.14"`` for CPU-only TensorFlow 2. Installing NiftyNet package diff --git a/doc/source/window_sizes.rst b/doc/source/window_sizes.rst index 4cf7aad1..2ee1c8a8 100644 --- a/doc/source/window_sizes.rst +++ b/doc/source/window_sizes.rst @@ -1,4 +1,4 @@ -Patch-base analysis +Patch-based analysis =================== NiftyNet is designed to facilitate patch-based medical image analysis. @@ -117,7 +117,7 @@ locations get evaluated by the network. Therefore, -1. for the grid window sampler, we would like to have ``100 x 100``-pixel +1. for the grid window sampler, we would like to have ``100 x 100``-pixel window generated with a step size of ``76`` in both directions; 2. for the window aggregation, the spatial coordinates of the ``76 x 76``-pixel @@ -218,5 +218,3 @@ accept array values up to three elements. For multi-channel input images with sizes such as ``height x width x depth x num_modalities``, the shape of the windows will be ``window height x window width x window depth x num_modalities``. - - diff --git a/niftynet/__init__.py b/niftynet/__init__.py index da6151ff..c7e489d9 100755 --- a/niftynet/__init__.py +++ b/niftynet/__init__.py @@ -22,6 +22,7 @@ try: from distutils.version import LooseVersion + minimal_required_version = LooseVersion("1.5") tf_version = LooseVersion(tf.__version__) if tf_version < minimal_required_version: @@ -51,6 +52,10 @@ require_module('blinker', descriptor='New dependency', mandatory=True) +from tensorflow.python.util import deprecation + +deprecation._PRINT_DEPRECATION_WARNINGS = False + from niftynet.engine.signal import TRAIN, INFER, EVAL import niftynet.utilities.util_common as util import niftynet.utilities.user_parameters_parser as user_parameters_parser diff --git a/niftynet/application/autoencoder_application.py b/niftynet/application/autoencoder_application.py index 2afaf30a..575490b1 100755 --- a/niftynet/application/autoencoder_application.py +++ b/niftynet/application/autoencoder_application.py @@ -130,6 +130,8 @@ def switch_sampler(for_training): return sampler.pop_batch_op() if self.is_training: + self.patience = self.action_param.patience + self.mode = self.action_param.early_stopping_mode if self.action_param.validation_every_n > 0: data_dict = tf.cond(tf.logical_not(self.is_validation), lambda: switch_sampler(True), @@ -156,11 +158,21 @@ def switch_sampler(for_training): reg_loss = tf.reduce_mean( [tf.reduce_mean(reg_loss) for reg_loss in reg_losses]) loss = loss + reg_loss + + self.total_loss = loss grads = self.optimiser.compute_gradients( loss, colocate_gradients_with_ops=True) # collecting gradients variables gradients_collector.add_to_collection([grads]) + outputs_collector.add_to_collection( + var=self.total_loss, name='total_loss', + average_over_devices=True, collection=CONSOLE) + outputs_collector.add_to_collection( + var=self.total_loss, name='total_loss', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + outputs_collector.add_to_collection( var=data_loss, name='variational_lower_bound', average_over_devices=True, collection=CONSOLE) @@ -262,17 +274,17 @@ def interpret_output(self, batch_output): SUPPORTED_INFERENCE) if infer_type == 'encode': return self.output_decoder.decode_batch( - batch_output['embedded'], + {'window_embedded':batch_output['embedded']}, batch_output['location'][:, 0:1]) if infer_type == 'encode-decode': return self.output_decoder.decode_batch( - batch_output['generated_image'], + {'window_generated_image':batch_output['generated_image']}, batch_output['location'][:, 0:1]) if infer_type == 'sample': return self.output_decoder.decode_batch( - batch_output['generated_image'], + {'window_generated_image':batch_output['generated_image']}, None) if infer_type == 'linear_interpolation': return self.output_decoder.decode_batch( - batch_output['generated_image'], + {'window_generated_image':batch_output['generated_image']}, batch_output['location'][:, :2]) diff --git a/niftynet/application/base_application.py b/niftynet/application/base_application.py index edd45ed4..58f0a63f 100755 --- a/niftynet/application/base_application.py +++ b/niftynet/application/base_application.py @@ -70,6 +70,12 @@ class BaseApplication(with_metaclass(SingletonApplication, object)): outputs_collector = None gradients_collector = None + # performance + total_loss = None + patience = None + performance_history = [] + mode = None + def initialise_dataset_loader( self, data_param=None, task_param=None, data_partitioner=None): """ diff --git a/niftynet/application/classification_application.py b/niftynet/application/classification_application.py index 098e9b6d..258d5151 100755 --- a/niftynet/application/classification_application.py +++ b/niftynet/application/classification_application.py @@ -16,8 +16,8 @@ from niftynet.engine.application_variables import \ CONSOLE, NETWORK_OUTPUT, TF_SUMMARIES from niftynet.engine.sampler_resize_v2 import ResizeSampler -from niftynet.engine.windows_aggregator_classifier import \ - ClassifierSamplesAggregator +from niftynet.engine.windows_aggregator_resize import ResizeSamplesAggregator +from niftynet.engine.windows_aggregator_grid import GridSamplesAggregator from niftynet.io.image_reader import ImageReader from niftynet.layer.discrete_label_normalisation import \ DiscreteLabelNormalisationLayer @@ -136,7 +136,8 @@ def initialise_dataset_loader( augmentation_layers.append(RandomSpatialScalingLayer( min_percentage=train_param.scaling_percentage[0], max_percentage=train_param.scaling_percentage[1], - antialiasing=train_param.antialiasing)) + antialiasing=train_param.antialiasing, + isotropic=train_param.isotropic_scaling)) if train_param.rotation_angle or \ self.action_param.rotation_angle_x or \ self.action_param.rotation_angle_y or \ @@ -159,6 +160,15 @@ def initialise_dataset_loader( for reader in self.readers[1:]: reader.add_preprocessing_layers(normalisation_layers) + # Checking num_classes is set correctly + if self.classification_param.num_classes <= 1: + raise ValueError("Number of classes must be at least 2 for classification") + for preprocessor in self.readers[0].preprocessors: + if preprocessor.name == 'label_norm': + if len(preprocessor.label_map[preprocessor.key[0]]) != self.classification_param.num_classes: + raise ValueError("Number of unique labels must be equal to " + "number of classes (check histogram_ref file)") + def initialise_resize_sampler(self): self.sampler = [[ResizeSampler( reader=reader, @@ -169,7 +179,7 @@ def initialise_resize_sampler(self): self.readers]] def initialise_aggregator(self): - self.output_decoder = ClassifierSamplesAggregator( + self.output_decoder = ResizeSamplesAggregator( image_reader=self.readers[0], output_path=self.action_param.save_seg_dir, postfix=self.action_param.output_postfix) @@ -213,10 +223,8 @@ def add_confusion_matrix_summaries_(self, labels = tf.reshape(tf.cast(data_dict['label'], tf.int64), [-1]) prediction = tf.reshape(tf.argmax(net_out, -1), [-1]) num_classes = self.classification_param.num_classes - conf_mat = tf.contrib.metrics.confusion_matrix(labels, - prediction, - num_classes) - conf_mat = tf.to_float(conf_mat) / float(self.net_param.batch_size) + conf_mat = tf.contrib.metrics.confusion_matrix(labels, prediction, num_classes) + conf_mat = tf.to_float(conf_mat) if self.classification_param.num_classes == 2: outputs_collector.add_to_collection( var=conf_mat[1][1], name='true_positives', @@ -257,6 +265,8 @@ def switch_sampler(for_training): return sampler.pop_batch_op() if self.is_training: + self.patience = self.action_param.patience + self.mode = self.action_param.early_stopping_mode if self.action_param.validation_every_n > 0: data_dict = tf.cond(tf.logical_not(self.is_validation), lambda: switch_sampler(for_training=True), @@ -287,8 +297,19 @@ def switch_sampler(for_training): loss = data_loss + reg_loss else: loss = data_loss + + self.total_loss = loss + grads = self.optimiser.compute_gradients( loss, colocate_gradients_with_ops=True) + + outputs_collector.add_to_collection( + var=self.total_loss, name='total_loss', + average_over_devices=True, collection=CONSOLE) + outputs_collector.add_to_collection( + var=self.total_loss, name='total_loss', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) # collecting gradients variables gradients_collector.add_to_collection([grads]) # collecting output variables @@ -336,7 +357,8 @@ def switch_sampler(for_training): def interpret_output(self, batch_output): if not self.is_training: return self.output_decoder.decode_batch( - batch_output['window'], batch_output['location']) + {'csv': batch_output['window']}, + batch_output['location']) return True def initialise_evaluator(self, eval_param): diff --git a/niftynet/application/gan_application.py b/niftynet/application/gan_application.py index ae09ee5f..c86e5f8a 100755 --- a/niftynet/application/gan_application.py +++ b/niftynet/application/gan_application.py @@ -104,7 +104,8 @@ def initialise_dataset_loader( augmentation_layers.append(RandomSpatialScalingLayer( min_percentage=self.action_param.scaling_percentage[0], max_percentage=self.action_param.scaling_percentage[1], - antialiasing=self.action_param.antialiasing)) + antialiasing=self.action_param.antialiasing, + isotropic=self.action_param.isotropic_scaling)) if self.action_param.rotation_angle: augmentation_layers.append(RandomRotationLayer()) augmentation_layers[-1].init_uniform_angle( @@ -156,6 +157,8 @@ def connect_data_and_network(self, outputs_collector=None, gradients_collector=None): if self.is_training: + self.patience = self.action_param.patience + self.mode = self.action_param.early_stopping_mode def switch_sampler(for_training): with tf.name_scope('train' if for_training else 'validation'): sampler = self.get_sampler()[0][0 if for_training else -1] @@ -193,6 +196,16 @@ def switch_sampler(for_training): lossD = lossD + reg_loss lossG = lossG + reg_loss + self.total_loss = lossD + lossG + + outputs_collector.add_to_collection( + var=self.total_loss, name='total_loss', + average_over_devices=True, collection=CONSOLE) + outputs_collector.add_to_collection( + var=self.total_loss, name='total_loss', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + # variables to display in STDOUT outputs_collector.add_to_collection( var=lossD, name='lossD', average_over_devices=True, @@ -263,4 +276,5 @@ def interpret_output(self, batch_output): if self.is_training: return True return self.output_decoder.decode_batch( - batch_output['image'], batch_output['location']) + {'window_image': batch_output['image']}, + batch_output['location']) diff --git a/niftynet/application/label_driven_registration.py b/niftynet/application/label_driven_registration.py index dc90d9c4..2d37da02 100755 --- a/niftynet/application/label_driven_registration.py +++ b/niftynet/application/label_driven_registration.py @@ -124,6 +124,8 @@ def switch_samplers(for_training): return sampler() # returns image only if self.is_training: + self.patience = self.action_param.patience + self.mode = self.action_param.early_stopping_mode if self.action_param.validation_every_n > 0: sampler_window = \ tf.cond(tf.logical_not(self.is_validation), @@ -168,6 +170,8 @@ def switch_samplers(for_training): total_loss = total_loss + \ self.net_param.decay * tf.reduce_mean(reg_loss) + self.total_loss = total_loss + # compute training gradients with tf.name_scope('Optimiser'): optimiser_class = OptimiserFactory.create( @@ -204,7 +208,7 @@ def switch_samplers(for_training): collection=TF_SUMMARIES) outputs_collector.add_to_collection( var=total_loss, - name='averaged_total_loss', + name='total_loss', average_over_devices=True, summary_type='scalar', collection=TF_SUMMARIES) @@ -300,6 +304,6 @@ def interpret_output(self, batch_output): if self.is_training: return True return self.output_decoder.decode_batch( - batch_output['resampled_moving_image'], + {'window_resampled':batch_output['resampled_moving_image']}, batch_output['locations']) diff --git a/niftynet/application/regression_application.py b/niftynet/application/regression_application.py index 9777468e..ff00e961 100755 --- a/niftynet/application/regression_application.py +++ b/niftynet/application/regression_application.py @@ -25,8 +25,11 @@ from niftynet.layer.rand_flip import RandomFlipLayer from niftynet.layer.rand_rotation import RandomRotationLayer from niftynet.layer.rand_spatial_scaling import RandomSpatialScalingLayer +from niftynet.layer.rgb_histogram_equilisation import \ + RGBHistogramEquilisationLayer from niftynet.evaluation.regression_evaluator import RegressionEvaluator from niftynet.layer.rand_elastic_deform import RandomElasticDeformationLayer +from niftynet.engine.windows_aggregator_identity import WindowAsImageAggregator SUPPORTED_INPUT = set(['image', 'output', 'weight', 'sampler', 'inferred']) @@ -100,19 +103,24 @@ def initialise_dataset_loader( name='hist_norm_layer') \ if (self.net_param.histogram_ref_file and self.net_param.normalisation) else None + rgb_normaliser = RGBHistogramEquilisationLayer( + image_name='image', + name='rbg_norm_layer') if self.net_param.rgb_normalisation else None normalisation_layers = [] if histogram_normaliser is not None: normalisation_layers.append(histogram_normaliser) if mean_var_normaliser is not None: normalisation_layers.append(mean_var_normaliser) + if rgb_normaliser is not None: + normalisation_layers.append(rgb_normaliser) - volume_padding_layer = [] - if self.net_param.volume_padding_size: - volume_padding_layer.append(PadLayer( - image_name=SUPPORTED_INPUT, - border=self.net_param.volume_padding_size, - mode=self.net_param.volume_padding_mode)) + volume_padding_layer = [PadLayer( + image_name=SUPPORTED_INPUT, + border=self.net_param.volume_padding_size, + mode=self.net_param.volume_padding_mode, + pad_to=self.net_param.volume_padding_to_size) + ] # initialise training data augmentation layers augmentation_layers = [] @@ -125,7 +133,8 @@ def initialise_dataset_loader( augmentation_layers.append(RandomSpatialScalingLayer( min_percentage=train_param.scaling_percentage[0], max_percentage=train_param.scaling_percentage[1], - antialiasing=train_param.antialiasing)) + antialiasing=train_param.antialiasing, + isotropic=train_param.isotropic_scaling)) if train_param.rotation_angle: rotation_layer = RandomRotationLayer() if train_param.rotation_angle: @@ -202,7 +211,8 @@ def initialise_grid_aggregator(self): output_path=self.action_param.save_seg_dir, window_border=self.action_param.border, interp_order=self.action_param.output_interp_order, - postfix=self.action_param.output_postfix) + postfix=self.action_param.output_postfix, + fill_constant=self.action_param.fill_constant) def initialise_resize_aggregator(self): self.output_decoder = ResizeSamplesAggregator( @@ -211,6 +221,12 @@ def initialise_resize_aggregator(self): window_border=self.action_param.border, interp_order=self.action_param.output_interp_order, postfix=self.action_param.output_postfix) + + def initialise_identity_aggregator(self): + self.output_decoder = WindowAsImageAggregator( + image_reader=self.readers[0], + output_path=self.action_param.save_seg_dir, + postfix=self.action_param.output_postfix) def initialise_sampler(self): if self.is_training: @@ -219,7 +235,10 @@ def initialise_sampler(self): self.SUPPORTED_SAMPLING[self.net_param.window_sampling][1]() def initialise_aggregator(self): - self.SUPPORTED_SAMPLING[self.net_param.window_sampling][2]() + if self.net_param.force_output_identity_resizing: + self.initialise_identity_aggregator() + else: + self.SUPPORTED_SAMPLING[self.net_param.window_sampling][2]() def initialise_network(self): w_regularizer = None @@ -255,6 +274,8 @@ def switch_sampler(for_training): return sampler.pop_batch_op() if self.is_training: + self.patience = self.action_param.patience + self.mode = self.action_param.early_stopping_mode if self.action_param.validation_every_n > 0: data_dict = tf.cond(tf.logical_not(self.is_validation), lambda: switch_sampler(for_training=True), @@ -274,13 +295,20 @@ def switch_sampler(for_training): learning_rate=self.action_param.lr) loss_func = LossFunction(loss_type=self.action_param.loss_type) - crop_layer = CropLayer(border=self.regression_param.loss_border) weight_map = data_dict.get('weight', None) - weight_map = None if weight_map is None else crop_layer(weight_map) - data_loss = loss_func( - prediction=crop_layer(net_out), - ground_truth=crop_layer(data_dict['output']), - weight_map=weight_map) + border=self.regression_param.loss_border + if border == None or tf.reduce_sum(tf.abs(border)) == 0: + data_loss = loss_func( + prediction=net_out, + ground_truth=data_dict['output'], + weight_map=weight_map) + else: + crop_layer = CropLayer(border) + weight_map = None if weight_map is None else crop_layer(weight_map) + data_loss = loss_func( + prediction=crop_layer(net_out), + ground_truth=crop_layer(data_dict['output']), + weight_map=weight_map) reg_losses = tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES) if self.net_param.decay > 0.0 and reg_losses: reg_loss = tf.reduce_mean( @@ -307,11 +335,21 @@ def switch_sampler(for_training): len(tf.trainable_variables()), vars_to_freeze) + self.total_loss = loss + grads = self.optimiser.compute_gradients( loss, var_list=to_optimise, colocate_gradients_with_ops=True) # collecting gradients variables gradients_collector.add_to_collection([grads]) + # collecting output variables + outputs_collector.add_to_collection( + var=self.total_loss, name='total_loss', + average_over_devices=True, collection=CONSOLE) + outputs_collector.add_to_collection( + var=self.total_loss, name='total_loss', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) outputs_collector.add_to_collection( var=data_loss, name='loss', average_over_devices=False, collection=CONSOLE) @@ -319,6 +357,8 @@ def switch_sampler(for_training): var=data_loss, name='loss', average_over_devices=True, summary_type='scalar', collection=TF_SUMMARIES) + + elif self.is_inference: data_dict = switch_sampler(for_training=False) image = tf.cast(data_dict['image'], tf.float32) @@ -338,7 +378,7 @@ def switch_sampler(for_training): def interpret_output(self, batch_output): if self.is_inference: return self.output_decoder.decode_batch( - batch_output['window'], batch_output['location']) + {'window_reg':batch_output['window']}, batch_output['location']) return True def initialise_evaluator(self, eval_param): diff --git a/niftynet/application/segmentation_application.py b/niftynet/application/segmentation_application.py index 644d28f1..a7f9f73b 100755 --- a/niftynet/application/segmentation_application.py +++ b/niftynet/application/segmentation_application.py @@ -27,10 +27,13 @@ from niftynet.layer.rand_flip import RandomFlipLayer from niftynet.layer.rand_rotation import RandomRotationLayer from niftynet.layer.rand_spatial_scaling import RandomSpatialScalingLayer +from niftynet.layer.rgb_histogram_equilisation import \ + RGBHistogramEquilisationLayer from niftynet.evaluation.segmentation_evaluator import SegmentationEvaluator from niftynet.layer.rand_elastic_deform import RandomElasticDeformationLayer -SUPPORTED_INPUT = set(['image', 'label', 'weight', 'sampler', 'inferred']) +SUPPORTED_INPUT = set( + ['image', 'label', 'weight', 'sampler', 'inferred', 'value']) class SegmentationApplication(BaseApplication): @@ -109,6 +112,9 @@ def initialise_dataset_loader( name='hist_norm_layer') \ if (self.net_param.histogram_ref_file and self.net_param.normalisation) else None + rgb_normaliser = RGBHistogramEquilisationLayer( + image_name='image', + name='rbg_norm_layer') if self.net_param.rgb_normalisation else None label_normalisers = None if self.net_param.histogram_ref_file and \ task_param.label_normalisation: @@ -127,23 +133,26 @@ def initialise_dataset_loader( normalisation_layers = [] if histogram_normaliser is not None: normalisation_layers.append(histogram_normaliser) + if rgb_normaliser is not None: + normalisation_layers.append(rgb_normaliser) if mean_var_normaliser is not None: normalisation_layers.append(mean_var_normaliser) if task_param.label_normalisation and \ (self.is_training or not task_param.output_prob): normalisation_layers.extend(label_normalisers) - volume_padding_layer = [] - if self.net_param.volume_padding_size: - volume_padding_layer.append(PadLayer( - image_name=SUPPORTED_INPUT, - border=self.net_param.volume_padding_size, - mode=self.net_param.volume_padding_mode)) - + volume_padding_layer = [PadLayer( + image_name=SUPPORTED_INPUT, + border=self.net_param.volume_padding_size, + mode=self.net_param.volume_padding_mode, + pad_to=self.net_param.volume_padding_to_size) + ] # initialise training data augmentation layers augmentation_layers = [] if self.is_training: train_param = self.action_param + self.patience = train_param.patience + self.mode = self.action_param.early_stopping_mode if train_param.random_flipping_axes != -1: augmentation_layers.append(RandomFlipLayer( flip_axes=train_param.random_flipping_axes)) @@ -151,7 +160,8 @@ def initialise_dataset_loader( augmentation_layers.append(RandomSpatialScalingLayer( min_percentage=train_param.scaling_percentage[0], max_percentage=train_param.scaling_percentage[1], - antialiasing=train_param.antialiasing)) + antialiasing=train_param.antialiasing, + isotropic=train_param.isotropic_scaling)) if train_param.rotation_angle or \ train_param.rotation_angle_x or \ train_param.rotation_angle_y or \ @@ -182,6 +192,15 @@ def initialise_dataset_loader( reader.add_preprocessing_layers( volume_padding_layer + normalisation_layers) + # Checking num_classes is set correctly + if self.segmentation_param.num_classes <= 1: + raise ValueError("Number of classes must be at least 2 for segmentation") + for preprocessor in self.readers[0].preprocessors: + if preprocessor.name == 'label_norm': + if len(preprocessor.label_map[preprocessor.key[0]]) != self.segmentation_param.num_classes: + raise ValueError("Number of unique labels must be equal to " + "number of classes (check histogram_ref file)") + def initialise_uniform_sampler(self): self.sampler = [[UniformSampler( reader=reader, @@ -236,7 +255,8 @@ def initialise_grid_aggregator(self): output_path=self.action_param.save_seg_dir, window_border=self.action_param.border, interp_order=self.action_param.output_interp_order, - postfix=self.action_param.output_postfix) + postfix=self.action_param.output_postfix, + fill_constant=self.action_param.fill_constant) def initialise_resize_aggregator(self): self.output_decoder = ResizeSamplesAggregator( @@ -288,13 +308,64 @@ def switch_sampler(for_training): sampler = self.get_sampler()[0][0 if for_training else -1] return sampler.pop_batch_op() + def mixup_switch_sampler(for_training): + # get first set of samples + d_dict = switch_sampler(for_training=for_training) + + mix_fields = ('image', 'weight', 'label') + + if not for_training: + with tf.name_scope('nomix'): + # ensure label is appropriate for dense loss functions + ground_truth = tf.cast(d_dict['label'], tf.int32) + one_hot = tf.one_hot(tf.squeeze(ground_truth, axis=-1), + depth=self.segmentation_param.num_classes) + d_dict['label'] = one_hot + else: + with tf.name_scope('mixup'): + # get the mixing parameter from the Beta distribution + alpha = self.segmentation_param.mixup_alpha + beta = tf.distributions.Beta(alpha, alpha) # 1, 1: uniform: + rand_frac = beta.sample() + + # get another minibatch + d_dict_to_mix = switch_sampler(for_training=True) + + # look at binarised labels: sort them + if self.segmentation_param.mix_match: + # sum up the positive labels to sort by their volumes + inds1 = tf.argsort(tf.map_fn(tf.reduce_sum, tf.cast(d_dict['label'], tf.int64))) + inds2 = tf.argsort(tf.map_fn(tf.reduce_sum, tf.cast(d_dict_to_mix['label'] > 0, tf.int64))) + for field in [field for field in mix_fields if field in d_dict]: + d_dict[field] = tf.gather(d_dict[field], indices=inds1) + # note: sorted for opposite directions for d_dict_to_mix + d_dict_to_mix[field] = tf.gather(d_dict_to_mix[field], indices=inds2[::-1]) + + # making the labels dense and one-hot + for d in (d_dict, d_dict_to_mix): + ground_truth = tf.cast(d['label'], tf.int32) + one_hot = tf.one_hot(tf.squeeze(ground_truth, axis=-1), + depth=self.segmentation_param.num_classes) + d['label'] = one_hot + + # do the mixing for any fields that are relevant and present + mixed_up = {field: d_dict[field] * rand_frac + d_dict_to_mix[field] * (1 - rand_frac) for field + in mix_fields if field in d_dict} + # reassign all relevant values in d_dict + d_dict.update(mixed_up) + + return d_dict + if self.is_training: - if self.action_param.validation_every_n > 0: + if not self.segmentation_param.do_mixup: data_dict = tf.cond(tf.logical_not(self.is_validation), lambda: switch_sampler(for_training=True), lambda: switch_sampler(for_training=False)) else: - data_dict = switch_sampler(for_training=True) + # mix up the samples if not in validation phase + data_dict = tf.cond(tf.logical_not(self.is_validation), + lambda: mixup_switch_sampler(for_training=True), + lambda: mixup_switch_sampler(for_training=False)) # don't mix the validation image = tf.cast(data_dict['image'], tf.float32) net_args = {'is_training': self.is_training, @@ -343,9 +414,19 @@ def switch_sampler(for_training): grads = self.optimiser.compute_gradients( loss, var_list=to_optimise, colocate_gradients_with_ops=True) + self.total_loss = loss + # collecting gradients variables gradients_collector.add_to_collection([grads]) + # collecting output variables + outputs_collector.add_to_collection( + var=self.total_loss, name='total_loss', + average_over_devices=True, collection=CONSOLE) + outputs_collector.add_to_collection( + var=self.total_loss, name='total_loss', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) outputs_collector.add_to_collection( var=data_loss, name='loss', average_over_devices=False, collection=CONSOLE) @@ -401,7 +482,9 @@ def switch_sampler(for_training): def interpret_output(self, batch_output): if self.is_inference: return self.output_decoder.decode_batch( - batch_output['window'], batch_output['location']) + {'window_seg': batch_output['window']}, + batch_output['location']) + return True def initialise_evaluator(self, eval_param): diff --git a/niftynet/contrib/csv_reader/applications_maybe/__init__.py b/niftynet/contrib/csv_reader/applications_maybe/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/niftynet/contrib/csv_reader/applications_maybe/autoencoder_application.py b/niftynet/contrib/csv_reader/applications_maybe/autoencoder_application.py new file mode 100755 index 00000000..e3f9d234 --- /dev/null +++ b/niftynet/contrib/csv_reader/applications_maybe/autoencoder_application.py @@ -0,0 +1,300 @@ +import tensorflow as tf + +from niftynet.application.base_application import BaseApplication +from niftynet.engine.application_factory import ApplicationNetFactory +from niftynet.engine.application_factory import OptimiserFactory +from niftynet.engine.application_variables import CONSOLE +from niftynet.engine.application_variables import NETWORK_OUTPUT +from niftynet.engine.application_variables import TF_SUMMARIES +from niftynet.engine.sampler_linear_interpolate_v2 import LinearInterpolateSampler +from niftynet.contrib.csv_reader.sampler_linear_interpolate_v2_csv import \ + LinearInterpolateSamplerCSV as LinearInterpolateSampler +from niftynet.engine.sampler_resize_v2 import ResizeSampler +from niftynet.contrib.csv_reader.sampler_resize_v2_csv import \ + ResizeSamplerCSV as ResizeSampler +from niftynet.contrib.csv_reader.csv_reader import CSVReader +from niftynet.engine.windows_aggregator_identity import WindowAsImageAggregator +from niftynet.io.image_reader import ImageReader +from niftynet.layer.loss_autoencoder import LossFunction +from niftynet.utilities.util_common import look_up_operations + +SUPPORTED_INPUT = set(['image', 'feature']) +SUPPORTED_INFERENCE = \ + set(['encode', 'encode-decode', 'sample', 'linear_interpolation']) + + +class AutoencoderApplication(BaseApplication): + REQUIRED_CONFIG_SECTION = "AUTOENCODER" + + def __init__(self, net_param, action_param, action): + BaseApplication.__init__(self) + tf.logging.info('starting autoencoder application') + + self.action = action + + self.net_param = net_param + self.action_param = action_param + + self.data_param = None + self.autoencoder_param = None + + def initialise_dataset_loader( + self, data_param=None, task_param=None, data_partitioner=None): + self.data_param = data_param + self.autoencoder_param = task_param + + if not self.is_training: + self._infer_type = look_up_operations( + self.autoencoder_param.inference_type, SUPPORTED_INFERENCE) + else: + self._infer_type = None + try: + reader_phase = self.action_param.dataset_to_infer + except AttributeError: + reader_phase = None + file_lists = data_partitioner.get_file_lists_by( + phase=reader_phase, action=self.action) + # read each line of csv files into an instance of Subject + if self.is_evaluation: + NotImplementedError('Evaluation is not yet ' + 'supported in this application.') + if self.is_training: + self.readers = [] + self.csv_reader = [] + for file_list in file_lists: + reader = ImageReader(['image']) + reader.initialise(data_param, task_param, file_list) + self.readers.append(reader) + if self._infer_type in ('encode', 'encode-decode'): + self.readers = [ImageReader(['image'])] + self.readers[0].initialise(data_param, task_param, file_lists[0]) + elif self._infer_type == 'sample': + self.readers = [] + self.csv_reader = [] + elif self._infer_type == 'linear_interpolation': + self.csv_reader = [] + self.readers = [ImageReader(['feature'])] + self.readers[0].initialise(data_param, task_param, file_lists[0]) + # if self.is_training or self._infer_type in ('encode', 'encode-decode'): + # mean_var_normaliser = MeanVarNormalisationLayer(image_name='image') + # self.reader.add_preprocessing_layers([mean_var_normaliser]) + + def initialise_sampler(self): + self.sampler = [] + if self.is_training: + self.sampler.append([ResizeSampler( + reader=reader, + csv_reader=None, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + windows_per_image=1, + shuffle=True, + queue_length=self.net_param.queue_length) for reader in + self.readers]) + return + if self._infer_type in ('encode', 'encode-decode'): + self.sampler.append([ResizeSampler( + reader=reader, + csv_reader=None, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + windows_per_image=1, + shuffle=False, + queue_length=self.net_param.queue_length) for reader in + self.readers]) + return + if self._infer_type == 'linear_interpolation': + self.sampler.append([LinearInterpolateSampler( + reader=reader, + csv_reader=None, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + n_interpolations=self.autoencoder_param.n_interpolations, + queue_length=self.net_param.queue_length) for reader in + self.readers]) + return + + def initialise_network(self): + w_regularizer = None + b_regularizer = None + reg_type = self.net_param.reg_type.lower() + decay = self.net_param.decay + if reg_type == 'l2' and decay > 0: + from tensorflow.contrib.layers.python.layers import regularizers + w_regularizer = regularizers.l2_regularizer(decay) + b_regularizer = regularizers.l2_regularizer(decay) + elif reg_type == 'l1' and decay > 0: + from tensorflow.contrib.layers.python.layers import regularizers + w_regularizer = regularizers.l1_regularizer(decay) + b_regularizer = regularizers.l1_regularizer(decay) + + self.net = ApplicationNetFactory.create(self.net_param.name)( + w_regularizer=w_regularizer, + b_regularizer=b_regularizer) + + def connect_data_and_network(self, + outputs_collector=None, + gradients_collector=None): + def switch_sampler(for_training): + with tf.name_scope('train' if for_training else 'validation'): + sampler = self.get_sampler()[0][0 if for_training else -1] + return sampler.pop_batch_op() + + if self.is_training: + self.patience = self.action_param.patience + if self.action_param.validation_every_n > 0: + data_dict = tf.cond(tf.logical_not(self.is_validation), + lambda: switch_sampler(True), + lambda: switch_sampler(False)) + else: + data_dict = switch_sampler(for_training=True) + + image = tf.cast(data_dict['image'], tf.float32) + net_output = self.net(image, is_training=self.is_training) + + with tf.name_scope('Optimiser'): + optimiser_class = OptimiserFactory.create( + name=self.action_param.optimiser) + self.optimiser = optimiser_class.get_instance( + learning_rate=self.action_param.lr) + + loss_func = LossFunction(loss_type=self.action_param.loss_type) + data_loss = loss_func(net_output) + loss = data_loss + if self.net_param.decay > 0.0: + reg_losses = tf.get_collection( + tf.GraphKeys.REGULARIZATION_LOSSES) + if reg_losses: + reg_loss = tf.reduce_mean( + [tf.reduce_mean(reg_loss) for reg_loss in reg_losses]) + loss = loss + reg_loss + + self.total_loss = loss + grads = self.optimiser.compute_gradients( + loss, colocate_gradients_with_ops=True) + # collecting gradients variables + gradients_collector.add_to_collection([grads]) + + outputs_collector.add_to_collection( + var=self.total_loss, name='total_loss', + average_over_devices=True, collection=CONSOLE) + outputs_collector.add_to_collection( + var=self.total_loss, name='total_loss', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + + outputs_collector.add_to_collection( + var=data_loss, name='variational_lower_bound', + average_over_devices=True, collection=CONSOLE) + outputs_collector.add_to_collection( + var=data_loss, name='variational_lower_bound', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + + outputs_collector.add_to_collection( + var=net_output[4], name='Originals', + average_over_devices=False, summary_type='image3_coronal', + collection=TF_SUMMARIES) + outputs_collector.add_to_collection( + var=net_output[2], name='Means', + average_over_devices=False, summary_type='image3_coronal', + collection=TF_SUMMARIES) + outputs_collector.add_to_collection( + var=net_output[5], name='Variances', + average_over_devices=False, summary_type='image3_coronal', + collection=TF_SUMMARIES) + else: + if self._infer_type in ('encode', 'encode-decode'): + data_dict = self.get_sampler()[0][0].pop_batch_op() + image = tf.cast(data_dict['image'], dtype=tf.float32) + net_output = self.net(image, is_training=False) + + outputs_collector.add_to_collection( + var=data_dict['image_location'], name='location', + average_over_devices=True, collection=NETWORK_OUTPUT) + + if self._infer_type == 'encode-decode': + outputs_collector.add_to_collection( + var=net_output[2], name='generated_image', + average_over_devices=True, collection=NETWORK_OUTPUT) + if self._infer_type == 'encode': + outputs_collector.add_to_collection( + var=net_output[7], name='embedded', + average_over_devices=True, collection=NETWORK_OUTPUT) + + self.output_decoder = WindowAsImageAggregator( + image_reader=self.readers[0], + output_path=self.action_param.save_seg_dir) + return + elif self._infer_type == 'sample': + image_size = (self.net_param.batch_size,) + \ + self.action_param.spatial_window_size + (1,) + dummy_image = tf.zeros(image_size) + net_output = self.net(dummy_image, is_training=False) + noise_shape = net_output[-1].shape.as_list() + stddev = self.autoencoder_param.noise_stddev + noise = tf.random_normal(shape=noise_shape, + mean=0.0, + stddev=stddev, + dtype=tf.float32) + partially_decoded_sample = self.net.shared_decoder( + noise, is_training=False) + decoder_output = self.net.decoder_means( + partially_decoded_sample, is_training=False) + + outputs_collector.add_to_collection( + var=decoder_output, name='generated_image', + average_over_devices=True, collection=NETWORK_OUTPUT) + self.output_decoder = WindowAsImageAggregator( + image_reader=None, + output_path=self.action_param.save_seg_dir) + return + elif self._infer_type == 'linear_interpolation': + # construct the entire network + image_size = (self.net_param.batch_size,) + \ + self.action_param.spatial_window_size + (1,) + dummy_image = tf.zeros(image_size) + net_output = self.net(dummy_image, is_training=False) + data_dict = self.get_sampler()[0][0].pop_batch_op() + real_code = data_dict['feature'] + real_code = tf.reshape(real_code, net_output[-1].get_shape()) + partially_decoded_sample = self.net.shared_decoder( + real_code, is_training=False) + decoder_output = self.net.decoder_means( + partially_decoded_sample, is_training=False) + + outputs_collector.add_to_collection( + var=decoder_output, name='generated_image', + average_over_devices=True, collection=NETWORK_OUTPUT) + outputs_collector.add_to_collection( + var=data_dict['feature_location'], name='location', + average_over_devices=True, collection=NETWORK_OUTPUT) + self.output_decoder = WindowAsImageAggregator( + image_reader=self.readers[0], + output_path=self.action_param.save_seg_dir) + else: + raise NotImplementedError + + def interpret_output(self, batch_output): + if self.is_training: + return True + else: + infer_type = look_up_operations( + self.autoencoder_param.inference_type, + SUPPORTED_INFERENCE) + if infer_type == 'encode': + return self.output_decoder.decode_batch( + {'window_embedded':batch_output['embedded']}, + batch_output['location'][:, 0:1]) + if infer_type == 'encode-decode': + return self.output_decoder.decode_batch( + {'window_generated_image':batch_output['generated_image']}, + batch_output['location'][:, 0:1]) + if infer_type == 'sample': + return self.output_decoder.decode_batch( + {'generated_image':batch_output['generated_image']}, + None) + if infer_type == 'linear_interpolation': + return self.output_decoder.decode_batch( + {'generated_image':batch_output['generated_image']}, + batch_output['location'][:, :2]) diff --git a/niftynet/contrib/csv_reader/applications_maybe/gan_application.py b/niftynet/contrib/csv_reader/applications_maybe/gan_application.py new file mode 100755 index 00000000..2263720d --- /dev/null +++ b/niftynet/contrib/csv_reader/applications_maybe/gan_application.py @@ -0,0 +1,282 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function + +import tensorflow as tf + +from niftynet.application.base_application import BaseApplication +from niftynet.engine.application_factory import ApplicationNetFactory +from niftynet.engine.application_factory import OptimiserFactory +from niftynet.engine.application_variables import \ + CONSOLE, NETWORK_OUTPUT, TF_SUMMARIES +from niftynet.engine.sampler_random_vector_v2 import RandomVectorSampler +from niftynet.engine.sampler_resize_v2 import ResizeSampler +from niftynet.contrib.csv_reader.sampler_resize_v2_csv import \ + ResizeSamplerCSV as ResizeSampler +from niftynet.engine.windows_aggregator_identity import WindowAsImageAggregator +from niftynet.io.image_reader import ImageReader +from niftynet.layer.binary_masking import BinaryMaskingLayer +from niftynet.layer.histogram_normalisation import \ + HistogramNormalisationLayer +from niftynet.layer.loss_gan import LossFunction +from niftynet.layer.mean_variance_normalisation import \ + MeanVarNormalisationLayer +from niftynet.layer.rand_flip import RandomFlipLayer +from niftynet.layer.rand_rotation import RandomRotationLayer +from niftynet.layer.rand_spatial_scaling import RandomSpatialScalingLayer + +SUPPORTED_INPUT = set(['image', 'conditioning']) + + +class GANApplication(BaseApplication): + REQUIRED_CONFIG_SECTION = "GAN" + + def __init__(self, net_param, action_param, action): + BaseApplication.__init__(self) + tf.logging.info('starting GAN application') + self.action = action + + self.net_param = net_param + self.action_param = action_param + + self.data_param = None + self.gan_param = None + + def initialise_dataset_loader( + self, data_param=None, task_param=None, data_partitioner=None): + self.data_param = data_param + self.gan_param = task_param + + if self.is_training: + reader_names = ('image', 'conditioning') + elif self.is_inference: + # in the inference process use `conditioning` input only + reader_names = ('conditioning',) + elif self.is_evaluation: + tf.logging.fatal( + 'Evaluation is not yet supported in this application.') + raise NotImplementedError + else: + tf.logging.fatal( + 'Action `%s` not supported. Expected one of %s', + self.action, self.SUPPORTED_PHASES) + raise ValueError + try: + reader_phase = self.action_param.dataset_to_infer + except AttributeError: + reader_phase = None + file_lists = data_partitioner.get_file_lists_by( + phase=reader_phase, action=self.action) + self.readers = [ + ImageReader(reader_names).initialise( + data_param, task_param, file_list) for file_list in file_lists] + + # initialise input preprocessing layers + foreground_masking_layer = BinaryMaskingLayer( + type_str=self.net_param.foreground_type, + multimod_fusion=self.net_param.multimod_foreground_type, + threshold=0.0) \ + if self.net_param.normalise_foreground_only else None + mean_var_normaliser = MeanVarNormalisationLayer( + image_name='image', binary_masking_func=foreground_masking_layer) \ + if self.net_param.whitening else None + histogram_normaliser = HistogramNormalisationLayer( + image_name='image', + modalities=vars(task_param).get('image'), + model_filename=self.net_param.histogram_ref_file, + binary_masking_func=foreground_masking_layer, + norm_type=self.net_param.norm_type, + cutoff=self.net_param.cutoff, + name='hist_norm_layer') \ + if (self.net_param.histogram_ref_file and + self.net_param.normalisation) else None + + normalisation_layers = [] + if histogram_normaliser is not None: + normalisation_layers.append(histogram_normaliser) + if mean_var_normaliser is not None: + normalisation_layers.append(mean_var_normaliser) + + # initialise training data augmentation layers + augmentation_layers = [] + if self.is_training: + if self.action_param.random_flipping_axes != -1: + augmentation_layers.append(RandomFlipLayer( + flip_axes=self.action_param.random_flipping_axes)) + if self.action_param.scaling_percentage: + augmentation_layers.append(RandomSpatialScalingLayer( + min_percentage=self.action_param.scaling_percentage[0], + max_percentage=self.action_param.scaling_percentage[1], + antialiasing=self.action_param.antialiasing, + isotropic=self.action_param.isotropic_scaling)) + if self.action_param.rotation_angle: + augmentation_layers.append(RandomRotationLayer()) + augmentation_layers[-1].init_uniform_angle( + self.action_param.rotation_angle) + + # only add augmentation to first reader (not validation reader) + self.readers[0].add_preprocessing_layers( + normalisation_layers + augmentation_layers) + + for reader in self.readers[1:]: + reader.add_preprocessing_layers(normalisation_layers) + + def initialise_sampler(self): + self.sampler = [] + if self.is_training: + self.sampler.append([ResizeSampler( + reader=reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + windows_per_image=1, + shuffle=True, + queue_length=self.net_param.queue_length) for reader in + self.readers]) + else: + self.sampler.append([RandomVectorSampler( + names=('vector',), + vector_size=(self.gan_param.noise_size,), + batch_size=self.net_param.batch_size, + n_interpolations=self.gan_param.n_interpolations, + repeat=None, + queue_length=self.net_param.queue_length) for _ in + self.readers]) + # repeat each resized image n times, so that each + # image matches one random vector, + # (n = self.gan_param.n_interpolations) + self.sampler.append([ResizeSampler( + reader=reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + windows_per_image=self.gan_param.n_interpolations, + shuffle=False, + queue_length=self.net_param.queue_length) for reader in + self.readers]) + + def initialise_network(self): + self.net = ApplicationNetFactory.create(self.net_param.name)() + + def connect_data_and_network(self, + outputs_collector=None, + gradients_collector=None): + if self.is_training: + self.patience = self.action_param.patience + + def switch_sampler(for_training): + with tf.name_scope('train' if for_training else 'validation'): + sampler = self.get_sampler()[0][0 if for_training else -1] + return sampler.pop_batch_op() + + if self.action_param.validation_every_n > 0: + data_dict = tf.cond(tf.logical_not(self.is_validation), + lambda: switch_sampler(for_training=True), + lambda: switch_sampler(for_training=False)) + else: + data_dict = switch_sampler(for_training=True) + + images = tf.cast(data_dict['image'], tf.float32) + noise_shape = [self.net_param.batch_size, + self.gan_param.noise_size] + noise = tf.random_normal(shape=noise_shape, + mean=0.0, + stddev=1.0, + dtype=tf.float32) + conditioning = data_dict['conditioning'] + net_output = self.net( + noise, images, conditioning, self.is_training) + + loss_func = LossFunction( + loss_type=self.action_param.loss_type) + real_logits = net_output[1] + fake_logits = net_output[2] + lossG, lossD = loss_func(real_logits, fake_logits) + if self.net_param.decay > 0: + reg_losses = tf.get_collection( + tf.GraphKeys.REGULARIZATION_LOSSES) + if reg_losses: + reg_loss = tf.reduce_mean( + [tf.reduce_mean(l_reg) for l_reg in reg_losses]) + lossD = lossD + reg_loss + lossG = lossG + reg_loss + + self.total_loss = lossD + lossG + + outputs_collector.add_to_collection( + var=self.total_loss, name='total_loss', + average_over_devices=True, collection=CONSOLE) + outputs_collector.add_to_collection( + var=self.total_loss, name='total_loss', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + + # variables to display in STDOUT + outputs_collector.add_to_collection( + var=lossD, name='lossD', average_over_devices=True, + collection=CONSOLE) + outputs_collector.add_to_collection( + var=lossG, name='lossG', average_over_devices=False, + collection=CONSOLE) + # variables to display in tensorboard + outputs_collector.add_to_collection( + var=lossG, name='lossG', average_over_devices=False, + collection=TF_SUMMARIES) + outputs_collector.add_to_collection( + var=lossG, name='lossD', average_over_devices=True, + collection=TF_SUMMARIES) + + with tf.name_scope('Optimiser'): + optimiser_class = OptimiserFactory.create( + name=self.action_param.optimiser) + self.optimiser = optimiser_class.get_instance( + learning_rate=self.action_param.lr) + + with tf.name_scope('ComputeGradients'): + # gradients of generator + generator_variables = tf.get_collection( + tf.GraphKeys.TRAINABLE_VARIABLES, scope='generator') + generator_grads = self.optimiser.compute_gradients( + lossG, + var_list=generator_variables, + colocate_gradients_with_ops=True) + + # gradients of discriminator + discriminator_variables = tf.get_collection( + tf.GraphKeys.TRAINABLE_VARIABLES, scope='discriminator') + discriminator_grads = self.optimiser.compute_gradients( + lossD, + var_list=discriminator_variables, + colocate_gradients_with_ops=True) + grads = [generator_grads, discriminator_grads] + + # add the grads back to application_driver's training_grads + gradients_collector.add_to_collection(grads) + else: + data_dict = self.get_sampler()[0][0].pop_batch_op() + conditioning_dict = self.get_sampler()[1][0].pop_batch_op() + conditioning = conditioning_dict['conditioning'] + image_size = conditioning.shape.as_list()[:-1] + dummy_image = tf.zeros(image_size + [1]) + net_output = self.net(data_dict['vector'], + dummy_image, + conditioning, + self.is_training) + outputs_collector.add_to_collection( + var=net_output[0], + name='image', + average_over_devices=False, + collection=NETWORK_OUTPUT) + outputs_collector.add_to_collection( + var=conditioning_dict['conditioning_location'], + name='location', + average_over_devices=False, + collection=NETWORK_OUTPUT) + + self.output_decoder = WindowAsImageAggregator( + image_reader=self.readers[0], + output_path=self.action_param.save_seg_dir) + + def interpret_output(self, batch_output): + if self.is_training: + return True + return self.output_decoder.decode_batch( + {'window_image': batch_output['image']}, + batch_output['location']) diff --git a/niftynet/contrib/csv_reader/applications_maybe/label_driven_registration.py b/niftynet/contrib/csv_reader/applications_maybe/label_driven_registration.py new file mode 100755 index 00000000..79475bd2 --- /dev/null +++ b/niftynet/contrib/csv_reader/applications_maybe/label_driven_registration.py @@ -0,0 +1,309 @@ +""" +A preliminary re-implementation of: + Hu et al., Weakly-Supervised Convolutional Neural Networks for + Multimodal Image Registration, Medical Image Analysis (2018) + https://doi.org/10.1016/j.media.2018.07.002 + +The original implementation and tutorial is available at: + https://github.com/YipengHu/label-reg +""" + +from __future__ import absolute_import, division, print_function + +import tensorflow as tf + +from niftynet.application.base_application import BaseApplication +from niftynet.io.image_reader import ImageReader +from niftynet.contrib.sampler_pairwise.sampler_pairwise_uniform import \ + PairwiseUniformSampler +from niftynet.contrib.sampler_pairwise.sampler_pairwise_resize import \ + PairwiseResizeSampler +from niftynet.contrib.csv_reader.csv_reader import CSVReader +from niftynet.engine.application_factory import \ + OptimiserFactory, ApplicationNetFactory +from niftynet.engine.application_variables import \ + NETWORK_OUTPUT, CONSOLE, TF_SUMMARIES +from niftynet.engine.windows_aggregator_resize import ResizeSamplesAggregator + +from niftynet.layer.resampler import ResamplerLayer +from niftynet.layer.pad import PadLayer +from niftynet.layer.loss_segmentation import LossFunction + + +SUPPORTED_INPUT = {'moving_image', 'moving_label', + 'fixed_image', 'fixed_label'} + + +class RegApp(BaseApplication): + + REQUIRED_CONFIG_SECTION = "REGISTRATION" + + def __init__(self, net_param, action_param, action): + BaseApplication.__init__(self) + tf.logging.info('starting label-driven registration') + self.action = action + + self.net_param = net_param + self.action_param = action_param + + self.registration_param = None + self.data_param = None + + def initialise_dataset_loader( + self, data_param=None, task_param=None, data_partitioner=None): + self.data_param = data_param + self.registration_param = task_param + + if self.is_evaluation: + NotImplementedError('Evaluation is not yet ' + 'supported in this application.') + try: + reader_phase = self.action_param.dataset_to_infer + except AttributeError: + reader_phase = None + file_lists = data_partitioner.get_file_lists_by( + phase=reader_phase, action=self.action) + + self.readers = [] + for file_list in file_lists: + fixed_reader = ImageReader({'fixed_image', 'fixed_label'}) + fixed_reader.initialise(data_param, task_param, file_list) + self.readers.append(fixed_reader) + + moving_reader = ImageReader({'moving_image', 'moving_label'}) + moving_reader.initialise(data_param, task_param, file_list) + self.readers.append(moving_reader) + + # pad the fixed target only + # moving image will be resampled to match the targets + #volume_padding_layer = [] + #if self.net_param.volume_padding_size: + # volume_padding_layer.append(PadLayer( + # image_name=('fixed_image', 'fixed_label'), + # border=self.net_param.volume_padding_size)) + + #for reader in self.readers: + # reader.add_preprocessing_layers(volume_padding_layer) + + + def initialise_sampler(self): + if self.is_training: + self.sampler = [] + assert len(self.readers) >= 2, 'at least two readers are required' + training_sampler = PairwiseUniformSampler( + reader_0=self.readers[0], + reader_1=self.readers[1], + data_param=self.data_param, + batch_size=self.net_param.batch_size) + self.sampler.append(training_sampler) + # adding validation readers if possible + if len(self.readers) >= 4: + validation_sampler = PairwiseUniformSampler( + reader_0=self.readers[2], + reader_1=self.readers[3], + data_param=self.data_param, + batch_size=self.net_param.batch_size) + self.sampler.append(validation_sampler) + else: + self.sampler = PairwiseResizeSampler( + reader_0=self.readers[0], + reader_1=self.readers[1], + data_param=self.data_param, + batch_size=self.net_param.batch_size) + + def initialise_network(self): + decay = self.net_param.decay + self.net = ApplicationNetFactory.create(self.net_param.name)(decay) + + def connect_data_and_network(self, + outputs_collector=None, + gradients_collector=None): + + def switch_samplers(for_training): + with tf.name_scope('train' if for_training else 'validation'): + sampler = self.get_sampler()[0 if for_training else -1] + return sampler() # returns image only + + if self.is_training: + self.patience = self.action_param.patience + if self.action_param.validation_every_n > 0: + sampler_window = \ + tf.cond(tf.logical_not(self.is_validation), + lambda: switch_samplers(True), + lambda: switch_samplers(False)) + else: + sampler_window = switch_samplers(True) + + image_windows, _ = sampler_window + # image_windows, locations = sampler_window + + # decode channels for moving and fixed images + image_windows_list = [ + tf.expand_dims(img, axis=-1) + for img in tf.unstack(image_windows, axis=-1)] + fixed_image, fixed_label, moving_image, moving_label = \ + image_windows_list + + # estimate ddf + dense_field = self.net(fixed_image, moving_image) + if isinstance(dense_field, tuple): + dense_field = dense_field[0] + + # transform the moving labels + resampler = ResamplerLayer( + interpolation='linear', boundary='replicate') + resampled_moving_label = resampler(moving_label, dense_field) + + # compute label loss (foreground only) + loss_func = LossFunction( + n_class=1, + loss_type=self.action_param.loss_type, + softmax=False) + label_loss = loss_func(prediction=resampled_moving_label, + ground_truth=fixed_label) + + dice_fg = 1.0 - label_loss + # appending regularisation loss + total_loss = label_loss + reg_loss = tf.get_collection('bending_energy') + if reg_loss: + total_loss = total_loss + \ + self.net_param.decay * tf.reduce_mean(reg_loss) + + self.total_loss = total_loss + + # compute training gradients + with tf.name_scope('Optimiser'): + optimiser_class = OptimiserFactory.create( + name=self.action_param.optimiser) + self.optimiser = optimiser_class.get_instance( + learning_rate=self.action_param.lr) + grads = self.optimiser.compute_gradients( + total_loss, colocate_gradients_with_ops=True) + gradients_collector.add_to_collection(grads) + + metrics_dice = loss_func( + prediction=tf.to_float(resampled_moving_label >= 0.5), + ground_truth=tf.to_float(fixed_label >= 0.5)) + metrics_dice = 1.0 - metrics_dice + + # command line output + outputs_collector.add_to_collection( + var=dice_fg, name='one_minus_data_loss', + collection=CONSOLE) + outputs_collector.add_to_collection( + var=tf.reduce_mean(reg_loss), name='bending_energy', + collection=CONSOLE) + outputs_collector.add_to_collection( + var=total_loss, name='total_loss', collection=CONSOLE) + outputs_collector.add_to_collection( + var=metrics_dice, name='ave_fg_dice', collection=CONSOLE) + + # for tensorboard + outputs_collector.add_to_collection( + var=dice_fg, + name='data_loss', + average_over_devices=True, + summary_type='scalar', + collection=TF_SUMMARIES) + outputs_collector.add_to_collection( + var=total_loss, + name='total_loss', + average_over_devices=True, + summary_type='scalar', + collection=TF_SUMMARIES) + outputs_collector.add_to_collection( + var=metrics_dice, + name='averaged_foreground_Dice', + average_over_devices=True, + summary_type='scalar', + collection=TF_SUMMARIES) + + # for visualisation debugging + # resampled_moving_image = resampler(moving_image, dense_field) + # outputs_collector.add_to_collection( + # var=fixed_image, name='fixed_image', + # collection=NETWORK_OUTPUT) + # outputs_collector.add_to_collection( + # var=fixed_label, name='fixed_label', + # collection=NETWORK_OUTPUT) + # outputs_collector.add_to_collection( + # var=moving_image, name='moving_image', + # collection=NETWORK_OUTPUT) + # outputs_collector.add_to_collection( + # var=moving_label, name='moving_label', + # collection=NETWORK_OUTPUT) + # outputs_collector.add_to_collection( + # var=resampled_moving_image, name='resampled_image', + # collection=NETWORK_OUTPUT) + # outputs_collector.add_to_collection( + # var=resampled_moving_label, name='resampled_label', + # collection=NETWORK_OUTPUT) + # outputs_collector.add_to_collection( + # var=dense_field, name='ddf', collection=NETWORK_OUTPUT) + # outputs_collector.add_to_collection( + # var=locations, name='locations', collection=NETWORK_OUTPUT) + + # outputs_collector.add_to_collection( + # var=shift[0], name='a', collection=CONSOLE) + # outputs_collector.add_to_collection( + # var=shift[1], name='b', collection=CONSOLE) + else: + image_windows, locations = self.sampler() + image_windows_list = [ + tf.expand_dims(img, axis=-1) + for img in tf.unstack(image_windows, axis=-1)] + fixed_image, fixed_label, moving_image, moving_label = \ + image_windows_list + + dense_field = self.net(fixed_image, moving_image) + if isinstance(dense_field, tuple): + dense_field = dense_field[0] + + # transform the moving labels + resampler = ResamplerLayer( + interpolation='linear', boundary='replicate') + resampled_moving_image = resampler(moving_image, dense_field) + resampled_moving_label = resampler(moving_label, dense_field) + + outputs_collector.add_to_collection( + var=fixed_image, name='fixed_image', + collection=NETWORK_OUTPUT) + outputs_collector.add_to_collection( + var=moving_image, name='moving_image', + collection=NETWORK_OUTPUT) + outputs_collector.add_to_collection( + var=resampled_moving_image, + name='resampled_moving_image', + collection=NETWORK_OUTPUT) + outputs_collector.add_to_collection( + var=resampled_moving_label, + name='resampled_moving_label', + collection=NETWORK_OUTPUT) + + outputs_collector.add_to_collection( + var=fixed_label, name='fixed_label', + collection=NETWORK_OUTPUT) + outputs_collector.add_to_collection( + var=moving_label, name='moving_label', + collection=NETWORK_OUTPUT) + #outputs_collector.add_to_collection( + # var=dense_field, name='field', + # collection=NETWORK_OUTPUT) + outputs_collector.add_to_collection( + var=locations, name='locations', + collection=NETWORK_OUTPUT) + + self.output_decoder = ResizeSamplesAggregator( + image_reader=self.readers[0], # fixed image reader + name='fixed_image', + output_path=self.action_param.save_seg_dir, + interp_order=self.action_param.output_interp_order) + + def interpret_output(self, batch_output): + if self.is_training: + return True + return self.output_decoder.decode_batch( + {'window_resampled':batch_output['resampled_moving_image']}, + batch_output['locations']) + diff --git a/niftynet/contrib/csv_reader/applications_maybe/regression_application.py b/niftynet/contrib/csv_reader/applications_maybe/regression_application.py new file mode 100755 index 00000000..d55f9509 --- /dev/null +++ b/niftynet/contrib/csv_reader/applications_maybe/regression_application.py @@ -0,0 +1,394 @@ +# -*- coding: utf-8 -*- +import tensorflow as tf + +from niftynet.application.base_application import BaseApplication +from niftynet.engine.application_factory import \ + ApplicationNetFactory, InitializerFactory, OptimiserFactory +from niftynet.engine.application_variables import \ + CONSOLE, NETWORK_OUTPUT, TF_SUMMARIES +from niftynet.engine.sampler_grid_v2 import GridSampler +from niftynet.engine.sampler_resize_v2 import ResizeSampler +from niftynet.engine.sampler_uniform_v2 import UniformSampler +from niftynet.engine.sampler_weighted_v2 import WeightedSampler +from niftynet.engine.sampler_balanced_v2 import BalancedSampler +from niftynet.engine.windows_aggregator_grid import GridSamplesAggregator +from niftynet.engine.windows_aggregator_resize import ResizeSamplesAggregator +from niftynet.io.image_reader import ImageReader +from niftynet.layer.crop import CropLayer +from niftynet.layer.histogram_normalisation import \ + HistogramNormalisationLayer +from niftynet.layer.loss_regression import LossFunction +from niftynet.layer.mean_variance_normalisation import \ + MeanVarNormalisationLayer +from niftynet.layer.pad import PadLayer +from niftynet.layer.post_processing import PostProcessingLayer +from niftynet.layer.rand_flip import RandomFlipLayer +from niftynet.layer.rand_rotation import RandomRotationLayer +from niftynet.layer.rand_spatial_scaling import RandomSpatialScalingLayer +from niftynet.layer.rgb_histogram_equilisation import \ + RGBHistogramEquilisationLayer +from niftynet.evaluation.regression_evaluator import RegressionEvaluator +from niftynet.layer.rand_elastic_deform import RandomElasticDeformationLayer +from niftynet.contrib.csv_reader.csv_reader import CSVReader +from niftynet.contrib.csv_reader.sampler_resize_v2_csv import ResizeSamplerCSV as ResizeSampler +from niftynet.contrib.csv_reader.sampler_grid_v2_csv import GridSamplerCSV as \ + GridSampler +from niftynet.contrib.csv_reader.sampler_uniform_v2_csv import \ + UniformSamplerCSV \ + as UniformSampler +from niftynet.contrib.csv_reader.sampler_weighted_v2_csv import \ + WeightedSamplerCSV as WeightedSampler +from niftynet.contrib.csv_reader.sampler_balanced_v2_csv import \ + BalancedSamplerCSV as BalanceSampler + +SUPPORTED_INPUT = set(['image', 'output', 'weight', 'sampler', 'inferred']) + + +class RegressionApplication(BaseApplication): + REQUIRED_CONFIG_SECTION = "REGRESSION" + + def __init__(self, net_param, action_param, action): + BaseApplication.__init__(self) + tf.logging.info('starting regression application') + self.action = action + + self.net_param = net_param + self.action_param = action_param + + self.data_param = None + self.regression_param = None + self.SUPPORTED_SAMPLING = { + 'uniform': (self.initialise_uniform_sampler, + self.initialise_grid_sampler, + self.initialise_grid_aggregator), + 'weighted': (self.initialise_weighted_sampler, + self.initialise_grid_sampler, + self.initialise_grid_aggregator), + 'resize': (self.initialise_resize_sampler, + self.initialise_resize_sampler, + self.initialise_resize_aggregator), + 'balanced': (self.initialise_balanced_sampler, + self.initialise_grid_sampler, + self.initialise_grid_aggregator), + } + + def initialise_dataset_loader( + self, data_param=None, task_param=None, data_partitioner=None): + + self.data_param = data_param + self.regression_param = task_param + + # initialise input image readers + if self.is_training: + reader_names = ('image', 'output', 'weight', 'sampler') + csv_reader_names = () + elif self.is_inference: + # in the inference process use `image` input only + reader_names = ('image',) + csv_reader_names = () + elif self.is_evaluation: + reader_names = ('image', 'output', 'inferred') + csv_reader_names =() + else: + tf.logging.fatal( + 'Action `%s` not supported. Expected one of %s', + self.action, self.SUPPORTED_PHASES) + raise ValueError + try: + reader_phase = self.action_param.dataset_to_infer + except AttributeError: + reader_phase = None + file_lists = data_partitioner.get_file_lists_by( + phase=reader_phase, action=self.action) + self.readers = [ + ImageReader(reader_names).initialise( + data_param, task_param, file_list) for file_list in file_lists] + self.csv_readers = [CSVReader(csv_reader_names).initialise( + data_param, task_param, file_list) for file_list in file_lists] + + # initialise input preprocessing layers + mean_var_normaliser = MeanVarNormalisationLayer(image_name='image') \ + if self.net_param.whitening else None + histogram_normaliser = HistogramNormalisationLayer( + image_name='image', + modalities=vars(task_param).get('image'), + model_filename=self.net_param.histogram_ref_file, + norm_type=self.net_param.norm_type, + cutoff=self.net_param.cutoff, + name='hist_norm_layer') \ + if (self.net_param.histogram_ref_file and + self.net_param.normalisation) else None + rgb_normaliser = RGBHistogramEquilisationLayer( + image_name='image', + name='rbg_norm_layer') if self.net_param.rgb_normalisation else None + + normalisation_layers = [] + if histogram_normaliser is not None: + normalisation_layers.append(histogram_normaliser) + if mean_var_normaliser is not None: + normalisation_layers.append(mean_var_normaliser) + if rgb_normaliser is not None: + normalisation_layers.append(rgb_normaliser) + + volume_padding_layer = [PadLayer( + image_name=SUPPORTED_INPUT, + border=self.net_param.volume_padding_size, + mode=self.net_param.volume_padding_mode, + pad_to=self.net_param.volume_padding_to_size) + ] + + # initialise training data augmentation layers + augmentation_layers = [] + if self.is_training: + train_param = self.action_param + if train_param.random_flipping_axes != -1: + augmentation_layers.append(RandomFlipLayer( + flip_axes=train_param.random_flipping_axes)) + if train_param.scaling_percentage: + augmentation_layers.append(RandomSpatialScalingLayer( + min_percentage=train_param.scaling_percentage[0], + max_percentage=train_param.scaling_percentage[1], + antialiasing=train_param.antialiasing, + isotropic=train_param.isotropic_scaling)) + if train_param.rotation_angle: + rotation_layer = RandomRotationLayer() + if train_param.rotation_angle: + rotation_layer.init_uniform_angle( + train_param.rotation_angle) + augmentation_layers.append(rotation_layer) + if train_param.do_elastic_deformation: + spatial_rank = list(self.readers[0].spatial_ranks.values())[0] + augmentation_layers.append(RandomElasticDeformationLayer( + spatial_rank=spatial_rank, + num_controlpoints=train_param.num_ctrl_points, + std_deformation_sigma=train_param.deformation_sigma, + proportion_to_augment=train_param.proportion_to_deform)) + + # only add augmentation to first reader (not validation reader) + self.readers[0].add_preprocessing_layers( + volume_padding_layer + normalisation_layers + augmentation_layers) + + for reader in self.readers[1:]: + reader.add_preprocessing_layers( + volume_padding_layer + normalisation_layers) + + def initialise_uniform_sampler(self): + self.sampler = [[UniformSampler( + reader=reader, + csv_reader=None, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + windows_per_image=self.action_param.sample_per_volume, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_weighted_sampler(self): + self.sampler = [[WeightedSampler( + reader=reader, + csv_reader=None, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + windows_per_image=self.action_param.sample_per_volume, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_resize_sampler(self): + self.sampler = [[ResizeSampler( + reader=reader, + csv_reader=None, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + shuffle=self.is_training, + smaller_final_batch_mode=self.net_param.smaller_final_batch_mode, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_grid_sampler(self): + self.sampler = [[GridSampler( + reader=reader, + csv_reader=None, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + spatial_window_size=self.action_param.spatial_window_size, + window_border=self.action_param.border, + smaller_final_batch_mode=self.net_param.smaller_final_batch_mode, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_balanced_sampler(self): + self.sampler = [[BalancedSampler( + reader=reader, + csv_reader=None, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + windows_per_image=self.action_param.sample_per_volume, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_grid_aggregator(self): + self.output_decoder = GridSamplesAggregator( + image_reader=self.readers[0], + output_path=self.action_param.save_seg_dir, + window_border=self.action_param.border, + interp_order=self.action_param.output_interp_order, + postfix=self.action_param.output_postfix, + fill_constant=self.action_param.fill_constant) + + def initialise_resize_aggregator(self): + self.output_decoder = ResizeSamplesAggregator( + image_reader=self.readers[0], + output_path=self.action_param.save_seg_dir, + window_border=self.action_param.border, + interp_order=self.action_param.output_interp_order, + postfix=self.action_param.output_postfix) + + def initialise_sampler(self): + if self.is_training: + self.SUPPORTED_SAMPLING[self.net_param.window_sampling][0]() + elif self.is_inference: + self.SUPPORTED_SAMPLING[self.net_param.window_sampling][1]() + + def initialise_aggregator(self): + self.SUPPORTED_SAMPLING[self.net_param.window_sampling][2]() + + def initialise_network(self): + w_regularizer = None + b_regularizer = None + reg_type = self.net_param.reg_type.lower() + decay = self.net_param.decay + if reg_type == 'l2' and decay > 0: + from tensorflow.contrib.layers.python.layers import regularizers + w_regularizer = regularizers.l2_regularizer(decay) + b_regularizer = regularizers.l2_regularizer(decay) + elif reg_type == 'l1' and decay > 0: + from tensorflow.contrib.layers.python.layers import regularizers + w_regularizer = regularizers.l1_regularizer(decay) + b_regularizer = regularizers.l1_regularizer(decay) + + self.net = ApplicationNetFactory.create(self.net_param.name)( + num_classes=1, + w_initializer=InitializerFactory.get_initializer( + name=self.net_param.weight_initializer), + b_initializer=InitializerFactory.get_initializer( + name=self.net_param.bias_initializer), + w_regularizer=w_regularizer, + b_regularizer=b_regularizer, + acti_func=self.net_param.activation_function) + + def connect_data_and_network(self, + outputs_collector=None, + gradients_collector=None): + + def switch_sampler(for_training): + with tf.name_scope('train' if for_training else 'validation'): + sampler = self.get_sampler()[0][0 if for_training else -1] + return sampler.pop_batch_op() + + if self.is_training: + self.patience = self.action_param.patience + if self.action_param.validation_every_n > 0: + data_dict = tf.cond(tf.logical_not(self.is_validation), + lambda: switch_sampler(for_training=True), + lambda: switch_sampler(for_training=False)) + else: + data_dict = switch_sampler(for_training=True) + + image = tf.cast(data_dict['image'], tf.float32) + net_args = {'is_training': self.is_training, + 'keep_prob': self.net_param.keep_prob} + net_out = self.net(image, **net_args) + + with tf.name_scope('Optimiser'): + optimiser_class = OptimiserFactory.create( + name=self.action_param.optimiser) + self.optimiser = optimiser_class.get_instance( + learning_rate=self.action_param.lr) + loss_func = LossFunction(loss_type=self.action_param.loss_type) + + crop_layer = CropLayer(border=self.regression_param.loss_border) + weight_map = data_dict.get('weight', None) + weight_map = None if weight_map is None else crop_layer(weight_map) + data_loss = loss_func( + prediction=crop_layer(net_out), + ground_truth=crop_layer(data_dict['output']), + weight_map=weight_map) + reg_losses = tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES) + if self.net_param.decay > 0.0 and reg_losses: + reg_loss = tf.reduce_mean( + [tf.reduce_mean(reg_loss) for reg_loss in reg_losses]) + loss = data_loss + reg_loss + else: + loss = data_loss + + # Get all vars + to_optimise = tf.trainable_variables() + vars_to_freeze = \ + self.action_param.vars_to_freeze or \ + self.action_param.vars_to_restore + if vars_to_freeze: + import re + var_regex = re.compile(vars_to_freeze) + # Only optimise vars that are not frozen + to_optimise = \ + [v for v in to_optimise if not var_regex.search(v.name)] + tf.logging.info( + "Optimizing %d out of %d trainable variables, " + "the other variables are fixed (--vars_to_freeze %s)", + len(to_optimise), + len(tf.trainable_variables()), + vars_to_freeze) + + self.total_loss = loss + + grads = self.optimiser.compute_gradients( + loss, var_list=to_optimise, colocate_gradients_with_ops=True) + # collecting gradients variables + gradients_collector.add_to_collection([grads]) + + # collecting output variables + outputs_collector.add_to_collection( + var=self.total_loss, name='total_loss', + average_over_devices=True, collection=CONSOLE) + outputs_collector.add_to_collection( + var=self.total_loss, name='total_loss', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + outputs_collector.add_to_collection( + var=data_loss, name='loss', + average_over_devices=False, collection=CONSOLE) + outputs_collector.add_to_collection( + var=data_loss, name='loss', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + + + elif self.is_inference: + data_dict = switch_sampler(for_training=False) + image = tf.cast(data_dict['image'], tf.float32) + net_args = {'is_training': self.is_training, + 'keep_prob': self.net_param.keep_prob} + net_out = self.net(image, **net_args) + net_out = PostProcessingLayer('IDENTITY')(net_out) + + outputs_collector.add_to_collection( + var=net_out, name='window', + average_over_devices=False, collection=NETWORK_OUTPUT) + outputs_collector.add_to_collection( + var=data_dict['image_location'], name='location', + average_over_devices=False, collection=NETWORK_OUTPUT) + self.initialise_aggregator() + + def interpret_output(self, batch_output): + if self.is_inference: + return self.output_decoder.decode_batch( + {'window_reg':batch_output['window']}, batch_output['location']) + return True + + def initialise_evaluator(self, eval_param): + self.eval_param = eval_param + self.evaluator = RegressionEvaluator(self.readers[0], + self.regression_param, + eval_param) + + def add_inferred_output(self, data_param, task_param): + return self.add_inferred_output_like(data_param, task_param, 'output') diff --git a/niftynet/contrib/csv_reader/applications_maybe/segmentation_application.py b/niftynet/contrib/csv_reader/applications_maybe/segmentation_application.py new file mode 100755 index 00000000..2e144200 --- /dev/null +++ b/niftynet/contrib/csv_reader/applications_maybe/segmentation_application.py @@ -0,0 +1,433 @@ +# -*- coding: utf-8 -*- +import tensorflow as tf + +from niftynet.application.base_application import BaseApplication +from niftynet.engine.application_factory import \ + ApplicationNetFactory, InitializerFactory, OptimiserFactory +from niftynet.engine.application_variables import \ + CONSOLE, NETWORK_OUTPUT, TF_SUMMARIES +from niftynet.engine.sampler_grid_v2 import GridSampler +from niftynet.engine.sampler_resize_v2 import ResizeSampler +from niftynet.engine.sampler_uniform_v2 import UniformSampler +from niftynet.engine.sampler_weighted_v2 import WeightedSampler +from niftynet.engine.sampler_balanced_v2 import BalancedSampler +from niftynet.engine.windows_aggregator_grid import GridSamplesAggregator +from niftynet.engine.windows_aggregator_resize import ResizeSamplesAggregator +from niftynet.io.image_reader import ImageReader +from niftynet.layer.binary_masking import BinaryMaskingLayer +from niftynet.layer.discrete_label_normalisation import \ + DiscreteLabelNormalisationLayer +from niftynet.layer.histogram_normalisation import \ + HistogramNormalisationLayer +from niftynet.layer.loss_segmentation import LossFunction +from niftynet.layer.mean_variance_normalisation import \ + MeanVarNormalisationLayer +from niftynet.layer.pad import PadLayer +from niftynet.layer.post_processing import PostProcessingLayer +from niftynet.layer.rand_flip import RandomFlipLayer +from niftynet.layer.rand_rotation import RandomRotationLayer +from niftynet.layer.rand_spatial_scaling import RandomSpatialScalingLayer +from niftynet.layer.rgb_histogram_equilisation import \ + RGBHistogramEquilisationLayer +from niftynet.evaluation.segmentation_evaluator import SegmentationEvaluator +from niftynet.layer.rand_elastic_deform import RandomElasticDeformationLayer + +SUPPORTED_INPUT = set(['image', 'label', 'weight', 'sampler', 'inferred']) + + +class SegmentationApplication(BaseApplication): + REQUIRED_CONFIG_SECTION = "SEGMENTATION" + + def __init__(self, net_param, action_param, action): + super(SegmentationApplication, self).__init__() + tf.logging.info('starting segmentation application') + self.action = action + + self.net_param = net_param + self.action_param = action_param + + self.data_param = None + self.segmentation_param = None + self.SUPPORTED_SAMPLING = { + 'uniform': (self.initialise_uniform_sampler, + self.initialise_grid_sampler, + self.initialise_grid_aggregator), + 'weighted': (self.initialise_weighted_sampler, + self.initialise_grid_sampler, + self.initialise_grid_aggregator), + 'resize': (self.initialise_resize_sampler, + self.initialise_resize_sampler, + self.initialise_resize_aggregator), + 'balanced': (self.initialise_balanced_sampler, + self.initialise_grid_sampler, + self.initialise_grid_aggregator), + } + + def initialise_dataset_loader( + self, data_param=None, task_param=None, data_partitioner=None): + + self.data_param = data_param + self.segmentation_param = task_param + + # initialise input image readers + if self.is_training: + reader_names = ('image', 'label', 'weight', 'sampler') + elif self.is_inference: + # in the inference process use `image` input only + reader_names = ('image',) + elif self.is_evaluation: + reader_names = ('image', 'label', 'inferred') + else: + tf.logging.fatal( + 'Action `%s` not supported. Expected one of %s', + self.action, self.SUPPORTED_PHASES) + raise ValueError + try: + reader_phase = self.action_param.dataset_to_infer + except AttributeError: + reader_phase = None + file_lists = data_partitioner.get_file_lists_by( + phase=reader_phase, action=self.action) + self.readers = [ + ImageReader(reader_names).initialise( + data_param, task_param, file_list) for file_list in file_lists] + + # initialise input preprocessing layers + foreground_masking_layer = BinaryMaskingLayer( + type_str=self.net_param.foreground_type, + multimod_fusion=self.net_param.multimod_foreground_type, + threshold=0.0) \ + if self.net_param.normalise_foreground_only else None + mean_var_normaliser = MeanVarNormalisationLayer( + image_name='image', binary_masking_func=foreground_masking_layer) \ + if self.net_param.whitening else None + histogram_normaliser = HistogramNormalisationLayer( + image_name='image', + modalities=vars(task_param).get('image'), + model_filename=self.net_param.histogram_ref_file, + binary_masking_func=foreground_masking_layer, + norm_type=self.net_param.norm_type, + cutoff=self.net_param.cutoff, + name='hist_norm_layer') \ + if (self.net_param.histogram_ref_file and + self.net_param.normalisation) else None + rgb_normaliser = RGBHistogramEquilisationLayer( + image_name='image', + name='rbg_norm_layer') if self.net_param.rgb_normalisation else None + label_normalisers = None + if self.net_param.histogram_ref_file and \ + task_param.label_normalisation: + label_normalisers = [DiscreteLabelNormalisationLayer( + image_name='label', + modalities=vars(task_param).get('label'), + model_filename=self.net_param.histogram_ref_file)] + if self.is_evaluation: + label_normalisers.append( + DiscreteLabelNormalisationLayer( + image_name='inferred', + modalities=vars(task_param).get('inferred'), + model_filename=self.net_param.histogram_ref_file)) + label_normalisers[-1].key = label_normalisers[0].key + + normalisation_layers = [] + if histogram_normaliser is not None: + normalisation_layers.append(histogram_normaliser) + if rgb_normaliser is not None: + normalisation_layers.append(rgb_normaliser) + if mean_var_normaliser is not None: + normalisation_layers.append(mean_var_normaliser) + if task_param.label_normalisation and \ + (self.is_training or not task_param.output_prob): + normalisation_layers.extend(label_normalisers) + + volume_padding_layer = [PadLayer( + image_name=SUPPORTED_INPUT, + border=self.net_param.volume_padding_size, + mode=self.net_param.volume_padding_mode, + pad_to=self.net_param.volume_padding_to_size) + ] + # initialise training data augmentation layers + augmentation_layers = [] + if self.is_training: + train_param = self.action_param + self.patience = train_param.patience + if train_param.random_flipping_axes != -1: + augmentation_layers.append(RandomFlipLayer( + flip_axes=train_param.random_flipping_axes)) + if train_param.scaling_percentage: + augmentation_layers.append(RandomSpatialScalingLayer( + min_percentage=train_param.scaling_percentage[0], + max_percentage=train_param.scaling_percentage[1], + antialiasing=train_param.antialiasing, + isotropic=train_param.isotropic_scaling)) + if train_param.rotation_angle or \ + train_param.rotation_angle_x or \ + train_param.rotation_angle_y or \ + train_param.rotation_angle_z: + rotation_layer = RandomRotationLayer() + if train_param.rotation_angle: + rotation_layer.init_uniform_angle( + train_param.rotation_angle) + else: + rotation_layer.init_non_uniform_angle( + train_param.rotation_angle_x, + train_param.rotation_angle_y, + train_param.rotation_angle_z) + augmentation_layers.append(rotation_layer) + if train_param.do_elastic_deformation: + spatial_rank = list(self.readers[0].spatial_ranks.values())[0] + augmentation_layers.append(RandomElasticDeformationLayer( + spatial_rank=spatial_rank, + num_controlpoints=train_param.num_ctrl_points, + std_deformation_sigma=train_param.deformation_sigma, + proportion_to_augment=train_param.proportion_to_deform)) + + # only add augmentation to first reader (not validation reader) + self.readers[0].add_preprocessing_layers( + volume_padding_layer + normalisation_layers + augmentation_layers) + + for reader in self.readers[1:]: + reader.add_preprocessing_layers( + volume_padding_layer + normalisation_layers) + + def initialise_uniform_sampler(self): + self.sampler = [[UniformSampler( + reader=reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + windows_per_image=self.action_param.sample_per_volume, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_weighted_sampler(self): + self.sampler = [[WeightedSampler( + reader=reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + windows_per_image=self.action_param.sample_per_volume, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_resize_sampler(self): + self.sampler = [[ResizeSampler( + reader=reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + shuffle=self.is_training, + smaller_final_batch_mode=self.net_param.smaller_final_batch_mode, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_grid_sampler(self): + self.sampler = [[GridSampler( + reader=reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + spatial_window_size=self.action_param.spatial_window_size, + window_border=self.action_param.border, + smaller_final_batch_mode=self.net_param.smaller_final_batch_mode, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_balanced_sampler(self): + self.sampler = [[BalancedSampler( + reader=reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + windows_per_image=self.action_param.sample_per_volume, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_grid_aggregator(self): + self.output_decoder = GridSamplesAggregator( + image_reader=self.readers[0], + output_path=self.action_param.save_seg_dir, + window_border=self.action_param.border, + interp_order=self.action_param.output_interp_order, + postfix=self.action_param.output_postfix, + fill_constant=self.action_param.fill_constant) + + def initialise_resize_aggregator(self): + self.output_decoder = ResizeSamplesAggregator( + image_reader=self.readers[0], + output_path=self.action_param.save_seg_dir, + window_border=self.action_param.border, + interp_order=self.action_param.output_interp_order, + postfix=self.action_param.output_postfix) + + def initialise_sampler(self): + if self.is_training: + self.SUPPORTED_SAMPLING[self.net_param.window_sampling][0]() + elif self.is_inference: + self.SUPPORTED_SAMPLING[self.net_param.window_sampling][1]() + + def initialise_aggregator(self): + self.SUPPORTED_SAMPLING[self.net_param.window_sampling][2]() + + def initialise_network(self): + w_regularizer = None + b_regularizer = None + reg_type = self.net_param.reg_type.lower() + decay = self.net_param.decay + if reg_type == 'l2' and decay > 0: + from tensorflow.contrib.layers.python.layers import regularizers + w_regularizer = regularizers.l2_regularizer(decay) + b_regularizer = regularizers.l2_regularizer(decay) + elif reg_type == 'l1' and decay > 0: + from tensorflow.contrib.layers.python.layers import regularizers + w_regularizer = regularizers.l1_regularizer(decay) + b_regularizer = regularizers.l1_regularizer(decay) + + self.net = ApplicationNetFactory.create(self.net_param.name)( + num_classes=self.segmentation_param.num_classes, + w_initializer=InitializerFactory.get_initializer( + name=self.net_param.weight_initializer), + b_initializer=InitializerFactory.get_initializer( + name=self.net_param.bias_initializer), + w_regularizer=w_regularizer, + b_regularizer=b_regularizer, + acti_func=self.net_param.activation_function) + + def connect_data_and_network(self, + outputs_collector=None, + gradients_collector=None): + + def switch_sampler(for_training): + with tf.name_scope('train' if for_training else 'validation'): + sampler = self.get_sampler()[0][0 if for_training else -1] + return sampler.pop_batch_op() + + if self.is_training: + if self.action_param.validation_every_n > 0: + data_dict = tf.cond(tf.logical_not(self.is_validation), + lambda: switch_sampler(for_training=True), + lambda: switch_sampler(for_training=False)) + else: + data_dict = switch_sampler(for_training=True) + + image = tf.cast(data_dict['image'], tf.float32) + net_args = {'is_training': self.is_training, + 'keep_prob': self.net_param.keep_prob} + net_out = self.net(image, **net_args) + + with tf.name_scope('Optimiser'): + optimiser_class = OptimiserFactory.create( + name=self.action_param.optimiser) + self.optimiser = optimiser_class.get_instance( + learning_rate=self.action_param.lr) + loss_func = LossFunction( + n_class=self.segmentation_param.num_classes, + loss_type=self.action_param.loss_type, + softmax=self.segmentation_param.softmax) + data_loss = loss_func( + prediction=net_out, + ground_truth=data_dict.get('label', None), + weight_map=data_dict.get('weight', None)) + reg_losses = tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES) + if self.net_param.decay > 0.0 and reg_losses: + reg_loss = tf.reduce_mean( + [tf.reduce_mean(reg_loss) for reg_loss in reg_losses]) + loss = data_loss + reg_loss + else: + loss = data_loss + + # Get all vars + to_optimise = tf.trainable_variables() + vars_to_freeze = \ + self.action_param.vars_to_freeze or \ + self.action_param.vars_to_restore + if vars_to_freeze: + import re + var_regex = re.compile(vars_to_freeze) + # Only optimise vars that are not frozen + to_optimise = \ + [v for v in to_optimise if not var_regex.search(v.name)] + tf.logging.info( + "Optimizing %d out of %d trainable variables, " + "the other variables fixed (--vars_to_freeze %s)", + len(to_optimise), + len(tf.trainable_variables()), + vars_to_freeze) + + grads = self.optimiser.compute_gradients( + loss, var_list=to_optimise, colocate_gradients_with_ops=True) + + self.total_loss = loss + + # collecting gradients variables + gradients_collector.add_to_collection([grads]) + + # collecting output variables + outputs_collector.add_to_collection( + var=self.total_loss, name='total_loss', + average_over_devices=True, collection=CONSOLE) + outputs_collector.add_to_collection( + var=self.total_loss, name='total_loss', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + outputs_collector.add_to_collection( + var=data_loss, name='loss', + average_over_devices=False, collection=CONSOLE) + outputs_collector.add_to_collection( + var=data_loss, name='loss', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + + # outputs_collector.add_to_collection( + # var=image*180.0, name='image', + # average_over_devices=False, summary_type='image3_sagittal', + # collection=TF_SUMMARIES) + + # outputs_collector.add_to_collection( + # var=image, name='image', + # average_over_devices=False, + # collection=NETWORK_OUTPUT) + + # outputs_collector.add_to_collection( + # var=tf.reduce_mean(image), name='mean_image', + # average_over_devices=False, summary_type='scalar', + # collection=CONSOLE) + elif self.is_inference: + # converting logits into final output for + # classification probabilities or argmax classification labels + data_dict = switch_sampler(for_training=False) + image = tf.cast(data_dict['image'], tf.float32) + net_args = {'is_training': self.is_training, + 'keep_prob': self.net_param.keep_prob} + net_out = self.net(image, **net_args) + + output_prob = self.segmentation_param.output_prob + num_classes = self.segmentation_param.num_classes + if output_prob and num_classes > 1: + post_process_layer = PostProcessingLayer( + 'SOFTMAX', num_classes=num_classes) + elif not output_prob and num_classes > 1: + post_process_layer = PostProcessingLayer( + 'ARGMAX', num_classes=num_classes) + else: + post_process_layer = PostProcessingLayer( + 'IDENTITY', num_classes=num_classes) + net_out = post_process_layer(net_out) + + outputs_collector.add_to_collection( + var=net_out, name='window', + average_over_devices=False, collection=NETWORK_OUTPUT) + outputs_collector.add_to_collection( + var=data_dict['image_location'], name='location', + average_over_devices=False, collection=NETWORK_OUTPUT) + self.initialise_aggregator() + + def interpret_output(self, batch_output): + if self.is_inference: + return self.output_decoder.decode_batch( + {'window_seg':batch_output['window']}, batch_output['location']) + return True + + def initialise_evaluator(self, eval_param): + self.eval_param = eval_param + self.evaluator = SegmentationEvaluator(self.readers[0], + self.segmentation_param, + eval_param) + + def add_inferred_output(self, data_param, task_param): + return self.add_inferred_output_like(data_param, task_param, 'label') diff --git a/niftynet/contrib/csv_reader/class_seg_finnet.py b/niftynet/contrib/csv_reader/class_seg_finnet.py new file mode 100755 index 00000000..aee02a3a --- /dev/null +++ b/niftynet/contrib/csv_reader/class_seg_finnet.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function + +from six.moves import range + +from niftynet.layer import layer_util +from niftynet.layer.activation import ActiLayer +from niftynet.layer.base_layer import TrainableLayer +from niftynet.layer.bn import BNLayer +from niftynet.layer.convolution import ConvLayer, ConvolutionalLayer +from niftynet.layer.dilatedcontext import DilatedTensor +from niftynet.layer.elementwise import ElementwiseLayer +from niftynet.network.base_net import BaseNet +from niftynet.layer.layer_util import infer_spatial_rank +from niftynet.layer.fully_connected import FullyConnectedLayer +# from niftynet.layer.pool_full import PoolingLayer +import tensorflow as tf + + +class ClassSegFinnet(BaseNet): + """ + implementation of HighRes3DNet: + Li et al., "On the compactness, efficiency, and representation of 3D + convolutional networks: Brain parcellation as a pretext task", IPMI '17 + """ + + def __init__(self, + num_classes, + w_initializer=None, + w_regularizer=None, + b_initializer=None, + b_regularizer=None, + acti_func='prelu', + name='FinalClassSeg'): + + super(ClassSegFinnet, self).__init__( + num_classes=num_classes, + w_initializer=w_initializer, + w_regularizer=w_regularizer, + b_initializer=b_initializer, + b_regularizer=b_regularizer, + acti_func=acti_func, + name=name) + self.num_classes = num_classes + self.layers = [ + {'name': 'fc_seg', 'n_features': 10, + 'kernel_size': 1}, + {'name': 'pool', 'n_features': 10, + 'stride': 1, 'func': 'AVG'}, + {'name': 'fc_seg', 'n_features': num_classes, + 'kernel_size': 1}, + {'name': 'fc_class', 'n_features': 2, 'kernel_size': 1}] + + def layer_op(self, images, is_training, layer_id=-1): + # go through self.layers, create an instance of each layer + # and plugin data + layer_instances = [] + + # class convolution layer + params = self.layers[0] + fc_seg = ConvolutionalLayer( + with_bn=False, + n_output_chns=params['n_features'], + kernel_size=params['kernel_size'], + acti_func=self.acti_func, + padding='VALID', + w_initializer=self.initializers['w'], + w_regularizer=self.regularizers['w'], + name=params['name']) + flow = fc_seg(images, is_training) + layer_instances.append((fc_seg, flow)) + + # pooling layer + params = self.layers[1] + # pool_layer = PoolingLayer( + # func=params['func'], + # name=params['name']) + # flow_pool = pool_layer(flow) + flow_pool = flow + flow_pool = tf.reshape(flow_pool, [tf.shape(images)[0], 1, 1, 1, + self.layers[1][ + 'n_features']]) + print("check flow pooling", flow_pool.shape) + layer_instances.append((pool_layer, flow_pool)) + + # seg convolution layer + params = self.layers[2] + seg_conv_layer = ConvolutionalLayer( + with_bn=False, + n_output_chns=params['n_features'], + acti_func=self.acti_func, + w_initializer=self.initializers['w'], + w_regularizer=self.regularizers['w'], + kernel_size=1, + name=params['name']) + seg_flow = seg_conv_layer(flow, is_training) + layer_instances.append((seg_conv_layer, seg_flow)) + + # class convolution layer + params = self.layers[3] + class_conv_layer = ConvolutionalLayer( + with_bn=False, + n_output_chns=params['n_features'], + acti_func=self.acti_func, + w_initializer=self.initializers['w'], + w_regularizer=self.regularizers['w'], + kernel_size=1, + name=params['name']) + class_flow = class_conv_layer(flow_pool, is_training) + layer_instances.append((class_conv_layer, class_flow)) + + # set training properties + if is_training: + self._print(layer_instances) + return layer_instances[-2][1], layer_instances[-1][1] + return layer_instances[-2][1], layer_instances[layer_id][1] + + def _print(self, list_of_layers): + for (op, _) in list_of_layers: + print(op) diff --git a/niftynet/contrib/csv_reader/classification_application.py b/niftynet/contrib/csv_reader/classification_application.py new file mode 100755 index 00000000..f7e2ad3c --- /dev/null +++ b/niftynet/contrib/csv_reader/classification_application.py @@ -0,0 +1,353 @@ +# -*- coding: utf-8 -*- +""" +This module defines an image-level classification application +that maps from images to scalar, multi-class labels. + +This class is instantiated and initalized by the application_driver. +""" + +import os + +import tensorflow as tf + +from niftynet.application.base_application import BaseApplication +from niftynet.engine.application_factory import \ + ApplicationNetFactory, InitializerFactory, OptimiserFactory +from niftynet.engine.application_variables import \ + CONSOLE, NETWORK_OUTPUT, TF_SUMMARIES +from niftynet.contrib.csv_reader.sampler_resize_v2_csv import ResizeSamplerCSV as ResizeSampler +# from niftynet.engine.windows_aggregator_classifier import \ +# ClassifierSamplesAggregator +from niftynet.io.image_reader import ImageReader +from niftynet.contrib.csv_reader.csv_reader import CSVReader +from niftynet.layer.discrete_label_normalisation import \ + DiscreteLabelNormalisationLayer +from niftynet.layer.histogram_normalisation import \ + HistogramNormalisationLayer +from niftynet.layer.binary_masking import BinaryMaskingLayer +from niftynet.layer.post_processing import PostProcessingLayer +from niftynet.layer.loss_classification import LossFunction +from niftynet.layer.mean_variance_normalisation import \ + MeanVarNormalisationLayer +from niftynet.layer.rand_flip import RandomFlipLayer +from niftynet.layer.rand_rotation import RandomRotationLayer +from niftynet.layer.rand_spatial_scaling import RandomSpatialScalingLayer +from niftynet.evaluation.classification_evaluator import ClassificationEvaluator + +SUPPORTED_INPUT = set(['image', 'label', 'sampler', 'inferred']) + + +class ClassificationApplication(BaseApplication): + """This class defines an application for image-level classification + problems mapping from images to scalar labels. + + This is the application class to be instantiated by the driver + and referred to in configuration files. + + Although structurally similar to segmentation, this application + supports different samplers/aggregators (because patch-based + processing is not appropriate), and monitoring metrics.""" + + REQUIRED_CONFIG_SECTION = "CLASSIFICATION" + + def __init__(self, net_param, action_param, action): + super(ClassificationApplication, self).__init__() + tf.logging.info('starting classification application') + self.action = action + + self.net_param = net_param + self.action_param = action_param + + self.data_param = None + self.classification_param = None + self.SUPPORTED_SAMPLING = { + 'resize': (self.initialise_resize_sampler, + self.initialise_resize_sampler), + } + + def initialise_dataset_loader( + self, data_param=None, task_param=None, data_partitioner=None): + + self.data_param = data_param + self.classification_param = task_param + + if self.is_training: + image_reader_names = ('image', 'sampler') + csv_reader_names = ('label',) + elif self.is_inference: + image_reader_names = ('image',) + elif self.is_evaluation: + image_reader_names = ('image', 'inferred') + csv_reader_names = ('label',) + else: + tf.logging.fatal( + 'Action `%s` not supported. Expected one of %s', + self.action, self.SUPPORTED_PHASES) + raise ValueError + try: + reader_phase = self.action_param.dataset_to_infer + except AttributeError: + reader_phase = None + file_lists = data_partitioner.get_file_lists_by( + phase=reader_phase, action=self.action) + self.readers = [ + ImageReader(image_reader_names).initialise( + data_param, task_param, file_list) for file_list in file_lists] + self.csv_readers = [ + CSVReader(csv_reader_names).initialise( + data_param, task_param, file_list) for file_list in file_lists] + + foreground_masking_layer = BinaryMaskingLayer( + type_str=self.net_param.foreground_type, + multimod_fusion=self.net_param.multimod_foreground_type, + threshold=0.0) \ + if self.net_param.normalise_foreground_only else None + + mean_var_normaliser = MeanVarNormalisationLayer( + image_name='image', binary_masking_func=foreground_masking_layer) \ + if self.net_param.whitening else None + histogram_normaliser = HistogramNormalisationLayer( + image_name='image', + modalities=vars(task_param).get('image'), + model_filename=self.net_param.histogram_ref_file, + binary_masking_func=foreground_masking_layer, + norm_type=self.net_param.norm_type, + cutoff=self.net_param.cutoff, + name='hist_norm_layer') \ + if (self.net_param.histogram_ref_file and + self.net_param.normalisation) else None + + label_normaliser = DiscreteLabelNormalisationLayer( + image_name='label', + modalities=vars(task_param).get('label'), + model_filename=self.net_param.histogram_ref_file) \ + if (self.net_param.histogram_ref_file and + task_param.label_normalisation) else None + + normalisation_layers = [] + if histogram_normaliser is not None: + normalisation_layers.append(histogram_normaliser) + if mean_var_normaliser is not None: + normalisation_layers.append(mean_var_normaliser) + if label_normaliser is not None: + normalisation_layers.append(label_normaliser) + + augmentation_layers = [] + if self.is_training: + train_param = self.action_param + if train_param.random_flipping_axes != -1: + augmentation_layers.append(RandomFlipLayer( + flip_axes=train_param.random_flipping_axes)) + if train_param.scaling_percentage: + augmentation_layers.append(RandomSpatialScalingLayer( + min_percentage=train_param.scaling_percentage[0], + max_percentage=train_param.scaling_percentage[1])) + if train_param.rotation_angle or \ + self.action_param.rotation_angle_x or \ + self.action_param.rotation_angle_y or \ + self.action_param.rotation_angle_z: + rotation_layer = RandomRotationLayer() + if train_param.rotation_angle: + rotation_layer.init_uniform_angle( + train_param.rotation_angle) + else: + rotation_layer.init_non_uniform_angle( + self.action_param.rotation_angle_x, + self.action_param.rotation_angle_y, + self.action_param.rotation_angle_z) + augmentation_layers.append(rotation_layer) + + # only add augmentation to first reader (not validation reader) + self.readers[0].add_preprocessing_layers( + normalisation_layers + augmentation_layers) + + for reader in self.readers[1:]: + reader.add_preprocessing_layers(normalisation_layers) + + def initialise_resize_sampler(self): + self.sampler = [[ResizeSampler( + reader=image_reader, + csv_reader=csv_reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + shuffle=self.is_training, + queue_length=self.net_param.queue_length) for image_reader, csv_reader in + zip(self.readers, self.csv_readers)]] + + def initialise_aggregator(self): + self.output_decoder = ClassifierSamplesAggregator( + image_reader=self.readers[0], + output_path=self.action_param.save_seg_dir, + postfix=self.action_param.output_postfix) + + def initialise_sampler(self): + if self.is_training: + self.SUPPORTED_SAMPLING[self.net_param.window_sampling][0]() + else: + self.SUPPORTED_SAMPLING[self.net_param.window_sampling][1]() + + def initialise_network(self): + w_regularizer = None + b_regularizer = None + reg_type = self.net_param.reg_type.lower() + decay = self.net_param.decay + if reg_type == 'l2' and decay > 0: + from tensorflow.contrib.layers.python.layers import regularizers + w_regularizer = regularizers.l2_regularizer(decay) + b_regularizer = regularizers.l2_regularizer(decay) + elif reg_type == 'l1' and decay > 0: + from tensorflow.contrib.layers.python.layers import regularizers + w_regularizer = regularizers.l1_regularizer(decay) + b_regularizer = regularizers.l1_regularizer(decay) + + self.net = ApplicationNetFactory.create(self.net_param.name)( + num_classes=self.classification_param.num_classes, + w_initializer=InitializerFactory.get_initializer( + name=self.net_param.weight_initializer), + b_initializer=InitializerFactory.get_initializer( + name=self.net_param.bias_initializer), + w_regularizer=w_regularizer, + b_regularizer=b_regularizer, + acti_func=self.net_param.activation_function) + + def add_confusion_matrix_summaries_(self, + outputs_collector, + net_out, + data_dict): + """ This method defines several monitoring metrics that + are derived from the confusion matrix """ + labels = tf.reshape(tf.cast(data_dict['label'], tf.int64), [-1]) + prediction = tf.reshape(tf.argmax(net_out, -1), [-1]) + num_classes = self.classification_param.num_classes + conf_mat = tf.confusion_matrix(labels, prediction, num_classes) + conf_mat = tf.to_float(conf_mat) + if self.classification_param.num_classes == 2: + outputs_collector.add_to_collection( + var=conf_mat[1][1], name='true_positives', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + outputs_collector.add_to_collection( + var=conf_mat[1][0], name='false_negatives', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + outputs_collector.add_to_collection( + var=conf_mat[0][1], name='false_positives', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + outputs_collector.add_to_collection( + var=conf_mat[0][0], name='true_negatives', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + else: + outputs_collector.add_to_collection( + var=conf_mat[tf.newaxis, :, :, tf.newaxis], + name='confusion_matrix', + average_over_devices=True, summary_type='image', + collection=TF_SUMMARIES) + + outputs_collector.add_to_collection( + var=tf.trace(conf_mat), name='accuracy', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + + + def connect_data_and_network(self, + outputs_collector=None, + gradients_collector=None): + + def switch_sampler(for_training): + with tf.name_scope('train' if for_training else 'validation'): + sampler = self.get_sampler()[0][0 if for_training else -1] + return sampler.pop_batch_op() + + if self.is_training: + if self.action_param.validation_every_n > 0: + data_dict = tf.cond(tf.logical_not(self.is_validation), + lambda: switch_sampler(for_training=True), + lambda: switch_sampler(for_training=False)) + else: + data_dict = switch_sampler(for_training=True) + + image = tf.cast(data_dict['image'], tf.float32) + net_args = {'is_training': self.is_training, + 'keep_prob': self.net_param.keep_prob} + net_out = self.net(image, **net_args) + + with tf.name_scope('Optimiser'): + optimiser_class = OptimiserFactory.create( + name=self.action_param.optimiser) + self.optimiser = optimiser_class.get_instance( + learning_rate=self.action_param.lr) + loss_func = LossFunction( + n_class=self.classification_param.num_classes, + loss_type=self.action_param.loss_type) + data_loss = loss_func( + prediction=net_out, + ground_truth=data_dict.get('label', None)) + reg_losses = tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES) + if self.net_param.decay > 0.0 and reg_losses: + reg_loss = tf.reduce_mean( + [tf.reduce_mean(reg_loss) for reg_loss in reg_losses]) + loss = data_loss + reg_loss + else: + loss = data_loss + grads = self.optimiser.compute_gradients( + loss, colocate_gradients_with_ops=True) + # collecting gradients variables + gradients_collector.add_to_collection([grads]) + # collecting output variables + outputs_collector.add_to_collection( + var=data_loss, name='data_loss', + average_over_devices=False, collection=CONSOLE) + outputs_collector.add_to_collection( + var=data_loss, name='data_loss', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + self.add_confusion_matrix_summaries_(outputs_collector, + net_out, + data_dict) + else: + # converting logits into final output for + # classification probabilities or argmax classification labels + data_dict = switch_sampler(for_training=False) + image = tf.cast(data_dict['image'], tf.float32) + net_args = {'is_training': self.is_training, + 'keep_prob': self.net_param.keep_prob} + net_out = self.net(image, **net_args) + tf.logging.info( + 'net_out.shape may need to be resized: %s', net_out.shape) + output_prob = self.classification_param.output_prob + num_classes = self.classification_param.num_classes + if output_prob and num_classes > 1: + post_process_layer = PostProcessingLayer( + 'SOFTMAX', num_classes=num_classes) + elif not output_prob and num_classes > 1: + post_process_layer = PostProcessingLayer( + 'ARGMAX', num_classes=num_classes) + else: + post_process_layer = PostProcessingLayer( + 'IDENTITY', num_classes=num_classes) + net_out = post_process_layer(net_out) + + outputs_collector.add_to_collection( + var=net_out, name='window', + average_over_devices=False, collection=NETWORK_OUTPUT) + outputs_collector.add_to_collection( + var=data_dict['image_location'], name='location', + average_over_devices=False, collection=NETWORK_OUTPUT) + self.initialise_aggregator() + + def interpret_output(self, batch_output): + if not self.is_training: + return self.output_decoder.decode_batch( + batch_output['window'], batch_output['location']) + return True + + def initialise_evaluator(self, eval_param): + self.eval_param = eval_param + self.evaluator = ClassificationEvaluator(self.readers[0], + self.classification_param, + eval_param) + + def add_inferred_output(self, data_param, task_param): + return self.add_inferred_output_like(data_param, task_param, 'label') diff --git a/niftynet/contrib/csv_reader/csv_reader.py b/niftynet/contrib/csv_reader/csv_reader.py index 41642c84..9529a31a 100644 --- a/niftynet/contrib/csv_reader/csv_reader.py +++ b/niftynet/contrib/csv_reader/csv_reader.py @@ -1,60 +1,366 @@ + +import random import numpy as np import pandas as pd import tensorflow as tf from tensorflow.python.data.util import nest from niftynet.layer.base_layer import Layer +from niftynet.io.image_reader import param_to_dict + class CSVReader(Layer): - + ''' + Class that performs the reading of the csv_input and select the lines + according to the subject id if available + ''' def __init__(self, names=None): + self.names = names self._paths = None self._labels = None self._df = None self.label_names = None + self.n_samples_per_id = None self.dims = None - + self.data_param = None + self._dims = None + self._indexable_output = {} + self.file_list = None + self.subject_ids = None + self._shapes = {} + self._input_sources = None + self.df_by_task = {} + self.valid_by_task = {} + self.pad_by_task = {} + self.dims_by_task = {} + self.type_by_task = {} + self._dtypes = {} + self.task_param = None super(CSVReader, self).__init__(name='csv_reader') - def initialise(self, path_to_csv): - label_df = pd.read_csv(path_to_csv, header=None, names=['subject_ids', 'labels']) - self._paths = label_df['subject_ids'].values - self.label_names = list(label_df['labels'].unique()) - self._df = label_df - self.dims = len(self.label_names) - - self._labels = self.to_ohe(label_df['labels'].values) + def initialise(self, data_param, task_param=None, file_list=None, + sample_per_volume=1): + """ + this function takes in a data_param specifying the name of the source and the location of + the csv data. Three input modes are supported: + - 'label' - expects a csv with header subject_id,label. + - 'features' - expects a csv with header subject_id,, + e.g.:: + + data_param = {'label': {'csv_data_file': 'path/to/some_data.csv', 'to_ohe': False}} + + :param data_param: dictionary of input sections + :param task_param: Namespace object + :param file_list: a dataframe generated by ImagePartitioner + :param sample_per_volume: number of samples taken per volume (useful + to know how much to tile the csv output + for cross validation, so + that the reader only loads files in training/inference phases. + """ + assert self.names is not None + data_param = param_to_dict(data_param) + self.n_samples_per_id = sample_per_volume + print(data_param) + if not task_param: + task_param = {mod: (mod,) for mod in list(data_param)} + try: + if not isinstance(task_param, dict): + task_param = vars(task_param) + except ValueError: + tf.logging.fatal( + "To concatenate multiple input data arrays,\n" + "task_param should be a dictionary in the form:\n" + "{'new_modality_name': ['modality_1', 'modality_2',...]}.") + raise + self.task_param = task_param + valid_names = [name for name in self.names if self.task_param.get( + name, None)] + if not valid_names: + tf.logging.fatal("CSVReader requires task input keywords %s, but " + "not exist in the config file.\n" + "Available task keywords: %s", + self.names, list(self.task_param)) + raise ValueError + self.names = valid_names + self.data_param = data_param + self._dims = None + self._indexable_output = {} + self.file_list = file_list + self.subject_ids = self.file_list['subject_id'].values + + self._input_sources = dict((name, self.task_param.get(name)) + for name in self.names) + self.df_by_task = {} + self.valid_by_task = {} + self.pad_by_task = {} + self.dims_by_task = {} + self.type_by_task = {} + + for name in valid_names: + df_fin, _indexable_output, _dims = self._parse_csv( + path_to_csv=data_param[name].get('csv_data_file', None), + to_ohe=data_param[name].get('to_ohe', False) + ) + self.df_by_task[name] = df_fin + self.dims_by_task[name] = _dims + self._indexable_output[name] = _indexable_output + self.valid_by_task[name] = -1 * np.ones( [self.df_by_task[name].shape[0]]) # -1 means they have not been checked + + self.pad_by_task[name] = np.zeros( + [self.df_by_task[name].shape[0], 2*_dims]) + if df_fin.shape[0] > len(set(self.subject_ids)): + self.type_by_task[name] = 'multi' + else: + self.type_by_task[name] = 'mono' + # Converts Dictionary of Lists to List of Dictionaries + # self._indexable_output = pd.DataFrame( + # self._indexable_output).to_dict('records') + assert file_list is not None return self - def to_ohe(self, labels): - return [np.eye(len(self.label_names))[self.label_names.index(label)] for label in labels] - - def layer_op(self, idx=None, shuffle=True): - # def apply_expand_dims(x, n): - # if n==0: - # return x - # return np.expand_dims(apply_expand_dims(x, n - 1), -1) - data = self._labels[idx] - while len(data.shape) < 4: - data = np.expand_dims(data, -1) - label_dict = {'label': data} - # label_dict = {'label': apply_expand_dims(np.expand_dims(np.array(data).astype(np.float32), 0), 4)} - return idx, label_dict, None + def _parse_csv(self, path_to_csv, to_ohe): + tf.logging.warning('This method will read your entire csv into memory') + df_init = pd.read_csv(path_to_csv, index_col=0, header=None) + + df_init.index = df_init.index.map(str) + + if set(df_init.index) != set(self.subject_ids): + print("probably different because of split file - drop not " + "relevant ones") + df_fin = df_init.drop(index=[s for s in set(df_init.index) if s + not in set(self.subject_ids)]) + # df.reset_index(drop=True, inplace=True) + + if set(df_fin.index) != set(self.subject_ids): + print(set(self.subject_ids) - set(df_fin.index)) + tf.logging.fatal('csv file provided at: {} does not have ' + 'all the subject_ids'.format(path_to_csv)) + raise Exception + else: + df_fin = df_init.copy() + if to_ohe and len(df_fin.columns) == 1: + _dims = len(list(df_fin[1].unique())) + _indexable_output = self.to_ohe(df_fin[1].values, _dims) + return df_fin, _indexable_output, _dims + elif not to_ohe and len(df_fin.columns) == 1: + _dims = 1 + _indexable_output = self.to_categorical(df_fin[1].values, + np.sort(df_fin[1].unique())) + return df_fin, _indexable_output, _dims + elif not to_ohe: + _dims = len(df_fin.columns) + _indexable_output = list(df_fin.values) + return df_fin, _indexable_output, _dims + tf.logging.fatal('Unrecognised input format for {}'.format(path_to_csv)) + raise Exception('Unrecognised input format for {}'.format(path_to_csv)) + + @staticmethod + def to_ohe(labels, _dims): + ''' + Transform the labeling to one hot encoding + :param labels: labels to encode + :param _dims: + :return: + ''' + label_names = list(set(labels)) + ohe = [np.eye(_dims)[label_names.index(label)].astype(np.float32) + for label in labels] + return ohe + + @staticmethod + def to_categorical(labels, label_names): + ''' + Transformation of labels to categorical + :param labels: labels to change + :param label_names: + :return: + ''' + return [np.array(list(label_names).index(label)).astype(np.float32) + for label in labels] + + def layer_op(self, idx=None, subject_id=None, mode='single', reject=True): + ''' + Perform the csv_reading and assignment to dictionary + :param idx: index of the image + :param subject_id: subject id + :param mode: chosen mode (multi or single) + :param reject: if some elements should be rejected + :return: + ''' + if idx is None and subject_id is not None: + print("Need to decide upon idx from subject %s" % subject_id) + idx_dict = {} + if mode == 'single': + print("Taking only one index among other valid") + # Take the list of idx corresponding to subject id and randomly + # sample from there + for name in self.names: + + relevant_indices = self.df_by_task[name].reset_index()[ + self.df_by_task[name].reset_index()[0] == subject_id].index.values + if reject: + relevant_valid = np.asarray(np.where(np.abs( + self.valid_by_task[name][relevant_indices]) > 0)[0]) + if relevant_valid is None: + relevant_valid = [] + print(relevant_valid, reject) + else: + relevant_valid = np.arange(len(relevant_indices)) + print(relevant_valid, " is list of indices to sample " + "from") + print(np.asarray(relevant_valid).shape[0], "is shape of " + "relevant_valid") + relevant_final = [relevant_indices[v] for v in + relevant_valid] if \ + np.asarray(relevant_valid).shape[0] > 0 else [] + print(relevant_final, "is relevant final") + idx_dict[name] = random.choice(relevant_final) if \ + list(relevant_final) else [] + + else: #self.df_by_task[self.df_by_task[name] == subject_id] + # mode full i.e. output all the lines corresponding to + # subject_id + print(" Taking all valid indices") + for name in self.names: + + relevant_indices = self.df_by_task[name].reset_index()[ + self.df_by_task[name].reset_index()[0] == subject_id].index.values + if reject: + relevant_valid = np.asarray(np.where(np.abs( + self.valid_by_task[name][relevant_indices]) > 0)[0]) + if relevant_valid is None: + relevant_valid = [] + else: + relevant_valid = np.arange(len(relevant_indices)) + relevant_final = [relevant_indices[v] for v in + relevant_valid] if \ + np.asarray(relevant_valid).shape[0] > 0 else [] + idx_dict[name] = relevant_final + + elif idx is None and subject_id is None: + idx_dict = {} + print("Need to also choose subject id") + for name in self.names: + if subject_id is None: + idx_dict[name] = np.random.randint( + self.df_by_task[name].shape[0]) + subject_id = self.df_by_task[name].iloc[idx_dict[name]].name + print("new subject id is ", subject_id) + if mode == 'single': + # Take the list of idx corresponding to subject id + # and randomly sample from there + print("Need to find index in single mode") + + relevant_indices = np.asarray(np.where(self.df_by_task[ + name].index.get_loc(subject_id))[0]) + # print("Found initial relevant", relevant_indices, + # set(self.df_by_task[name].index), name, + # self.df_by_task[name].index.get_loc(subject_id).shape) + if reject: + relevant_valid = np.asarray(np.where(np.abs( + self.valid_by_task[name][relevant_indices]) > 0)[0]) + else: + relevant_valid = np.arange(len(relevant_indices)) + # print("Found corresponding valid", relevant_valid, + # np.max(relevant_valid), relevant_indices.shape) + relevant_final = [relevant_indices[v] for v in + relevant_valid] + # print(relevant_indices, subject_id) + # relevant_indices = self._df.loc[subject_id] + assert list(relevant_final), 'no valid index for subject ' \ + '%s and field %s' % (subject_id, name) + idx_dict[name] = random.choice(relevant_final) + else: # mode full i.e. output all the lines corresponding to + # subject_id + relevant_indices = np.asarray(np.where(self.df_by_task[ + name].index.get_loc(subject_id))[0]) + # print("Found initial relevant", relevant_indices, + # set(self.df_by_task[name].index), name, + # self.df_by_task[name].index.get_loc(subject_id).shape) + if reject: + relevant_valid = np.asarray(np.where(np.abs( + self.valid_by_task[name][relevant_indices]) > 0)[0]) + else: + relevant_valid = np.ones_like(relevant_indices) + # print("Found corresponding valid", relevant_valid, + # np.max(relevant_valid), relevant_indices.shape) + relevant_final = [relevant_indices[v] for v in + relevant_valid] + assert list(relevant_final), 'no valid index for subject ' \ + '%s and field %s' % (subject_id, name) + idx_dict[name] = relevant_final + elif not isinstance(idx, dict): + idx_dict = {} + for name in self.names: + idx_dict[name] = idx + if subject_id is None: + subject_id = self.df_by_task[name].iloc[idx_dict[name]].name + else: + idx_dict = {} + for name in self.names: + idx_dict[name] = idx[name] + assert list(idx[name]), 'no valid index for %s' % name + if subject_id is None: + subject_id = self.df_by_task[name].iloc[idx_dict[name]].name + + if self._indexable_output is not None: + output_dict = {k: self.apply_niftynet_format_to_data( + self.tile_nsamples(np.asarray( + self._indexable_output[k])[idx_dict[k]])) for k in + idx_dict.keys()} + # print(idx_dict, self._indexable_output['modality_label'][ + # idx_dict['modality_label']]) + return idx_dict, output_dict, subject_id + raise Exception('Invalid mode') + + + def tile_nsamples(self, data): + ''' + Tile the csv_read to have the same value applied to the nsamples + extracted from the volume + :param data: csv data to tile for all samples extracted in the volume + :return: tiled data + ''' + if self.n_samples_per_id > 1: + print("preparing tiling") + data = np.expand_dims(data, 1) + data = np.tile(data, np.asarray( + np.concatenate( + ([self.n_samples_per_id], + [1, ]*(len(np.asarray(data.shape))))), + dtype=np.int)) + print("tiling done", data.shape) + return data + + else: + return data @property def shapes(self): """ :return: dict of label shape and label location shape """ - self._shapes = {'label': (1, self.dims, 1, 1, 1, 1), 'label_location': (1, 7)} + for name in self.names: + if self.n_samples_per_id == 1: + self._shapes.update({name: (1, self.dims_by_task[name], + 1, 1, 1, 1), + name + '_location': (1, 7)}) + else: + self._shapes.update( + {name: (self.n_samples_per_id, + self.dims_by_task[name], + 1, 1, 1, 1), + name + '_location': (self.n_samples_per_id, 7)}) return self._shapes - + @property def tf_dtypes(self): """ Infer input data dtypes in TF """ - self._dtypes = {'label': tf.float32, 'label_location': tf.int32} + for name in self.names: + self._dtypes.update({name: tf.float32, + name + '_location': tf.int32}) return self._dtypes @property @@ -64,4 +370,23 @@ def tf_shapes(self): """ output_shapes = nest.map_structure_up_to( self.tf_dtypes, tf.TensorShape, self.shapes) - return output_shapes \ No newline at end of file + return output_shapes + + + @staticmethod + def apply_niftynet_format_to_data(data): + ''' +<<<<<<< HEAD + Transform the dtaa to be of dimension 5d +======= + Transform the data to be of dimension 5d +>>>>>>> 7a0386e78f01c88b707e08f759f910abba9b71b1 + :param data: data to expand + :return: expanded data + ''' + if len(data.shape) == 1: + data = np.expand_dims(data, 0) + while len(data.shape) < 6: + data = np.expand_dims(data, -1) + return data + diff --git a/niftynet/contrib/csv_reader/default_segmentation_csvsampler.ini b/niftynet/contrib/csv_reader/default_segmentation_csvsampler.ini new file mode 100755 index 00000000..defce3c0 --- /dev/null +++ b/niftynet/contrib/csv_reader/default_segmentation_csvsampler.ini @@ -0,0 +1,84 @@ +############################ input configuration sections +[modality1] +path_to_search = ./example_volumes/csv_data +filename_contains = MahalT1Mask_ +filename_removefromid = MahalT1Mask_ +filename_not_contains = +spatial_window_size = (35, 35, 35) +interp_order = 3 +pixdim=(2.0, 1.0, 1.0) +axcodes=(A, R, S) + +[label] +path_to_search = ./example_volumes/csv_data/ +filename_contains = BinLabel +filename_not_contains = +filename_removefromid = BinLabel_ +spatial_window_size = (35, 35, 35) +interp_order = 0 +pixdim=(2.0, 1.0, 1.0) +axcodes=(A, R, S) + +[sampler] +csv_data_file = ./example_volumes/csv_data/PlacesLabels.csv + +############################## system configuration sections +[SYSTEM] +cuda_devices = "" +num_threads = 1 +num_gpus = 1 +model_dir = ./models/model_monomodal_toy + +[NETWORK] +name = niftynet.network.toynet.ToyNet +activation_function = prelu +batch_size = 1 +decay = 0.1 +reg_type = L2 + +# volume level preprocessing +volume_padding_size = 1 +# histogram normalisation +histogram_ref_file = ./example_volumes/monomodal_parcellation/standardisation_models.txt +norm_type = percentile +cutoff = (0.01, 0.99) +normalisation = False +whitening = False +normalise_foreground_only=False +foreground_type = otsu_plus +multimod_foreground_type = and +window_sampling=patch +queue_length = 2 + + +[TRAINING] +sample_per_volume = 1 +rotation_angle = (-10.0, 10.0) +scaling_percentage = (-10.0, 10.0) +random_flipping_axes= 1 +lr = 0.01 +loss_type = Dice +starting_iter = 0 +save_every_n = 100 +max_iter = 10 +max_checkpoints = 20 + + +[INFERENCE] +border = (0, 0, 1) +#inference_iter = 10 +save_seg_dir = ./output/toy +output_interp_order = 0 +spatial_window_size = (0, 0, 3) + +[EVALUATION] +evaluations=Dice + +############################ custom configuration sections +[SEGMENTATION] +image = modality1 +label = label +sampler = sampler +output_prob = False +num_classes = 160 +label_normalisation = True diff --git a/niftynet/contrib/csv_reader/default_segmentation_multitask.ini b/niftynet/contrib/csv_reader/default_segmentation_multitask.ini new file mode 100755 index 00000000..6f5c8bd0 --- /dev/null +++ b/niftynet/contrib/csv_reader/default_segmentation_multitask.ini @@ -0,0 +1,84 @@ +############################ input configuration sections +[modality1] +csv_file= +path_to_search = ./example_volumes/monomodal_parcellation +filename_contains = T1 +filename_removefromid = _T1 +filename_not_contains = +spatial_window_size = (20, 42, 42) +interp_order = 3 +pixdim=(1.0, 1.0, 1.0) +axcodes=(A, R, S) + +[value] +csv_data_file = ./example_volumes/monomodal_parcellation/class_pat.csv + +[label] +path_to_search = ./example_volumes/monomodal_parcellation +filename_contains = Label +filename_not_contains = +filename_removefromid = _Label +spatial_window_size = (20, 42, 42) +interp_order = 0 +pixdim=(1.0, 1.0, 1.0) +axcodes=(A, R, S) + +############################## system configuration sections +[SYSTEM] +cuda_devices = "" +num_threads = 2 +num_gpus = 1 +model_dir = ./models/model_monomodal_multitask + +[NETWORK] +name = toynet +activation_function = prelu +batch_size = 2 +decay = 0.1 +reg_type = L2 + +# volume level preprocessing +volume_padding_size = 21 +# histogram normalisation +histogram_ref_file = ./example_volumes/monomodal_parcellation/standardisation_models.txt +norm_type = percentile +cutoff = (0.01, 0.99) +normalisation = False +whitening = False +normalise_foreground_only=True +foreground_type = otsu_plus +multimod_foreground_type = and +window_sampling=uniform +queue_length = 10 + + +[TRAINING] +sample_per_volume = 12 +rotation_angle = (-10.0, 10.0) +scaling_percentage = (-10.0, 10.0) +random_flipping_axes= 1 +lr = 0.01 +loss_type = Dice +starting_iter = 0 +save_every_n = 100 +max_iter = 10 +max_checkpoints = 20 + +[INFERENCE] +border = (0, 0, 1) +#inference_iter = 10 +save_seg_dir = ./output/toy +output_interp_order = 0 +spatial_window_size = (20, 42, 42) + +[EVALUATION] +evaluations=Dice + +############################ custom configuration sections +[SEGMENTATION] +image = modality1 +label = label +value = value +output_prob = False +num_classes = 160 +label_normalisation = True diff --git a/niftynet/contrib/csv_reader/highres3dnet_features.py b/niftynet/contrib/csv_reader/highres3dnet_features.py new file mode 100755 index 00000000..0dab2991 --- /dev/null +++ b/niftynet/contrib/csv_reader/highres3dnet_features.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function + +from six.moves import range + +from niftynet.layer import layer_util +from niftynet.layer.activation import ActiLayer +from niftynet.layer.base_layer import TrainableLayer +from niftynet.layer.bn import BNLayer +from niftynet.layer.convolution import ConvLayer, ConvolutionalLayer +from niftynet.layer.dilatedcontext import DilatedTensor +from niftynet.layer.elementwise import ElementwiseLayer +from niftynet.network.base_net import BaseNet +import tensorflow as tf + + +class HighRes3DNetFeatures(BaseNet): + """ + implementation of HighRes3DNet: + Li et al., "On the compactness, efficiency, and representation of 3D + convolutional networks: Brain parcellation as a pretext task", IPMI '17 + """ + + def __init__(self, + w_initializer=None, + w_regularizer=None, + b_initializer=None, + b_regularizer=None, + acti_func='prelu', + name='HighRes3DNet'): + + super(HighRes3DNetFeatures, self).__init__( + w_initializer=w_initializer, + w_regularizer=w_regularizer, + b_initializer=b_initializer, + b_regularizer=b_regularizer, + acti_func=acti_func, + name=name) + + self.layers = [ + {'name': 'conv_0', 'n_features': 16, 'kernel_size': 3}, + {'name': 'res_1', 'n_features': 16, 'kernels': (3, 3), 'repeat': 3}, + {'name': 'res_2', 'n_features': 32, 'kernels': (3, 3), 'repeat': 3}, + {'name': 'res_3', 'n_features': 64, 'kernels': (3, 3), 'repeat': 3}, + {'name': 'conv_1', 'n_features': 80, 'kernel_size': 1}] + + def layer_op(self, images, is_training, layer_id=-1): + assert layer_util.check_spatial_dims( + images, lambda x: x % 8 == 0) + # go through self.layers, create an instance of each layer + # and plugin data + layer_instances = [] + + # first convolution layer + params = self.layers[0] + first_conv_layer = ConvolutionalLayer( + n_output_chns=params['n_features'], + kernel_size=params['kernel_size'], + acti_func=self.acti_func, + w_initializer=self.initializers['w'], + w_regularizer=self.regularizers['w'], + name=params['name']) + flow = first_conv_layer(images, is_training) + layer_instances.append((first_conv_layer, flow)) + + # resblocks, all kernels dilated by 1 (normal convolution) + params = self.layers[1] + with DilatedTensor(flow, dilation_factor=1) as dilated: + for j in range(params['repeat']): + res_block = HighResBlock( + params['n_features'], + params['kernels'], + acti_func=self.acti_func, + w_initializer=self.initializers['w'], + w_regularizer=self.regularizers['w'], + name='%s_%d' % (params['name'], j)) + dilated.tensor = res_block(dilated.tensor, is_training) + layer_instances.append((res_block, dilated.tensor)) + flow = dilated.tensor + # tf.add_to_collection('checkpoints', flow) + + # resblocks, all kernels dilated by 2 + params = self.layers[2] + with DilatedTensor(flow, dilation_factor=2) as dilated: + for j in range(params['repeat']): + res_block = HighResBlock( + params['n_features'], + params['kernels'], + acti_func=self.acti_func, + w_initializer=self.initializers['w'], + w_regularizer=self.regularizers['w'], + name='%s_%d' % (params['name'], j)) + dilated.tensor = res_block(dilated.tensor, is_training) + layer_instances.append((res_block, dilated.tensor)) + flow = dilated.tensor + # tf.add_to_collection('checkpoints', flow) + + # resblocks, all kernels dilated by 4 + params = self.layers[3] + with DilatedTensor(flow, dilation_factor=4) as dilated: + for j in range(params['repeat']): + res_block = HighResBlock( + params['n_features'], + params['kernels'], + acti_func=self.acti_func, + w_initializer=self.initializers['w'], + w_regularizer=self.regularizers['w'], + name='%s_%d' % (params['name'], j)) + dilated.tensor = res_block(dilated.tensor, is_training) + layer_instances.append((res_block, dilated.tensor)) + flow = dilated.tensor + # tf.add_to_collection('checkpoints', flow) + + # 1x1x1 convolution layer + params = self.layers[4] + fc_layer = ConvolutionalLayer( + n_output_chns=params['n_features'], + kernel_size=params['kernel_size'], + acti_func=self.acti_func, + w_initializer=self.initializers['w'], + w_regularizer=self.regularizers['w'], + name=params['name']) + flow = fc_layer(flow, is_training) + layer_instances.append((fc_layer, flow)) + + # set training properties + if is_training: + self._print(layer_instances) + return layer_instances[-1][1] + return layer_instances[layer_id][1] + + def _print(self, list_of_layers): + for (op, _) in list_of_layers: + print(op) + + +class HighResBlock(TrainableLayer): + """ + This class define a high-resolution block with residual connections + kernels + + - specify kernel sizes of each convolutional layer + - e.g.: kernels=(5, 5, 5) indicate three conv layers of kernel_size 5 + + with_res + + - whether to add residual connections to bypass the conv layers + """ + + def __init__(self, + n_output_chns, + kernels=(3, 3), + acti_func='relu', + w_initializer=None, + w_regularizer=None, + with_res=True, + name='HighResBlock'): + + super(HighResBlock, self).__init__(name=name) + + self.n_output_chns = n_output_chns + if hasattr(kernels, "__iter__"): # a list of layer kernel_sizes + self.kernels = kernels + else: # is a single number (indicating single layer) + self.kernels = [kernels] + self.acti_func = acti_func + self.with_res = with_res + + self.initializers = {'w': w_initializer} + self.regularizers = {'w': w_regularizer} + + def layer_op(self, input_tensor, is_training): + output_tensor = input_tensor + for (i, k) in enumerate(self.kernels): + # create parameterised layers + bn_op = BNLayer(regularizer=self.regularizers['w'], + name='bn_{}'.format(i)) + acti_op = ActiLayer(func=self.acti_func, + regularizer=self.regularizers['w'], + name='acti_{}'.format(i)) + conv_op = ConvLayer(n_output_chns=self.n_output_chns, + kernel_size=k, + stride=1, + w_initializer=self.initializers['w'], + w_regularizer=self.regularizers['w'], + name='conv_{}'.format(i)) + # connect layers + output_tensor = bn_op(output_tensor, is_training) + output_tensor = acti_op(output_tensor) + output_tensor = conv_op(output_tensor) + # make residual connections + if self.with_res: + output_tensor = ElementwiseLayer('SUM')(output_tensor, input_tensor) + return output_tensor diff --git a/niftynet/contrib/csv_reader/multitask_classifseg_application.py b/niftynet/contrib/csv_reader/multitask_classifseg_application.py new file mode 100755 index 00000000..867e8564 --- /dev/null +++ b/niftynet/contrib/csv_reader/multitask_classifseg_application.py @@ -0,0 +1,550 @@ +# -*- coding: utf-8 -*- +""" +This module defines an image-level classification application +that maps from images to scalar, multi-class labels. + +This class is instantiated and initalized by the application_driver. +""" + +import tensorflow as tf + +from niftynet.application.base_application import BaseApplication +from niftynet.engine.application_factory import \ + ApplicationNetFactory, InitializerFactory, OptimiserFactory +from niftynet.engine.application_variables import \ + CONSOLE, NETWORK_OUTPUT, TF_SUMMARIES +from niftynet.contrib.csv_reader.sampler_resize_v2_csv import ResizeSamplerCSV \ + as ResizeSampler +from niftynet.contrib.csv_reader.sampler_uniform_v2_csv import \ + UniformSamplerCSV as UniformSampler +from niftynet.contrib.csv_reader.sampler_weighted_v2_csv import \ + WeightedSamplerCSV as WeightedSampler +from niftynet.contrib.csv_reader.sampler_balanced_v2_csv import \ + BalancedSamplerCSV as BalancedSampler +from niftynet.contrib.csv_reader.sampler_grid_v2_csv import GridSamplerCSV as\ + GridSampler +from niftynet.engine.windows_aggregator_grid import GridSamplesAggregator +from niftynet.engine.windows_aggregator_resize import ResizeSamplesAggregator +from niftynet.io.image_reader import ImageReader +from niftynet.contrib.csv_reader.csv_reader import CSVReader +from niftynet.layer.discrete_label_normalisation import \ + DiscreteLabelNormalisationLayer +from niftynet.layer.histogram_normalisation import \ + HistogramNormalisationLayer +from niftynet.layer.binary_masking import BinaryMaskingLayer +from niftynet.layer.post_processing import PostProcessingLayer +from niftynet.layer.loss_classification import LossFunction as \ + LossFunctionClassification +from niftynet.layer.loss_segmentation import \ + LossFunction as LossFunctionSegmentation +from niftynet.layer.mean_variance_normalisation import \ + MeanVarNormalisationLayer +from niftynet.layer.rand_flip import RandomFlipLayer +from niftynet.layer.rand_rotation import RandomRotationLayer +from niftynet.layer.rand_spatial_scaling import RandomSpatialScalingLayer +from niftynet.evaluation.classification_evaluator import ClassificationEvaluator + +SUPPORTED_INPUT = set(['image', 'value', 'label', 'sampler', 'inferred']) + + +class MultiClassifSegApplication(BaseApplication): + """This class defines an application for image-level classification + problems mapping from images to scalar labels. + + This is the application class to be instantiated by the driver + and referred to in configuration files. + + Although structurally similar to segmentation, this application + supports different samplers/aggregators (because patch-based + processing is not appropriate), and monitoring metrics.""" + + REQUIRED_CONFIG_SECTION = "SEGMENTATION" + + def __init__(self, net_param, action_param, action): + super(MultiClassifSegApplication, self).__init__() + tf.logging.info('starting classification application') + self.action = action + + self.net_param = net_param + self.eval_param = None + self.evaluator = None + self.action_param = action_param + self.net_multi = None + self.data_param = None + self.segmentation_param = None + self.csv_readers = None + self.SUPPORTED_SAMPLING = { + 'uniform': (self.initialise_uniform_sampler, + self.initialise_grid_sampler, + self.initialise_grid_aggregator), + 'weighted': (self.initialise_weighted_sampler, + self.initialise_grid_sampler, + self.initialise_grid_aggregator), + 'resize': (self.initialise_resize_sampler, + self.initialise_resize_sampler, + self.initialise_resize_aggregator), + 'balanced': (self.initialise_balanced_sampler, + self.initialise_grid_sampler, + self.initialise_grid_aggregator), + } + + def initialise_dataset_loader( + self, data_param=None, task_param=None, data_partitioner=None): + ''' + Initialise the data loader both csv readers and image readers and + specify preprocessing layers + :param data_param: + :param task_param: + :param data_partitioner: + :return: + ''' + + self.data_param = data_param + self.segmentation_param = task_param + + if self.is_training: + image_reader_names = ('image', 'sampler', 'label') + csv_reader_names = ('value',) + elif self.is_inference: + image_reader_names = ('image',) + csv_reader_names = () + elif self.is_evaluation: + image_reader_names = ('image', 'inferred', 'label') + csv_reader_names = ('value',) + else: + tf.logging.fatal( + 'Action `%s` not supported. Expected one of %s', + self.action, self.SUPPORTED_PHASES) + raise ValueError + try: + reader_phase = self.action_param.dataset_to_infer + except AttributeError: + reader_phase = None + file_lists = data_partitioner.get_file_lists_by( + phase=reader_phase, action=self.action) + self.readers = [ + ImageReader(image_reader_names).initialise( + data_param, task_param, file_list) for file_list in file_lists] + if self.is_inference: + self.action_param.sample_per_volume = 1 + if csv_reader_names is not None and list(csv_reader_names): + self.csv_readers = [ + CSVReader(csv_reader_names).initialise( + data_param, task_param, file_list, + sample_per_volume=self.action_param.sample_per_volume) + for file_list in file_lists] + else: + self.csv_readers = [None for file_list in file_lists] + + foreground_masking_layer = BinaryMaskingLayer( + type_str=self.net_param.foreground_type, + multimod_fusion=self.net_param.multimod_foreground_type, + threshold=0.0) \ + if self.net_param.normalise_foreground_only else None + + mean_var_normaliser = MeanVarNormalisationLayer( + image_name='image', binary_masking_func=foreground_masking_layer) \ + if self.net_param.whitening else None + histogram_normaliser = HistogramNormalisationLayer( + image_name='image', + modalities=vars(task_param).get('image'), + model_filename=self.net_param.histogram_ref_file, + binary_masking_func=foreground_masking_layer, + norm_type=self.net_param.norm_type, + cutoff=self.net_param.cutoff, + name='hist_norm_layer') \ + if (self.net_param.histogram_ref_file and + self.net_param.normalisation) else None + + label_normaliser = DiscreteLabelNormalisationLayer( + image_name='label', + modalities=vars(task_param).get('label'), + model_filename=self.net_param.histogram_ref_file) \ + if (self.net_param.histogram_ref_file and + task_param.label_normalisation) else None + + normalisation_layers = [] + if histogram_normaliser is not None: + normalisation_layers.append(histogram_normaliser) + if mean_var_normaliser is not None: + normalisation_layers.append(mean_var_normaliser) + if label_normaliser is not None: + normalisation_layers.append(label_normaliser) + + augmentation_layers = [] + if self.is_training: + train_param = self.action_param + if train_param.random_flipping_axes != -1: + augmentation_layers.append(RandomFlipLayer( + flip_axes=train_param.random_flipping_axes)) + if train_param.scaling_percentage: + augmentation_layers.append(RandomSpatialScalingLayer( + min_percentage=train_param.scaling_percentage[0], + max_percentage=train_param.scaling_percentage[1])) + if train_param.rotation_angle or \ + self.action_param.rotation_angle_x or \ + self.action_param.rotation_angle_y or \ + self.action_param.rotation_angle_z: + rotation_layer = RandomRotationLayer() + if train_param.rotation_angle: + rotation_layer.init_uniform_angle( + train_param.rotation_angle) + else: + rotation_layer.init_non_uniform_angle( + self.action_param.rotation_angle_x, + self.action_param.rotation_angle_y, + self.action_param.rotation_angle_z) + augmentation_layers.append(rotation_layer) + + # only add augmentation to first reader (not validation reader) + self.readers[0].add_preprocessing_layers( + normalisation_layers + augmentation_layers) + for reader in self.readers[1:]: + reader.add_preprocessing_layers(normalisation_layers) + + def initialise_uniform_sampler(self): + ''' + Create the uniform sampler using information from readers + :return: + ''' + self.sampler = [[UniformSampler( + reader=reader, + csv_reader=csv_reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + windows_per_image=self.action_param.sample_per_volume, + queue_length=self.net_param.queue_length) for reader, csv_reader in + zip(self.readers, self.csv_readers)]] + + def initialise_weighted_sampler(self): + ''' + Create the weighted sampler using the info from the csv_readers and + image_readers and the configuration parameters + :return: + ''' + self.sampler = [[WeightedSampler( + reader=reader, + csv_reader=csv_reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + windows_per_image=self.action_param.sample_per_volume, + queue_length=self.net_param.queue_length) for reader, csv_reader in + zip(self.readers, self.csv_readers)]] + + def initialise_resize_sampler(self): + ''' + Define the resize sampler using the information from the + configuration parameters, csv_readers and image_readers + :return: + ''' + self.sampler = [[ResizeSampler( + reader=reader, + csv_reader=csv_reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + shuffle=self.is_training, + smaller_final_batch_mode=self.net_param.smaller_final_batch_mode, + queue_length=self.net_param.queue_length) for reader, csv_reader in + zip(self.readers, self.csv_readers)]] + + def initialise_grid_sampler(self): + ''' + Define the grid sampler based on the information from configuration + and the csv_readers and image_readers specifications + :return: + ''' + self.sampler = [[GridSampler( + reader=reader, + csv_reader=csv_reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + spatial_window_size=self.action_param.spatial_window_size, + window_border=self.action_param.border, + smaller_final_batch_mode=self.net_param.smaller_final_batch_mode, + queue_length=self.net_param.queue_length) for reader, csv_reader in + zip(self.readers, self.csv_readers)]] + + def initialise_balanced_sampler(self): + ''' + Define the balanced sampler based on the information from configuration + and the csv_readers and image_readers specifications + :return: + ''' + self.sampler = [[BalancedSampler( + reader=reader, + csv_reader=csv_reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + windows_per_image=self.action_param.sample_per_volume, + queue_length=self.net_param.queue_length) for reader, csv_reader in + zip(self.readers, self.csv_readers)]] + + def initialise_grid_aggregator(self): + ''' + Define the grid aggregator used for decoding using configuration + parameters + :return: + ''' + self.output_decoder = GridSamplesAggregator( + image_reader=self.readers[0], + output_path=self.action_param.save_seg_dir, + window_border=self.action_param.border, + interp_order=self.action_param.output_interp_order, + postfix=self.action_param.output_postfix, + fill_constant=self.action_param.fill_constant) + + def initialise_resize_aggregator(self): + ''' + Define the resize aggregator used for decoding using the + configuration parameters + :return: + ''' + self.output_decoder = ResizeSamplesAggregator( + image_reader=self.readers[0], + output_path=self.action_param.save_seg_dir, + window_border=self.action_param.border, + interp_order=self.action_param.output_interp_order, + postfix=self.action_param.output_postfix) + + def initialise_sampler(self): + ''' + Specifies the sampler used among those previously defined based on + the sampling choice + :return: + ''' + if self.is_training: + self.SUPPORTED_SAMPLING[self.net_param.window_sampling][0]() + elif self.is_inference: + self.SUPPORTED_SAMPLING[self.net_param.window_sampling][1]() + + def initialise_aggregator(self): + ''' + Specifies the aggregator used based on the sampling choice + :return: + ''' + self.SUPPORTED_SAMPLING[self.net_param.window_sampling][2]() + + def initialise_network(self): + ''' + Initialise the network and specifies the ordering of elements + :return: + ''' + w_regularizer = None + b_regularizer = None + reg_type = self.net_param.reg_type.lower() + decay = self.net_param.decay + if reg_type == 'l2' and decay > 0: + from tensorflow.contrib.layers.python.layers import regularizers + w_regularizer = regularizers.l2_regularizer(decay) + b_regularizer = regularizers.l2_regularizer(decay) + elif reg_type == 'l1' and decay > 0: + from tensorflow.contrib.layers.python.layers import regularizers + w_regularizer = regularizers.l1_regularizer(decay) + b_regularizer = regularizers.l1_regularizer(decay) + + self.net = ApplicationNetFactory.create( + 'niftynet.contrib.csv_reader.toynet_features.ToyNetFeat')( + num_classes=self.segmentation_param.num_classes, + w_initializer=InitializerFactory.get_initializer( + name=self.net_param.weight_initializer), + b_initializer=InitializerFactory.get_initializer( + name=self.net_param.bias_initializer), + w_regularizer=w_regularizer, + b_regularizer=b_regularizer, + acti_func=self.net_param.activation_function) + self.net_multi = ApplicationNetFactory.create( + 'niftynet.contrib.csv_reader.class_seg_finnet.ClassSegFinnet')( + num_classes=self.segmentation_param.num_classes, + w_initializer=InitializerFactory.get_initializer( + name=self.net_param.weight_initializer), + b_initializer=InitializerFactory.get_initializer( + name=self.net_param.bias_initializer), + w_regularizer=w_regularizer, + b_regularizer=b_regularizer, + acti_func=self.net_param.activation_function) + + def add_confusion_matrix_summaries_(self, + outputs_collector, + net_out, + data_dict): + """ This method defines several monitoring metrics that + are derived from the confusion matrix """ + labels = tf.reshape(tf.cast(data_dict['label'], tf.int64), [-1]) + prediction = tf.reshape(tf.argmax(net_out, -1), [-1]) + num_classes = 2 + conf_mat = tf.confusion_matrix(labels, prediction, num_classes) + conf_mat = tf.to_float(conf_mat) + if self.segmentation_param.num_classes == 2: + outputs_collector.add_to_collection( + var=conf_mat[1][1], name='true_positives', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + outputs_collector.add_to_collection( + var=conf_mat[1][0], name='false_negatives', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + outputs_collector.add_to_collection( + var=conf_mat[0][1], name='false_positives', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + outputs_collector.add_to_collection( + var=conf_mat[0][0], name='true_negatives', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + else: + outputs_collector.add_to_collection( + var=conf_mat[tf.newaxis, :, :, tf.newaxis], + name='confusion_matrix', + average_over_devices=True, summary_type='image', + collection=TF_SUMMARIES) + + outputs_collector.add_to_collection( + var=tf.trace(conf_mat), name='accuracy', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + + def connect_data_and_network(self, + outputs_collector=None, + gradients_collector=None): + + def switch_sampler(for_training): + with tf.name_scope('train' if for_training else 'validation'): + sampler = self.get_sampler()[0][0 if for_training else -1] + return sampler.pop_batch_op() + + if self.is_training: + if self.action_param.validation_every_n > 0: + data_dict = tf.cond(tf.logical_not(self.is_validation), + lambda: switch_sampler(for_training=True), + lambda: switch_sampler(for_training=False)) + else: + data_dict = switch_sampler(for_training=True) + + image = tf.cast(data_dict['image'], tf.float32) + net_args = {'is_training': self.is_training, + 'keep_prob': self.net_param.keep_prob} + net_out = self.net(image, **net_args) + net_out_seg, net_out_class = self.net_multi(net_out, + self.is_training) + + with tf.name_scope('Optimiser'): + optimiser_class = OptimiserFactory.create( + name=self.action_param.optimiser) + self.optimiser = optimiser_class.get_instance( + learning_rate=self.action_param.lr) + loss_func_class = LossFunctionClassification( + n_class=2, + loss_type='CrossEntropy') + loss_func_seg = LossFunctionSegmentation( + n_class=self.segmentation_param.num_classes, + loss_type=self.action_param.loss_type) + data_loss_seg = loss_func_seg( + prediction=net_out_seg, + ground_truth=data_dict.get('label', None)) + data_loss_class = loss_func_class( + prediction=net_out_class, + ground_truth=data_dict.get('value', None)) + reg_losses = tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES) + if self.net_param.decay > 0.0 and reg_losses: + reg_loss = tf.reduce_mean( + [tf.reduce_mean(reg_loss) for reg_loss in reg_losses]) + loss = data_loss_seg + data_loss_class + reg_loss + else: + loss = data_loss_seg + data_loss_class + self.total_loss = loss + self.total_loss = tf.Print(tf.cast(self.total_loss, tf.float32), + [loss, tf.shape(net_out_seg), + tf.shape(net_out_class)], + message='test') + grads = self.optimiser.compute_gradients( + loss, colocate_gradients_with_ops=True) + # collecting gradients variables + gradients_collector.add_to_collection([grads]) + # collecting output variables + outputs_collector.add_to_collection( + var=data_loss_class, name='data_loss', + average_over_devices=False, collection=CONSOLE) + outputs_collector.add_to_collection( + var=data_loss_seg, name='data_loss', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + # self.add_confusion_matrix_summaries_(outputs_collector, + # net_out_class, + # data_dict) + else: + # converting logits into final output for + # classification probabilities or argmax classification labels + data_dict = switch_sampler(for_training=False) + image = tf.cast(data_dict['image'], tf.float32) + net_args = {'is_training': self.is_training, + 'keep_prob': self.net_param.keep_prob} + net_out = self.net(image, **net_args) + net_out_seg, net_out_class = self.net_multi(net_out, + self.is_training) + tf.logging.info( + 'net_out.shape may need to be resized: %s', net_out.shape) + output_prob = self.segmentation_param.output_prob + num_classes = self.segmentation_param.num_classes + if output_prob and num_classes > 1: + post_process_layer_class = PostProcessingLayer( + 'SOFTMAX', num_classes=num_classes) + post_process_layer_seg = PostProcessingLayer('SOFTMAX', + num_classes=2) + elif not output_prob and num_classes > 1: + post_process_layer_class = PostProcessingLayer( + 'ARGMAX', num_classes=num_classes) + post_process_layer_seg = PostProcessingLayer('ARGMAX', + num_classes=2) + else: + post_process_layer_class = PostProcessingLayer( + 'IDENTITY', num_classes=num_classes) + post_process_layer_seg = PostProcessingLayer('IDENTITY', + num_classes=2) + + net_out_class = post_process_layer_class(net_out_class) + net_out_seg = post_process_layer_seg(net_out_seg) + + outputs_collector.add_to_collection( + var=net_out_seg, name='seg', + average_over_devices=False, collection=NETWORK_OUTPUT) + outputs_collector.add_to_collection(var=net_out_class, + name='value', + average_over_devices=False, + collection=NETWORK_OUTPUT) + outputs_collector.add_to_collection( + var=data_dict['image_location'], name='location', + average_over_devices=False, collection=NETWORK_OUTPUT) + self.initialise_aggregator() + + def interpret_output(self, batch_output): + ''' + Specifies how the output should be decoded + :param batch_output: + :return: + ''' + if not self.is_training: + return self.output_decoder.decode_batch( + {'window_seg': batch_output['seg'], + 'csv_class': batch_output['value']}, + batch_output['location']) + return True + + def initialise_evaluator(self, eval_param): + ''' + Define the evaluator + :param eval_param: + :return: + ''' + self.eval_param = eval_param + self.evaluator = ClassificationEvaluator(self.readers[0], + self.segmentation_param, + eval_param) + + def add_inferred_output(self, data_param, task_param): + ''' + Define how to treat added inferred output + :param data_param: + :param task_param: + :return: + ''' + return self.add_inferred_output_like(data_param, task_param, 'label') diff --git a/niftynet/contrib/csv_reader/sampler_balanced_v2_csv.py b/niftynet/contrib/csv_reader/sampler_balanced_v2_csv.py new file mode 100755 index 00000000..d0bf87e6 --- /dev/null +++ b/niftynet/contrib/csv_reader/sampler_balanced_v2_csv.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +""" +Generate image windows from a balanced sampling map as if every label +had the same probability of occurrence. + +Consider a mask with three classes I, J, K with prevalence 0.1, 0.1, and +0.8, respectively. If 100 samples are drawn from the balanced sampler, the +classes should be approximately 33 I, 33 J, and 33 K. + +This can also be considered a "balanced random cropping" layer of the +input image. +""" +from __future__ import absolute_import, division, print_function + +import numpy as np +import tensorflow as tf + +from niftynet.engine.sampler_uniform_v2 import UniformSampler +from niftynet.contrib.csv_reader.sampler_uniform_v2_csv import UniformSamplerCSV +from niftynet.engine.image_window import N_SPATIAL +from niftynet.engine.sampler_weighted_v2 import crop_sampling_map + + +class BalancedSamplerCSV(UniformSampler): + """ + This class generators samples from a user provided frequency map for each + input volume. The sampling likelihood of each voxel is proportional its + intra class frequency. That is, if a given voxel is of class `A` and there + are 20 voxels with class `A`, the probability of selecting this voxel is + 5%. If there are 10 classes, the probability becomes 10% * 5% = 0.5%. + + In general, the likelihood of sampling a voxel is given by: + p(v) = (1)/(# of unique labels * # of voxels with same class as v) + + This is done for balanced sampling. In the case of unbalanced labels, + this sampler should produce a roughly equal probability of sampling each + class. + + This layer can be considered as a "balanced random cropping" layer of the + input image. + """ + + def __init__(self, + reader, + csv_reader, + window_sizes, + batch_size=1, + windows_per_image=1, + queue_length=10, + name='balanced_sampler'): + UniformSamplerCSV.__init__(self, + reader=reader, + csv_reader=csv_reader, + window_sizes=window_sizes, + batch_size=batch_size, + windows_per_image=windows_per_image, + queue_length=queue_length, + name=name) + tf.logging.info('Initialised balanced sampler window instance') + self.window_centers_sampler = balanced_spatial_coordinates + + +def balanced_spatial_coordinates( + n_samples, img_spatial_size, win_spatial_size, sampler_map): + """ + Perform balanced sampling. + + Each label in the input tensor has an equal probability of + being sampled. + + :param n_samples: number of random coordinates to generate + :param img_spatial_size: input image size + :param win_spatial_size: input window size + :param sampler_map: sampling prior map, it's spatial shape should be + consistent with `img_spatial_size` + :return: (n_samples, N_SPATIAL) coordinates representing sampling + window centres relative to img_spatial_size + """ + assert sampler_map is not None, \ + 'sampling prior map is not specified, ' \ + 'please check `sampler=` option in the config.' + assert np.all(img_spatial_size[:N_SPATIAL] == + sampler_map.shape[:N_SPATIAL]), \ + 'image and sampling map shapes do not match' + + # Find the number of unique labels + win_spatial_size = np.asarray(win_spatial_size, dtype=np.int32) + cropped_map = crop_sampling_map(sampler_map, win_spatial_size) + + flatten_map = cropped_map.flatten() + unique_labels = np.unique(flatten_map) + if len(unique_labels) > 500: + tf.logging.warning( + "unusual discrete volume: number of unique " + "labels: %s", len(unique_labels)) + + # system parameter? + class_probs = [1.0 / len(unique_labels)] * len(unique_labels) + label_counts = np.random.multinomial(n_samples, class_probs) + # Look inside each label and sample `count`. Add the middle_coord of + # each sample to `middle_coords` + middle_coords = np.zeros((n_samples, N_SPATIAL), dtype=np.int32) + sample_count = 0 + for label, count in zip(unique_labels, label_counts): + # Get indices where(cropped_map == label) + valid_locations = np.where(flatten_map == label)[0] + + # Sample `count` from those indices. Need replace=True. Consider the + # case where all pixels are background except for one pixel which is + # foreground. We ask for 10 samples. We should get 5 samples from + # background and the foreground pixel sampled 5 times (give or take + # random fluctuation). + try: + samples = np.random.choice( + valid_locations, + size=count, + replace=True) + except ValueError: + tf.logging.fatal("unable to choose sampling window based on " + "the current frequency map.") + raise + + assert count == samples.size, "Unable to sample from the image" + + # Place into `middle_coords` + for sample in samples: + middle_coords[sample_count, :N_SPATIAL] = \ + np.unravel_index(sample, cropped_map.shape)[:N_SPATIAL] + sample_count += 1 + + # re-shift coords due to the crop + half_win = np.floor(win_spatial_size / 2).astype(np.int32) + middle_coords[:, :N_SPATIAL] = \ + middle_coords[:, :N_SPATIAL] + half_win[:N_SPATIAL] + return middle_coords diff --git a/niftynet/contrib/csv_reader/sampler_csv_rows.py b/niftynet/contrib/csv_reader/sampler_csv_rows.py index dc74c977..859fc070 100644 --- a/niftynet/contrib/csv_reader/sampler_csv_rows.py +++ b/niftynet/contrib/csv_reader/sampler_csv_rows.py @@ -1,3 +1,4 @@ +import numpy as np from niftynet.engine.image_window_dataset import ImageWindowDataset from niftynet.engine.image_window import N_SPATIAL, LOCATION_FORMAT @@ -15,10 +16,12 @@ def __init__(self, windows_per_image=1, shuffle=True, queue_length=10, + num_threads=4, epoch=-1, smaller_final_batch_mode='pad', name='random_vector_sampler'): self.csv_reader = csv_reader + print("assigned csv_reader") ImageWindowDataset.__init__( self, reader=reader, @@ -30,6 +33,8 @@ def __init__(self, epoch=epoch, smaller_final_batch_mode=smaller_final_batch_mode, name=name) + print("initialised IWD") + self.set_num_threads(num_threads) def layer_op(self, idx=None): """ @@ -73,20 +78,44 @@ def layer_op(self, idx=None): # dataset: from a window generator # assumes self.window.n_samples == 1 # the generator should yield one window at each iteration - assert self.window.n_samples == 1, \ - 'image_window_dataset.layer_op() requires: ' \ - 'windows_per_image should be 1.' - image_id, image_data, _ = self.reader(idx=idx) - for mod in list(image_data): - spatial_shape = image_data[mod].shape[:N_SPATIAL] - coords = self.dummy_coordinates(image_id, spatial_shape, 1) - image_data[LOCATION_FORMAT.format(mod)] = coords - image_data[mod] = image_data[mod][np.newaxis, ...] - if self.csv_reader is not None: - _, label_data, _ = self.csv_reader(idx=image_id) - image_data['label'] = label_data['label'] - image_data['label_location'] = image_data['image_location'] - return image_data + + if self.window.n_samples == 1: + assert self.window.n_samples == 1, \ + 'image_window_dataset.layer_op() requires: ' \ + 'windows_per_image should be 1.' + + image_id, image_data, _ = self.reader(idx=idx) + print(image_id, idx) + for mod in list(image_data): + spatial_shape = image_data[mod].shape[:N_SPATIAL] + coords = self.dummy_coordinates(image_id, spatial_shape, 1) + image_data[LOCATION_FORMAT.format(mod)] = coords + image_data[mod] = image_data[mod][np.newaxis, ...] + if self.csv_reader is not None: + _, label_dict, _ = self.csv_reader(subject_id=image_id) + print(label_dict, image_id, idx) + image_data.update(label_dict) + for name in self.csv_reader.names: + image_data[name + '_location'] = \ + image_data['image_location'] + return image_data + else: + print("Warning, it may not be ready yet") + image_id, image_data, _ = self.reader(idx=idx) + print(image_id, idx) + for mod in list(image_data): + spatial_shape = image_data[mod].shape[:N_SPATIAL] + coords = self.dummy_coordinates(image_id, spatial_shape, 1) + image_data[LOCATION_FORMAT.format(mod)] = coords + image_data[mod] = image_data[mod][np.newaxis, ...] + if self.csv_reader is not None: + _, label_dict, _ = self.csv_reader(subject_id=image_id) + print(label_dict, image_id, idx) + image_data.update(label_dict) + for name in self.csv_reader.names: + image_data[name + '_location'] = image_data[ + 'image_location'] + return image_data @property def tf_shapes(self): diff --git a/niftynet/contrib/csv_reader/sampler_csvpatch.py b/niftynet/contrib/csv_reader/sampler_csvpatch.py new file mode 100755 index 00000000..c76c2862 --- /dev/null +++ b/niftynet/contrib/csv_reader/sampler_csvpatch.py @@ -0,0 +1,669 @@ +# -*- coding: utf-8 -*- +""" +Generating uniformly distributed image window from input image +This can also be considered as a "random cropping" layer of the +input image. +""" +from __future__ import absolute_import, division, print_function + +import numpy as np +import tensorflow as tf + + +from niftynet.contrib.csv_reader.sampler_csv_rows import ImageWindowDatasetCSV +# from niftynet.engine.image_window import LOCATION_FORMAT +# from niftynet.engine.image_window_dataset import ImageWindowDataset +from niftynet.engine.image_window import N_SPATIAL, LOCATION_FORMAT +from niftynet.io.misc_io import do_reorientation_idx, do_resampling_idx + +SUPPORTED_MODES_CORRECTION=['pad', 'remove', 'random'] + + +class CSVPatchSampler(ImageWindowDatasetCSV): + """ + This class generates samples using the coordinates of the centre as + extracted from a csv file + + This layer can be considered as a "guided cropping" layer of the + input image based on preselected input. + """ + + def __init__(self, + reader, csv_reader, + window_sizes, + batch_size=1, + windows_per_image=1, + queue_length=10, + mode_correction='pad', + name='csv_patchsampler_v2'): + ImageWindowDatasetCSV.__init__( + self, + reader=reader, + csv_reader=csv_reader, + window_sizes=window_sizes, + batch_size=batch_size, + windows_per_image=windows_per_image, + queue_length=queue_length, + shuffle=True, + epoch=-1, + smaller_final_batch_mode='drop', + name=name) + + tf.logging.info("initialised csv patch sampler %s ", self.window.shapes) + self.mode_correction = mode_correction + self.window_centers_sampler = rand_spatial_coordinates + self.available_subjects = reader._file_list.subject_id + + # pylint: disable=too-many-locals + def layer_op(self, idx=None): + """ + This function generates sampling windows to the input buffer + image data are from ``self.reader()`` + + It first find the appropriate indices from the data frame in which + the centre samples are stored and extract information about the + windows to draw on the data. + The final dictionary is filled according to the appropriate samples. + Different modes on how to take care of unsuitable centres (too big + patch size for instance are implemented) + + :return: output data dictionary + ``{image_modality: data_array, image_location: n_samples * 7}`` + """ + + if self.window.n_samples > 1: + raise ValueError("\nThe number of windows per image has to be " + "1 with a csv_reader") + # flag_multi_row = False + print("Trying to run csv patch sampler ") + if 'sampler' not in self.csv_reader.names: + tf.logging.warning('Uniform sampling because no csv sampler ' + 'provided') + + # if 'multi' in self.csv_reader.type_by_task.values(): + # flag_multi_row = True + try: + _, _, subject_id = self.csv_reader(idx) + except ValueError: + tf.logging.fatal("No available subject") + raise + + assert len(self.available_subjects) >0, "No available subject from " \ + "check" + + # assert len(self.available_subjects) > 0, "No available subject from " \ + # "check" + + + print("subject id is ", subject_id) + if len(self.available_subjects) > 0: + idx_subject_id = np.where( + self.available_subjects == subject_id)[0][0] + image_id, data, _ = self.reader(idx=idx_subject_id, shuffle=True) + subj_indices, csv_data, _ = self.csv_reader(subject_id=subject_id) + image_shapes = dict( + (name, data[name].shape) for name in self.window.names) + static_window_shapes = self.window.match_image_shapes(image_shapes) + + # Perform the checks relative to the sample choices and create the + # corresponding (if needed) padding information to be applied + num_idx, num_discard = self.check_csv_sampler_valid(subject_id, + image_shapes, + static_window_shapes + ) + + print(num_idx, num_discard, "available, discarded") + + + if 'sampler' not in self.csv_reader.names: + tf.logging.warning('Uniform sampling because no csv sampler ' + 'provided') + + + + # In the remove configuration, none of the unsuitable sample is used. + # Thus if the chosen subject does not have any suitable sample, + # another one must be drawn. An error is raised if none of the + # subjects has suitable samples + if self.mode_correction == 'remove': + if num_idx == num_discard: + if subject_id in set(self.available_subjects): + self.available_subjects.drop([idx_subject_id], inplace=True) + + print('self.available_subjects', self.available_subjects, idx_subject_id) + + subject_id = None + else: + tf.logging.warning('%s may have already been dropped from list of available subjects' %subject_id) + subject_id = None + while subject_id is None and len(self.available_subjects) > 0: + _, _, subject_id = self.csv_reader(idx) + print('list of available subjects is ', + self.available_subjects, idx_subject_id) + # print("subject id is ", subject_id) + # Find the index corresponding to the drawn subject id in + # the reader + if subject_id in set(self.available_subjects): + idx_subject_id = np.where( + self.available_subjects == subject_id)[0][0] + image_id, data, _ = self.reader(idx=idx_subject_id, + shuffle=True) + subj_indices, csv_data, _ = self.csv_reader( + subject_id=subject_id) + if 'sampler' not in self.csv_reader.names: + tf.logging.warning( + 'Uniform sampling because no csv sampler provided') + image_shapes = dict( + (name, data[name].shape) for name in self.window.names) + static_window_shapes = self.window.match_image_shapes( + image_shapes) + num_idx, num_discard = self.check_csv_sampler_valid( + subject_id, + image_shapes, + static_window_shapes) + if num_idx == num_discard: + if subject_id in set(self.available_subjects): + self.available_subjects.drop(idx_subject_id) + subject_id = None + else: + subject_id = None + else: + subject_id = None + if subject_id is None: + tf.logging.fatal("None of the subjects has any suitable " + "samples. Consider using a different " + "alternative to unsuitable samples or " + "reducing your patch size") + raise ValueError + + + # find csv coordinates and return coordinates (not corrected) and + # corresponding csv indices + try: + print('subject id to try is %s' % subject_id) + coordinates, idx = self.csvcenter_spatial_coordinates( + subject_id=subject_id, + data=data, + img_sizes=image_shapes, + win_sizes=static_window_shapes, + n_samples=self.window.n_samples, + mode_correction=self.mode_correction + ) + reject = False + if self.mode_correction == 'remove': + reject = True + # print(idx, "index selected") + # initialise output dict, placeholders as dictionary keys + # this dictionary will be used in + # enqueue operation in the form of: `feed_dict=output_dict` + output_dict = {} + potential_pad = self.csv_reader.pad_by_task['sampler'][idx][0] + potential_pad_corr_end = -1.0 * np.asarray(potential_pad[N_SPATIAL:]) + potential_pad_corr = np.concatenate((potential_pad[:N_SPATIAL], + potential_pad_corr_end), 0) + + # fill output dict with data + for name in list(data): + coordinates_key = LOCATION_FORMAT.format(name) + image_data_key = name + + # fill the coordinates + location_array = coordinates[name] + output_dict[coordinates_key] = location_array + + # fill output window array + image_array = [] + for window_id in range(self.window.n_samples): + x_start, y_start, z_start, x_end, y_end, z_end = \ + location_array[window_id, 1:].astype(np.int32) + \ + potential_pad_corr.astype(np.int32) + # print(location_array[window_id, 1:]+potential_pad_corr) + try: + image_window = data[name][ + x_start:x_end, y_start:y_end, + z_start:z_end, ...] + if np.sum(potential_pad) > 0: + new_pad = np.reshape(potential_pad, [2, N_SPATIAL]).T + add_pad = np.tile([0, 0], [len(np.shape( + image_window))-N_SPATIAL, 1]) + new_pad = np.concatenate((new_pad, add_pad), + 0).astype(np.int32) + # print(new_pad, "is padding") + new_img = np.pad(image_window, pad_width=new_pad, + mode='constant', + constant_values=0) + image_array.append(new_img[np.newaxis, ...]) + else: + image_array.append(image_window[np.newaxis, ...]) + except ValueError: + tf.logging.fatal( + "dimensionality miss match in input volumes, " + "please specify spatial_window_size with a " + "3D tuple and make sure each element is " + "smaller than the image length in each dim. " + "Current coords %s", location_array[window_id]) + raise + if len(image_array) > 1: + output_dict[image_data_key] = \ + np.concatenate(image_array, axis=0) + else: + output_dict[image_data_key] = image_array[0] + # fill output dict with csv_data + # print("filling output dict") + if self.csv_reader is not None: + idx_dict = {} + list_keys = self.csv_reader.df_by_task.keys() + for k in list_keys: + if self.csv_reader.type_by_task[k] == 'multi': + idx_dict[k] = idx + else: + for n in range(0, self.window.n_samples): + idx_dict[k] = 0 + _, csv_data_dict, _ = self.csv_reader(idx=idx_dict, + subject_id=subject_id, + reject=reject) + for name in csv_data_dict.keys(): + csv_data_array = [] + for n in range(0, self.window.n_samples): + csv_data_array.append(csv_data_dict[name]) + if len(csv_data_array) == 1: + output_dict[name] = np.asarray(csv_data_array[0], + dtype=np.float32) + else: + output_dict[name] = np.concatenate( + csv_data_array, 0).astype(dtype=np.float32) + + for name in csv_data_dict.keys(): + output_dict[name + '_location'] = output_dict['image_location'] + return output_dict + # the output image shape should be + # [enqueue_batch_size, x, y, z, time, modality] + # where enqueue_batch_size = windows_per_image + except ValueError: + tf.logging.fatal("Cannot provide output for %s" %subject_id) + raise + else: + tf.logging.fatal("%s not in available list of subjects" %subject_id) + raise ValueError + + def csvcenter_spatial_coordinates(self, + subject_id, + data, + img_sizes, + win_sizes, + mode_correction='pad', + n_samples=1): + """ + Generate spatial coordinates for sampling. + + Values in ``win_sizes`` could be different -- + for example in a segmentation network ``win_sizes`` could be + ``{'training_image_spatial_window': (32, 32, 10), + 'Manual_label_spatial_window': (16, 16, 10)}`` + (the network reduces x-y plane spatial resolution). + + This function handles this situation by first find the largest + window across these window definitions, and generate the coordinates. + These coordinates are then adjusted for each of the + smaller window sizes (the output windows are almost concentric) + This function provide the appropriate sampled coordinates modified + according to knowledge of the reader constraints on resolution and + orientation. + """ + + assert data is not None, "No input from image reader. Please check" \ + "the configuration file." + + # infer the largest spatial window size and check image spatial shapes + img_spatial_size, win_spatial_size = \ + _infer_spatial_size(img_sizes, win_sizes) + + window_centres = [] + reject = False + if mode_correction == 'remove': + reject = True + + # try: + # window_centres = csv_data.get('sampler', None) + # except AttributeError: + # pass + + n_samples = max(n_samples, 1) + all_coordinates = {} + +# If there is no csv reader for the sampler, we fall back to a uniform sampling + if 'sampler' not in self.csv_reader.task_param.keys(): + window_centres = rand_spatial_coordinates(n_samples, + img_spatial_size, + win_spatial_size, + None) + list_idx = np.arange(0, n_samples) + + else: + window_centres_list = [] + list_idx = [] + _, _ = self.check_csv_sampler_valid(subject_id, img_sizes, + win_sizes) + idx_check, _, _ = self.csv_reader( + subject_id=subject_id, mode='multi', reject=False) + idx_multi = idx_check['sampler'] + for mod in self.csv_reader.task_param: + all_coordinates[mod] = [] + for n in range(0, n_samples): + # print("reject value is ", reject) + idx, data_csv, _ = self.csv_reader( + subject_id=subject_id, mode='single', reject=reject) + # print(data_csv['sampler'].shape[0], 'data_sampler') + if data_csv['sampler'].shape[0] > 0: + centre_transform = self.transform_centres( + subject_id, img_sizes, + np.expand_dims(np.squeeze(data_csv['sampler']), 0)) + # centre_tmp = np.expand_dims(centre_transform,0) + # for mod in idx.keys(): + # all_coordinates[mod].append(np.expand_dims( + # np.squeeze(data_csv[mod]), 0)) + list_idx.append(idx['sampler']) + print(centre_transform.shape) + window_centres_list.append(centre_transform) + window_centres = np.concatenate(window_centres_list, 0) + # If nothing is valid and the mode of correction is rand, then we + # default back to a uniform sampling + if np.sum(self.csv_reader.valid_by_task['sampler'][idx_multi]) ==\ + 0 and np.asarray(window_centres).shape[0] == 0 and \ + mode_correction == 'rand': + tf.logging.warning("Nothing is valid, taking random centres") + window_centres = rand_spatial_coordinates(n_samples, + img_spatial_size, + win_spatial_size, + None) + list_idx = np.arange(0, n_samples) + print("all prepared and added ") + + assert window_centres.shape == (n_samples, N_SPATIAL), \ + "the coordinates generator should return " \ + "{} samples of rank {} locations".format(n_samples, N_SPATIAL) + + # adjust spatial coordinates based on each mod spatial window size + + for mod in list(win_sizes): + win_size = np.asarray(win_sizes[mod][:N_SPATIAL]) + half_win = np.floor(win_size / 2.0).astype(int) + + # Make starting coordinates of the window + spatial_coords = np.zeros( + (1, N_SPATIAL * 2), dtype=np.int32) + if mode_correction != 'pad': + spatial_coords[:, :N_SPATIAL] = np.maximum( + window_centres[0, :N_SPATIAL] - half_win[:N_SPATIAL], 0) + else: + spatial_coords[:, :N_SPATIAL] = window_centres[0, :N_SPATIAL] \ + - half_win[:N_SPATIAL] + + # Make the opposite corner of the window is + # just adding the mod specific window size + spatial_coords[:, N_SPATIAL:] = \ + spatial_coords[:, :N_SPATIAL] + win_size[:N_SPATIAL] + + # assert np.all(spatial_coords[:, N_SPATIAL:] <= img_spatial_size), + # 'spatial coords: out of bounds.' + + # include subject id as the 1st column of all_coordinates values + idx_subject_id = np.where(self.reader._file_list.subject_id == + subject_id)[0][0] + idx_subject_id = np.ones((n_samples,), + dtype=np.int32) * idx_subject_id + spatial_coords = np.append( + idx_subject_id[:, None], spatial_coords, axis=1) + all_coordinates[mod] = spatial_coords + + return all_coordinates, list_idx + + def transform_centres(self, subject_id, img_sizes, windows_centres): + # For the moment assuming that same img size and orientations across + # modalities + list_mod = list(img_sizes.keys()) + + print(list_mod) + idx_subject_id = np.where(self.reader._file_list.subject_id == + subject_id)[0][0] + input_shape = self.reader.output_list[idx_subject_id][list_mod[ + 0]].original_shape[:N_SPATIAL] + output_shape = self.reader.output_list[idx_subject_id][list_mod[ + 0]].shape[:N_SPATIAL] + init_axcodes = self.reader.output_list[idx_subject_id][list_mod[ + 0]].original_axcodes + + fin_axcodes = self.reader.output_list[idx_subject_id][ + list_mod[0]].output_axcodes + print(output_shape, init_axcodes[0], fin_axcodes[0]) + transformed_centres, ornt_transf = do_reorientation_idx( + windows_centres, init_axcodes[0], fin_axcodes[0], input_shape) + + transformed_centres = np.squeeze(transformed_centres.astype(np.int32)) + + # then taking care of change in pixdim + input_pixdim = self.reader.output_list[idx_subject_id][list_mod[ + 0]].original_pixdim[0] + output_pixdim = self.reader.output_list[idx_subject_id][list_mod[ + 0]].output_pixdim[0] + reorder_axes = np.squeeze(np.asarray(ornt_transf[:, 0]).astype( + np.int32)) + print("found pixdim to change", input_pixdim, output_pixdim, + reorder_axes) + input_pixdim_no = [input_pixdim[r] for r in reorder_axes] + transformed_centres = do_resampling_idx(transformed_centres, + input_pixdim_no, output_pixdim) + + if transformed_centres.ndim == 1: + transformed_centres = np.expand_dims(transformed_centres, 0) + + padding = (np.asarray(img_sizes[list_mod[0]][:N_SPATIAL]) - + np.asarray(output_shape)) / 2.0 + padding = padding.astype(np.int32) + print(transformed_centres.shape, padding.shape) + transformed_centres += np.tile(np.expand_dims(padding, 0), + [len(windows_centres), 1]) + return transformed_centres + + def check_csv_sampler_valid(self, subject_id, img_sizes, win_sizes): + print("Checking if csv_sampler valid is updated") + reject = False + if self.mode_correction != 'pad': + reject = True + idx_multi, csv_data, _ = self.csv_reader(subject_id=subject_id, + idx=None, mode='multi', + reject=reject) + + windows_centres = csv_data['sampler'] + # print("Windows extracted", windows_centres) + numb = windows_centres.shape[0] + if windows_centres.shape[0] > 0: + checked = self.csv_reader.valid_by_task['sampler'][ + idx_multi['sampler']] + print(np.sum(checked), 'is sum of checked') + min_checked = np.min(checked) + numb_valid = np.sum(checked) + else: + min_checked = 0 + numb_valid = 0 + if min_checked >= 0: + print("Already checked, no need for further analysis") + return numb, numb-numb_valid + else: + transformed_centres = self.transform_centres(subject_id, img_sizes, + windows_centres) + + img_spatial_size, win_spatial_size = _infer_spatial_size( + img_sizes, win_sizes) + tf.logging.warning("Need to checked validity of samples for " + "subject %s" %subject_id) + checked = np.ones([numb]) + pad = np.zeros([numb, 2*N_SPATIAL]) + print(list(win_sizes)) + for mod in list(win_sizes): + print("mod is %s" % mod) + print(img_spatial_size) + win_size = np.asarray(win_sizes[mod][:N_SPATIAL]) + half_win = np.floor(win_size / 2.0).astype(int) + + # Make starting coordinates of the window + spatial_coords = np.zeros( + (numb, N_SPATIAL * 2), dtype=np.int32) + half_win_tiled = np.tile(half_win[:N_SPATIAL], [numb, 1]) + reshaped_windows = np.reshape(transformed_centres[:, + :N_SPATIAL], + half_win_tiled.shape) + spatial_coords[:, :N_SPATIAL] = reshaped_windows - \ + half_win_tiled + + min_spatial_coords = np.max(-1*spatial_coords, 1) + checked = np.asarray(np.where(min_spatial_coords > 0, + np.zeros_like(checked), checked)) + pad = np.maximum(-1*spatial_coords, pad) + # Make the opposite corner of the window is + # just adding the mod specific window size + spatial_coords[:, N_SPATIAL:] = spatial_coords[:, :N_SPATIAL] +\ + np.tile(win_size[:N_SPATIAL], + [numb, 1]) + + max_spatial_coords = np.max(spatial_coords[:, N_SPATIAL:] - + np.tile(img_spatial_size, + [numb, 1]), + axis=1) + diff_spatial_size = spatial_coords[:, N_SPATIAL:] - np.tile( + img_spatial_size, [numb, 1]) + checked = np.asarray(np.where(max_spatial_coords > 0, + np.zeros_like(checked), checked)) + pad[:, N_SPATIAL:] = np.maximum(diff_spatial_size, + pad[:, N_SPATIAL:]) + + tf.logging.warning("to discard or pad is %d out of %d for mod " + "%s" % (numb-np.sum(checked), numb, mod)) + + idx_discarded = [] + for i in range(0, len(checked)): + self.csv_reader.valid_by_task['sampler'][idx_multi[ + 'sampler'][i]] = checked[i] + self.csv_reader.pad_by_task['sampler'][idx_multi['sampler'][ + i]] = pad[i] + if checked[i] == 0: + idx_discarded.append(idx_multi['sampler'][i]) + # self.csv_reader.valid_by_task['sampler'][np.asarray(idx_multi[ + # 'sampler'])] = checked + print('Updated check') + if np.sum(checked) < numb: + tf.logging.warning("The following indices are not valid for " + "%s %s " %(subject_id, ' '.join(map(str, + idx_discarded)))) + print( + "updated valid part of csv_reader for subject %s" % subject_id) + return numb, numb-np.sum(checked) + + +# def correction_coordinates(coordinates, idx, pb_coord, img_sizes, win_sizes, +# csv_sampler, mode="remove"): +# # infer the largest spatial window size and check image spatial shapes +# +# img_spatial_size, win_spatial_size = _infer_spatial_size( +# img_sizes, win_sizes) +# overall_pb = np.zeros([len(idx), 1]) +# numb_wind = len(idx) +# for mod in list(win_sizes): +# overall_pb += np.sum(np.abs(pb_coord[mod]), 1) +# if np.sum(overall_pb) == 0: +# return coordinates, None +# else: +# +# list_nopb = np.where(overall_pb == 0) +# list_pb = np.where(overall_pb > 0) +# idx_pb = idx[list_pb] +# if mode == "remove": +# for mod in list(win_sizes): +# coordinates[mod]=coordinates[mod][list_nopb, :] +# return coordinates, idx_pb +# elif mode == "replace" : +# n_pb = np.sum(overall_pb) +# window_centres_replacement = rand_spatial_coordinates( +# n_pb, img_spatial_size, win_spatial_size, None) +# spatial_coords_replacement = np.zeros( +# (n_pb, N_SPATIAL * 2), dtype=np.int32) +# +# for mod in list(win_sizes): +# win_size = np.asarray(win_sizes[mod][:N_SPATIAL]) +# half_win = np.floor(win_size / 2.0).astype(int) +# +# # Make starting coordinates of the window +# spatial_coords_replacement[:, :N_SPATIAL] = np.maximum( +# window_centres_replacement[:, :N_SPATIAL] - np.tile( +# half_win[:N_SPATIAL], [n_pb, 1]), 0) +# spatial_coords_replacement[:, N_SPATIAL:] = \ +# spatial_coords_replacement[:, :N_SPATIAL] + np.tile( +# win_size[:N_SPATIAL], [n_pb, 1]) +# n_replaced = 0 +# for n in range(0, numb_wind): +# if overall_pb[n]: +# coordinates[n, :] = spatial_coords_replacement[n_replaced] +# n_replaced += 1 +# return coordinates, idx_pb + + +def rand_spatial_coordinates( + n_samples, img_spatial_size, win_spatial_size, sampler_map): + """ + Generate spatial coordinates from a discrete uniform distribution. + + :param n_samples: number of random coordinates to generate + :param img_spatial_size: input image size + :param win_spatial_size: input window size + :param sampler_map: sampling prior map (not in use) + :return: (n_samples, N_SPATIAL) coordinates representing sampling + window centres relative to img_spatial_size + """ + tf.logging.debug('uniform sampler, prior %s ignored', sampler_map) + + # Sample coordinates at random + half_win = np.floor(np.asarray(win_spatial_size) / 2.0).astype(np.int32) + max_coords = np.zeros((n_samples, N_SPATIAL), dtype=np.int32) + for (idx, (img, win)) in enumerate( + zip(img_spatial_size[:N_SPATIAL], win_spatial_size[:N_SPATIAL])): + max_coords[:, idx] = np.random.randint( + 0, max(img - win + 1, 1), n_samples) + max_coords[:, :N_SPATIAL] = \ + max_coords[:, :N_SPATIAL] + half_win[:N_SPATIAL] + return max_coords + + +def _infer_spatial_size(img_sizes, win_sizes): + """ + Utility function to find the spatial size of image, + and the largest spatial window size across input sections. + + Raises NotImplementedError if the images have + different spatial dimensions. + + :param img_sizes: dictionary of {'input_name': (img_size_x, img_size,y,...)} + :param win_sizes: dictionary of {'input_name': (win_size_x, win_size_y,...)} + :return: (image_spatial_size, window_largest_spatial_size) + """ + uniq_spatial_size = \ + set([img_size[:N_SPATIAL] for img_size in list(img_sizes.values())]) + if len(uniq_spatial_size) != 1: + tf.logging.fatal("Don't know how to generate sampling " + "locations: Spatial dimensions of the " + "grouped input sources are not " + "consistent. %s", uniq_spatial_size) + raise NotImplementedError + img_spatial_size = np.asarray(uniq_spatial_size.pop(), dtype=np.int32) + + # find the largest spatial window across input sections + _win_spatial_sizes = \ + [win_size[:N_SPATIAL] for win_size in win_sizes.values()] + _win_spatial_sizes = np.asarray(_win_spatial_sizes, dtype=np.int32) + win_spatial_size = np.max(_win_spatial_sizes, axis=0) + + assert all([img_spatial_size[i] >= win_spatial_size[i] + for i in range(N_SPATIAL)]), \ + "window size {} is larger than image size {}".format( + win_spatial_size, img_spatial_size) + + return img_spatial_size, win_spatial_size diff --git a/niftynet/contrib/csv_reader/sampler_grid_v2_csv.py b/niftynet/contrib/csv_reader/sampler_grid_v2_csv.py new file mode 100755 index 00000000..ecf0ec45 --- /dev/null +++ b/niftynet/contrib/csv_reader/sampler_grid_v2_csv.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +""" +Sampling image by a sliding window. +""" +from __future__ import absolute_import, division, print_function + +import numpy as np +import tensorflow as tf + +from niftynet.engine.image_window_dataset import ImageWindowDataset +from niftynet.contrib.csv_reader.sampler_csv_rows import ImageWindowDatasetCSV +from niftynet.engine.image_window import N_SPATIAL, LOCATION_FORMAT + + +# pylint: disable=too-many-locals +class GridSamplerCSV(ImageWindowDatasetCSV): + """ + This class generators ND image samples with a sliding window. + """ + + def __init__(self, + reader, + csv_reader, + window_sizes, + batch_size=1, + spatial_window_size=None, + window_border=None, + queue_length=10, + smaller_final_batch_mode='pad', + name='grid_sampler'): + + # override all spatial window defined in input + # modalities sections + # this is useful when do inference with a spatial window + # which is different from the training specifications + ImageWindowDatasetCSV.__init__( + self, + reader=reader, + csv_reader=csv_reader, + window_sizes=spatial_window_size or window_sizes, + batch_size=batch_size, + windows_per_image=1, + queue_length=queue_length, + shuffle=False, + epoch=1, + smaller_final_batch_mode=smaller_final_batch_mode, + name=name) + self.csv_reader = csv_reader + self.border_size = window_border or (0, 0, 0) + assert isinstance(self.border_size, (list, tuple)), \ + "window_border should be a list or tuple" + while len(self.border_size) < N_SPATIAL: + self.border_size = tuple(self.border_size) + \ + (self.border_size[-1],) + self.border_size = self.border_size[:N_SPATIAL] + tf.logging.info('initialised window instance') + tf.logging.info("initialised grid sampler %s", self.window.shapes) + + def layer_op(self, idx=None): + while True: + image_id, data, _ = self.reader(idx=None, shuffle=False) + if not data: + break + image_shapes = {name: data[name].shape + for name in self.window.names} + static_window_shapes = self.window.match_image_shapes(image_shapes) + coordinates = grid_spatial_coordinates( + image_id, image_shapes, static_window_shapes, self.border_size) + + # extend the number of sampling locations to be divisible + # by batch size + n_locations = list(coordinates.values())[0].shape[0] + extra_locations = 0 + if (n_locations % self.batch_size) > 0: + extra_locations = \ + self.batch_size - n_locations % self.batch_size + total_locations = n_locations + extra_locations + + tf.logging.info( + 'grid sampling image sizes: %s', image_shapes) + tf.logging.info( + 'grid sampling window sizes: %s', static_window_shapes) + if extra_locations > 0: + tf.logging.info( + "yielding %s locations from image, " + "extended to %s to be divisible by batch size %s", + n_locations, total_locations, self.batch_size) + else: + tf.logging.info( + "yielding %s locations from image", n_locations) + for i in range(total_locations): + idx = i % n_locations + #  initialise output dict + output_dict = {} + for name in list(data): + assert coordinates[name].shape[0] == n_locations, \ + "different number of grid samples from the input" \ + "images, don't know how to combine them in the queue" + x_start, y_start, z_start, x_end, y_end, z_end = \ + coordinates[name][idx, 1:] + try: + image_window = data[name][ + x_start:x_end, y_start:y_end, z_start:z_end, ...] + except ValueError: + tf.logging.fatal( + "dimensionality miss match in input volumes, " + "please specify spatial_window_size with a " + "3D tuple and make sure each element is " + "smaller than the image length in each dim.") + raise + # fill output dict with data + coord_key = LOCATION_FORMAT.format(name) + image_key = name + output_dict[coord_key] = coordinates[name][idx:idx+1, ...] + output_dict[image_key] = image_window[np.newaxis, ...] + if self.csv_reader is not None: + _, label_dict, _ = self.csv_reader(idx=image_id) + output_dict.update(label_dict) + for name in self.csv_reader.names: + output_dict[name + '_location'] = output_dict[ + 'image_location'] + yield output_dict + + # this is needed because otherwise reading beyond the last element + # raises an out-of-range error, and the last grid sample + # will not be processed properly. + try: + for name in list(output_dict): + output_dict[name] = np.ones_like(output_dict[name]) * -1 + if self.csv_reader is not None: + _, label_dict, _ = self.csv_reader(idx=image_id) + output_dict.update(label_dict) + for name in self.csv_reader.task_param.keys(): + output_dict[name + '_location'] = output_dict[ + 'image_location'] + yield output_dict + except (NameError, KeyError): + tf.logging.fatal("No feasible samples from %s", self) + raise + +def grid_spatial_coordinates(subject_id, img_sizes, win_sizes, border_size): + """ + This function generates all coordinates of feasible windows, with + step sizes specified in grid_size parameter. + + The border size changes the sampling locations but not the + corresponding window sizes of the coordinates. + + :param subject_id: integer value indicates the position of of this + image in ``image_reader.file_list`` + :param img_sizes: a dictionary of image shapes, ``{input_name: shape}`` + :param win_sizes: a dictionary of window shapes, ``{input_name: shape}`` + :param border_size: size of padding on both sides of each dim + :return: + """ + all_coordinates = {} + for name, image_shape in img_sizes.items(): + window_shape = win_sizes[name] + grid_size = [max(win_size - 2 * border, 0) + for (win_size, border) in zip(window_shape, border_size)] + assert len(image_shape) >= N_SPATIAL, \ + 'incompatible image shapes in grid_spatial_coordinates' + assert len(window_shape) >= N_SPATIAL, \ + 'incompatible window shapes in grid_spatial_coordinates' + assert len(grid_size) >= N_SPATIAL, \ + 'incompatible border sizes in grid_spatial_coordinates' + steps_along_each_dim = [ + _enumerate_step_points(starting=0, + ending=image_shape[i], + win_size=window_shape[i], + step_size=grid_size[i]) + for i in range(N_SPATIAL)] + starting_coords = np.asanyarray(np.meshgrid(*steps_along_each_dim)) + starting_coords = starting_coords.reshape((N_SPATIAL, -1)).T + n_locations = starting_coords.shape[0] + # prepare the output coordinates matrix + spatial_coords = np.zeros((n_locations, N_SPATIAL * 2), dtype=np.int32) + spatial_coords[:, :N_SPATIAL] = starting_coords + for idx in range(N_SPATIAL): + spatial_coords[:, N_SPATIAL + idx] = \ + starting_coords[:, idx] + window_shape[idx] + max_coordinates = np.max(spatial_coords, axis=0)[N_SPATIAL:] + assert np.all(max_coordinates <= image_shape[:N_SPATIAL]), \ + "window size greater than the spatial coordinates {} : {}".format( + max_coordinates, image_shape) + subject_list = np.ones((n_locations, 1), dtype=np.int32) * subject_id + spatial_coords = np.append(subject_list, spatial_coords, axis=1) + all_coordinates[name] = spatial_coords + return all_coordinates + + +def _enumerate_step_points(starting, ending, win_size, step_size): + """ + generate all possible sampling size in between starting and ending. + + :param starting: integer of starting value + :param ending: integer of ending value + :param win_size: integer of window length + :param step_size: integer of distance between two sampling points + :return: a set of unique sampling points + """ + try: + starting = max(int(starting), 0) + ending = max(int(ending), 0) + win_size = max(int(win_size), 1) + step_size = max(int(step_size), 1) + except (TypeError, ValueError): + tf.logging.fatal( + 'step points should be specified by integers, received:' + '%s, %s, %s, %s', starting, ending, win_size, step_size) + raise ValueError + if starting > ending: + starting, ending = ending, starting + sampling_point_set = [] + while (starting + win_size) <= ending: + sampling_point_set.append(starting) + starting = starting + step_size + additional_last_point = ending - win_size + sampling_point_set.append(max(additional_last_point, 0)) + sampling_point_set = np.unique(sampling_point_set).flatten() + if len(sampling_point_set) == 2: + # in case of too few samples, adding + # an additional sampling point to + # the middle between starting and ending + sampling_point_set = np.append( + sampling_point_set, np.round(np.mean(sampling_point_set))) + _, uniq_idx = np.unique(sampling_point_set, return_index=True) + return sampling_point_set[np.sort(uniq_idx)] diff --git a/niftynet/contrib/csv_reader/sampler_linear_interpolate_v2_csv.py b/niftynet/contrib/csv_reader/sampler_linear_interpolate_v2_csv.py new file mode 100755 index 00000000..06e3e2f6 --- /dev/null +++ b/niftynet/contrib/csv_reader/sampler_linear_interpolate_v2_csv.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +""" +Generating samples by linearly combining two input images. +""" +from __future__ import absolute_import, print_function, division + +import numpy as np +import tensorflow as tf + +from niftynet.engine.image_window_dataset import ImageWindowDataset +from niftynet.contrib.csv_reader.sampler_csv_rows import ImageWindowDatasetCSV +from niftynet.engine.image_window import N_SPATIAL, LOCATION_FORMAT + + +class LinearInterpolateSamplerCSV(ImageWindowDatasetCSV): + """ + This class reads two feature vectors from files (often generated + by running feature extractors on images in advance) + and returns n linear combinations of the vectors. + The coefficients are generated by:: + + np.linspace(0, 1, n_interpolations) + """ + + def __init__(self, + reader, + csv_reader, + window_sizes, + batch_size=10, + n_interpolations=10, + queue_length=10, + name='linear_interpolation_sampler'): + ImageWindowDatasetCSV.__init__( + self, + reader, + csv_reader=csv_reader, + window_sizes=window_sizes, + batch_size=batch_size, + queue_length=queue_length, + shuffle=False, + epoch=1, + smaller_final_batch_mode='drop', + name=name) + self.n_interpolations = n_interpolations + # only try to use the first spatial shape available + image_spatial_shape = list(self.reader.shapes.values())[0][:3] + self.window.set_spatial_shape(image_spatial_shape) + tf.logging.info( + "initialised linear interpolation sampler %s ", self.window.shapes) + assert not self.window.has_dynamic_shapes, \ + "dynamic shapes not supported, please specify " \ + "spatial_window_size = (1, 1, 1)" + + def layer_op(self, *_unused_args, **_unused_kwargs): + """ + This function first reads two vectors, and interpolates them + with self.n_interpolations mixing coefficients. + + Location coordinates are set to ``np.ones`` for all the vectors. + """ + output_dict = {} + image_id_x, data_x, _ = self.reader(idx=None, shuffle=False) + image_id_y, data_y, _ = self.reader(idx=None, shuffle=True) + if not data_x or not data_y: + return + if image_id_x == image_id_y: + while image_id_x == image_id_y: + image_id_x, data_x, _ = self.reader(idx=None, shuffle=False) + image_id_y, data_y, _ = self.reader(idx=None, shuffle=True) + if not data_x or not data_y: + return + embedding_x = data_x[self.window.names[0]] + embedding_y = data_y[self.window.names[0]] + + steps = np.linspace(0, 1, self.n_interpolations) + for (_, mixture) in enumerate(steps): + output_vector = \ + embedding_x * mixture + embedding_y * (1 - mixture) + coordinates = np.ones((1, N_SPATIAL * 2 + 1), dtype=np.int32) + coordinates[0, 0:2] = [image_id_x, image_id_y] + output_dict = {} + for name in self.window.names: + coordinates_key = LOCATION_FORMAT.format(name) + image_data_key = name + output_dict[coordinates_key] = coordinates + output_dict[image_data_key] = output_vector[np.newaxis, ...] + if self.csv_reader is not None: + _, label_dict_x, _ = self.csv_reader(idx=image_id_x) + _, label_dict_y, _ = self.csv_reader(idx=image_id_y) + output_dict.update(label_dict_x) + output_dict.update(label_dict_y) + for name in self.csv_reader.names(): + output_dict[name + '_location'] = output_dict[ + 'image_location'] + return output_dict diff --git a/niftynet/contrib/csv_reader/sampler_random_vector_v2_csv.py b/niftynet/contrib/csv_reader/sampler_random_vector_v2_csv.py new file mode 100755 index 00000000..e958d8bb --- /dev/null +++ b/niftynet/contrib/csv_reader/sampler_random_vector_v2_csv.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +""" +Generating sample arrays from random distributions. +""" +from __future__ import absolute_import, print_function, division + +import numpy as np +import tensorflow as tf + +from niftynet.engine.image_window_dataset import ImageWindowDataset +from niftynet.contrib.csv_reader.sampler_csv_rows import ImageWindowDatasetCSV +from niftynet.engine.image_window import \ + N_SPATIAL, LOCATION_FORMAT, ImageWindow + + +class RandomVectorSampler(ImageWindowDataset): + """ + This class generates two samples from the standard normal + distribution. These two samples are mixed with n + mixing coefficients. The coefficients are generated + by ``np.linspace(0, 1, n_interpolations)`` + """ + + def __init__(self, + names=('vector',), + vector_size=(100,), + batch_size=10, + n_interpolations=10, + mean=0.0, + stddev=1.0, + repeat=1, + queue_length=10, + name='random_vector_sampler'): + # repeat=None for infinite loops + self.n_interpolations = max(n_interpolations, 1) + self.mean = mean + self.stddev = stddev + self.repeat = repeat + self.names = names + + ImageWindowDatasetCSV.__init__( + self, + reader=None, + csv_reader=None, + window_sizes={names[0]: {'spatial_window_size': vector_size}}, + batch_size=batch_size, + queue_length=queue_length, + shuffle=False, + epoch=1, + smaller_final_batch_mode='drop', + name=name) + self.window = ImageWindow(shapes={names[0]: vector_size}, + dtypes={names[0]: tf.float32}) + tf.logging.info("initialised sampler output %s ", self.window.shapes) + + def layer_op(self): + """ + This function first draws two samples, and interpolates them + with self.n_interpolations mixing coefficients. + + Location coordinates are set to ``np.ones`` for all the vectors. + """ + total_iter = self.repeat if self.repeat is not None else 1 + while total_iter > 0: + total_iter = total_iter - 1 if self.repeat is not None else 1 + embedding_x = np.random.normal( + self.mean, + self.stddev, + self.window.shapes[self.window.names[0]]) + embedding_y = np.random.normal( + self.mean, + self.stddev, + self.window.shapes[self.window.names[0]]) + steps = np.linspace(0, 1, self.n_interpolations) + for (_, mixture) in enumerate(steps): + output_vector = \ + embedding_x * mixture + embedding_y * (1 - mixture) + coordinates = np.ones((1, N_SPATIAL * 2 + 1), dtype=np.int32) + output_dict = {} + for name in self.window.names: + coordinates_key = LOCATION_FORMAT.format(name) + image_data_key = name + output_dict[coordinates_key] = coordinates + output_dict[image_data_key] = output_vector + yield output_dict diff --git a/niftynet/contrib/csv_reader/sampler_resize_v2_csv.py b/niftynet/contrib/csv_reader/sampler_resize_v2_csv.py index f5affa03..e3f16da4 100755 --- a/niftynet/contrib/csv_reader/sampler_resize_v2_csv.py +++ b/niftynet/contrib/csv_reader/sampler_resize_v2_csv.py @@ -29,6 +29,7 @@ def __init__(self, windows_per_image=1, shuffle=True, queue_length=10, + num_threads=4, smaller_final_batch_mode='pad', name='resize_sampler_v2'): tf.logging.info('reading size of preprocessed images') @@ -41,6 +42,7 @@ def __init__(self, batch_size=batch_size, windows_per_image=windows_per_image, queue_length=queue_length, + num_threads=num_threads, shuffle=shuffle, epoch=-1 if shuffle else 1, smaller_final_batch_mode=smaller_final_batch_mode, @@ -65,55 +67,54 @@ def layer_op(self, idx=None): :return: output data dictionary ``{'image_modality': data_array}`` """ - while True: - image_id, data, interp_orders = self.reader(idx=idx) - image_shapes = \ - dict((name, data[name].shape) for name in self.window.names) - # window shapes can be dynamic, here they - # are converted to static ones - # as now we know the image shapes - static_window_shapes = self.window.match_image_shapes(image_shapes) + image_id, data, interp_orders = self.reader(idx=idx) + image_shapes = \ + dict((name, data[name].shape) for name in self.window.names) + # window shapes can be dynamic, here they + # are converted to static ones + # as now we know the image shapes + static_window_shapes = self.window.match_image_shapes(image_shapes) + # for resize sampler the coordinates are not used + # simply use the spatial dims of the input image + output_dict = {} + for name in list(data): + # prepare output dictionary keys + coordinates_key = LOCATION_FORMAT.format(name) + image_data_key = name - # for resize sampler the coordinates are not used - # simply use the spatial dims of the input image - output_dict = {} - for name in list(data): - # prepare output dictionary keys - coordinates_key = LOCATION_FORMAT.format(name) - image_data_key = name + output_dict[coordinates_key] = self.dummy_coordinates( + image_id, static_window_shapes[name], self.window.n_samples) + image_array = [] + for _ in range(self.window.n_samples): + # prepare image data + image_shape = image_shapes[name] + window_shape = static_window_shapes[name] - output_dict[coordinates_key] = np.squeeze(self.dummy_coordinates( - image_id, static_window_shapes[name], self.window.n_samples), axis=0) - image_array = [] - for _ in range(self.window.n_samples): - # prepare image data - image_shape = image_shapes[name] - window_shape = static_window_shapes[name] - - if image_shape == window_shape or interp_orders[name][0] < 0: - # already in the same shape - image_window = data[name] - else: - zoom_ratio = [float(p) / float(d) for p, d in - zip(window_shape, image_shape)] - image_window = zoom_3d(image=data[name], - ratio=zoom_ratio, interp_order= - interp_orders[name][0]) - image_array.append(image_window[np.newaxis, ...]) - if len(image_array) > 1: - output_dict[image_data_key] = \ - np.concatenate(image_array, axis=0) + if image_shape == window_shape or interp_orders[name][0] < 0: + # already in the same shape + image_window = data[name] else: - output_dict[image_data_key] = np.squeeze(image_array[0], axis=0) - # the output image shape should be - # [enqueue_batch_size, x, y, z, time, modality] - # here enqueue_batch_size = 1 as we only have one sample - # per image - if self.csv_reader is not None: - _, label_dict, _ = self.csv_reader(idx=image_id) - output_dict['label'] = np.squeeze(label_dict['label'], axis=0) - output_dict['label_location'] = output_dict['image_location'] - yield output_dict + zoom_ratio = [float(p) / float(d) for p, d in + zip(window_shape, image_shape)] + image_window = zoom_3d(image=data[name], + ratio=zoom_ratio, interp_order= + interp_orders[name][0]) + image_array.append(image_window[np.newaxis, ...]) + if len(image_array) > 1: + output_dict[image_data_key] = \ + np.concatenate(image_array, axis=0) + else: + output_dict[image_data_key] = image_array[0] + # the output image shape should be + # [enqueue_batch_size, x, y, z, time, modality] + # here enqueue_batch_size = 1 as we only have one sample + # per image + if self.csv_reader is not None: + _, label_dict, _ = self.csv_reader(idx=image_id) + output_dict.update(label_dict) + for name in self.csv_reader.names: + output_dict[name + '_location'] = output_dict['image_location'] + return output_dict def zoom_3d(image, ratio, interp_order): diff --git a/niftynet/contrib/csv_reader/sampler_uniform_v2_csv.py b/niftynet/contrib/csv_reader/sampler_uniform_v2_csv.py new file mode 100644 index 00000000..08c4add3 --- /dev/null +++ b/niftynet/contrib/csv_reader/sampler_uniform_v2_csv.py @@ -0,0 +1,253 @@ +# -*- coding: utf-8 -*- +""" +Generating uniformly distributed image window from input image +This can also be considered as a "random cropping" layer of the +input image. +""" +from __future__ import absolute_import, division, print_function + +import numpy as np +import tensorflow as tf + +from niftynet.contrib.csv_reader.sampler_csv_rows import ImageWindowDatasetCSV +from niftynet.engine.image_window import N_SPATIAL, LOCATION_FORMAT + + +class UniformSamplerCSV(ImageWindowDatasetCSV): + """ + This class generates samples by uniformly sampling each input volume + currently the coordinates are randomised for spatial dims only, + i.e., the first three dims of image. + + This layer can be considered as a "random cropping" layer of the + input image. + """ + + def __init__(self, + reader, + csv_reader, + window_sizes, + batch_size=1, + windows_per_image=1, + queue_length=10, + num_threads=4, + smaller_final_batch_mode='drop', + name='uniform_sampler_v2'): + ImageWindowDatasetCSV.__init__( + self, + reader=reader, + csv_reader=csv_reader, + window_sizes=window_sizes, + batch_size=batch_size, + windows_per_image=windows_per_image, + queue_length=queue_length, + num_threads=num_threads, + shuffle=True, + epoch=-1, + smaller_final_batch_mode=smaller_final_batch_mode, + name=name) + + tf.logging.info("initialised uniform sampler %s ", self.window.shapes) + self.window_centers_sampler = rand_spatial_coordinates + + # pylint: disable=too-many-locals + def layer_op(self, idx=None): + """ + This function generates sampling windows to the input buffer + image data are from ``self.reader()`` + + It first completes window shapes based on image data, + then finds random coordinates based on the window shapes + finally extract window with the coordinates and output + a dictionary (required by input buffer). + + :return: output data dictionary + ``{image_modality: data_array, image_location: n_samples * 7}`` + """ + image_id, data, _ = self.reader(idx=idx, shuffle=True) + image_shapes = dict( + (name, data[name].shape) for name in self.window.names) + static_window_shapes = self.window.match_image_shapes(image_shapes) + # find random coordinates based on window and image shapes + coordinates = self._spatial_coordinates_generator( + subject_id=image_id, + data=data, + img_sizes=image_shapes, + win_sizes=static_window_shapes, + n_samples=self.window.n_samples) + + # initialise output dict, placeholders as dictionary keys + # this dictionary will be used in + # enqueue operation in the form of: `feed_dict=output_dict` + output_dict = {} + # fill output dict with data + for name in list(data): + coordinates_key = LOCATION_FORMAT.format(name) + image_data_key = name + + # fill the coordinates + location_array = coordinates[name] + output_dict[coordinates_key] = location_array + + # fill output window array + image_array = [] + for window_id in range(self.window.n_samples): + x_start, y_start, z_start, x_end, y_end, z_end = \ + location_array[window_id, 1:] + try: + image_window = data[name][ + x_start:x_end, y_start:y_end, z_start:z_end, ...] + image_array.append(image_window[np.newaxis, ...]) + except ValueError: + tf.logging.fatal( + "dimensionality miss match in input volumes, " + "please specify spatial_window_size with a " + "3D tuple and make sure each element is " + "smaller than the image length in each dim. " + "Current coords %s", location_array[window_id]) + raise + if len(image_array) > 1: + output_dict[image_data_key] = \ + np.concatenate(image_array, axis=0) + else: + output_dict[image_data_key] = image_array[0] + # the output image shape should be + # [enqueue_batch_size, x, y, z, time, modality] + # where enqueue_batch_size = windows_per_image + if self.csv_reader is not None: + _, label_dict, _ = self.csv_reader(idx=image_id) + output_dict.update(label_dict) + for name in self.csv_reader.names: + output_dict[name + '_location'] = output_dict['image_location'] + print("output_dict gotten", output_dict.keys(), len(output_dict.keys())) + return output_dict + + def _spatial_coordinates_generator(self, + subject_id, + data, + img_sizes, + win_sizes, + n_samples=1): + """ + Generate spatial coordinates for sampling. + + Values in ``win_sizes`` could be different -- + for example in a segmentation network ``win_sizes`` could be + ``{'training_image_spatial_window': (32, 32, 10), + 'Manual_label_spatial_window': (16, 16, 10)}`` + (the network reduces x-y plane spatial resolution). + + This function handles this situation by first find the largest + window across these window definitions, and generate the coordinates. + These coordinates are then adjusted for each of the + smaller window sizes (the output windows are almost concentric). + """ + + assert data is not None, "No input from image reader. Please check" \ + "the configuration file." + + # infer the largest spatial window size and check image spatial shapes + img_spatial_size, win_spatial_size = \ + _infer_spatial_size(img_sizes, win_sizes) + + sampling_prior_map = None + try: + sampling_prior_map = data.get('sampler', None) + except AttributeError: + pass + + n_samples = max(n_samples, 1) + window_centres = self.window_centers_sampler( + n_samples, img_spatial_size, win_spatial_size, sampling_prior_map) + assert window_centres.shape == (n_samples, N_SPATIAL), \ + "the coordinates generator should return " \ + "{} samples of rank {} locations".format(n_samples, N_SPATIAL) + + # adjust spatial coordinates based on each mod spatial window size + all_coordinates = {} + for mod in list(win_sizes): + win_size = np.asarray(win_sizes[mod][:N_SPATIAL]) + half_win = np.floor(win_size / 2.0).astype(int) + + # Make starting coordinates of the window + spatial_coords = np.zeros( + (n_samples, N_SPATIAL * 2), dtype=np.int32) + spatial_coords[:, :N_SPATIAL] = np.maximum( + window_centres[:, :N_SPATIAL] - half_win[:N_SPATIAL], 0) + + # Make the opposite corner of the window is + # just adding the mod specific window size + spatial_coords[:, N_SPATIAL:] = \ + spatial_coords[:, :N_SPATIAL] + win_size[:N_SPATIAL] + assert np.all(spatial_coords[:, N_SPATIAL:] <= img_spatial_size), \ + 'spatial coords: out of bounds.' + + # include subject id as the 1st column of all_coordinates values + subject_id = np.ones((n_samples,), dtype=np.int32) * subject_id + spatial_coords = np.append( + subject_id[:, None], spatial_coords, axis=1) + all_coordinates[mod] = spatial_coords + + return all_coordinates + + +def rand_spatial_coordinates( + n_samples, img_spatial_size, win_spatial_size, sampler_map): + """ + Generate spatial coordinates from a discrete uniform distribution. + + :param n_samples: number of random coordinates to generate + :param img_spatial_size: input image size + :param win_spatial_size: input window size + :param sampler_map: sampling prior map (not in use) + :return: (n_samples, N_SPATIAL) coordinates representing sampling + window centres relative to img_spatial_size + """ + tf.logging.debug('uniform sampler, prior %s ignored', sampler_map) + + # Sample coordinates at random + half_win = np.floor(np.asarray(win_spatial_size) / 2.0).astype(np.int32) + max_coords = np.zeros((n_samples, N_SPATIAL), dtype=np.int32) + for (idx, (img, win)) in enumerate( + zip(img_spatial_size[:N_SPATIAL], win_spatial_size[:N_SPATIAL])): + max_coords[:, idx] = np.random.randint( + 0, max(img - win + 1, 1), n_samples) + max_coords[:, :N_SPATIAL] = \ + max_coords[:, :N_SPATIAL] + half_win[:N_SPATIAL] + return max_coords + + +def _infer_spatial_size(img_sizes, win_sizes): + """ + Utility function to find the spatial size of image, + and the largest spatial window size across input sections. + + Raises NotImplementedError if the images have + different spatial dimensions. + + :param img_sizes: dictionary of {'input_name': (img_size_x, img_size,y,...)} + :param win_sizes: dictionary of {'input_name': (win_size_x, win_size_y,...)} + :return: (image_spatial_size, window_largest_spatial_size) + """ + uniq_spatial_size = \ + set([img_size[:N_SPATIAL] for img_size in list(img_sizes.values())]) + if len(uniq_spatial_size) != 1: + tf.logging.fatal("Don't know how to generate sampling " + "locations: Spatial dimensions of the " + "grouped input sources are not " + "consistent. %s", uniq_spatial_size) + raise NotImplementedError + img_spatial_size = np.asarray(uniq_spatial_size.pop(), dtype=np.int32) + + # find the largest spatial window across input sections + _win_spatial_sizes = \ + [win_size[:N_SPATIAL] for win_size in win_sizes.values()] + _win_spatial_sizes = np.asarray(_win_spatial_sizes, dtype=np.int32) + win_spatial_size = np.max(_win_spatial_sizes, axis=0) + + assert all([img_spatial_size[i] >= win_spatial_size[i] + for i in range(N_SPATIAL)]), \ + "window size {} is larger than image size {}".format( + win_spatial_size, img_spatial_size) + + return img_spatial_size, win_spatial_size diff --git a/niftynet/contrib/csv_reader/sampler_weighted_v2_csv.py b/niftynet/contrib/csv_reader/sampler_weighted_v2_csv.py new file mode 100755 index 00000000..978ba7e9 --- /dev/null +++ b/niftynet/contrib/csv_reader/sampler_weighted_v2_csv.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +""" +Generating image window by weighted sampling map from input image +This can also be considered as a "weighted random cropping" layer of the +input image. +""" +from __future__ import absolute_import, division, print_function + +import numpy as np +import tensorflow as tf + +from niftynet.contrib.csv_reader.sampler_uniform_v2_csv import UniformSamplerCSV +from niftynet.engine.image_window import N_SPATIAL + + +class WeightedSamplerCSV(UniformSamplerCSV): + """ + This class generators samples from a user provided + frequency map for each input volume + The sampling likelihood of each voxel (and window around) + is proportional to its frequency + + This is implemented in a closed form using cumulative histograms + for efficiency purposes i.e., the first three dims of image. + + This layer can be considered as a "weighted random cropping" layer of the + input image. + """ + + def __init__(self, + reader, + csv_reader, + window_sizes, + batch_size=1, + windows_per_image=1, + queue_length=10, + name='weighted_sampler'): + UniformSamplerCSV.__init__(self, + reader=reader, + csv_reader=csv_reader, + window_sizes=window_sizes, + batch_size=batch_size, + windows_per_image=windows_per_image, + queue_length=queue_length, + name=name) + tf.logging.info('Initialised weighted sampler window instance') + self.window_centers_sampler = weighted_spatial_coordinates + + +def weighted_spatial_coordinates( + n_samples, img_spatial_size, win_spatial_size, sampler_map): + """ + Weighted sampling from a map. + This function uses a cumulative histogram for fast sampling. + + see also `sampler_uniform.rand_spatial_coordinates` + + :param n_samples: number of random coordinates to generate + :param img_spatial_size: input image size + :param win_spatial_size: input window size + :param sampler_map: sampling prior map, it's spatial shape should be + consistent with `img_spatial_size` + :return: (n_samples, N_SPATIAL) coordinates representing sampling + window centres relative to img_spatial_size + """ + assert sampler_map is not None, \ + 'sampling prior map is not specified, ' \ + 'please check `sampler=` option in the config.' + # Get the cumulative sum of the normalised sorted intensities + # i.e. first sort the sampling frequencies, normalise them + # to sum to one, and then accumulate them in order + assert np.all(img_spatial_size[:N_SPATIAL] == + sampler_map.shape[:N_SPATIAL]), \ + 'image and sampling map shapes do not match' + win_spatial_size = np.asarray(win_spatial_size, dtype=np.int32) + cropped_map = crop_sampling_map(sampler_map, win_spatial_size) + flatten_map = cropped_map.flatten() + flatten_map = flatten_map - np.min(flatten_map) + normaliser = flatten_map.sum() + # get the sorting indexes to that we can invert the sorting later on. + sorted_indexes = np.argsort(flatten_map) + sorted_data = np.cumsum( + np.true_divide(flatten_map[sorted_indexes], normaliser)) + + middle_coords = np.zeros((n_samples, N_SPATIAL), dtype=np.int32) + for sample in range(0, n_samples): + # get n_sample from the cumulative histogram, spaced by 1/n_samples, + # plus a random perturbation to give us a stochastic sampler + sample_ratio = 1 - (np.random.random() + sample) / (n_samples + 1) + # find the index where the cumulative it above the sample threshold + try: + if normaliser == 0: + # constant map? reducing to a uniform sampling + sample_index = np.random.randint(len(sorted_data)) + else: + sample_index = np.argmax(sorted_data >= sample_ratio) + except ValueError: + tf.logging.fatal("unable to choose sampling window based on " + "the current frequency map.") + raise + # invert the sample index to the pre-sorted index + inverted_sample_index = sorted_indexes[sample_index] + # get the x,y,z coordinates on the cropped_map + middle_coords[sample, :N_SPATIAL] = np.unravel_index( + inverted_sample_index, cropped_map.shape)[:N_SPATIAL] + + # re-shift coords due to the crop + half_win = np.floor(win_spatial_size / 2).astype(np.int32) + middle_coords[:, :N_SPATIAL] = \ + middle_coords[:, :N_SPATIAL] + half_win[:N_SPATIAL] + return middle_coords + + +def crop_sampling_map(input_map, win_spatial_size): + """ + Utility function for generating a cropped version of the + input sampling prior map (the input weight map where the centre of + the window might be). If the centre of the window was outside of + this crop area, the patch would be outside of the field of view + + :param input_map: the input weight map where the centre of + the window might be + :param win_spatial_size: size of the borders to be cropped + :return: cropped sampling map + """ + + # prepare cropping indices + _start, _end = [], [] + for win_size, img_size in \ + zip(win_spatial_size[:N_SPATIAL], input_map.shape[:N_SPATIAL]): + # cropping floor of the half window + d_start = int(win_size / 2.0) + # using ceil of half window + d_end = img_size - win_size + int(win_size / 2.0 + 0.6) + + _start.append(d_start) + _end.append(d_end + 1 if d_start == d_end else d_end) + + try: + assert len(_start) == 3 + cropped_map = input_map[ + _start[0]:_end[0], _start[1]:_end[1], _start[2]:_end[2], 0, 0] + assert np.all(cropped_map.shape) > 0 + except (IndexError, KeyError, TypeError, AssertionError): + tf.logging.fatal( + "incompatible map: %s and window size: %s\n" + "try smaller (fully-specified) spatial window sizes?", + input_map.shape, win_spatial_size) + raise + return cropped_map diff --git a/niftynet/contrib/csv_reader/segmentation_application_patchsampler.py b/niftynet/contrib/csv_reader/segmentation_application_patchsampler.py new file mode 100755 index 00000000..171c892b --- /dev/null +++ b/niftynet/contrib/csv_reader/segmentation_application_patchsampler.py @@ -0,0 +1,413 @@ +# -*- coding: utf-8 -*- +import tensorflow as tf + +from niftynet.application.base_application import BaseApplication +from niftynet.engine.application_factory import \ + ApplicationNetFactory, InitializerFactory, OptimiserFactory +from niftynet.engine.application_variables import \ + CONSOLE, NETWORK_OUTPUT, TF_SUMMARIES +from niftynet.engine.sampler_grid_v2 import GridSampler +from niftynet.engine.sampler_resize_v2 import ResizeSampler +from niftynet.engine.sampler_uniform_v2 import UniformSampler +from niftynet.engine.sampler_weighted_v2 import WeightedSampler +from niftynet.engine.sampler_balanced_v2 import BalancedSampler +from niftynet.engine.windows_aggregator_grid import GridSamplesAggregator +from niftynet.engine.windows_aggregator_resize import ResizeSamplesAggregator +from niftynet.io.image_reader import ImageReader +from niftynet.layer.binary_masking import BinaryMaskingLayer +from niftynet.layer.discrete_label_normalisation import \ + DiscreteLabelNormalisationLayer +from niftynet.layer.histogram_normalisation import \ + HistogramNormalisationLayer +from niftynet.layer.loss_segmentation import LossFunction +from niftynet.layer.mean_variance_normalisation import \ + MeanVarNormalisationLayer +from niftynet.layer.pad import PadLayer +from niftynet.layer.post_processing import PostProcessingLayer +from niftynet.layer.rand_flip import RandomFlipLayer +from niftynet.layer.rand_rotation import RandomRotationLayer +from niftynet.layer.rand_spatial_scaling import RandomSpatialScalingLayer +from niftynet.evaluation.segmentation_evaluator import SegmentationEvaluator +from niftynet.layer.rand_elastic_deform import RandomElasticDeformationLayer +from niftynet.contrib.csv_reader.csv_reader import CSVReader +from niftynet.contrib.csv_reader.sampler_csvpatch import CSVPatchSampler + +SUPPORTED_INPUT = set(['image', 'label', 'weight', 'sampler', 'inferred']) + + +class SegmentationApplicationPatchSampler(BaseApplication): + REQUIRED_CONFIG_SECTION = "SEGMENTATION" + + def __init__(self, net_param, action_param, action): + super(SegmentationApplicationPatchSampler, self).__init__() + tf.logging.info('starting segmentation application') + self.action = action + + self.net_param = net_param + self.action_param = action_param + + self.data_param = None + self.segmentation_param = None + self.SUPPORTED_SAMPLING = { + 'patch': (self.initialise_csvsampler, + self.initialise_grid_sampler, + self.initialise_grid_aggregator), + 'uniform': (self.initialise_uniform_sampler, + self.initialise_grid_sampler, + self.initialise_grid_aggregator), + 'weighted': (self.initialise_weighted_sampler, + self.initialise_grid_sampler, + self.initialise_grid_aggregator), + 'resize': (self.initialise_resize_sampler, + self.initialise_resize_sampler, + self.initialise_resize_aggregator), + 'balanced': (self.initialise_balanced_sampler, + self.initialise_grid_sampler, + self.initialise_grid_aggregator), + } + + def initialise_dataset_loader( + self, data_param=None, task_param=None, data_partitioner=None): + + self.data_param = data_param + self.segmentation_param = task_param + + # initialise input image readers + if self.is_training: + reader_names = ('image', 'label', 'weight') + csv_reader_names = ('sampler',) + elif self.is_inference: + # in the inference process use `image` input only + reader_names = ('image',) + elif self.is_evaluation: + reader_names = ('image', 'label', 'inferred') + else: + tf.logging.fatal( + 'Action `%s` not supported. Expected one of %s', + self.action, self.SUPPORTED_PHASES) + raise ValueError + try: + reader_phase = self.action_param.dataset_to_infer + except AttributeError: + reader_phase = None + file_lists = data_partitioner.get_file_lists_by( + phase=reader_phase, action=self.action) + self.readers = [ + ImageReader(reader_names).initialise( + data_param, task_param, file_list) for file_list in file_lists] + self.csv_readers = [ + CSVReader(csv_reader_names).initialise( + data_param, task_param, file_list) for file_list in file_lists] + + # initialise input preprocessing layers + foreground_masking_layer = BinaryMaskingLayer( + type_str=self.net_param.foreground_type, + multimod_fusion=self.net_param.multimod_foreground_type, + threshold=0.0) \ + if self.net_param.normalise_foreground_only else None + mean_var_normaliser = MeanVarNormalisationLayer( + image_name='image', binary_masking_func=foreground_masking_layer) \ + if self.net_param.whitening else None + histogram_normaliser = HistogramNormalisationLayer( + image_name='image', + modalities=vars(task_param).get('image'), + model_filename=self.net_param.histogram_ref_file, + binary_masking_func=foreground_masking_layer, + norm_type=self.net_param.norm_type, + cutoff=self.net_param.cutoff, + name='hist_norm_layer') \ + if (self.net_param.histogram_ref_file and + self.net_param.normalisation) else None + label_normalisers = None + if self.net_param.histogram_ref_file and \ + task_param.label_normalisation: + label_normalisers = [DiscreteLabelNormalisationLayer( + image_name='label', + modalities=vars(task_param).get('label'), + model_filename=self.net_param.histogram_ref_file)] + if self.is_evaluation: + label_normalisers.append( + DiscreteLabelNormalisationLayer( + image_name='inferred', + modalities=vars(task_param).get('inferred'), + model_filename=self.net_param.histogram_ref_file)) + label_normalisers[-1].key = label_normalisers[0].key + + normalisation_layers = [] + if histogram_normaliser is not None: + normalisation_layers.append(histogram_normaliser) + if mean_var_normaliser is not None: + normalisation_layers.append(mean_var_normaliser) + if task_param.label_normalisation and \ + (self.is_training or not task_param.output_prob): + normalisation_layers.extend(label_normalisers) + + volume_padding_layer = [] + if self.net_param.volume_padding_size: + volume_padding_layer.append(PadLayer( + image_name=SUPPORTED_INPUT, + border=self.net_param.volume_padding_size, + mode=self.net_param.volume_padding_mode)) + + # initialise training data augmentation layers + augmentation_layers = [] + if self.is_training: + train_param = self.action_param + if train_param.random_flipping_axes != -1: + augmentation_layers.append(RandomFlipLayer( + flip_axes=train_param.random_flipping_axes)) + if train_param.scaling_percentage: + augmentation_layers.append(RandomSpatialScalingLayer( + min_percentage=train_param.scaling_percentage[0], + max_percentage=train_param.scaling_percentage[1])) + if train_param.rotation_angle or \ + train_param.rotation_angle_x or \ + train_param.rotation_angle_y or \ + train_param.rotation_angle_z: + rotation_layer = RandomRotationLayer() + if train_param.rotation_angle: + rotation_layer.init_uniform_angle( + train_param.rotation_angle) + else: + rotation_layer.init_non_uniform_angle( + train_param.rotation_angle_x, + train_param.rotation_angle_y, + train_param.rotation_angle_z) + augmentation_layers.append(rotation_layer) + if train_param.do_elastic_deformation: + spatial_rank = list(self.readers[0].spatial_ranks.values())[0] + augmentation_layers.append(RandomElasticDeformationLayer( + spatial_rank=spatial_rank, + num_controlpoints=train_param.num_ctrl_points, + std_deformation_sigma=train_param.deformation_sigma, + proportion_to_augment=train_param.proportion_to_deform)) + + # only add augmentation to first reader (not validation reader) + self.readers[0].add_preprocessing_layers( + volume_padding_layer + normalisation_layers + augmentation_layers) + + for reader in self.readers[1:]: + reader.add_preprocessing_layers( + volume_padding_layer + normalisation_layers) + + def initialise_csvsampler(self): + self.sampler = [[CSVPatchSampler(reader=image_reader, + csv_reader=csv_reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + windows_per_image=self.action_param.sample_per_volume, + queue_length=self.net_param.queue_length + ) + for image_reader, csv_reader in + zip(self.readers, self.csv_readers)]] + + def initialise_uniform_sampler(self): + self.sampler = [[UniformSampler( + reader=reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + windows_per_image=self.action_param.sample_per_volume, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_weighted_sampler(self): + self.sampler = [[WeightedSampler( + reader=reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + windows_per_image=self.action_param.sample_per_volume, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_resize_sampler(self): + self.sampler = [[ResizeSampler( + reader=reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + shuffle=self.is_training, + smaller_final_batch_mode=self.net_param.smaller_final_batch_mode, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_grid_sampler(self): + self.sampler = [[GridSampler( + reader=reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + spatial_window_size=self.action_param.spatial_window_size, + window_border=self.action_param.border, + smaller_final_batch_mode=self.net_param.smaller_final_batch_mode, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_balanced_sampler(self): + self.sampler = [[BalancedSampler( + reader=reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + windows_per_image=self.action_param.sample_per_volume, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_grid_aggregator(self): + self.output_decoder = GridSamplesAggregator( + image_reader=self.readers[0], + output_path=self.action_param.save_seg_dir, + window_border=self.action_param.border, + interp_order=self.action_param.output_interp_order, + postfix=self.action_param.output_postfix) + + def initialise_resize_aggregator(self): + self.output_decoder = ResizeSamplesAggregator( + image_reader=self.readers[0], + output_path=self.action_param.save_seg_dir, + window_border=self.action_param.border, + interp_order=self.action_param.output_interp_order, + postfix=self.action_param.output_postfix) + + def initialise_sampler(self): + if self.is_training: + self.SUPPORTED_SAMPLING[self.net_param.window_sampling][0]() + elif self.is_inference: + self.SUPPORTED_SAMPLING[self.net_param.window_sampling][1]() + + def initialise_aggregator(self): + self.SUPPORTED_SAMPLING[self.net_param.window_sampling][2]() + + def initialise_network(self): + w_regularizer = None + b_regularizer = None + reg_type = self.net_param.reg_type.lower() + decay = self.net_param.decay + if reg_type == 'l2' and decay > 0: + from tensorflow.contrib.layers.python.layers import regularizers + w_regularizer = regularizers.l2_regularizer(decay) + b_regularizer = regularizers.l2_regularizer(decay) + elif reg_type == 'l1' and decay > 0: + from tensorflow.contrib.layers.python.layers import regularizers + w_regularizer = regularizers.l1_regularizer(decay) + b_regularizer = regularizers.l1_regularizer(decay) + + self.net = ApplicationNetFactory.create(self.net_param.name)( + num_classes=self.segmentation_param.num_classes, + w_initializer=InitializerFactory.get_initializer( + name=self.net_param.weight_initializer), + b_initializer=InitializerFactory.get_initializer( + name=self.net_param.bias_initializer), + w_regularizer=w_regularizer, + b_regularizer=b_regularizer, + acti_func=self.net_param.activation_function) + + def connect_data_and_network(self, + outputs_collector=None, + gradients_collector=None): + + def switch_sampler(for_training): + with tf.name_scope('train' if for_training else 'validation'): + sampler = self.get_sampler()[0][0 if for_training else -1] + return sampler.pop_batch_op() + + if self.is_training: + if self.action_param.validation_every_n > 0: + data_dict = tf.cond(tf.logical_not(self.is_validation), + lambda: switch_sampler(for_training=True), + lambda: switch_sampler(for_training=False)) + else: + data_dict = switch_sampler(for_training=True) + + image = tf.cast(data_dict['image'], tf.float32) + net_args = {'is_training': self.is_training, + 'keep_prob': self.net_param.keep_prob} + net_out = self.net(image, **net_args) + + with tf.name_scope('Optimiser'): + optimiser_class = OptimiserFactory.create( + name=self.action_param.optimiser) + self.optimiser = optimiser_class.get_instance( + learning_rate=self.action_param.lr) + loss_func = LossFunction( + n_class=self.segmentation_param.num_classes, + loss_type=self.action_param.loss_type, + softmax=self.segmentation_param.softmax) + data_loss = loss_func( + prediction=net_out, + ground_truth=data_dict.get('label', None), + weight_map=data_dict.get('weight', None)) + reg_losses = tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES) + if self.net_param.decay > 0.0 and reg_losses: + reg_loss = tf.reduce_mean( + [tf.reduce_mean(reg_loss) for reg_loss in reg_losses]) + loss = data_loss + reg_loss + else: + loss = data_loss + grads = self.optimiser.compute_gradients( + loss, colocate_gradients_with_ops=True) + # collecting gradients variables + gradients_collector.add_to_collection([grads]) + # collecting output variables + outputs_collector.add_to_collection( + var=data_loss, name='loss', + average_over_devices=False, collection=CONSOLE) + outputs_collector.add_to_collection( + var=data_loss, name='loss', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + + # outputs_collector.add_to_collection( + # var=image*180.0, name='image', + # average_over_devices=False, summary_type='image3_sagittal', + # collection=TF_SUMMARIES) + + # outputs_collector.add_to_collection( + # var=image, name='image', + # average_over_devices=False, + # collection=NETWORK_OUTPUT) + + # outputs_collector.add_to_collection( + # var=tf.reduce_mean(image), name='mean_image', + # average_over_devices=False, summary_type='scalar', + # collection=CONSOLE) + elif self.is_inference: + # converting logits into final output for + # classification probabilities or argmax classification labels + data_dict = switch_sampler(for_training=False) + image = tf.cast(data_dict['image'], tf.float32) + net_args = {'is_training': self.is_training, + 'keep_prob': self.net_param.keep_prob} + net_out = self.net(image, **net_args) + + output_prob = self.segmentation_param.output_prob + num_classes = self.segmentation_param.num_classes + if output_prob and num_classes > 1: + post_process_layer = PostProcessingLayer( + 'SOFTMAX', num_classes=num_classes) + elif not output_prob and num_classes > 1: + post_process_layer = PostProcessingLayer( + 'ARGMAX', num_classes=num_classes) + else: + post_process_layer = PostProcessingLayer( + 'IDENTITY', num_classes=num_classes) + net_out = post_process_layer(net_out) + + outputs_collector.add_to_collection( + var=net_out, name='window', + average_over_devices=False, collection=NETWORK_OUTPUT) + outputs_collector.add_to_collection( + var=data_dict['image_location'], name='location', + average_over_devices=False, collection=NETWORK_OUTPUT) + self.initialise_aggregator() + + def interpret_output(self, batch_output): + if self.is_inference: + return self.output_decoder.decode_batch( + batch_output['window'], batch_output['location']) + return True + + def initialise_evaluator(self, eval_param): + self.eval_param = eval_param + self.evaluator = SegmentationEvaluator(self.readers[0], + self.segmentation_param, + eval_param) + + def add_inferred_output(self, data_param, task_param): + return self.add_inferred_output_like(data_param, task_param, 'label') diff --git a/niftynet/contrib/csv_reader/toynet_features.py b/niftynet/contrib/csv_reader/toynet_features.py new file mode 100755 index 00000000..1f487469 --- /dev/null +++ b/niftynet/contrib/csv_reader/toynet_features.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function + +from niftynet.layer.convolution import ConvolutionalLayer +from niftynet.network.base_net import BaseNet + + +class ToyNetFeat(BaseNet): + def __init__(self, + num_classes, + w_initializer=None, + w_regularizer=None, + b_initializer=None, + b_regularizer=None, + acti_func='prelu', + name='ToyNet'): + + super(ToyNetFeat, self).__init__( + num_classes=num_classes, + w_initializer=w_initializer, + w_regularizer=w_regularizer, + b_initializer=b_initializer, + b_regularizer=b_regularizer, + acti_func=acti_func, + name=name) + + self.hidden_features = 10 + + def layer_op(self, images, is_training=True, **unused_kwargs): + conv_1 = ConvolutionalLayer(self.hidden_features, + kernel_size=3, + w_initializer=self.initializers['w'], + w_regularizer=self.regularizers['w'], + b_initializer=self.initializers['b'], + b_regularizer=self.regularizers['b'], + acti_func='relu', + name='conv_input') + + # conv_2 = ConvolutionalLayer(self.num_classes, + # kernel_size=1, + # w_initializer=self.initializers['w'], + # w_regularizer=self.regularizers['w'], + # b_initializer=self.initializers['b'], + # b_regularizer=self.regularizers['b'], + # acti_func=None, + # name='conv_output') + + flow = conv_1(images, is_training) + # flow = conv_2(flow, is_training) + return flow diff --git a/niftynet/contrib/deep_boosted_regression/README.md b/niftynet/contrib/deep_boosted_regression/README.md new file mode 100644 index 00000000..cd40e129 --- /dev/null +++ b/niftynet/contrib/deep_boosted_regression/README.md @@ -0,0 +1,7 @@ +# Deep Boosted Regression + +In order to train a neural network as in [1] you can add your data to a folder such as `/home/foo/data/` and then run the following line + +`python net_run.py train -c niftynet/contrib/deep_boosted_regression/net_DBR.ini -a niftynet.contrib.deep_boosted_regression.regression_rec_application.RegressionRecApplication` + +[1] Kläser K. et al. (2018) Deep Boosted Regression for MR to CT Synthesis. In: Gooya A., Goksel O., Oguz I., Burgos N. (eds) Simulation and Synthesis in Medical Imaging. SASHIMI 2018. Lecture Notes in Computer Science, vol 11037 \ No newline at end of file diff --git a/niftynet/contrib/deep_boosted_regression/__init__.py b/niftynet/contrib/deep_boosted_regression/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/niftynet/contrib/deep_boosted_regression/net_DBR.ini b/niftynet/contrib/deep_boosted_regression/net_DBR.ini new file mode 100755 index 00000000..14cf195a --- /dev/null +++ b/niftynet/contrib/deep_boosted_regression/net_DBR.ini @@ -0,0 +1,83 @@ + +[T1] +spatial_window_size=(56, 56, 56) +filename_contains=() +filename_not_contains=() +path_to_search=/home/foo/data/ +interp_order=3 + +[T2] +spatial_window_size=(56, 56, 56) +filename_contains=() +filename_not_contains=() +path_to_search=/home/foo/data/ +interp_order=3 + +[CT] +spatial_window_size=(56, 56, 56) +filename_contains=() +filename_not_contains=() +path_to_search=/home/foo/data/ +interp_order=3 + +[SAMPWEIGHT] +spatial_window_size=(56,56,56) +filename_contains=() +filename_not_contains=() +path_to_search=/home/foo/data/sampweight/ +interp_order=3 + +[TRAINING] +loss_type=RMSE +sample_per_volume=32 +tensorboard_every_n=10 +max_iter=40000 +save_every_n=1000 +max_checkpoints=100 +optimiser=adam +lr=0.001 +starting_iter=0 +rotation_angle = (-10.0, 10.0) +scaling_percentage = (-10.0, 10.0) +random_flipping_axes= 1 +validation_every_n=200 +validation_max_iter=20 +exclude_fraction_for_validation=0.1 +exclude_fraction_for_inference=0.2 + +[NETWORK] +cutoff=(0.01, 0.99) +multimod_foreground_type=and +volume_padding_size=(16, 16, 16) +name=highres3dnet +decay=0.00000001 +activation_function=prelu +normalise_foreground_only=False +histogram_ref_file=/home/foo/model/ +batch_size=1 +norm_type=percentile +foreground_type=otsu_plus +window_sampling=weighted +whitening=True +reg_type=L2 +normalisation=False + +[INFERENCE] +border=(16, 16, 16) +output_interp_order=3 +inference_iter=36000 +save_seg_dir=/home/foo/output/ +spatial_window_size=(144, 144, 144) + +[SYSTEM] +cuda_devices="" +num_gpus=1 +num_threads=2 +queue_length=5 +model_dir=/home/foo/model/ + +[REGRESSION] +output=CT +image=T1, T2 +sampler=SAMPWEIGHT +loss_border=8 diff --git a/niftynet/contrib/deep_boosted_regression/regression_rec_application.py b/niftynet/contrib/deep_boosted_regression/regression_rec_application.py new file mode 100755 index 00000000..d1f1b053 --- /dev/null +++ b/niftynet/contrib/deep_boosted_regression/regression_rec_application.py @@ -0,0 +1,356 @@ +import tensorflow as tf +import copy + +from niftynet.application.base_application import BaseApplication +from niftynet.engine.application_factory import ApplicationNetFactory +from niftynet.engine.application_factory import OptimiserFactory +from niftynet.engine.application_variables import CONSOLE +from niftynet.engine.application_variables import NETWORK_OUTPUT +from niftynet.engine.application_variables import TF_SUMMARIES +from niftynet.engine.sampler_grid_v2 import GridSampler +from niftynet.engine.sampler_resize_v2 import ResizeSampler +from niftynet.engine.sampler_uniform_v2 import UniformSampler +from niftynet.engine.sampler_weighted_v2 import WeightedSampler +from niftynet.engine.windows_aggregator_grid import GridSamplesAggregator +from niftynet.engine.windows_aggregator_resize import ResizeSamplesAggregator +from niftynet.io.image_reader import ImageReader +from niftynet.layer.crop import CropLayer +from niftynet.layer.histogram_normalisation import \ + HistogramNormalisationLayer +from niftynet.layer.loss_regression import LossFunction +from niftynet.layer.mean_variance_normalisation import \ + MeanVarNormalisationLayer +from niftynet.layer.pad import PadLayer +from niftynet.layer.post_processing import PostProcessingLayer +from niftynet.layer.rand_flip import RandomFlipLayer +from niftynet.layer.rand_rotation import RandomRotationLayer +from niftynet.layer.rand_spatial_scaling import RandomSpatialScalingLayer + +SUPPORTED_INPUT = set(['image', 'output', 'weight', 'sampler']) + + +class RegressionRecApplication(BaseApplication): + REQUIRED_CONFIG_SECTION = "REGRESSION" + + def __init__(self, net_param, action_param, action): + BaseApplication.__init__(self) + tf.logging.info('starting recursive regression application') + self.action = action + + self.net_param = net_param + self.net2_param = copy.deepcopy(net_param) + self.action_param = action_param + self.regression_param = None + + self.data_param = None + self.SUPPORTED_SAMPLING = { + 'uniform': (self.initialise_uniform_sampler, + self.initialise_grid_sampler, + self.initialise_grid_aggregator), + 'weighted': (self.initialise_weighted_sampler, + self.initialise_grid_sampler, + self.initialise_grid_aggregator), + 'resize': (self.initialise_resize_sampler, + self.initialise_resize_sampler, + self.initialise_resize_aggregator), + } + + def initialise_dataset_loader( + self, data_param=None, task_param=None, data_partitioner=None): + self.data_param = data_param + self.regression_param = task_param + + # read each line of csv files into an instance of Subject + if self.is_training: + file_lists = [] + if self.action_param.validation_every_n > 0: + file_lists.append(data_partitioner.train_files) + file_lists.append(data_partitioner.validation_files) + else: + file_lists.append(data_partitioner.train_files) + + self.readers = [] + for file_list in file_lists: + reader = ImageReader(SUPPORTED_INPUT) + reader.initialise(data_param, task_param, file_list) + self.readers.append(reader) + else: + inference_reader = ImageReader(['image']) + file_list = data_partitioner.inference_files + inference_reader.initialise(data_param, task_param, file_list) + self.readers = [inference_reader] + + mean_var_normaliser = MeanVarNormalisationLayer( + image_name='image') + histogram_normaliser = None + if self.net_param.histogram_ref_file: + histogram_normaliser = HistogramNormalisationLayer( + image_name='image', + modalities=vars(task_param).get('image'), + model_filename=self.net_param.histogram_ref_file, + norm_type=self.net_param.norm_type, + cutoff=self.net_param.cutoff, + name='hist_norm_layer') + + normalisation_layers = [] + if self.net_param.normalisation: + normalisation_layers.append(histogram_normaliser) + if self.net_param.whitening: + normalisation_layers.append(mean_var_normaliser) + + augmentation_layers = [] + if self.is_training: + if self.action_param.random_flipping_axes != -1: + augmentation_layers.append(RandomFlipLayer( + flip_axes=self.action_param.random_flipping_axes)) + if self.action_param.scaling_percentage: + augmentation_layers.append(RandomSpatialScalingLayer( + min_percentage=self.action_param.scaling_percentage[0], + max_percentage=self.action_param.scaling_percentage[1])) + if self.action_param.rotation_angle: + augmentation_layers.append(RandomRotationLayer()) + augmentation_layers[-1].init_uniform_angle( + self.action_param.rotation_angle) + + volume_padding_layer = [] + if self.net_param.volume_padding_size: + volume_padding_layer.append(PadLayer( + image_name=SUPPORTED_INPUT, + border=self.net_param.volume_padding_size)) + for reader in self.readers: + reader.add_preprocessing_layers(volume_padding_layer + + normalisation_layers + + augmentation_layers) + + def initialise_uniform_sampler(self): + self.sampler = [[UniformSampler( + reader=reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + windows_per_image=self.action_param.sample_per_volume, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_weighted_sampler(self): + self.sampler = [[WeightedSampler( + reader=reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + windows_per_image=self.action_param.sample_per_volume, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_resize_sampler(self): + self.sampler = [[ResizeSampler( + reader=reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + shuffle=self.is_training, + smaller_final_batch_mode=self.net_param.smaller_final_batch_mode, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_grid_sampler(self): + self.sampler = [[GridSampler( + reader=reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + spatial_window_size=self.action_param.spatial_window_size, + window_border=self.action_param.border, + smaller_final_batch_mode=self.net_param.smaller_final_batch_mode, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_balanced_sampler(self): + self.sampler = [[BalancedSampler( + reader=reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + windows_per_image=self.action_param.sample_per_volume, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_grid_aggregator(self): + self.output_decoder = GridSamplesAggregator( + image_reader=self.readers[0], + output_path=self.action_param.save_seg_dir, + window_border=self.action_param.border, + interp_order=self.action_param.output_interp_order) + + def initialise_resize_aggregator(self): + self.output_decoder = ResizeSamplesAggregator( + image_reader=self.readers[0], + output_path=self.action_param.save_seg_dir, + window_border=self.action_param.border, + interp_order=self.action_param.output_interp_order) + + def initialise_sampler(self): + if self.is_training: + self.SUPPORTED_SAMPLING[self.net_param.window_sampling][0]() + else: + self.SUPPORTED_SAMPLING[self.net_param.window_sampling][1]() + + def initialise_network(self): + w_regularizer = None + b_regularizer = None + reg_type = self.net_param.reg_type.lower() + decay = self.net_param.decay + if reg_type == 'l2' and decay > 0: + from tensorflow.contrib.layers.python.layers import regularizers + w_regularizer = regularizers.l2_regularizer(decay) + b_regularizer = regularizers.l2_regularizer(decay) + elif reg_type == 'l1' and decay > 0: + from tensorflow.contrib.layers.python.layers import regularizers + w_regularizer = regularizers.l1_regularizer(decay) + b_regularizer = regularizers.l1_regularizer(decay) + + self.net = ApplicationNetFactory.create(self.net_param.name)( + num_classes=1, + w_regularizer=w_regularizer, + b_regularizer=b_regularizer, + acti_func=self.net_param.activation_function) + + self.net2 = ApplicationNetFactory.create(self.net2_param.name)( + num_classes=1, + w_regularizer=w_regularizer, + b_regularizer=b_regularizer, + acti_func=self.net2_param.activation_function) + + def connect_data_and_network(self, + outputs_collector=None, + gradients_collector=None): + + def switch_sampler(for_training): + with tf.name_scope('train' if for_training else 'validation'): + sampler = self.get_sampler()[0][0 if for_training else -1] + return sampler.pop_batch_op() + + if self.is_training: + if self.action_param.validation_every_n > 0: + data_dict = tf.cond(tf.logical_not(self.is_validation), + lambda: switch_sampler(True), + lambda: switch_sampler(False)) + else: + data_dict = switch_sampler(for_training=True) + + image = tf.cast(data_dict['image'], tf.float32) + pct1_out = self.net(image, self.is_training) + res2_out = self.net2(tf.concat([image, pct1_out],4), self.is_training) + pct2_out = tf.add(pct1_out,res2_out) + res3_out = self.net2(tf.concat([image, pct2_out],4), self.is_training) + pct3_out = tf.add(pct2_out,res3_out) + #res4_out = self.net2(tf.concat([image, pct3_out],4), self.is_training) + #pct4_out = tf.add(pct3_out,res4_out) + #net_out = self.net(image, is_training=self.is_training) + with tf.name_scope('Optimiser'): + optimiser_class = OptimiserFactory.create( + name=self.action_param.optimiser) + self.optimiser = optimiser_class.get_instance( + learning_rate=self.action_param.lr) + loss_func = LossFunction( + loss_type=self.action_param.loss_type) + + crop_layer = CropLayer( + border=self.regression_param.loss_border, name='crop-88') + + data_loss1 = loss_func( + prediction=crop_layer(pct1_out), + ground_truth=crop_layer(data_dict.get('output', None)), + weight_map=None if data_dict.get('weight', None) is None else crop_layer(data_dict.get('weight', None))) + data_loss2 = loss_func( + prediction=crop_layer(pct2_out), + ground_truth=crop_layer(data_dict.get('output', None)), + weight_map=None if data_dict.get('weight', None) is None else crop_layer(data_dict.get('weight', None))) + data_loss3 = loss_func( + prediction=crop_layer(pct3_out), + ground_truth=crop_layer(data_dict.get('output', None)), + weight_map=None if data_dict.get('weight', None) is None else crop_layer(data_dict.get('weight', None))) + + #prediction = crop_layer(net_out) + #ground_truth = crop_layer(data_dict.get('output', None)) + #weight_map = None if data_dict.get('weight', None) is None \ + #else crop_layer(data_dict.get('weight', None)) + #data_loss = loss_func(prediction=prediction, + #ground_truth=ground_truth, + #weight_map=weight_map) + + reg_losses = tf.get_collection( + tf.GraphKeys.REGULARIZATION_LOSSES) + if self.net_param.decay > 0.0 and reg_losses: + reg_loss = tf.reduce_mean( + [tf.reduce_mean(reg_loss) for reg_loss in reg_losses]) + loss = reg_loss + data_loss1 + data_loss2 + data_loss3 + else: + loss = data_loss1 + data_loss2 + data_loss3 + grads = self.optimiser.compute_gradients(loss) + # collecting gradients variables + gradients_collector.add_to_collection([grads]) + # collecting output variables + outputs_collector.add_to_collection( + var=loss, name='Loss', + average_over_devices=False, collection=CONSOLE) + outputs_collector.add_to_collection( + var=data_loss1, name='data_loss1', + average_over_devices=True, + collection=CONSOLE) + outputs_collector.add_to_collection( + var=data_loss2, name='data_loss2', + average_over_devices=True, + collection=CONSOLE) + outputs_collector.add_to_collection( + var=data_loss3, name='data_loss3', + average_over_devices=True, + collection=CONSOLE) + outputs_collector.add_to_collection( + var=data_loss1, name='data_loss1', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + outputs_collector.add_to_collection( + var=data_loss2, name='data_loss2', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + outputs_collector.add_to_collection( + var=data_loss3, name='data_loss3', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + outputs_collector.add_to_collection( + var=loss, name='LossSum', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) +# outputs_collector.add_to_collection( +# var=pct3_out, name="pct3_out", +# average_over_devices=True, summary_type="image3_axial", +# collection=TF_SUMMARIES) + + else: + data_dict = switch_sampler(for_training=False) + image = tf.cast(data_dict['image'], tf.float32) + #net_out = self.net(image, is_training=self.is_training) + pct1_out = self.net(image, self.is_training) + res2_out = self.net2(tf.concat([image, pct1_out],4), self.is_training) + pct2_out = tf.add(pct1_out,res2_out) + res3_out = self.net2(tf.concat([image, pct2_out],4), self.is_training) + pct3_out = tf.add(pct2_out,res3_out) + res4_out = self.net2(tf.concat([image, pct3_out],4), self.is_training) + pct4_out = tf.add(pct3_out,res4_out) + crop_layer = CropLayer(border=0, name='crop-88') + post_process_layer = PostProcessingLayer('IDENTITY') + net_out = post_process_layer(crop_layer(pct4_out)) + + outputs_collector.add_to_collection( + var=net_out, name='window', + average_over_devices=False, collection=NETWORK_OUTPUT) + outputs_collector.add_to_collection( + var=data_dict['image_location'], name='location', + average_over_devices=False, collection=NETWORK_OUTPUT) + init_aggregator = \ + self.SUPPORTED_SAMPLING[self.net_param.window_sampling][2] + init_aggregator() + + def interpret_output(self, batch_output): + if not self.is_training: + return self.output_decoder.decode_batch( + {'window_image': batch_output['window']}, + batch_output['location']) + else: + return True diff --git a/niftynet/contrib/layer/resampler_optional_niftyreg.py b/niftynet/contrib/layer/resampler_optional_niftyreg.py new file mode 100644 index 00000000..1da7ad33 --- /dev/null +++ b/niftynet/contrib/layer/resampler_optional_niftyreg.py @@ -0,0 +1,32 @@ +# Flag stating whether C++/CUDA image resampling is available +HAS_NIFTYREG_RESAMPLING = False + +try: + from niftyreg_image_resampling import NiftyregImageResamplingLayer + import niftyreg_image_resampling as resampler_module + + ResamplerOptionalNiftyRegLayer = NiftyregImageResamplingLayer + + HAS_NIFTYREG_RESAMPLING = True +except ImportError: + import tensorflow as tf + + tf.logging.warning(''' + niftyreg_image_resampling is not installed; falling back onto + niftynet.layer.resampler.ResamplerLayer. To install + niftyreg_image_resampling please see + niftynet/contrib/niftyreg_image_resampling/README.md + ''') + + from niftynet.layer.resampler import ResamplerLayer + import niftynet.layer.resampler as resampler_module + + ResamplerOptionalNiftyRegLayer = ResamplerLayer + + +# Passthrough of supported boundary types +SUPPORTED_BOUNDARY = resampler_module.SUPPORTED_BOUNDARY + + +# Passthrough of supported interpolation types +SUPPORTED_INTERPOLATION = resampler_module.SUPPORTED_INTERPOLATION diff --git a/niftynet/contrib/multi_output/__init__.py b/niftynet/contrib/multi_output/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/niftynet/contrib/multi_output/multi_output_test.py b/niftynet/contrib/multi_output/multi_output_test.py new file mode 100644 index 00000000..19df9ff8 --- /dev/null +++ b/niftynet/contrib/multi_output/multi_output_test.py @@ -0,0 +1,324 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function + +import tensorflow as tf + +from niftynet.application.base_application import BaseApplication +from niftynet.engine.application_factory import \ + ApplicationNetFactory, InitializerFactory, OptimiserFactory +from niftynet.io.image_reader import ImageReader +from niftynet.engine.sampler_grid_v2 import GridSampler +from niftynet.engine.sampler_resize_v2 import ResizeSampler +from niftynet.engine.sampler_uniform_v2 import UniformSampler +from niftynet.engine.sampler_weighted_v2 import WeightedSampler +from niftynet.engine.sampler_balanced_v2 import BalancedSampler +from niftynet.engine.windows_aggregator_grid import GridSamplesAggregator +from niftynet.engine.windows_aggregator_resize import ResizeSamplesAggregator +from niftynet.engine.windows_aggregator_identity import WindowAsImageAggregator +# from niftynet.engine.windows_aggregator_classifier import ClassifierSamplesAggregator +from niftynet.layer.loss_segmentation import LossFunction +from niftynet.layer.post_processing import PostProcessingLayer +from niftynet.engine.application_variables import CONSOLE, TF_SUMMARIES, NETWORK_OUTPUT + + +SUPPORTED_INPUT = set(['image', 'label', 'weight', 'sampler', 'inferred']) + + +class MultiOutputApplication(BaseApplication): + REQUIRED_CONFIG_SECTION = "SEGMENTATION" + + def __init__(self, net_param, action_param, action): + BaseApplication.__init__(self) + tf.logging.info('starting multioutput test') + self.action = action + + self.net_param = net_param + self.action_param = action_param + + self.data_param = None + self.multioutput_param = None + + self.SUPPORTED_SAMPLING = { + 'uniform': (self.initialise_uniform_sampler, + self.initialise_grid_sampler, + self.initialise_grid_aggregator), + 'weighted': (self.initialise_weighted_sampler, + self.initialise_grid_sampler, + self.initialise_grid_aggregator), + 'resize': (self.initialise_resize_sampler, + self.initialise_resize_sampler, + self.initialise_resize_aggregator), + 'classifier': (self.initialise_resize_sampler, + self.initialise_resize_sampler, + self.initialise_classifier_aggregator), + 'identity': (self.initialise_uniform_sampler, + self.initialise_resize_sampler, + self.initialise_identity_aggregator) + } + + def initialise_dataset_loader( + self, data_param=None, task_param=None, data_partitioner=None): + + self.data_param = data_param + self.multioutput_param = task_param + + # initialise input image readers + if self.is_training: + reader_names = ('image', 'label', 'weight', 'sampler') + elif self.is_inference: + # in the inference process use `image` input only + reader_names = ('image',) + elif self.is_evaluation: + reader_names = ('image', 'label', 'inferred') + else: + tf.logging.fatal( + 'Action `%s` not supported. Expected one of %s', + self.action, self.SUPPORTED_PHASES) + raise ValueError + try: + reader_phase = self.action_param.dataset_to_infer + except AttributeError: + reader_phase = None + file_lists = data_partitioner.get_file_lists_by( + phase=reader_phase, action=self.action) + self.readers = [ + ImageReader(reader_names).initialise( + data_param, task_param, file_list) for file_list in file_lists] + + def initialise_uniform_sampler(self): + self.sampler = [[UniformSampler( + reader=reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + windows_per_image=self.action_param.sample_per_volume, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_weighted_sampler(self): + self.sampler = [[WeightedSampler( + reader=reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + windows_per_image=self.action_param.sample_per_volume, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_resize_sampler(self): + self.sampler = [[ResizeSampler( + reader=reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + shuffle=self.is_training, + smaller_final_batch_mode=self.net_param.smaller_final_batch_mode, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_grid_sampler(self): + self.sampler = [[GridSampler( + reader=reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + spatial_window_size=self.action_param.spatial_window_size, + window_border=self.action_param.border, + smaller_final_batch_mode=self.net_param.smaller_final_batch_mode, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_balanced_sampler(self): + self.sampler = [[BalancedSampler( + reader=reader, + window_sizes=self.data_param, + batch_size=self.net_param.batch_size, + windows_per_image=self.action_param.sample_per_volume, + queue_length=self.net_param.queue_length) for reader in + self.readers]] + + def initialise_grid_aggregator(self): + self.output_decoder = GridSamplesAggregator( + image_reader=self.readers[0], + output_path=self.action_param.save_seg_dir, + window_border=self.action_param.border, + interp_order=self.action_param.output_interp_order, + postfix=self.action_param.output_postfix, + fill_constant=self.action_param.fill_constant) + + def initialise_resize_aggregator(self): + self.output_decoder = ResizeSamplesAggregator( + image_reader=self.readers[0], + output_path=self.action_param.save_seg_dir, + window_border=self.action_param.border, + interp_order=self.action_param.output_interp_order, + postfix=self.action_param.output_postfix) + + def initialise_identity_aggregator(self): + self.output_decoder = WindowAsImageAggregator( + image_reader=self.readers[0], + output_path=self.action_param.save_seg_dir, + postfix=self.action_param.output_postfix) + + def initialise_classifier_aggregator(self): + pass + # self.output_decoder = ClassifierSamplesAggregator( + # image_reader=self.readers[0], + # output_path=self.action_param.save_seg_dir, + # postfix=self.action_param.output_postfix) + + def initialise_sampler(self): + if self.is_training: + self.SUPPORTED_SAMPLING[self.net_param.window_sampling][0]() + elif self.is_inference: + self.SUPPORTED_SAMPLING[self.net_param.window_sampling][1]() + + def initialise_aggregator(self): + self.SUPPORTED_SAMPLING[self.net_param.window_sampling][2]() + + def initialise_network(self): + w_regularizer = None + b_regularizer = None + reg_type = self.net_param.reg_type.lower() + decay = self.net_param.decay + if reg_type == 'l2' and decay > 0: + from tensorflow.contrib.layers.python.layers import regularizers + w_regularizer = regularizers.l2_regularizer(decay) + b_regularizer = regularizers.l2_regularizer(decay) + elif reg_type == 'l1' and decay > 0: + from tensorflow.contrib.layers.python.layers import regularizers + w_regularizer = regularizers.l1_regularizer(decay) + b_regularizer = regularizers.l1_regularizer(decay) + + self.net = ApplicationNetFactory.create('toynet')( + num_classes=self.multioutput_param.num_classes, + w_initializer=InitializerFactory.get_initializer( + name=self.net_param.weight_initializer), + b_initializer=InitializerFactory.get_initializer( + name=self.net_param.bias_initializer), + w_regularizer=w_regularizer, + b_regularizer=b_regularizer, + acti_func=self.net_param.activation_function) + + def connect_data_and_network(self, + outputs_collector=None, + gradients_collector=None): + + def switch_sampler(for_training): + with tf.name_scope('train' if for_training else 'validation'): + sampler = self.get_sampler()[0][0 if for_training else -1] + return sampler.pop_batch_op() + + if self.is_training: + # extract data + if self.action_param.validation_every_n > 0: + data_dict = tf.cond(tf.logical_not(self.is_validation), + lambda: switch_sampler(for_training=True), + lambda: switch_sampler(for_training=False)) + else: + data_dict = switch_sampler(for_training=True) + + image = tf.cast(data_dict['image'], tf.float32) + net_args = {'is_training': self.is_training, + 'keep_prob': self.net_param.keep_prob} + net_out = self.net(image, **net_args) + + with tf.name_scope('Optimiser'): + optimiser_class = OptimiserFactory.create( + name=self.action_param.optimiser) + self.optimiser = optimiser_class.get_instance( + learning_rate=self.action_param.lr) + + loss_func = LossFunction( + n_class=self.multioutput_param.num_classes, + loss_type=self.action_param.loss_type, + softmax=self.multioutput_param.softmax) + data_loss = loss_func( + prediction=net_out, + ground_truth=data_dict.get('label', None), + weight_map=data_dict.get('weight', None)) + reg_losses = tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES) + if self.net_param.decay > 0.0 and reg_losses: + reg_loss = tf.reduce_mean( + [tf.reduce_mean(reg_loss) for reg_loss in reg_losses]) + loss = data_loss + reg_loss + else: + loss = data_loss + + # set the optimiser and the gradient + to_optimise = tf.trainable_variables() + vars_to_freeze = \ + self.action_param.vars_to_freeze or \ + self.action_param.vars_to_restore + if vars_to_freeze: + import re + var_regex = re.compile(vars_to_freeze) + # Only optimise vars that are not frozen + to_optimise = \ + [v for v in to_optimise if not var_regex.search(v.name)] + tf.logging.info( + "Optimizing %d out of %d trainable variables, " + "the other variables fixed (--vars_to_freeze %s)", + len(to_optimise), + len(tf.trainable_variables()), + vars_to_freeze) + + grads = self.optimiser.compute_gradients( + loss, var_list=to_optimise, colocate_gradients_with_ops=True) + + # collecting gradients variables + gradients_collector.add_to_collection([grads]) + # collecting output variables + outputs_collector.add_to_collection( + var=data_loss, name='loss', + average_over_devices=False, collection=CONSOLE) + outputs_collector.add_to_collection( + var=data_loss, name='loss', + average_over_devices=True, summary_type='scalar', + collection=TF_SUMMARIES) + + elif self.is_inference: + + data_dict = switch_sampler(for_training=False) + image = tf.cast(data_dict['image'], tf.float32) + net_args = {'is_training': self.is_training, + 'keep_prob': self.net_param.keep_prob} + net_out = self.net(image, **net_args) + + num_classes = self.multioutput_param.num_classes + argmax_layer = PostProcessingLayer( + 'ARGMAX', num_classes=num_classes) + softmax_layer = PostProcessingLayer( + 'SOFTMAX', num_classes=num_classes) + + arg_max_out = argmax_layer(net_out) + soft_max_out = softmax_layer(net_out) + # sum_prob_out = tf.reshape(tf.reduce_sum(soft_max_out),[1,1]) + # min_prob_out = tf.reshape(tf.reduce_min(soft_max_out),[1,1]) + sum_prob_out = tf.reduce_sum(soft_max_out) + min_prob_out = tf.reduce_min(soft_max_out) + + outputs_collector.add_to_collection( + var=arg_max_out, name='window_argmax', + average_over_devices=False, collection=NETWORK_OUTPUT) + outputs_collector.add_to_collection( + var=soft_max_out, name='window_softmax', + average_over_devices=False, collection=NETWORK_OUTPUT) + outputs_collector.add_to_collection( + var=sum_prob_out, name='csv_sum', + average_over_devices=False, collection=NETWORK_OUTPUT) + outputs_collector.add_to_collection( + var=min_prob_out, name='csv_min', + average_over_devices=False, collection=NETWORK_OUTPUT) + outputs_collector.add_to_collection( + var=data_dict['image_location'], name='location', + average_over_devices=False, collection=NETWORK_OUTPUT) + self.initialise_aggregator() + + def interpret_output(self, batch_output): + if self.is_inference: + return self.output_decoder.decode_batch( + {'window_argmax': batch_output['window_argmax'], + 'window_softmax': batch_output['window_softmax'], + 'csv_sum': batch_output['csv_sum'], + 'csv_min': batch_output['csv_min']}, + batch_output['location']) + return True + + diff --git a/niftynet/contrib/niftyreg_image_resampling/.gitignore b/niftynet/contrib/niftyreg_image_resampling/.gitignore new file mode 100644 index 00000000..b25c15b8 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/.gitignore @@ -0,0 +1 @@ +*~ diff --git a/niftynet/contrib/niftyreg_image_resampling/LICENSE b/niftynet/contrib/niftyreg_image_resampling/LICENSE new file mode 100644 index 00000000..0e5af657 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2009, University College London, United-Kingdom +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +Neither the name of the University College London nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/niftynet/contrib/niftyreg_image_resampling/README.md b/niftynet/contrib/niftyreg_image_resampling/README.md new file mode 100644 index 00000000..e80cfde8 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/README.md @@ -0,0 +1,25 @@ +# NiftyNet GPU Image Resampling Module + +## Purpose and Scope + +This module provides a faster implementation of image resampling. For most usage scenarios, it is a drop-in replacement for niftynet.layer.resampler.ResamplerLayer, however, its feature set is limited to: + +* ZERO (zero-padding), REPLICATE (clamping of intensities at edges), and SYMMETRIC (mirroring) boundaries +* NEAREST (constant), LINEAR, and BSPLINE (cubic spline) interpolation. +* Differentiation with respect to the floating image is a CPU-only operation. + +To provide compatibility where this module is not installed, the following module can be used: niftynet.contrib.layer.resampler_optional_niftyreg.ResamplerOptionalNiftyRegLayer. This module will try to load NiftyregImageResamplingLayer, if that fails, it defaults to niftynet.layer.resampler.ResamplerLayer. + +## Building and Installing + +Building and installing is performed as usual via the setup file. + +Building the module requires that a CUDA toolkit and CMake be installed, and nvcc and cmake can be found on the executables search path. +CMake variables can be overriden through the `override` command and `--settings`/`-s` switch as a list of colon (':') separated variable-value pairs. E.g., `python setup.py override -s "CMAKE_CXX_COMPILER:/usr/bin/g++-6:CMAKE_C_COMPILER:/usr/bin/gcc-6" build`. + +Building was only tested with CUDA 9.0/gcc 5.4 and gcc 6.4, on Ubuntu 16.04 and 18.04. + +## Acknowledgements + +The image resampling code contained in this module is heavily based on code extracted from NiftyReg (https://sourceforge.net/projects/niftyreg/). + diff --git a/niftynet/contrib/niftyreg_image_resampling/__init__.py b/niftynet/contrib/niftyreg_image_resampling/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/CMakeLists.txt b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/CMakeLists.txt new file mode 100644 index 00000000..855a4ac5 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/CMakeLists.txt @@ -0,0 +1,64 @@ +project(NiftyNet_gpu_resampling) +cmake_minimum_required(VERSION 3.5) + +set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules;${CMAKE_MODULE_PATH}") + +set(GPU_RESAMPLING_CONFIGFILE_DIR ${CMAKE_CURRENT_BINARY_DIR} CACHE STRING "Destination directory for configured files.") + +add_definitions("-DGOOGLE_CUDA") +set(GPU_RESAMPLING_LIB_TARGET niftyreg_image_resampling_ops) +set(CMAKE_CXX_STANDARD 11) +# This may not be portable (almost certainly isn't)! +# -DNDEBUG: a constexpr applied to function returning std::string is causing the build to fail; the code appears to be inside an assert, so disabling asserts "solves" the problem. +set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -std=c++11 --expt-relaxed-constexpr -DNDEBUG --disable-warnings") + +find_package(CUDA) +find_package(Tensorflow REQUIRED) + +include_directories(nifti) +include_directories(SYSTEM "${Tensorflow_INCLUDE_DIRS}") +link_directories("${Tensorflow_LIBRARY_DIRS}") +set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} ${Tensorflow_CFLAGS}") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${Tensorflow_CFLAGS}") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${Tensorflow_CFLAGS}") +set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS}") + +set(GPU_RESAMPLING_CXX_SRC + _reg_resampling.cpp + _reg_tools.cpp + _reg_maths.cpp + + niftyreg_cpu_resample_op.cpp + niftyreg_cpu_resample_gradient_op.cpp + niftyreg_cpu_resample_image_gradient_op.cpp + + nifti/nifti1_io.c + nifti/znzlib.c + ) + +if (CUDA_FOUND) + set(GPU_RESAMPLING_CU_SRC + resampleKernel.cu + _reg_common_cuda.cu + _reg_resampling_gpu.cu + + niftyreg_gpu_resample_op.cu + niftyreg_gpu_resample_gradient_op.cu + ) + + cuda_add_library(${GPU_RESAMPLING_LIB_TARGET} SHARED + ${GPU_RESAMPLING_CXX_SRC} + ${GPU_RESAMPLING_CU_SRC} + ) +else () + message(WARNING "No CUDA toolkit was found, if your tensorflow install was built with CUDA support, the build will fail.") + add_library(${GPU_RESAMPLING_LIB_TARGET} SHARED + ${GPU_RESAMPLING_CXX_SRC} + ) +endif (CUDA_FOUND) + +set_target_properties(${GPU_RESAMPLING_LIB_TARGET} PROPERTIES PREFIX "") +target_link_libraries(${GPU_RESAMPLING_LIB_TARGET} + ${Tensorflow_LIBRARIES} + ) +file(GENERATE OUTPUT "${GPU_RESAMPLING_CONFIGFILE_DIR}/niftyreg_module_loader.py" INPUT "${CMAKE_CURRENT_SOURCE_DIR}/../niftyreg_module_loader.py.in") diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_common_cuda.cu b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_common_cuda.cu new file mode 100755 index 00000000..c5bba8aa --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_common_cuda.cu @@ -0,0 +1,693 @@ +/** + * @file _reg_comon_gpu.cu + * @author Marc Modat + * @date 25/03/2009 + * Copyright (c) 2009, University College London. All rights reserved. + * Centre for Medical Image Computing (CMIC) + * See the LICENSE.txt file in the nifty_reg root folder + * + */ + +#ifndef _REG_COMMON_GPU_CU +#define _REG_COMMON_GPU_CU + +#include "_reg_common_cuda.h" +#include "_reg_tools.h" +/* ******************************** */ +void cudaCommon_computeGridConfiguration(dim3 &r_blocks, dim3 &r_grid, const int targetVoxelNumber) { + unsigned int maxThreads = 256; + unsigned int maxBlocks = 65365; + unsigned int blocks = (targetVoxelNumber % maxThreads) ? (targetVoxelNumber / maxThreads) + 1 : targetVoxelNumber / maxThreads; + + blocks = (std::min)(blocks, maxBlocks); + + r_grid = dim3(blocks, 1, 1); + r_blocks = dim3(maxThreads, 1, 1); +} +/* ******************************** */ +/* ******************************** */ +template +int cudaCommon_transferNiftiToNiftiOnDevice1(nifti_image **image_d, nifti_image *img) { + + const unsigned int memSize = img->dim[1] * img->dim[2] * img->dim[3] * sizeof(NIFTI_TYPE); + + int *g_dim; + float* g_pixdim; + NIFTI_TYPE* g_data; + + NR_CUDA_SAFE_CALL(cudaMalloc((void**)&g_dim, 8 * sizeof(int))); + NR_CUDA_SAFE_CALL(cudaMalloc((void**)&g_pixdim, 8 * sizeof(float))); + NR_CUDA_SAFE_CALL(cudaMalloc((void**)&g_data, memSize)); + + NIFTI_TYPE *array_h = static_cast( img->data ); + NR_CUDA_SAFE_CALL(cudaMemcpy(( *image_d ), img, sizeof(nifti_image), cudaMemcpyHostToDevice)); + + NR_CUDA_SAFE_CALL(cudaMemcpy((*image_d)->data, array_h, memSize, cudaMemcpyHostToDevice)); + NR_CUDA_SAFE_CALL(cudaMemcpy(( *image_d )->dim, img->dim, 8 * sizeof(int), cudaMemcpyHostToDevice)); + NR_CUDA_SAFE_CALL(cudaMemcpy(( *image_d )->pixdim, img->pixdim, 8 * sizeof(float), cudaMemcpyHostToDevice)); + + return EXIT_SUCCESS; +} +template int cudaCommon_transferNiftiToNiftiOnDevice1(nifti_image **image_d, nifti_image *img); +template int cudaCommon_transferNiftiToNiftiOnDevice1(nifti_image **image_d, nifti_image *img); +/* ******************************** */ +/* ******************************** */ +template +int cudaCommon_transferNiftiToArrayOnDevice1(DTYPE **array_d, const nifti_image *img) +{ + if(sizeof(DTYPE)!=sizeof(NIFTI_TYPE)){ + reg_print_fct_error("cudaCommon_transferNiftiToArrayOnDevice1"); + reg_print_msg_error("The host and device arrays are of different types"); + return EXIT_FAILURE; + } + else{ + const unsigned int memSize = img->nvox*sizeof(DTYPE); + const NIFTI_TYPE *array_h=static_cast(img->data); + NR_CUDA_SAFE_CALL(cudaMemcpy(*array_d, array_h, memSize, cudaMemcpyHostToDevice)); + } + return EXIT_SUCCESS; +} +/* ******************************** */ +template +int cudaCommon_transferNiftiToArrayOnDevice(DTYPE **array_d, const nifti_image *img) +{ + if( sizeof(DTYPE)==sizeof(float4) ){ + if( (img->datatype!=NIFTI_TYPE_FLOAT32) || (img->dim[5]<2) || (img->dim[4]>1)){ + reg_print_fct_error("cudaCommon_transferNiftiToArrayOnDevice"); + reg_print_msg_error("The specified image is not a single precision deformation field image"); + return EXIT_FAILURE; + } + const float *niftiImgValues = static_cast(img->data); + float4 *array_h=(float4 *)calloc(img->nx*img->ny*img->nz,sizeof(float4)); + const int voxelNumber = img->nx*img->ny*img->nz; + for(int i=0; idim[5]>=2){ + for(int i=0; idim[5]>=3){ + for(int i=0; idim[5]>=4){ + for(int i=0; inx*img->ny*img->nz*sizeof(float4), cudaMemcpyHostToDevice)); + free(array_h); + } + else{ // All these else could be removed but the nvcc compiler would warn for unreachable statement + switch(img->datatype){ + case NIFTI_TYPE_FLOAT32: + return cudaCommon_transferNiftiToArrayOnDevice1(array_d, img); + default: + reg_print_fct_error("cudaCommon_transferNiftiToArrayOnDevice"); + reg_print_msg_error("The image data type is not supported"); + return EXIT_FAILURE; + } + } + return EXIT_SUCCESS; +} +template int cudaCommon_transferNiftiToArrayOnDevice(double **, const nifti_image *); +template int cudaCommon_transferNiftiToArrayOnDevice(float **, const nifti_image *); +template int cudaCommon_transferNiftiToArrayOnDevice(int **, const nifti_image *); +template int cudaCommon_transferNiftiToArrayOnDevice(float4 **, const nifti_image *); +/* ******************************** */ + +template +int cudaCommon_transferNiftiToArrayOnDevice1(DTYPE **array_d, DTYPE **array2_d, nifti_image *img) +{ + if(sizeof(DTYPE)!=sizeof(NIFTI_TYPE)){ + reg_print_fct_error("cudaCommon_transferNiftiToArrayOnDevice1"); + reg_print_msg_error("The host and device arrays are of different types"); + return EXIT_FAILURE; + } + else{ + const unsigned int memSize = img->dim[1] * img->dim[2] * img->dim[3] * sizeof(DTYPE); + NIFTI_TYPE *array_h=static_cast(img->data); + NIFTI_TYPE *array2_h=&array_h[img->dim[1] * img->dim[2] * img->dim[3]]; + NR_CUDA_SAFE_CALL(cudaMemcpy(*array_d, array_h, memSize, cudaMemcpyHostToDevice)); + NR_CUDA_SAFE_CALL(cudaMemcpy(*array2_d, array2_h, memSize, cudaMemcpyHostToDevice)); + } + return EXIT_SUCCESS; +} +/* ******************************** */ +template +int cudaCommon_transferNiftiToArrayOnDevice(DTYPE **array_d, DTYPE **array2_d, nifti_image *img) +{ + if(sizeof(DTYPE)==sizeof(float4) ){ + if( (img->datatype!=NIFTI_TYPE_FLOAT32) || (img->dim[5]<2) || (img->dim[4]>1)){ + reg_print_fct_error("cudaCommon_transferNiftiToArrayOnDevice"); + reg_print_msg_error("The specified image is not a single precision deformation field image"); + return EXIT_FAILURE; + } + float *niftiImgValues = static_cast(img->data); + float4 *array_h=(float4 *)calloc(img->nx*img->ny*img->nz,sizeof(float4)); + float4 *array2_h=(float4 *)calloc(img->nx*img->ny*img->nz,sizeof(float4)); + const int voxelNumber = img->nx*img->ny*img->nz; + for(int i=0; idim[5]>=2){ + for(int i=0; idim[5]>=3){ + for(int i=0; idim[5]>=4){ + for(int i=0; inx*img->ny*img->nz*sizeof(float4), cudaMemcpyHostToDevice)); + NR_CUDA_SAFE_CALL(cudaMemcpy(*array2_d, array2_h, img->nx*img->ny*img->nz*sizeof(float4), cudaMemcpyHostToDevice)); + free(array_h); + free(array2_h); + } + else{ // All these else could be removed but the nvcc compiler would warn for unreachable statement + switch(img->datatype){ + case NIFTI_TYPE_FLOAT32: + return cudaCommon_transferNiftiToArrayOnDevice1(array_d, array2_d, img); + default: + reg_print_fct_error("cudaCommon_transferNiftiToArrayOnDevice"); + reg_print_msg_error("The image data type is not supported"); + return EXIT_FAILURE; + } + } + return EXIT_SUCCESS; +} +template int cudaCommon_transferNiftiToArrayOnDevice(float **,float **, nifti_image *); +template int cudaCommon_transferNiftiToArrayOnDevice(double **,double **, nifti_image *); +template int cudaCommon_transferNiftiToArrayOnDevice(float4 **,float4 **, nifti_image *); // for deformation field +/* ******************************** */ +/* ******************************** */ +template +int cudaCommon_transferNiftiToArrayOnDevice1(cudaArray **cuArray_d, nifti_image *img) +{ + if(sizeof(DTYPE)!=sizeof(NIFTI_TYPE)){ + reg_print_fct_error("cudaCommon_transferNiftiToArrayOnDevice1"); + reg_print_msg_error("The host and device arrays are of different types"); + return EXIT_FAILURE; + } + else{ + NIFTI_TYPE *array_h=static_cast(img->data); + + cudaMemcpy3DParms copyParams; memset(©Params, 0, sizeof(copyParams)); + copyParams.extent = make_cudaExtent(img->dim[1], img->dim[2], img->dim[3]); + copyParams.srcPtr = make_cudaPitchedPtr((void *) array_h, + copyParams.extent.width*sizeof(DTYPE), + copyParams.extent.width, + copyParams.extent.height); + copyParams.dstArray = *cuArray_d; + copyParams.kind = cudaMemcpyHostToDevice; + NR_CUDA_SAFE_CALL(cudaMemcpy3D(©Params)); + } + return EXIT_SUCCESS; +} +/* ******************************** */ +template +int cudaCommon_transferNiftiToArrayOnDevice(cudaArray **cuArray_d, nifti_image *img) +{ + if( sizeof(DTYPE)==sizeof(float4) ){ + if( (img->datatype!=NIFTI_TYPE_FLOAT32) || (img->dim[5]<2) || (img->dim[4]>1) ){ + reg_print_fct_error("cudaCommon_transferNiftiToArrayOnDevice"); + reg_print_msg_error("The specified image is not a single precision deformation field image"); + return EXIT_FAILURE; + } + float *niftiImgValues = static_cast(img->data); + float4 *array_h=(float4 *)calloc(img->nx*img->ny*img->nz,sizeof(float4)); + + for(int i=0; inx*img->ny*img->nz; i++) + array_h[i].x= *niftiImgValues++; + if(img->dim[5]>=2) + { + for(int i=0; inx*img->ny*img->nz; i++) + array_h[i].y= *niftiImgValues++; + } + if(img->dim[5]>=3) + { + for(int i=0; inx*img->ny*img->nz; i++) + array_h[i].z= *niftiImgValues++; + } + if(img->dim[5]==3) + { + for(int i=0; inx*img->ny*img->nz; i++) + array_h[i].w= *niftiImgValues++; + } + cudaMemcpy3DParms copyParams; memset(©Params, 0, sizeof(copyParams)); + copyParams.extent = make_cudaExtent(img->dim[1], img->dim[2], img->dim[3]); + copyParams.srcPtr = make_cudaPitchedPtr((void *) array_h, + copyParams.extent.width*sizeof(DTYPE), + copyParams.extent.width, + copyParams.extent.height); + copyParams.dstArray = *cuArray_d; + copyParams.kind = cudaMemcpyHostToDevice; + NR_CUDA_SAFE_CALL(cudaMemcpy3D(©Params)) + free(array_h); + } + else{ // All these else could be removed but the nvcc compiler would warn for unreachable statement + switch(img->datatype){ + case NIFTI_TYPE_FLOAT32: + return cudaCommon_transferNiftiToArrayOnDevice1(cuArray_d, img); + default: + reg_print_fct_error("cudaCommon_transferNiftiToArrayOnDevice"); + reg_print_msg_error("The image data type is not supported"); + return EXIT_FAILURE; + } + } + return EXIT_SUCCESS; +} +template int cudaCommon_transferNiftiToArrayOnDevice(cudaArray **, nifti_image *); +template int cudaCommon_transferNiftiToArrayOnDevice(cudaArray **, nifti_image *); +template int cudaCommon_transferNiftiToArrayOnDevice(cudaArray **, nifti_image *); +template int cudaCommon_transferNiftiToArrayOnDevice(cudaArray **, nifti_image *); // for deformation field +/* ******************************** */ +/* ******************************** */ +template +int cudaCommon_transferNiftiToArrayOnDevice1(cudaArray **cuArray_d, cudaArray **cuArray2_d, nifti_image *img) +{ + if(sizeof(DTYPE)!=sizeof(NIFTI_TYPE)){ + reg_print_fct_error("cudaCommon_transferNiftiToArrayOnDevice1"); + reg_print_msg_error("The host and device arrays are of different types"); + return EXIT_FAILURE; + } + else{ + NIFTI_TYPE *array_h = static_cast(img->data); + NIFTI_TYPE *array2_h = &array_h[img->dim[1]*img->dim[2]*img->dim[3]]; + + cudaMemcpy3DParms copyParams; memset(©Params, 0, sizeof(copyParams)); + copyParams.extent = make_cudaExtent(img->dim[1], img->dim[2], img->dim[3]); + copyParams.kind = cudaMemcpyHostToDevice; + // First timepoint + copyParams.srcPtr = make_cudaPitchedPtr((void *) array_h, + copyParams.extent.width*sizeof(DTYPE), + copyParams.extent.width, + copyParams.extent.height); + copyParams.dstArray = *cuArray_d; + NR_CUDA_SAFE_CALL(cudaMemcpy3D(©Params)); + // Second timepoint + copyParams.srcPtr = make_cudaPitchedPtr((void *) array2_h, + copyParams.extent.width*sizeof(DTYPE), + copyParams.extent.width, + copyParams.extent.height); + copyParams.dstArray = *cuArray2_d; + NR_CUDA_SAFE_CALL(cudaMemcpy3D(©Params)); + } + return EXIT_SUCCESS; +} +/* ******************************** */ +template +int cudaCommon_transferNiftiToArrayOnDevice(cudaArray **cuArray_d, cudaArray **cuArray2_d, nifti_image *img) +{ + if( sizeof(DTYPE)==sizeof(float4) ){ + if( (img->datatype!=NIFTI_TYPE_FLOAT32) || (img->dim[5]<2) || (img->dim[4]>1) ) + { + reg_print_fct_error("cudaCommon_transferNiftiToArrayOnDevice1"); + reg_print_msg_error("The specified image is not a single precision deformation field image"); + return EXIT_FAILURE; + } + float *niftiImgValues = static_cast(img->data); + float4 *array_h=(float4 *)calloc(img->nx*img->ny*img->nz,sizeof(float4)); + float4 *array2_h=(float4 *)calloc(img->nx*img->ny*img->nz,sizeof(float4)); + + for(int i=0; inx*img->ny*img->nz; i++) + array_h[i].x= *niftiImgValues++; + for(int i=0; inx*img->ny*img->nz; i++) + array2_h[i].x= *niftiImgValues++; + + if(img->dim[5]>=2){ + for(int i=0; inx*img->ny*img->nz; i++) + array_h[i].y= *niftiImgValues++; + for(int i=0; inx*img->ny*img->nz; i++) + array2_h[i].y= *niftiImgValues++; + } + + if(img->dim[5]>=3){ + for(int i=0; inx*img->ny*img->nz; i++) + array_h[i].z= *niftiImgValues++; + for(int i=0; inx*img->ny*img->nz; i++) + array2_h[i].z= *niftiImgValues++; + } + + if(img->dim[5]==3){ + for(int i=0; inx*img->ny*img->nz; i++) + array_h[i].w= *niftiImgValues++; + for(int i=0; inx*img->ny*img->nz; i++) + array2_h[i].w= *niftiImgValues++; + } + + cudaMemcpy3DParms copyParams; memset(©Params, 0, sizeof(copyParams)); + copyParams.extent = make_cudaExtent(img->dim[1], img->dim[2], img->dim[3]); + copyParams.kind = cudaMemcpyHostToDevice; + // First timepoint + copyParams.srcPtr = make_cudaPitchedPtr((void *) array_h, + copyParams.extent.width*sizeof(DTYPE), + copyParams.extent.width, + copyParams.extent.height); + copyParams.dstArray = *cuArray_d; + NR_CUDA_SAFE_CALL(cudaMemcpy3D(©Params)); + free(array_h); + // Second timepoint + copyParams.srcPtr = make_cudaPitchedPtr((void *) array2_h, + copyParams.extent.width*sizeof(DTYPE), + copyParams.extent.width, + copyParams.extent.height); + copyParams.dstArray = *cuArray2_d; + NR_CUDA_SAFE_CALL(cudaMemcpy3D(©Params)); + free(array2_h); + } + else{ // All these else could be removed but the nvcc compiler would warn for unreachable statement + switch(img->datatype){ + case NIFTI_TYPE_FLOAT32: + return cudaCommon_transferNiftiToArrayOnDevice1(cuArray_d, cuArray2_d, img); + default: + reg_print_fct_error("cudaCommon_transferNiftiToArrayOnDevice1"); + reg_print_msg_error("The image data type is not supported"); + return EXIT_FAILURE; + } + } + return EXIT_SUCCESS; +} +template int cudaCommon_transferNiftiToArrayOnDevice(cudaArray **, cudaArray **, nifti_image *); +template int cudaCommon_transferNiftiToArrayOnDevice(cudaArray **, cudaArray **, nifti_image *); +template int cudaCommon_transferNiftiToArrayOnDevice(cudaArray **, cudaArray **, nifti_image *); // for deformation field +/* ******************************** */ +/* ******************************** */ +template +int cudaCommon_allocateArrayToDevice(cudaArray **cuArray_d, int *dim) +{ + const cudaExtent volumeSize = make_cudaExtent(dim[1], dim[2], dim[3]); + cudaChannelFormatDesc texDesc = cudaCreateChannelDesc(); + NR_CUDA_SAFE_CALL(cudaMalloc3DArray(cuArray_d, &texDesc, volumeSize)); + return EXIT_SUCCESS; +}template int cudaCommon_allocateArrayToDevice(cudaArray **, int *); +template int cudaCommon_allocateArrayToDevice(cudaArray **, int *); +template int cudaCommon_allocateArrayToDevice(cudaArray **, int *); // for deformation field +/* ******************************** */ +/* ******************************** */ +template +int cudaCommon_allocateArrayToDevice(cudaArray **cuArray_d, cudaArray **cuArray2_d, int *dim) +{ + const cudaExtent volumeSize = make_cudaExtent(dim[1], dim[2], dim[3]); + cudaChannelFormatDesc texDesc = cudaCreateChannelDesc(); + NR_CUDA_SAFE_CALL(cudaMalloc3DArray(cuArray_d, &texDesc, volumeSize)); + NR_CUDA_SAFE_CALL(cudaMalloc3DArray(cuArray2_d, &texDesc, volumeSize)); + return EXIT_SUCCESS; +} +template int cudaCommon_allocateArrayToDevice(cudaArray **,cudaArray **, int *); +template int cudaCommon_allocateArrayToDevice(cudaArray **,cudaArray **, int *); +template int cudaCommon_allocateArrayToDevice(cudaArray **,cudaArray **, int *); // for deformation field +/* ******************************** */ +/* ******************************** */ +template +int cudaCommon_allocateArrayToDevice(DTYPE **array_d, int *dim) +{ + const unsigned int memSize = dim[1] * dim[2] * dim[3] * sizeof(DTYPE); + NR_CUDA_SAFE_CALL(cudaMalloc(array_d, memSize)); + return EXIT_SUCCESS; +} +template int cudaCommon_allocateArrayToDevice(float **, int *); +template int cudaCommon_allocateArrayToDevice(double **, int *); +template int cudaCommon_allocateArrayToDevice(int **, int *); +template int cudaCommon_allocateArrayToDevice(float4 **, int *); // for deformation field +/* ******************************** */ +template +int cudaCommon_allocateArrayToDevice(DTYPE **array_d, int vox) +{ + const unsigned int memSize = vox * sizeof(DTYPE); + NR_CUDA_SAFE_CALL(cudaMalloc(array_d, memSize)); + return EXIT_SUCCESS; +} +template int cudaCommon_allocateArrayToDevice(float **, int); +template int cudaCommon_allocateArrayToDevice(double **, int); +template int cudaCommon_allocateArrayToDevice(int **, int); +template int cudaCommon_allocateArrayToDevice(float4 **, int); // for deformation field +/* ******************************** */ +/* ******************************** */ +template +int cudaCommon_allocateArrayToDevice(DTYPE **array_d, DTYPE **array2_d, int *dim) +{ + const unsigned int memSize = dim[1] * dim[2] * dim[3] * sizeof(DTYPE); + NR_CUDA_SAFE_CALL(cudaMalloc(array_d, memSize)); + NR_CUDA_SAFE_CALL(cudaMalloc(array2_d, memSize)); + return EXIT_SUCCESS; +} +template int cudaCommon_allocateArrayToDevice(float **, float **, int *); +template int cudaCommon_allocateArrayToDevice(double **, double **, int *); +template int cudaCommon_allocateArrayToDevice(float4 **, float4 **, int *); // for deformation field +/* ******************************** */ +/* ******************************** */ +template +int cudaCommon_transferFromDeviceToCpu(DTYPE *cpuPtr, DTYPE **cuPtr, const unsigned int nElements) +{ + + NR_CUDA_SAFE_CALL(cudaMemcpy((void *)cpuPtr, (void *)*cuPtr, nElements*sizeof(DTYPE), cudaMemcpyDeviceToHost)); + //NR_CUDA_SAFE_CALL(cudaThreadSynchronize()); + return EXIT_SUCCESS; +} +template int cudaCommon_transferFromDeviceToCpu(float *cpuPtr, float **cuPtr, const unsigned int nElements); +template int cudaCommon_transferFromDeviceToCpu(double *cpuPtr, double **cuPtr, const unsigned int nElements); + +/* ******************************** */ +/* ******************************** */ +/* ******************************** */ +template +int cudaCommon_transferFromDeviceToNifti1(nifti_image *img, DTYPE **array_d) +{ + if(sizeof(DTYPE)!=sizeof(NIFTI_TYPE)){ + reg_print_fct_error("cudaCommon_transferFromDeviceToNifti1"); + reg_print_msg_error("The host and device arrays are of different types"); + return EXIT_FAILURE; + } + else + { + NIFTI_TYPE *array_h=static_cast(img->data); + NR_CUDA_SAFE_CALL(cudaMemcpy((void *)array_h, (void *)*array_d, img->nvox*sizeof(DTYPE), cudaMemcpyDeviceToHost)); + } + return EXIT_SUCCESS; +} +template int cudaCommon_transferFromDeviceToNifti1(nifti_image *img, float **array_d); +template int cudaCommon_transferFromDeviceToNifti1(nifti_image *img, double **array_d); +/* ******************************** */ +template +int cudaCommon_transferFromDeviceToNifti(nifti_image *img, DTYPE **array_d) +{ + if(sizeof(DTYPE)==sizeof(float4)){ + // A nifti 5D volume is expected + if(img->dim[0]<5 || img->dim[4]>1 || img->dim[5]<2 || img->datatype!=NIFTI_TYPE_FLOAT32){ + reg_print_fct_error("cudaCommon_transferFromDeviceToNifti"); + reg_print_msg_error("The nifti image is not a 5D volume"); + return EXIT_FAILURE; + } + const int voxelNumber = img->nx*img->ny*img->nz; + + float4 *array_h; + NR_CUDA_SAFE_CALL(cudaMallocHost(&array_h, voxelNumber*sizeof(float4))); + NR_CUDA_SAFE_CALL(cudaMemcpy((void *)array_h, (const void *)*array_d, voxelNumber*sizeof(float4), cudaMemcpyDeviceToHost)); + float *niftiImgValues = static_cast(img->data); + + for(int i=0; idim[5]>=2){ + for(int i=0; idim[5]>=3){ + for(int i=0; idim[5]>=4){ + for(int i=0; idatatype){ + case NIFTI_TYPE_FLOAT32: + return cudaCommon_transferFromDeviceToNifti1(img, array_d); + default: + reg_print_fct_error("cudaCommon_transferFromDeviceToNifti"); + reg_print_msg_error("The image data type is not supported"); + return EXIT_FAILURE; + } + } +} +template int cudaCommon_transferFromDeviceToNifti(nifti_image *, float **); +template int cudaCommon_transferFromDeviceToNifti(nifti_image *, double **); +template int cudaCommon_transferFromDeviceToNifti(nifti_image *, float4 **); // for deformation field +/* ******************************** */ +/* ******************************** */ +template +int cudaCommon_transferFromDeviceToNifti1(nifti_image *img, DTYPE **array_d, DTYPE **array2_d) +{ + if(sizeof(DTYPE)!=sizeof(NIFTI_TYPE)){ + reg_print_fct_error("cudaCommon_transferFromDeviceToNifti1"); + reg_print_msg_error("The host and device arrays are of different types"); + return EXIT_FAILURE; + } + else{ + unsigned int voxelNumber=img->nx*img->ny*img->nz; + NIFTI_TYPE *array_h=static_cast(img->data); + NIFTI_TYPE *array2_h=&array_h[voxelNumber]; + NR_CUDA_SAFE_CALL(cudaMemcpy((void *)array_h, (void *)*array_d, voxelNumber*sizeof(DTYPE), cudaMemcpyDeviceToHost)); + NR_CUDA_SAFE_CALL(cudaMemcpy((void *)array2_h, (void *)*array2_d, voxelNumber*sizeof(DTYPE), cudaMemcpyDeviceToHost)); + } + return EXIT_SUCCESS; +} +/* ******************************** */ +template +int cudaCommon_transferFromDeviceToNifti(nifti_image *img, DTYPE **array_d, DTYPE **array2_d) +{ + if(sizeof(DTYPE)==sizeof(float4)){ + // A nifti 5D volume is expected + if(img->dim[0]<5 || img->dim[4]>1 || img->dim[5]<2 || img->datatype!=NIFTI_TYPE_FLOAT32){ + reg_print_fct_error("cudaCommon_transferFromDeviceToNifti"); + reg_print_msg_error("The nifti image is not a 5D volume"); + return EXIT_FAILURE; + } + const int voxelNumber = img->nx*img->ny*img->nz; + float4 *array_h=NULL; + float4 *array2_h=NULL; + NR_CUDA_SAFE_CALL(cudaMallocHost(&array_h, voxelNumber*sizeof(float4))); + NR_CUDA_SAFE_CALL(cudaMallocHost(&array2_h, voxelNumber*sizeof(float4))); + NR_CUDA_SAFE_CALL(cudaMemcpy((void *)array_h, (const void *)*array_d, voxelNumber*sizeof(float4), cudaMemcpyDeviceToHost)); + NR_CUDA_SAFE_CALL(cudaMemcpy((void *)array2_h, (const void *)*array2_d, voxelNumber*sizeof(float4), cudaMemcpyDeviceToHost)); + float *niftiImgValues = static_cast(img->data); + for(int i=0; idim[5]>=2){ + for(int i=0; idim[5]>=3){ + for(int i=0; idim[5]>=4){ + for(int i=0; idatatype){ + case NIFTI_TYPE_FLOAT32: + return cudaCommon_transferFromDeviceToNifti1(img, array_d, array2_d); + default: + reg_print_fct_error("cudaCommon_transferFromDeviceToNifti"); + reg_print_msg_error("The image data type is not supported"); + return EXIT_FAILURE; + } + } +} +template int cudaCommon_transferFromDeviceToNifti(nifti_image *, float **, float **); +template int cudaCommon_transferFromDeviceToNifti(nifti_image *, double **, double **); +template int cudaCommon_transferFromDeviceToNifti(nifti_image *, float4 **, float4 **); // for deformation field +/* ******************************** */ +/* ******************************** */ +void cudaCommon_free(cudaArray **cuArray_d) +{ + NR_CUDA_SAFE_CALL(cudaFreeArray(*cuArray_d)); + return; +} +/* ******************************** */ +/* ******************************** */ +template +void cudaCommon_free(DTYPE **array_d) +{ + NR_CUDA_SAFE_CALL(cudaFree(*array_d)); + return; +} +template void cudaCommon_free(int **); +template void cudaCommon_free(float **); +template void cudaCommon_free(double **); +template void cudaCommon_free(float4 **); +/* ******************************** */ +/* ******************************** */ +template +int cudaCommon_transferFromDeviceToNiftiSimple(DTYPE **array_d, nifti_image *img) +{ + NR_CUDA_SAFE_CALL(cudaMemcpy(*array_d, img->data, img->nvox * sizeof(DTYPE), cudaMemcpyHostToDevice)); + + return EXIT_SUCCESS; +} +template int cudaCommon_transferFromDeviceToNiftiSimple(int **array_d, nifti_image *img); +template int cudaCommon_transferFromDeviceToNiftiSimple(float **array_d, nifti_image *img); +template int cudaCommon_transferFromDeviceToNiftiSimple(double **array_d, nifti_image *img); +/* ******************************** */ +/* ******************************** */ +template +int cudaCommon_transferFromDeviceToNiftiSimple1(DTYPE **array_d, DTYPE *img, const unsigned int nvox) +{ + NR_CUDA_SAFE_CALL(cudaMemcpy(*array_d, img, nvox * sizeof(DTYPE), cudaMemcpyHostToDevice)); + return EXIT_SUCCESS; +} +template int cudaCommon_transferFromDeviceToNiftiSimple1(int **array_d, int *img, const unsigned); +template int cudaCommon_transferFromDeviceToNiftiSimple1(float **array_d, float *img, const unsigned); +template int cudaCommon_transferFromDeviceToNiftiSimple1(double **array_d, double *img, const unsigned); +/* ******************************** */ +/* ******************************** */ +/* ******************************** */ +/* ******************************** */ +template +int cudaCommon_transferArrayFromCpuToDevice(DTYPE *array_d, const DTYPE *array_cpu, const unsigned int nElements) { + + const unsigned int memSize = nElements * sizeof(DTYPE); + //copyData + NR_CUDA_SAFE_CALL(cudaMemcpy(array_d, array_cpu, memSize, cudaMemcpyHostToDevice)); + // + return EXIT_SUCCESS; +} +template int cudaCommon_transferArrayFromCpuToDevice(int *array_d, const int *array_cpu, const unsigned int nElements); +template int cudaCommon_transferArrayFromCpuToDevice(float *array_d, const float *array_cpu, const unsigned int nElements); +template int cudaCommon_transferArrayFromCpuToDevice(double *array_d, const double *array_cpu, const unsigned int nElements); +/* ******************************** */ +/* ******************************** */ +/* ******************************** */ +/* ******************************** */ +template +int cudaCommon_transferArrayFromDeviceToCpu(DTYPE *array_cpu, DTYPE *array_d, const unsigned int nElements) { + + const unsigned int memSize = nElements * sizeof(DTYPE); + //copyData + NR_CUDA_SAFE_CALL(cudaMemcpy(array_cpu, array_d, memSize, cudaMemcpyDeviceToHost)); + // + return EXIT_SUCCESS; +} +template int cudaCommon_transferArrayFromDeviceToCpu(int *array_cpu, int *array_d, const unsigned int nElements); +template int cudaCommon_transferArrayFromDeviceToCpu(float *array_cpu, float *array_d, const unsigned int nElements); +template int cudaCommon_transferArrayFromDeviceToCpu(double *array_cpu, double *array_d, const unsigned int nElements); +#endif +/* ******************************** */ +/* ******************************** */ diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_common_cuda.h b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_common_cuda.h new file mode 100755 index 00000000..1e2ea69d --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_common_cuda.h @@ -0,0 +1,179 @@ +/** @file _reg_common_gpu.h + * @author Marc Modat + * @date 25/03/2009. + * Copyright (c) 2009, University College London. All rights reserved. + * Centre for Medical Image Computing (CMIC) + * See the LICENSE.txt file in the nifty_reg root folder + */ + +#ifndef _REG_COMMON_GPU_H +#define _REG_COMMON_GPU_H + +#include "nifti1_io.h" +#include "cuda_runtime.h" +#include "cuda.h" + +/* ******************************** */ +/* ******************************** */ +#ifndef __VECTOR_TYPES_H__ +#define __VECTOR_TYPES_H__ +struct __attribute__((aligned(4))) float4 +{ + float x,y,z,w; +}; +#endif +/* ******************************** */ +/* ******************************** */ +#if CUDART_VERSION >= 3200 +# define NR_CUDA_SAFE_CALL(call) { \ + call; \ + cudaError err = cudaPeekAtLastError(); \ + if( cudaSuccess != err) { \ + fprintf(stderr, "[NiftyReg CUDA ERROR] file '%s' in line %i : %s.\n", \ + __FILE__, __LINE__, cudaGetErrorString(err)); \ + reg_exit(); \ + } \ + } +# define NR_CUDA_CHECK_KERNEL(grid,block) { \ + cudaThreadSynchronize(); \ + cudaError err = cudaPeekAtLastError(); \ + if( err != cudaSuccess) { \ + fprintf(stderr, "[NiftyReg CUDA ERROR] file '%s' in line %i : %s.\n", \ + __FILE__, __LINE__, cudaGetErrorString(err)); \ + fprintf(stderr, "Grid [%ix%ix%i] | Block [%ix%ix%i]\n", \ + grid.x,grid.y,grid.z,block.x,block.y,block.z); \ + reg_exit(); \ + } \ + else{\ + printf("[NiftyReg CUDA DEBUG] kernel: %s - Grid size [%i %i %i] - Block size [%i %i %i]\n", \ + cudaGetErrorString(cudaGetLastError()), grid.x, grid.y, grid.z, block.x, block.y, block.z);\ + }\ + } +#else //CUDART_VERSION >= 3200 +# define NR_CUDA_SAFE_CALL(call) { \ + call; \ + cudaError err = cudaThreadSynchronize(); \ + if( cudaSuccess != err) { \ + fprintf(stderr, "[NiftyReg CUDA ERROR] file '%s' in line %i : %s.\n", \ + __FILE__, __LINE__, cudaGetErrorString(err)); \ + reg_exit(); \ + } \ + } +# define NR_CUDA_CHECK_KERNEL(grid,block) { \ + cudaError err = cudaThreadSynchronize(); \ + if( err != cudaSuccess) { \ + fprintf(stderr, "[NiftyReg CUDA ERROR] file '%s' in line %i : %s.\n", \ + __FILE__, __LINE__, cudaGetErrorString(err)); \ + fprintf(stderr, "Grid [%ix%ix%i] | Block [%ix%ix%i]\n", \ + grid.x,grid.y,grid.z,block.x,block.y,block.z); \ + reg_exit(); \ + } \ + } +#endif //CUDART_VERSION >= 3200 +/* ******************************** */ +/* ******************************** */ +/** \brief Computes a reasonable grid configuration for resampling in a given reference space */ +void cudaCommon_computeGridConfiguration(dim3 &r_blocks, dim3 &r_grid, const int targetVoxelNumber); + +/* ******************************** */ +int cudaCommon_setCUDACard(CUcontext *ctx, + bool verbose); +/* ******************************** */ +void cudaCommon_unsetCUDACard(CUcontext *ctx); +/* ******************************** */ +/* ******************************** */ +extern "C++" +template +int cudaCommon_allocateArrayToDevice(cudaArray **, int *); +/* ******************************** */ +extern "C++" +template +int cudaCommon_allocateArrayToDevice(cudaArray **, cudaArray **, int *); +/* ******************************** */ +extern "C++" +template +int cudaCommon_allocateArrayToDevice(DTYPE **, int); +/* ******************************** */ +extern "C++" +template +int cudaCommon_allocateArrayToDevice(DTYPE **, int *); +/* ******************************** */ +extern "C++" +template +int cudaCommon_allocateArrayToDevice(DTYPE **, DTYPE **, int *); +/* ******************************** */ +/* ******************************** */ +extern "C++" +template +int cudaCommon_transferNiftiToArrayOnDevice(cudaArray **, nifti_image *); +/* ******************************** */ +extern "C++" +template +int cudaCommon_transferNiftiToArrayOnDevice(cudaArray **, cudaArray **, const nifti_image *); +/* ******************************** */ +extern "C++" +template +int cudaCommon_transferNiftiToArrayOnDevice(DTYPE **, const nifti_image *); +/* ******************************** */ +extern "C++" +template +int cudaCommon_transferNiftiToArrayOnDevice(DTYPE **, DTYPE **, nifti_image *); +/* ******************************** */ +/* ******************************** */ +extern "C++" +template +int cudaCommon_transferFromDeviceToNifti1(nifti_image *, DTYPE **); +/* ******************************** */ +extern "C++" +template +int cudaCommon_transferFromDeviceToNifti(nifti_image *, DTYPE **); +/* ******************************** */ +extern "C++" +template +int cudaCommon_transferFromDeviceToNifti(nifti_image *, DTYPE **, DTYPE **); +/* ******************************** */ +/* ******************************** */ +extern "C++" +void cudaCommon_free(cudaArray **); +/* ******************************** */ +extern "C++" template +void cudaCommon_free(DTYPE **); +/* ******************************** */ +/* ******************************** */ +extern "C++" template +int cudaCommon_allocateNiftiToDevice(nifti_image **image_d, int *dim); + +template +int cudaCommon_transferNiftiToNiftiOnDevice1(nifti_image **image_d, nifti_image *img); + + +/* ******************************** */ +/* ******************************** */ +extern "C++" +template +int cudaCommon_transferFromDeviceToNiftiSimple(DTYPE **, nifti_image * ); + +extern "C++" +template +int cudaCommon_transferFromDeviceToNiftiSimple1(DTYPE **array_d, DTYPE *img, const unsigned nvox); + +extern "C++" +template +int cudaCommon_transferFromDeviceToCpu(DTYPE *cpuPtr, DTYPE **cuPtr, const unsigned int nElements); +/* ******************************** */ +/* ******************************** */ +/* ******************************** */ +/* ******************************** */ +extern "C++" +template +int cudaCommon_transferArrayFromCpuToDevice(DTYPE *array_d, const DTYPE *array_cpu, const unsigned int nElements); +/* ******************************** */ +/* ******************************** */ +extern "C++" +template +int cudaCommon_transferArrayFromDeviceToCpu(DTYPE *array_cpu, DTYPE *array_d, const unsigned int nElements); +/* ******************************** */ +/* ******************************** */ +void showCUDACardInfo(void); +/* ******************************** */ +#endif diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_maths.cpp b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_maths.cpp new file mode 100644 index 00000000..b21175c9 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_maths.cpp @@ -0,0 +1,993 @@ +#ifndef _REG_MATHS_CPP +#define _REG_MATHS_CPP + +#include "_reg_maths.h" +//STD +#include +#include + +#define mat(i,j,dim) mat[i*dim+j] + +/* *************************************************************** */ +/* *************************************************************** */ +template +void reg_LUdecomposition(T *mat, + size_t dim, + size_t *index) +{ + T *vv = (T *)malloc(dim * sizeof(T)); + size_t i, j, k, imax = 0; + + for (i = 0; i < dim; ++i) + { + T big = 0.f; + T temp; + for (j = 0; j < dim; ++j) + if ((temp = fabs(mat(i, j, dim)))>big) + big = temp; + if (big == 0.f) + { + reg_print_fct_error("reg_LUdecomposition"); + reg_print_msg_error("Singular matrix"); + reg_exit(); + } + vv[i] = 1.0 / big; + } + for (j = 0; j < dim; ++j) + { + for (i = 0; i < j; ++i) + { + T sum = mat(i, j, dim); + for (k = 0; k < i; k++) sum -= mat(i, k, dim)*mat(k, j, dim); + mat(i, j, dim) = sum; + } + T big = 0.f; + T dum; + for (i = j; i < dim; ++i) + { + T sum = mat(i, j, dim); + for (k = 0; k < j; ++k) sum -= mat(i, k, dim)*mat(k, j, dim); + mat(i, j, dim) = sum; + if ((dum = vv[i] * fabs(sum)) >= big) + { + big = dum; + imax = i; + } + } + if (j != imax) + { + for (k = 0; k < dim; ++k) + { + dum = mat(imax, k, dim); + mat(imax, k, dim) = mat(j, k, dim); + mat(j, k, dim) = dum; + } + vv[imax] = vv[j]; + } + index[j] = imax; + if (mat(j, j, dim) == 0) mat(j, j, dim) = 1.0e-20; + if (j != dim - 1) + { + dum = 1.0 / mat(j, j, dim); + for (i = j + 1; i < dim; ++i) mat(i, j, dim) *= dum; + } + } + free(vv); + return; +} +/* *************************************************************** */ +/* *************************************************************** */ +template +void reg_matrixInvertMultiply(T *mat, + size_t dim, + size_t *index, + T *vec) +{ + // Perform the LU decomposition if necessary + if (index == NULL) + reg_LUdecomposition(mat, dim, index); + + int ii = 0; + for (size_t i = 0; i < dim; ++i) + { + int ip = index[i]; + T sum = vec[ip]; + vec[ip] = vec[i]; + if (ii != 0) + { + for (int j = ii - 1; j < (int)i; ++j) + sum -= mat(i, j, dim)*vec[j]; + } + else if (sum != 0) + ii = i + 1; + vec[i] = sum; + } + for (int i = (int)dim - 1; i > -1; --i) + { + T sum = vec[i]; + for (int j = i + 1; j < (int)dim; ++j) + sum -= mat(i, j, dim)*vec[j]; + vec[i] = sum / mat(i, i, dim); + } +} +template void reg_matrixInvertMultiply(float *, size_t, size_t *, float *); +template void reg_matrixInvertMultiply(double *, size_t, size_t *, double *); +/* *************************************************************** */ +/* *************************************************************** */ +template +void reg_matrixMultiply(T *mat1, + T *mat2, + size_t *dim1, + size_t *dim2, + T * &res) +{ + // First check that the dimension are appropriate + if (dim1[1] != dim2[0]) + { + char text[255]; sprintf(text, "Matrices can not be multiplied due to their size: [%zu %zu] [%zu %zu]", + dim1[0], dim1[1], dim2[0], dim2[1]); + reg_print_fct_error("reg_matrixMultiply"); + reg_print_msg_error(text); + reg_exit(); + } + size_t resDim[2] = {dim1[0], dim2[1]}; + // Allocate the result matrix + if (res != NULL) + free(res); + res = (T *)calloc(resDim[0] * resDim[1], sizeof(T)); + // Multiply both matrices + for (size_t j = 0; j < resDim[1]; ++j) + { + for (size_t i = 0; i < resDim[0]; ++i) + { + double sum = 0.0; + for (size_t k = 0; k < dim1[1]; ++k) + { + sum += mat1[k * dim1[0] + i] * mat2[j * dim2[0] + k]; + } + res[j * resDim[0] + i] = sum; + } // i + } // j +} +template void reg_matrixMultiply(float *, float *, size_t *, size_t *, float * &); +template void reg_matrixMultiply(double *, double *, size_t *, size_t *, double * &); +/* *************************************************************** */ +/* *************************************************************** */ +/* *************************************************************** */ +/* *************************************************************** */ +template +T* reg_matrix1DAllocate(size_t arraySize) { + T* res = (T*)malloc(arraySize*sizeof(T)); + return res; +} +template bool* reg_matrix1DAllocate(size_t arraySize); +template float* reg_matrix1DAllocate(size_t arraySize); +template double* reg_matrix1DAllocate(size_t arraySize); +/* *************************************************************** */ +template +T* reg_matrix1DAllocateAndInitToZero(size_t arraySize) { + T* res = (T*)calloc(arraySize, sizeof(T)); + return res; +} +template bool* reg_matrix1DAllocateAndInitToZero(size_t arraySize); +template float* reg_matrix1DAllocateAndInitToZero(size_t arraySize); +template double* reg_matrix1DAllocateAndInitToZero(size_t arraySize); +/* *************************************************************** */ +template +void reg_matrix1DDeallocate(T* mat) { + free(mat); +} +template void reg_matrix1DDeallocate(bool* mat); +template void reg_matrix1DDeallocate(float* mat); +template void reg_matrix1DDeallocate(double* mat); +/* *************************************************************** */ +template +T** reg_matrix2DAllocate(size_t arraySizeX, size_t arraySizeY) { + T** res; + res = (T**)malloc(arraySizeX*sizeof(T*)); + for (size_t i = 0; i < arraySizeX; i++) { + res[i] = (T*)malloc(arraySizeY*sizeof(T)); + } + return res; +} +template float** reg_matrix2DAllocate(size_t arraySizeX, size_t arraySizeY); +template double** reg_matrix2DAllocate(size_t arraySizeX, size_t arraySizeY); +/* *************************************************************** */ +template +T** reg_matrix2DAllocateAndInitToZero(size_t arraySizeX, size_t arraySizeY) { + T** res; + res = (T**)calloc(arraySizeX, sizeof(T*)); + for (size_t i = 0; i < arraySizeX; i++) { + res[i] = (T*)calloc(arraySizeY, sizeof(T)); + } + return res; +} +template float** reg_matrix2DAllocateAndInitToZero(size_t arraySizeX, size_t arraySizeY); +template double** reg_matrix2DAllocateAndInitToZero(size_t arraySizeX, size_t arraySizeY); +/* *************************************************************** */ +template +void reg_matrix2DDeallocate(size_t arraySizeX, T** mat) { + for (size_t i = 0; i < arraySizeX; i++) { + free(mat[i]); + } + free(mat); +} +template void reg_matrix2DDeallocate(size_t arraySizeX, float** mat); +template void reg_matrix2DDeallocate(size_t arraySizeX, double** mat); +/* *************************************************************** */ +template +T** reg_matrix2DTranspose(T** mat, size_t arraySizeX, size_t arraySizeY) { + T** res; + res = (T**)malloc(arraySizeY*sizeof(T*)); + for (size_t i = 0; i < arraySizeY; i++) { + res[i] = (T*)malloc(arraySizeX*sizeof(T)); + } + for (size_t i = 0; i < arraySizeX; i++) { + for (size_t j = 0; j < arraySizeY; j++) { + res[j][i] = mat[i][j]; + } + } + return res; +} +template float** reg_matrix2DTranspose(float** mat, size_t arraySizeX, size_t arraySizeY); +template double** reg_matrix2DTranspose(double** mat, size_t arraySizeX, size_t arraySizeY); +/* *************************************************************** */ +template +T** reg_matrix2DMultiply(T** mat1, size_t mat1X, size_t mat1Y, T** mat2, size_t mat2X, size_t mat2Y, bool transposeMat2) { + if (transposeMat2 == false) { + // First check that the dimension are appropriate + if (mat1Y != mat2X) { + char text[255]; sprintf(text, "Matrices can not be multiplied due to their size: [%zu %zu] [%zu %zu]", + mat1X, mat1Y, mat2X, mat2Y); + reg_print_fct_error("reg_matrix2DMultiply"); + reg_print_msg_error(text); + reg_exit(); + } + + size_t nbElement = mat1Y; + double resTemp = 0; + T** res = reg_matrix2DAllocate(mat1X,mat2Y); + + for (size_t i = 0; i < mat1X; i++) { + for (size_t j = 0; j < mat2Y; j++) { + resTemp = 0; + for (size_t k = 0; k < nbElement; k++) { + resTemp += static_cast(mat1[i][k]) * static_cast(mat2[k][j]); + } + res[i][j] = static_cast(resTemp); + } + } + //Output + return res; + } + else { + // First check that the dimension are appropriate + if (mat1Y != mat2Y) { + char text[255]; sprintf(text, "Matrices can not be multiplied due to their size: [%zu %zu] [%zu %zu]", + mat1X, mat1Y, mat2Y, mat2X); + reg_print_fct_error("reg_matrix2DMultiply"); + reg_print_msg_error(text); + reg_exit(); + } + size_t nbElement = mat1Y; + double resTemp = 0; + T** res = reg_matrix2DAllocate(mat1X,mat2X); + + for (size_t i = 0; i < mat1X; i++) { + for (size_t j = 0; j < mat2X; j++) { + resTemp = 0; + for (size_t k = 0; k < nbElement; k++) { + resTemp += static_cast(mat1[i][k]) * static_cast(mat2[j][k]); + } + res[i][j] = static_cast(resTemp); + } + } + //Output + return res; + } +} +template float** reg_matrix2DMultiply(float** mat1, size_t mat1X, size_t mat1Y, float** mat2, size_t mat2X, size_t mat2Y, bool transposeMat2); +template double** reg_matrix2DMultiply(double** mat1, size_t mat1X, size_t mat1Y, double** mat2, size_t mat2X, size_t mat2Y, bool transposeMat2); +/* *************************************************************** */ +template +void reg_matrix2DMultiply(T** mat1, size_t mat1X, size_t mat1Y, T** mat2, size_t mat2X, size_t mat2Y, T** resT, bool transposeMat2) { + if (transposeMat2 == false) { + // First check that the dimension are appropriate + if (mat1Y != mat2X) { + char text[255]; sprintf(text, "Matrices can not be multiplied due to their size: [%zu %zu] [%zu %zu]", + mat1X, mat1Y, mat2X, mat2Y); + reg_print_fct_error("reg_matrix2DMultiply"); + reg_print_msg_error(text); + reg_exit(); + } + size_t nbElement = mat1Y; + double resTemp; + + for (size_t i = 0; i < mat1X; i++) { + for (size_t j = 0; j < mat2Y; j++) { + resTemp = 0; + for (size_t k = 0; k < nbElement; k++) { + resTemp += static_cast(mat1[i][k]) * static_cast(mat2[k][j]); + } + resT[i][j] = static_cast(resTemp); + } + } + } + else { + // First check that the dimension are appropriate + if (mat1Y != mat2Y) { + char text[255]; sprintf(text, "Matrices can not be multiplied due to their size: [%zu %zu] [%zu %zu]", + mat1X, mat1Y, mat2Y, mat2X); + reg_print_fct_error("reg_matrix2DMultiply"); + reg_print_msg_error(text); + reg_exit(); + } + size_t nbElement = mat1Y; + double resTemp; + + for (size_t i = 0; i < mat1X; i++) { + for (size_t j = 0; j < mat2X; j++) { + resTemp = 0; + for (size_t k = 0; k < nbElement; k++) { + resTemp += static_cast(mat1[i][k]) * static_cast(mat2[j][k]); + } + resT[i][j] = static_cast(resTemp); + } + } + } +} +template void reg_matrix2DMultiply(float** mat1, size_t mat1X, size_t mat1Y, float** mat2, size_t mat2X, size_t mat2Y, float** resT, bool transposeMat2); +template void reg_matrix2DMultiply(double** mat1, size_t mat1X, size_t mat1Y, double** mat2, size_t mat2X, size_t mat2Y, double** resT, bool transposeMat2); +/* *************************************************************** */ +// Multiply a matrix with a vector - we assume correct dimension +template +T* reg_matrix2DVectorMultiply(T** mat, size_t m, size_t n, T* vect) { + + T* res = reg_matrix1DAllocate(m); + double resTemp; + + for (size_t i = 0; i < m; i++) { + resTemp = 0; + for (size_t k = 0; k < n; k++) { + resTemp += static_cast(mat[i][k]) * static_cast(vect[k]); + } + res[i] = static_cast(resTemp); + } + return res; +} +template float* reg_matrix2DVectorMultiply(float** mat, size_t m, size_t n, float* vect); +template double* reg_matrix2DVectorMultiply(double** mat, size_t m, size_t n, double* vect); +/* *************************************************************** */ +template +void reg_matrix2DVectorMultiply(T** mat, size_t m, size_t n, T* vect, T* res) { + + double resTemp = 0; + + for (size_t i = 0; i < m; i++) { + resTemp = 0; + for (size_t k = 0; k < n; k++) { + resTemp += static_cast(mat[i][k]) * static_cast(vect[k]); + } + res[i] = static_cast(resTemp); + } +} +template void reg_matrix2DVectorMultiply(float** mat, size_t m, size_t n, float* vect, float* res); +template void reg_matrix2DVectorMultiply(double** mat, size_t m, size_t n, double* vect, double* res); +/* *************************************************************** */ +/* *************************************************************** */ +/* *************************************************************** */ +/* *************************************************************** */ +// Heap sort +void reg_heapSort(float *array_tmp, int *index_tmp, int blockNum) +{ + float *array = &array_tmp[-1]; + int *index = &index_tmp[-1]; + int l = (blockNum >> 1) + 1; + int ir = blockNum; + float val; + int iVal; + for (;;) + { + if (l > 1) + { + val = array[--l]; + iVal = index[l]; + } + else + { + val = array[ir]; + iVal = index[ir]; + array[ir] = array[1]; + index[ir] = index[1]; + if (--ir == 1) + { + array[1] = val; + index[1] = iVal; + break; + } + } + int i = l; + int j = l + l; + while (j <= ir) + { + if (j < ir && array[j] < array[j + 1]) + j++; + if (val < array[j]) + { + array[i] = array[j]; + index[i] = index[j]; + i = j; + j <<= 1; + } + else + break; + } + array[i] = val; + index[i] = iVal; + } +} +/* *************************************************************** */ +// Heap sort +template +void reg_heapSort(DTYPE *array_tmp, int blockNum) +{ + DTYPE *array = &array_tmp[-1]; + int l = (blockNum >> 1) + 1; + int ir = blockNum; + DTYPE val; + for (;;) + { + if (l > 1) + { + val = array[--l]; + } + else + { + val = array[ir]; + array[ir] = array[1]; + if (--ir == 1) + { + array[1] = val; + break; + } + } + int i = l; + int j = l + l; + while (j <= ir) + { + if (j < ir && array[j] < array[j + 1]) + j++; + if (val < array[j]) + { + array[i] = array[j]; + i = j; + j <<= 1; + } + else + break; + } + array[i] = val; + } +} +template void reg_heapSort(float *array_tmp, int blockNum); +template void reg_heapSort(double *array_tmp, int blockNum); +/* *************************************************************** */ +/* *************************************************************** */ +bool operator==(mat44 A, mat44 B) +{ + for (unsigned i = 0; i < 4; ++i) + { + for (unsigned j = 0; j < 4; ++j) + { + if (A.m[i][j] != B.m[i][j]) + return false; + } + } + return true; +} +/* *************************************************************** */ +bool operator!=(mat44 A, mat44 B) +{ + for (unsigned i = 0; i < 4; ++i) + { + for (unsigned j = 0; j < 4; ++j) + { + if (A.m[i][j] != B.m[i][j]) + return true; + } + } + return false; +} +/* *************************************************************** */ +/* *************************************************************** */ +template +T reg_mat44_det(mat44 const* A) +{ + double D = + static_cast(A->m[0][0]) * static_cast(A->m[1][1]) * static_cast(A->m[2][2]) * static_cast(A->m[3][3]) + - static_cast(A->m[0][0]) * static_cast(A->m[1][1]) * static_cast(A->m[3][2]) * static_cast(A->m[2][3]) + - static_cast(A->m[0][0]) * static_cast(A->m[2][1]) * static_cast(A->m[1][2]) * static_cast(A->m[3][3]) + + static_cast(A->m[0][0]) * static_cast(A->m[2][1]) * static_cast(A->m[3][2]) * static_cast(A->m[1][3]) + + static_cast(A->m[0][0]) * static_cast(A->m[3][1]) * static_cast(A->m[1][2]) * static_cast(A->m[2][3]) + - static_cast(A->m[0][0]) * static_cast(A->m[3][1]) * static_cast(A->m[2][2]) * static_cast(A->m[1][3]) + - static_cast(A->m[1][0]) * static_cast(A->m[0][1]) * static_cast(A->m[2][2]) * static_cast(A->m[3][3]) + + static_cast(A->m[1][0]) * static_cast(A->m[0][1]) * static_cast(A->m[3][2]) * static_cast(A->m[2][3]) + + static_cast(A->m[1][0]) * static_cast(A->m[2][1]) * static_cast(A->m[0][2]) * static_cast(A->m[3][3]) + - static_cast(A->m[1][0]) * static_cast(A->m[2][1]) * static_cast(A->m[3][2]) * static_cast(A->m[0][3]) + - static_cast(A->m[1][0]) * static_cast(A->m[3][1]) * static_cast(A->m[0][2]) * static_cast(A->m[2][3]) + + static_cast(A->m[1][0]) * static_cast(A->m[3][1]) * static_cast(A->m[2][2]) * static_cast(A->m[0][3]) + + static_cast(A->m[2][0]) * static_cast(A->m[0][1]) * static_cast(A->m[1][2]) * static_cast(A->m[3][3]) + - static_cast(A->m[2][0]) * static_cast(A->m[0][1]) * static_cast(A->m[3][2]) * static_cast(A->m[1][3]) + - static_cast(A->m[2][0]) * static_cast(A->m[1][1]) * static_cast(A->m[0][2]) * static_cast(A->m[3][3]) + + static_cast(A->m[2][0]) * static_cast(A->m[1][1]) * static_cast(A->m[3][2]) * static_cast(A->m[0][3]) + + static_cast(A->m[2][0]) * static_cast(A->m[3][1]) * static_cast(A->m[0][2]) * static_cast(A->m[1][3]) + - static_cast(A->m[2][0]) * static_cast(A->m[3][1]) * static_cast(A->m[1][2]) * static_cast(A->m[0][3]) + - static_cast(A->m[3][0]) * static_cast(A->m[0][1]) * static_cast(A->m[1][2]) * static_cast(A->m[2][3]) + + static_cast(A->m[3][0]) * static_cast(A->m[0][1]) * static_cast(A->m[2][2]) * static_cast(A->m[1][3]) + + static_cast(A->m[3][0]) * static_cast(A->m[1][1]) * static_cast(A->m[0][2]) * static_cast(A->m[2][3]) + - static_cast(A->m[3][0]) * static_cast(A->m[1][1]) * static_cast(A->m[2][2]) * static_cast(A->m[0][3]) + - static_cast(A->m[3][0]) * static_cast(A->m[2][1]) * static_cast(A->m[0][2]) * static_cast(A->m[1][3]) + + static_cast(A->m[3][0]) * static_cast(A->m[2][1]) * static_cast(A->m[1][2]) * static_cast(A->m[0][3]); + return static_cast(D); +} +template float reg_mat44_det(mat44 const* A); +template double reg_mat44_det(mat44 const* A); +/* *************************************************************** */ +/* *************************************************************** */ +template +T reg_mat33_det(mat33 const* A) +{ + double D = static_cast((static_cast(A->m[0][0]) * (static_cast(A->m[1][1]) * static_cast(A->m[2][2]) - static_cast(A->m[1][2]) * static_cast(A->m[2][1]))) - + (static_cast(A->m[0][1]) * (static_cast(A->m[1][0]) * static_cast(A->m[2][2]) - static_cast(A->m[1][2]) * static_cast(A->m[2][0]))) + + (static_cast(A->m[0][2]) * (static_cast(A->m[1][0]) * static_cast(A->m[2][1]) - static_cast(A->m[1][1]) * static_cast(A->m[2][0])))); + return static_cast(D); +} +template float reg_mat33_det(mat33 const* A); +template double reg_mat33_det(mat33 const* A); +/* *************************************************************** */ +/* *************************************************************** */ +void reg_mat33_to_nan(mat33 *A) +{ + for(int i=0;i<3;++i) + for(int j=0;j<3;++j) + A->m[i][j] = std::numeric_limits::quiet_NaN(); +} +/* *************************************************************** */ +/* *************************************************************** */ +mat33 reg_mat44_to_mat33(mat44 const* A) +{ + mat33 out; + out.m[0][0] = A->m[0][0]; + out.m[0][1] = A->m[0][1]; + out.m[0][2] = A->m[0][2]; + out.m[1][0] = A->m[1][0]; + out.m[1][1] = A->m[1][1]; + out.m[1][2] = A->m[1][2]; + out.m[2][0] = A->m[2][0]; + out.m[2][1] = A->m[2][1]; + out.m[2][2] = A->m[2][2]; + return out; +} +/* *************************************************************** */ +/* *************************************************************** */ +mat44 reg_mat44_mul(mat44 const* A, mat44 const* B) +{ + mat44 R; + for (int i = 0; i < 4; i++) + { + for (int j = 0; j < 4; j++) + { + R.m[i][j] = static_cast(static_cast(A->m[i][0]) * static_cast(B->m[0][j]) + + static_cast(A->m[i][1]) * static_cast(B->m[1][j]) + + static_cast(A->m[i][2]) * static_cast(B->m[2][j]) + + static_cast(A->m[i][3]) * static_cast(B->m[3][j])); + } + } + return R; +} +/* *************************************************************** */ +mat44 operator*(mat44 A, mat44 B) +{ + return reg_mat44_mul(&A, &B); +} +/* *************************************************************** */ +void reg_mat33_mul(mat44 const* mat, + float const* in, + float *out) +{ + out[0] = static_cast( + static_cast(in[0])*static_cast(mat->m[0][0]) + + static_cast(in[1])*static_cast(mat->m[0][1]) + + static_cast(mat->m[0][3])); + out[1] = static_cast( + static_cast(in[0])*static_cast(mat->m[1][0]) + + static_cast(in[1])*static_cast(mat->m[1][1]) + + static_cast(mat->m[1][3])); + return; +} +/* *************************************************************** */ +void reg_mat33_mul(mat33 const* mat, + float const* in, + float *out) +{ + out[0] = static_cast( + static_cast(in[0])*static_cast(mat->m[0][0]) + + static_cast(in[1])*static_cast(mat->m[0][1]) + + static_cast(mat->m[0][2])); + out[1] = static_cast( + static_cast(in[0])*static_cast(mat->m[1][0]) + + static_cast(in[1])*static_cast(mat->m[1][1]) + + static_cast(mat->m[1][2])); + return; +} +/* *************************************************************** */ +mat33 reg_mat33_mul(mat33 const* A, mat33 const* B) +{ + mat33 R; + for (int i = 0; i < 3; i++) + { + for (int j = 0; j < 3; j++) + { + R.m[i][j] = static_cast(static_cast(A->m[i][0]) * static_cast(B->m[0][j]) + + static_cast(A->m[i][1]) * static_cast(B->m[1][j]) + + static_cast(A->m[i][2]) * static_cast(B->m[2][j])); + } + } + return R; +} +/* *************************************************************** */ +mat33 operator*(mat33 A, mat33 B) +{ + return reg_mat33_mul(&A, &B); +} +/* *************************************************************** */ +/* *************************************************************** */ +mat33 reg_mat33_add(mat33 const* A, mat33 const* B) +{ + mat33 R; + for (int i = 0; i < 3; i++) + { + for (int j = 0; j < 3; j++) + { + R.m[i][j] = static_cast(static_cast(A->m[i][j]) + static_cast(B->m[i][j])); + } + } + return R; +} +/* *************************************************************** */ +/* *************************************************************** */ +mat33 reg_mat33_trans(mat33 A) +{ + mat33 R; + for (int i = 0; i < 3; i++) + { + for (int j = 0; j < 3; j++) + { + R.m[j][i] = A.m[i][j]; + } + } + return R; +} +/* *************************************************************** */ +/* *************************************************************** */ +mat33 operator+(mat33 A, mat33 B) +{ + return reg_mat33_add(&A, &B); +} +/* *************************************************************** */ +/* *************************************************************** */ +mat44 reg_mat44_add(mat44 const* A, mat44 const* B) +{ + mat44 R; + for (int i = 0; i < 4; i++) + { + for (int j = 0; j < 4; j++) + { + R.m[i][j] = static_cast(static_cast(A->m[i][j]) + static_cast(B->m[i][j])); + } + } + return R; +} +/* *************************************************************** */ +/* *************************************************************** */ +mat44 operator+(mat44 A, mat44 B) +{ + return reg_mat44_add(&A, &B); +} +/* *************************************************************** */ +/* *************************************************************** */ +mat33 reg_mat33_minus(mat33 const* A, mat33 const* B) +{ + mat33 R; + for (int i = 0; i < 3; i++) + { + for (int j = 0; j < 3; j++) + { + R.m[i][j] = static_cast(static_cast(A->m[i][j]) - static_cast(B->m[i][j])); + } + } + return R; +} +/* *************************************************************** */ +/* *************************************************************** */ +void reg_mat33_diagonalize(mat33 const* A, mat33 * Q, mat33 * D) +{ + // A must be a symmetric matrix. + // returns Q and D such that + // Diagonal matrix D = QT * A * Q; and A = Q*D*QT + const int maxsteps = 24; // certainly wont need that many. + int k0, k1, k2; + float o[3], m[3]; + float q[4] = { 0.0, 0.0, 0.0, 1.0 }; + float jr[4]; + float sqw, sqx, sqy, sqz; + float tmp1, tmp2, mq; + mat33 AQ; + float thet, sgn, t, c; + for (int i = 0; i < maxsteps; ++i) + { + // quat to matrix + sqx = q[0] * q[0]; + sqy = q[1] * q[1]; + sqz = q[2] * q[2]; + sqw = q[3] * q[3]; + Q->m[0][0] = (sqx - sqy - sqz + sqw); + Q->m[1][1] = (-sqx + sqy - sqz + sqw); + Q->m[2][2] = (-sqx - sqy + sqz + sqw); + tmp1 = q[0] * q[1]; + tmp2 = q[2] * q[3]; + Q->m[1][0] = 2.0 * (tmp1 + tmp2); + Q->m[0][1] = 2.0 * (tmp1 - tmp2); + tmp1 = q[0] * q[2]; + tmp2 = q[1] * q[3]; + Q->m[2][0] = 2.0 * (tmp1 - tmp2); + Q->m[0][2] = 2.0 * (tmp1 + tmp2); + tmp1 = q[1] * q[2]; + tmp2 = q[0] * q[3]; + Q->m[2][1] = 2.0 * (tmp1 + tmp2); + Q->m[1][2] = 2.0 * (tmp1 - tmp2); + + // AQ = A * Q + AQ.m[0][0] = Q->m[0][0] * A->m[0][0] + Q->m[1][0] * A->m[0][1] + Q->m[2][0] * A->m[0][2]; + AQ.m[0][1] = Q->m[0][1] * A->m[0][0] + Q->m[1][1] * A->m[0][1] + Q->m[2][1] * A->m[0][2]; + AQ.m[0][2] = Q->m[0][2] * A->m[0][0] + Q->m[1][2] * A->m[0][1] + Q->m[2][2] * A->m[0][2]; + AQ.m[1][0] = Q->m[0][0] * A->m[0][1] + Q->m[1][0] * A->m[1][1] + Q->m[2][0] * A->m[1][2]; + AQ.m[1][1] = Q->m[0][1] * A->m[0][1] + Q->m[1][1] * A->m[1][1] + Q->m[2][1] * A->m[1][2]; + AQ.m[1][2] = Q->m[0][2] * A->m[0][1] + Q->m[1][2] * A->m[1][1] + Q->m[2][2] * A->m[1][2]; + AQ.m[2][0] = Q->m[0][0] * A->m[0][2] + Q->m[1][0] * A->m[1][2] + Q->m[2][0] * A->m[2][2]; + AQ.m[2][1] = Q->m[0][1] * A->m[0][2] + Q->m[1][1] * A->m[1][2] + Q->m[2][1] * A->m[2][2]; + AQ.m[2][2] = Q->m[0][2] * A->m[0][2] + Q->m[1][2] * A->m[1][2] + Q->m[2][2] * A->m[2][2]; + // D = Qt * AQ + D->m[0][0] = AQ.m[0][0] * Q->m[0][0] + AQ.m[1][0] * Q->m[1][0] + AQ.m[2][0] * Q->m[2][0]; + D->m[0][1] = AQ.m[0][0] * Q->m[0][1] + AQ.m[1][0] * Q->m[1][1] + AQ.m[2][0] * Q->m[2][1]; + D->m[0][2] = AQ.m[0][0] * Q->m[0][2] + AQ.m[1][0] * Q->m[1][2] + AQ.m[2][0] * Q->m[2][2]; + D->m[1][0] = AQ.m[0][1] * Q->m[0][0] + AQ.m[1][1] * Q->m[1][0] + AQ.m[2][1] * Q->m[2][0]; + D->m[1][1] = AQ.m[0][1] * Q->m[0][1] + AQ.m[1][1] * Q->m[1][1] + AQ.m[2][1] * Q->m[2][1]; + D->m[1][2] = AQ.m[0][1] * Q->m[0][2] + AQ.m[1][1] * Q->m[1][2] + AQ.m[2][1] * Q->m[2][2]; + D->m[2][0] = AQ.m[0][2] * Q->m[0][0] + AQ.m[1][2] * Q->m[1][0] + AQ.m[2][2] * Q->m[2][0]; + D->m[2][1] = AQ.m[0][2] * Q->m[0][1] + AQ.m[1][2] * Q->m[1][1] + AQ.m[2][2] * Q->m[2][1]; + D->m[2][2] = AQ.m[0][2] * Q->m[0][2] + AQ.m[1][2] * Q->m[1][2] + AQ.m[2][2] * Q->m[2][2]; + o[0] = D->m[1][2]; + o[1] = D->m[0][2]; + o[2] = D->m[0][1]; + m[0] = fabs(o[0]); + m[1] = fabs(o[1]); + m[2] = fabs(o[2]); + + k0 = (m[0] > m[1] && m[0] > m[2]) ? 0 : (m[1] > m[2]) ? 1 : 2; // index of largest element of offdiag + k1 = (k0 + 1) % 3; + k2 = (k0 + 2) % 3; + if (o[k0] == 0.0) + { + break; // diagonal already + } + thet = (D->m[k2][k2] - D->m[k1][k1]) / (2.0*o[k0]); + sgn = (thet > 0.0) ? 1.0 : -1.0; + thet *= sgn; // make it positive + t = sgn / (thet + ((thet < 1.E6) ? sqrt(thet*thet + 1.0) : thet)); // sign(T)/(|T|+sqrt(T^2+1)) + c = 1.0 / sqrt(t*t + 1.0); // c= 1/(t^2+1) , t=s/c + if (c == 1.0) + { + break; // no room for improvement - reached machine precision. + } + jr[0] = jr[1] = jr[2] = jr[3] = 0.0; + jr[k0] = sgn*sqrt((1.0 - c) / 2.0); // using 1/2 angle identity sin(a/2) = sqrt((1-cos(a))/2) + jr[k0] *= -1.0; // since our quat-to-matrix convention was for v*M instead of M*v + jr[3] = sqrt(1.0f - jr[k0] * jr[k0]); + if (jr[3] == 1.0) + { + break; // reached limits of floating point precision + } + q[0] = (q[3] * jr[0] + q[0] * jr[3] + q[1] * jr[2] - q[2] * jr[1]); + q[1] = (q[3] * jr[1] - q[0] * jr[2] + q[1] * jr[3] + q[2] * jr[0]); + q[2] = (q[3] * jr[2] + q[0] * jr[1] - q[1] * jr[0] + q[2] * jr[3]); + q[3] = (q[3] * jr[3] - q[0] * jr[0] - q[1] * jr[1] - q[2] * jr[2]); + mq = sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]); + q[0] /= mq; + q[1] /= mq; + q[2] /= mq; + q[3] /= mq; + } +} + +/* *************************************************************** */ +/* *************************************************************** */ +mat33 operator-(mat33 A, mat33 B) +{ + return reg_mat33_minus(&A, &B); +} +/* *************************************************************** */ +/* *************************************************************** */ +void reg_mat33_eye(mat33 *mat) +{ + mat->m[0][0] = 1.f; + mat->m[0][1] = mat->m[0][2] = 0.f; + mat->m[1][1] = 1.f; + mat->m[1][0] = mat->m[1][2] = 0.f; + mat->m[2][2] = 1.f; + mat->m[2][0] = mat->m[2][1] = 0.f; +} +/* *************************************************************** */ +/* *************************************************************** */ +mat44 reg_mat44_minus(mat44 const* A, mat44 const* B) +{ + mat44 R; + for (int i = 0; i < 4; i++) + { + for (int j = 0; j < 4; j++) + { + R.m[i][j] = static_cast(static_cast(A->m[i][j]) - static_cast(B->m[i][j])); + } + } + return R; +} + +/* *************************************************************** */ +/* *************************************************************** */ +mat44 operator-(mat44 A, mat44 B) +{ + return reg_mat44_minus(&A, &B); +} + +/* *************************************************************** */ +/* *************************************************************** */ +void reg_mat44_eye(mat44 *mat) +{ + mat->m[0][0] = 1.f; + mat->m[0][1] = mat->m[0][2] = mat->m[0][3] = 0.f; + mat->m[1][1] = 1.f; + mat->m[1][0] = mat->m[1][2] = mat->m[1][3] = 0.f; + mat->m[2][2] = 1.f; + mat->m[2][0] = mat->m[2][1] = mat->m[2][3] = 0.f; + mat->m[3][3] = 1.f; + mat->m[3][0] = mat->m[3][1] = mat->m[3][2] = 0.f; +} +/* *************************************************************** */ +/* *************************************************************** */ +float reg_mat44_norm_inf(mat44 const* mat) +{ + float maxval = 0.0; + float newval = 0.0; + for (int i = 0; i < 4; i++) + { + for (int j = 0; j < 4; j++) + { + newval = fabsf(mat->m[i][j]); + maxval = (newval > maxval) ? newval : maxval; + } + } + return maxval; +} +/* *************************************************************** */ +/* *************************************************************** */ +void reg_mat44_mul(mat44 const* mat, + float const* in, + float *out) +{ + out[0] = static_cast(static_cast(mat->m[0][0]) * static_cast(in[0]) + + static_cast(mat->m[0][1]) * static_cast(in[1]) + + static_cast(mat->m[0][2]) * static_cast(in[2]) + + static_cast(mat->m[0][3])); + out[1] = static_cast(static_cast(mat->m[1][0]) * static_cast(in[0]) + + static_cast(mat->m[1][1]) * static_cast(in[1]) + + static_cast(mat->m[1][2]) * static_cast(in[2]) + + static_cast(mat->m[1][3])); + out[2] = static_cast(static_cast(mat->m[2][0]) * static_cast(in[0]) + + static_cast(mat->m[2][1]) * static_cast(in[1]) + + static_cast(mat->m[2][2]) * static_cast(in[2]) + + static_cast(mat->m[2][3])); +} +/* *************************************************************** */ +/* *************************************************************** */ +void reg_mat44_mul(mat44 const* mat, + double const* in, + double *out) +{ + double matD[4][4]; + for (int i = 0; i < 4; ++i) + for (int j = 0; j < 4; ++j) + matD[i][j] = static_cast(mat->m[i][j]); + + out[0] = matD[0][0] * in[0] + + matD[0][1] * in[1] + + matD[0][2] * in[2] + + matD[0][3]; + out[1] = matD[1][0] * in[0] + + matD[1][1] * in[1] + + matD[1][2] * in[2] + + matD[1][3]; + out[2] = matD[2][0] * in[0] + + matD[2][1] * in[1] + + matD[2][2] * in[2] + + matD[2][3]; + return; +} +/* *************************************************************** */ +/* *************************************************************** */ +mat44 reg_mat44_mul(mat44 const* A, double scalar) +{ + mat44 out; + out.m[0][0] = A->m[0][0] * scalar; + out.m[0][1] = A->m[0][1] * scalar; + out.m[0][2] = A->m[0][2] * scalar; + out.m[0][3] = A->m[0][3] * scalar; + out.m[1][0] = A->m[1][0] * scalar; + out.m[1][1] = A->m[1][1] * scalar; + out.m[1][2] = A->m[1][2] * scalar; + out.m[1][3] = A->m[1][3] * scalar; + out.m[2][0] = A->m[2][0] * scalar; + out.m[2][1] = A->m[2][1] * scalar; + out.m[2][2] = A->m[2][2] * scalar; + out.m[2][3] = A->m[2][3] * scalar; + out.m[3][0] = A->m[3][0] * scalar; + out.m[3][1] = A->m[3][1] * scalar; + out.m[3][2] = A->m[3][2] * scalar; + out.m[3][3] = A->m[3][3] * scalar; + return out; +} +/* *************************************************************** */ +void reg_mat44_disp(mat44 *mat, char * title){ + printf("%s:\n%.7g\t%.7g\t%.7g\t%.7g\n%.7g\t%.7g\t%.7g\t%.7g\n%.7g\t%.7g\t%.7g\t%.7g\n%.7g\t%.7g\t%.7g\t%.7g\n", title, + mat->m[0][0], mat->m[0][1], mat->m[0][2], mat->m[0][3], + mat->m[1][0], mat->m[1][1], mat->m[1][2], mat->m[1][3], + mat->m[2][0], mat->m[2][1], mat->m[2][2], mat->m[2][3], + mat->m[3][0], mat->m[3][1], mat->m[3][2], mat->m[3][3]); +} + +/* *************************************************************** */ +/* *************************************************************** */ +void reg_mat33_disp(mat33 *mat, char * title){ + printf("%s:\n%g\t%g\t%g\n%g\t%g\t%g\n%g\t%g\t%g\n", title, + mat->m[0][0], mat->m[0][1], mat->m[0][2], + mat->m[1][0], mat->m[1][1], mat->m[1][2], + mat->m[2][0], mat->m[2][1], mat->m[2][2]); +} +/* *************************************************************** */ +//is it square distance or just distance? +// Helper function: Get the square of the Euclidean distance +double get_square_distance3D(float * first_point3D, float * second_point3D) { + return sqrt(reg_pow2(first_point3D[0] - second_point3D[0]) + + reg_pow2(first_point3D[1] - second_point3D[1]) + + reg_pow2(first_point3D[2] - second_point3D[2])); +} +/* *************************************************************** */ +//is it square distance or just distance? +double get_square_distance2D(float * first_point2D, float * second_point2D) { + return sqrt(reg_pow2(first_point2D[0] - second_point2D[0]) + + reg_pow2(first_point2D[1] - second_point2D[1])); +} +/* *************************************************************** */ +// Calculate pythagorean distance +template +T pythag(T a, T b) +{ + T absa, absb; + absa = fabs(a); + absb = fabs(b); + + if (absa > absb) + return (T)(absa * sqrt(1.0f + SQR(absb / absa))); + else + return (absb == 0.0f ? 0.0f : (T)(absb * sqrt(1.0f + SQR(absa / absb)))); +} +#endif // _REG_MATHS_CPP diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_maths.h b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_maths.h new file mode 100644 index 00000000..4e34b603 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_maths.h @@ -0,0 +1,289 @@ +/** + * @file _reg_maths.h + * @brief Library that contains small math routines + * @author Marc Modat + * @date 25/03/2009 + * + * Created by Marc Modat on 25/03/2009. + * Copyright (c) 2009, University College London. All rights reserved. + * Centre for Medical Image Computing (CMIC) + * See the LICENSE.txt file in the nifty_reg root folder + * + */ +#ifndef _REG_MATHS_H +#define _REG_MATHS_H + +#include +#include +#include +#include +#include +#include "nifti1_io.h" + +#if defined (_OPENMP) +#include +#endif + +#if _USE_SSE +#include +#include +#ifdef __SSE3__ +#include +#endif +#endif + +#define NR_THROW_EXCEP 1 + +typedef enum +{ + DEF_FIELD, + DISP_FIELD, + CUB_SPLINE_GRID, + DEF_VEL_FIELD, + DISP_VEL_FIELD, + SPLINE_VEL_GRID, + LIN_SPLINE_GRID +} NREG_TRANS_TYPE; + +/* *************************************************************** */ +#define reg_pow2(a) ((a)*(a)) +#define reg_ceil(a) (ceil(a)) +#define reg_round(a) ((a)>0.0 ?(int)((a)+0.5):(int)((a)-0.5)) +#ifdef _WIN32 +#define reg_floor(a) ((a)>0?(int)(a):(int)((a)-1)) +#define reg_floor_size_t(a) ((a)>0?(long)(a):(long)((a)-1)) +#else +#define reg_floor(a) ((a)>=0?(int)(a):floor(a)) +#endif +#define SIGN(a,b) ((b) >= 0.0 ? fabs(a) : -fabs(a)) +#define FMAX(a,b) (a > b ? a : b) +#define IMIN(a,b) (a < b ? a : b) +#define SQR(a) (a==0.0 ? 0.0 : a*a) +/* *************************************************************** */ +#ifdef RNIFTYREG +#include // This may have to change to Rcpp.h or RcppEigen.h later +#define reg_exit(){error("[NiftyReg] Fatal error");} +#define reg_print_info(executable,text){Rprintf("[%s] %s\n", executable, text);} +#define reg_print_fct_debug(text){Rprintf("[NiftyReg DEBUG] Function: %s called\n", text);} +#define reg_print_msg_debug(text){Rprintf("[NiftyReg DEBUG] %s\n", text);} +#define reg_print_fct_warn(text){REprintf("[NiftyReg WARNING] Function: %s\n", text);} +#define reg_print_msg_warn(text){REprintf("[NiftyReg WARNING] %s\n", text);} +#define reg_print_fct_error(text){REprintf("[NiftyReg ERROR] Function: %s\n", text);} +#define reg_print_msg_error(text){REprintf("[NiftyReg ERROR] %s\n", text);} +#else +#ifdef NR_THROW_EXCEP +#define reg_exit(){ \ + throw std::runtime_error("[NiftyReg] Exception"); \ +} +#else // NR_THROW_EXCEP +#define reg_exit(){ \ + fprintf(stderr,"[NiftyReg] Exit here. File: %s:%i\n",__FILE__, __LINE__); \ + exit(1); \ +} +#endif // NR_THROW_EXCEP +#define reg_print_info(executable,text){printf("[%s] %s\n", executable, text);} +#define reg_print_fct_debug(text){printf("[NiftyReg DEBUG] Function: %s called\n", text);} +#define reg_print_msg_debug(text){printf("[NiftyReg DEBUG] %s\n", text);} +#define reg_print_fct_warn(text){printf("[NiftyReg WARNING] Function: %s\n", text);} +#define reg_print_msg_warn(text){printf("[NiftyReg WARNING] %s\n", text);} +#define reg_print_fct_error(text){fprintf(stderr,"[NiftyReg ERROR] Function: %s\n", text);} +#define reg_print_msg_error(text){fprintf(stderr,"[NiftyReg ERROR] %s\n", text);} +#endif +/* *************************************************************** */ +#if defined(_WIN32) && !defined(__CYGWIN__) +#include +#include +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif +#ifndef isnan +#define isnan(_X) _isnan(_X) +#endif +#if (_MSC_VER < 1900) +#ifndef strtof +#define strtof(_s, _t) (float) strtod(_s, _t) +#endif +#endif +template inline int round(PrecisionType x) +{ + return int(x > 0.0 ? (x + 0.5) : (x - 0.5)); +} +#if _MSC_VER < 1800 //test if visual studio version older than 2013 +templateinline bool isinf(T value) +{ + return std::numeric_limits::has_infinity && value == std::numeric_limits::infinity(); +} +#endif +inline int fabs(int _x) +{ + return (int)fabs((float)(_x)); +} +#endif // If on windows... +/* *************************************************************** */ +extern "C++" template +void reg_LUdecomposition(T *inputMatrix, + size_t dim, + size_t *index); +/* *************************************************************** */ +extern "C++" template +void reg_matrixMultiply(T *mat1, + T *mat2, + size_t *dim1, + size_t *dim2, + T * &res); +/* *************************************************************** */ +extern "C++" template +void reg_matrixInvertMultiply(T *mat, + size_t dim, + size_t *index, + T *vec); +/* *************************************************************** */ +/* *************************************************************** */ +/* *************************************************************** */ +/* *************************************************************** */ +extern "C++" template +T* reg_matrix1DAllocate(size_t arraySize); +/* *************************************************************** */ +extern "C++" template +T* reg_matrix1DAllocateAndInitToZero(size_t arraySize); +/* *************************************************************** */ +extern "C++" template +void reg_matrix1DDeallocate(T* mat); +/* *************************************************************** */ +extern "C++" template +T** reg_matrix2DAllocate(size_t arraySizeX, size_t arraySizeY); +/* *************************************************************** */ +extern "C++" template +T** reg_matrix2DAllocateAndInitToZero(size_t arraySizeX, size_t arraySizeY); +/* *************************************************************** */ +extern "C++" template +void reg_matrix2DDeallocate(size_t arraySizeX, T** mat); +/* *************************************************************** */ +extern "C++" template +T** reg_matrix2DTranspose(T** mat, size_t arraySizeX, size_t arraySizeY); +/* *************************************************************** */ +extern "C++" template +T** reg_matrix2DMultiply(T** mat1, size_t mat1X, size_t mat1Y, T** mat2, size_t mat2X, size_t mat2Y, bool transposeMat2); +extern "C++" template +void reg_matrix2DMultiply(T** mat1, size_t mat1X, size_t mat1Y, T** mat2, size_t mat2X, size_t mat2Y, T** res, bool transposeMat2); +/* *************************************************************** */ +extern "C++" template +T* reg_matrix2DVectorMultiply(T** mat, size_t m, size_t n, T* vect); +extern "C++" template +void reg_matrix2DVectorMultiply(T** mat, size_t m, size_t n, T* vect, T* res); +/* *************************************************************** */ +/* *************************************************************** */ +/* *************************************************************** */ +/* *************************************************************** */ +/** @brief Add two 3-by-3 matrices +*/ +mat33 reg_mat33_add(mat33 const* A, mat33 const* B); +mat33 operator+(mat33 A, mat33 B); +/* *************************************************************** */ +/** @brief Multipy two 3-by-3 matrices +*/ +mat33 reg_mat33_mul(mat33 const* A, + mat33 const* B); +mat33 operator*(mat33 A, + mat33 B); +/* *************************************************************** */ +//The mat33 represent a 3x3 matrix +void reg_mat33_mul(mat44 const* mat, float const* in, float *out); +void reg_mat33_mul(mat33 const* mat, float const* in, float *out); +/* *************************************************************** */ +/** @brief Substract two 3-by-3 matrices +*/ +mat33 reg_mat33_minus(mat33 const* A, mat33 const* B); +mat33 operator-(mat33 A, mat33 B); +/* *************************************************************** */ +/** @brief Transpose a 3-by-3 matrix +*/ +mat33 reg_mat33_trans(mat33 A); +/* *************************************************************** */ +/** @brief Diagonalize a 3-by-3 matrix +*/ +void reg_mat33_diagonalize(mat33 const* A, mat33 * Q, mat33 * D); +/* *************************************************************** */ +/** @brief Set up a 3-by-3 matrix with an identity +*/ +void reg_mat33_eye(mat33 *mat); +/* *************************************************************** */ +/** @brief Compute the determinant of a 3-by-3 matrix +*/ +template T reg_mat33_det(mat33 const* A); +/* *************************************************************** */ +/** @brief Compute the determinant of a 3-by-3 matrix +*/ +void reg_mat33_to_nan(mat33 *A); +/* *************************************************************** */ +/** @brief Transform a mat44 to a mat33 matrix +*/ +mat33 reg_mat44_to_mat33(mat44 const* A); +extern "C++" +void reg_heapSort(float *array_tmp, int *index_tmp, int blockNum); +/* *************************************************************** */ +extern "C++" template +void reg_heapSort(T *array_tmp,int blockNum); +/* *************************************************************** */ +/* *************************************************************** */ +bool operator==(mat44 A,mat44 B); +/* *************************************************************** */ +bool operator!=(mat44 A,mat44 B); +/* *************************************************************** */ +/** @brief Multipy two 4-by-4 matrices + */ +mat44 reg_mat44_mul(mat44 const* A, + mat44 const* B); +mat44 operator*(mat44 A, + mat44 B); +/* *************************************************************** */ +/** @brief Multipy a vector with a 4-by-4 matrix + */ +void reg_mat44_mul(mat44 const* mat, + float const* in, + float *out); + +void reg_mat44_mul(mat44 const* mat, + double const* in, + double *out); +/* *************************************************************** */ +/** @brief Multipy a 4-by-4 matrix with a scalar + */ +mat44 reg_mat44_mul(mat44 const* mat, + double scalar); +/* *************************************************************** */ +/** @brief Add two 4-by-4 matrices + */ +mat44 reg_mat44_add(mat44 const* A, mat44 const* B); +mat44 operator+(mat44 A,mat44 B); +/* *************************************************************** */ +/** @brief Substract two 4-by-4 matrices + */ +mat44 reg_mat44_minus(mat44 const* A, mat44 const* B); +mat44 operator-(mat44 A,mat44 B); +/* *************************************************************** */ +/** @brief Set up a 4-by-4 matrix with an identity + */ +void reg_mat44_eye(mat44 *mat); +/* *************************************************************** */ +/** @brief Compute the determinant of a 4-by-4 matrix + */ +template T reg_mat44_det(mat44 const* A); +/* *************************************************************** */ +float reg_mat44_norm_inf(mat44 const* mat); +/* *************************************************************** */ +/** @brief Display a mat44 matrix + */ +void reg_mat44_disp(mat44 *mat, + char * title); +/* *************************************************************** */ +/** @brief Display a mat33 matrix + */ +void reg_mat33_disp(mat33 *mat, + char * title); +/* *************************************************************** */ +double get_square_distance3D(float * first_point3D, float * second_point3D); +/* *************************************************************** */ +double get_square_distance2D(float * first_point2D, float * second_point2D); +/* *************************************************************** */ +#endif // _REG_MATHS_H diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_maths_eigen.cpp b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_maths_eigen.cpp new file mode 100644 index 00000000..b9dc020d --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_maths_eigen.cpp @@ -0,0 +1,362 @@ +#define USE_EIGEN + +#include "_reg_maths_eigen.h" +#include "_reg_maths.h" +#include "nifti1_io.h" + +// Eigen headers are in there because of the nvcc preprocessing step +#include "Eigen/Core" +#include "Eigen/SVD" +#include "unsupported/Eigen/MatrixFunctions" + +//_reg_maths_eigen.cpp +/* *************************************************************** */ +/** @brief SVD +* @param in input matrix to decompose - in place +* @param size_m row +* @param size_n colomn +* @param w diagonal term +* @param v rotation part +*/ +template +void svd(T **in, size_t size_m, size_t size_n, T * w, T **v) { + if (size_m == 0 || size_n == 0) { + reg_print_fct_error("svd"); + reg_print_msg_error("The specified matrix is empty"); + reg_exit(); + } + +#ifdef _WIN32 + long sm, sn, sn2; + long size__m = (long)size_m, size__n = (long)size_n; +#else + size_t sm, sn, sn2; + size_t size__m = size_m, size__n = size_n; +#endif + Eigen::MatrixXd m(size_m, size_n); + + //Convert to Eigen matrix +#if defined (_OPENMP) +#pragma omp parallel for default(none) \ + shared(in,m, size__m, size__n) \ + private(sm, sn) +#endif + for (sm = 0; sm < size__m; sm++) + { + for (sn = 0; sn < size__n; sn++) + { + m(sm, sn) = static_cast(in[sm][sn]); + } + } + + Eigen::JacobiSVD svd(m, Eigen::ComputeThinU | Eigen::ComputeThinV); + +#if defined (_OPENMP) +#pragma omp parallel for default(none) \ + shared(in,svd,v,w, size__n,size__m) \ + private(sn2, sn, sm) +#endif + for (sn = 0; sn < size__n; sn++) { + w[sn] = static_cast(svd.singularValues()(sn)); + for (sn2 = 0; sn2 < size__n; sn2++) { + v[sn2][sn] = static_cast(svd.matrixV()(sn2, sn)); + } + for (sm = 0; sm < size__m; sm++) { + in[sm][sn] = static_cast(svd.matrixU()(sm, sn)); + } + } +} +template void svd(float **in, size_t m, size_t n, float * w, float **v); +template void svd(double **in, size_t m, size_t n, double * w, double **v); +/* *************************************************************** */ +/** +* @brief SVD +* @param in input matrix to decompose +* @param size_m row +* @param size_n colomn +* @param U unitary matrices +* @param S diagonal matrix +* @param V unitary matrices +* X = U*S*V' +*/ +template +void svd(T **in, size_t size_m, size_t size_n, T ***U, T ***S, T ***V) { + if (in == NULL) { + reg_print_fct_error("svd"); + reg_print_msg_error("The specified matrix is empty"); + reg_exit(); + } + +#ifdef _WIN32 + long sm, sn, sn2, min_dim, i, j; + long size__m = (long)size_m, size__n = (long)size_n; +#else + size_t sm, sn, min_dim, i, j; + size_t size__m = size_m, size__n = size_n; +#endif + Eigen::MatrixXd m(size__m, size__n); + + //Convert to Eigen matrix +#if defined (_OPENMP) +#pragma omp parallel for default(none) \ + shared(in, m, size__m, size__n) \ + private(sm, sn) +#endif + for (sm = 0; sm < size__m; sm++) + { + for (sn = 0; sn < size__n; sn++) + { + m(sm, sn) = static_cast(in[sm][sn]); + } + } + + Eigen::JacobiSVD svd(m, Eigen::ComputeThinU | Eigen::ComputeThinV); + + min_dim = std::min(size__m, size__n); +#if defined (_OPENMP) +#pragma omp parallel for default(none) \ + shared(svd, min_dim, S) \ + private(i, j) +#endif + //Convert to C matrix + for (i = 0; i < min_dim; i++) { + for (j = 0; j < min_dim; j++) { + if (i == j) { + (*S)[i][j] = static_cast(svd.singularValues()(i)); + } + else { + (*S)[i][j] = 0; + } + } + } + + if (size__m > size__n) { +#if defined (_OPENMP) +#pragma omp parallel for default(none) \ + shared(svd, min_dim, V) \ + private(i, j) +#endif + //Convert to C matrix + for (i = 0; i < min_dim; i++) { + for (j = 0; j < min_dim; j++) { + (*V)[i][j] = static_cast(svd.matrixV()(i, j)); + + } + } +#if defined (_OPENMP) +#pragma omp parallel for default(none) \ + shared(svd, size__m, size__n, U) \ + private(i, j) +#endif + for (i = 0; i < size__m; i++) { + for (j = 0; j < size__n; j++) { + (*U)[i][j] = static_cast(svd.matrixU()(i, j)); + } + } + } + else { +#if defined (_OPENMP) +#pragma omp parallel for default(none) \ + shared(svd, min_dim, U) \ + private(i, j) +#endif + //Convert to C matrix + for (i = 0; i < min_dim; i++) { + for (j = 0; j < min_dim; j++) { + (*U)[i][j] = static_cast(svd.matrixU()(i, j)); + + } + } +#if defined (_OPENMP) +#pragma omp parallel for default(none) \ + shared(svd, size__m, size__n, V) \ + private(i, j) +#endif + for (i = 0; i < size__n; i++) { + for (j = 0; j < size__m; j++) { + (*V)[i][j] = static_cast(svd.matrixV()(i, j)); + } + } + } + +} +template void svd(float **in, size_t size_m, size_t size_n, float ***U, float ***S, float ***V); +template void svd(double **in, size_t size_m, size_t size_n, double ***U, double ***S, double ***V); +/* *************************************************************** */ +template +T reg_matrix2DDet(T** mat, size_t m, size_t n) { + if (m != n) { + char text[255]; sprintf(text, "The matrix have to be square: [%zu %zu]", + m, n); + reg_print_fct_error("reg_matrix2DDeterminant"); + reg_print_msg_error(text); + reg_exit(); + } + double res; + if (m == 2) { + res = static_cast(mat[0][0]) * static_cast(mat[1][1]) - static_cast(mat[1][0]) * static_cast(mat[0][1]); + } + else if (m == 3) { + res = (static_cast(mat[0][0]) * (static_cast(mat[1][1]) * static_cast(mat[2][2]) - static_cast(mat[1][2]) * static_cast(mat[2][1]))) - + (static_cast(mat[0][1]) * (static_cast(mat[1][0]) * static_cast(mat[2][2]) - static_cast(mat[1][2]) * static_cast(mat[2][0]))) + + (static_cast(mat[0][2]) * (static_cast(mat[1][0]) * static_cast(mat[2][1]) - static_cast(mat[1][1]) * static_cast(mat[2][0]))); + } + else { + // Convert to Eigen format + Eigen::MatrixXd eigenRes(m, n); + for (size_t i = 0; i < m; i++) { + for (size_t j = 0; j < n; j++) { + eigenRes(i, j) = static_cast(mat[i][j]); + } + } + res = eigenRes.determinant(); + } + return static_cast(res); +} +template float reg_matrix2DDet(float** mat, size_t m, size_t n); +template double reg_matrix2DDet(double** mat, size_t m, size_t n); +/* *************************************************************** */ +mat44 reg_mat44_sqrt(mat44 const* mat) +{ + mat44 X; + Eigen::Matrix4d m; + for (size_t i = 0; i < 4; ++i) + { + for (size_t j = 0; j < 4; ++j) + { + m(i, j) = static_cast(mat->m[i][j]); + } + } + m = m.sqrt(); + for (size_t i = 0; i < 4; ++i) + for (size_t j = 0; j < 4; ++j) + X.m[i][j] = static_cast(m(i, j)); + return X; +} +/* *************************************************************** */ +void reg_mat33_expm(mat33 *in_tensor) +{ + int sm, sn; + Eigen::Matrix3d tensor; + + // Convert to Eigen format + for (sm = 0; sm < 3; sm++){ + for (sn = 0; sn < 3; sn++){ + float val=in_tensor->m[sm][sn]; + if(val!=val) return; + tensor(sm, sn) = static_cast(val); + } + } + + // Compute exp(E) + tensor = tensor.exp(); + + // Convert the result to mat33 format + for (sm = 0; sm < 3; sm++) + for (sn = 0; sn < 3; sn++) + in_tensor->m[sm][sn] = static_cast(tensor(sm, sn)); +} +/* *************************************************************** */ +mat44 reg_mat44_expm(mat44 const* mat) +{ + mat44 X; + Eigen::Matrix4d m; + for (size_t i = 0; i < 4; ++i) { + for (size_t j = 0; j < 4; ++j) { + m(i, j) = static_cast(mat->m[i][j]); + } + } + m = m.exp(); + // + for (size_t i = 0; i < 4; ++i) + for (size_t j = 0; j < 4; ++j) + X.m[i][j] = static_cast(m(i, j)); + + return X; +} +/* *************************************************************** */ +void reg_mat33_logm(mat33 *in_tensor) +{ + int sm, sn; + Eigen::Matrix3d tensor; + + // Convert to Eigen format + bool all_zeros = true; + double det = 0; + for (sm = 0; sm < 3; sm++){ + for (sn = 0; sn < 3; sn++){ + float val=in_tensor->m[sm][sn]; + if(val!=0.f) all_zeros=false; + if(val!=val) return; + tensor(sm, sn) = static_cast(val); + } + } + // Actually R case requires invertible and no negative real ev, + // but the only observed case so far was non-invertible. + // determinant is not a perfect check for invertibility and + // identity with zero not great either, but the alternative + // is a general eigensolver and the logarithm function should + // suceed unless convergence just isn't happening. + det = tensor.determinant(); + if(all_zeros==true || det == 0){ + reg_mat33_to_nan(in_tensor); + return; + } + + // Compute the actual matrix log + tensor = tensor.log(); + + // Convert the result to mat33 format + for (sm = 0; sm < 3; sm++) + for (sn = 0; sn < 3; sn++) + in_tensor->m[sm][sn] = static_cast(tensor(sm, sn)); +} +/* *************************************************************** */ +mat44 reg_mat44_logm(mat44 const* mat) +{ + mat44 X; + Eigen::Matrix4d m; + for (size_t i = 0; i < 4; ++i) { + for (size_t j = 0; j < 4; ++j) { + m(i, j) = static_cast(mat->m[i][j]); + } + } + m = m.log(); + for (size_t i = 0; i < 4; ++i) + for (size_t j = 0; j < 4; ++j) + X.m[i][j] = static_cast(m(i, j)); + return X; +} +/* *************************************************************** */ +mat44 reg_mat44_inv(mat44 const* mat) +{ + mat44 out; + Eigen::Matrix4d m, m_inv; + for (size_t i = 0; i < 4; ++i) { + for (size_t j = 0; j < 4; ++j) { + m(i, j) = static_cast(mat->m[i][j]); + } + } + m_inv = m.inverse(); + for (size_t i = 0; i < 4; ++i) + for (size_t j = 0; j < 4; ++j) + out.m[i][j] = static_cast(m_inv(i, j)); + // + return out; + +} +/* *************************************************************** */ +mat44 reg_mat44_avg2(mat44 const* A, mat44 const* B) +{ + mat44 out; + mat44 logA = reg_mat44_logm(A); + mat44 logB = reg_mat44_logm(B); + for (int i = 0; i < 4; ++i) { + logA.m[3][i] = 0.f; + logB.m[3][i] = 0.f; + } + logA = reg_mat44_add(&logA, &logB); + out = reg_mat44_mul(&logA, 0.5); + return reg_mat44_expm(&out); + +} diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_maths_eigen.h b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_maths_eigen.h new file mode 100644 index 00000000..5ac56dd9 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_maths_eigen.h @@ -0,0 +1,51 @@ +//_reg_maths_eigen.h +#ifndef _REG_MATHS_EIGEN_H +#define _REG_MATHS_EIGEN_H + +#include "nifti1_io.h" + +/* *************************************************************** */ +/* Functions calling the Eigen library */ +/* See http://eigen.tuxfamily.org/index.php?title=Main_Page */ +/* *************************************************************** */ + +/* *************************************************************** */ +extern "C++" template +void svd(T **in, size_t m, size_t n, T * w, T **v); +/* *************************************************************** */ +extern "C++" template +void svd(T **in, size_t m, size_t n, T ***U, T ***S, T ***V); +/* *************************************************************** */ +extern "C++" template +T reg_matrix2DDet(T** mat, size_t m, size_t n); +/* *************************************************************** */ +/** @brief Compute the inverse of a 4-by-4 matrix +*/ +mat44 reg_mat44_inv(mat44 const* mat); +/* *************************************************************** */ +/** @brief Compute the square root of a 4-by-4 matrix +*/ +mat44 reg_mat44_sqrt(mat44 const* mat); +/* *************************************************************** */ +/** @brief Compute the log of a 3-by-3 matrix +*/ +void reg_mat33_expm(mat33 *in_tensor); +/* *************************************************************** */ +/** @brief Compute the exp of a 4-by-4 matrix +*/ +mat44 reg_mat44_expm(const mat44 *mat); +/* *************************************************************** */ +/** @brief Compute the log of a 3-by-3 matrix +*/ +void reg_mat33_logm(mat33 *in_tensor); +/* *************************************************************** */ +/** @brief Compute the log of a 4-by-4 matrix +*/ +mat44 reg_mat44_logm(const mat44 *mat); +/* *************************************************************** */ +/** @brief Compute the average of two matrices using a log-euclidean +* framework +*/ +mat44 reg_mat44_avg2(mat44 const* A, mat44 const* b); + +#endif diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_resampling.cpp b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_resampling.cpp new file mode 100755 index 00000000..4b6ab856 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_resampling.cpp @@ -0,0 +1,1415 @@ +/* + * _reg_resampling.cpp + * + * + * Created by Marc Modat on 24/03/2009. + * Copyright (c) 2009, University College London. All rights reserved. + * Centre for Medical Image Computing (CMIC) + * See the LICENSE.txt file in the nifty_reg root folder + * + */ + +#ifndef _REG_RESAMPLING_CPP +#define _REG_RESAMPLING_CPP + +#include "_reg_resampling.h" +#include "_reg_maths.h" +#include "_reg_maths_eigen.h" +#include "_reg_tools.h" +#include "interpolations.h" + +#include + +#define SINC_KERNEL_RADIUS 3 +#define SINC_KERNEL_SIZE SINC_KERNEL_RADIUS*2 + +/* *************************************************************** */ +void interpWindowedSincKernel(double relative, double *basis) +{ + if(relative<0.0) relative=0.0; //reg_rounding error + int j=0; + double sum=0.; + for(int i=-SINC_KERNEL_RADIUS; i(i); + if(x==0.0) + basis[j]=1.0; + else if(fabs(x)>=static_cast(SINC_KERNEL_RADIUS)) + basis[j]=0; + else{ + double pi_x=M_PI*x; + basis[j]=static_cast(SINC_KERNEL_RADIUS) * + sin(pi_x) * + sin(pi_x/static_cast(SINC_KERNEL_RADIUS)) / + (pi_x*pi_x); + } + sum+=basis[j]; + j++; + } + for(int i=0;i=static_cast(kernelsize)) + return 0; + else{ + double pi_x=M_PI*fabs(x); + return static_cast(kernelsize) * + sin(pi_x) * + sin(pi_x/static_cast(kernelsize)) / + (pi_x*pi_x); + } +} +/* *************************************************************** */ +/* *************************************************************** */ +void interpLinearKernel(double relative, double *basis) +{ + if(relative<0.0) relative=0.0; //reg_rounding error + basis[1]=relative; + basis[0]=1.0-relative; +} +/* *************************************************************** */ +void interpLinearKernel(double relative, double *basis, double *derivative) +{ + interpLinearKernel(relative,basis); + derivative[1]=1.0; + derivative[0]=0.0; +} +/* *************************************************************** */ +/* *************************************************************** */ +void interpNearestNeighKernel(double relative, double *basis) +{ + if(relative<0.0) relative=0.0; //reg_rounding error + basis[0]=basis[1]=0; + if(relative>=0.5) + basis[1]=1; + else basis[0]=1; +} +/* *************************************************************** */ +template +void ResampleImage3D(nifti_image *floatingImage, + nifti_image *deformationField, + nifti_image *warpedImage, + FieldTYPE paddingValue, + int kernel) +{ +#ifdef _WIN32 + long index; + long warpedVoxelNumber = (long)warpedImage->nx*warpedImage->ny*warpedImage->nz; + long floatingVoxelNumber = (long)floatingImage->nx*floatingImage->ny*floatingImage->nz; +#else + size_t index; + size_t warpedVoxelNumber = (size_t)warpedImage->nx*warpedImage->ny*warpedImage->nz; + size_t floatingVoxelNumber = (size_t)floatingImage->nx*floatingImage->ny*floatingImage->nz; +#endif + FloatingTYPE *floatingIntensityPtr = static_cast(floatingImage->data); + FloatingTYPE *warpedIntensityPtr = static_cast(warpedImage->data); + FieldTYPE *deformationFieldPtrX = static_cast(deformationField->data); + FieldTYPE *deformationFieldPtrY = &deformationFieldPtrX[warpedVoxelNumber]; + FieldTYPE *deformationFieldPtrZ = &deformationFieldPtrY[warpedVoxelNumber]; + + // Define the kernel to use + int kernel_size; + int kernel_offset=0; + void (*kernelCompFctPtr)(double,double *); + switch(kernel){ + case 0: + kernel_size=2; + kernelCompFctPtr=&interpNearestNeighKernel; + kernel_offset=0; + break; // nereast-neighboor interpolation + case 1: + kernel_size=2; + kernelCompFctPtr=&interpLinearKernel; + kernel_offset=0; + break; // linear interpolation + case 4: + kernel_size=SINC_KERNEL_SIZE; + kernelCompFctPtr=&interpWindowedSincKernel; + kernel_offset=SINC_KERNEL_RADIUS; + break; // sinc interpolation + default: + kernel_size=4; + kernelCompFctPtr=®_getNiftynetCubicSpline; + kernel_offset=1; + break; // cubic spline interpolation + } + + // Iteration over the different volume along the 4th axis + for(size_t t=0; t<(size_t)warpedImage->nt*warpedImage->nu; t++) + { +#ifndef NDEBUG + char text[255]; + sprintf(text, "3D resampling of volume number %zu",t); + reg_print_msg_debug(text); +#endif + + FloatingTYPE *warpedIntensity = &warpedIntensityPtr[t*warpedVoxelNumber]; + FloatingTYPE *floatingIntensity = &floatingIntensityPtr[t*floatingVoxelNumber]; + + int a, b, c, Y, Z, previous[3]; + + FloatingTYPE *zPointer, *xyzPointer; + double xBasis[SINC_KERNEL_SIZE], yBasis[SINC_KERNEL_SIZE], zBasis[SINC_KERNEL_SIZE], relative[3]; + double xTempNewValue, yTempNewValue, intensity; + float position[3]; +#if defined (_OPENMP) +#pragma omp parallel for default(none) \ + private(index, intensity, position, previous, xBasis, yBasis, zBasis, relative, \ + a, b, c, Y, Z, zPointer, xyzPointer, xTempNewValue, yTempNewValue) \ + shared(floatingIntensity, warpedIntensity, warpedVoxelNumber, floatingVoxelNumber, \ + deformationFieldPtrX, deformationFieldPtrY, deformationFieldPtrZ, \ + floatingImage, paddingValue, kernel_size, kernel_offset, kernelCompFctPtr) +#endif // _OPENMP + for(index=0; index(deformationFieldPtrX[index]); + position[1]=static_cast(deformationFieldPtrY[index]); + position[2]=static_cast(deformationFieldPtrZ[index]); + + previous[0] = static_cast(reg_floor(position[0])); + previous[1] = static_cast(reg_floor(position[1])); + previous[2] = static_cast(reg_floor(position[2])); + + relative[0]=static_cast(position[0])-static_cast(previous[0]); + relative[1]=static_cast(position[1])-static_cast(previous[1]); + relative[2]=static_cast(position[2])-static_cast(previous[2]); + + (*kernelCompFctPtr)(relative[0], xBasis); + (*kernelCompFctPtr)(relative[1], yBasis); + (*kernelCompFctPtr)(relative[2], zBasis); + previous[0]-=kernel_offset; + previous[1]-=kernel_offset; + previous[2]-=kernel_offset; + + intensity=0.0; + if(-1<(previous[0]) && (previous[0]+kernel_size-1)nx && + -1<(previous[1]) && (previous[1]+kernel_size-1)ny && + -1<(previous[2]) && (previous[2]+kernel_size-1)nz){ + for(c=0; cnx*floatingImage->ny]; + yTempNewValue=0.0; + for(b=0; bnx+previous[0]]; + xTempNewValue=0.0; + for(a=0; a(*xyzPointer++) * xBasis[a]; + } + yTempNewValue += xTempNewValue * yBasis[b]; + } + intensity += yTempNewValue * zBasis[c]; + } + } + else{ + for(c=0; c(previous[2] + c, floatingImage->nz); + zPointer = &floatingIntensity[Z*floatingImage->nx*floatingImage->ny]; + yTempNewValue=0.0; + + for(b=0; b(previous[1] + b, floatingImage->ny); + xyzPointer = &zPointer[Y*floatingImage->nx]; + xTempNewValue=0.0; + for(a=0; a(previous[0] + a, floatingImage->nx); + + if(reg_checkImageDimensionIndex(X, floatingImage->nx) + && reg_checkImageDimensionIndex(Y, floatingImage->ny) + && reg_checkImageDimensionIndex(Z, floatingImage->nz)) { + xTempNewValue += static_cast(xyzPointer[X]) * xBasis[a]; + } + else + { + // paddingValue + xTempNewValue += static_cast(paddingValue) * xBasis[a]; + } + } + yTempNewValue += xTempNewValue * yBasis[b]; + } + intensity += yTempNewValue * zBasis[c]; + } + } + + switch(floatingImage->datatype) + { + case NIFTI_TYPE_FLOAT32: + warpedIntensity[index]=static_cast(intensity); + break; + case NIFTI_TYPE_FLOAT64: + warpedIntensity[index]=intensity; + break; + case NIFTI_TYPE_UINT8: + if(intensity!=intensity) + intensity=0; + intensity=(intensity<=255?reg_round(intensity):255); // 255=2^8-1 + warpedIntensity[index]=static_cast(intensity>0?reg_round(intensity):0); + break; + case NIFTI_TYPE_UINT16: + if(intensity!=intensity) + intensity=0; + intensity=(intensity<=65535?reg_round(intensity):65535); // 65535=2^16-1 + warpedIntensity[index]=static_cast(intensity>0?reg_round(intensity):0); + break; + case NIFTI_TYPE_UINT32: + if(intensity!=intensity) + intensity=0; + intensity=(intensity<=4294967295?reg_round(intensity):4294967295); // 4294967295=2^32-1 + warpedIntensity[index]=static_cast(intensity>0?reg_round(intensity):0); + break; + default: + if(intensity!=intensity) + intensity=0; + warpedIntensity[index]=static_cast(reg_round(intensity)); + break; + } + } + } +} +/* *************************************************************** */ +template +void ResampleImage2D(nifti_image *floatingImage, + nifti_image *deformationField, + nifti_image *warpedImage, + FieldTYPE paddingValue, + int kernel) +{ +#ifdef _WIN32 + long index; + long warpedVoxelNumber = (long)warpedImage->nx*warpedImage->ny; + long floatingVoxelNumber = (long)floatingImage->nx*floatingImage->ny; +#else + size_t index; + size_t warpedVoxelNumber = (size_t)warpedImage->nx*warpedImage->ny; + size_t floatingVoxelNumber = (size_t)floatingImage->nx*floatingImage->ny; +#endif + FloatingTYPE *floatingIntensityPtr = static_cast(floatingImage->data); + FloatingTYPE *warpedIntensityPtr = static_cast(warpedImage->data); + FieldTYPE *deformationFieldPtrX = static_cast(deformationField->data); + FieldTYPE *deformationFieldPtrY = &deformationFieldPtrX[warpedVoxelNumber]; + + int kernel_size; + int kernel_offset=0; + void (*kernelCompFctPtr)(double,double *); + switch(kernel){ + case 0: + kernel_size=2; + kernelCompFctPtr=&interpNearestNeighKernel; + kernel_offset=0; + break; // nereast-neighboor interpolation + case 1: + kernel_size=2; + kernelCompFctPtr=&interpLinearKernel; + kernel_offset=0; + break; // linear interpolation + case 4: + kernel_size=SINC_KERNEL_SIZE; + kernelCompFctPtr=&interpWindowedSincKernel; + kernel_offset=SINC_KERNEL_RADIUS; + break; // sinc interpolation + default: + kernel_size=4; + kernelCompFctPtr=®_getNiftynetCubicSpline; + kernel_offset=1; + break; // cubic spline interpolation + } + + // Iteration over the different volume along the 4th axis + for(size_t t=0; t<(size_t)warpedImage->nt*warpedImage->nu; t++) + { +#ifndef NDEBUG + char text[255]; + sprintf(text, "2D resampling of volume number %zu",t); + reg_print_msg_debug(text); +#endif + FloatingTYPE *warpedIntensity = &warpedIntensityPtr[t*warpedVoxelNumber]; + FloatingTYPE *floatingIntensity = &floatingIntensityPtr[t*floatingVoxelNumber]; + + int a, b, Y, previous[2]; + + FloatingTYPE *xyzPointer; + double xBasis[SINC_KERNEL_SIZE], yBasis[SINC_KERNEL_SIZE], relative[2]; + double xTempNewValue, intensity; + float position[3] = {0.0, 0.0, 0.0}; +#if defined (_OPENMP) +#pragma omp parallel for default(none) \ + private(index, intensity, position, previous, xBasis, yBasis, relative, \ + a, b, Y, xyzPointer, xTempNewValue) \ + shared(floatingIntensity, warpedIntensity, warpedVoxelNumber, floatingVoxelNumber, \ + deformationFieldPtrX, deformationFieldPtrY, \ + floatingImage, paddingValue, kernel_size, kernel_offset, kernelCompFctPtr) +#endif // _OPENMP + for(index=0; index(deformationFieldPtrX[index]); + position[1] = static_cast(deformationFieldPtrY[index]); + + previous[0] = static_cast(reg_floor(position[0])); + previous[1] = static_cast(reg_floor(position[1])); + + relative[0] = static_cast(position[0])-static_cast(previous[0]); + relative[1] = static_cast(position[1])-static_cast(previous[1]); + + (*kernelCompFctPtr)(relative[0], xBasis); + (*kernelCompFctPtr)(relative[1], yBasis); + previous[0]-=kernel_offset; + previous[1]-=kernel_offset; + + intensity=0.0; + for(b=0; b(previous[1] + b, floatingImage->ny); + xyzPointer = &floatingIntensity[Y*floatingImage->nx]; + xTempNewValue=0.0; + for(a=0; a(previous[0] + a, floatingImage->nx); + + if(reg_checkImageDimensionIndex(X, floatingImage->nx) + && reg_checkImageDimensionIndex(Y, floatingImage->ny)) { + xTempNewValue += static_cast(xyzPointer[X]) * xBasis[a]; + } + else + { + // paddingValue + xTempNewValue += static_cast(paddingValue) * xBasis[a]; + } + } + intensity += xTempNewValue * yBasis[b]; + + switch(floatingImage->datatype) + { + case NIFTI_TYPE_FLOAT32: + warpedIntensity[index]=static_cast(intensity); + break; + case NIFTI_TYPE_FLOAT64: + warpedIntensity[index]=intensity; + break; + case NIFTI_TYPE_UINT8: + intensity=(intensity<=255?reg_round(intensity):255); // 255=2^8-1 + warpedIntensity[index]=static_cast(intensity>0?reg_round(intensity):0); + break; + case NIFTI_TYPE_UINT16: + intensity=(intensity<=65535?reg_round(intensity):65535); // 65535=2^16-1 + warpedIntensity[index]=static_cast(intensity>0?reg_round(intensity):0); + break; + case NIFTI_TYPE_UINT32: + intensity=(intensity<=4294967295?reg_round(intensity):4294967295); // 4294967295=2^32-1 + warpedIntensity[index]=static_cast(intensity>0?reg_round(intensity):0); + break; + default: + warpedIntensity[index]=static_cast(reg_round(intensity)); + break; + } + } + } + } +} +/* *************************************************************** */ +/* *************************************************************** */ + +/** This function resample a floating image into the referential + * of a reference image by applying an affine transformation and + * a deformation field. The affine transformation has to be in + * real coordinate and the deformation field is in mm in the space + * of the reference image. + * interp can be either 0, 1 or 3 meaning nearest neighbor, linear + * or cubic spline interpolation. + * every voxel which is not fully in the floating image takes the + * backgreg_round value. + */ +template +void reg_resampleImage2(nifti_image *floatingImage, + nifti_image *warpedImage, + nifti_image *deformationFieldImage, + int interp, + resampler_boundary_e boundaryTreatment) +{ + const FieldTYPE paddingValue = reg_getPaddingValue(boundaryTreatment); + + // The deformation field contains the position in the real world + if(deformationFieldImage->nz>1 || floatingImage->nz>1) + { + if (boundaryTreatment == resampler_boundary_e::ZEROPAD || boundaryTreatment == resampler_boundary_e::NANPAD) { + ResampleImage3D(floatingImage, + deformationFieldImage, + warpedImage, + paddingValue, + interp); + } else if (boundaryTreatment == resampler_boundary_e::CLAMPING) { + ResampleImage3D(floatingImage, + deformationFieldImage, + warpedImage, + paddingValue, + interp); + } else if (boundaryTreatment == resampler_boundary_e::REFLECTING) { + ResampleImage3D(floatingImage, + deformationFieldImage, + warpedImage, + paddingValue, + interp); + } + } + else + { + if (boundaryTreatment == resampler_boundary_e::ZEROPAD || boundaryTreatment == resampler_boundary_e::NANPAD) { + ResampleImage2D(floatingImage, + deformationFieldImage, + warpedImage, + paddingValue, + interp); + } else if (boundaryTreatment == resampler_boundary_e::CLAMPING) { + ResampleImage2D(floatingImage, + deformationFieldImage, + warpedImage, + paddingValue, + interp); + } else if (boundaryTreatment == resampler_boundary_e::REFLECTING) { + ResampleImage2D(floatingImage, + deformationFieldImage, + warpedImage, + paddingValue, + interp); + } + } +} +/* *************************************************************** */ +void reg_resampleImage(nifti_image *floatingImage, + nifti_image *warpedImage, + nifti_image *deformationField, + int interp, + resampler_boundary_e boundaryTreatment) +{ + if(floatingImage->datatype != warpedImage->datatype) + { + reg_print_fct_error("reg_resampleImage"); + reg_print_msg_error("The floating and warped image should have the same data type"); + reg_exit(); + } + + if(floatingImage->nt != warpedImage->nt) + { + reg_print_fct_error("reg_resampleImage"); + reg_print_msg_error("The floating and warped images have different dimension along the time axis"); + reg_exit(); + } + + switch ( deformationField->datatype ) + { + case NIFTI_TYPE_FLOAT32: + switch ( floatingImage->datatype ) + { + case NIFTI_TYPE_FLOAT32: + reg_resampleImage2(floatingImage, + warpedImage, + deformationField, + interp, + boundaryTreatment); + break; + case NIFTI_TYPE_FLOAT64: + reg_resampleImage2(floatingImage, + warpedImage, + deformationField, + interp, + boundaryTreatment); + break; + default: + printf("floating pixel type unsupported."); + break; + } + break; + case NIFTI_TYPE_FLOAT64: + switch ( floatingImage->datatype ) + { + case NIFTI_TYPE_FLOAT32: + reg_resampleImage2(floatingImage, + warpedImage, + deformationField, + interp, + boundaryTreatment); + break; + case NIFTI_TYPE_FLOAT64: + reg_resampleImage2(floatingImage, + warpedImage, + deformationField, + interp, + boundaryTreatment); + break; + default: + printf("floating pixel type unsupported."); + break; + } + break; + default: + printf("Deformation field pixel type unsupported."); + break; + } +} +/* *************************************************************** */ +/* *************************************************************** */ +template +void TrilinearImageGradient(nifti_image *floatingImage, + nifti_image *deformationField, + nifti_image *warImgGradient, + float paddingValue, + int active_timepoint) +{ + if(active_timepoint<0 || active_timepoint>=(std::max)(floatingImage->nt,floatingImage->nu)){ + reg_print_fct_error("TrilinearImageGradient"); + reg_print_msg_error("The specified active timepoint is not defined in the floating image"); + reg_exit(); + } +#ifdef _WIN32 + long index; + long referenceVoxelNumber = (long)warImgGradient->nx*warImgGradient->ny*warImgGradient->nz; + long floatingVoxelNumber = (long)floatingImage->nx*floatingImage->ny*floatingImage->nz; +#else + size_t index; + size_t referenceVoxelNumber = (size_t)warImgGradient->nx*warImgGradient->ny*warImgGradient->nz; + size_t floatingVoxelNumber = (size_t)floatingImage->nx*floatingImage->ny*floatingImage->nz; +#endif + FloatingTYPE *floatingIntensityPtr = static_cast(floatingImage->data); + FloatingTYPE *floatingIntensity = &floatingIntensityPtr[active_timepoint*floatingVoxelNumber]; + + FieldTYPE *deformationFieldPtrX = static_cast(deformationField->data); + FieldTYPE *deformationFieldPtrY = &deformationFieldPtrX[referenceVoxelNumber]; + FieldTYPE *deformationFieldPtrZ = &deformationFieldPtrY[referenceVoxelNumber]; + + GradientTYPE *warpedGradientPtrX = static_cast(warImgGradient->data); + GradientTYPE *warpedGradientPtrY = &warpedGradientPtrX[referenceVoxelNumber]; + GradientTYPE *warpedGradientPtrZ = &warpedGradientPtrY[referenceVoxelNumber]; + +#ifndef NDEBUG + char text[255]; + sprintf(text, "3D linear gradient computation of volume number %i", active_timepoint); + reg_print_msg_debug(text); +#endif + + int previous[3], a, b, c, X, Y, Z; + FieldTYPE position[3], xBasis[2], yBasis[2], zBasis[2]; + FieldTYPE deriv[2]; + deriv[0]=-1; + deriv[1]=1; + FieldTYPE relative, grad[3], coeff; + FieldTYPE xxTempNewValue, yyTempNewValue, zzTempNewValue, xTempNewValue, yTempNewValue; + FloatingTYPE *zPointer, *xyzPointer; +#if defined (_OPENMP) +#pragma omp parallel for default(none) \ + private(index, position, previous, xBasis, yBasis, zBasis, relative, grad, coeff, \ + a, b, c, X, Y, Z, zPointer, xyzPointer, xTempNewValue, yTempNewValue, xxTempNewValue, yyTempNewValue, zzTempNewValue) \ + shared(floatingIntensity, referenceVoxelNumber, floatingVoxelNumber, deriv, paddingValue, \ + deformationFieldPtrX, deformationFieldPtrY, deformationFieldPtrZ, \ + floatingImage, warpedGradientPtrX, warpedGradientPtrY, warpedGradientPtrZ) +#endif // _OPENMP + for(index=0; index(reg_floor(position[0])); + previous[1] = static_cast(reg_floor(position[1])); + previous[2] = static_cast(reg_floor(position[2])); + // basis values along the x axis + relative=position[0]-(FieldTYPE)previous[0]; + xBasis[0]= (FieldTYPE)(1.0-relative); + xBasis[1]= relative; + // basis values along the y axis + relative=position[1]-(FieldTYPE)previous[1]; + yBasis[0]= (FieldTYPE)(1.0-relative); + yBasis[1]= relative; + // basis values along the z axis + relative=position[2]-(FieldTYPE)previous[2]; + zBasis[0]= (FieldTYPE)(1.0-relative); + zBasis[1]= relative; + + // The padding value is used for interpolation if it is different from NaN + if(tBoundary == resampler_boundary_e::ZEROPAD && paddingValue==paddingValue) + { + for(c=0; c<2; c++) + { + Z=previous[2]+c; + if(Z>-1 && Znz) + { + zPointer = &floatingIntensity[Z*floatingImage->nx*floatingImage->ny]; + xxTempNewValue=0.0; + yyTempNewValue=0.0; + zzTempNewValue=0.0; + for(b=0; b<2; b++) + { + Y=previous[1]+b; + if(Y>-1 && Yny) + { + xyzPointer = &zPointer[Y*floatingImage->nx+previous[0]]; + xTempNewValue=0.0; + yTempNewValue=0.0; + for(a=0; a<2; a++) + { + X=previous[0]+a; + if(X>-1 && Xnx) + { + coeff = *xyzPointer; + xTempNewValue += coeff * deriv[a]; + yTempNewValue += coeff * xBasis[a]; + } // end X in range + else + { + xTempNewValue += paddingValue * deriv[a]; + yTempNewValue += paddingValue * xBasis[a]; + } + xyzPointer++; + } // end a + xxTempNewValue += xTempNewValue * yBasis[b]; + yyTempNewValue += yTempNewValue * deriv[b]; + zzTempNewValue += yTempNewValue * yBasis[b]; + } // end Y in range + else + { + xxTempNewValue += paddingValue * yBasis[b]; + yyTempNewValue += paddingValue * deriv[b]; + zzTempNewValue += paddingValue * yBasis[b]; + } + } // end b + grad[0] += xxTempNewValue * zBasis[c]; + grad[1] += yyTempNewValue * zBasis[c]; + grad[2] += zzTempNewValue * deriv[c]; + } // end Z in range + else + { + grad[0] += paddingValue * zBasis[c]; + grad[1] += paddingValue * zBasis[c]; + grad[2] += paddingValue * deriv[c]; + } + } // end c + } // end padding value is different from NaN + else if(reg_checkImageDimensionIndex(previous[0],floatingImage->nx - 1) + && reg_checkImageDimensionIndex(previous[1],floatingImage->ny - 1) + && reg_checkImageDimensionIndex(previous[2],floatingImage->nz - 1)) { + for(c=0; c<2; c++) + { + Z = reg_applyBoundary(previous[2] + c, floatingImage->nz); + zPointer = &floatingIntensity[Z*floatingImage->nx*floatingImage->ny]; + xxTempNewValue=0.0; + yyTempNewValue=0.0; + zzTempNewValue=0.0; + for(b=0; b<2; b++) + { + Y = reg_applyBoundary(previous[1] + b, floatingImage->ny); + xyzPointer = &zPointer[Y*floatingImage->nx]; + xTempNewValue=0.0; + yTempNewValue=0.0; + for(a=0; a<2; a++) + { + X = reg_applyBoundary(previous[0] + a, floatingImage->nx); + coeff = xyzPointer[X]; + xTempNewValue += coeff * deriv[a]; + yTempNewValue += coeff * xBasis[a]; + } // end a + xxTempNewValue += xTempNewValue * yBasis[b]; + yyTempNewValue += yTempNewValue * deriv[b]; + zzTempNewValue += yTempNewValue * yBasis[b]; + } // end b + grad[0] += xxTempNewValue * zBasis[c]; + grad[1] += yyTempNewValue * zBasis[c]; + grad[2] += zzTempNewValue * deriv[c]; + } // end c + } // end padding value is NaN + else grad[0]=grad[1]=grad[2]=0; + + warpedGradientPtrX[index] = (GradientTYPE)grad[0]; + warpedGradientPtrY[index] = (GradientTYPE)grad[1]; + warpedGradientPtrZ[index] = (GradientTYPE)grad[2]; + } +} +/* *************************************************************** */ +template +void BilinearImageGradient(nifti_image *floatingImage, + nifti_image *deformationField, + nifti_image *warImgGradient, + float paddingValue, + int active_timepoint) +{ + if(active_timepoint<0 || active_timepoint>=(std::max)(floatingImage->nt, floatingImage->nu)){ + reg_print_fct_error("BilinearImageGradient"); + reg_print_msg_error("The specified active timepoint is not defined in the floating image"); + reg_exit(); + } +#ifdef _WIN32 + long index; + long referenceVoxelNumber = (long)warImgGradient->nx*warImgGradient->ny; + long floatingVoxelNumber = (long)floatingImage->nx*floatingImage->ny; +#else + size_t index; + size_t referenceVoxelNumber = (size_t)warImgGradient->nx*warImgGradient->ny; + size_t floatingVoxelNumber = (size_t)floatingImage->nx*floatingImage->ny; +#endif + + FloatingTYPE *floatingIntensityPtr = static_cast(floatingImage->data); + FloatingTYPE *floatingIntensity = &floatingIntensityPtr[active_timepoint*floatingVoxelNumber]; + + FieldTYPE *deformationFieldPtrX = static_cast(deformationField->data); + FieldTYPE *deformationFieldPtrY = &deformationFieldPtrX[referenceVoxelNumber]; + + GradientTYPE *warpedGradientPtrX = static_cast(warImgGradient->data); + GradientTYPE *warpedGradientPtrY = &warpedGradientPtrX[referenceVoxelNumber]; + +#ifndef NDEBUG + char text[255]; + sprintf(text, "2D linear gradient computation of volume number %i",active_timepoint); + reg_print_msg_debug(text); +#endif + + FieldTYPE position[3], xBasis[2], yBasis[2], relative, grad[2]; + FieldTYPE deriv[2]; + deriv[0]=-1; + deriv[1]=1; + FieldTYPE coeff, xTempNewValue, yTempNewValue; + + int previous[3], a, b, X, Y; + FloatingTYPE *xyPointer; + + assert(deformationField->nu == 2); + +#if defined (_OPENMP) +#pragma omp parallel for default(none) \ + private(index, position, previous, xBasis, yBasis, relative, grad, coeff, \ + a, b, X, Y, xyPointer, xTempNewValue, yTempNewValue) \ + shared(floatingIntensity, referenceVoxelNumber, floatingVoxelNumber, deriv, \ + deformationFieldPtrX, deformationFieldPtrY, paddingValue, \ + floatingImage, warpedGradientPtrX, warpedGradientPtrY) +#endif // _OPENMP + for(index=0; index(reg_floor(position[0])); + previous[1] = static_cast(reg_floor(position[1])); + // basis values along the x axis + relative=position[0]-(FieldTYPE)previous[0]; + relative=relative>0?relative:0; + xBasis[0]= (FieldTYPE)(1.0-relative); + xBasis[1]= relative; + // basis values along the y axis + relative=position[1]-(FieldTYPE)previous[1]; + relative=relative>0?relative:0; + yBasis[0]= (FieldTYPE)(1.0-relative); + yBasis[1]= relative; + + for(b=0; b<2; b++) + { + Y= reg_applyBoundary(previous[1] + b, floatingImage->ny); + if (reg_checkImageDimensionIndex(Y, floatingImage->ny)) { + xyPointer = &floatingIntensity[Y*floatingImage->nx]; + xTempNewValue=0.0; + yTempNewValue=0.0; + for(a=0; a<2; a++) + { + X = reg_applyBoundary(previous[0] + a, floatingImage->nx); + if (reg_checkImageDimensionIndex(X, floatingImage->nx)) { + coeff = xyPointer[X]; + xTempNewValue += coeff * deriv[a]; + yTempNewValue += coeff * xBasis[a]; + } + else + { + xTempNewValue += paddingValue * deriv[a]; + yTempNewValue += paddingValue * xBasis[a]; + } + } + grad[0] += xTempNewValue * yBasis[b]; + grad[1] += yTempNewValue * deriv[b]; + } + else + { + grad[0] += paddingValue * yBasis[b]; + grad[1] += paddingValue * deriv[b]; + } + } + if(grad[0]!=grad[0]) grad[0]=0; + if(grad[1]!=grad[1]) grad[1]=0; + + warpedGradientPtrX[index] = (GradientTYPE)grad[0]; + warpedGradientPtrY[index] = (GradientTYPE)grad[1]; + } +} +/* *************************************************************** */ +template +void CubicSplineImageGradient3D(nifti_image *floatingImage, + nifti_image *deformationField, + nifti_image *warImgGradient, + float paddingValue, + int active_timepoint) +{ + if(active_timepoint<0 || active_timepoint>=(std::max)(floatingImage->nt, floatingImage->nu)){ + reg_print_fct_error("CubicSplineImageGradient3D"); + reg_print_msg_error("The specified active timepoint is not defined in the floating image"); + reg_exit(); + } +#ifdef _WIN32 + long index; + long referenceVoxelNumber = (long)warImgGradient->nx*warImgGradient->ny*warImgGradient->nz; + long floatingVoxelNumber = (long)floatingImage->nx*floatingImage->ny*floatingImage->nz; +#else + size_t index; + size_t referenceVoxelNumber = (size_t)warImgGradient->nx*warImgGradient->ny*warImgGradient->nz; + size_t floatingVoxelNumber = (size_t)floatingImage->nx*floatingImage->ny*floatingImage->nz; +#endif + FloatingTYPE *floatingIntensityPtr = static_cast(floatingImage->data); + FloatingTYPE *floatingIntensity = &floatingIntensityPtr[active_timepoint*floatingVoxelNumber]; + + FieldTYPE *deformationFieldPtrX = static_cast(deformationField->data); + FieldTYPE *deformationFieldPtrY = &deformationFieldPtrX[referenceVoxelNumber]; + FieldTYPE *deformationFieldPtrZ = &deformationFieldPtrY[referenceVoxelNumber]; + + GradientTYPE *warpedGradientPtrX = static_cast(warImgGradient->data); + GradientTYPE *warpedGradientPtrY = &warpedGradientPtrX[referenceVoxelNumber]; + GradientTYPE *warpedGradientPtrZ = &warpedGradientPtrY[referenceVoxelNumber]; + +#ifndef NDEBUG + char text[255]; + sprintf(text, "3D cubic spline gradient computation of volume number %i",active_timepoint); + reg_print_msg_debug(text); +#endif + + int previous[3], c, Z, b, Y, a; + + double xBasis[4], yBasis[4], zBasis[4], xDeriv[4], yDeriv[4], zDeriv[4], relative; + FieldTYPE coeff, position[3], grad[3]; + FieldTYPE xxTempNewValue, yyTempNewValue, zzTempNewValue, xTempNewValue, yTempNewValue; + FloatingTYPE *zPointer, *yzPointer; +#if defined (_OPENMP) +#pragma omp parallel for default(none) \ + private(index, position, previous, xBasis, yBasis, zBasis, xDeriv, yDeriv, zDeriv, relative, grad, coeff, \ + a, b, c, Y, Z, zPointer, yzPointer, xTempNewValue, yTempNewValue, xxTempNewValue, yyTempNewValue, zzTempNewValue) \ + shared(floatingIntensity, referenceVoxelNumber, floatingVoxelNumber, paddingValue, \ + deformationFieldPtrX, deformationFieldPtrY, deformationFieldPtrZ, \ + floatingImage, warpedGradientPtrX, warpedGradientPtrY, warpedGradientPtrZ) +#endif // _OPENMP + for(index=0; index(reg_floor(position[0])); + previous[1] = static_cast(reg_floor(position[1])); + previous[2] = static_cast(reg_floor(position[2])); + + // basis values along the x axis + relative=position[0]-(FieldTYPE)previous[0]; + reg_getNiftynetCubicSpline(relative, xBasis); + reg_getNiftynetCubicSplineDerivative(relative, xDeriv); + + // basis values along the y axis + relative=position[1]-(FieldTYPE)previous[1]; + reg_getNiftynetCubicSpline(relative, yBasis); + reg_getNiftynetCubicSplineDerivative(relative, yDeriv); + + // basis values along the z axis + relative=position[2]-(FieldTYPE)previous[2]; + reg_getNiftynetCubicSpline(relative, zBasis); + reg_getNiftynetCubicSplineDerivative(relative, zDeriv); + + previous[0]--; + previous[1]--; + previous[2]--; + + for(c=0; c<4; c++) + { + Z = reg_applyBoundary(previous[2] + c, floatingImage->nz); + if (reg_checkImageDimensionIndex(Z, floatingImage->nz)) { + zPointer = &floatingIntensity[Z*floatingImage->nx*floatingImage->ny]; + xxTempNewValue=0.0; + yyTempNewValue=0.0; + zzTempNewValue=0.0; + for(b=0; b<4; b++) + { + Y = reg_applyBoundary(previous[1] + b, floatingImage->ny); + yzPointer = &zPointer[Y*floatingImage->nx]; + if (reg_checkImageDimensionIndex(Y, floatingImage->ny)) { + xTempNewValue=0.0; + yTempNewValue=0.0; + for(a=0; a<4; a++) + { + int X = reg_applyBoundary(previous[0] + a, floatingImage->nx); + + if (reg_checkImageDimensionIndex(X, floatingImage->nx)) { + coeff = yzPointer[X]; + xTempNewValue += coeff * xDeriv[a]; + yTempNewValue += coeff * xBasis[a]; + } // previous[0]+a in range + else + { + xTempNewValue += paddingValue * xDeriv[a]; + yTempNewValue += paddingValue * xBasis[a]; + } + } // a + xxTempNewValue += xTempNewValue * yBasis[b]; + yyTempNewValue += yTempNewValue * yDeriv[b]; + zzTempNewValue += yTempNewValue * yBasis[b]; + } // Y in range + else + { + xxTempNewValue += paddingValue * yBasis[b]; + yyTempNewValue += paddingValue * yDeriv[b]; + zzTempNewValue += paddingValue * yBasis[b]; + } + } // b + grad[0] += xxTempNewValue * zBasis[c]; + grad[1] += yyTempNewValue * zBasis[c]; + grad[2] += zzTempNewValue * zDeriv[c]; + } // Z in range + else + { + grad[0] += paddingValue * zBasis[c]; + grad[1] += paddingValue * zBasis[c]; + grad[2] += paddingValue * zDeriv[c]; + } + } // c + + grad[0]=grad[0]==grad[0]?grad[0]:0.0; + grad[1]=grad[1]==grad[1]?grad[1]:0.0; + grad[2]=grad[2]==grad[2]?grad[2]:0.0; + + warpedGradientPtrX[index] = (GradientTYPE)grad[0]; + warpedGradientPtrY[index] = (GradientTYPE)grad[1]; + warpedGradientPtrZ[index] = (GradientTYPE)grad[2]; + } +} +/* *************************************************************** */ +template +void CubicSplineImageGradient2D(nifti_image *floatingImage, + nifti_image *deformationField, + nifti_image *warImgGradient, + float paddingValue, + int active_timepoint) +{ + if(active_timepoint<0 || active_timepoint>=(std::max)(floatingImage->nt, floatingImage->nu)){ + reg_print_fct_error("CubicSplineImageGradient2D"); + reg_print_msg_error("The specified active timepoint is not defined in the floating image"); + reg_exit(); + } +#ifdef _WIN32 + long index; + long referenceVoxelNumber = (long)warImgGradient->nx*warImgGradient->ny; + long floatingVoxelNumber = (long)floatingImage->nx*floatingImage->ny; +#else + size_t index; + size_t referenceVoxelNumber = (size_t)warImgGradient->nx*warImgGradient->ny; + size_t floatingVoxelNumber = (size_t)floatingImage->nx*floatingImage->ny; +#endif + FloatingTYPE *floatingIntensityPtr = static_cast(floatingImage->data); + FloatingTYPE *floatingIntensity = &floatingIntensityPtr[active_timepoint*floatingVoxelNumber]; + + FieldTYPE *deformationFieldPtrX = static_cast(deformationField->data); + FieldTYPE *deformationFieldPtrY = &deformationFieldPtrX[referenceVoxelNumber]; + + GradientTYPE *warpedGradientPtrX = static_cast(warImgGradient->data); + GradientTYPE *warpedGradientPtrY = &warpedGradientPtrX[referenceVoxelNumber]; + +#ifndef NDEBUG + char text[255]; + sprintf(text, "2D cubic spline gradient computation of volume number %i",active_timepoint); + reg_print_msg_debug(text); +#endif + int previous[2], b, Y, a; + double xBasis[4], yBasis[4], xDeriv[4], yDeriv[4], relative; + FieldTYPE coeff, position[3], grad[2]; + FieldTYPE xTempNewValue, yTempNewValue; + FloatingTYPE *yPointer; +#if defined (_OPENMP) +#pragma omp parallel for default(none) \ + private(index, position, previous, xBasis, yBasis, xDeriv, yDeriv, relative, grad, coeff, \ + a, b, Y, yPointer, xTempNewValue, yTempNewValue) \ + shared(floatingIntensity, referenceVoxelNumber, floatingVoxelNumber, \ + deformationFieldPtrX, deformationFieldPtrY, paddingValue, \ + floatingImage, warpedGradientPtrX, warpedGradientPtrY) +#endif // _OPENMP + for(index=0; index(reg_floor(position[0])); + previous[1] = static_cast(reg_floor(position[1])); + // basis values along the x axis + relative=position[0]-(FieldTYPE)previous[0]; + relative=relative>0?relative:0; + reg_getNiftynetCubicSpline(relative, xBasis); + reg_getNiftynetCubicSplineDerivative(relative, xDeriv); + // basis values along the y axis + relative=position[1]-(FieldTYPE)previous[1]; + relative=relative>0?relative:0; + reg_getNiftynetCubicSpline(relative, yBasis); + reg_getNiftynetCubicSplineDerivative(relative, yDeriv); + + previous[0]--; + previous[1]--; + + for(b=0; b<4; b++) + { + Y= reg_applyBoundary(previous[1] + b, floatingImage->ny); + yPointer = &floatingIntensity[Y*floatingImage->nx]; + if (reg_checkImageDimensionIndex(Y, floatingImage->ny)) { + xTempNewValue=0.0; + yTempNewValue=0.0; + for(a=0; a<4; a++) + { + int X = reg_applyBoundary(previous[0] + a, floatingImage->nx); + + if (reg_checkImageDimensionIndex(X, floatingImage->nx)) { + coeff = yPointer[X]; + xTempNewValue += coeff * xDeriv[a]; + yTempNewValue += coeff * xBasis[a]; + } // previous[0]+a in range + else + { + xTempNewValue += paddingValue * xDeriv[a]; + yTempNewValue += paddingValue * xBasis[a]; + } + } // a + grad[0] += xTempNewValue * yBasis[b]; + grad[1] += yTempNewValue * yDeriv[b]; + } // Y in range + else + { + grad[0] += paddingValue * yBasis[b]; + grad[1] += paddingValue * yDeriv[b]; + } + } // b + + grad[0]=grad[0]==grad[0]?grad[0]:0.0; + grad[1]=grad[1]==grad[1]?grad[1]:0.0; + + warpedGradientPtrX[index] = (GradientTYPE)grad[0]; + warpedGradientPtrY[index] = (GradientTYPE)grad[1]; + } +} +/* *************************************************************** */ +template +void reg_getImageGradient3(nifti_image *floatingImage, + nifti_image *warImgGradient, + nifti_image *deformationField, + int interp, + float paddingValue, + int active_timepoint, + nifti_image *warpedImage = NULL + ) +{ + /* The deformation field contains the position in the real world */ + if(interp==3) + { + if(floatingImage->nz>1 || deformationField->nz>1) + { + CubicSplineImageGradient3D + (floatingImage, + deformationField, + warImgGradient, + paddingValue, + active_timepoint); + } + else + { + CubicSplineImageGradient2D + (floatingImage, + deformationField, + warImgGradient, + paddingValue, + active_timepoint); + } + } + else // trilinear interpolation [ by default ] + { + if(floatingImage->nz>1 || deformationField->nz>1) + { + TrilinearImageGradient + (floatingImage, + deformationField, + warImgGradient, + paddingValue, + active_timepoint); + } + else + { + BilinearImageGradient + (floatingImage, + deformationField, + warImgGradient, + paddingValue, + active_timepoint); + } + } +} +/* *************************************************************** */ +template +void reg_getImageGradient2(nifti_image *floatingImage, + nifti_image *warImgGradient, + nifti_image *deformationField, + int interp, + float paddingValue, + int active_timepoint, + nifti_image *warpedImage + ) +{ + switch(warImgGradient->datatype) + { + case NIFTI_TYPE_FLOAT32: + reg_getImageGradient3 + (floatingImage,warImgGradient,deformationField,interp,paddingValue,active_timepoint, warpedImage); + break; + case NIFTI_TYPE_FLOAT64: + reg_getImageGradient3 + (floatingImage,warImgGradient,deformationField,interp,paddingValue,active_timepoint, warpedImage); + break; + default: + reg_print_fct_error("reg_getImageGradient2"); + reg_print_msg_error("The warped image data type is not supported"); + reg_exit(); + } +} +/* *************************************************************** */ +template +void reg_getImageGradient1(nifti_image *floatingImage, + nifti_image *warImgGradient, + nifti_image *deformationField, + int interp, + float paddingValue, + int active_timepoint, + nifti_image *warpedImage + ) +{ + switch(floatingImage->datatype) + { + case NIFTI_TYPE_FLOAT32: + reg_getImageGradient2 + (floatingImage,warImgGradient,deformationField,interp,paddingValue,active_timepoint, warpedImage); + break; + case NIFTI_TYPE_FLOAT64: + reg_getImageGradient2 + (floatingImage,warImgGradient,deformationField,interp,paddingValue,active_timepoint, warpedImage); + break; + default: + reg_print_fct_error("reg_getImageGradient1"); + reg_print_msg_error("Unsupported floating image datatype"); + reg_exit(); + } +} +/* *************************************************************** */ +void reg_getImageGradient(nifti_image *floatingImage, + nifti_image *warImgGradient, + nifti_image *deformationField, + int interp, + resampler_boundary_e boundary, + int active_timepoint, + nifti_image *warpedImage + ) +{ + const float paddingValue = reg_getPaddingValue(boundary); + + switch(deformationField->datatype) + { + case NIFTI_TYPE_FLOAT32: + if (boundary == resampler_boundary_e::CLAMPING) { + reg_getImageGradient1 + (floatingImage,warImgGradient,deformationField,interp,paddingValue,active_timepoint, warpedImage); + } else if (boundary == resampler_boundary_e::REFLECTING) { + reg_getImageGradient1 + (floatingImage,warImgGradient,deformationField,interp,paddingValue,active_timepoint, warpedImage); + } else { + reg_getImageGradient1 + (floatingImage,warImgGradient,deformationField,interp,paddingValue,active_timepoint, warpedImage); + } + break; + + case NIFTI_TYPE_FLOAT64: + if (boundary == resampler_boundary_e::CLAMPING) { + reg_getImageGradient1 + (floatingImage,warImgGradient,deformationField,interp,paddingValue,active_timepoint, warpedImage); + } else if (boundary == resampler_boundary_e::REFLECTING) { + reg_getImageGradient1 + (floatingImage,warImgGradient,deformationField,interp,paddingValue,active_timepoint, warpedImage); + } else { + reg_getImageGradient1 + (floatingImage,warImgGradient,deformationField,interp,paddingValue,active_timepoint, warpedImage); + } + break; + default: + reg_print_fct_error("reg_getImageGradient"); + reg_print_msg_error("Unsupported deformation field image datatype"); + reg_exit(); + break; + } +} +/* *************************************************************** */ +/* *************************************************************** */ +template +static void _compute_image_derivative(nifti_image &r_destination, const nifti_image &image, const nifti_image &deformation, const nifti_image &gradient_out, + const float padvalue, const interp_function_tt &interp_function) { + const long deformation_spatial_size = deformation.nx*deformation.ny*deformation.nz; + const long image_spatial_size = image.nx*image.ny*image.nz; + const float *outgradient_base = (const float*)(gradient_out.data); + + float *p_out_base = (float*)(r_destination.data); + float const *ppc_deformation_components[t_nof_dims]; + + assert(t_nof_dims == 3 || image.nz == 1); + assert(gradient_out.nx == deformation.nx && gradient_out.ny == deformation.ny && gradient_out.nz == deformation.nz); + assert(gradient_out.nu == image.nu); + + for (int i = 0; i < t_nof_dims; ++i) { + ppc_deformation_components[i] = (const float*)(deformation.data) + i*deformation_spatial_size; + } + + std::fill(p_out_base, p_out_base + r_destination.nvox, 0.f); + + for (long index = 0; index < deformation_spatial_size; ++index) { + int base_index[t_nof_dims]; + double basis[t_nof_dims][t_kernel_size]; + + { + double relative[t_nof_dims]; + float position[t_nof_dims]; + + for (int l = 0; l < t_nof_dims; ++l) { + position[l] = ppc_deformation_components[l][index]; + base_index[l] = int(reg_floor(position[l])); + relative[l] = position[l] - base_index[l]; + interp_function(relative[l], basis[l]); + base_index[l] -= t_kernel_size/4; + } + } + + { + const auto x_loop = [&](const int off_x, const double &basis_multiplier) { + for (int a = 0; a < t_kernel_size; ++a) { + int x = reg_applyBoundary(base_index[0] + a, image.nx); + + if (reg_checkImageDimensionIndex(x, image.nx)) { + float const *pc_out_grad = outgradient_base + index; + float *p_out = p_out_base + off_x + x; + + for (int m = 0; m < image.nu; ++m, pc_out_grad += deformation_spatial_size, p_out += image_spatial_size) { + *p_out += float(basis_multiplier*basis[0][a]*(*pc_out_grad)); + } + } + } + }; + + if (t_nof_dims == 3) { + for (int c = 0; c < t_kernel_size; ++c) { + int z = reg_applyBoundary(base_index[2] + c, image.nz); + + if (reg_checkImageDimensionIndex(z, image.nz)) { + const int off_y = z*image.ny; + + for (int b = 0; b < t_kernel_size; ++b) { + int y = reg_applyBoundary(base_index[1] + b, image.ny); + + if (reg_checkImageDimensionIndex(y, image.ny)) { + x_loop((y + off_y)*image.nx, basis[2][c]*basis[1][b]); + } + } + } + } + } else { + for (int b = 0; b < t_kernel_size; ++b) { + int y = reg_applyBoundary(base_index[1] + b, image.ny); + + if (reg_checkImageDimensionIndex(y, image.ny)) { + x_loop(y*image.nx, basis[1][b]); + } + } + } + } + } +} +/* *************************************************************** */ +template +static void _compute_gradient_product_bdy(nifti_image &r_destination, const nifti_image &image, const nifti_image &deformation, const nifti_image &gradient_out, + const float padvalue, const int interpolation) { + switch (interpolation) { + case 0: + _compute_image_derivative(r_destination, image, deformation, gradient_out, padvalue, interpNearestNeighKernel); + break; + + case 1: + _compute_image_derivative(r_destination, image, deformation, gradient_out, padvalue, [](const double rel, double *p_out) { + interpLinearKernel(rel, p_out); + }); + break; + + case 3: + _compute_image_derivative(r_destination, image, deformation, gradient_out, padvalue, reg_getNiftynetCubicSpline); + break; + + default: + reg_print_msg_error("Unsupported interpolation type."); + reg_exit(); + } + +} +/* *************************************************************** */ +template +static void _compute_gradient_product_nd(nifti_image &r_destination, const nifti_image &image, const nifti_image &deformation, const nifti_image &gradient_out, + const resampler_boundary_e boundary, const int interpolation) { + const float padvalue = reg_getPaddingValue(boundary); + + switch (boundary) { + case resampler_boundary_e::CLAMPING: + _compute_gradient_product_bdy(r_destination, image, deformation, gradient_out, padvalue, interpolation); + break; + + case resampler_boundary_e::REFLECTING: + _compute_gradient_product_bdy(r_destination, image, deformation, gradient_out, padvalue, interpolation); + break; + + default: + _compute_gradient_product_bdy(r_destination, image, deformation, gradient_out, padvalue, interpolation); + } +} +/* *************************************************************** */ +void compute_gradient_product(nifti_image &r_destination, const nifti_image &image, const nifti_image &deformation, const nifti_image &gradient_out, + const resampler_boundary_e boundary, const int interpolation) { + if (image.nz != 1 || deformation.nz != 1) { + _compute_gradient_product_nd(r_destination, image, deformation, gradient_out, boundary, interpolation); + } else { + _compute_gradient_product_nd(r_destination, image, deformation, gradient_out, boundary, interpolation); + } +} + +#endif diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_resampling.h b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_resampling.h new file mode 100755 index 00000000..30df71b9 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_resampling.h @@ -0,0 +1,62 @@ +/** + * @file _reg_resampling.h + * @author Marc Modat + * @date 24/03/2009 + * + * Created by Marc Modat on 24/03/2009. + * Copyright (c) 2009, University College London. All rights reserved. + * Centre for Medical Image Computing (CMIC) + * See the LICENSE.txt file in the nifty_reg root folder + * + */ + +#ifndef _REG_RESAMPLING_H +#define _REG_RESAMPLING_H + +#include "nifti1_io.h" +#include "resampler_boundary.h" + +/** @brief This function resample a floating image into the space of a reference/warped image. + * The deformation is provided by a 4D nifti image which is in the space of the reference image. + * In the 4D image, for each voxel i,j,k, the position in the real word for the floating image is store. + * Interpolation can be nearest Neighbor (0), linear (1) or cubic spline (3). + * The cubic spline interpolation assume a padding value of 0 + * The padding value for the NN and the LIN interpolation are user defined. + * @param floatingImage Floating image that is interpolated + * @param warpedImage Warped image that is being generated + * @param deformationField Vector field image that contains the dense correspondences + * @param interp Interpolation type. 0, 1 or 3 correspond to nearest neighbor, linear or cubic + * interpolation + * @param boundaryTreatment specifies how to treat image boundaries + * reference image space. + * \sa resampler_boundary_e + */ +extern "C++" +void reg_resampleImage(nifti_image *floatingImage, + nifti_image *warpedImage, + nifti_image *deformationField, + int interp, + resampler_boundary_e boundaryTreatment); + +extern "C++" +void reg_getImageGradient(nifti_image *floatingImage, + nifti_image *warImgGradient, + nifti_image *deformationField, + int interp, + resampler_boundary_e boundaryTreatment, + int active_timepoint, + nifti_image *warpedImage = NULL); + +/** + * \brief Computes the tensor product of an outgoing gradient and the derivative of the warped image wrt. the floating image + * \param r_destination output image (same dimensions as floating image) + * \param image floating image + * \param deformation sampling indices/deformation field + * \param gradient_out downstream derivative wrt. the warped image + * \param boundary boundary treatment flag + * \parm interpolation standard (0, 1, 3) interpolation code + */ +void compute_gradient_product(nifti_image &r_destination, const nifti_image &image, const nifti_image &deformation, const nifti_image &gradient_out, + const resampler_boundary_e boundary, const int interpolation); + +#endif diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_resampling_gpu.cu b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_resampling_gpu.cu new file mode 100755 index 00000000..b65d50d7 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_resampling_gpu.cu @@ -0,0 +1,328 @@ +/* + * _reg_resampling_gpu.cu + * + * + * Created by Marc Modat on 24/03/2009. + * Copyright (c) 2009, University College London. All rights reserved. + * Centre for Medical Image Computing (CMIC) + * See the LICENSE.txt file in the nifty_reg root folder + * + */ + +#ifndef _REG_RESAMPLING_GPU_CU +#define _REG_RESAMPLING_GPU_CU + +#include "_reg_resampling_gpu.h" +#include "_reg_tools.h" +#include "interpolations.h" + +/* *************************************************************** */ +/* *************************************************************** */ +template +__global__ void reg_getImageGradient_spline_kernel(float *p_gradientArray, + const float *pc_floating, + const float *pc_deformation, + const int3 floating_dims, + const int3 deformation_dims, + const float paddingValue, + const int ref_size) { + const int tid= (blockIdx.y*gridDim.x+blockIdx.x)*blockDim.x+threadIdx.x; + const int nof_dims = 2 + int(tIs3D); + const int kernel_size = 4; + + if(tid(voxel[2] + c, floating_dims.z); + float3 tempValueY = make_float3(0.0f, 0.0f, 0.0f); + + for(short b = 0; b < kernel_size; ++b){ + float2 tempValueX = make_float2(0.0f, 0.0f); + int y = reg_applyBoundary(voxel[1] + b, floating_dims.y); + + for(short a = 0; a < kernel_size; ++a){ + int x = reg_applyBoundary(voxel[0] + a, floating_dims.x); + float intensity = paddingValue; + + if (reg_checkImageDimensionIndex(x, floating_dims.x) + && reg_checkImageDimensionIndex(y, floating_dims.y) + && reg_checkImageDimensionIndex(z, floating_dims.z)) { + intensity = pc_floating[((z*floating_dims.y)+y)*floating_dims.x+x]; + } + + tempValueX.x += intensity*derivative[0][a]; + tempValueX.y += intensity*basis[0][a]; + } + tempValueY.x += tempValueX.x*basis[1][b]; + tempValueY.y += tempValueX.y*derivative[1][b]; + tempValueY.z += tempValueX.y*basis[1][b]; + } + gradientValue.x += tempValueY.x*basis[2][c]; + gradientValue.y += tempValueY.y*basis[2][c]; + gradientValue.z += tempValueY.z*derivative[2][c]; + } + } else { + for(short b = 0; b < kernel_size; ++b){ + float2 tempValueX = make_float2(0.0f, 0.0f); + int y = reg_applyBoundary(voxel[1] + b, floating_dims.y); + + for(short a = 0; a < kernel_size; ++a){ + int x = reg_applyBoundary(voxel[0] + a, floating_dims.x); + float intensity=paddingValue; + + if (reg_checkImageDimensionIndex(x, floating_dims.x) + && reg_checkImageDimensionIndex(y, floating_dims.y)) { + intensity = pc_floating[y*floating_dims.x+x]; + } + + tempValueX.x += intensity*derivative[0][a]; + tempValueX.y += intensity*basis[0][a]; + } + gradientValue.x += tempValueX.x*basis[1][b]; + gradientValue.y += tempValueX.y*derivative[1][b]; + } + } + + p_gradientArray[tid] = gradientValue.x; + p_gradientArray[ref_size+tid] = gradientValue.y; + if (tIs3D) { + p_gradientArray[2*ref_size+tid] = gradientValue.z; + } + } +} +/* *************************************************************** */ +template +__global__ void reg_getImageGradient_kernel(float *p_gradientArray, + const float *pc_floating, + const float *pc_deformation, + const int3 floating_dims, + const int3 deformation_dims, + const float paddingValue, + const int ref_size) +{ + const int tid= (blockIdx.y*gridDim.x+blockIdx.x)*blockDim.x+threadIdx.x; + + if(tid(voxel.z + c, floating_dims.z); + + float3 tempValueY=make_float3(0.0f, 0.0f, 0.0f); + for(short b=0; b<2; b++){ + float2 tempValueX=make_float2(0.0f, 0.0f); + int y = reg_applyBoundary(voxel.y + b, floating_dims.y); + + for(short a=0; a<2; a++){ + int x= reg_applyBoundary(voxel.x + a, floating_dims.x); + float intensity=paddingValue; + + if (reg_checkImageDimensionIndex(x, floating_dims.x) + && reg_checkImageDimensionIndex(y, floating_dims.y) + && reg_checkImageDimensionIndex(z, floating_dims.z)) { + intensity = pc_floating[((z*floating_dims.y)+y)*floating_dims.x+x]; + } + + tempValueX.x += (1 - 2*int(a == 0))*intensity; + tempValueX.y += intensity * xBasis[a]; + } + tempValueY.x += tempValueX.x * yBasis[b]; + tempValueY.y += (1 - 2*int(b == 0))*tempValueX.y; + tempValueY.z += tempValueX.y * yBasis[b]; + } + gradientValue.x += tempValueY.x * zBasis[c]; + gradientValue.y += tempValueY.y * zBasis[c]; + gradientValue.z += (1 - 2*int(c == 0))*tempValueY.z; + } + } else { + for(short b=0; b<2; b++){ + float2 tempValueX=make_float2(0.0f, 0.0f); + int y = reg_applyBoundary(voxel.y + b, floating_dims.y); + + for(short a=0; a<2; a++){ + int x = reg_applyBoundary(voxel.x + a, floating_dims.x); + float intensity=paddingValue; + + if (reg_checkImageDimensionIndex(x, floating_dims.x) + && reg_checkImageDimensionIndex(y, floating_dims.y)) { + intensity = pc_floating[y*floating_dims.x+x]; + } + + tempValueX.x += intensity*(1 - 2*(a == 0)); + tempValueX.y += intensity * xBasis[a]; + } + gradientValue.x += tempValueX.x * yBasis[b]; + gradientValue.y += tempValueX.y*(1 - 2*(b == 0)); + } + } + + p_gradientArray[tid] = gradientValue.x; + p_gradientArray[ref_size+tid] = gradientValue.y; + if (tIs3D) { + p_gradientArray[2*ref_size+tid] = gradientValue.z; + } + } +} +/* *************************************************************** */ +template +static void _launchGradientKernelBoundary(const nifti_image &sourceImage, + const nifti_image &deformationImage, + const float *sourceImageArray_d, + const float *positionFieldImageArray_d, + float *resultGradientArray_d, + const float pad, + const int interpolation) { + int3 floatingDim = make_int3(sourceImage.nx, sourceImage.ny, sourceImage.nz); + int3 deformationDim = make_int3(deformationImage.nx, deformationImage.ny, deformationImage.nz); + dim3 B1; + dim3 G1; + int ref_size = deformationImage.nx*deformationImage.ny*deformationImage.nz; + + cudaCommon_computeGridConfiguration(B1, G1, ref_size); + + if (interpolation == 3) { + reg_getImageGradient_spline_kernel <<>> (resultGradientArray_d, + sourceImageArray_d, + positionFieldImageArray_d, + floatingDim, + deformationDim, + pad, + ref_size); + } else { + reg_getImageGradient_kernel <<>> (resultGradientArray_d, + sourceImageArray_d, + positionFieldImageArray_d, + floatingDim, + deformationDim, + pad, + ref_size); + } +} +/* *************************************************************** */ +template +static void _launchGradientKernelND(const nifti_image &sourceImage, + const nifti_image &deformationImage, + const float *sourceImageArray_d, + const float *positionFieldImageArray_d, + float *resultGradientArray_d, + const resampler_boundary_e boundary, + const int interpolation) { + const float pad = reg_getPaddingValue(boundary); + + switch (boundary) { + case resampler_boundary_e::CLAMPING: + _launchGradientKernelBoundary(sourceImage, + deformationImage, + sourceImageArray_d, + positionFieldImageArray_d, + resultGradientArray_d, + pad, + interpolation); + break; + + case resampler_boundary_e::REFLECTING: + _launchGradientKernelBoundary(sourceImage, + deformationImage, + sourceImageArray_d, + positionFieldImageArray_d, + resultGradientArray_d, + pad, + interpolation); + break; + + default: + _launchGradientKernelBoundary(sourceImage, + deformationImage, + sourceImageArray_d, + positionFieldImageArray_d, + resultGradientArray_d, + pad, + interpolation); + } +} +/* *************************************************************** */ +void reg_getImageGradient_gpu(const nifti_image &sourceImage, + const nifti_image &deformationImage, + const float *sourceImageArray_d, + const float *positionFieldImageArray_d, + float *resultGradientArray_d, + const resampler_boundary_e boundary, + const int interpolation) { + if (sourceImage.nz > 1 || deformationImage.nz > 1) { + _launchGradientKernelND(sourceImage, + deformationImage, + sourceImageArray_d, + positionFieldImageArray_d, + resultGradientArray_d, + boundary, + interpolation); + } else { + _launchGradientKernelND(sourceImage, + deformationImage, + sourceImageArray_d, + positionFieldImageArray_d, + resultGradientArray_d, + boundary, + interpolation); + } +} +/* *************************************************************** */ +/* *************************************************************** */ + +#endif diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_resampling_gpu.h b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_resampling_gpu.h new file mode 100755 index 00000000..b533b97a --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_resampling_gpu.h @@ -0,0 +1,26 @@ +/* + * _reg_resampling_gpu.h + * + * + * Created by Marc Modat on 24/03/2009. + * Copyright (c) 2009, University College London. All rights reserved. + * Centre for Medical Image Computing (CMIC) + * See the LICENSE.txt file in the nifty_reg root folder + * + */ + +#ifndef _REG_RESAMPLING_GPU_H +#define _REG_RESAMPLING_GPU_H + +#include "_reg_common_cuda.h" +#include "resampler_boundary.h" + +extern "C++" +void reg_getImageGradient_gpu(const nifti_image &sourceImage, + const nifti_image &deformationImage, + const float *sourceImageArray_d, + const float *positionFieldImageArray_d, + float *resultGradientArray_d, + const resampler_boundary_e boundary, + const int interpolation); +#endif diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_tools.cpp b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_tools.cpp new file mode 100755 index 00000000..3d197339 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_tools.cpp @@ -0,0 +1,574 @@ +/** + * @file _reg_tools.cpp + * @author Marc Modat + * @date 25/03/2009 + * @brief Set of useful functions + * + * Copyright (c) 2009, University College London. All rights reserved. + * Centre for Medical Image Computing (CMIC) + * See the LICENSE.txt file in the nifty_reg root folder + * + */ + +#ifndef _REG_TOOLS_CPP +#define _REG_TOOLS_CPP + +#include +#include "_reg_tools.h" + +/* *************************************************************** */ +/* *************************************************************** */ +void reg_getRealImageSpacing(nifti_image *image, + float *spacingValues) +{ + float indexVoxel1[3]= {0,0,0}; + float indexVoxel2[3], realVoxel1[3], realVoxel2[3]; + reg_mat44_mul(&(image->sto_xyz), indexVoxel1, realVoxel1); + + indexVoxel2[1]=indexVoxel2[2]=0; + indexVoxel2[0]=1; + reg_mat44_mul(&(image->sto_xyz), indexVoxel2, realVoxel2); + spacingValues[0]=sqrtf(reg_pow2(realVoxel1[0]-realVoxel2[0])+reg_pow2(realVoxel1[1]-realVoxel2[1])+reg_pow2(realVoxel1[2]-realVoxel2[2])); + + indexVoxel2[0]=indexVoxel2[2]=0; + indexVoxel2[1]=1; + reg_mat44_mul(&(image->sto_xyz), indexVoxel2, realVoxel2); + spacingValues[1]=sqrtf(reg_pow2(realVoxel1[0]-realVoxel2[0])+reg_pow2(realVoxel1[1]-realVoxel2[1])+reg_pow2(realVoxel1[2]-realVoxel2[2])); + + if(image->nz>1) + { + indexVoxel2[0]=indexVoxel2[1]=0; + indexVoxel2[2]=1; + reg_mat44_mul(&(image->sto_xyz), indexVoxel2, realVoxel2); + spacingValues[2]=sqrtf(reg_pow2(realVoxel1[0]-realVoxel2[0])+reg_pow2(realVoxel1[1]-realVoxel2[1])+reg_pow2(realVoxel1[2]-realVoxel2[2])); + } +} + +/* *************************************************************** */ +/* *************************************************************** */ +void reg_checkAndCorrectDimension(nifti_image *image) +{ + // Ensure that no dimension is set to zero + if(image->nx<1 || image->dim[1]<1) image->dim[1]=image->nx=1; + if(image->ny<1 || image->dim[2]<1) image->dim[2]=image->ny=1; + if(image->nz<1 || image->dim[3]<1) image->dim[3]=image->nz=1; + if(image->nt<1 || image->dim[4]<1) image->dim[4]=image->nt=1; + if(image->nu<1 || image->dim[5]<1) image->dim[5]=image->nu=1; + if(image->nv<1 || image->dim[6]<1) image->dim[6]=image->nv=1; + if(image->nw<1 || image->dim[7]<1) image->dim[7]=image->nw=1; + //Correcting the dim of the images + for(int i=1;i<8;++i) { + if(image->dim[i]>1) { + image->dim[0]=image->ndim=i; + } + } + // Set the slope to 1 if undefined + if(image->scl_slope==0) image->scl_slope=1.f; + // Ensure that no spacing is set to zero + if(image->ny==1 && (image->dy==0 || image->pixdim[2]==0)) + image->dy=image->pixdim[2]=1; + if(image->nz==1 && (image->dz==0 || image->pixdim[3]==0)) + image->dz=image->pixdim[3]=1; + // Create the qform matrix if required + if(image->qform_code==0 && image->sform_code==0) + { + image->qto_xyz=nifti_quatern_to_mat44(image->quatern_b, + image->quatern_c, + image->quatern_d, + image->qoffset_x, + image->qoffset_y, + image->qoffset_z, + image->dx, + image->dy, + image->dz, + image->qfac); + image->qto_ijk=nifti_mat44_inverse(image->qto_xyz); + } + // Set the voxel spacing to millimeters + if(image->xyz_units==NIFTI_UNITS_MICRON) + { + for(int d=1; d<=image->ndim; ++d) + image->pixdim[d] /= 1000.f; + image->xyz_units=NIFTI_UNITS_MM; + } + if(image->xyz_units==NIFTI_UNITS_METER) + { + for(int d=1; d<=image->ndim; ++d) + image->pixdim[d] *= 1000.f; + image->xyz_units=NIFTI_UNITS_MM; + } + image->dx=image->pixdim[1]; + image->dy=image->pixdim[2]; + image->dz=image->pixdim[3]; + image->dt=image->pixdim[4]; + image->du=image->pixdim[5]; + image->dv=image->pixdim[6]; + image->dw=image->pixdim[7]; +} +/* *************************************************************** */ +/* *************************************************************** */ +template +void reg_tools_changeDatatype1(nifti_image *image,int type) +{ + // the initial array is saved and freeed + DTYPE *initialValue = (DTYPE *)malloc(image->nvox*sizeof(DTYPE)); + memcpy(initialValue, image->data, image->nvox*sizeof(DTYPE)); + + // the new array is allocated and then filled + if(type>-1){ + image->datatype=type; + } + else{ + if(sizeof(NewTYPE)==sizeof(unsigned char)) { + image->datatype = NIFTI_TYPE_UINT8; +#ifndef NDEBUG + reg_print_msg_debug("new datatype is NIFTI_TYPE_UINT8"); +#endif + } + else if(sizeof(NewTYPE)==sizeof(float)) { + image->datatype = NIFTI_TYPE_FLOAT32; +#ifndef NDEBUG + reg_print_msg_debug("new datatype is NIFTI_TYPE_FLOAT32"); +#endif + } + else if(sizeof(NewTYPE)==sizeof(double)) { + image->datatype = NIFTI_TYPE_FLOAT64; +#ifndef NDEBUG + reg_print_msg_debug("new datatype is NIFTI_TYPE_FLOAT64"); +#endif + } + else { + reg_print_fct_error("reg_tools_changeDatatype1"); + reg_print_msg_error("Only change to unsigned char, float or double are supported"); + reg_exit(); + } + } + free(image->data); + image->nbyper = sizeof(NewTYPE); + image->data = (void *)calloc(image->nvox,sizeof(NewTYPE)); + NewTYPE *dataPtr = static_cast(image->data); + for (size_t i = 0; i < image->nvox; i++) { + dataPtr[i] = (NewTYPE)(initialValue[i]); + } + + free(initialValue); + return; +} +/* *************************************************************** */ +template +void reg_tools_changeDatatype(nifti_image *image, int type) +{ + switch(image->datatype) + { + case NIFTI_TYPE_UINT8: + reg_tools_changeDatatype1(image,type); + break; + case NIFTI_TYPE_INT8: + reg_tools_changeDatatype1(image,type); + break; + case NIFTI_TYPE_UINT16: + reg_tools_changeDatatype1(image,type); + break; + case NIFTI_TYPE_INT16: + reg_tools_changeDatatype1(image,type); + break; + case NIFTI_TYPE_UINT32: + reg_tools_changeDatatype1(image,type); + break; + case NIFTI_TYPE_INT32: + reg_tools_changeDatatype1(image,type); + break; + case NIFTI_TYPE_FLOAT32: + reg_tools_changeDatatype1(image,type); + break; + case NIFTI_TYPE_FLOAT64: + reg_tools_changeDatatype1(image,type); + break; + default: + reg_print_fct_error("reg_tools_changeDatatype"); + reg_print_msg_error("Unsupported datatype"); + reg_exit(); + } +} +/* *************************************************************** */ +template void reg_tools_changeDatatype(nifti_image *, int); +template void reg_tools_changeDatatype(nifti_image *, int); +template void reg_tools_changeDatatype(nifti_image *, int); +template void reg_tools_changeDatatype(nifti_image *, int); +template void reg_tools_changeDatatype(nifti_image *, int); +template void reg_tools_changeDatatype(nifti_image *, int); +template void reg_tools_changeDatatype(nifti_image *, int); +template void reg_tools_changeDatatype(nifti_image *, int); +/* *************************************************************** */ +/* *************************************************************** */ +template +void reg_getDisplacementFromDeformation_2D(nifti_image *field) +{ + DTYPE *ptrX = static_cast(field->data); + DTYPE *ptrY = &ptrX[field->nx*field->ny]; + + mat44 matrix; + if(field->sform_code>0) + matrix=field->sto_xyz; + else matrix=field->qto_xyz; + + int x, y, index; + DTYPE xInit, yInit; +#if defined (_OPENMP) +#pragma omp parallel for default(none) \ + shared(field, matrix, ptrX, ptrY) \ + private(x, y, index, xInit, yInit) +#endif + for(y=0; yny; y++) + { + index=y*field->nx; + for(x=0; xnx; x++) + { + + // Get the initial control point position + xInit = matrix.m[0][0]*(DTYPE)x + + matrix.m[0][1]*(DTYPE)y + + matrix.m[0][3]; + yInit = matrix.m[1][0]*(DTYPE)x + + matrix.m[1][1]*(DTYPE)y + + matrix.m[1][3]; + + // The initial position is subtracted from every values + ptrX[index] -= xInit; + ptrY[index] -= yInit; + index++; + } + } +} +/* *************************************************************** */ +template +void reg_getDisplacementFromDeformation_3D(nifti_image *field) +{ + DTYPE *ptrX = static_cast(field->data); + DTYPE *ptrY = &ptrX[field->nx*field->ny*field->nz]; + DTYPE *ptrZ = &ptrY[field->nx*field->ny*field->nz]; + + mat44 matrix; + if(field->sform_code>0) + matrix=field->sto_xyz; + else matrix=field->qto_xyz; + + int x, y, z, index; + float xInit, yInit, zInit; +#if defined (_OPENMP) +#pragma omp parallel for default(none) \ + shared(field, matrix, \ + ptrX, ptrY, ptrZ) \ + private(x, y, z, index, xInit, yInit, zInit) +#endif + for(z=0; znz; z++) + { + index=z*field->nx*field->ny; + for(y=0; yny; y++) + { + for(x=0; xnx; x++) + { + // Get the initial control point position + xInit = matrix.m[0][0]*static_cast(x) + + matrix.m[0][1]*static_cast(y) + + matrix.m[0][2]*static_cast(z) + + matrix.m[0][3]; + yInit = matrix.m[1][0]*static_cast(x) + + matrix.m[1][1]*static_cast(y) + + matrix.m[1][2]*static_cast(z) + + matrix.m[1][3]; + zInit = matrix.m[2][0]*static_cast(x) + + matrix.m[2][1]*static_cast(y) + + matrix.m[2][2]*static_cast(z) + + matrix.m[2][3]; + + // The initial position is subtracted from every values + ptrX[index] -= static_cast(xInit); + ptrY[index] -= static_cast(yInit); + ptrZ[index] -= static_cast(zInit); + index++; + } + } + } +} +/* *************************************************************** */ +int reg_getDisplacementFromDeformation(nifti_image *field) +{ + if(field->datatype==NIFTI_TYPE_FLOAT32) + { + switch(field->nu) + { + case 2: + reg_getDisplacementFromDeformation_2D(field); + break; + case 3: + reg_getDisplacementFromDeformation_3D(field); + break; + default: + reg_print_fct_error("reg_getDisplacementFromDeformation"); + reg_print_msg_error("Only implemented for 5D image with 2 or 3 components in the fifth dimension"); + reg_exit(); + } + } + else if(field->datatype==NIFTI_TYPE_FLOAT64) + { + switch(field->nu) + { + case 2: + reg_getDisplacementFromDeformation_2D(field); + break; + case 3: + reg_getDisplacementFromDeformation_3D(field); + break; + default: + reg_print_fct_error("reg_getDisplacementFromDeformation"); + reg_print_msg_error("Only implemented for 5D image with 2 or 3 components in the fifth dimension"); + reg_exit(); + } + } + else + { + reg_print_fct_error("reg_getDisplacementFromDeformation"); + reg_print_msg_error("Only single or double floating precision have been implemented"); + reg_exit(); + } + field->intent_code=NIFTI_INTENT_VECTOR; + memset(field->intent_name, 0, 16); + strcpy(field->intent_name,"NREG_TRANS"); + if(field->intent_p1==DEF_FIELD) + field->intent_p1=DISP_FIELD; + if(field->intent_p1==DEF_VEL_FIELD) + field->intent_p1=DISP_VEL_FIELD; + return EXIT_SUCCESS; +} +/* *************************************************************** */ +/* *************************************************************** */ +template +void reg_getDeformationFromDisplacement_2D(nifti_image *field) +{ + DTYPE *ptrX = static_cast(field->data); + DTYPE *ptrY = &ptrX[field->nx*field->ny]; + + mat44 matrix; + if(field->sform_code>0) + matrix=field->sto_xyz; + else matrix=field->qto_xyz; + + int x, y, index; + DTYPE xInit, yInit; +#if defined (_OPENMP) +#pragma omp parallel for default(none) \ + shared(field, matrix, \ + ptrX, ptrY) \ + private(x, y, index, xInit, yInit) +#endif + for(y=0; yny; y++) + { + index=y*field->nx; + for(x=0; xnx; x++) + { + + // Get the initial control point position + xInit = matrix.m[0][0]*(DTYPE)x + + matrix.m[0][1]*(DTYPE)y + + matrix.m[0][3]; + yInit = matrix.m[1][0]*(DTYPE)x + + matrix.m[1][1]*(DTYPE)y + + matrix.m[1][3]; + + // The initial position is added from every values + ptrX[index] += xInit; + ptrY[index] += yInit; + index++; + } + } +} +/* *************************************************************** */ +/* *************************************************************** */ +template +void reg_getDeformationFromDisplacement_3D(nifti_image *field) +{ + DTYPE *ptrX = static_cast(field->data); + DTYPE *ptrY = &ptrX[field->nx*field->ny*field->nz]; + DTYPE *ptrZ = &ptrY[field->nx*field->ny*field->nz]; + + mat44 matrix; + if(field->sform_code>0) + matrix=field->sto_xyz; + else matrix=field->qto_xyz; + + int x, y, z, index; + float xInit, yInit, zInit; +#if defined (_OPENMP) +#pragma omp parallel for default(none) \ + shared(field, matrix, ptrX, ptrY, ptrZ) \ + private(x, y, z, index, xInit, yInit, zInit) +#endif + for(z=0; znz; z++) + { + index=z*field->nx*field->ny; + for(y=0; yny; y++) + { + for(x=0; xnx; x++) + { + + // Get the initial control point position + xInit = matrix.m[0][0]*static_cast(x) + + matrix.m[0][1]*static_cast(y) + + matrix.m[0][2]*static_cast(z) + + matrix.m[0][3]; + yInit = matrix.m[1][0]*static_cast(x) + + matrix.m[1][1]*static_cast(y) + + matrix.m[1][2]*static_cast(z) + + matrix.m[1][3]; + zInit = matrix.m[2][0]*static_cast(x) + + matrix.m[2][1]*static_cast(y) + + matrix.m[2][2]*static_cast(z) + + matrix.m[2][3]; + + // The initial position is subtracted from every values + ptrX[index] += static_cast(xInit); + ptrY[index] += static_cast(yInit); + ptrZ[index] += static_cast(zInit); + index++; + } + } + } +} +/* *************************************************************** */ +/* *************************************************************** */ +int reg_getDeformationFromDisplacement(nifti_image *field) +{ + if(field->datatype==NIFTI_TYPE_FLOAT32) + { + switch(field->nu) + { + case 2: + reg_getDeformationFromDisplacement_2D(field); + break; + case 3: + reg_getDeformationFromDisplacement_3D(field); + break; + default: + reg_print_fct_error("reg_getDeformationFromDisplacement"); + reg_print_msg_error("Only implemented for 2 or 3D deformation fields"); + reg_exit(); + } + } + else if(field->datatype==NIFTI_TYPE_FLOAT64) + { + switch(field->nu) + { + case 2: + reg_getDeformationFromDisplacement_2D(field); + break; + case 3: + reg_getDeformationFromDisplacement_3D(field); + break; + default: + reg_print_fct_error("reg_getDeformationFromDisplacement"); + reg_print_msg_error("Only implemented for 2 or 3D deformation fields"); + reg_exit(); + } + } + else + { + reg_print_fct_error("reg_getDeformationFromDisplacement"); + reg_print_msg_error("Only single or double floating precision have been implemented"); + reg_exit(); + } + + field->intent_code=NIFTI_INTENT_VECTOR; + memset(field->intent_name, 0, 16); + strcpy(field->intent_name,"NREG_TRANS"); + if(field->intent_p1==DISP_FIELD) + field->intent_p1=DEF_FIELD; + if(field->intent_p1==DISP_VEL_FIELD) + field->intent_p1=DEF_VEL_FIELD; + return EXIT_SUCCESS; +} +/* *************************************************************** */ +void mat44ToCptr(mat44 mat, float* cMat) +{ + for (int i = 0; i < 4; i++) + { + for (int j = 0; j < 4; j++) + { + cMat[i * 4 + j] = mat.m[i][j]; + } + } +} +/* *************************************************************** */ +void cPtrToMat44(mat44 *mat, float* cMat) +{ + for (int i = 0; i < 4; i++) + { + for (int j = 0; j < 4; j++) + { + mat->m[i][j]=cMat[i * 4 + j]; + } + } +} +/* *************************************************************** */ +void mat33ToCptr(mat33 *mat, float* cMat, const unsigned int numMats) +{ + for (size_t k = 0; k < numMats; k++) + { + for (int i = 0; i < 3; i++) + { + for (int j = 0; j < 3; j++) + { + cMat[9*k +i * 3 + j] = mat[k].m[i][j]; + + } + } + } +} +/* *************************************************************** */ +void cPtrToMat33(mat33 *mat, float* cMat) +{ + for (int i = 0; i < 3; i++) + { + for (int j = 0; j < 3; j++) + { + mat->m[i][j]=cMat[i * 3 + j]; + } + } +} +/* *************************************************************** */ +template +void matmnToCptr(T** mat, T* cMat, unsigned int m, unsigned int n) { + for (unsigned int i = 0; i < m; i++) + { + for (unsigned int j = 0; j < n; j++) + { + cMat[i * n + j] = mat[i][j]; + } + } +} +template void matmnToCptr(float** mat, float* cMat, unsigned int m, unsigned int n); +template void matmnToCptr(double** mat, double* cMat, unsigned int m, unsigned int n); +/* *************************************************************** */ +template +void cPtrToMatmn(T** mat, T* cMat, unsigned int m, unsigned int n) { + for (unsigned int i = 0; i < m; i++) + { + for (unsigned int j = 0; j < n; j++) + { + mat[i][j]=cMat[i * n + j]; + } + } +} +template void cPtrToMatmn(float** mat, float* cMat, unsigned int m, unsigned int n); +template void cPtrToMatmn(double** mat, double* cMat, unsigned int m, unsigned int n); +/* *************************************************************** */ +void coordinateFromLinearIndex(int index, int maxValue_x, int maxValue_y, int &x, int &y, int &z) +{ + x = index % (maxValue_x+1); + index /= (maxValue_x+1); + y = index % (maxValue_y+1); + index /= (maxValue_y+1); + z = index; +} +/* *************************************************************** */ +#endif diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_tools.h b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_tools.h new file mode 100755 index 00000000..8f19987c --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/_reg_tools.h @@ -0,0 +1,126 @@ +/** + * @file _reg_tools.h + * @author Marc Modat + * @date 25/03/2009 + * @brief Set of useful functions + * + * Created by Marc Modat on 25/03/2009. + * Copyright (c) 2009, University College London. All rights reserved. + * Centre for Medical Image Computing (CMIC) + * See the LICENSE.txt file in the nifty_reg root folder + * + */ + +#ifndef _REG_TOOLS_H +#define _REG_TOOLS_H + +#include +#include + +#include "_reg_maths.h" +#include "resampler_boundary.h" + +typedef enum +{ + MEAN_KERNEL, + LINEAR_KERNEL, + GAUSSIAN_KERNEL, + CUBIC_SPLINE_KERNEL +} NREG_CONV_KERNEL_TYPE; +/* *************************************************************** */ +/** @brief This function check some header parameters and correct them in + * case of error. For example no dimension is lower than one. The scl_sclope + * can not be equal to zero. The qto_xyz and qto_ijk are populated if + * both qform_code and sform_code are set to zero. + * @param image Input image to check and correct if necessary + */ +extern "C++" +void reg_checkAndCorrectDimension(nifti_image *image); + +/* *************************************************************** */ +/** @brief reg_getRealImageSpacing + * @param image image + * @param spacingValues spacingValues + */ +extern "C++" +void reg_getRealImageSpacing(nifti_image *image, + float *spacingValues); + +/* *************************************************************** */ +/** @brief Check if the specified filename corresponds to an image. + * @param name Input filename + * @return True is the specified filename corresponds to an image, + * false otherwise. + */ +extern "C++" +bool reg_isAnImageFileName(char *name); + +/* *************************************************************** */ +/** @brief Rescale an input image between two user-defined values. + * Some threshold can also be applied concurrenlty + * @param image Image to be rescaled + * @param newMin Intensity lower bound after rescaling + * @param newMax Intensity higher bound after rescaling + * @param lowThr Intensity to use as lower threshold + * @param upThr Intensity to use as higher threshold + */ +extern "C++" +void reg_intensityRescale(nifti_image *image, + int timepoint, + float newMin, + float newMax + ); + +/* *************************************************************** */ +/** @brief This function converts an image containing deformation + * field into a displacement field + * The conversion is done using the appropriate qform/sform + * @param image Image that contains a deformation field and will be + * converted into a displacement field + */ +extern "C++" +int reg_getDisplacementFromDeformation(nifti_image *image); +/* *************************************************************** */ +/** @brief This function converts an image containing a displacement field + * into a displacement field. + * The conversion is done using the appropriate qform/sform + * @param image Image that contains a deformation field and will be + * converted into a displacement field + */ +extern "C++" +int reg_getDeformationFromDisplacement(nifti_image *image); +/* *************************************************************** */ +/** @brief The functions returns the largest ratio between input image intensities + * The returned value is the largest value computed as ((A/B)-1) + * If A or B are zeros then the (A-B) value is returned. + */ +extern "C++" +double reg_test_compare_images(nifti_image *imgA, + nifti_image *imgB); +/* *************************************************************** */ +/** @brief The absolute operator is applied to the input image + */ +extern "C++" +void reg_tools_abs_image(nifti_image *img); +/* *************************************************************** */ +extern "C++" +void mat44ToCptr(mat44 mat, float* cMat); +/* *************************************************************** */ +extern "C++" +void cPtrToMat44(mat44 *mat, float* cMat); +/* *************************************************************** */ +extern "C++" +void mat33ToCptr(mat33* mat, float* cMat, const unsigned int numMats); +/* *************************************************************** */ +extern "C++" +void cPtrToMat33(mat33 *mat, float* cMat); +/* *************************************************************** */ +extern "C++" template +void matmnToCptr(T** mat, T* cMat, unsigned int m, unsigned int n); +/* *************************************************************** */ +extern "C++" template +void cPtrToMatmn(T** mat, T* cMat, unsigned int m, unsigned int n); +/* *************************************************************** */ +void coordinateFromLinearIndex(int index, int maxValue_x, int maxValue_y, int &x, int &y, int &z); +/* *************************************************************** */ +#endif diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/cmake/modules/FindNumPy.cmake b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/cmake/modules/FindNumPy.cmake new file mode 100644 index 00000000..e6732749 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/cmake/modules/FindNumPy.cmake @@ -0,0 +1,10 @@ +if (NOT NumPy_FOUND) + find_package(PythonInterp REQUIRED) + + execute_process(COMMAND "${PYTHON_EXECUTABLE}" "-c" "import numpy; print(numpy.get_include())" + OUTPUT_VARIABLE NumPy_INCLUDE_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + set(NumPy_FOUND 1) +endif (NOT NumPy_FOUND) diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/cmake/modules/FindTensorflow.cmake b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/cmake/modules/FindTensorflow.cmake new file mode 100644 index 00000000..f12b0632 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/cmake/modules/FindTensorflow.cmake @@ -0,0 +1,25 @@ +if (NOT Tensorflow_FOUND) + find_package(PythonInterp REQUIRED) + + macro (get_tensorflow_setting OUTVAR SETTING_GETTER) + execute_process( + COMMAND "${PYTHON_EXECUTABLE}" -c "import tensorflow; ${SETTING_GETTER}" + OUTPUT_VARIABLE ${OUTVAR} + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE + TIMEOUT 120 + ) + endmacro (get_tensorflow_setting) + + get_tensorflow_setting(Tensorflow_INCLUDE_DIRS "print(tensorflow.sysconfig.get_include())") + get_tensorflow_setting(Tensorflow_LIBRARY_DIRS "print(tensorflow.sysconfig.get_lib())") + get_tensorflow_setting(Tensorflow_CFLAGS "print(' '.join(tensorflow.sysconfig.get_compile_flags()))") + get_tensorflow_setting(Tensorflow_LFLAGS "print(' '.join(tensorflow.sysconfig.get_compile_link()))") + + set(Tensorflow_LIBRARIES + "tensorflow_framework") + + if (Tensorflow_INCLUDE_DIRS) + set(Tensorflow_FOUND 1) + endif (Tensorflow_INCLUDE_DIRS) +endif (NOT Tensorflow_FOUND) diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/interpolations.h b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/interpolations.h new file mode 100644 index 00000000..e4f02a6b --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/interpolations.h @@ -0,0 +1,19 @@ +#pragma once + +#include "resampler_boundary.h" + +/** + * \brief Cubic spline formula matching the one from niftynet.layer.resampler + * \param relative floating point index relative to kernel base index. + */ +template +NR_HOST_DEV void reg_getNiftynetCubicSpline(const TCoord relative, TBasis *p_basis); + +/** + * \brief Analytic derivative of cubic spline formula matching the one from niftynet.layer.resampler + * \param relative floating point index relative to kernel base index. + */ +template +NR_HOST_DEV void reg_getNiftynetCubicSplineDerivative(const TCoord relative, TBasis *p_basis); + +#include "interpolations.tpp" diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/interpolations.tpp b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/interpolations.tpp new file mode 100644 index 00000000..1818e603 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/interpolations.tpp @@ -0,0 +1,21 @@ +#pragma once + +#include "interpolations.h" + +template +NR_HOST_DEV void reg_getNiftynetCubicSpline(const TCoord relative, TBasis *p_basis) { + const TCoord sqr_relative = relative*relative; + + p_basis[0] = (((-relative + 3)*relative - 3)*relative + 1)/6; + p_basis[1] = ((3*relative - 6)*sqr_relative + 4)/6; + p_basis[2] = (((-3*relative + 3)*relative + 3)*relative + 1)/6; + p_basis[3] = sqr_relative*relative/6; +} + +template +NR_HOST_DEV void reg_getNiftynetCubicSplineDerivative(const TCoord relative, TBasis *p_basis) { + p_basis[0] = ((-3*relative + 6)*relative - 3)/6; + p_basis[1] = (9*relative - 12)*relative/6; + p_basis[2] = ((-9*relative + 6)*relative + 3)/6; + p_basis[3] = relative*relative/2; +} diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/nifti/LICENSE b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/nifti/LICENSE new file mode 100755 index 00000000..cd7ce566 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/nifti/LICENSE @@ -0,0 +1,9 @@ +Niftilib has been developed by members of the NIFTI DFWG and volunteers in the +neuroimaging community and serves as a reference implementation of the nifti-1 +file format. + +http://nifti.nimh.nih.gov/ + +Nifticlib code is released into the public domain, developers are encouraged to +incorporate niftilib code into their applications, and, to contribute changes +and enhancements to niftilib. diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/nifti/nifti1.h b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/nifti/nifti1.h new file mode 100755 index 00000000..f3feadfb --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/nifti/nifti1.h @@ -0,0 +1,1508 @@ +/** \file nifti1.h + \brief Official definition of the nifti1 header. Written by Bob Cox, SSCC, NIMH. + + HISTORY: + + 29 Nov 2007 [rickr] + - added DT_RGBA32 and NIFTI_TYPE_RGBA32 + - added NIFTI_INTENT codes: + TIME_SERIES, NODE_INDEX, RGB_VECTOR, RGBA_VECTOR, SHAPE + */ + +#ifndef _NIFTI_HEADER_ +#define _NIFTI_HEADER_ + +/***************************************************************************** + ** This file defines the "NIFTI-1" header format. ** + ** It is derived from 2 meetings at the NIH (31 Mar 2003 and ** + ** 02 Sep 2003) of the Data Format Working Group (DFWG), ** + ** chartered by the NIfTI (Neuroimaging Informatics Technology ** + ** Initiative) at the National Institutes of Health (NIH). ** + **--------------------------------------------------------------** + ** Neither the National Institutes of Health (NIH), the DFWG, ** + ** nor any of the members or employees of these institutions ** + ** imply any warranty of usefulness of this material for any ** + ** purpose, and do not assume any liability for damages, ** + ** incidental or otherwise, caused by any use of this document. ** + ** If these conditions are not acceptable, do not use this! ** + **--------------------------------------------------------------** + ** Author: Robert W Cox (NIMH, Bethesda) ** + ** Advisors: John Ashburner (FIL, London), ** + ** Stephen Smith (FMRIB, Oxford), ** + ** Mark Jenkinson (FMRIB, Oxford) ** +******************************************************************************/ + +/*---------------------------------------------------------------------------*/ +/* Note that the ANALYZE 7.5 file header (dbh.h) is + (c) Copyright 1986-1995 + Biomedical Imaging Resource + Mayo Foundation + Incorporation of components of dbh.h are by permission of the + Mayo Foundation. + + Changes from the ANALYZE 7.5 file header in this file are released to the + public domain, including the functional comments and any amusing asides. +-----------------------------------------------------------------------------*/ + +/*---------------------------------------------------------------------------*/ +/*! INTRODUCTION TO NIFTI-1: + ------------------------ + The twin (and somewhat conflicting) goals of this modified ANALYZE 7.5 + format are: + (a) To add information to the header that will be useful for functional + neuroimaging data analysis and display. These additions include: + - More basic data types. + - Two affine transformations to specify voxel coordinates. + - "Intent" codes and parameters to describe the meaning of the data. + - Affine scaling of the stored data values to their "true" values. + - Optional storage of the header and image data in one file (.nii). + (b) To maintain compatibility with non-NIFTI-aware ANALYZE 7.5 compatible + software (i.e., such a program should be able to do something useful + with a NIFTI-1 dataset -- at least, with one stored in a traditional + .img/.hdr file pair). + + Most of the unused fields in the ANALYZE 7.5 header have been taken, + and some of the lesser-used fields have been co-opted for other purposes. + Notably, most of the data_history substructure has been co-opted for + other purposes, since the ANALYZE 7.5 format describes this substructure + as "not required". + + NIFTI-1 FLAG (MAGIC STRINGS): + ---------------------------- + To flag such a struct as being conformant to the NIFTI-1 spec, the last 4 + bytes of the header must be either the C String "ni1" or "n+1"; + in hexadecimal, the 4 bytes + 6E 69 31 00 or 6E 2B 31 00 + (in any future version of this format, the '1' will be upgraded to '2', + etc.). Normally, such a "magic number" or flag goes at the start of the + file, but trying to avoid clobbering widely-used ANALYZE 7.5 fields led to + putting this marker last. However, recall that "the last shall be first" + (Matthew 20:16). + + If a NIFTI-aware program reads a header file that is NOT marked with a + NIFTI magic string, then it should treat the header as an ANALYZE 7.5 + structure. + + NIFTI-1 FILE STORAGE: + -------------------- + "ni1" means that the image data is stored in the ".img" file corresponding + to the header file (starting at file offset 0). + + "n+1" means that the image data is stored in the same file as the header + information. We recommend that the combined header+data filename suffix + be ".nii". When the dataset is stored in one file, the first byte of image + data is stored at byte location (int)vox_offset in this combined file. + The minimum allowed value of vox_offset is 352; for compatibility with + some software, vox_offset should be an integral multiple of 16. + + GRACE UNDER FIRE: + ---------------- + Most NIFTI-aware programs will only be able to handle a subset of the full + range of datasets possible with this format. All NIFTI-aware programs + should take care to check if an input dataset conforms to the program's + needs and expectations (e.g., check datatype, intent_code, etc.). If the + input dataset can't be handled by the program, the program should fail + gracefully (e.g., print a useful warning; not crash). + + SAMPLE CODES: + ------------ + The associated files nifti1_io.h and nifti1_io.c provide a sample + implementation in C of a set of functions to read, write, and manipulate + NIFTI-1 files. The file nifti1_test.c is a sample program that uses + the nifti1_io.c functions. +-----------------------------------------------------------------------------*/ + +/*---------------------------------------------------------------------------*/ +/* HEADER STRUCT DECLARATION: + ------------------------- + In the comments below for each field, only NIFTI-1 specific requirements + or changes from the ANALYZE 7.5 format are described. For convenience, + the 348 byte header is described as a single struct, rather than as the + ANALYZE 7.5 group of 3 substructs. + + Further comments about the interpretation of various elements of this + header are after the data type definition itself. Fields that are + marked as ++UNUSED++ have no particular interpretation in this standard. + (Also see the UNUSED FIELDS comment section, far below.) + + The presumption below is that the various C types have particular sizes: + sizeof(int) = sizeof(float) = 4 ; sizeof(short) = 2 +-----------------------------------------------------------------------------*/ + +/*=================*/ +#ifdef __cplusplus +extern "C" { +#endif + /*=================*/ + + /*! \struct nifti_1_header + \brief Data structure defining the fields in the nifti1 header. + This binary header should be found at the beginning of a valid + NIFTI-1 header file. + */ + /*************************/ /************************/ + struct nifti_1_header + { + /* NIFTI-1 usage */ /* ANALYZE 7.5 field(s) */ + /*************************/ /************************/ + + /*--- was header_key substruct ---*/ + int sizeof_hdr; /*!< MUST be 348 */ /* int sizeof_hdr; */ + char data_type[10]; /*!< ++UNUSED++ */ /* char data_type[10]; */ + char db_name[18]; /*!< ++UNUSED++ */ /* char db_name[18]; */ + int extents; /*!< ++UNUSED++ */ /* int extents; */ + short session_error; /*!< ++UNUSED++ */ /* short session_error; */ + char regular; /*!< ++UNUSED++ */ /* char regular; */ + char dim_info; /*!< MRI slice ordering. */ /* char hkey_un0; */ + + /*--- was image_dimension substruct ---*/ + short dim[8]; /*!< Data array dimensions.*/ /* short dim[8]; */ + float intent_p1 ; /*!< 1st intent parameter. */ /* short unused8; */ + /* short unused9; */ + float intent_p2 ; /*!< 2nd intent parameter. */ /* short unused10; */ + /* short unused11; */ + float intent_p3 ; /*!< 3rd intent parameter. */ /* short unused12; */ + /* short unused13; */ + short intent_code ; /*!< NIFTI_INTENT_* code. */ /* short unused14; */ + short datatype; /*!< Defines data type! */ /* short datatype; */ + short bitpix; /*!< Number bits/voxel. */ /* short bitpix; */ + short slice_start; /*!< First slice index. */ /* short dim_un0; */ + float pixdim[8]; /*!< Grid spacings. */ /* float pixdim[8]; */ + float vox_offset; /*!< Offset into .nii file */ /* float vox_offset; */ + float scl_slope ; /*!< Data scaling: slope. */ /* float funused1; */ + float scl_inter ; /*!< Data scaling: offset. */ /* float funused2; */ + short slice_end; /*!< Last slice index. */ /* float funused3; */ + char slice_code ; /*!< Slice timing order. */ + char xyzt_units ; /*!< Units of pixdim[1..4] */ + float cal_max; /*!< Max display intensity */ /* float cal_max; */ + float cal_min; /*!< Min display intensity */ /* float cal_min; */ + float slice_duration;/*!< Time for 1 slice. */ /* float compressed; */ + float toffset; /*!< Time axis shift. */ /* float verified; */ + int glmax; /*!< ++UNUSED++ */ /* int glmax; */ + int glmin; /*!< ++UNUSED++ */ /* int glmin; */ + + /*--- was data_history substruct ---*/ + char descrip[80]; /*!< any text you like. */ /* char descrip[80]; */ + char aux_file[24]; /*!< auxiliary filename. */ /* char aux_file[24]; */ + + short qform_code ; /*!< NIFTI_XFORM_* code. */ /*-- all ANALYZE 7.5 ---*/ + short sform_code ; /*!< NIFTI_XFORM_* code. */ /* fields below here */ + /* are replaced */ + float quatern_b ; /*!< Quaternion b param. */ + float quatern_c ; /*!< Quaternion c param. */ + float quatern_d ; /*!< Quaternion d param. */ + float qoffset_x ; /*!< Quaternion x shift. */ + float qoffset_y ; /*!< Quaternion y shift. */ + float qoffset_z ; /*!< Quaternion z shift. */ + + float srow_x[4] ; /*!< 1st row affine transform. */ + float srow_y[4] ; /*!< 2nd row affine transform. */ + float srow_z[4] ; /*!< 3rd row affine transform. */ + + char intent_name[16];/*!< 'name' or meaning of data. */ + + char magic[4] ; /*!< MUST be "ni1\0" or "n+1\0". */ + + } ; /**** 348 bytes total ****/ + + typedef struct nifti_1_header nifti_1_header ; + + /*---------------------------------------------------------------------------*/ + /* HEADER EXTENSIONS: + ----------------- + After the end of the 348 byte header (e.g., after the magic field), + the next 4 bytes are a char array field named "extension". By default, + all 4 bytes of this array should be set to zero. In a .nii file, these + 4 bytes will always be present, since the earliest start point for + the image data is byte #352. In a separate .hdr file, these bytes may + or may not be present. If not present (i.e., if the length of the .hdr + file is 348 bytes), then a NIfTI-1 compliant program should use the + default value of extension={0,0,0,0}. The first byte (extension[0]) + is the only value of this array that is specified at present. The other + 3 bytes are reserved for future use. + + If extension[0] is nonzero, it indicates that extended header information + is present in the bytes following the extension array. In a .nii file, + this extended header data is before the image data (and vox_offset + must be set correctly to allow for this). In a .hdr file, this extended + data follows extension and proceeds (potentially) to the end of the file. + + The format of extended header data is weakly specified. Each extension + must be an integer multiple of 16 bytes long. The first 8 bytes of each + extension comprise 2 integers: + int esize , ecode ; + These values may need to be byte-swapped, as indicated by dim[0] for + the rest of the header. + * esize is the number of bytes that form the extended header data + + esize must be a positive integral multiple of 16 + + this length includes the 8 bytes of esize and ecode themselves + * ecode is a non-negative integer that indicates the format of the + extended header data that follows + + different ecode values are assigned to different developer groups + + at present, the "registered" values for code are + = 0 = unknown private format (not recommended!) + = 2 = DICOM format (i.e., attribute tags and values) + = 4 = AFNI group (i.e., ASCII XML-ish elements) + In the interests of interoperability (a primary rationale for NIfTI), + groups developing software that uses this extension mechanism are + encouraged to document and publicize the format of their extensions. + To this end, the NIfTI DFWG will assign even numbered codes upon request + to groups submitting at least rudimentary documentation for the format + of their extension; at present, the contact is mailto:rwcox@nih.gov. + The assigned codes and documentation will be posted on the NIfTI + website. All odd values of ecode (and 0) will remain unassigned; + at least, until the even ones are used up, when we get to 2,147,483,646. + + Note that the other contents of the extended header data section are + totally unspecified by the NIfTI-1 standard. In particular, if binary + data is stored in such a section, its byte order is not necessarily + the same as that given by examining dim[0]; it is incumbent on the + programs dealing with such data to determine the byte order of binary + extended header data. + + Multiple extended header sections are allowed, each starting with an + esize,ecode value pair. The first esize value, as described above, + is at bytes #352-355 in the .hdr or .nii file (files start at byte #0). + If this value is positive, then the second (esize2) will be found + starting at byte #352+esize1 , the third (esize3) at byte #352+esize1+esize2, + et cetera. Of course, in a .nii file, the value of vox_offset must + be compatible with these extensions. If a malformed file indicates + that an extended header data section would run past vox_offset, then + the entire extended header section should be ignored. In a .hdr file, + if an extended header data section would run past the end-of-file, + that extended header data should also be ignored. + + With the above scheme, a program can successively examine the esize + and ecode values, and skip over each extended header section if the + program doesn't know how to interpret the data within. Of course, any + program can simply ignore all extended header sections simply by jumping + straight to the image data using vox_offset. + -----------------------------------------------------------------------------*/ + + /*! \struct nifti1_extender + \brief This structure represents a 4-byte string that should follow the + binary nifti_1_header data in a NIFTI-1 header file. If the char + values are {1,0,0,0}, the file is expected to contain extensions, + values of {0,0,0,0} imply the file does not contain extensions. + Other sequences of values are not currently defined. + */ + struct nifti1_extender + { + char extension[4] ; + } ; + typedef struct nifti1_extender nifti1_extender ; + + /*! \struct nifti1_extension + \brief Data structure defining the fields of a header extension. + */ + struct nifti1_extension + { + int esize ; /*!< size of extension, in bytes (must be multiple of 16) */ + int ecode ; /*!< extension code, one of the NIFTI_ECODE_ values */ + char * edata ; /*!< raw data, with no byte swapping (length is esize-8) */ + } ; + typedef struct nifti1_extension nifti1_extension ; + + /*---------------------------------------------------------------------------*/ + /* DATA DIMENSIONALITY (as in ANALYZE 7.5): + --------------------------------------- + dim[0] = number of dimensions; + - if dim[0] is outside range 1..7, then the header information + needs to be byte swapped appropriately + - ANALYZE supports dim[0] up to 7, but NIFTI-1 reserves + dimensions 1,2,3 for space (x,y,z), 4 for time (t), and + 5,6,7 for anything else needed. + + dim[i] = length of dimension #i, for i=1..dim[0] (must be positive) + - also see the discussion of intent_code, far below + + pixdim[i] = voxel width along dimension #i, i=1..dim[0] (positive) + - cf. ORIENTATION section below for use of pixdim[0] + - the units of pixdim can be specified with the xyzt_units + field (also described far below). + + Number of bits per voxel value is in bitpix, which MUST correspond with + the datatype field. The total number of bytes in the image data is + dim[1] * ... * dim[dim[0]] * bitpix / 8 + + In NIFTI-1 files, dimensions 1,2,3 are for space, dimension 4 is for time, + and dimension 5 is for storing multiple values at each spatiotemporal + voxel. Some examples: + - A typical whole-brain FMRI experiment's time series: + - dim[0] = 4 + - dim[1] = 64 pixdim[1] = 3.75 xyzt_units = NIFTI_UNITS_MM + - dim[2] = 64 pixdim[2] = 3.75 | NIFTI_UNITS_SEC + - dim[3] = 20 pixdim[3] = 5.0 + - dim[4] = 120 pixdim[4] = 2.0 + - A typical T1-weighted anatomical volume: + - dim[0] = 3 + - dim[1] = 256 pixdim[1] = 1.0 xyzt_units = NIFTI_UNITS_MM + - dim[2] = 256 pixdim[2] = 1.0 + - dim[3] = 128 pixdim[3] = 1.1 + - A single slice EPI time series: + - dim[0] = 4 + - dim[1] = 64 pixdim[1] = 3.75 xyzt_units = NIFTI_UNITS_MM + - dim[2] = 64 pixdim[2] = 3.75 | NIFTI_UNITS_SEC + - dim[3] = 1 pixdim[3] = 5.0 + - dim[4] = 1200 pixdim[4] = 0.2 + - A 3-vector stored at each point in a 3D volume: + - dim[0] = 5 + - dim[1] = 256 pixdim[1] = 1.0 xyzt_units = NIFTI_UNITS_MM + - dim[2] = 256 pixdim[2] = 1.0 + - dim[3] = 128 pixdim[3] = 1.1 + - dim[4] = 1 pixdim[4] = 0.0 + - dim[5] = 3 intent_code = NIFTI_INTENT_VECTOR + - A single time series with a 3x3 matrix at each point: + - dim[0] = 5 + - dim[1] = 1 xyzt_units = NIFTI_UNITS_SEC + - dim[2] = 1 + - dim[3] = 1 + - dim[4] = 1200 pixdim[4] = 0.2 + - dim[5] = 9 intent_code = NIFTI_INTENT_GENMATRIX + - intent_p1 = intent_p2 = 3.0 (indicates matrix dimensions) + -----------------------------------------------------------------------------*/ + + /*---------------------------------------------------------------------------*/ + /* DATA STORAGE: + ------------ + If the magic field is "n+1", then the voxel data is stored in the + same file as the header. In this case, the voxel data starts at offset + (int)vox_offset into the header file. Thus, vox_offset=352.0 means that + the data starts immediately after the NIFTI-1 header. If vox_offset is + greater than 352, the NIFTI-1 format does not say much about the + contents of the dataset file between the end of the header and the + start of the data. + + FILES: + ----- + If the magic field is "ni1", then the voxel data is stored in the + associated ".img" file, starting at offset 0 (i.e., vox_offset is not + used in this case, and should be set to 0.0). + + When storing NIFTI-1 datasets in pairs of files, it is customary to name + the files in the pattern "name.hdr" and "name.img", as in ANALYZE 7.5. + When storing in a single file ("n+1"), the file name should be in + the form "name.nii" (the ".nft" and ".nif" suffixes are already taken; + cf. http://www.icdatamaster.com/n.html ). + + BYTE ORDERING: + ------------- + The byte order of the data arrays is presumed to be the same as the byte + order of the header (which is determined by examining dim[0]). + + Floating point types are presumed to be stored in IEEE-754 format. + -----------------------------------------------------------------------------*/ + + /*---------------------------------------------------------------------------*/ + /* DETAILS ABOUT vox_offset: + ------------------------ + In a .nii file, the vox_offset field value is interpreted as the start + location of the image data bytes in that file. In a .hdr/.img file pair, + the vox_offset field value is the start location of the image data + bytes in the .img file. + * If vox_offset is less than 352 in a .nii file, it is equivalent + to 352 (i.e., image data never starts before byte #352 in a .nii file). + * The default value for vox_offset in a .nii file is 352. + * In a .hdr file, the default value for vox_offset is 0. + * vox_offset should be an integer multiple of 16; otherwise, some + programs may not work properly (e.g., SPM). This is to allow + memory-mapped input to be properly byte-aligned. + Note that since vox_offset is an IEEE-754 32 bit float (for compatibility + with the ANALYZE-7.5 format), it effectively has a 24 bit mantissa. All + integers from 0 to 2^24 can be represented exactly in this format, but not + all larger integers are exactly storable as IEEE-754 32 bit floats. However, + unless you plan to have vox_offset be potentially larger than 16 MB, this + should not be an issue. (Actually, any integral multiple of 16 up to 2^27 + can be represented exactly in this format, which allows for up to 128 MB + of random information before the image data. If that isn't enough, then + perhaps this format isn't right for you.) + + In a .img file (i.e., image data stored separately from the NIfTI-1 + header), data bytes between #0 and #vox_offset-1 (inclusive) are completely + undefined and unregulated by the NIfTI-1 standard. One potential use of + having vox_offset > 0 in the .hdr/.img file pair storage method is to make + the .img file be a copy of (or link to) a pre-existing image file in some + other format, such as DICOM; then vox_offset would be set to the offset of + the image data in this file. (It may not be possible to follow the + "multiple-of-16 rule" with an arbitrary external file; using the NIfTI-1 + format in such a case may lead to a file that is incompatible with software + that relies on vox_offset being a multiple of 16.) + + In a .nii file, data bytes between #348 and #vox_offset-1 (inclusive) may + be used to store user-defined extra information; similarly, in a .hdr file, + any data bytes after byte #347 are available for user-defined extra + information. The (very weak) regulation of this extra header data is + described elsewhere. + -----------------------------------------------------------------------------*/ + + /*---------------------------------------------------------------------------*/ + /* DATA SCALING: + ------------ + If the scl_slope field is nonzero, then each voxel value in the dataset + should be scaled as + y = scl_slope * x + scl_inter + where x = voxel value stored + y = "true" voxel value + Normally, we would expect this scaling to be used to store "true" floating + values in a smaller integer datatype, but that is not required. That is, + it is legal to use scaling even if the datatype is a float type (crazy, + perhaps, but legal). + - However, the scaling is to be ignored if datatype is DT_RGB24. + - If datatype is a complex type, then the scaling is to be + applied to both the real and imaginary parts. + + The cal_min and cal_max fields (if nonzero) are used for mapping (possibly + scaled) dataset values to display colors: + - Minimum display intensity (black) corresponds to dataset value cal_min. + - Maximum display intensity (white) corresponds to dataset value cal_max. + - Dataset values below cal_min should display as black also, and values + above cal_max as white. + - Colors "black" and "white", of course, may refer to any scalar display + scheme (e.g., a color lookup table specified via aux_file). + - cal_min and cal_max only make sense when applied to scalar-valued + datasets (i.e., dim[0] < 5 or dim[5] = 1). + -----------------------------------------------------------------------------*/ + + /*---------------------------------------------------------------------------*/ + /* TYPE OF DATA (acceptable values for datatype field): + --------------------------------------------------- + Values of datatype smaller than 256 are ANALYZE 7.5 compatible. + Larger values are NIFTI-1 additions. These are all multiples of 256, so + that no bits below position 8 are set in datatype. But there is no need + to use only powers-of-2, as the original ANALYZE 7.5 datatype codes do. + + The additional codes are intended to include a complete list of basic + scalar types, including signed and unsigned integers from 8 to 64 bits, + floats from 32 to 128 bits, and complex (float pairs) from 64 to 256 bits. + + Note that most programs will support only a few of these datatypes! + A NIFTI-1 program should fail gracefully (e.g., print a warning message) + when it encounters a dataset with a type it doesn't like. + -----------------------------------------------------------------------------*/ + +#undef DT_UNKNOWN /* defined in dirent.h on some Unix systems */ + + /*! \defgroup NIFTI1_DATATYPES + \brief nifti1 datatype codes + @{ + */ + /*--- the original ANALYZE 7.5 type codes ---*/ +#define DT_NONE 0 +#define DT_UNKNOWN 0 /* what it says, dude */ +#define DT_BINARY 1 /* binary (1 bit/voxel) */ +#define DT_UNSIGNED_CHAR 2 /* unsigned char (8 bits/voxel) */ +#define DT_SIGNED_SHORT 4 /* signed short (16 bits/voxel) */ +#define DT_SIGNED_INT 8 /* signed int (32 bits/voxel) */ +#define DT_FLOAT 16 /* float (32 bits/voxel) */ +#define DT_COMPLEX 32 /* complex (64 bits/voxel) */ +#define DT_DOUBLE 64 /* double (64 bits/voxel) */ +#define DT_RGB 128 /* RGB triple (24 bits/voxel) */ +#define DT_ALL 255 /* not very useful (?) */ + + /*----- another set of names for the same ---*/ +#define DT_UINT8 2 +#define DT_INT16 4 +#define DT_INT32 8 +#define DT_FLOAT32 16 +#define DT_COMPLEX64 32 +#define DT_FLOAT64 64 +#define DT_RGB24 128 + + /*------------------- new codes for NIFTI ---*/ +#define DT_INT8 256 /* signed char (8 bits) */ +#define DT_UINT16 512 /* unsigned short (16 bits) */ +#define DT_UINT32 768 /* unsigned int (32 bits) */ +#define DT_INT64 1024 /* long long (64 bits) */ +#define DT_UINT64 1280 /* unsigned long long (64 bits) */ +#define DT_FLOAT128 1536 /* long double (128 bits) */ +#define DT_COMPLEX128 1792 /* double pair (128 bits) */ +#define DT_COMPLEX256 2048 /* long double pair (256 bits) */ +#define DT_RGBA32 2304 /* 4 byte RGBA (32 bits/voxel) */ + /* @} */ + + + /*------- aliases for all the above codes ---*/ + + /*! \defgroup NIFTI1_DATATYPE_ALIASES + \brief aliases for the nifti1 datatype codes + @{ + */ + /*! unsigned char. */ +#define NIFTI_TYPE_UINT8 2 + /*! signed short. */ +#define NIFTI_TYPE_INT16 4 + /*! signed int. */ +#define NIFTI_TYPE_INT32 8 + /*! 32 bit float. */ +#define NIFTI_TYPE_FLOAT32 16 + /*! 64 bit complex = 2 32 bit floats. */ +#define NIFTI_TYPE_COMPLEX64 32 + /*! 64 bit float = double. */ +#define NIFTI_TYPE_FLOAT64 64 + /*! 3 8 bit bytes. */ +#define NIFTI_TYPE_RGB24 128 + /*! signed char. */ +#define NIFTI_TYPE_INT8 256 + /*! unsigned short. */ +#define NIFTI_TYPE_UINT16 512 + /*! unsigned int. */ +#define NIFTI_TYPE_UINT32 768 + /*! signed long long. */ +#define NIFTI_TYPE_INT64 1024 + /*! unsigned long long. */ +#define NIFTI_TYPE_UINT64 1280 + /*! 128 bit float = long double. */ +#define NIFTI_TYPE_FLOAT128 1536 + /*! 128 bit complex = 2 64 bit floats. */ +#define NIFTI_TYPE_COMPLEX128 1792 + /*! 256 bit complex = 2 128 bit floats */ +#define NIFTI_TYPE_COMPLEX256 2048 + /*! 4 8 bit bytes. */ +#define NIFTI_TYPE_RGBA32 2304 + /* @} */ + + /*-------- sample typedefs for complicated types ---*/ +#if 0 + typedef struct + { + float r,i; + } complex_float ; + typedef struct + { + double r,i; + } complex_double ; + typedef struct + { + long double r,i; + } complex_longdouble ; + typedef struct + { + unsigned char r,g,b; + } rgb_byte ; +#endif + + /*---------------------------------------------------------------------------*/ + /* INTERPRETATION OF VOXEL DATA: + ---------------------------- + The intent_code field can be used to indicate that the voxel data has + some particular meaning. In particular, a large number of codes is + given to indicate that the the voxel data should be interpreted as + being drawn from a given probability distribution. + + VECTOR-VALUED DATASETS: + ---------------------- + The 5th dimension of the dataset, if present (i.e., dim[0]=5 and + dim[5] > 1), contains multiple values (e.g., a vector) to be stored + at each spatiotemporal location. For example, the header values + - dim[0] = 5 + - dim[1] = 64 + - dim[2] = 64 + - dim[3] = 20 + - dim[4] = 1 (indicates no time axis) + - dim[5] = 3 + - datatype = DT_FLOAT + - intent_code = NIFTI_INTENT_VECTOR + mean that this dataset should be interpreted as a 3D volume (64x64x20), + with a 3-vector of floats defined at each point in the 3D grid. + + A program reading a dataset with a 5th dimension may want to reformat + the image data to store each voxels' set of values together in a struct + or array. This programming detail, however, is beyond the scope of the + NIFTI-1 file specification! Uses of dimensions 6 and 7 are also not + specified here. + + STATISTICAL PARAMETRIC DATASETS (i.e., SPMs): + -------------------------------------------- + Values of intent_code from NIFTI_FIRST_STATCODE to NIFTI_LAST_STATCODE + (inclusive) indicate that the numbers in the dataset should be interpreted + as being drawn from a given distribution. Most such distributions have + auxiliary parameters (e.g., NIFTI_INTENT_TTEST has 1 DOF parameter). + + If the dataset DOES NOT have a 5th dimension, then the auxiliary parameters + are the same for each voxel, and are given in header fields intent_p1, + intent_p2, and intent_p3. + + If the dataset DOES have a 5th dimension, then the auxiliary parameters + are different for each voxel. For example, the header values + - dim[0] = 5 + - dim[1] = 128 + - dim[2] = 128 + - dim[3] = 1 (indicates a single slice) + - dim[4] = 1 (indicates no time axis) + - dim[5] = 2 + - datatype = DT_FLOAT + - intent_code = NIFTI_INTENT_TTEST + mean that this is a 2D dataset (128x128) of t-statistics, with the + t-statistic being in the first "plane" of data and the degrees-of-freedom + parameter being in the second "plane" of data. + + If the dataset 5th dimension is used to store the voxel-wise statistical + parameters, then dim[5] must be 1 plus the number of parameters required + by that distribution (e.g., intent_code=NIFTI_INTENT_TTEST implies dim[5] + must be 2, as in the example just above). + + Note: intent_code values 2..10 are compatible with AFNI 1.5x (which is + why there is no code with value=1, which is obsolescent in AFNI). + + OTHER INTENTIONS: + ---------------- + The purpose of the intent_* fields is to help interpret the values + stored in the dataset. Some non-statistical values for intent_code + and conventions are provided for storing other complex data types. + + The intent_name field provides space for a 15 character (plus 0 byte) + 'name' string for the type of data stored. Examples: + - intent_code = NIFTI_INTENT_ESTIMATE; intent_name = "T1"; + could be used to signify that the voxel values are estimates of the + NMR parameter T1. + - intent_code = NIFTI_INTENT_TTEST; intent_name = "House"; + could be used to signify that the voxel values are t-statistics + for the significance of 'activation' response to a House stimulus. + - intent_code = NIFTI_INTENT_DISPVECT; intent_name = "ToMNI152"; + could be used to signify that the voxel values are a displacement + vector that transforms each voxel (x,y,z) location to the + corresponding location in the MNI152 standard brain. + - intent_code = NIFTI_INTENT_SYMMATRIX; intent_name = "DTI"; + could be used to signify that the voxel values comprise a diffusion + tensor image. + + If no data name is implied or needed, intent_name[0] should be set to 0. + -----------------------------------------------------------------------------*/ + + /*! default: no intention is indicated in the header. */ + +#define NIFTI_INTENT_NONE 0 + + /*-------- These codes are for probability distributions ---------------*/ + /* Most distributions have a number of parameters, + below denoted by p1, p2, and p3, and stored in + - intent_p1, intent_p2, intent_p3 if dataset doesn't have 5th dimension + - image data array if dataset does have 5th dimension + + Functions to compute with many of the distributions below can be found + in the CDF library from U Texas. + + Formulas for and discussions of these distributions can be found in the + following books: + + [U] Univariate Discrete Distributions, + NL Johnson, S Kotz, AW Kemp. + + [C1] Continuous Univariate Distributions, vol. 1, + NL Johnson, S Kotz, N Balakrishnan. + + [C2] Continuous Univariate Distributions, vol. 2, + NL Johnson, S Kotz, N Balakrishnan. */ + /*----------------------------------------------------------------------*/ + + /*! [C2, chap 32] Correlation coefficient R (1 param): + p1 = degrees of freedom + R/sqrt(1-R*R) is t-distributed with p1 DOF. */ + + /*! \defgroup NIFTI1_INTENT_CODES + \brief nifti1 intent codes, to describe intended meaning of dataset contents + @{ + */ +#define NIFTI_INTENT_CORREL 2 + + /*! [C2, chap 28] Student t statistic (1 param): p1 = DOF. */ + +#define NIFTI_INTENT_TTEST 3 + + /*! [C2, chap 27] Fisher F statistic (2 params): + p1 = numerator DOF, p2 = denominator DOF. */ + +#define NIFTI_INTENT_FTEST 4 + + /*! [C1, chap 13] Standard normal (0 params): Density = N(0,1). */ + +#define NIFTI_INTENT_ZSCORE 5 + + /*! [C1, chap 18] Chi-squared (1 param): p1 = DOF. + Density(x) proportional to exp(-x/2) * x^(p1/2-1). */ + +#define NIFTI_INTENT_CHISQ 6 + + /*! [C2, chap 25] Beta distribution (2 params): p1=a, p2=b. + Density(x) proportional to x^(a-1) * (1-x)^(b-1). */ + +#define NIFTI_INTENT_BETA 7 + + /*! [U, chap 3] Binomial distribution (2 params): + p1 = number of trials, p2 = probability per trial. + Prob(x) = (p1 choose x) * p2^x * (1-p2)^(p1-x), for x=0,1,...,p1. */ + +#define NIFTI_INTENT_BINOM 8 + + /*! [C1, chap 17] Gamma distribution (2 params): + p1 = shape, p2 = scale. + Density(x) proportional to x^(p1-1) * exp(-p2*x). */ + +#define NIFTI_INTENT_GAMMA 9 + + /*! [U, chap 4] Poisson distribution (1 param): p1 = mean. + Prob(x) = exp(-p1) * p1^x / x! , for x=0,1,2,.... */ + +#define NIFTI_INTENT_POISSON 10 + + /*! [C1, chap 13] Normal distribution (2 params): + p1 = mean, p2 = standard deviation. */ + +#define NIFTI_INTENT_NORMAL 11 + + /*! [C2, chap 30] Noncentral F statistic (3 params): + p1 = numerator DOF, p2 = denominator DOF, + p3 = numerator noncentrality parameter. */ + +#define NIFTI_INTENT_FTEST_NONC 12 + + /*! [C2, chap 29] Noncentral chi-squared statistic (2 params): + p1 = DOF, p2 = noncentrality parameter. */ + +#define NIFTI_INTENT_CHISQ_NONC 13 + + /*! [C2, chap 23] Logistic distribution (2 params): + p1 = location, p2 = scale. + Density(x) proportional to sech^2((x-p1)/(2*p2)). */ + +#define NIFTI_INTENT_LOGISTIC 14 + + /*! [C2, chap 24] Laplace distribution (2 params): + p1 = location, p2 = scale. + Density(x) proportional to exp(-abs(x-p1)/p2). */ + +#define NIFTI_INTENT_LAPLACE 15 + + /*! [C2, chap 26] Uniform distribution: p1 = lower end, p2 = upper end. */ + +#define NIFTI_INTENT_UNIFORM 16 + + /*! [C2, chap 31] Noncentral t statistic (2 params): + p1 = DOF, p2 = noncentrality parameter. */ + +#define NIFTI_INTENT_TTEST_NONC 17 + + /*! [C1, chap 21] Weibull distribution (3 params): + p1 = location, p2 = scale, p3 = power. + Density(x) proportional to + ((x-p1)/p2)^(p3-1) * exp(-((x-p1)/p2)^p3) for x > p1. */ + +#define NIFTI_INTENT_WEIBULL 18 + + /*! [C1, chap 18] Chi distribution (1 param): p1 = DOF. + Density(x) proportional to x^(p1-1) * exp(-x^2/2) for x > 0. + p1 = 1 = 'half normal' distribution + p1 = 2 = Rayleigh distribution + p1 = 3 = Maxwell-Boltzmann distribution. */ + +#define NIFTI_INTENT_CHI 19 + + /*! [C1, chap 15] Inverse Gaussian (2 params): + p1 = mu, p2 = lambda + Density(x) proportional to + exp(-p2*(x-p1)^2/(2*p1^2*x)) / x^3 for x > 0. */ + +#define NIFTI_INTENT_INVGAUSS 20 + + /*! [C2, chap 22] Extreme value type I (2 params): + p1 = location, p2 = scale + cdf(x) = exp(-exp(-(x-p1)/p2)). */ + +#define NIFTI_INTENT_EXTVAL 21 + + /*! Data is a 'p-value' (no params). */ + +#define NIFTI_INTENT_PVAL 22 + + /*! Data is ln(p-value) (no params). + To be safe, a program should compute p = exp(-abs(this_value)). + The nifti_stats.c library returns this_value + as positive, so that this_value = -log(p). */ + + +#define NIFTI_INTENT_LOGPVAL 23 + + /*! Data is log10(p-value) (no params). + To be safe, a program should compute p = pow(10.,-abs(this_value)). + The nifti_stats.c library returns this_value + as positive, so that this_value = -log10(p). */ + +#define NIFTI_INTENT_LOG10PVAL 24 + + /*! Smallest intent_code that indicates a statistic. */ + +#define NIFTI_FIRST_STATCODE 2 + + /*! Largest intent_code that indicates a statistic. */ + +#define NIFTI_LAST_STATCODE 24 + + /*---------- these values for intent_code aren't for statistics ----------*/ + + /*! To signify that the value at each voxel is an estimate + of some parameter, set intent_code = NIFTI_INTENT_ESTIMATE. + The name of the parameter may be stored in intent_name. */ + +#define NIFTI_INTENT_ESTIMATE 1001 + + /*! To signify that the value at each voxel is an index into + some set of labels, set intent_code = NIFTI_INTENT_LABEL. + The filename with the labels may stored in aux_file. */ + +#define NIFTI_INTENT_LABEL 1002 + + /*! To signify that the value at each voxel is an index into the + NeuroNames labels set, set intent_code = NIFTI_INTENT_NEURONAME. */ + +#define NIFTI_INTENT_NEURONAME 1003 + + /*! To store an M x N matrix at each voxel: + - dataset must have a 5th dimension (dim[0]=5 and dim[5]>1) + - intent_code must be NIFTI_INTENT_GENMATRIX + - dim[5] must be M*N + - intent_p1 must be M (in float format) + - intent_p2 must be N (ditto) + - the matrix values A[i][[j] are stored in row-order: + - A[0][0] A[0][1] ... A[0][N-1] + - A[1][0] A[1][1] ... A[1][N-1] + - etc., until + - A[M-1][0] A[M-1][1] ... A[M-1][N-1] */ + +#define NIFTI_INTENT_GENMATRIX 1004 + + /*! To store an NxN symmetric matrix at each voxel: + - dataset must have a 5th dimension + - intent_code must be NIFTI_INTENT_SYMMATRIX + - dim[5] must be N*(N+1)/2 + - intent_p1 must be N (in float format) + - the matrix values A[i][[j] are stored in row-order: + - A[0][0] + - A[1][0] A[1][1] + - A[2][0] A[2][1] A[2][2] + - etc.: row-by-row */ + +#define NIFTI_INTENT_SYMMATRIX 1005 + + /*! To signify that the vector value at each voxel is to be taken + as a displacement field or vector: + - dataset must have a 5th dimension + - intent_code must be NIFTI_INTENT_DISPVECT + - dim[5] must be the dimensionality of the displacment + vector (e.g., 3 for spatial displacement, 2 for in-plane) */ + +#define NIFTI_INTENT_DISPVECT 1006 /* specifically for displacements */ +#define NIFTI_INTENT_VECTOR 1007 /* for any other type of vector */ + + /*! To signify that the vector value at each voxel is really a + spatial coordinate (e.g., the vertices or nodes of a surface mesh): + - dataset must have a 5th dimension + - intent_code must be NIFTI_INTENT_POINTSET + - dim[0] = 5 + - dim[1] = number of points + - dim[2] = dim[3] = dim[4] = 1 + - dim[5] must be the dimensionality of space (e.g., 3 => 3D space). + - intent_name may describe the object these points come from + (e.g., "pial", "gray/white" , "EEG", "MEG"). */ + +#define NIFTI_INTENT_POINTSET 1008 + + /*! To signify that the vector value at each voxel is really a triple + of indexes (e.g., forming a triangle) from a pointset dataset: + - dataset must have a 5th dimension + - intent_code must be NIFTI_INTENT_TRIANGLE + - dim[0] = 5 + - dim[1] = number of triangles + - dim[2] = dim[3] = dim[4] = 1 + - dim[5] = 3 + - datatype should be an integer type (preferably DT_INT32) + - the data values are indexes (0,1,...) into a pointset dataset. */ + +#define NIFTI_INTENT_TRIANGLE 1009 + + /*! To signify that the vector value at each voxel is a quaternion: + - dataset must have a 5th dimension + - intent_code must be NIFTI_INTENT_QUATERNION + - dim[0] = 5 + - dim[5] = 4 + - datatype should be a floating point type */ + +#define NIFTI_INTENT_QUATERNION 1010 + + /*! Dimensionless value - no params - although, as in _ESTIMATE + the name of the parameter may be stored in intent_name. */ + +#define NIFTI_INTENT_DIMLESS 1011 + + /*---------- these values apply to GIFTI datasets ----------*/ + + /*! To signify that the value at each location is from a time series. */ + +#define NIFTI_INTENT_TIME_SERIES 2001 + + /*! To signify that the value at each location is a node index, from + a complete surface dataset. */ + +#define NIFTI_INTENT_NODE_INDEX 2002 + + /*! To signify that the vector value at each location is an RGB triplet, + of whatever type. + - dataset must have a 5th dimension + - dim[0] = 5 + - dim[1] = number of nodes + - dim[2] = dim[3] = dim[4] = 1 + - dim[5] = 3 + */ + +#define NIFTI_INTENT_RGB_VECTOR 2003 + + /*! To signify that the vector value at each location is a 4 valued RGBA + vector, of whatever type. + - dataset must have a 5th dimension + - dim[0] = 5 + - dim[1] = number of nodes + - dim[2] = dim[3] = dim[4] = 1 + - dim[5] = 4 + */ + +#define NIFTI_INTENT_RGBA_VECTOR 2004 + + /*! To signify that the value at each location is a shape value, such + as the curvature. */ + +#define NIFTI_INTENT_SHAPE 2005 + + /* @} */ + + /*---------------------------------------------------------------------------*/ + /* 3D IMAGE (VOLUME) ORIENTATION AND LOCATION IN SPACE: + --------------------------------------------------- + There are 3 different methods by which continuous coordinates can + attached to voxels. The discussion below emphasizes 3D volumes, and + the continuous coordinates are referred to as (x,y,z). The voxel + index coordinates (i.e., the array indexes) are referred to as (i,j,k), + with valid ranges: + i = 0 .. dim[1]-1 + j = 0 .. dim[2]-1 (if dim[0] >= 2) + k = 0 .. dim[3]-1 (if dim[0] >= 3) + The (x,y,z) coordinates refer to the CENTER of a voxel. In methods + 2 and 3, the (x,y,z) axes refer to a subject-based coordinate system, + with + +x = Right +y = Anterior +z = Superior. + This is a right-handed coordinate system. However, the exact direction + these axes point with respect to the subject depends on qform_code + (Method 2) and sform_code (Method 3). + + N.B.: The i index varies most rapidly, j index next, k index slowest. + Thus, voxel (i,j,k) is stored starting at location + (i + j*dim[1] + k*dim[1]*dim[2]) * (bitpix/8) + into the dataset array. + + N.B.: The ANALYZE 7.5 coordinate system is + +x = Left +y = Anterior +z = Superior + which is a left-handed coordinate system. This backwardness is + too difficult to tolerate, so this NIFTI-1 standard specifies the + coordinate order which is most common in functional neuroimaging. + + N.B.: The 3 methods below all give the locations of the voxel centers + in the (x,y,z) coordinate system. In many cases, programs will wish + to display image data on some other grid. In such a case, the program + will need to convert its desired (x,y,z) values into (i,j,k) values + in order to extract (or interpolate) the image data. This operation + would be done with the inverse transformation to those described below. + + N.B.: Method 2 uses a factor 'qfac' which is either -1 or 1; qfac is + stored in the otherwise unused pixdim[0]. If pixdim[0]=0.0 (which + should not occur), we take qfac=1. Of course, pixdim[0] is only used + when reading a NIFTI-1 header, not when reading an ANALYZE 7.5 header. + + N.B.: The units of (x,y,z) can be specified using the xyzt_units field. + + METHOD 1 (the "old" way, used only when qform_code = 0): + ------------------------------------------------------- + The coordinate mapping from (i,j,k) to (x,y,z) is the ANALYZE + 7.5 way. This is a simple scaling relationship: + + x = pixdim[1] * i + y = pixdim[2] * j + z = pixdim[3] * k + + No particular spatial orientation is attached to these (x,y,z) + coordinates. (NIFTI-1 does not have the ANALYZE 7.5 orient field, + which is not general and is often not set properly.) This method + is not recommended, and is present mainly for compatibility with + ANALYZE 7.5 files. + + METHOD 2 (used when qform_code > 0, which should be the "normal" case): + --------------------------------------------------------------------- + The (x,y,z) coordinates are given by the pixdim[] scales, a rotation + matrix, and a shift. This method is intended to represent + "scanner-anatomical" coordinates, which are often embedded in the + image header (e.g., DICOM fields (0020,0032), (0020,0037), (0028,0030), + and (0018,0050)), and represent the nominal orientation and location of + the data. This method can also be used to represent "aligned" + coordinates, which would typically result from some post-acquisition + alignment of the volume to a standard orientation (e.g., the same + subject on another day, or a rigid rotation to true anatomical + orientation from the tilted position of the subject in the scanner). + The formula for (x,y,z) in terms of header parameters and (i,j,k) is: + + [ x ] [ R11 R12 R13 ] [ pixdim[1] * i ] [ qoffset_x ] + [ y ] = [ R21 R22 R23 ] [ pixdim[2] * j ] + [ qoffset_y ] + [ z ] [ R31 R32 R33 ] [ qfac * pixdim[3] * k ] [ qoffset_z ] + + The qoffset_* shifts are in the NIFTI-1 header. Note that the center + of the (i,j,k)=(0,0,0) voxel (first value in the dataset array) is + just (x,y,z)=(qoffset_x,qoffset_y,qoffset_z). + + The rotation matrix R is calculated from the quatern_* parameters. + This calculation is described below. + + The scaling factor qfac is either 1 or -1. The rotation matrix R + defined by the quaternion parameters is "proper" (has determinant 1). + This may not fit the needs of the data; for example, if the image + grid is + i increases from Left-to-Right + j increases from Anterior-to-Posterior + k increases from Inferior-to-Superior + Then (i,j,k) is a left-handed triple. In this example, if qfac=1, + the R matrix would have to be + + [ 1 0 0 ] + [ 0 -1 0 ] which is "improper" (determinant = -1). + [ 0 0 1 ] + + If we set qfac=-1, then the R matrix would be + + [ 1 0 0 ] + [ 0 -1 0 ] which is proper. + [ 0 0 -1 ] + + This R matrix is represented by quaternion [a,b,c,d] = [0,1,0,0] + (which encodes a 180 degree rotation about the x-axis). + + METHOD 3 (used when sform_code > 0): + ----------------------------------- + The (x,y,z) coordinates are given by a general affine transformation + of the (i,j,k) indexes: + + x = srow_x[0] * i + srow_x[1] * j + srow_x[2] * k + srow_x[3] + y = srow_y[0] * i + srow_y[1] * j + srow_y[2] * k + srow_y[3] + z = srow_z[0] * i + srow_z[1] * j + srow_z[2] * k + srow_z[3] + + The srow_* vectors are in the NIFTI_1 header. Note that no use is + made of pixdim[] in this method. + + WHY 3 METHODS? + -------------- + Method 1 is provided only for backwards compatibility. The intention + is that Method 2 (qform_code > 0) represents the nominal voxel locations + as reported by the scanner, or as rotated to some fiducial orientation and + location. Method 3, if present (sform_code > 0), is to be used to give + the location of the voxels in some standard space. The sform_code + indicates which standard space is present. Both methods 2 and 3 can be + present, and be useful in different contexts (method 2 for displaying the + data on its original grid; method 3 for displaying it on a standard grid). + + In this scheme, a dataset would originally be set up so that the + Method 2 coordinates represent what the scanner reported. Later, + a registration to some standard space can be computed and inserted + in the header. Image display software can use either transform, + depending on its purposes and needs. + + In Method 2, the origin of coordinates would generally be whatever + the scanner origin is; for example, in MRI, (0,0,0) is the center + of the gradient coil. + + In Method 3, the origin of coordinates would depend on the value + of sform_code; for example, for the Talairach coordinate system, + (0,0,0) corresponds to the Anterior Commissure. + + QUATERNION REPRESENTATION OF ROTATION MATRIX (METHOD 2) + ------------------------------------------------------- + The orientation of the (x,y,z) axes relative to the (i,j,k) axes + in 3D space is specified using a unit quaternion [a,b,c,d], where + a*a+b*b+c*c+d*d=1. The (b,c,d) values are all that is needed, since + we require that a = sqrt(1.0-(b*b+c*c+d*d)) be nonnegative. The (b,c,d) + values are stored in the (quatern_b,quatern_c,quatern_d) fields. + + The quaternion representation is chosen for its compactness in + representing rotations. The (proper) 3x3 rotation matrix that + corresponds to [a,b,c,d] is + + [ a*a+b*b-c*c-d*d 2*b*c-2*a*d 2*b*d+2*a*c ] + R = [ 2*b*c+2*a*d a*a+c*c-b*b-d*d 2*c*d-2*a*b ] + [ 2*b*d-2*a*c 2*c*d+2*a*b a*a+d*d-c*c-b*b ] + + [ R11 R12 R13 ] + = [ R21 R22 R23 ] + [ R31 R32 R33 ] + + If (p,q,r) is a unit 3-vector, then rotation of angle h about that + direction is represented by the quaternion + + [a,b,c,d] = [cos(h/2), p*sin(h/2), q*sin(h/2), r*sin(h/2)]. + + Requiring a >= 0 is equivalent to requiring -Pi <= h <= Pi. (Note that + [-a,-b,-c,-d] represents the same rotation as [a,b,c,d]; there are 2 + quaternions that can be used to represent a given rotation matrix R.) + To rotate a 3-vector (x,y,z) using quaternions, we compute the + quaternion product + + [0,x',y',z'] = [a,b,c,d] * [0,x,y,z] * [a,-b,-c,-d] + + which is equivalent to the matrix-vector multiply + + [ x' ] [ x ] + [ y' ] = R [ y ] (equivalence depends on a*a+b*b+c*c+d*d=1) + [ z' ] [ z ] + + Multiplication of 2 quaternions is defined by the following: + + [a,b,c,d] = a*1 + b*I + c*J + d*K + where + I*I = J*J = K*K = -1 (I,J,K are square roots of -1) + I*J = K J*K = I K*I = J + J*I = -K K*J = -I I*K = -J (not commutative!) + For example + [a,b,0,0] * [0,0,0,1] = [0,0,-b,a] + since this expands to + (a+b*I)*(K) = (a*K+b*I*K) = (a*K-b*J). + + The above formula shows how to go from quaternion (b,c,d) to + rotation matrix and direction cosines. Conversely, given R, + we can compute the fields for the NIFTI-1 header by + + a = 0.5 * sqrt(1+R11+R22+R33) (not stored) + b = 0.25 * (R32-R23) / a => quatern_b + c = 0.25 * (R13-R31) / a => quatern_c + d = 0.25 * (R21-R12) / a => quatern_d + + If a=0 (a 180 degree rotation), alternative formulas are needed. + See the nifti1_io.c function mat44_to_quatern() for an implementation + of the various cases in converting R to [a,b,c,d]. + + Note that R-transpose (= R-inverse) would lead to the quaternion + [a,-b,-c,-d]. + + The choice to specify the qoffset_x (etc.) values in the final + coordinate system is partly to make it easy to convert DICOM images to + this format. The DICOM attribute "Image Position (Patient)" (0020,0032) + stores the (Xd,Yd,Zd) coordinates of the center of the first voxel. + Here, (Xd,Yd,Zd) refer to DICOM coordinates, and Xd=-x, Yd=-y, Zd=z, + where (x,y,z) refers to the NIFTI coordinate system discussed above. + (i.e., DICOM +Xd is Left, +Yd is Posterior, +Zd is Superior, + whereas +x is Right, +y is Anterior , +z is Superior. ) + Thus, if the (0020,0032) DICOM attribute is extracted into (px,py,pz), then + qoffset_x = -px qoffset_y = -py qoffset_z = pz + is a reasonable setting when qform_code=NIFTI_XFORM_SCANNER_ANAT. + + That is, DICOM's coordinate system is 180 degrees rotated about the z-axis + from the neuroscience/NIFTI coordinate system. To transform between DICOM + and NIFTI, you just have to negate the x- and y-coordinates. + + The DICOM attribute (0020,0037) "Image Orientation (Patient)" gives the + orientation of the x- and y-axes of the image data in terms of 2 3-vectors. + The first vector is a unit vector along the x-axis, and the second is + along the y-axis. If the (0020,0037) attribute is extracted into the + value (xa,xb,xc,ya,yb,yc), then the first two columns of the R matrix + would be + [ -xa -ya ] + [ -xb -yb ] + [ xc yc ] + The negations are because DICOM's x- and y-axes are reversed relative + to NIFTI's. The third column of the R matrix gives the direction of + displacement (relative to the subject) along the slice-wise direction. + This orientation is not encoded in the DICOM standard in a simple way; + DICOM is mostly concerned with 2D images. The third column of R will be + either the cross-product of the first 2 columns or its negative. It is + possible to infer the sign of the 3rd column by examining the coordinates + in DICOM attribute (0020,0032) "Image Position (Patient)" for successive + slices. However, this method occasionally fails for reasons that I + (RW Cox) do not understand. + -----------------------------------------------------------------------------*/ + + /* [qs]form_code value: */ /* x,y,z coordinate system refers to: */ + /*-----------------------*/ /*---------------------------------------*/ + + /*! \defgroup NIFTI1_XFORM_CODES + \brief nifti1 xform codes to describe the "standard" coordinate system + @{ + */ + /*! Arbitrary coordinates (Method 1). */ + +#define NIFTI_XFORM_UNKNOWN 0 + + /*! Scanner-based anatomical coordinates */ + +#define NIFTI_XFORM_SCANNER_ANAT 1 + + /*! Coordinates aligned to another file's, + or to anatomical "truth". */ + +#define NIFTI_XFORM_ALIGNED_ANAT 2 + + /*! Coordinates aligned to Talairach- + Tournoux Atlas; (0,0,0)=AC, etc. */ + +#define NIFTI_XFORM_TALAIRACH 3 + + /*! MNI 152 normalized coordinates. */ + +#define NIFTI_XFORM_MNI_152 4 + /* @} */ + + /*---------------------------------------------------------------------------*/ + /* UNITS OF SPATIAL AND TEMPORAL DIMENSIONS: + ---------------------------------------- + The codes below can be used in xyzt_units to indicate the units of pixdim. + As noted earlier, dimensions 1,2,3 are for x,y,z; dimension 4 is for + time (t). + - If dim[4]=1 or dim[0] < 4, there is no time axis. + - A single time series (no space) would be specified with + - dim[0] = 4 (for scalar data) or dim[0] = 5 (for vector data) + - dim[1] = dim[2] = dim[3] = 1 + - dim[4] = number of time points + - pixdim[4] = time step + - xyzt_units indicates units of pixdim[4] + - dim[5] = number of values stored at each time point + + Bits 0..2 of xyzt_units specify the units of pixdim[1..3] + (e.g., spatial units are values 1..7). + Bits 3..5 of xyzt_units specify the units of pixdim[4] + (e.g., temporal units are multiples of 8). + + This compression of 2 distinct concepts into 1 byte is due to the + limited space available in the 348 byte ANALYZE 7.5 header. The + macros XYZT_TO_SPACE and XYZT_TO_TIME can be used to mask off the + undesired bits from the xyzt_units fields, leaving "pure" space + and time codes. Inversely, the macro SPACE_TIME_TO_XYZT can be + used to assemble a space code (0,1,2,...,7) with a time code + (0,8,16,32,...,56) into the combined value for xyzt_units. + + Note that codes are provided to indicate the "time" axis units are + actually frequency in Hertz (_HZ), in part-per-million (_PPM) + or in radians-per-second (_RADS). + + The toffset field can be used to indicate a nonzero start point for + the time axis. That is, time point #m is at t=toffset+m*pixdim[4] + for m=0..dim[4]-1. + -----------------------------------------------------------------------------*/ + + /*! \defgroup NIFTI1_UNITS + \brief nifti1 units codes to describe the unit of measurement for + each dimension of the dataset + @{ + */ + /*! NIFTI code for unspecified units. */ +#define NIFTI_UNITS_UNKNOWN 0 + + /** Space codes are multiples of 1. **/ + /*! NIFTI code for meters. */ +#define NIFTI_UNITS_METER 1 + /*! NIFTI code for millimeters. */ +#define NIFTI_UNITS_MM 2 + /*! NIFTI code for micrometers. */ +#define NIFTI_UNITS_MICRON 3 + + /** Time codes are multiples of 8. **/ + /*! NIFTI code for seconds. */ +#define NIFTI_UNITS_SEC 8 + /*! NIFTI code for milliseconds. */ +#define NIFTI_UNITS_MSEC 16 + /*! NIFTI code for microseconds. */ +#define NIFTI_UNITS_USEC 24 + + /*** These units are for spectral data: ***/ + /*! NIFTI code for Hertz. */ +#define NIFTI_UNITS_HZ 32 + /*! NIFTI code for ppm. */ +#define NIFTI_UNITS_PPM 40 + /*! NIFTI code for radians per second. */ +#define NIFTI_UNITS_RADS 48 + /* @} */ + +#undef XYZT_TO_SPACE +#undef XYZT_TO_TIME +#define XYZT_TO_SPACE(xyzt) ( (xyzt) & 0x07 ) +#define XYZT_TO_TIME(xyzt) ( (xyzt) & 0x38 ) + +#undef SPACE_TIME_TO_XYZT +#define SPACE_TIME_TO_XYZT(ss,tt) ( (((char)(ss)) & 0x07) \ + | (((char)(tt)) & 0x38) ) + + /*---------------------------------------------------------------------------*/ + /* MRI-SPECIFIC SPATIAL AND TEMPORAL INFORMATION: + --------------------------------------------- + A few fields are provided to store some extra information + that is sometimes important when storing the image data + from an FMRI time series experiment. (After processing such + data into statistical images, these fields are not likely + to be useful.) + + { freq_dim } = These fields encode which spatial dimension (1,2, or 3) + { phase_dim } = corresponds to which acquisition dimension for MRI data. + { slice_dim } = + Examples: + Rectangular scan multi-slice EPI: + freq_dim = 1 phase_dim = 2 slice_dim = 3 (or some permutation) + Spiral scan multi-slice EPI: + freq_dim = phase_dim = 0 slice_dim = 3 + since the concepts of frequency- and phase-encoding directions + don't apply to spiral scan + + slice_duration = If this is positive, AND if slice_dim is nonzero, + indicates the amount of time used to acquire 1 slice. + slice_duration*dim[slice_dim] can be less than pixdim[4] + with a clustered acquisition method, for example. + + slice_code = If this is nonzero, AND if slice_dim is nonzero, AND + if slice_duration is positive, indicates the timing + pattern of the slice acquisition. The following codes + are defined: + NIFTI_SLICE_SEQ_INC == sequential increasing + NIFTI_SLICE_SEQ_DEC == sequential decreasing + NIFTI_SLICE_ALT_INC == alternating increasing + NIFTI_SLICE_ALT_DEC == alternating decreasing + NIFTI_SLICE_ALT_INC2 == alternating increasing #2 + NIFTI_SLICE_ALT_DEC2 == alternating decreasing #2 + { slice_start } = Indicates the start and end of the slice acquisition + { slice_end } = pattern, when slice_code is nonzero. These values + are present to allow for the possible addition of + "padded" slices at either end of the volume, which + don't fit into the slice timing pattern. If there + are no padding slices, then slice_start=0 and + slice_end=dim[slice_dim]-1 are the correct values. + For these values to be meaningful, slice_start must + be non-negative and slice_end must be greater than + slice_start. Otherwise, they should be ignored. + + The following table indicates the slice timing pattern, relative to + time=0 for the first slice acquired, for some sample cases. Here, + dim[slice_dim]=7 (there are 7 slices, labeled 0..6), slice_duration=0.1, + and slice_start=1, slice_end=5 (1 padded slice on each end). + + slice + index SEQ_INC SEQ_DEC ALT_INC ALT_DEC ALT_INC2 ALT_DEC2 + 6 : n/a n/a n/a n/a n/a n/a n/a = not applicable + 5 : 0.4 0.0 0.2 0.0 0.4 0.2 (slice time offset + 4 : 0.3 0.1 0.4 0.3 0.1 0.0 doesn't apply to + 3 : 0.2 0.2 0.1 0.1 0.3 0.3 slices outside + 2 : 0.1 0.3 0.3 0.4 0.0 0.1 the range + 1 : 0.0 0.4 0.0 0.2 0.2 0.4 slice_start .. + 0 : n/a n/a n/a n/a n/a n/a slice_end) + + The SEQ slice_codes are sequential ordering (uncommon but not unknown), + either increasing in slice number or decreasing (INC or DEC), as + illustrated above. + + The ALT slice codes are alternating ordering. The 'standard' way for + these to operate (without the '2' on the end) is for the slice timing + to start at the edge of the slice_start .. slice_end group (at slice_start + for INC and at slice_end for DEC). For the 'ALT_*2' slice_codes, the + slice timing instead starts at the first slice in from the edge (at + slice_start+1 for INC2 and at slice_end-1 for DEC2). This latter + acquisition scheme is found on some Siemens scanners. + + The fields freq_dim, phase_dim, slice_dim are all squished into the single + byte field dim_info (2 bits each, since the values for each field are + limited to the range 0..3). This unpleasantness is due to lack of space + in the 348 byte allowance. + + The macros DIM_INFO_TO_FREQ_DIM, DIM_INFO_TO_PHASE_DIM, and + DIM_INFO_TO_SLICE_DIM can be used to extract these values from the + dim_info byte. + + The macro FPS_INTO_DIM_INFO can be used to put these 3 values + into the dim_info byte. + -----------------------------------------------------------------------------*/ + +#undef DIM_INFO_TO_FREQ_DIM +#undef DIM_INFO_TO_PHASE_DIM +#undef DIM_INFO_TO_SLICE_DIM + +#define DIM_INFO_TO_FREQ_DIM(di) ( ((di) ) & 0x03 ) +#define DIM_INFO_TO_PHASE_DIM(di) ( ((di) >> 2) & 0x03 ) +#define DIM_INFO_TO_SLICE_DIM(di) ( ((di) >> 4) & 0x03 ) + +#undef FPS_INTO_DIM_INFO +#define FPS_INTO_DIM_INFO(fd,pd,sd) ( ( ( ((char)(fd)) & 0x03) ) | \ + ( ( ((char)(pd)) & 0x03) << 2 ) | \ + ( ( ((char)(sd)) & 0x03) << 4 ) ) + + /*! \defgroup NIFTI1_SLICE_ORDER + \brief nifti1 slice order codes, describing the acquisition order + of the slices + @{ + */ +#define NIFTI_SLICE_UNKNOWN 0 +#define NIFTI_SLICE_SEQ_INC 1 +#define NIFTI_SLICE_SEQ_DEC 2 +#define NIFTI_SLICE_ALT_INC 3 +#define NIFTI_SLICE_ALT_DEC 4 +#define NIFTI_SLICE_ALT_INC2 5 /* 05 May 2005: RWCox */ +#define NIFTI_SLICE_ALT_DEC2 6 /* 05 May 2005: RWCox */ + /* @} */ + + /*---------------------------------------------------------------------------*/ + /* UNUSED FIELDS: + ------------- + Some of the ANALYZE 7.5 fields marked as ++UNUSED++ may need to be set + to particular values for compatibility with other programs. The issue + of interoperability of ANALYZE 7.5 files is a murky one -- not all + programs require exactly the same set of fields. (Unobscuring this + murkiness is a principal motivation behind NIFTI-1.) + + Some of the fields that may need to be set for other (non-NIFTI aware) + software to be happy are: + + extents dbh.h says this should be 16384 + regular dbh.h says this should be the character 'r' + glmin, } dbh.h says these values should be the min and max voxel + glmax } values for the entire dataset + + It is best to initialize ALL fields in the NIFTI-1 header to 0 + (e.g., with calloc()), then fill in what is needed. + -----------------------------------------------------------------------------*/ + + /*---------------------------------------------------------------------------*/ + /* MISCELLANEOUS C MACROS + -----------------------------------------------------------------------------*/ + + /*.................*/ + /*! Given a nifti_1_header struct, check if it has a good magic number. + Returns NIFTI version number (1..9) if magic is good, 0 if it is not. */ + +#define NIFTI_VERSION(h) \ + ( ( (h).magic[0]=='n' && (h).magic[3]=='\0' && \ + ( (h).magic[1]=='i' || (h).magic[1]=='+' ) && \ + ( (h).magic[2]>='1' && (h).magic[2]<='9' ) ) \ + ? (h).magic[2]-'0' : 0 ) + + /*.................*/ + /*! Check if a nifti_1_header struct says if the data is stored in the + same file or in a separate file. Returns 1 if the data is in the same + file as the header, 0 if it is not. */ + +#define NIFTI_ONEFILE(h) ( (h).magic[1] == '+' ) + + /*.................*/ + /*! Check if a nifti_1_header struct needs to be byte swapped. + Returns 1 if it needs to be swapped, 0 if it does not. */ + +#define NIFTI_NEEDS_SWAP(h) ( (h).dim[0] < 0 || (h).dim[0] > 7 ) + + /*.................*/ + /*! Check if a nifti_1_header struct contains a 5th (vector) dimension. + Returns size of 5th dimension if > 1, returns 0 otherwise. */ + +#define NIFTI_5TH_DIM(h) ( ((h).dim[0]>4 && (h).dim[5]>1) ? (h).dim[5] : 0 ) + + /*****************************************************************************/ + + /*=================*/ +#ifdef __cplusplus +} +#endif +/*=================*/ + +#endif /* _NIFTI_HEADER_ */ diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/nifti/nifti1_io.c b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/nifti/nifti1_io.c new file mode 100755 index 00000000..bea49cc6 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/nifti/nifti1_io.c @@ -0,0 +1,7512 @@ +#define _NIFTI1_IO_C_ + +#include "nifti1_io.h" /* typedefs, prototypes, macros, etc. */ + +/*****===================================================================*****/ +/***** Sample functions to deal with NIFTI-1 and ANALYZE files *****/ +/*****...................................................................*****/ +/***** This code is released to the public domain. *****/ +/*****...................................................................*****/ +/***** Author: Robert W Cox, SSCC/DIRP/NIMH/NIH/DHHS/USA/EARTH *****/ +/***** Date: August 2003 *****/ +/*****...................................................................*****/ +/***** Neither the National Institutes of Health (NIH), nor any of its *****/ +/***** employees imply any warranty of usefulness of this software for *****/ +/***** any purpose, and do not assume any liability for damages, *****/ +/***** incidental or otherwise, caused by any use of this document. *****/ +/*****===================================================================*****/ + +/** \file nifti1_io.c + \brief main collection of nifti1 i/o routines + - written by Bob Cox, SSCC NIMH + - revised by Mark Jenkinson, FMRIB + - revised by Rick Reynolds, SSCC, NIMH + - revised by Kate Fissell, University of Pittsburgh + + The library history can be viewed via "nifti_tool -nifti_hist". +
The library version can be viewed via "nifti_tool -nifti_ver". + */ + +/*! global history and version strings, for printing */ +static char * gni_history[] = +{ + "----------------------------------------------------------------------\n" + "history (of nifti library changes):\n" + "\n", + "0.0 August, 2003 [rwcox]\n" + " (Robert W Cox of the National Institutes of Health, SSCC/DIRP/NIMH)\n" + " - initial version\n" + "\n", + "0.1 July/August, 2004 [Mark Jenkinson]\n" + " (FMRIB Centre, University of Oxford, UK)\n" + " - Mainly adding low-level IO and changing things to allow gzipped\n" + " files to be read and written\n" + " - Full backwards compatability should have been maintained\n" + "\n", + "0.2 16 Nov 2004 [rickr]\n" + " (Rick Reynolds of the National Institutes of Health, SSCC/DIRP/NIMH)\n" + " - included Mark's changes in the AFNI distribution (including znzlib/)\n" + " (HAVE_ZLIB is commented out for the standard distribution)\n" + " - modified nifti_validfilename() and nifti_makebasename()\n" + " - added nifti_find_file_extension()\n" + "\n", + "0.3 3 Dec 2004 [rickr]\n" + " - note: header extensions are not yet checked for\n" + " - added formatted history as global string, for printing\n" + " - added nifti_disp_lib_hist(), to display the nifti library history\n" + " - added nifti_disp_lib_version(), to display the nifti library history\n", + " - re-wrote nifti_findhdrname()\n" + " o used nifti_find_file_extension()\n" + " o changed order of file tests (default is .nii, depends on input)\n" + " o free hdrname on failure\n" + " - made similar changes to nifti_findimgname()\n" + " - check for NULL return from nifti_findhdrname() calls\n", + " - removed most of ERREX() macros\n" + " - modified nifti_image_read()\n" + " o added debug info and error checking (on gni_debug > 0, only)\n" + " o fail if workingname is NULL\n" + " o check for failure to open header file\n" + " o free workingname on failure\n" + " o check for failure of nifti_image_load()\n" + " o check for failure of nifti_convert_nhdr2nim()\n", + " - changed nifti_image_load() to int, and check nifti_read_buffer return\n" + " - changed nifti_read_buffer() to fail on short read, and to count float\n" + " fixes (to print on debug)\n" + " - changed nifti_image_infodump to print to stderr\n" + " - updated function header comments, or moved comments above header\n" + " - removed const keyword\n" + " - added LNI_FERR() macro for error reporting on input files\n" + "\n", + "0.4 10 Dec 2004 [rickr] - added header extensions\n" + " - in nifti1_io.h:\n" + " o added num_ext and ext_list to the definition of nifti_image\n" + " o made many functions static (more to follow)\n" + " o added LNI_MAX_NIA_EXT_LEN, for max nifti_type 3 extension length\n", + " - added __DATE__ to version output in nifti_disp_lib_version()\n" + " - added nifti_disp_matrix_orient() to print orientation information\n" + " - added '.nia' as a valid file extension in nifti_find_file_extension()\n" + " - added much more debug output\n" + " - in nifti_image_read(), in the case of an ASCII header, check for\n" + " extensions after the end of the header\n", + " - added nifti_read_extensions() function\n" + " - added nifti_read_next_extension() function\n" + " - added nifti_add_exten_to_list() function\n" + " - added nifti_check_extension() function\n" + " - added nifti_write_extensions() function\n" + " - added nifti_extension_size() function\n" + " - in nifti_set_iname_offest():\n" + " o adjust offset by the extension size and the extender size\n", + " o fixed the 'ceiling modulo 16' computation\n" + " - in nifti_image_write_hdr_img2(): \n" + " o added extension writing\n" + " o check for NULL return from nifti_findimgname()\n" + " - include number of extensions in nifti_image_to_ascii() output\n" + " - in nifti_image_from_ascii():\n" + " o return bytes_read as a parameter, computed from the final spos\n" + " o extract num_ext from ASCII header\n" + "\n", + "0.5 14 Dec 2004 [rickr] - added sub-brick reading functions\n" + " - added nifti_brick_list type to nifti1_io.h, along with new prototypes\n" + " - added main nifti_image_read_bricks() function, with description\n" + " - added nifti_image_load_bricks() - library function (requires nim)\n" + " - added valid_nifti_brick_list() - library function\n" + " - added free_NBL() - library function\n", + " - added update_nifti_image_for_brick_list() for dimension update\n" + " - added nifti_load_NBL_bricks(), nifti_alloc_NBL_mem(),\n" + " nifti_copynsort() and force_positive() (static functions)\n" + " - in nifti_image_read(), check for failed load only if read_data is set\n" + " - broke most of nifti_image_load() into nifti_image_load_prep()\n" + "\n", + "0.6 15 Dec 2004 [rickr] - added sub-brick writing functionality\n" + " - in nifti1_io.h, removed znzlib directory from include - all nifti\n" + " library files are now under the nifti directory\n" + " - nifti_read_extensions(): print no offset warning for nifti_type 3\n" + " - nifti_write_all_data():\n" + " o pass nifti_brick_list * NBL, for optional writing\n" + " o if NBL, write each sub-brick, sequentially\n", + " - nifti_set_iname_offset(): case 1 must have sizeof() cast to int\n" + " - pass NBL to nifti_image_write_hdr_img2(), and allow NBL or data\n" + " - added nifti_image_write_bricks() wrapper for ...write_hdr_img2()\n" + " - included compression abilities\n" + "\n", + "0.7 16 Dec 2004 [rickr] - minor changes to extension reading\n" + "\n", + "0.8 21 Dec 2004 [rickr] - restrict extension reading, and minor changes\n" + " - in nifti_image_read(), compute bytes for extensions (see remaining)\n" + " - in nifti_read_extensions(), pass 'remain' as space for extensions,\n" + " pass it to nifti_read_next_ext(), and update for each one read \n" + " - in nifti_check_extension(), require (size <= remain)\n", + " - in update_nifti_image_brick_list(), update nvox\n" + " - in nifti_image_load_bricks(), make explicit check for nbricks <= 0\n" + " - in int_force_positive(), check for (!list)\n" + " - in swap_nifti_header(), swap sizeof_hdr, and reorder to struct order\n" + " - change get_filesize functions to signed ( < 0 is no file or error )\n", + " - in nifti_validfilename(), lose redundant (len < 0) check\n" + " - make print_hex_vals() static\n" + " - in disp_nifti_1_header, restrict string field widths\n" + "\n", + "0.9 23 Dec 2004 [rickr] - minor changes\n" + " - broke ASCII header reading out of nifti_image_read(), into new\n" + " functions has_ascii_header() and read_ascii_image()\n", + " - check image_read failure and znzseek failure\n" + " - altered some debug output\n" + " - nifti_write_all_data() now returns an int\n" + "\n", + "0.10 29 Dec 2004 [rickr]\n" + " - renamed nifti_valid_extension() to nifti_check_extension()\n" + " - added functions nifti_makehdrname() and nifti_makeimgname()\n" + " - added function valid_nifti_extensions()\n" + " - in nifti_write_extensions(), check for validity before writing\n", + " - rewrote nifti_image_write_hdr_img2():\n" + " o set write_data and leave_open flags from write_opts\n" + " o add debug print statements\n" + " o use nifti_write_ascii_image() for the ascii case\n" + " o rewrote the logic of all cases to be easier to follow\n", + " - broke out code as nifti_write_ascii_image() function\n" + " - added debug to top-level write functions, and free the znzFile\n" + " - removed unused internal function nifti_image_open()\n" + "\n", + "0.11 30 Dec 2004 [rickr] - small mods\n" + " - moved static function prototypes from header to C file\n" + " - free extensions in nifti_image_free()\n" + "\n", + "1.0 07 Jan 2005 [rickr] - INITIAL RELEASE VERSION\n" + " - added function nifti_set_filenames()\n" + " - added function nifti_read_header()\n" + " - added static function nhdr_looks_good()\n" + " - added static function need_nhdr_swap()\n" + " - exported nifti_add_exten_to_list symbol\n", + " - fixed #bytes written in nifti_write_extensions()\n" + " - only modify offset if it is too small (nifti_set_iname_offset)\n" + " - added nifti_type 3 to nifti_makehdrname and nifti_makeimgname\n" + " - added function nifti_set_filenames()\n" + "\n", + "1.1 07 Jan 2005 [rickr]\n" + " - in nifti_read_header(), swap if needed\n" + "\n", + "1.2 07 Feb 2005 [kate fissell c/o rickr] \n" + " - nifti1.h: added doxygen comments for main struct and #define groups\n" + " - nifti1_io.h: added doxygen comments for file and nifti_image struct\n" + " - nifti1_io.h: added doxygen comments for file and some functions\n" + " - nifti1_io.c: changed nifti_copy_nim_info to use memcpy\n" + "\n", + "1.3 09 Feb 2005 [rickr]\n" + " - nifti1.h: added doxygen comments for extension structs\n" + " - nifti1_io.h: put most #defines in #ifdef _NIFTI1_IO_C_ block\n" + " - added a doxygen-style description to every exported function\n" + " - added doxygen-style comments within some functions\n" + " - re-exported many znzFile functions that I had made static\n" + " - re-added nifti_image_open (sorry, Mark)\n" + " - every exported function now has 'nifti' in the name (19 functions)\n", + " - made sure every alloc() has a failure test\n" + " - added nifti_copy_extensions function, for use in nifti_copy_nim_info\n" + " - nifti_is_gzfile: added initial strlen test\n" + " - nifti_set_filenames: added set_byte_order parameter option\n" + " (it seems appropriate to set the BO when new files are associated)\n" + " - disp_nifti_1_header: prints to stdout (a.o.t. stderr), with fflush\n" + "\n", + "1.4 23 Feb 2005 [rickr] - sourceforge merge\n" + " - merged into the nifti_io CVS directory structure at sourceforge.net\n" + " - merged in 4 changes by Mark, and re-added his const keywords\n" + " - cast some pointers to (void *) for -pedantic compile option\n" + " - added nifti_free_extensions()\n" + "\n", + "1.5 02 Mar 2005 [rickr] - started nifti global options\n" + " - gni_debug is now g_opts.debug\n" + " - added validity check parameter to nifti_read_header\n" + " - need_nhdr_swap no longer does test swaps on the stack\n" + "\n", + "1.6 05 April 2005 [rickr] - validation and collapsed_image_read\n" + " - added nifti_read_collapsed_image(), an interface for reading partial\n" + " datasets, specifying a subset of array indices\n" + " - for read_collapsed_image, added static functions: rci_read_data(),\n" + " rci_alloc_mem(), and make_pivot_list()\n", + " - added nifti_nim_is_valid() to check for consistency (more to do)\n" + " - added nifti_nim_has_valid_dims() to do many dimensions tests\n" + "\n", + "1.7 08 April 2005 [rickr]\n" + " - added nifti_update_dims_from_array() - to update dimensions\n" + " - modified nifti_makehdrname() and nifti_makeimgname():\n" + " if prefix has a valid extension, use it (else make one up)\n" + " - added nifti_get_intlist - for making an array of ints\n" + " - fixed init of NBL->bsize in nifti_alloc_NBL_mem() {thanks, Bob}\n" + "\n", + "1.8 14 April 2005 [rickr]\n" + " - added nifti_set_type_from_names(), for nifti_set_filenames()\n" + " (only updates type if number of files does not match it)\n" + " - added is_valid_nifti_type(), just to be sure\n" + " - updated description of nifti_read_collapsed_image() for *data change\n" + " (if *data is already set, assume memory exists for results)\n" + " - modified rci_alloc_mem() to allocate only if *data is NULL\n" + "\n", + "1.9 19 April 2005 [rickr]\n" + " - added extension codes NIFTI_ECODE_COMMENT and NIFTI_ECODE_XCEDE\n" + " - added nifti_type codes NIFTI_MAX_ECODE and NIFTI_MAX_FTYPE\n" + " - added nifti_add_extension() {exported}\n" + " - added nifti_fill_extension() as a static function\n" + " - added nifti_is_valid_ecode() {exported}\n", + " - nifti_type values are now NIFTI_FTYPE_* file codes\n" + " - in nifti_read_extensions(), decrement 'remain' by extender size, 4\n" + " - in nifti_set_iname_offset(), case 1, update if offset differs\n" + " - only output '-d writing nifti file' if debug > 1\n" + "\n", + "1.10 10 May 2005 [rickr]\n" + " - files are read using ZLIB only if they end in '.gz'\n" + "\n", + "1.11 12 August 2005 [kate fissell]\n" + " - Kate's 0.2 release packaging, for sourceforge\n" + "\n", + "1.12 17 August 2005 [rickr] - comment (doxygen) updates\n" + " - updated comments for most functions (2 updates from Cinly Ooi)\n" + " - added nifti_type_and_names_match()\n" + "\n", + "1.12a 24 August 2005 [rickr] - remove all tabs from Clibs/*/*.[ch]\n", + "1.12b 25 August 2005 [rickr] - changes by Hans Johnson\n", + "1.13 25 August 2005 [rickr]\n", + " - finished changes by Hans for Insight\n" + " - added const in all appropraite parameter locations (30-40)\n" + " (any pointer referencing data that will not change)\n" + " - shortened all string constants below 509 character limit\n" + "1.14 28 October 2005 [HJohnson]\n", + " - use nifti_set_filenames() in nifti_convert_nhdr2nim()\n" + "1.15 02 November 2005 [rickr]\n", + " - added skip_blank_ext to nifti_global_options\n" + " - added nifti_set_skip_blank_ext(), to set option\n" + " - if skip_blank_ext and no extensions, do not read/write extender\n" + "1.16 18 November 2005 [rickr]\n", + " - removed any test or access of dim[i], i>dim[0]\n" + " - do not set pixdim for collapsed dims to 1.0, leave them as they are\n" + " - added magic and dim[i] tests in nifti_hdr_looks_good()\n" + " - added 2 size_t casts\n" + "1.17 22 November 2005 [rickr]\n", + " - in hdr->nim, for i > dim[0], pass 0 or 1, else set to 1\n" + "1.18 02 March 2006 [rickr]\n", + " - in nifti_alloc_NBL_mem(), fixed nt=0 case from 1.17 change\n" + "1.19 23 May 2006 [HJohnson,rickr]\n", + " - nifti_write_ascii_image(): free(hstr)\n" + " - nifti_copy_extensions(): clear num_ext and ext_list\n" + "1.20 27 Jun 2006 [rickr]\n", + " - nifti_findhdrname(): fixed assign of efirst to match stated logic\n" + " (problem found by Atle Bjørnerud)\n" + "1.21 05 Sep 2006 [rickr] update for nifticlib-0.4 release\n", + " - was reminded to actually add nifti_set_skip_blank_ext()\n" + " - init g_opts.skip_blank_ext to 0\n" + "1.22 01 Jun 2007 nifticlib-0.5 release\n", + "1.23 05 Jun 2007 nifti_add_exten_to_list: revert on failure, free old list\n" + "1.24 07 Jun 2007 nifti_copy_extensions: use esize-8 for data size\n" + "1.25 12 Jun 2007 [rickr] EMPTY_IMAGE creation\n", + " - added nifti_make_new_header() - to create from dims/dtype\n" + " - added nifti_make_new_nim() - to create from dims/dtype/fill\n" + " - added nifti_is_valid_datatype(), and more debug info\n", + "1.26 27 Jul 2007 [rickr] handle single volumes > 2^31 bytes (but < 2^32)\n", + "1.27 28 Jul 2007 [rickr] nim->nvox, NBL-bsize are now type size_t\n" + "1.28 30 Jul 2007 [rickr] size_t updates\n", + "1.29 08 Aug 2007 [rickr] for list, valid_nifti_brick_list requires 3 dims\n" + "1.30 08 Nov 2007 [Yaroslav/rickr]\n" + " - fix ARM struct alignment problem in byte-swapping routines\n", + "1.31 29 Nov 2007 [rickr] for nifticlib-1.0.0\n" + " - added nifti_datatype_to/from_string routines\n" + " - added DT_RGBA32/NIFTI_TYPE_RGBA32 datatype macros (2304)\n" + " - added NIFTI_ECODE_FREESURFER (14)\n", + "1.32 08 Dec 2007 [rickr]\n" + " - nifti_hdr_looks_good() allows ANALYZE headers (req. by V. Luccio)\n" + " - added nifti_datatype_is_valid()\n", + "1.33 05 Feb 2008 [hansj,rickr] - block nia.gz use\n" + "1.34 13 Jun 2008 [rickr] - added nifti_compiled_with_zlib()\n" + "1.35 03 Aug 2008 [rickr]\n", + " - deal with swapping, so that CPU type does not affect output\n" + " (motivated by C Burns)\n" + " - added nifti_analyze75 structure and nifti_swap_as_analyze()\n" + " - previous swap_nifti_header is saved as old_swap_nifti_header\n" + " - also swap UNUSED fields in nifti_1_header struct\n", + "1.36 07 Oct 2008 [rickr]\n", + " - added nifti_NBL_matches_nim() check for write_bricks()\n" + "1.37 10 Mar 2009 [rickr]\n", + " - H Johnson cast updates (06 Feb)\n" + " - added NIFTI_ECODE_PYPICKLE for PyNIfTI (06 Feb)\n" + " - added NIFTI_ECODEs 18-28 for the LONI MiND group\n" + "1.38 28 Apr 2009 [rickr]\n", + " - uppercase extensions are now valid (requested by M. Coursolle)\n" + " - nifti_set_allow_upper_fext controls this option (req by C. Ooi)\n" + "1.39 23 Jun 2009 [rickr]: added 4 checks of alloc() returns\n", + "1.40 16 Mar 2010 [rickr]: added NIFTI_ECODE_VOXBO for D. Kimberg\n", + "1.41 28 Apr 2010 [rickr]: added NIFTI_ECODE_CARET for J. Harwell\n", + "1.42 06 Jul 2010 [rickr]: trouble with large (gz) files\n", + " - noted/investigated by M Hanke and Y Halchenko\n" + " - fixed znzread/write, noting example by M Adler\n" + " - changed nifti_swap_* routines/calls to take size_t (6)\n" + "1.43 07 Jul 2010 [rickr]: fixed znzR/W to again return nmembers\n", + "----------------------------------------------------------------------\n" +}; +static char gni_version[] = "nifti library version 1.43 (7 July, 2010)"; + +/*! global nifti options structure - init with defaults */ +static nifti_global_options g_opts = { + 1, /* debug level */ + 0, /* skip_blank_ext - skip extender if no extensions */ + 1 /* allow_upper_fext - allow uppercase file extensions */ +}; + +/*! global nifti types structure list (per type, ordered oldest to newest) */ +static nifti_type_ele nifti_type_list[] = { + /* type nbyper swapsize name */ + { 0, 0, 0, "DT_UNKNOWN" }, + { 0, 0, 0, "DT_NONE" }, + { 1, 0, 0, "DT_BINARY" }, /* not usable */ + { 2, 1, 0, "DT_UNSIGNED_CHAR" }, + { 2, 1, 0, "DT_UINT8" }, + { 2, 1, 0, "NIFTI_TYPE_UINT8" }, + { 4, 2, 2, "DT_SIGNED_SHORT" }, + { 4, 2, 2, "DT_INT16" }, + { 4, 2, 2, "NIFTI_TYPE_INT16" }, + { 8, 4, 4, "DT_SIGNED_INT" }, + { 8, 4, 4, "DT_INT32" }, + { 8, 4, 4, "NIFTI_TYPE_INT32" }, + { 16, 4, 4, "DT_FLOAT" }, + { 16, 4, 4, "DT_FLOAT32" }, + { 16, 4, 4, "NIFTI_TYPE_FLOAT32" }, + { 32, 8, 4, "DT_COMPLEX" }, + { 32, 8, 4, "DT_COMPLEX64" }, + { 32, 8, 4, "NIFTI_TYPE_COMPLEX64" }, + { 64, 8, 8, "DT_DOUBLE" }, + { 64, 8, 8, "DT_FLOAT64" }, + { 64, 8, 8, "NIFTI_TYPE_FLOAT64" }, + { 128, 3, 0, "DT_RGB" }, + { 128, 3, 0, "DT_RGB24" }, + { 128, 3, 0, "NIFTI_TYPE_RGB24" }, + { 255, 0, 0, "DT_ALL" }, + { 256, 1, 0, "DT_INT8" }, + { 256, 1, 0, "NIFTI_TYPE_INT8" }, + { 512, 2, 2, "DT_UINT16" }, + { 512, 2, 2, "NIFTI_TYPE_UINT16" }, + { 768, 4, 4, "DT_UINT32" }, + { 768, 4, 4, "NIFTI_TYPE_UINT32" }, + { 1024, 8, 8, "DT_INT64" }, + { 1024, 8, 8, "NIFTI_TYPE_INT64" }, + { 1280, 8, 8, "DT_UINT64" }, + { 1280, 8, 8, "NIFTI_TYPE_UINT64" }, + { 1536, 16, 16, "DT_FLOAT128" }, + { 1536, 16, 16, "NIFTI_TYPE_FLOAT128" }, + { 1792, 16, 8, "DT_COMPLEX128" }, + { 1792, 16, 8, "NIFTI_TYPE_COMPLEX128" }, + { 2048, 32, 16, "DT_COMPLEX256" }, + { 2048, 32, 16, "NIFTI_TYPE_COMPLEX256" }, + { 2304, 4, 0, "DT_RGBA32" }, + { 2304, 4, 0, "NIFTI_TYPE_RGBA32" }, +}; + +/*---------------------------------------------------------------------------*/ +/* prototypes for internal functions - not part of exported library */ + +/* extension routines */ +static int nifti_read_extensions( nifti_image *nim, znzFile fp, int remain ); +static int nifti_read_next_extension( nifti1_extension * nex, nifti_image *nim, int remain, znzFile fp ); +static int nifti_check_extension(nifti_image *nim, int size,int code, int rem); +static void update_nifti_image_for_brick_list(nifti_image * nim , int nbricks); +static int nifti_add_exten_to_list(nifti1_extension * new_ext, + nifti1_extension ** list, int new_length); +static int nifti_fill_extension(nifti1_extension * ext, const char * data, + int len, int ecode); + +/* NBL routines */ +static int nifti_load_NBL_bricks(nifti_image * nim , int * slist, int * sindex, nifti_brick_list * NBL, znzFile fp ); +static int nifti_alloc_NBL_mem( nifti_image * nim, int nbricks, + nifti_brick_list * nbl); +static int nifti_copynsort(int nbricks, const int *blist, int **slist, + int **sindex); +static int nifti_NBL_matches_nim(const nifti_image *nim, + const nifti_brick_list *NBL); + +/* for nifti_read_collapsed_image: */ +static int rci_read_data(nifti_image *nim, int *pivots, int *prods, int nprods, + const int dims[], char *data, znzFile fp, size_t base_offset); +static int rci_alloc_mem(void ** data, int prods[8], int nprods, int nbyper ); +static int make_pivot_list(nifti_image * nim, const int dims[], int pivots[], + int prods[], int * nprods ); + +/* misc */ +static int compare_strlist (const char * str, char ** strlist, int len); +static int fileext_compare (const char * test_ext, const char * known_ext); +static int fileext_n_compare (const char * test_ext, + const char * known_ext, int maxlen); +static int is_mixedcase (const char * str); +static int is_uppercase (const char * str); +static int make_lowercase (char * str); +static int make_uppercase (char * str); +static int need_nhdr_swap (short dim0, int hdrsize); +static int print_hex_vals (const char * data, int nbytes, FILE * fp); +static int unescape_string (char *str); /* string utility functions */ +static char *escapize_string (const char *str); + +/* internal I/O routines */ +static znzFile nifti_image_load_prep( nifti_image *nim ); +static int has_ascii_header(znzFile fp); +/*---------------------------------------------------------------------------*/ + + +/* for calling from some main program */ + +/*----------------------------------------------------------------------*/ +/*! display the nifti library module history (via stdout) +*//*--------------------------------------------------------------------*/ +void nifti_disp_lib_hist( void ) +{ + int c, len = sizeof(gni_history)/sizeof(char *); + for( c = 0; c < len; c++ ) + fputs(gni_history[c], stdout); +} + +/*----------------------------------------------------------------------*/ +/*! display the nifti library version (via stdout) +*//*--------------------------------------------------------------------*/ +void nifti_disp_lib_version( void ) +{ + printf("%s, compiled %s\n", gni_version, __DATE__); +} + + +/*----------------------------------------------------------------------*/ +/*! nifti_image_read_bricks - read nifti data as array of bricks + * + * 13 Dec 2004 [rickr] + * + * \param hname - filename of dataset to read (must be valid) + * \param nbricks - number of sub-bricks to read + * (if blist is valid, nbricks must be > 0) + * \param blist - list of sub-bricks to read + * (can be NULL; if NULL, read complete dataset) + * \param NBL - pointer to empty nifti_brick_list struct + * (must be a valid pointer) + * + * \return + *
nim - same as nifti_image_read, but + * nim->nt = NBL->nbricks (or nt*nu*nv*nw) + * nim->nu,nv,nw = 1 + * nim->data = NULL + *
NBL - filled with data volumes + * + * By default, this function will read the nifti dataset and break the data + * into a list of nt*nu*nv*nw sub-bricks, each having size nx*ny*nz elements. + * That is to say, instead of reading the entire dataset as a single array, + * break it up into sub-bricks (volumes), each of size nx*ny*nz elements. + * + * Note: in the returned nifti_image, nu, nv and nw will always be 1. The + * intention of this function is to collapse the dataset into a single + * array of volumes (of length nbricks or nt*nu*nv*nw). + * + * If 'blist' is valid, it is taken to be a list of sub-bricks, of length + * 'nbricks'. The data will still be separated into sub-bricks of size + * nx*ny*nz elements, but now 'nbricks' sub-bricks will be returned, of the + * caller's choosing via 'blist'. + * + * E.g. consider a dataset with 12 sub-bricks (numbered 0..11), and the + * following code: + * + *
+ * { nifti_brick_list   NB_orig, NB_select;
+ *   nifti_image      * nim_orig, * nim_select;
+ *   int                blist[5] = { 7, 0, 5, 5, 9 };
+ *
+ *   nim_orig   = nifti_image_read_bricks("myfile.nii", 0, NULL,  &NB_orig);
+ *   nim_select = nifti_image_read_bricks("myfile.nii", 5, blist, &NB_select);
+ * }
+ * 
+ * + * Here, nim_orig gets the entire dataset, where NB_orig.nbricks = 12. But + * nim_select has NB_select.nbricks = 5. + * + * Note that the first case is not quite the same as just calling the + * nifti_image_read function, as here the data is separated into sub-bricks. + * + * Note that valid blist elements are in [0..nt*nu*nv*nw-1], + * or written [ 0 .. (dim[4]*dim[5]*dim[6]*dim[7] - 1) ]. + * + * Note that, as is the case with all of the reading functions, the + * data will be allocated, read in, and properly byte-swapped, if + * necessary. + * + * \sa nifti_image_load_bricks, nifti_free_NBL, valid_nifti_brick_list, + nifti_image_read +*//*----------------------------------------------------------------------*/ +nifti_image *nifti_image_read_bricks(const char * hname, int nbricks, + const int * blist, nifti_brick_list * NBL) +{ + nifti_image * nim; + + if( !hname || !NBL ){ + fprintf(stderr,"** nifti_image_read_bricks: bad params (%p,%p)\n", + hname, (void *)NBL); + return NULL; + } + + if( blist && nbricks <= 0 ){ + fprintf(stderr,"** nifti_image_read_bricks: bad nbricks, %d\n", nbricks); + return NULL; + } + + nim = nifti_image_read(hname, 0); /* read header, but not data */ + + if( !nim ) return NULL; /* errors were already printed */ + + /* if we fail, free image and return */ + if( nifti_image_load_bricks(nim, nbricks, blist, NBL) <= 0 ){ + nifti_image_free(nim); + return NULL; + } + + if( blist ) update_nifti_image_for_brick_list(nim, nbricks); + + return nim; +} + + +/*---------------------------------------------------------------------- + * update_nifti_image_for_brick_list - update nifti_image + * + * When loading a specific brick list, the distinction between + * nt, nu, nv and nw is lost. So put everything in t, and set + * dim[0] = 4. + *----------------------------------------------------------------------*/ +static void update_nifti_image_for_brick_list( nifti_image * nim , int nbricks ) +{ + int ndim; + + if( g_opts.debug > 2 ){ + fprintf(stderr,"+d updating image dimensions for %d bricks in list\n", + nbricks); + fprintf(stderr," ndim = %d\n",nim->ndim); + fprintf(stderr," nx,ny,nz,nt,nu,nv,nw: (%d,%d,%d,%d,%d,%d,%d)\n", + nim->nx, nim->ny, nim->nz, nim->nt, nim->nu, nim->nv, nim->nw); + } + + nim->nt = nbricks; + nim->nu = nim->nv = nim->nw = 1; + nim->dim[4] = nbricks; + nim->dim[5] = nim->dim[6] = nim->dim[7] = 1; + + /* compute nvox */ + /* do not rely on dimensions above dim[0] 16 Nov 2005 [rickr] */ + for( nim->nvox = 1, ndim = 1; ndim <= nim->dim[0]; ndim++ ) + nim->nvox *= nim->dim[ndim]; + + /* update the dimensions to 4 or lower */ + for( ndim = 4; (ndim > 1) && (nim->dim[ndim] <= 1); ndim-- ) + ; + + if( g_opts.debug > 2 ){ + fprintf(stderr,"+d ndim = %d -> %d\n",nim->ndim, ndim); + fprintf(stderr," --> (%d,%d,%d,%d,%d,%d,%d)\n", + nim->nx, nim->ny, nim->nz, nim->nt, nim->nu, nim->nv, nim->nw); + } + + nim->dim[0] = nim->ndim = ndim; +} + + +/*----------------------------------------------------------------------*/ +/*! nifti_update_dims_from_array - update nx, ny, ... from nim->dim[] + + Fix all the dimension information, based on a new nim->dim[]. + + Note: we assume that dim[0] will not increase. + + Check for updates to pixdim[], dx,..., nx,..., nvox, ndim, dim[0]. +*//*--------------------------------------------------------------------*/ +int nifti_update_dims_from_array( nifti_image * nim ) +{ + int c, ndim; + + if( !nim ){ + fprintf(stderr,"** update_dims: missing nim\n"); + return 1; + } + + if( g_opts.debug > 2 ){ + fprintf(stderr,"+d updating image dimensions given nim->dim:"); + for( c = 0; c < 8; c++ ) fprintf(stderr," %d", nim->dim[c]); + fputc('\n',stderr); + } + + /* verify dim[0] first */ + if(nim->dim[0] < 1 || nim->dim[0] > 7){ + fprintf(stderr,"** invalid dim[0], dim[] = "); + for( c = 0; c < 8; c++ ) fprintf(stderr," %d", nim->dim[c]); + fputc('\n',stderr); + return 1; + } + + /* set nx, ny ..., dx, dy, ..., one by one */ + + /* less than 1, set to 1, else copy */ + if(nim->dim[1] < 1) nim->nx = nim->dim[1] = 1; + else nim->nx = nim->dim[1]; + nim->dx = nim->pixdim[1]; + + /* if undefined, or less than 1, set to 1 */ + if(nim->dim[0] < 2 || (nim->dim[0] >= 2 && nim->dim[2] < 1)) + nim->ny = nim->dim[2] = 1; + else + nim->ny = nim->dim[2]; + /* copy delta values, in any case */ + nim->dy = nim->pixdim[2]; + + if(nim->dim[0] < 3 || (nim->dim[0] >= 3 && nim->dim[3] < 1)) + nim->nz = nim->dim[3] = 1; + else /* just copy vals from arrays */ + nim->nz = nim->dim[3]; + nim->dz = nim->pixdim[3]; + + if(nim->dim[0] < 4 || (nim->dim[0] >= 4 && nim->dim[4] < 1)) + nim->nt = nim->dim[4] = 1; + else /* just copy vals from arrays */ + nim->nt = nim->dim[4]; + nim->dt = nim->pixdim[4]; + + if(nim->dim[0] < 5 || (nim->dim[0] >= 5 && nim->dim[5] < 1)) + nim->nu = nim->dim[5] = 1; + else /* just copy vals from arrays */ + nim->nu = nim->dim[5]; + nim->du = nim->pixdim[5]; + + if(nim->dim[0] < 6 || (nim->dim[0] >= 6 && nim->dim[6] < 1)) + nim->nv = nim->dim[6] = 1; + else /* just copy vals from arrays */ + nim->nv = nim->dim[6]; + nim->dv = nim->pixdim[6]; + + if(nim->dim[0] < 7 || (nim->dim[0] >= 7 && nim->dim[7] < 1)) + nim->nw = nim->dim[7] = 1; + else /* just copy vals from arrays */ + nim->nw = nim->dim[7]; + nim->dw = nim->pixdim[7]; + + for( c = 1, nim->nvox = 1; c <= nim->dim[0]; c++ ) + nim->nvox *= nim->dim[c]; + + /* compute ndim, assuming it can be no larger than the old one */ + for( ndim = nim->dim[0]; (ndim > 1) && (nim->dim[ndim] <= 1); ndim-- ) + ; + + if( g_opts.debug > 2 ){ + fprintf(stderr,"+d ndim = %d -> %d\n",nim->ndim, ndim); + fprintf(stderr," --> (%d,%d,%d,%d,%d,%d,%d)\n", + nim->nx, nim->ny, nim->nz, nim->nt, nim->nu, nim->nv, nim->nw); + } + + nim->dim[0] = nim->ndim = ndim; + + return 0; +} + + +/*----------------------------------------------------------------------*/ +/*! Load the image data from disk into an already-prepared image struct. + * + * \param nim - initialized nifti_image, without data + * \param nbricks - the length of blist (must be 0 if blist is NULL) + * \param blist - an array of xyz volume indices to read (can be NULL) + * \param NBL - pointer to struct where resulting data will be stored + * + * If blist is NULL, read all sub-bricks. + * + * \return the number of loaded bricks (NBL->nbricks), + * 0 on failure, < 0 on error + * + * NOTE: it is likely that another function will copy the data pointers + * out of NBL, in which case the only pointer the calling function + * will want to free is NBL->bricks (not each NBL->bricks[i]). +*//*--------------------------------------------------------------------*/ +int nifti_image_load_bricks( nifti_image * nim , int nbricks, + const int * blist, nifti_brick_list * NBL ) +{ + int * slist = NULL, * sindex = NULL, rv; + znzFile fp; + + /* we can have blist == NULL */ + if( !nim || !NBL ){ + fprintf(stderr,"** nifti_image_load_bricks, bad params (%p,%p)\n", + (void *)nim, (void *)NBL); + return -1; + } + + if( blist && nbricks <= 0 ){ + if( g_opts.debug > 1 ) + fprintf(stderr,"-d load_bricks: received blist with nbricks = %d," + "ignoring blist\n", nbricks); + blist = NULL; /* pretend nothing was passed */ + } + + if( blist && ! valid_nifti_brick_list(nim, nbricks, blist, g_opts.debug>0) ) + return -1; + + /* for efficiency, let's read the file in order */ + if( blist && nifti_copynsort( nbricks, blist, &slist, &sindex ) != 0 ) + return -1; + + /* open the file and position the FILE pointer */ + fp = nifti_image_load_prep( nim ); + if( !fp ){ + if( g_opts.debug > 0 ) + fprintf(stderr,"** nifti_image_load_bricks, failed load_prep\n"); + if( blist ){ free(slist); free(sindex); } + return -1; + } + + /* this will flag to allocate defaults */ + if( !blist ) nbricks = 0; + if( nifti_alloc_NBL_mem( nim, nbricks, NBL ) != 0 ){ + if( blist ){ free(slist); free(sindex); } + znzclose(fp); + return -1; + } + + rv = nifti_load_NBL_bricks(nim, slist, sindex, NBL, fp); + + if( rv != 0 ){ + nifti_free_NBL( NBL ); /* failure! */ + NBL->nbricks = 0; /* repetative, but clear */ + } + + if( slist ){ free(slist); free(sindex); } + + znzclose(fp); + + return NBL->nbricks; +} + + +/*----------------------------------------------------------------------*/ +/*! nifti_free_NBL - free all pointers and clear structure + * + * note: this does not presume to free the structure pointer +*//*--------------------------------------------------------------------*/ +void nifti_free_NBL( nifti_brick_list * NBL ) +{ + int c; + + if( NBL->bricks ){ + for( c = 0; c < NBL->nbricks; c++ ) + if( NBL->bricks[c] ) free(NBL->bricks[c]); + free(NBL->bricks); + NBL->bricks = NULL; + } + + NBL->bsize = NBL->nbricks = 0; +} + + +/*---------------------------------------------------------------------- + * nifti_load_NBL_bricks - read the file data into the NBL struct + * + * return 0 on success, -1 on failure + *----------------------------------------------------------------------*/ +static int nifti_load_NBL_bricks( nifti_image * nim , int * slist, int * sindex, + nifti_brick_list * NBL, znzFile fp ) +{ + size_t oposn, fposn; /* orig and current file positions */ + size_t rv; + long test; + int c; + int prev, isrc, idest; /* previous and current sub-brick, and new index */ + + test = znztell(fp); /* store current file position */ + if( test < 0 ){ + fprintf(stderr,"** load bricks: ztell failed??\n"); + return -1; + } + fposn = oposn = test; + + /* first, handle the default case, no passed blist */ + if( !slist ){ + for( c = 0; c < NBL->nbricks; c++ ) { + rv = nifti_read_buffer(fp, NBL->bricks[c], NBL->bsize, nim); + if( rv != NBL->bsize ){ + fprintf(stderr,"** load bricks: cannot read brick %d from '%s'\n", + c, nim->iname ? nim->iname : nim->fname); + return -1; + } + } + if( g_opts.debug > 1 ) + fprintf(stderr,"+d read %d default %u-byte bricks from file %s\n", + NBL->nbricks, (unsigned int)NBL->bsize, + nim->iname ? nim->iname:nim->fname ); + return 0; + } + + if( !sindex ){ + fprintf(stderr,"** load_NBL_bricks: missing index list\n"); + return -1; + } + + prev = -1; /* use prev for previous sub-brick */ + for( c = 0; c < NBL->nbricks; c++ ){ + isrc = slist[c]; /* this is original brick index (c is new one) */ + idest = sindex[c]; /* this is the destination index for this data */ + + /* if this sub-brick is not the previous, we must read from disk */ + if( isrc != prev ){ + + /* if we are not looking at the correct sub-brick, scan forward */ + if( fposn != (oposn + isrc*NBL->bsize) ){ + fposn = oposn + isrc*NBL->bsize; + if( znzseek(fp, (long)fposn, SEEK_SET) < 0 ){ + fprintf(stderr,"** failed to locate brick %d in file '%s'\n", + isrc, nim->iname ? nim->iname : nim->fname); + return -1; + } + } + + /* only 10,000 lines later and we're actually reading something! */ + rv = nifti_read_buffer(fp, NBL->bricks[idest], NBL->bsize, nim); + if( rv != NBL->bsize ){ + fprintf(stderr,"** failed to read brick %d from file '%s'\n", + isrc, nim->iname ? nim->iname : nim->fname); + if( g_opts.debug > 1 ) + fprintf(stderr," (read %u of %u bytes)\n", + (unsigned int)rv, (unsigned int)NBL->bsize); + return -1; + } + fposn += NBL->bsize; + } else { + /* we have already read this sub-brick, just copy the previous one */ + /* note that this works because they are sorted */ + memcpy(NBL->bricks[idest], NBL->bricks[sindex[c-1]], NBL->bsize); + } + + prev = isrc; /* in any case, note the now previous sub-brick */ + } + + return 0; +} + + +/*---------------------------------------------------------------------- + * nifti_alloc_NBL_mem - allocate memory for bricks + * + * return 0 on success, -1 on failure + *----------------------------------------------------------------------*/ +static int nifti_alloc_NBL_mem(nifti_image * nim, int nbricks, + nifti_brick_list * nbl) +{ + int c; + + /* if nbricks is not specified, use the default */ + if( nbricks > 0 ) nbl->nbricks = nbricks; + else { /* I missed this one with the 1.17 change 02 Mar 2006 [rickr] */ + nbl->nbricks = 1; + for( c = 4; c <= nim->ndim; c++ ) + nbl->nbricks *= nim->dim[c]; + } + + nbl->bsize = (size_t)nim->nx * nim->ny * nim->nz * nim->nbyper;/* bytes */ + nbl->bricks = (void **)malloc(nbl->nbricks * sizeof(void *)); + + if( ! nbl->bricks ){ + fprintf(stderr,"** NANM: failed to alloc %d void ptrs\n",nbricks); + return -1; + } + + for( c = 0; c < nbl->nbricks; c++ ){ + nbl->bricks[c] = (void *)malloc(nbl->bsize); + if( ! nbl->bricks[c] ){ + fprintf(stderr,"** NANM: failed to alloc %u bytes for brick %d\n", + (unsigned int)nbl->bsize, c); + /* so free and clear everything before returning */ + while( c > 0 ){ + c--; + free(nbl->bricks[c]); + } + free(nbl->bricks); + nbl->bricks = NULL; + nbl->bsize = nbl->nbricks = 0; + return -1; + } + } + + if( g_opts.debug > 2 ) + fprintf(stderr,"+d NANM: alloc'd %d bricks of %u bytes for NBL\n", + nbl->nbricks, (unsigned int)nbl->bsize); + + return 0; +} + + +/*---------------------------------------------------------------------- + * nifti_copynsort - copy int list, and sort with indices + * + * 1. duplicate the incoming list + * 2. create an sindex list, and init with 0..nbricks-1 + * 3. do a slow insertion sort on the small slist, along with sindex list + * 4. check results, just to be positive + * + * So slist is sorted, and sindex hold original positions. + * + * return 0 on success, -1 on failure + *----------------------------------------------------------------------*/ +static int nifti_copynsort(int nbricks, const int * blist, int ** slist, + int ** sindex) +{ + int * stmp, * itmp; /* for ease of typing/reading */ + int c1, c2, spos, tmp; + + *slist = (int *)malloc(nbricks * sizeof(int)); + *sindex = (int *)malloc(nbricks * sizeof(int)); + + if( !*slist || !*sindex ){ + fprintf(stderr,"** NCS: failed to alloc %d ints for sorting\n",nbricks); + if(*slist) free(*slist); /* maybe one succeeded */ + if(*sindex) free(*sindex); + return -1; + } + + /* init the lists */ + memcpy(*slist, blist, nbricks*sizeof(int)); + for( c1 = 0; c1 < nbricks; c1++ ) (*sindex)[c1] = c1; + + /* now actually sort slist */ + stmp = *slist; + itmp = *sindex; + for( c1 = 0; c1 < nbricks-1; c1++ ) { + /* find smallest value, init to current */ + spos = c1; + for( c2 = c1+1; c2 < nbricks; c2++ ) + if( stmp[c2] < stmp[spos] ) spos = c2; + if( spos != c1 ) /* swap: fine, don't maintain sub-order, see if I care */ + { + tmp = stmp[c1]; /* first swap the sorting values */ + stmp[c1] = stmp[spos]; + stmp[spos] = tmp; + + tmp = itmp[c1]; /* then swap the index values */ + itmp[c1] = itmp[spos]; + itmp[spos] = tmp; + } + } + + if( g_opts.debug > 2 ){ + fprintf(stderr, "+d sorted indexing list:\n"); + fprintf(stderr, " orig : "); + for( c1 = 0; c1 < nbricks; c1++ ) fprintf(stderr," %d",blist[c1]); + fprintf(stderr,"\n new : "); + for( c1 = 0; c1 < nbricks; c1++ ) fprintf(stderr," %d",stmp[c1]); + fprintf(stderr,"\n indices: "); + for( c1 = 0; c1 < nbricks; c1++ ) fprintf(stderr," %d",itmp[c1]); + fputc('\n', stderr); + } + + /* check the sort (why not? I've got time...) */ + for( c1 = 0; c1 < nbricks-1; c1++ ){ + if( (stmp[c1] > stmp[c1+1]) || (blist[itmp[c1]] != stmp[c1]) ){ + fprintf(stderr,"** sorting screw-up, way to go, rick!\n"); + free(stmp); free(itmp); *slist = NULL; *sindex = NULL; + return -1; + } + } + + if( g_opts.debug > 2 ) fprintf(stderr,"-d sorting is okay\n"); + + return 0; +} + + +/*----------------------------------------------------------------------*/ +/*! valid_nifti_brick_list - check sub-brick list for image + * + * This function verifies that nbricks and blist are appropriate + * for use with this nim, based on the dimensions. + * + * \param nim nifti_image to check against + * \param nbricks number of brick indices in blist + * \param blist list of brick indices to check in nim + * \param disp_error if this flag is set, report errors to user + * + * \return 1 if valid, 0 if not +*//*--------------------------------------------------------------------*/ +int valid_nifti_brick_list(nifti_image * nim , int nbricks, + const int * blist, int disp_error) +{ + int c, nsubs; + + if( !nim ){ + if( disp_error || g_opts.debug > 0 ) + fprintf(stderr,"** valid_nifti_brick_list: missing nifti image\n"); + return 0; + } + + if( nbricks <= 0 || !blist ){ + if( disp_error || g_opts.debug > 1 ) + fprintf(stderr,"** valid_nifti_brick_list: no brick list to check\n"); + return 0; + } + + if( nim->dim[0] < 3 ){ + if( disp_error || g_opts.debug > 1 ) + fprintf(stderr,"** cannot read explict brick list from %d-D dataset\n", + nim->dim[0]); + return 0; + } + + /* nsubs sub-brick is nt*nu*nv*nw */ + for( c = 4, nsubs = 1; c <= nim->dim[0]; c++ ) + nsubs *= nim->dim[c]; + + if( nsubs <= 0 ){ + fprintf(stderr,"** VNBL warning: bad dim list (%d,%d,%d,%d)\n", + nim->dim[4], nim->dim[5], nim->dim[6], nim->dim[7]); + return 0; + } + + for( c = 0; c < nbricks; c++ ) + if( (blist[c] < 0) || (blist[c] >= nsubs) ){ + if( disp_error || g_opts.debug > 1 ) + fprintf(stderr, + "** volume index %d (#%d) is out of range [0,%d]\n", + blist[c], c, nsubs-1); + return 0; + } + + return 1; /* all is well */ +} + +/*----------------------------------------------------------------------*/ +/* verify that NBL struct is a valid data source for the image + * + * return 1 if so, 0 otherwise +*//*--------------------------------------------------------------------*/ +static int nifti_NBL_matches_nim(const nifti_image *nim, + const nifti_brick_list *NBL) +{ + size_t volbytes = 0; /* bytes per volume */ + int ind, errs = 0, nvols = 0; + + + if( !nim || !NBL ) { + if( g_opts.debug > 0 ) + fprintf(stderr,"** nifti_NBL_matches_nim: NULL pointer(s)\n"); + return 0; + } + + /* for nim, compute volbytes and nvols */ + if( nim->ndim > 0 ) { + /* first 3 indices are over a single volume */ + volbytes = (size_t)nim->nbyper; + for( ind = 1; ind <= nim->ndim && ind < 4; ind++ ) + volbytes *= (size_t)nim->dim[ind]; + + for( ind = 4, nvols = 1; ind <= nim->ndim; ind++ ) + nvols *= nim->dim[ind]; + } + + if( volbytes != NBL->bsize ) { + if( g_opts.debug > 1 ) + fprintf(stderr,"** NBL/nim mismatch, volbytes = %u, %u\n", + (unsigned)NBL->bsize, (unsigned)volbytes); + errs++; + } + + if( nvols != NBL->nbricks ) { + if( g_opts.debug > 1 ) + fprintf(stderr,"** NBL/nim mismatch, nvols = %d, %d\n", + NBL->nbricks, nvols); + errs++; + } + + if( errs ) return 0; + else if ( g_opts.debug > 2 ) + fprintf(stderr,"-- nim/NBL agree: nvols = %d, nbytes = %u\n", + nvols, (unsigned)volbytes); + + return 1; +} + +/* end of new nifti_image_read_bricks() functionality */ + +/*----------------------------------------------------------------------*/ +/*! display the orientation from the quaternian fields + * + * \param mesg if non-NULL, display this message first + * \param mat the matrix to convert to "nearest" orientation + * + * \return -1 if results cannot be determined, 0 if okay +*//*--------------------------------------------------------------------*/ +int nifti_disp_matrix_orient( const char * mesg, mat44 mat ) +{ + int i, j, k; + + if ( mesg ) fputs( mesg, stderr ); /* use stdout? */ + + nifti_mat44_to_orientation( mat, &i,&j,&k ); + if ( i <= 0 || j <= 0 || k <= 0 ) return -1; + + /* so we have good codes */ + fprintf(stderr, " i orientation = '%s'\n" + " j orientation = '%s'\n" + " k orientation = '%s'\n", + nifti_orientation_string(i), + nifti_orientation_string(j), + nifti_orientation_string(k) ); + return 0; +} + + +/*----------------------------------------------------------------------*/ +/*! duplicate the given string (alloc length+1) + * + * \return allocated pointer (or NULL on failure) +*//*--------------------------------------------------------------------*/ +char *nifti_strdup(const char *str) +{ + char *dup; + + if( !str ) return NULL; /* allow calls passing NULL */ + + dup = (char *)malloc(strlen(str) + 1); + + /* check for failure */ + if( dup ) strcpy(dup, str); + else fprintf(stderr,"** nifti_strdup: failed to alloc %u bytes\n", + (unsigned int)strlen(str)+1); + + return dup; +} + + +/*---------------------------------------------------------------------------*/ +/*! Return a pointer to a string holding the name of a NIFTI datatype. + + \param dt NIfTI-1 datatype + + \return pointer to static string holding the datatype name + + \warning Do not free() or modify this string! + It points to static storage. + + \sa NIFTI1_DATATYPES group in nifti1.h +*//*-------------------------------------------------------------------------*/ +char *nifti_datatype_string( int dt ) +{ + switch( dt ){ + case DT_UNKNOWN: return "UNKNOWN" ; + case DT_BINARY: return "BINARY" ; + case DT_INT8: return "INT8" ; + case DT_UINT8: return "UINT8" ; + case DT_INT16: return "INT16" ; + case DT_UINT16: return "UINT16" ; + case DT_INT32: return "INT32" ; + case DT_UINT32: return "UINT32" ; + case DT_INT64: return "INT64" ; + case DT_UINT64: return "UINT64" ; + case DT_FLOAT32: return "FLOAT32" ; + case DT_FLOAT64: return "FLOAT64" ; + case DT_FLOAT128: return "FLOAT128" ; + case DT_COMPLEX64: return "COMPLEX64" ; + case DT_COMPLEX128: return "COMPLEX128" ; + case DT_COMPLEX256: return "COMPLEX256" ; + case DT_RGB24: return "RGB24" ; + case DT_RGBA32: return "RGBA32" ; + } + return "**ILLEGAL**" ; +} + +/*----------------------------------------------------------------------*/ +/*! Determine if the datatype code dt is an integer type (1=YES, 0=NO). + + \return whether the given NIfTI-1 datatype code is valid + + \sa NIFTI1_DATATYPES group in nifti1.h +*//*--------------------------------------------------------------------*/ +int nifti_is_inttype( int dt ) +{ + switch( dt ){ + case DT_UNKNOWN: return 0 ; + case DT_BINARY: return 0 ; + case DT_INT8: return 1 ; + case DT_UINT8: return 1 ; + case DT_INT16: return 1 ; + case DT_UINT16: return 1 ; + case DT_INT32: return 1 ; + case DT_UINT32: return 1 ; + case DT_INT64: return 1 ; + case DT_UINT64: return 1 ; + case DT_FLOAT32: return 0 ; + case DT_FLOAT64: return 0 ; + case DT_FLOAT128: return 0 ; + case DT_COMPLEX64: return 0 ; + case DT_COMPLEX128: return 0 ; + case DT_COMPLEX256: return 0 ; + case DT_RGB24: return 1 ; + case DT_RGBA32: return 1 ; + } + return 0 ; +} + +/*---------------------------------------------------------------------------*/ +/*! Return a pointer to a string holding the name of a NIFTI units type. + + \param uu NIfTI-1 unit code + + \return pointer to static string for the given unit type + + \warning Do not free() or modify this string! + It points to static storage. + + \sa NIFTI1_UNITS group in nifti1.h +*//*-------------------------------------------------------------------------*/ +char *nifti_units_string( int uu ) +{ + switch( uu ){ + case NIFTI_UNITS_METER: return "m" ; + case NIFTI_UNITS_MM: return "mm" ; + case NIFTI_UNITS_MICRON: return "um" ; + case NIFTI_UNITS_SEC: return "s" ; + case NIFTI_UNITS_MSEC: return "ms" ; + case NIFTI_UNITS_USEC: return "us" ; + case NIFTI_UNITS_HZ: return "Hz" ; + case NIFTI_UNITS_PPM: return "ppm" ; + case NIFTI_UNITS_RADS: return "rad/s" ; + } + return "Unknown" ; +} + +/*---------------------------------------------------------------------------*/ +/*! Return a pointer to a string holding the name of a NIFTI transform type. + + \param xx NIfTI-1 xform code + + \return pointer to static string describing xform code + + \warning Do not free() or modify this string! + It points to static storage. + + \sa NIFTI1_XFORM_CODES group in nifti1.h +*//*-------------------------------------------------------------------------*/ +char *nifti_xform_string( int xx ) +{ + switch( xx ){ + case NIFTI_XFORM_SCANNER_ANAT: return "Scanner Anat" ; + case NIFTI_XFORM_ALIGNED_ANAT: return "Aligned Anat" ; + case NIFTI_XFORM_TALAIRACH: return "Talairach" ; + case NIFTI_XFORM_MNI_152: return "MNI_152" ; + } + return "Unknown" ; +} + +/*---------------------------------------------------------------------------*/ +/*! Return a pointer to a string holding the name of a NIFTI intent type. + + \param ii NIfTI-1 intent code + + \return pointer to static string describing code + + \warning Do not free() or modify this string! + It points to static storage. + + \sa NIFTI1_INTENT_CODES group in nifti1.h +*//*-------------------------------------------------------------------------*/ +char *nifti_intent_string( int ii ) +{ + switch( ii ){ + case NIFTI_INTENT_CORREL: return "Correlation statistic" ; + case NIFTI_INTENT_TTEST: return "T-statistic" ; + case NIFTI_INTENT_FTEST: return "F-statistic" ; + case NIFTI_INTENT_ZSCORE: return "Z-score" ; + case NIFTI_INTENT_CHISQ: return "Chi-squared distribution" ; + case NIFTI_INTENT_BETA: return "Beta distribution" ; + case NIFTI_INTENT_BINOM: return "Binomial distribution" ; + case NIFTI_INTENT_GAMMA: return "Gamma distribution" ; + case NIFTI_INTENT_POISSON: return "Poisson distribution" ; + case NIFTI_INTENT_NORMAL: return "Normal distribution" ; + case NIFTI_INTENT_FTEST_NONC: return "F-statistic noncentral" ; + case NIFTI_INTENT_CHISQ_NONC: return "Chi-squared noncentral" ; + case NIFTI_INTENT_LOGISTIC: return "Logistic distribution" ; + case NIFTI_INTENT_LAPLACE: return "Laplace distribution" ; + case NIFTI_INTENT_UNIFORM: return "Uniform distribition" ; + case NIFTI_INTENT_TTEST_NONC: return "T-statistic noncentral" ; + case NIFTI_INTENT_WEIBULL: return "Weibull distribution" ; + case NIFTI_INTENT_CHI: return "Chi distribution" ; + case NIFTI_INTENT_INVGAUSS: return "Inverse Gaussian distribution" ; + case NIFTI_INTENT_EXTVAL: return "Extreme Value distribution" ; + case NIFTI_INTENT_PVAL: return "P-value" ; + + case NIFTI_INTENT_LOGPVAL: return "Log P-value" ; + case NIFTI_INTENT_LOG10PVAL: return "Log10 P-value" ; + + case NIFTI_INTENT_ESTIMATE: return "Estimate" ; + case NIFTI_INTENT_LABEL: return "Label index" ; + case NIFTI_INTENT_NEURONAME: return "NeuroNames index" ; + case NIFTI_INTENT_GENMATRIX: return "General matrix" ; + case NIFTI_INTENT_SYMMATRIX: return "Symmetric matrix" ; + case NIFTI_INTENT_DISPVECT: return "Displacement vector" ; + case NIFTI_INTENT_VECTOR: return "Vector" ; + case NIFTI_INTENT_POINTSET: return "Pointset" ; + case NIFTI_INTENT_TRIANGLE: return "Triangle" ; + case NIFTI_INTENT_QUATERNION: return "Quaternion" ; + + case NIFTI_INTENT_DIMLESS: return "Dimensionless number" ; + } + return "Unknown" ; +} + +/*---------------------------------------------------------------------------*/ +/*! Return a pointer to a string holding the name of a NIFTI slice_code. + + \param ss NIfTI-1 slice order code + + \return pointer to static string describing code + + \warning Do not free() or modify this string! + It points to static storage. + + \sa NIFTI1_SLICE_ORDER group in nifti1.h +*//*-------------------------------------------------------------------------*/ +char *nifti_slice_string( int ss ) +{ + switch( ss ){ + case NIFTI_SLICE_SEQ_INC: return "sequential_increasing" ; + case NIFTI_SLICE_SEQ_DEC: return "sequential_decreasing" ; + case NIFTI_SLICE_ALT_INC: return "alternating_increasing" ; + case NIFTI_SLICE_ALT_DEC: return "alternating_decreasing" ; + case NIFTI_SLICE_ALT_INC2: return "alternating_increasing_2" ; + case NIFTI_SLICE_ALT_DEC2: return "alternating_decreasing_2" ; + } + return "Unknown" ; +} + +/*---------------------------------------------------------------------------*/ +/*! Return a pointer to a string holding the name of a NIFTI orientation. + + \param ii orientation code + + \return pointer to static string holding the orientation information + + \warning Do not free() or modify the return string! + It points to static storage. + + \sa NIFTI_L2R in nifti1_io.h +*//*-------------------------------------------------------------------------*/ +char *nifti_orientation_string( int ii ) +{ + switch( ii ){ + case NIFTI_L2R: return "Left-to-Right" ; + case NIFTI_R2L: return "Right-to-Left" ; + case NIFTI_P2A: return "Posterior-to-Anterior" ; + case NIFTI_A2P: return "Anterior-to-Posterior" ; + case NIFTI_I2S: return "Inferior-to-Superior" ; + case NIFTI_S2I: return "Superior-to-Inferior" ; + } + return "Unknown" ; +} + +/*--------------------------------------------------------------------------*/ +/*! Given a datatype code, set number of bytes per voxel and the swapsize. + + \param datatype nifti1 datatype code + \param nbyper pointer to return value: number of bytes per voxel + \param swapsize pointer to return value: size of swap blocks + + \return appropriate values at nbyper and swapsize + + The swapsize is set to 0 if this datatype doesn't ever need swapping. + + \sa NIFTI1_DATATYPES in nifti1.h +*//*------------------------------------------------------------------------*/ +void nifti_datatype_sizes( int datatype , int *nbyper, int *swapsize ) +{ + int nb=0, ss=0 ; + switch( datatype ){ + case DT_INT8: + case DT_UINT8: nb = 1 ; ss = 0 ; break ; + + case DT_INT16: + case DT_UINT16: nb = 2 ; ss = 2 ; break ; + + case DT_RGB24: nb = 3 ; ss = 0 ; break ; + case DT_RGBA32: nb = 4 ; ss = 0 ; break ; + + case DT_INT32: + case DT_UINT32: + case DT_FLOAT32: nb = 4 ; ss = 4 ; break ; + + case DT_COMPLEX64: nb = 8 ; ss = 4 ; break ; + + case DT_FLOAT64: + case DT_INT64: + case DT_UINT64: nb = 8 ; ss = 8 ; break ; + + case DT_FLOAT128: nb = 16 ; ss = 16 ; break ; + + case DT_COMPLEX128: nb = 16 ; ss = 8 ; break ; + + case DT_COMPLEX256: nb = 32 ; ss = 16 ; break ; + } + + ASSIF(nbyper,nb) ; ASSIF(swapsize,ss) ; return ; +} + +/*---------------------------------------------------------------------------*/ +/*! Given the quaternion parameters (etc.), compute a transformation matrix. + + See comments in nifti1.h for details. + - qb,qc,qd = quaternion parameters + - qx,qy,qz = offset parameters + - dx,dy,dz = grid stepsizes (non-negative inputs are set to 1.0) + - qfac = sign of dz step (< 0 is negative; >= 0 is positive) + +
+   If qx=qy=qz=0, dx=dy=dz=1, then the output is a rotation matrix.
+   For qfac >= 0, the rotation is proper.
+   For qfac <  0, the rotation is improper.
+   
+ + \see "QUATERNION REPRESENTATION OF ROTATION MATRIX" in nifti1.h + \see nifti_mat44_to_quatern, nifti_make_orthog_mat44, + nifti_mat44_to_orientation + +*//*-------------------------------------------------------------------------*/ +mat44 nifti_quatern_to_mat44( float qb, float qc, float qd, + float qx, float qy, float qz, + float dx, float dy, float dz, float qfac ) +{ + mat44 R ; + double a,b=qb,c=qc,d=qd , xd,yd,zd ; + + /* last row is always [ 0 0 0 1 ] */ + + R.m[3][0]=R.m[3][1]=R.m[3][2] = 0.0 ; R.m[3][3]= 1.0 ; + + /* compute a parameter from b,c,d */ + + a = 1.0l - (b*b + c*c + d*d) ; + if( a < 1.e-7l ){ /* special case */ + a = 1.0l / sqrt(b*b+c*c+d*d) ; + b *= a ; c *= a ; d *= a ; /* normalize (b,c,d) vector */ + a = 0.0l ; /* a = 0 ==> 180 degree rotation */ + } else{ + a = sqrt(a) ; /* angle = 2*arccos(a) */ + } + + /* load rotation matrix, including scaling factors for voxel sizes */ + + xd = (dx > 0.0) ? dx : 1.0l ; /* make sure are positive */ + yd = (dy > 0.0) ? dy : 1.0l ; + zd = (dz > 0.0) ? dz : 1.0l ; + + if( qfac < 0.0 ) zd = -zd ; /* left handedness? */ + + R.m[0][0] = (a*a+b*b-c*c-d*d) * xd ; + R.m[0][1] = 2.0l * (b*c-a*d ) * yd ; + R.m[0][2] = 2.0l * (b*d+a*c ) * zd ; + R.m[1][0] = 2.0l * (b*c+a*d ) * xd ; + R.m[1][1] = (a*a+c*c-b*b-d*d) * yd ; + R.m[1][2] = 2.0l * (c*d-a*b ) * zd ; + R.m[2][0] = 2.0l * (b*d-a*c ) * xd ; + R.m[2][1] = 2.0l * (c*d+a*b ) * yd ; + R.m[2][2] = (a*a+d*d-c*c-b*b) * zd ; + + /* load offsets */ + + R.m[0][3] = qx ; R.m[1][3] = qy ; R.m[2][3] = qz ; + + return R ; +} + +/*---------------------------------------------------------------------------*/ +/*! Given the 3x4 upper corner of the matrix R, compute the quaternion + parameters that fit it. + + - Any NULL pointer on input won't get assigned (e.g., if you don't want + dx,dy,dz, just pass NULL in for those pointers). + - If the 3 input matrix columns are NOT orthogonal, they will be + orthogonalized prior to calculating the parameters, using + the polar decomposition to find the orthogonal matrix closest + to the column-normalized input matrix. + - However, if the 3 input matrix columns are NOT orthogonal, then + the matrix produced by nifti_quatern_to_mat44 WILL have orthogonal + columns, so it won't be the same as the matrix input here. + This "feature" is because the NIFTI 'qform' transform is + deliberately not fully general -- it is intended to model a volume + with perpendicular axes. + - If the 3 input matrix columns are not even linearly independent, + you'll just have to take your luck, won't you? + + \see "QUATERNION REPRESENTATION OF ROTATION MATRIX" in nifti1.h + + \see nifti_quatern_to_mat44, nifti_make_orthog_mat44, + nifti_mat44_to_orientation +*//*-------------------------------------------------------------------------*/ +void nifti_mat44_to_quatern( mat44 R , + float *qb, float *qc, float *qd, + float *qx, float *qy, float *qz, + float *dx, float *dy, float *dz, float *qfac ) +{ + double r11,r12,r13 , r21,r22,r23 , r31,r32,r33 ; + double xd,yd,zd , a,b,c,d ; + mat33 P,Q ; + + /* offset outputs are read write out of input matrix */ + + ASSIF(qx,R.m[0][3]) ; ASSIF(qy,R.m[1][3]) ; ASSIF(qz,R.m[2][3]) ; + + /* load 3x3 matrix into local variables */ + + r11 = R.m[0][0] ; r12 = R.m[0][1] ; r13 = R.m[0][2] ; + r21 = R.m[1][0] ; r22 = R.m[1][1] ; r23 = R.m[1][2] ; + r31 = R.m[2][0] ; r32 = R.m[2][1] ; r33 = R.m[2][2] ; + + /* compute lengths of each column; these determine grid spacings */ + + xd = sqrt( r11*r11 + r21*r21 + r31*r31 ) ; + yd = sqrt( r12*r12 + r22*r22 + r32*r32 ) ; + zd = sqrt( r13*r13 + r23*r23 + r33*r33 ) ; + + /* if a column length is zero, patch the trouble */ + + if( xd == 0.0l ){ r11 = 1.0l ; r21 = r31 = 0.0l ; xd = 1.0l ; } + if( yd == 0.0l ){ r22 = 1.0l ; r12 = r32 = 0.0l ; yd = 1.0l ; } + if( zd == 0.0l ){ r33 = 1.0l ; r13 = r23 = 0.0l ; zd = 1.0l ; } + + /* assign the output lengths */ + + ASSIF(dx,xd) ; ASSIF(dy,yd) ; ASSIF(dz,zd) ; + + /* normalize the columns */ + + r11 /= xd ; r21 /= xd ; r31 /= xd ; + r12 /= yd ; r22 /= yd ; r32 /= yd ; + r13 /= zd ; r23 /= zd ; r33 /= zd ; + + /* At this point, the matrix has normal columns, but we have to allow + for the fact that the hideous user may not have given us a matrix + with orthogonal columns. + + So, now find the orthogonal matrix closest to the current matrix. + + One reason for using the polar decomposition to get this + orthogonal matrix, rather than just directly orthogonalizing + the columns, is so that inputting the inverse matrix to R + will result in the inverse orthogonal matrix at this point. + If we just orthogonalized the columns, this wouldn't necessarily hold. */ + + Q.m[0][0] = r11 ; Q.m[0][1] = r12 ; Q.m[0][2] = r13 ; /* load Q */ + Q.m[1][0] = r21 ; Q.m[1][1] = r22 ; Q.m[1][2] = r23 ; + Q.m[2][0] = r31 ; Q.m[2][1] = r32 ; Q.m[2][2] = r33 ; + + P = nifti_mat33_polar(Q) ; /* P is orthog matrix closest to Q */ + + r11 = P.m[0][0] ; r12 = P.m[0][1] ; r13 = P.m[0][2] ; /* unload */ + r21 = P.m[1][0] ; r22 = P.m[1][1] ; r23 = P.m[1][2] ; + r31 = P.m[2][0] ; r32 = P.m[2][1] ; r33 = P.m[2][2] ; + + /* [ r11 r12 r13 ] */ + /* at this point, the matrix [ r21 r22 r23 ] is orthogonal */ + /* [ r31 r32 r33 ] */ + + /* compute the determinant to determine if it is proper */ + + zd = r11*r22*r33-r11*r32*r23-r21*r12*r33 + +r21*r32*r13+r31*r12*r23-r31*r22*r13 ; /* should be -1 or 1 */ + + if( zd > 0 ){ /* proper */ + ASSIF(qfac,1.0) ; + } else { /* improper ==> flip 3rd column */ + ASSIF(qfac,-1.0) ; + r13 = -r13 ; r23 = -r23 ; r33 = -r33 ; + } + + /* now, compute quaternion parameters */ + + a = r11 + r22 + r33 + 1.0l ; + + if( a > 0.5l ){ /* simplest case */ + a = 0.5l * sqrt(a) ; + b = 0.25l * (r32-r23) / a ; + c = 0.25l * (r13-r31) / a ; + d = 0.25l * (r21-r12) / a ; + } else { /* trickier case */ + xd = 1.0 + r11 - (r22+r33) ; /* 4*b*b */ + yd = 1.0 + r22 - (r11+r33) ; /* 4*c*c */ + zd = 1.0 + r33 - (r11+r22) ; /* 4*d*d */ + if( xd > 1.0 ){ + b = 0.5l * sqrt(xd) ; + c = 0.25l* (r12+r21) / b ; + d = 0.25l* (r13+r31) / b ; + a = 0.25l* (r32-r23) / b ; + } else if( yd > 1.0 ){ + c = 0.5l * sqrt(yd) ; + b = 0.25l* (r12+r21) / c ; + d = 0.25l* (r23+r32) / c ; + a = 0.25l* (r13-r31) / c ; + } else { + d = 0.5l * sqrt(zd) ; + b = 0.25l* (r13+r31) / d ; + c = 0.25l* (r23+r32) / d ; + a = 0.25l* (r21-r12) / d ; + } + if( a < 0.0l ){ b=-b ; c=-c ; d=-d; a=-a; } + } + + ASSIF(qb,b) ; ASSIF(qc,c) ; ASSIF(qd,d) ; + return ; +} + +/*---------------------------------------------------------------------------*/ +/*! Compute the inverse of a bordered 4x4 matrix. + +
+   - Some numerical code fragments were generated by Maple 8.
+   - If a singular matrix is input, the output matrix will be all zero.
+   - You can check for this by examining the [3][3] element, which will
+     be 1.0 for the normal case and 0.0 for the bad case.
+
+     The input matrix should have the form:
+        [ r11 r12 r13 v1 ]
+        [ r21 r22 r23 v2 ]
+        [ r31 r32 r33 v3 ]
+        [  0   0   0   1 ]
+     
+*//*-------------------------------------------------------------------------*/ +mat44 nifti_mat44_inverse( mat44 R ) +{ + double r11,r12,r13,r21,r22,r23,r31,r32,r33,v1,v2,v3 , deti ; + mat44 Q ; + /* INPUT MATRIX IS: */ + r11 = R.m[0][0]; r12 = R.m[0][1]; r13 = R.m[0][2]; /* [ r11 r12 r13 v1 ] */ + r21 = R.m[1][0]; r22 = R.m[1][1]; r23 = R.m[1][2]; /* [ r21 r22 r23 v2 ] */ + r31 = R.m[2][0]; r32 = R.m[2][1]; r33 = R.m[2][2]; /* [ r31 r32 r33 v3 ] */ + v1 = R.m[0][3]; v2 = R.m[1][3]; v3 = R.m[2][3]; /* [ 0 0 0 1 ] */ + + deti = r11*r22*r33-r11*r32*r23-r21*r12*r33 + +r21*r32*r13+r31*r12*r23-r31*r22*r13 ; + + if( deti != 0.0l ) deti = 1.0l / deti ; + + Q.m[0][0] = deti*( r22*r33-r32*r23) ; + Q.m[0][1] = deti*(-r12*r33+r32*r13) ; + Q.m[0][2] = deti*( r12*r23-r22*r13) ; + Q.m[0][3] = deti*(-r12*r23*v3+r12*v2*r33+r22*r13*v3 + -r22*v1*r33-r32*r13*v2+r32*v1*r23) ; + + Q.m[1][0] = deti*(-r21*r33+r31*r23) ; + Q.m[1][1] = deti*( r11*r33-r31*r13) ; + Q.m[1][2] = deti*(-r11*r23+r21*r13) ; + Q.m[1][3] = deti*( r11*r23*v3-r11*v2*r33-r21*r13*v3 + +r21*v1*r33+r31*r13*v2-r31*v1*r23) ; + + Q.m[2][0] = deti*( r21*r32-r31*r22) ; + Q.m[2][1] = deti*(-r11*r32+r31*r12) ; + Q.m[2][2] = deti*( r11*r22-r21*r12) ; + Q.m[2][3] = deti*(-r11*r22*v3+r11*r32*v2+r21*r12*v3 + -r21*r32*v1-r31*r12*v2+r31*r22*v1) ; + + Q.m[3][0] = Q.m[3][1] = Q.m[3][2] = 0.0l ; + Q.m[3][3] = (deti == 0.0l) ? 0.0l : 1.0l ; /* failure flag if deti == 0 */ + + return Q ; +} + +/*---------------------------------------------------------------------------*/ +/*! Input 9 floats and make an orthgonal mat44 out of them. + + Each row is normalized, then nifti_mat33_polar() is used to orthogonalize + them. If row #3 (r31,r32,r33) is input as zero, then it will be taken to + be the cross product of rows #1 and #2. + + This function can be used to create a rotation matrix for transforming + an oblique volume to anatomical coordinates. For this application: + - row #1 (r11,r12,r13) is the direction vector along the image i-axis + - row #2 (r21,r22,r23) is the direction vector along the image j-axis + - row #3 (r31,r32,r33) is the direction vector along the slice direction + (if available; otherwise enter it as 0's) + + The first 2 rows can be taken from the DICOM attribute (0020,0037) + "Image Orientation (Patient)". + + After forming the rotation matrix, the complete affine transformation from + (i,j,k) grid indexes to (x,y,z) spatial coordinates can be computed by + multiplying each column by the appropriate grid spacing: + - column #1 (R.m[0][0],R.m[1][0],R.m[2][0]) by delta-x + - column #2 (R.m[0][1],R.m[1][1],R.m[2][1]) by delta-y + - column #3 (R.m[0][2],R.m[1][2],R.m[2][2]) by delta-z + + and by then placing the center (x,y,z) coordinates of voxel (0,0,0) into + the column #4 (R.m[0][3],R.m[1][3],R.m[2][3]). + + \sa nifti_quatern_to_mat44, nifti_mat44_to_quatern, + nifti_mat44_to_orientation +*//*-------------------------------------------------------------------------*/ +mat44 nifti_make_orthog_mat44( float r11, float r12, float r13 , + float r21, float r22, float r23 , + float r31, float r32, float r33 ) +{ + mat44 R ; + mat33 Q , P ; + double val ; + + R.m[3][0] = R.m[3][1] = R.m[3][2] = 0.0l ; R.m[3][3] = 1.0l ; + + Q.m[0][0] = r11 ; Q.m[0][1] = r12 ; Q.m[0][2] = r13 ; /* load Q */ + Q.m[1][0] = r21 ; Q.m[1][1] = r22 ; Q.m[1][2] = r23 ; + Q.m[2][0] = r31 ; Q.m[2][1] = r32 ; Q.m[2][2] = r33 ; + + /* normalize row 1 */ + + val = Q.m[0][0]*Q.m[0][0] + Q.m[0][1]*Q.m[0][1] + Q.m[0][2]*Q.m[0][2] ; + if( val > 0.0l ){ + val = 1.0l / sqrt(val) ; + Q.m[0][0] *= val ; Q.m[0][1] *= val ; Q.m[0][2] *= val ; + } else { + Q.m[0][0] = 1.0l ; Q.m[0][1] = 0.0l ; Q.m[0][2] = 0.0l ; + } + + /* normalize row 2 */ + + val = Q.m[1][0]*Q.m[1][0] + Q.m[1][1]*Q.m[1][1] + Q.m[1][2]*Q.m[1][2] ; + if( val > 0.0l ){ + val = 1.0l / sqrt(val) ; + Q.m[1][0] *= val ; Q.m[1][1] *= val ; Q.m[1][2] *= val ; + } else { + Q.m[1][0] = 0.0l ; Q.m[1][1] = 1.0l ; Q.m[1][2] = 0.0l ; + } + + /* normalize row 3 */ + + val = Q.m[2][0]*Q.m[2][0] + Q.m[2][1]*Q.m[2][1] + Q.m[2][2]*Q.m[2][2] ; + if( val > 0.0l ){ + val = 1.0l / sqrt(val) ; + Q.m[2][0] *= val ; Q.m[2][1] *= val ; Q.m[2][2] *= val ; + } else { + Q.m[2][0] = Q.m[0][1]*Q.m[1][2] - Q.m[0][2]*Q.m[1][1] ; /* cross */ + Q.m[2][1] = Q.m[0][2]*Q.m[1][0] - Q.m[0][0]*Q.m[1][2] ; /* product */ + Q.m[2][2] = Q.m[0][0]*Q.m[1][1] - Q.m[0][1]*Q.m[1][0] ; + } + + P = nifti_mat33_polar(Q) ; /* P is orthog matrix closest to Q */ + + R.m[0][0] = P.m[0][0] ; R.m[0][1] = P.m[0][1] ; R.m[0][2] = P.m[0][2] ; + R.m[1][0] = P.m[1][0] ; R.m[1][1] = P.m[1][1] ; R.m[1][2] = P.m[1][2] ; + R.m[2][0] = P.m[2][0] ; R.m[2][1] = P.m[2][1] ; R.m[2][2] = P.m[2][2] ; + + R.m[0][3] = R.m[1][3] = R.m[2][3] = 0.0 ; return R ; +} + +/*----------------------------------------------------------------------*/ +/*! compute the inverse of a 3x3 matrix +*//*--------------------------------------------------------------------*/ +mat33 nifti_mat33_inverse( mat33 R ) /* inverse of 3x3 matrix */ +{ + double r11,r12,r13,r21,r22,r23,r31,r32,r33 , deti ; + mat33 Q ; + /* INPUT MATRIX: */ + r11 = R.m[0][0]; r12 = R.m[0][1]; r13 = R.m[0][2]; /* [ r11 r12 r13 ] */ + r21 = R.m[1][0]; r22 = R.m[1][1]; r23 = R.m[1][2]; /* [ r21 r22 r23 ] */ + r31 = R.m[2][0]; r32 = R.m[2][1]; r33 = R.m[2][2]; /* [ r31 r32 r33 ] */ + + deti = r11*r22*r33-r11*r32*r23-r21*r12*r33 + +r21*r32*r13+r31*r12*r23-r31*r22*r13 ; + + if( deti != 0.0l ) deti = 1.0l / deti ; + + Q.m[0][0] = deti*( r22*r33-r32*r23) ; + Q.m[0][1] = deti*(-r12*r33+r32*r13) ; + Q.m[0][2] = deti*( r12*r23-r22*r13) ; + + Q.m[1][0] = deti*(-r21*r33+r31*r23) ; + Q.m[1][1] = deti*( r11*r33-r31*r13) ; + Q.m[1][2] = deti*(-r11*r23+r21*r13) ; + + Q.m[2][0] = deti*( r21*r32-r31*r22) ; + Q.m[2][1] = deti*(-r11*r32+r31*r12) ; + Q.m[2][2] = deti*( r11*r22-r21*r12) ; + + return Q ; +} + +/*----------------------------------------------------------------------*/ +/*! compute the determinant of a 3x3 matrix +*//*--------------------------------------------------------------------*/ +float nifti_mat33_determ( mat33 R ) /* determinant of 3x3 matrix */ +{ + double r11,r12,r13,r21,r22,r23,r31,r32,r33 ; + /* INPUT MATRIX: */ + r11 = R.m[0][0]; r12 = R.m[0][1]; r13 = R.m[0][2]; /* [ r11 r12 r13 ] */ + r21 = R.m[1][0]; r22 = R.m[1][1]; r23 = R.m[1][2]; /* [ r21 r22 r23 ] */ + r31 = R.m[2][0]; r32 = R.m[2][1]; r33 = R.m[2][2]; /* [ r31 r32 r33 ] */ + + return r11*r22*r33-r11*r32*r23-r21*r12*r33 + +r21*r32*r13+r31*r12*r23-r31*r22*r13 ; +} + +/*----------------------------------------------------------------------*/ +/*! compute the max row norm of a 3x3 matrix +*//*--------------------------------------------------------------------*/ +float nifti_mat33_rownorm( mat33 A ) /* max row norm of 3x3 matrix */ +{ + float r1,r2,r3 ; + + r1 = fabs(A.m[0][0])+fabs(A.m[0][1])+fabs(A.m[0][2]) ; + r2 = fabs(A.m[1][0])+fabs(A.m[1][1])+fabs(A.m[1][2]) ; + r3 = fabs(A.m[2][0])+fabs(A.m[2][1])+fabs(A.m[2][2]) ; + if( r1 < r2 ) r1 = r2 ; + if( r1 < r3 ) r1 = r3 ; + return r1 ; +} + +/*----------------------------------------------------------------------*/ +/*! compute the max column norm of a 3x3 matrix +*//*--------------------------------------------------------------------*/ +float nifti_mat33_colnorm( mat33 A ) /* max column norm of 3x3 matrix */ +{ + float r1,r2,r3 ; + + r1 = fabs(A.m[0][0])+fabs(A.m[1][0])+fabs(A.m[2][0]) ; + r2 = fabs(A.m[0][1])+fabs(A.m[1][1])+fabs(A.m[2][1]) ; + r3 = fabs(A.m[0][2])+fabs(A.m[1][2])+fabs(A.m[2][2]) ; + if( r1 < r2 ) r1 = r2 ; + if( r1 < r3 ) r1 = r3 ; + return r1 ; +} + +/*----------------------------------------------------------------------*/ +/*! multiply 2 3x3 matrices +*//*--------------------------------------------------------------------*/ +mat33 nifti_mat33_mul( mat33 A , mat33 B ) /* multiply 2 3x3 matrices */ +{ + mat33 C ; int i,j ; + for( i=0 ; i < 3 ; i++ ) + for( j=0 ; j < 3 ; j++ ) + C.m[i][j] = A.m[i][0] * B.m[0][j] + + A.m[i][1] * B.m[1][j] + + A.m[i][2] * B.m[2][j] ; + return C ; +} + + +/*---------------------------------------------------------------------------*/ +/*! polar decomposition of a 3x3 matrix + + This finds the closest orthogonal matrix to input A + (in both the Frobenius and L2 norms). + + Algorithm is that from NJ Higham, SIAM J Sci Stat Comput, 7:1160-1174. +*//*-------------------------------------------------------------------------*/ +mat33 nifti_mat33_polar( mat33 A ) +{ + mat33 X , Y , Z ; + float alp,bet,gam,gmi , dif=1.0 ; + int k=0 ; + + X = A ; + + /* force matrix to be nonsingular */ + + gam = nifti_mat33_determ(X) ; + while( gam == 0.0 ){ /* perturb matrix */ + gam = 0.00001 * ( 0.001 + nifti_mat33_rownorm(X) ) ; + X.m[0][0] += gam ; X.m[1][1] += gam ; X.m[2][2] += gam ; + gam = nifti_mat33_determ(X) ; + } + + while(1){ + Y = nifti_mat33_inverse(X) ; + if( dif > 0.3 ){ /* far from convergence */ + alp = sqrt( nifti_mat33_rownorm(X) * nifti_mat33_colnorm(X) ) ; + bet = sqrt( nifti_mat33_rownorm(Y) * nifti_mat33_colnorm(Y) ) ; + gam = sqrt( bet / alp ) ; + gmi = 1.0 / gam ; + } else { + gam = gmi = 1.0 ; /* close to convergence */ + } + Z.m[0][0] = 0.5 * ( gam*X.m[0][0] + gmi*Y.m[0][0] ) ; + Z.m[0][1] = 0.5 * ( gam*X.m[0][1] + gmi*Y.m[1][0] ) ; + Z.m[0][2] = 0.5 * ( gam*X.m[0][2] + gmi*Y.m[2][0] ) ; + Z.m[1][0] = 0.5 * ( gam*X.m[1][0] + gmi*Y.m[0][1] ) ; + Z.m[1][1] = 0.5 * ( gam*X.m[1][1] + gmi*Y.m[1][1] ) ; + Z.m[1][2] = 0.5 * ( gam*X.m[1][2] + gmi*Y.m[2][1] ) ; + Z.m[2][0] = 0.5 * ( gam*X.m[2][0] + gmi*Y.m[0][2] ) ; + Z.m[2][1] = 0.5 * ( gam*X.m[2][1] + gmi*Y.m[1][2] ) ; + Z.m[2][2] = 0.5 * ( gam*X.m[2][2] + gmi*Y.m[2][2] ) ; + + dif = fabs(Z.m[0][0]-X.m[0][0])+fabs(Z.m[0][1]-X.m[0][1]) + +fabs(Z.m[0][2]-X.m[0][2])+fabs(Z.m[1][0]-X.m[1][0]) + +fabs(Z.m[1][1]-X.m[1][1])+fabs(Z.m[1][2]-X.m[1][2]) + +fabs(Z.m[2][0]-X.m[2][0])+fabs(Z.m[2][1]-X.m[2][1]) + +fabs(Z.m[2][2]-X.m[2][2]) ; + + k = k+1 ; + if( k > 100 || dif < 3.e-6 ) break ; /* convergence or exhaustion */ + X = Z ; + } + + return Z ; +} + +/*---------------------------------------------------------------------------*/ +/*! compute the (closest) orientation from a 4x4 ijk->xyz tranformation matrix + +
+   Input:  4x4 matrix that transforms (i,j,k) indexes to (x,y,z) coordinates,
+           where +x=Right, +y=Anterior, +z=Superior.
+           (Only the upper-left 3x3 corner of R is used herein.)
+   Output: 3 orientation codes that correspond to the closest "standard"
+           anatomical orientation of the (i,j,k) axes.
+   Method: Find which permutation of (x,y,z) has the smallest angle to the
+           (i,j,k) axes directions, which are the columns of the R matrix.
+   Errors: The codes returned will be zero.
+
+   For example, an axial volume might get return values of
+     *icod = NIFTI_R2L   (i axis is mostly Right to Left)
+     *jcod = NIFTI_P2A   (j axis is mostly Posterior to Anterior)
+     *kcod = NIFTI_I2S   (k axis is mostly Inferior to Superior)
+   
+ + \see "QUATERNION REPRESENTATION OF ROTATION MATRIX" in nifti1.h + + \see nifti_quatern_to_mat44, nifti_mat44_to_quatern, + nifti_make_orthog_mat44 +*//*-------------------------------------------------------------------------*/ +void nifti_mat44_to_orientation( mat44 R , int *icod, int *jcod, int *kcod ) +{ + float xi,xj,xk , yi,yj,yk , zi,zj,zk , val,detQ,detP ; + mat33 P , Q , M ; + int i,j,k=0,p,q,r , ibest,jbest,kbest,pbest,qbest,rbest ; + float vbest ; + + if( icod == NULL || jcod == NULL || kcod == NULL ) return ; /* bad */ + + *icod = *jcod = *kcod = 0 ; /* error returns, if sh*t happens */ + + /* load column vectors for each (i,j,k) direction from matrix */ + + /*-- i axis --*/ /*-- j axis --*/ /*-- k axis --*/ + + xi = R.m[0][0] ; xj = R.m[0][1] ; xk = R.m[0][2] ; + yi = R.m[1][0] ; yj = R.m[1][1] ; yk = R.m[1][2] ; + zi = R.m[2][0] ; zj = R.m[2][1] ; zk = R.m[2][2] ; + + /* normalize column vectors to get unit vectors along each ijk-axis */ + + /* normalize i axis */ + + val = sqrt( xi*xi + yi*yi + zi*zi ) ; + if( val == 0.0 ) return ; /* stupid input */ + xi /= val ; yi /= val ; zi /= val ; + + /* normalize j axis */ + + val = sqrt( xj*xj + yj*yj + zj*zj ) ; + if( val == 0.0 ) return ; /* stupid input */ + xj /= val ; yj /= val ; zj /= val ; + + /* orthogonalize j axis to i axis, if needed */ + + val = xi*xj + yi*yj + zi*zj ; /* dot product between i and j */ + if( fabs(val) > 1.e-4 ){ + xj -= val*xi ; yj -= val*yi ; zj -= val*zi ; + val = sqrt( xj*xj + yj*yj + zj*zj ) ; /* must renormalize */ + if( val == 0.0 ) return ; /* j was parallel to i? */ + xj /= val ; yj /= val ; zj /= val ; + } + + /* normalize k axis; if it is zero, make it the cross product i x j */ + + val = sqrt( xk*xk + yk*yk + zk*zk ) ; + if( val == 0.0 ){ xk = yi*zj-zi*yj; yk = zi*xj-zj*xi ; zk=xi*yj-yi*xj ; } + else { xk /= val ; yk /= val ; zk /= val ; } + + /* orthogonalize k to i */ + + val = xi*xk + yi*yk + zi*zk ; /* dot product between i and k */ + if( fabs(val) > 1.e-4 ){ + xk -= val*xi ; yk -= val*yi ; zk -= val*zi ; + val = sqrt( xk*xk + yk*yk + zk*zk ) ; + if( val == 0.0 ) return ; /* bad */ + xk /= val ; yk /= val ; zk /= val ; + } + + /* orthogonalize k to j */ + + val = xj*xk + yj*yk + zj*zk ; /* dot product between j and k */ + if( fabs(val) > 1.e-4 ){ + xk -= val*xj ; yk -= val*yj ; zk -= val*zj ; + val = sqrt( xk*xk + yk*yk + zk*zk ) ; + if( val == 0.0 ) return ; /* bad */ + xk /= val ; yk /= val ; zk /= val ; + } + + Q.m[0][0] = xi ; Q.m[0][1] = xj ; Q.m[0][2] = xk ; + Q.m[1][0] = yi ; Q.m[1][1] = yj ; Q.m[1][2] = yk ; + Q.m[2][0] = zi ; Q.m[2][1] = zj ; Q.m[2][2] = zk ; + + /* at this point, Q is the rotation matrix from the (i,j,k) to (x,y,z) axes */ + + detQ = nifti_mat33_determ( Q ) ; + if( detQ == 0.0 ) return ; /* shouldn't happen unless user is a DUFIS */ + + /* Build and test all possible +1/-1 coordinate permutation matrices P; + then find the P such that the rotation matrix M=PQ is closest to the + identity, in the sense of M having the smallest total rotation angle. */ + + /* Despite the formidable looking 6 nested loops, there are + only 3*3*3*2*2*2 = 216 passes, which will run very quickly. */ + + vbest = -666.0 ; ibest=pbest=qbest=rbest=1 ; jbest=2 ; kbest=3 ; + for( i=1 ; i <= 3 ; i++ ){ /* i = column number to use for row #1 */ + for( j=1 ; j <= 3 ; j++ ){ /* j = column number to use for row #2 */ + if( i == j ) continue ; + for( k=1 ; k <= 3 ; k++ ){ /* k = column number to use for row #3 */ + if( i == k || j == k ) continue ; + P.m[0][0] = P.m[0][1] = P.m[0][2] = + P.m[1][0] = P.m[1][1] = P.m[1][2] = + P.m[2][0] = P.m[2][1] = P.m[2][2] = 0.0 ; + for( p=-1 ; p <= 1 ; p+=2 ){ /* p,q,r are -1 or +1 */ + for( q=-1 ; q <= 1 ; q+=2 ){ /* and go into rows #1,2,3 */ + for( r=-1 ; r <= 1 ; r+=2 ){ + P.m[0][i-1] = p ; P.m[1][j-1] = q ; P.m[2][k-1] = r ; + detP = nifti_mat33_determ(P) ; /* sign of permutation */ + if( detP * detQ <= 0.0 ) continue ; /* doesn't match sign of Q */ + M = nifti_mat33_mul(P,Q) ; + + /* angle of M rotation = 2.0*acos(0.5*sqrt(1.0+trace(M))) */ + /* we want largest trace(M) == smallest angle == M nearest to I */ + + val = M.m[0][0] + M.m[1][1] + M.m[2][2] ; /* trace */ + if( val > vbest ){ + vbest = val ; + ibest = i ; jbest = j ; kbest = k ; + pbest = p ; qbest = q ; rbest = r ; + } + }}}}}} + + /* At this point ibest is 1 or 2 or 3; pbest is -1 or +1; etc. + + The matrix P that corresponds is the best permutation approximation + to Q-inverse; that is, P (approximately) takes (x,y,z) coordinates + to the (i,j,k) axes. + + For example, the first row of P (which contains pbest in column ibest) + determines the way the i axis points relative to the anatomical + (x,y,z) axes. If ibest is 2, then the i axis is along the y axis, + which is direction P2A (if pbest > 0) or A2P (if pbest < 0). + + So, using ibest and pbest, we can assign the output code for + the i axis. Mutatis mutandis for the j and k axes, of course. */ + + switch( ibest*pbest ){ + case 1: i = NIFTI_L2R ; break ; + case -1: i = NIFTI_R2L ; break ; + case 2: i = NIFTI_P2A ; break ; + case -2: i = NIFTI_A2P ; break ; + case 3: i = NIFTI_I2S ; break ; + case -3: i = NIFTI_S2I ; break ; + } + + switch( jbest*qbest ){ + case 1: j = NIFTI_L2R ; break ; + case -1: j = NIFTI_R2L ; break ; + case 2: j = NIFTI_P2A ; break ; + case -2: j = NIFTI_A2P ; break ; + case 3: j = NIFTI_I2S ; break ; + case -3: j = NIFTI_S2I ; break ; + } + + switch( kbest*rbest ){ + case 1: k = NIFTI_L2R ; break ; + case -1: k = NIFTI_R2L ; break ; + case 2: k = NIFTI_P2A ; break ; + case -2: k = NIFTI_A2P ; break ; + case 3: k = NIFTI_I2S ; break ; + case -3: k = NIFTI_S2I ; break ; + } + + *icod = i ; *jcod = j ; *kcod = k ; return ; +} + +/*---------------------------------------------------------------------------*/ +/* Routines to swap byte arrays in various ways: + - 2 at a time: ab -> ba [short] + - 4 at a time: abcd -> dcba [int, float] + - 8 at a time: abcdDCBA -> ABCDdcba [long long, double] + - 16 at a time: abcdefghHGFEDCBA -> ABCDEFGHhgfedcba [long double] +-----------------------------------------------------------------------------*/ + +/*----------------------------------------------------------------------*/ +/*! swap each byte pair from the given list of n pairs + * + * Due to alignment of structures at some architectures (e.g. on ARM), + * stick to char varaibles. + * Fixes http://bugs.debian.org/446893 Yaroslav + * +*//*--------------------------------------------------------------------*/ +void nifti_swap_2bytes( size_t n , void *ar ) /* 2 bytes at a time */ +{ + register size_t ii ; + unsigned char * cp1 = (unsigned char *)ar, * cp2 ; + unsigned char tval; + + for( ii=0 ; ii < n ; ii++ ){ + cp2 = cp1 + 1; + tval = *cp1; *cp1 = *cp2; *cp2 = tval; + cp1 += 2; + } + return ; +} + +/*----------------------------------------------------------------------*/ +/*! swap 4 bytes at a time from the given list of n sets of 4 bytes +*//*--------------------------------------------------------------------*/ +void nifti_swap_4bytes( size_t n , void *ar ) /* 4 bytes at a time */ +{ + register size_t ii ; + unsigned char * cp0 = (unsigned char *)ar, * cp1, * cp2 ; + register unsigned char tval ; + + for( ii=0 ; ii < n ; ii++ ){ + cp1 = cp0; cp2 = cp0+3; + tval = *cp1; *cp1 = *cp2; *cp2 = tval; + cp1++; cp2--; + tval = *cp1; *cp1 = *cp2; *cp2 = tval; + cp0 += 4; + } + return ; +} + +/*----------------------------------------------------------------------*/ +/*! swap 8 bytes at a time from the given list of n sets of 8 bytes + * + * perhaps use this style for the general Nbytes, as Yaroslav suggests +*//*--------------------------------------------------------------------*/ +void nifti_swap_8bytes( size_t n , void *ar ) /* 8 bytes at a time */ +{ + register size_t ii ; + unsigned char * cp0 = (unsigned char *)ar, * cp1, * cp2 ; + register unsigned char tval ; + + for( ii=0 ; ii < n ; ii++ ){ + cp1 = cp0; cp2 = cp0+7; + while ( cp2 > cp1 ) /* unroll? */ + { + tval = *cp1 ; *cp1 = *cp2 ; *cp2 = tval ; + cp1++; cp2--; + } + cp0 += 8; + } + return ; +} + +/*----------------------------------------------------------------------*/ +/*! swap 16 bytes at a time from the given list of n sets of 16 bytes +*//*--------------------------------------------------------------------*/ +void nifti_swap_16bytes( size_t n , void *ar ) /* 16 bytes at a time */ +{ + register size_t ii ; + unsigned char * cp0 = (unsigned char *)ar, * cp1, * cp2 ; + register unsigned char tval ; + + for( ii=0 ; ii < n ; ii++ ){ + cp1 = cp0; cp2 = cp0+15; + while ( cp2 > cp1 ) + { + tval = *cp1 ; *cp1 = *cp2 ; *cp2 = tval ; + cp1++; cp2--; + } + cp0 += 16; + } + return ; +} + +#if 0 /* not important: save for version update 6 Jul 2010 [rickr] */ + +/*----------------------------------------------------------------------*/ +/*! generic: swap siz bytes at a time from the given list of n sets +*//*--------------------------------------------------------------------*/ +void nifti_swap_bytes( size_t n , int siz , void *ar ) +{ + register size_t ii ; + unsigned char * cp0 = (unsigned char *)ar, * cp1, * cp2 ; + register unsigned char tval ; + + for( ii=0 ; ii < n ; ii++ ){ + cp1 = cp0; cp2 = cp0+(siz-1); + while ( cp2 > cp1 ) + { + tval = *cp1 ; *cp1 = *cp2 ; *cp2 = tval ; + cp1++; cp2--; + } + cp0 += siz; + } + return ; +} +#endif + +/*---------------------------------------------------------------------------*/ + +/*----------------------------------------------------------------------*/ +/*! based on siz, call the appropriate nifti_swap_Nbytes() function +*//*--------------------------------------------------------------------*/ +void nifti_swap_Nbytes( size_t n , int siz , void *ar ) /* subsuming case */ +{ + switch( siz ){ + case 2: nifti_swap_2bytes ( n , ar ) ; break ; + case 4: nifti_swap_4bytes ( n , ar ) ; break ; + case 8: nifti_swap_8bytes ( n , ar ) ; break ; + case 16: nifti_swap_16bytes( n , ar ) ; break ; + default: /* nifti_swap_bytes ( n , siz, ar ) ; */ + fprintf(stderr,"** NIfTI: cannot swap in %d byte blocks\n", siz); + break ; + } + return ; +} + + +/*-------------------------------------------------------------------------*/ +/*! Byte swap NIFTI-1 file header in various places and ways. + + If is_nifti, swap all (even UNUSED) fields of NIfTI header. + Else, swap as a nifti_analyze75 struct. +*//*---------------------------------------------------------------------- */ +void swap_nifti_header( struct nifti_1_header *h , int is_nifti ) +{ + + /* if ANALYZE, swap as such and return */ + if( ! is_nifti ) { + nifti_swap_as_analyze((nifti_analyze75 *)h); + return; + } + + /* otherwise, swap all NIFTI fields */ + + nifti_swap_4bytes(1, &h->sizeof_hdr); + nifti_swap_4bytes(1, &h->extents); + nifti_swap_2bytes(1, &h->session_error); + + nifti_swap_2bytes(8, h->dim); + nifti_swap_4bytes(1, &h->intent_p1); + nifti_swap_4bytes(1, &h->intent_p2); + nifti_swap_4bytes(1, &h->intent_p3); + + nifti_swap_2bytes(1, &h->intent_code); + nifti_swap_2bytes(1, &h->datatype); + nifti_swap_2bytes(1, &h->bitpix); + nifti_swap_2bytes(1, &h->slice_start); + + nifti_swap_4bytes(8, h->pixdim); + + nifti_swap_4bytes(1, &h->vox_offset); + nifti_swap_4bytes(1, &h->scl_slope); + nifti_swap_4bytes(1, &h->scl_inter); + nifti_swap_2bytes(1, &h->slice_end); + + nifti_swap_4bytes(1, &h->cal_max); + nifti_swap_4bytes(1, &h->cal_min); + nifti_swap_4bytes(1, &h->slice_duration); + nifti_swap_4bytes(1, &h->toffset); + nifti_swap_4bytes(1, &h->glmax); + nifti_swap_4bytes(1, &h->glmin); + + nifti_swap_2bytes(1, &h->qform_code); + nifti_swap_2bytes(1, &h->sform_code); + + nifti_swap_4bytes(1, &h->quatern_b); + nifti_swap_4bytes(1, &h->quatern_c); + nifti_swap_4bytes(1, &h->quatern_d); + nifti_swap_4bytes(1, &h->qoffset_x); + nifti_swap_4bytes(1, &h->qoffset_y); + nifti_swap_4bytes(1, &h->qoffset_z); + + nifti_swap_4bytes(4, h->srow_x); + nifti_swap_4bytes(4, h->srow_y); + nifti_swap_4bytes(4, h->srow_z); + + return ; +} + +/*-------------------------------------------------------------------------*/ +/*! Byte swap as an ANALYZE 7.5 header + * + * return non-zero on failure +*//*---------------------------------------------------------------------- */ +int nifti_swap_as_analyze( nifti_analyze75 * h ) +{ + if( !h ) return 1; + + nifti_swap_4bytes(1, &h->sizeof_hdr); + nifti_swap_4bytes(1, &h->extents); + nifti_swap_2bytes(1, &h->session_error); + + nifti_swap_2bytes(8, h->dim); + nifti_swap_2bytes(1, &h->unused8); + nifti_swap_2bytes(1, &h->unused9); + nifti_swap_2bytes(1, &h->unused10); + nifti_swap_2bytes(1, &h->unused11); + nifti_swap_2bytes(1, &h->unused12); + nifti_swap_2bytes(1, &h->unused13); + nifti_swap_2bytes(1, &h->unused14); + + nifti_swap_2bytes(1, &h->datatype); + nifti_swap_2bytes(1, &h->bitpix); + nifti_swap_2bytes(1, &h->dim_un0); + + nifti_swap_4bytes(8, h->pixdim); + + nifti_swap_4bytes(1, &h->vox_offset); + nifti_swap_4bytes(1, &h->funused1); + nifti_swap_4bytes(1, &h->funused2); + nifti_swap_4bytes(1, &h->funused3); + + nifti_swap_4bytes(1, &h->cal_max); + nifti_swap_4bytes(1, &h->cal_min); + nifti_swap_4bytes(1, &h->compressed); + nifti_swap_4bytes(1, &h->verified); + + nifti_swap_4bytes(1, &h->glmax); + nifti_swap_4bytes(1, &h->glmin); + + nifti_swap_4bytes(1, &h->views); + nifti_swap_4bytes(1, &h->vols_added); + nifti_swap_4bytes(1, &h->start_field); + nifti_swap_4bytes(1, &h->field_skip); + + nifti_swap_4bytes(1, &h->omax); + nifti_swap_4bytes(1, &h->omin); + nifti_swap_4bytes(1, &h->smax); + nifti_swap_4bytes(1, &h->smin); + + return 0; +} + +/*-------------------------------------------------------------------------*/ +/*! OLD VERSION of swap_nifti_header (left for undo/compare operations) + + Byte swap NIFTI-1 file header in various places and ways. + + If is_nifti is nonzero, will also swap the NIFTI-specific + components of the header; otherwise, only the components + common to NIFTI and ANALYZE will be swapped. +*//*---------------------------------------------------------------------- */ +void old_swap_nifti_header( struct nifti_1_header *h , int is_nifti ) +{ + /* this stuff is always present, for ANALYZE and NIFTI */ + + swap_4(h->sizeof_hdr) ; + nifti_swap_2bytes( 8 , h->dim ) ; + nifti_swap_4bytes( 8 , h->pixdim ) ; + + swap_2(h->datatype) ; + swap_2(h->bitpix) ; + + swap_4(h->vox_offset); swap_4(h->cal_max); swap_4(h->cal_min); + + /* this stuff is NIFTI specific */ + + if( is_nifti ){ + swap_4(h->intent_p1); swap_4(h->intent_p2); swap_4(h->intent_p3); + swap_2(h->intent_code); + + swap_2(h->slice_start); swap_2(h->slice_end); + swap_4(h->scl_slope); swap_4(h->scl_inter); + swap_4(h->slice_duration); swap_4(h->toffset); + + swap_2(h->qform_code); swap_2(h->sform_code); + swap_4(h->quatern_b); swap_4(h->quatern_c); swap_4(h->quatern_d); + swap_4(h->qoffset_x); swap_4(h->qoffset_y); swap_4(h->qoffset_z); + nifti_swap_4bytes(4,h->srow_x); + nifti_swap_4bytes(4,h->srow_y); + nifti_swap_4bytes(4,h->srow_z); + } + return ; +} + + +#define USE_STAT +#ifdef USE_STAT +/*---------------------------------------------------------------------------*/ +/* Return the file length (0 if file not found or has no contents). + This is a Unix-specific function, since it uses stat(). +-----------------------------------------------------------------------------*/ +#include +#include + +/*---------------------------------------------------------------------------*/ +/*! return the size of a file, in bytes + + \return size of file on success, -1 on error or no file + + changed to return int, -1 means no file or error 20 Dec 2004 [rickr] +*//*-------------------------------------------------------------------------*/ +int nifti_get_filesize( const char *pathname ) +{ + struct stat buf ; int ii ; + + if( pathname == NULL || *pathname == '\0' ) return -1 ; + ii = stat( pathname , &buf ); if( ii != 0 ) return -1 ; + return (unsigned int)buf.st_size ; +} + +#else /*---------- non-Unix version of the above, less efficient -----------*/ + +int nifti_get_filesize( const char *pathname ) +{ + znzFile fp ; int len ; + + if( pathname == NULL || *pathname == '\0' ) return -1 ; + fp = znzopen(pathname,"rb",0); if( znz_isnull(fp) ) return -1 ; + znzseek(fp,0L,SEEK_END) ; len = znztell(fp) ; + znzclose(fp) ; return len ; +} + +#endif /* USE_STAT */ + + +/*----------------------------------------------------------------------*/ +/*! return the total volume size, in bytes + + This is computed as nvox * nbyper. +*//*--------------------------------------------------------------------*/ +size_t nifti_get_volsize(const nifti_image *nim) +{ + return nim->nbyper * nim->nvox ; /* total bytes */ +} + + +/*--------------------------------------------------------------------------*/ +/* Support functions for filenames in read and write + - allows for gzipped files +*/ + + +/*----------------------------------------------------------------------*/ +/*! simple check for file existence + + \return 1 on existence, 0 otherwise +*//*--------------------------------------------------------------------*/ +int nifti_fileexists(const char* fname) +{ + znzFile fp; + fp = znzopen( fname , "rb" , 1 ) ; + if( !znz_isnull(fp) ) { znzclose(fp); return 1; } + return 0; /* fp is NULL */ +} + +/*----------------------------------------------------------------------*/ +/*! return whether the filename is valid + + Note: uppercase extensions are now valid. 27 Apr 2009 [rickr] + + The name is considered valid if the file basename has length greater than + zero, AND one of the valid nifti extensions is provided. + fname input | return | + =============================== + "myimage" | 0 | + "myimage.tif" | 0 | + "myimage.tif.gz" | 0 | + "myimage.nii" | 1 | + ".nii" | 0 | + ".myhiddenimage" | 0 | + ".myhiddenimage.nii" | 1 | +*//*--------------------------------------------------------------------*/ +int nifti_is_complete_filename(const char* fname) +{ + char * ext; + + /* check input file(s) for sanity */ + if( fname == NULL || *fname == '\0' ){ + if ( g_opts.debug > 1 ) + fprintf(stderr,"-- empty filename in nifti_validfilename()\n"); + return 0; + } + + ext = nifti_find_file_extension(fname); + if ( ext == NULL ) { /*Invalid extension given */ + if ( g_opts.debug > 0 ) + fprintf(stderr,"-- no nifti valid extension for filename '%s'\n", fname); + return 0; + } + + if ( ext && ext == fname ) { /* then no filename prefix */ + if ( g_opts.debug > 0 ) + fprintf(stderr,"-- no prefix for filename '%s'\n", fname); + return 0; + } + return 1; +} + +/*----------------------------------------------------------------------*/ +/*! return whether the filename is valid + + Allow uppercase extensions as valid. 27 Apr 2009 [rickr] + Any .gz extension case must match the base extension case. + + The name is considered valid if its length is positive, excluding + any nifti filename extension. + fname input | return | result of nifti_makebasename + ==================================================================== + "myimage" | 1 | "myimage" + "myimage.tif" | 1 | "myimage.tif" + "myimage.tif.gz" | 1 | "myimage.tif" + "myimage.nii" | 1 | "myimage" + ".nii" | 0 | + ".myhiddenimage" | 1 | ".myhiddenimage" + ".myhiddenimage.nii | 1 | ".myhiddenimage" +*//*--------------------------------------------------------------------*/ +int nifti_validfilename(const char* fname) +{ + char * ext; + + /* check input file(s) for sanity */ + if( fname == NULL || *fname == '\0' ){ + if ( g_opts.debug > 1 ) + fprintf(stderr,"-- empty filename in nifti_validfilename()\n"); + return 0; + } + + ext = nifti_find_file_extension(fname); + + if ( ext && ext == fname ) { /* then no filename prefix */ + if ( g_opts.debug > 0 ) + fprintf(stderr,"-- no prefix for filename '%s'\n", fname); + return 0; + } + + return 1; +} + +/*----------------------------------------------------------------------*/ +/*! check the end of the filename for a valid nifti extension + + Valid extensions are currently .nii, .hdr, .img, .nia, + or any of them followed by .gz. Note that '.' is part of + the extension. + + Uppercase extensions are also valid, but not mixed case. + + \return a pointer to the extension (within the filename), or NULL +*//*--------------------------------------------------------------------*/ +char * nifti_find_file_extension( const char * name ) +{ + char * ext, extcopy[8]; + int len; + char extnii[8] = ".nii"; /* modifiable, for possible uppercase */ + char exthdr[8] = ".hdr"; /* (leave space for .gz) */ + char extimg[8] = ".img"; + char extnia[8] = ".nia"; + char extgz[4] = ".gz"; + char * elist[4] = { NULL, NULL, NULL, NULL}; + + /* stupid compiler... */ + elist[0] = extnii; elist[1] = exthdr; elist[2] = extimg; elist[3] = extnia; + + if ( ! name ) return NULL; + + len = (int)strlen(name); + if ( len < 4 ) return NULL; + + ext = (char *)name + len - 4; + + /* make manipulation copy, and possibly convert to lowercase */ + strcpy(extcopy, ext); + if( g_opts.allow_upper_fext ) make_lowercase(extcopy); + + /* if it look like a basic extension, fail or return it */ + if( compare_strlist(extcopy, elist, 4) >= 0 ) { + if( is_mixedcase(ext) ) { + fprintf(stderr,"** mixed case extension '%s' is not valid\n", ext); + return NULL; + } + else return ext; + } + +#ifdef HAVE_ZLIB + if ( len < 7 ) return NULL; + + ext = (char *)name + len - 7; + + /* make manipulation copy, and possibly convert to lowercase */ + strcpy(extcopy, ext); + if( g_opts.allow_upper_fext ) make_lowercase(extcopy); + + /* go after .gz extensions using the modifiable strings */ + strcat(elist[0], extgz); strcat(elist[1], extgz); strcat(elist[2], extgz); + + if( compare_strlist(extcopy, elist, 3) >= 0 ) { + if( is_mixedcase(ext) ) { + fprintf(stderr,"** mixed case extension '%s' is not valid\n", ext); + return NULL; + } + else return ext; + } + +#endif + + if( g_opts.debug > 1 ) + fprintf(stderr,"** find_file_ext: failed for name '%s'\n", name); + + return NULL; +} + +/*----------------------------------------------------------------------*/ +/*! return whether the filename ends in ".gz" +*//*--------------------------------------------------------------------*/ +int nifti_is_gzfile(const char* fname) +{ + /* return true if the filename ends with .gz */ + if (fname == NULL) { return 0; } +#ifdef HAVE_ZLIB + { /* just so len doesn't generate compile warning */ + int len; + len = (int)strlen(fname); + if (len < 3) return 0; /* so we don't search before the name */ + if (fileext_compare(fname + strlen(fname) - 3,".gz")==0) { return 1; } + } +#endif + return 0; +} + +/*----------------------------------------------------------------------*/ +/*! return whether the given library was compiled with HAVE_ZLIB set +*//*--------------------------------------------------------------------*/ +int nifti_compiled_with_zlib(void) +{ +#ifdef HAVE_ZLIB + return 1; +#else + return 0; +#endif +} + +/*----------------------------------------------------------------------*/ +/*! duplicate the filename, while clearing any extension + + This allocates memory for basename which should eventually be freed. +*//*--------------------------------------------------------------------*/ +char * nifti_makebasename(const char* fname) +{ + char *basename, *ext; + + basename=nifti_strdup(fname); + + ext = nifti_find_file_extension(basename); + if ( ext ) *ext = '\0'; /* clear out extension */ + + return basename; /* in either case */ +} + +/*----------------------------------------------------------------------*/ +/*! set nifti's global debug level, for status reporting + + - 0 : quiet, nothing is printed to the terminal, but errors + - 1 : normal execution (the default) + - 2, 3 : more details +*//*--------------------------------------------------------------------*/ +void nifti_set_debug_level( int level ) +{ + g_opts.debug = level; +} + +/*----------------------------------------------------------------------*/ +/*! set nifti's global skip_blank_ext flag 5 Sep 2006 [rickr] + + explicitly set to 0 or 1 +*//*--------------------------------------------------------------------*/ +void nifti_set_skip_blank_ext( int skip ) +{ + g_opts.skip_blank_ext = skip ? 1 : 0; +} + +/*----------------------------------------------------------------------*/ +/*! set nifti's global allow_upper_fext flag 28 Apr 2009 [rickr] + + explicitly set to 0 or 1 +*//*--------------------------------------------------------------------*/ +void nifti_set_allow_upper_fext( int allow ) +{ + g_opts.allow_upper_fext = allow ? 1 : 0; +} + +/*----------------------------------------------------------------------*/ +/*! check current directory for existing header file + + \return filename of header on success and NULL if no appropriate file + could be found + + If fname has an uppercase extension, check for uppercase files. + + NB: it allocates memory for hdrname which should be freed + when no longer required +*//*-------------------------------------------------------------------*/ +char * nifti_findhdrname(const char* fname) +{ + char *basename, *hdrname, *ext; + char elist[2][5] = { ".hdr", ".nii" }; + char extzip[4] = ".gz"; + int efirst = 1; /* init to .nii extension */ + int eisupper = 0; /* init to lowercase extensions */ + + /**- check input file(s) for sanity */ + if( !nifti_validfilename(fname) ) return NULL; + + basename = nifti_makebasename(fname); + if( !basename ) return NULL; /* only on string alloc failure */ + + /**- return filename if it has a valid extension and exists + (except if it is an .img file (and maybe .gz)) */ + ext = nifti_find_file_extension(fname); + + if( ext ) eisupper = is_uppercase(ext); /* do we look for uppercase? */ + + /* if the file exists and is a valid header name (not .img), return it */ + if ( ext && nifti_fileexists(fname) ) { + /* allow for uppercase extension */ + if ( fileext_n_compare(ext,".img",4) != 0 ){ + hdrname = nifti_strdup(fname); + free(basename); + return hdrname; + } else + efirst = 0; /* note for below */ + } + + /* So the requested name is a basename, contains .img, or does not exist. */ + /* In any case, use basename. */ + + /**- if .img, look for .hdr, .hdr.gz, .nii, .nii.gz, in that order */ + /**- else, look for .nii, .nii.gz, .hdr, .hdr.gz, in that order */ + + /* if we get more extension choices, this could be a loop */ + + /* note: efirst is 0 in the case of ".img" */ + + /* if the user passed an uppercase entension (.IMG), search for uppercase */ + if( eisupper ) { + make_uppercase(elist[0]); + make_uppercase(elist[1]); + make_uppercase(extzip); + } + + hdrname = (char *)calloc(sizeof(char),strlen(basename)+8); + if( !hdrname ){ + fprintf(stderr,"** nifti_findhdrname: failed to alloc hdrname\n"); + free(basename); + return NULL; + } + + strcpy(hdrname,basename); + strcat(hdrname,elist[efirst]); + if (nifti_fileexists(hdrname)) { free(basename); return hdrname; } +#ifdef HAVE_ZLIB + strcat(hdrname,extzip); + if (nifti_fileexists(hdrname)) { free(basename); return hdrname; } +#endif + + /* okay, try the other possibility */ + + efirst = 1 - efirst; + + strcpy(hdrname,basename); + strcat(hdrname,elist[efirst]); + if (nifti_fileexists(hdrname)) { free(basename); return hdrname; } +#ifdef HAVE_ZLIB + strcat(hdrname,extzip); + if (nifti_fileexists(hdrname)) { free(basename); return hdrname; } +#endif + + /**- if nothing has been found, return NULL */ + free(basename); + free(hdrname); + return NULL; +} + + +/*------------------------------------------------------------------------*/ +/*! check current directory for existing image file + + \param fname filename to check for + \nifti_type nifti_type for dataset - this determines whether to + first check for ".nii" or ".img" (since both may exist) + + \return filename of data/img file on success and NULL if no appropriate + file could be found + + If fname has a valid, uppercase extension, apply all extensions as + uppercase. + + NB: it allocates memory for the image filename, which should be freed + when no longer required +*//*---------------------------------------------------------------------*/ +char * nifti_findimgname(const char* fname , int nifti_type) +{ + /* store all extensions as strings, in case we need to go uppercase */ + char *basename, *imgname, elist[2][5] = { ".nii", ".img" }; + char extzip[4] = ".gz"; + char extnia[5] = ".nia"; + char *ext; + int first; /* first extension to use */ + + /* check input file(s) for sanity */ + if( !nifti_validfilename(fname) ) return NULL; + + basename = nifti_makebasename(fname); + imgname = (char *)calloc(sizeof(char),strlen(basename)+8); + if( !imgname ){ + fprintf(stderr,"** nifti_findimgname: failed to alloc imgname\n"); + free(basename); + return NULL; + } + + /* if we are looking for uppercase, apply the fact now */ + ext = nifti_find_file_extension(fname); + if( ext && is_uppercase(ext) ) { + make_uppercase(elist[0]); + make_uppercase(elist[1]); + make_uppercase(extzip); + make_uppercase(extnia); + } + + /* only valid extension for ASCII type is .nia, handle first */ + if( nifti_type == NIFTI_FTYPE_ASCII ){ + strcpy(imgname,basename); + strcat(imgname,extnia); + if (nifti_fileexists(imgname)) { free(basename); return imgname; } + + } else { + + /**- test for .nii and .img (don't assume input type from image type) */ + /**- if nifti_type = 1, check for .nii first, else .img first */ + + /* if we get 3 or more extensions, can make a loop here... */ + + if (nifti_type == NIFTI_FTYPE_NIFTI1_1) first = 0; /* should match .nii */ + else first = 1; /* should match .img */ + + strcpy(imgname,basename); + strcat(imgname,elist[first]); + if (nifti_fileexists(imgname)) { free(basename); return imgname; } +#ifdef HAVE_ZLIB /* then also check for .gz */ + strcat(imgname,extzip); + if (nifti_fileexists(imgname)) { free(basename); return imgname; } +#endif + + /* failed to find image file with expected extension, try the other */ + + strcpy(imgname,basename); + strcat(imgname,elist[1-first]); /* can do this with only 2 choices */ + if (nifti_fileexists(imgname)) { free(basename); return imgname; } +#ifdef HAVE_ZLIB /* then also check for .gz */ + strcat(imgname,extzip); + if (nifti_fileexists(imgname)) { free(basename); return imgname; } +#endif + } + + /**- if nothing has been found, return NULL */ + free(basename); + free(imgname); + return NULL; +} + + +/*----------------------------------------------------------------------*/ +/*! creates a filename for storing the header, based on nifti_type + + \param prefix - this will be copied before the suffix is added + \param nifti_type - determines the extension, unless one is in prefix + \param check - check for existence (fail condition) + \param comp - add .gz for compressed name + + Note that if prefix provides a file suffix, nifti_type is not used. + + NB: this allocates memory which should be freed + + \sa nifti_set_filenames +*//*-------------------------------------------------------------------*/ +char * nifti_makehdrname(const char * prefix, int nifti_type, int check, + int comp) +{ + char * iname, * ext; + char extnii[5] = ".nii"; /* modifiable, for possible uppercase */ + char exthdr[5] = ".hdr"; + char extimg[5] = ".img"; + char extnia[5] = ".nia"; + char extgz[5] = ".gz"; + + if( !nifti_validfilename(prefix) ) return NULL; + + /* add space for extension, optional ".gz", and null char */ + iname = (char *)calloc(sizeof(char),strlen(prefix)+8); + if( !iname ){ fprintf(stderr,"** small malloc failure!\n"); return NULL; } + strcpy(iname, prefix); + + /* use any valid extension */ + if( (ext = nifti_find_file_extension(iname)) != NULL ){ + /* if uppercase, convert all extensions */ + if( is_uppercase(ext) ) { + make_uppercase(extnii); + make_uppercase(exthdr); + make_uppercase(extimg); + make_uppercase(extnia); + make_uppercase(extgz); + } + + if( strncmp(ext,extimg,4) == 0 ) + memcpy(ext,exthdr,4); /* then convert img name to hdr */ + } + /* otherwise, make one up */ + else if( nifti_type == NIFTI_FTYPE_NIFTI1_1 ) strcat(iname, extnii); + else if( nifti_type == NIFTI_FTYPE_ASCII ) strcat(iname, extnia); + else strcat(iname, exthdr); + +#ifdef HAVE_ZLIB /* if compression is requested, make sure of suffix */ + if( comp && (!ext || !strstr(iname,extgz)) ) strcat(iname,extgz); +#endif + + /* check for existence failure */ + if( check && nifti_fileexists(iname) ){ + fprintf(stderr,"** failure: header file '%s' already exists\n",iname); + free(iname); + return NULL; + } + + if(g_opts.debug > 2) fprintf(stderr,"+d made header filename '%s'\n", iname); + + return iname; +} + + +/*----------------------------------------------------------------------*/ +/*! creates a filename for storing the image, based on nifti_type + + \param prefix - this will be copied before the suffix is added + \param nifti_type - determines the extension, unless provided by prefix + \param check - check for existence (fail condition) + \param comp - add .gz for compressed name + + Note that if prefix provides a file suffix, nifti_type is not used. + + NB: it allocates memory which should be freed + + \sa nifti_set_filenames +*//*-------------------------------------------------------------------*/ +char * nifti_makeimgname(const char * prefix, int nifti_type, int check, + int comp) +{ + char * iname, * ext; + char extnii[5] = ".nii"; /* modifiable, for possible uppercase */ + char exthdr[5] = ".hdr"; + char extimg[5] = ".img"; + char extnia[5] = ".nia"; + char extgz[5] = ".gz"; + + if( !nifti_validfilename(prefix) ) return NULL; + + /* add space for extension, optional ".gz", and null char */ + iname = (char *)calloc(sizeof(char),strlen(prefix)+8); + if( !iname ){ fprintf(stderr,"** small malloc failure!\n"); return NULL; } + strcpy(iname, prefix); + + /* use any valid extension */ + if( (ext = nifti_find_file_extension(iname)) != NULL ){ + /* if uppercase, convert all extensions */ + if( is_uppercase(ext) ) { + make_uppercase(extnii); + make_uppercase(exthdr); + make_uppercase(extimg); + make_uppercase(extnia); + make_uppercase(extgz); + } + + if( strncmp(ext,exthdr,4) == 0 ) + memcpy(ext,extimg,4); /* then convert hdr name to img */ + } + /* otherwise, make one up */ + else if( nifti_type == NIFTI_FTYPE_NIFTI1_1 ) strcat(iname, extnii); + else if( nifti_type == NIFTI_FTYPE_ASCII ) strcat(iname, extnia); + else strcat(iname, extimg); + +#ifdef HAVE_ZLIB /* if compression is requested, make sure of suffix */ + if( comp && (!ext || !strstr(iname,extgz)) ) strcat(iname,extgz); +#endif + + /* check for existence failure */ + if( check && nifti_fileexists(iname) ){ + fprintf(stderr,"** failure: image file '%s' already exists\n",iname); + free(iname); + return NULL; + } + + if( g_opts.debug > 2 ) fprintf(stderr,"+d made image filename '%s'\n",iname); + + return iname; +} + + +/*----------------------------------------------------------------------*/ +/*! create and set new filenames, based on prefix and image type + + \param nim pointer to nifti_image in which to set filenames + \param prefix (required) prefix for output filenames + \param check check for previous existence of filename + (existence is an error condition) + \param set_byte_order flag to set nim->byteorder here + (this is probably a logical place to do so) + + \return 0 on successful update + + \warning this will free() any existing names and create new ones + + \sa nifti_makeimgname, nifti_makehdrname, nifti_type_and_names_match +*//*--------------------------------------------------------------------*/ +int nifti_set_filenames( nifti_image * nim, const char * prefix, int check, + int set_byte_order ) +{ + int comp = nifti_is_gzfile(prefix); + + if( !nim || !prefix ){ + fprintf(stderr,"** nifti_set_filenames, bad params %p, %p\n", + (void *)nim,prefix); + return -1; + } + + if( g_opts.debug > 1 ) + fprintf(stderr,"+d modifying output filenames using prefix %s\n", prefix); + + if( nim->fname ) free(nim->fname); + if( nim->iname ) free(nim->iname); + nim->fname = nifti_makehdrname(prefix, nim->nifti_type, check, comp); + nim->iname = nifti_makeimgname(prefix, nim->nifti_type, check, comp); + if( !nim->fname || !nim->iname ){ + LNI_FERR("nifti_set_filename","failed to set prefix for",prefix); + return -1; + } + + if( set_byte_order ) nim->byteorder = nifti_short_order() ; + + if( nifti_set_type_from_names(nim) < 0 ) + return -1; + + if( g_opts.debug > 2 ) + fprintf(stderr,"+d have new filenames %s and %s\n",nim->fname,nim->iname); + + return 0; +} + + +/*--------------------------------------------------------------------------*/ +/*! check whether nifti_type matches fname and iname for the nifti_image + + - if type 0 or 2, expect .hdr/.img pair + - if type 1, expect .nii (and names must match) + + \param nim given nifti_image + \param show_warn if set, print a warning message for any mis-match + + \return + - 1 if the values seem to match + - 0 if there is a mis-match + - -1 if there is not sufficient information to create file(s) + + \sa NIFTI_FTYPE_* codes in nifti1_io.h + \sa nifti_set_type_from_names, is_valid_nifti_type +*//*------------------------------------------------------------------------*/ +int nifti_type_and_names_match( nifti_image * nim, int show_warn ) +{ + char func[] = "nifti_type_and_names_match"; + char * ext_h, * ext_i; /* header and image filename extensions */ + int errs = 0; /* error counter */ + + /* sanity checks */ + if( !nim ){ + if( show_warn ) fprintf(stderr,"** %s: missing nifti_image\n", func); + return -1; + } + if( !nim->fname ){ + if( show_warn ) fprintf(stderr,"** %s: missing header filename\n", func); + errs++; + } + if( !nim->iname ){ + if( show_warn ) fprintf(stderr,"** %s: missing image filename\n", func); + errs++; + } + if( !is_valid_nifti_type(nim->nifti_type) ){ + if( show_warn ) + fprintf(stderr,"** %s: bad nifti_type %d\n", func, nim->nifti_type); + errs++; + } + + if( errs ) return -1; /* then do not proceed */ + + /* get pointers to extensions */ + ext_h = nifti_find_file_extension( nim->fname ); + ext_i = nifti_find_file_extension( nim->iname ); + + /* check for filename extensions */ + if( !ext_h ){ + if( show_warn ) + fprintf(stderr,"-d missing NIFTI extension in header filename, %s\n", + nim->fname); + errs++; + } + if( !ext_i ){ + if( show_warn ) + fprintf(stderr,"-d missing NIFTI extension in image filename, %s\n", + nim->iname); + errs++; + } + + if( errs ) return 0; /* do not proceed, but this is just a mis-match */ + + /* general tests */ + if( nim->nifti_type == NIFTI_FTYPE_NIFTI1_1 ){ /* .nii */ + if( fileext_n_compare(ext_h,".nii",4) ) { + if( show_warn ) + fprintf(stderr, + "-d NIFTI_FTYPE 1, but no .nii extension in header filename, %s\n", + nim->fname); + errs++; + } + if( fileext_n_compare(ext_i,".nii",4) ) { + if( show_warn ) + fprintf(stderr, + "-d NIFTI_FTYPE 1, but no .nii extension in image filename, %s\n", + nim->iname); + errs++; + } + if( strcmp(nim->fname, nim->iname) != 0 ){ + if( show_warn ) + fprintf(stderr, + "-d NIFTI_FTYPE 1, but header and image filenames differ: %s, %s\n", + nim->fname, nim->iname); + errs++; + } + } + else if( (nim->nifti_type == NIFTI_FTYPE_NIFTI1_2) || /* .hdr/.img */ + (nim->nifti_type == NIFTI_FTYPE_ANALYZE) ) + { + if( fileext_n_compare(ext_h,".hdr",4) != 0 ){ + if( show_warn ) + fprintf(stderr,"-d no '.hdr' extension, but NIFTI type is %d, %s\n", + nim->nifti_type, nim->fname); + errs++; + } + if( fileext_n_compare(ext_i,".img",4) != 0 ){ + if( show_warn ) + fprintf(stderr,"-d no '.img' extension, but NIFTI type is %d, %s\n", + nim->nifti_type, nim->iname); + errs++; + } + } + /* ignore any other nifti_type */ + + return 1; +} + +/* like strcmp, but also check against capitalization of known_ext + * (test as local string, with max length 7) */ +static int fileext_compare(const char * test_ext, const char * known_ext) +{ + char caps[8] = ""; + int c, cmp, len; + + /* if equal, don't need to check case (store to avoid multiple calls) */ + cmp = strcmp(test_ext, known_ext); + if( cmp == 0 ) return cmp; + + /* if anything odd, use default */ + if( !test_ext || !known_ext ) return cmp; + + len = strlen(known_ext); + if( len > 7 ) return cmp; + + /* if here, strings are different but need to check upper-case */ + + for(c = 0; c < len; c++ ) caps[c] = toupper(known_ext[c]); + caps[c] = '\0'; + + return strcmp(test_ext, caps); +} + +/* like strncmp, but also check against capitalization of known_ext + * (test as local string, with max length 7) */ +static int fileext_n_compare(const char * test_ext, + const char * known_ext, int maxlen) +{ + char caps[8] = ""; + int c, cmp, len; + + /* if equal, don't need to check case (store to avoid multiple calls) */ + cmp = strncmp(test_ext, known_ext, maxlen); + if( cmp == 0 ) return cmp; + + /* if anything odd, use default */ + if( !test_ext || !known_ext ) return cmp; + + len = strlen(known_ext); + if( len > maxlen ) len = maxlen; /* ignore anything past maxlen */ + if( len > 7 ) return cmp; + + /* if here, strings are different but need to check upper-case */ + + for(c = 0; c < len; c++ ) caps[c] = toupper(known_ext[c]); + caps[c] = '\0'; + + return strncmp(test_ext, caps, maxlen); +} + +/* return 1 if there are uppercase but no lowercase */ +static int is_uppercase(const char * str) +{ + unsigned int c, hasupper = 0; + + if( !str || !*str ) return 0; + + for(c = 0; c < strlen(str); c++ ) { + if( islower(str[c]) ) return 0; + if( !hasupper && isupper(str[c]) ) hasupper = 1; + } + + return hasupper; +} + +/* return 1 if there are both uppercase and lowercase characters */ +static int is_mixedcase(const char * str) +{ + unsigned int c, hasupper = 0, haslower = 0; + + if( !str || !*str ) return 0; + + for(c = 0; c < strlen(str); c++ ) { + if( !haslower && islower(str[c]) ) haslower = 1; + if( !hasupper && isupper(str[c]) ) hasupper = 1; + + if( haslower && hasupper ) return 1; + } + + return 0; +} + +/* convert any lowercase chars to uppercase */ +static int make_uppercase(char * str) +{ + unsigned int c; + + if( !str || !*str ) return 0; + + for(c = 0; c < strlen(str); c++ ) + if( islower(str[c]) ) str[c] = toupper(str[c]); + + return 0; +} + +/* convert any uppercase chars to lowercase */ +static int make_lowercase(char * str) +{ + unsigned int c; + + if( !str || !*str ) return 0; + + for(c = 0; c < strlen(str); c++ ) + if( isupper(str[c]) ) str[c] = tolower(str[c]); + + return 0; +} + +/* run strcmp against of list of strings + * return index of equality, if found + * else return -1 */ +static int compare_strlist(const char * str, char ** strlist, int len) +{ + int c; + if( len <= 0 || !str || !strlist ) return -1; + for( c = 0; c < len; c++ ) + if( strlist[c] && !strcmp(str, strlist[c]) ) return c; + return -1; +} + +/*--------------------------------------------------------------------------*/ +/*! check whether the given type is on the "approved" list + + The code is valid if it is non-negative, and does not exceed + NIFTI_MAX_FTYPE. + + \return 1 if nifti_type is valid, 0 otherwise + \sa NIFTI_FTYPE_* codes in nifti1_io.h +*//*------------------------------------------------------------------------*/ +int is_valid_nifti_type( int nifti_type ) +{ + if( nifti_type >= NIFTI_FTYPE_ANALYZE && /* smallest type, 0 */ + nifti_type <= NIFTI_MAX_FTYPE ) + return 1; + return 0; +} + + +/*--------------------------------------------------------------------------*/ +/*! check whether the given type is on the "approved" list + + The type is explicitly checked against the NIFTI_TYPE_* list + in nifti1.h. + + \return 1 if dtype is valid, 0 otherwise + \sa NIFTI_TYPE_* codes in nifti1.h +*//*------------------------------------------------------------------------*/ +int nifti_is_valid_datatype( int dtype ) +{ + if( dtype == NIFTI_TYPE_UINT8 || + dtype == NIFTI_TYPE_INT16 || + dtype == NIFTI_TYPE_INT32 || + dtype == NIFTI_TYPE_FLOAT32 || + dtype == NIFTI_TYPE_COMPLEX64 || + dtype == NIFTI_TYPE_FLOAT64 || + dtype == NIFTI_TYPE_RGB24 || + dtype == NIFTI_TYPE_RGBA32 || + dtype == NIFTI_TYPE_INT8 || + dtype == NIFTI_TYPE_UINT16 || + dtype == NIFTI_TYPE_UINT32 || + dtype == NIFTI_TYPE_INT64 || + dtype == NIFTI_TYPE_UINT64 || + dtype == NIFTI_TYPE_FLOAT128 || + dtype == NIFTI_TYPE_COMPLEX128 || + dtype == NIFTI_TYPE_COMPLEX256 ) return 1; + return 0; +} + + +/*--------------------------------------------------------------------------*/ +/*! set the nifti_type field based on fname and iname + + Note that nifti_type is changed only when it does not match + the filenames. + + \return 0 on success, -1 on error + + \sa is_valid_nifti_type, nifti_type_and_names_match +*//*------------------------------------------------------------------------*/ +int nifti_set_type_from_names( nifti_image * nim ) +{ + /* error checking first */ + if( !nim ){ fprintf(stderr,"** NSTFN: no nifti_image\n"); return -1; } + + if( !nim->fname || !nim->iname ){ + fprintf(stderr,"** NSTFN: missing filename(s) fname @ %p, iname @ %p\n", + nim->fname, nim->iname); + return -1; + } + + if( ! nifti_validfilename ( nim->fname ) || + ! nifti_validfilename ( nim->iname ) || + ! nifti_find_file_extension( nim->fname ) || + ! nifti_find_file_extension( nim->iname ) + ) { + fprintf(stderr,"** NSTFN: invalid filename(s) fname='%s', iname='%s'\n", + nim->fname, nim->iname); + return -1; + } + + if( g_opts.debug > 2 ) + fprintf(stderr,"-d verify nifti_type from filenames: %d",nim->nifti_type); + + /* type should be NIFTI_FTYPE_ASCII if extension is .nia */ + if( (fileext_compare(nifti_find_file_extension(nim->fname),".nia")==0)){ + nim->nifti_type = NIFTI_FTYPE_ASCII; + } else { + /* not too picky here, do what must be done, and then verify */ + if( strcmp(nim->fname, nim->iname) == 0 ) /* one file, type 1 */ + nim->nifti_type = NIFTI_FTYPE_NIFTI1_1; + else if( nim->nifti_type == NIFTI_FTYPE_NIFTI1_1 ) /* cannot be type 1 */ + nim->nifti_type = NIFTI_FTYPE_NIFTI1_2; + } + + if( g_opts.debug > 2 ) fprintf(stderr," -> %d\n",nim->nifti_type); + + if( g_opts.debug > 1 ) /* warn user about anything strange */ + nifti_type_and_names_match(nim, 1); + + if( is_valid_nifti_type(nim->nifti_type) ) return 0; /* success! */ + + fprintf(stderr,"** NSTFN: bad nifti_type %d, for '%s' and '%s'\n", + nim->nifti_type, nim->fname, nim->iname); + + return -1; +} + + +/*--------------------------------------------------------------------------*/ +/*! Determine if this is a NIFTI-formatted file. + +
+   \return  0 if file looks like ANALYZE 7.5 [checks sizeof_hdr field == 348]
+            1 if file marked as NIFTI (header+data in 1 file)
+            2 if file marked as NIFTI (header+data in 2 files)
+           -1 if it can't tell, file doesn't exist, etc.
+   
+*//*------------------------------------------------------------------------*/ +int is_nifti_file( const char *hname ) +{ + struct nifti_1_header nhdr ; + znzFile fp ; + int ii ; + char *tmpname; + + /* bad input name? */ + + if( !nifti_validfilename(hname) ) return -1 ; + + /* open file */ + + tmpname = nifti_findhdrname(hname); + if( tmpname == NULL ){ + if( g_opts.debug > 0 ) + fprintf(stderr,"** no header file found for '%s'\n",hname); + return -1; + } + fp = znzopen( tmpname , "rb" , nifti_is_gzfile(tmpname) ) ; + free(tmpname); + if (znz_isnull(fp)) return -1 ; /* bad open? */ + + /* read header, close file */ + + ii = (int)znzread( &nhdr , 1 , sizeof(nhdr) , fp ) ; + znzclose( fp ) ; + if( ii < (int) sizeof(nhdr) ) return -1 ; /* bad read? */ + + /* check for NIFTI-ness */ + + if( NIFTI_VERSION(nhdr) != 0 ){ + return ( NIFTI_ONEFILE(nhdr) ) ? 1 : 2 ; + } + + /* check for ANALYZE-ness (sizeof_hdr field == 348) */ + + ii = nhdr.sizeof_hdr ; + if( ii == (int)sizeof(nhdr) ) return 0 ; /* matches */ + + /* try byte-swapping header */ + + swap_4(ii) ; + if( ii == (int)sizeof(nhdr) ) return 0 ; /* matches */ + + return -1 ; /* not good */ +} + +static int print_hex_vals( const char * data, int nbytes, FILE * fp ) +{ + int c; + + if ( !data || nbytes < 1 || !fp ) return -1; + + fputs("0x", fp); + for ( c = 0; c < nbytes; c++ ) + fprintf(fp, " %x", data[c]); + + return 0; +} + +/*----------------------------------------------------------------------*/ +/*! display the contents of the nifti_1_header (send to stdout) + + \param info if non-NULL, print this character string + \param hp pointer to nifti_1_header +*//*--------------------------------------------------------------------*/ +int disp_nifti_1_header( const char * info, const nifti_1_header * hp ) +{ + int c; + + fputs( "-------------------------------------------------------\n", stdout ); + if ( info ) fputs( info, stdout ); + if ( !hp ){ fputs(" ** no nifti_1_header to display!\n",stdout); return 1; } + + fprintf(stdout," nifti_1_header :\n" + " sizeof_hdr = %d\n" + " data_type[10] = ", hp->sizeof_hdr); + print_hex_vals(hp->data_type, 10, stdout); + fprintf(stdout, "\n" + " db_name[18] = "); + print_hex_vals(hp->db_name, 18, stdout); + fprintf(stdout, "\n" + " extents = %d\n" + " session_error = %d\n" + " regular = 0x%x\n" + " dim_info = 0x%x\n", + hp->extents, hp->session_error, hp->regular, hp->dim_info ); + fprintf(stdout, " dim[8] ="); + for ( c = 0; c < 8; c++ ) fprintf(stdout," %d", hp->dim[c]); + fprintf(stdout, "\n" + " intent_p1 = %f\n" + " intent_p2 = %f\n" + " intent_p3 = %f\n" + " intent_code = %d\n" + " datatype = %d\n" + " bitpix = %d\n" + " slice_start = %d\n" + " pixdim[8] =", + hp->intent_p1, hp->intent_p2, hp->intent_p3, hp->intent_code, + hp->datatype, hp->bitpix, hp->slice_start); + /* break pixdim over 2 lines */ + for ( c = 0; c < 4; c++ ) fprintf(stdout," %f", hp->pixdim[c]); + fprintf(stdout, "\n "); + for ( c = 4; c < 8; c++ ) fprintf(stdout," %f", hp->pixdim[c]); + fprintf(stdout, "\n" + " vox_offset = %f\n" + " scl_slope = %f\n" + " scl_inter = %f\n" + " slice_end = %d\n" + " slice_code = %d\n" + " xyzt_units = 0x%x\n" + " cal_max = %f\n" + " cal_min = %f\n" + " slice_duration = %f\n" + " toffset = %f\n" + " glmax = %d\n" + " glmin = %d\n", + hp->vox_offset, hp->scl_slope, hp->scl_inter, hp->slice_end, + hp->slice_code, hp->xyzt_units, hp->cal_max, hp->cal_min, + hp->slice_duration, hp->toffset, hp->glmax, hp->glmin); + fprintf(stdout, + " descrip = '%.80s'\n" + " aux_file = '%.24s'\n" + " qform_code = %d\n" + " sform_code = %d\n" + " quatern_b = %f\n" + " quatern_c = %f\n" + " quatern_d = %f\n" + " qoffset_x = %f\n" + " qoffset_y = %f\n" + " qoffset_z = %f\n" + " srow_x[4] = %f, %f, %f, %f\n" + " srow_y[4] = %f, %f, %f, %f\n" + " srow_z[4] = %f, %f, %f, %f\n" + " intent_name = '%-.16s'\n" + " magic = '%-.4s'\n", + hp->descrip, hp->aux_file, hp->qform_code, hp->sform_code, + hp->quatern_b, hp->quatern_c, hp->quatern_d, + hp->qoffset_x, hp->qoffset_y, hp->qoffset_z, + hp->srow_x[0], hp->srow_x[1], hp->srow_x[2], hp->srow_x[3], + hp->srow_y[0], hp->srow_y[1], hp->srow_y[2], hp->srow_y[3], + hp->srow_z[0], hp->srow_z[1], hp->srow_z[2], hp->srow_z[3], + hp->intent_name, hp->magic); + fputs( "-------------------------------------------------------\n", stdout ); + fflush(stdout); + + return 0; +} + + +#undef ERREX +#define ERREX(msg) \ + do{ fprintf(stderr,"** ERROR: nifti_convert_nhdr2nim: %s\n", (msg) ) ; \ + return NULL ; } while(0) + +/*----------------------------------------------------------------------*/ +/*! convert a nifti_1_header into a nift1_image + + \return an allocated nifti_image, or NULL on failure +*//*--------------------------------------------------------------------*/ +nifti_image *nifti_convert_nhdr2nim(struct nifti_1_header nhdr, + const char * fname) +{ + int ii , doswap , ioff ; + int is_nifti , is_onefile ; + nifti_image *nim; + + nim = (nifti_image *)calloc( 1 , sizeof(nifti_image) ) ; + if( !nim ) ERREX("failed to allocate nifti image"); + + /* be explicit with pointers */ + nim->fname = NULL; + nim->iname = NULL; + nim->data = NULL; + + /**- check if we must swap bytes */ + + doswap = need_nhdr_swap(nhdr.dim[0], nhdr.sizeof_hdr); /* swap data flag */ + + if( doswap < 0 ){ + if( doswap == -1 ) ERREX("bad dim[0]") ; + ERREX("bad sizeof_hdr") ; /* else */ + } + + /**- determine if this is a NIFTI-1 compliant header */ + + is_nifti = NIFTI_VERSION(nhdr) ; + /* + * before swapping header, record the Analyze75 orient code + */ + if(!is_nifti) + { + /**- in analyze75, the orient code is at the same address as + * qform_code, but it's just one byte + * the qform_code will be zero, at which point you can check + * analyze75_orient if you care to. + */ + unsigned char c = *((char *)(&nhdr.qform_code)); + nim->analyze75_orient = (analyze_75_orient_code)c; + } + if( doswap ) { + if ( g_opts.debug > 3 ) disp_nifti_1_header("-d ni1 pre-swap: ", &nhdr); + swap_nifti_header( &nhdr , is_nifti ) ; + } + + if ( g_opts.debug > 2 ) disp_nifti_1_header("-d nhdr2nim : ", &nhdr); + + if( nhdr.datatype == DT_BINARY || + nhdr.datatype == DT_UNKNOWN ) ERREX("bad datatype") ; + + if( nhdr.dim[1] <= 0 ) ERREX("bad dim[1]") ; + + /* fix bad dim[] values in the defined dimension range */ + for( ii=2 ; ii <= nhdr.dim[0] ; ii++ ) + if( nhdr.dim[ii] <= 0 ) nhdr.dim[ii] = 1 ; + + /* fix any remaining bad dim[] values, so garbage does not propagate */ + /* (only values 0 or 1 seem rational, otherwise set to arbirary 1) */ + for( ii=nhdr.dim[0]+1 ; ii <= 7 ; ii++ ) + if( nhdr.dim[ii] != 1 && nhdr.dim[ii] != 0) nhdr.dim[ii] = 1 ; + +#if 0 /* rely on dim[0], do not attempt to modify it 16 Nov 2005 [rickr] */ + + /**- get number of dimensions (ignoring dim[0] now) */ + for( ii=7 ; ii >= 2 ; ii-- ) /* loop backwards until we */ + if( nhdr.dim[ii] > 1 ) break ; /* find a dim bigger than 1 */ + ndim = ii ; +#endif + + /**- set bad grid spacings to 1.0 */ + + for( ii=1 ; ii <= nhdr.dim[0] ; ii++ ){ + if( nhdr.pixdim[ii] == 0.0 || + !IS_GOOD_FLOAT(nhdr.pixdim[ii]) ) nhdr.pixdim[ii] = 1.0 ; + } + + is_onefile = is_nifti && NIFTI_ONEFILE(nhdr) ; + + if( is_nifti ) nim->nifti_type = (is_onefile) ? NIFTI_FTYPE_NIFTI1_1 + : NIFTI_FTYPE_NIFTI1_2 ; + else nim->nifti_type = NIFTI_FTYPE_ANALYZE ; + + ii = nifti_short_order() ; + if( doswap ) nim->byteorder = REVERSE_ORDER(ii) ; + else nim->byteorder = ii ; + + + /**- set dimensions of data array */ + + nim->ndim = nim->dim[0] = nhdr.dim[0]; + nim->nx = nim->dim[1] = nhdr.dim[1]; + nim->ny = nim->dim[2] = nhdr.dim[2]; + nim->nz = nim->dim[3] = nhdr.dim[3]; + nim->nt = nim->dim[4] = nhdr.dim[4]; + nim->nu = nim->dim[5] = nhdr.dim[5]; + nim->nv = nim->dim[6] = nhdr.dim[6]; + nim->nw = nim->dim[7] = nhdr.dim[7]; + + for( ii=1, nim->nvox=1; ii <= nhdr.dim[0]; ii++ ) + nim->nvox *= nhdr.dim[ii]; + + /**- set the type of data in voxels and how many bytes per voxel */ + + nim->datatype = nhdr.datatype ; + + nifti_datatype_sizes( nim->datatype , &(nim->nbyper) , &(nim->swapsize) ) ; + if( nim->nbyper == 0 ){ free(nim); ERREX("bad datatype"); } + + /**- set the grid spacings */ + + nim->dx = nim->pixdim[1] = nhdr.pixdim[1] ; + nim->dy = nim->pixdim[2] = nhdr.pixdim[2] ; + nim->dz = nim->pixdim[3] = nhdr.pixdim[3] ; + nim->dt = nim->pixdim[4] = nhdr.pixdim[4] ; + nim->du = nim->pixdim[5] = nhdr.pixdim[5] ; + nim->dv = nim->pixdim[6] = nhdr.pixdim[6] ; + nim->dw = nim->pixdim[7] = nhdr.pixdim[7] ; + + /**- compute qto_xyz transformation from pixel indexes (i,j,k) to (x,y,z) */ + + if( !is_nifti || nhdr.qform_code <= 0 ){ + /**- if not nifti or qform_code <= 0, use grid spacing for qto_xyz */ + + nim->qto_xyz.m[0][0] = nim->dx ; /* grid spacings */ + nim->qto_xyz.m[1][1] = nim->dy ; /* along diagonal */ + nim->qto_xyz.m[2][2] = nim->dz ; + + /* off diagonal is zero */ + + nim->qto_xyz.m[0][1]=nim->qto_xyz.m[0][2]=nim->qto_xyz.m[0][3] = 0.0; + nim->qto_xyz.m[1][0]=nim->qto_xyz.m[1][2]=nim->qto_xyz.m[1][3] = 0.0; + nim->qto_xyz.m[2][0]=nim->qto_xyz.m[2][1]=nim->qto_xyz.m[2][3] = 0.0; + + /* last row is always [ 0 0 0 1 ] */ + + nim->qto_xyz.m[3][0]=nim->qto_xyz.m[3][1]=nim->qto_xyz.m[3][2] = 0.0; + nim->qto_xyz.m[3][3]= 1.0 ; + + nim->qform_code = NIFTI_XFORM_UNKNOWN ; + + if( g_opts.debug > 1 ) fprintf(stderr,"-d no qform provided\n"); + } else { + /**- else NIFTI: use the quaternion-specified transformation */ + + nim->quatern_b = FIXED_FLOAT( nhdr.quatern_b ) ; + nim->quatern_c = FIXED_FLOAT( nhdr.quatern_c ) ; + nim->quatern_d = FIXED_FLOAT( nhdr.quatern_d ) ; + + nim->qoffset_x = FIXED_FLOAT(nhdr.qoffset_x) ; + nim->qoffset_y = FIXED_FLOAT(nhdr.qoffset_y) ; + nim->qoffset_z = FIXED_FLOAT(nhdr.qoffset_z) ; + + nim->qfac = (nhdr.pixdim[0] < 0.0) ? -1.0 : 1.0 ; /* left-handedness? */ + + nim->qto_xyz = nifti_quatern_to_mat44( + nim->quatern_b, nim->quatern_c, nim->quatern_d, + nim->qoffset_x, nim->qoffset_y, nim->qoffset_z, + nim->dx , nim->dy , nim->dz , + nim->qfac ) ; + + nim->qform_code = nhdr.qform_code ; + + if( g_opts.debug > 1 ) + nifti_disp_matrix_orient("-d qform orientations:\n", nim->qto_xyz); + } + + /**- load inverse transformation (x,y,z) -> (i,j,k) */ + + nim->qto_ijk = nifti_mat44_inverse( nim->qto_xyz ) ; + + /**- load sto_xyz affine transformation, if present */ + + if( !is_nifti || nhdr.sform_code <= 0 ){ + /**- if not nifti or sform_code <= 0, then no sto transformation */ + + nim->sform_code = NIFTI_XFORM_UNKNOWN ; + + if( g_opts.debug > 1 ) fprintf(stderr,"-d no sform provided\n"); + + } else { + /**- else set the sto transformation from srow_*[] */ + + nim->sto_xyz.m[0][0] = nhdr.srow_x[0] ; + nim->sto_xyz.m[0][1] = nhdr.srow_x[1] ; + nim->sto_xyz.m[0][2] = nhdr.srow_x[2] ; + nim->sto_xyz.m[0][3] = nhdr.srow_x[3] ; + + nim->sto_xyz.m[1][0] = nhdr.srow_y[0] ; + nim->sto_xyz.m[1][1] = nhdr.srow_y[1] ; + nim->sto_xyz.m[1][2] = nhdr.srow_y[2] ; + nim->sto_xyz.m[1][3] = nhdr.srow_y[3] ; + + nim->sto_xyz.m[2][0] = nhdr.srow_z[0] ; + nim->sto_xyz.m[2][1] = nhdr.srow_z[1] ; + nim->sto_xyz.m[2][2] = nhdr.srow_z[2] ; + nim->sto_xyz.m[2][3] = nhdr.srow_z[3] ; + + /* last row is always [ 0 0 0 1 ] */ + + nim->sto_xyz.m[3][0]=nim->sto_xyz.m[3][1]=nim->sto_xyz.m[3][2] = 0.0; + nim->sto_xyz.m[3][3]= 1.0 ; + + nim->sto_ijk = nifti_mat44_inverse( nim->sto_xyz ) ; + + nim->sform_code = nhdr.sform_code ; + + if( g_opts.debug > 1 ) + nifti_disp_matrix_orient("-d sform orientations:\n", nim->sto_xyz); + } + + /**- set miscellaneous NIFTI stuff */ + + if( is_nifti ){ + nim->scl_slope = FIXED_FLOAT( nhdr.scl_slope ) ; + nim->scl_inter = FIXED_FLOAT( nhdr.scl_inter ) ; + + nim->intent_code = nhdr.intent_code ; + + nim->intent_p1 = FIXED_FLOAT( nhdr.intent_p1 ) ; + nim->intent_p2 = FIXED_FLOAT( nhdr.intent_p2 ) ; + nim->intent_p3 = FIXED_FLOAT( nhdr.intent_p3 ) ; + + nim->toffset = FIXED_FLOAT( nhdr.toffset ) ; + + memcpy(nim->intent_name,nhdr.intent_name,15); nim->intent_name[15] = '\0'; + + nim->xyz_units = XYZT_TO_SPACE(nhdr.xyzt_units) ; + nim->time_units = XYZT_TO_TIME (nhdr.xyzt_units) ; + + nim->freq_dim = DIM_INFO_TO_FREQ_DIM ( nhdr.dim_info ) ; + nim->phase_dim = DIM_INFO_TO_PHASE_DIM( nhdr.dim_info ) ; + nim->slice_dim = DIM_INFO_TO_SLICE_DIM( nhdr.dim_info ) ; + + nim->slice_code = nhdr.slice_code ; + nim->slice_start = nhdr.slice_start ; + nim->slice_end = nhdr.slice_end ; + nim->slice_duration = FIXED_FLOAT(nhdr.slice_duration) ; + } + + /**- set Miscellaneous ANALYZE stuff */ + + nim->cal_min = FIXED_FLOAT(nhdr.cal_min) ; + nim->cal_max = FIXED_FLOAT(nhdr.cal_max) ; + + memcpy(nim->descrip ,nhdr.descrip ,79) ; nim->descrip [79] = '\0' ; + memcpy(nim->aux_file,nhdr.aux_file,23) ; nim->aux_file[23] = '\0' ; + + /**- set ioff from vox_offset (but at least sizeof(header)) */ + + is_onefile = is_nifti && NIFTI_ONEFILE(nhdr) ; + + if( is_onefile ){ + ioff = (int)nhdr.vox_offset ; + if( ioff < (int) sizeof(nhdr) ) ioff = (int) sizeof(nhdr) ; + } else { + ioff = (int)nhdr.vox_offset ; + } + nim->iname_offset = ioff ; + + + /**- deal with file names if set */ + if (fname!=NULL) { + nifti_set_filenames(nim,fname,0,0); + if (nim->iname==NULL) { ERREX("bad filename"); } + } else { + nim->fname = NULL; + nim->iname = NULL; + } + + /* clear extension fields */ + nim->num_ext = 0; + nim->ext_list = NULL; + + return nim; +} + +#undef ERREX +#define ERREX(msg) \ + do{ fprintf(stderr,"** ERROR: nifti_image_open(%s): %s\n", \ + (hname != NULL) ? hname : "(null)" , (msg) ) ; \ + return fptr ; } while(0) + +/*************************************************************** + * nifti_image_open + ***************************************************************/ +/*! znzFile nifti_image_open( char *hname, char *opts , nifti_image **nim) + \brief Read in NIFTI-1 or ANALYZE-7.5 file (pair) header information into a nifti_image struct. + + - The image data is not read from disk (it may be read later using + nifti_image_load(), for example). + - The image data will be stored in whatever data format the + input data is; no scaling will be applied. + - DT_BINARY data is not supported. + - nifti_image_free() can be used to delete the returned struct, + when you are done with it. + + \param hname filename of dataset .hdr or .nii file + \param opts options string for opening the header file + \param nim pointer to pointer to nifti_image struct + (this routine allocates the nifti_image struct) + \return file pointer (gzippable) to the file with the image data, + ready for reading. +
NULL if something fails badly. + \sa nifti_image_load, nifti_image_free + */ +znzFile nifti_image_open(const char * hname, char * opts, nifti_image ** nim) +{ + znzFile fptr=NULL; + /* open the hdr and reading it in, but do not load the data */ + *nim = nifti_image_read(hname,0); + /* open the image file, ready for reading (compressed works for all reads) */ + if( ((*nim) == NULL) || ((*nim)->iname == NULL) || + ((*nim)->nbyper <= 0) || ((*nim)->nvox <= 0) ) + ERREX("bad header info") ; + + /* open image data file */ + fptr = znzopen( (*nim)->iname, opts, nifti_is_gzfile((*nim)->iname) ); + if( znz_isnull(fptr) ) ERREX("Can't open data file") ; + + return fptr; +} + + +/*----------------------------------------------------------------------*/ +/*! return an allocated and filled nifti_1_header struct + + Read the binary header from disk, and swap bytes if necessary. + + \return an allocated nifti_1_header struct, or NULL on failure + + \param hname name of file containing header + \param swapped if not NULL, return whether header bytes were swapped + \param check flag to check for invalid nifti_1_header + + \warning ASCII header type is not supported + + \sa nifti_image_read, nifti_image_free, nifti_image_read_bricks +*//*--------------------------------------------------------------------*/ +nifti_1_header * nifti_read_header(const char * hname, int * swapped, int check) +{ + nifti_1_header nhdr, * hptr; + znzFile fp; + int bytes, lswap; + char * hfile; + char fname[] = { "nifti_read_header" }; + + /* determine file name to use for header */ + hfile = nifti_findhdrname(hname); + if( hfile == NULL ){ + if( g_opts.debug > 0 ) + LNI_FERR(fname,"failed to find header file for", hname); + return NULL; + } else if( g_opts.debug > 1 ) + fprintf(stderr,"-d %s: found header filename '%s'\n",fname,hfile); + + fp = znzopen( hfile, "rb", nifti_is_gzfile(hfile) ); + if( znz_isnull(fp) ){ + if( g_opts.debug > 0 ) LNI_FERR(fname,"failed to open header file",hfile); + free(hfile); + return NULL; + } + + free(hfile); /* done with filename */ + + if( has_ascii_header(fp) == 1 ){ + znzclose( fp ); + if( g_opts.debug > 0 ) + LNI_FERR(fname,"ASCII header type not supported",hname); + return NULL; + } + + /* read the binary header */ + bytes = (int)znzread( &nhdr, 1, sizeof(nhdr), fp ); + znzclose( fp ); /* we are done with the file now */ + + if( bytes < (int)sizeof(nhdr) ){ + if( g_opts.debug > 0 ){ + LNI_FERR(fname,"bad binary header read for file", hname); + fprintf(stderr," - read %d of %d bytes\n",bytes, (int)sizeof(nhdr)); + } + return NULL; + } + + /* now just decide on byte swapping */ + lswap = need_nhdr_swap(nhdr.dim[0], nhdr.sizeof_hdr); /* swap data flag */ + if( check && lswap < 0 ){ + LNI_FERR(fname,"bad nifti_1_header for file", hname); + return NULL; + } else if ( lswap < 0 ) { + lswap = 0; /* if swapping does not help, don't do it */ + if(g_opts.debug > 1) fprintf(stderr,"-- swap failure, none applied\n"); + } + + if( lswap ) { + if ( g_opts.debug > 3 ) disp_nifti_1_header("-d nhdr pre-swap: ", &nhdr); + swap_nifti_header( &nhdr , NIFTI_VERSION(nhdr) ) ; + } + + if ( g_opts.debug > 2 ) disp_nifti_1_header("-d nhdr post-swap: ", &nhdr); + + if ( check && ! nifti_hdr_looks_good(&nhdr) ){ + LNI_FERR(fname,"nifti_1_header looks bad for file", hname); + return NULL; + } + + /* all looks good, so allocate memory for and return the header */ + hptr = (nifti_1_header *)malloc(sizeof(nifti_1_header)); + if( ! hptr ){ + fprintf(stderr,"** nifti_read_hdr: failed to alloc nifti_1_header\n"); + return NULL; + } + + if( swapped ) *swapped = lswap; /* only if they care */ + + memcpy(hptr, &nhdr, sizeof(nifti_1_header)); + + return hptr; +} + + +/*----------------------------------------------------------------------*/ +/*! decide if this nifti_1_header structure looks reasonable + + Check dim[0], dim[1], sizeof_hdr, and datatype. + Check magic string for "n+1". + Maybe more tests will follow. + + \return 1 if the header seems valid, 0 otherwise + + \sa nifti_nim_is_valid, valid_nifti_extensions +*//*--------------------------------------------------------------------*/ +int nifti_hdr_looks_good(const nifti_1_header * hdr) +{ + int is_nifti, c, errs = 0; + + /* check dim[0] and sizeof_hdr */ + if( need_nhdr_swap(hdr->dim[0], hdr->sizeof_hdr) < 0 ){ + if( g_opts.debug > 0 ) + fprintf(stderr,"** bad nhdr fields: dim0, sizeof_hdr = %d, %d\n", + hdr->dim[0], hdr->sizeof_hdr); + errs++; + } + + /* check the valid dimension sizes (maybe dim[0] is bad) */ + for( c = 1; c <= hdr->dim[0] && c <= 7; c++ ) + if( hdr->dim[c] <= 0 ){ + if( g_opts.debug > 0 ) + fprintf(stderr,"** bad nhdr field: dim[%d] = %d\n",c,hdr->dim[c]); + errs++; + } + + is_nifti = NIFTI_VERSION(*hdr); /* determine header type */ + + if( is_nifti ){ /* NIFTI */ + + if( ! nifti_datatype_is_valid(hdr->datatype, 1) ){ + if( g_opts.debug > 0 ) + fprintf(stderr,"** bad NIFTI datatype in hdr, %d\n",hdr->datatype); + errs++; + } + + } else { /* ANALYZE 7.5 */ + + if( g_opts.debug > 1 ) /* maybe tell user it's an ANALYZE hdr */ + fprintf(stderr, + "-- nhdr magic field implies ANALYZE: magic = '%.4s'\n",hdr->magic); + + if( ! nifti_datatype_is_valid(hdr->datatype, 0) ){ + if( g_opts.debug > 0 ) + fprintf(stderr,"** bad ANALYZE datatype in hdr, %d\n",hdr->datatype); + errs++; + } + } + + if( errs ) return 0; /* problems */ + + if( g_opts.debug > 2 ) fprintf(stderr,"-d nifti header looks good\n"); + + return 1; /* looks good */ +} + + +/*---------------------------------------------------------------------- + * check whether byte swapping is needed + * + * dim[0] should be in [0,7], and sizeof_hdr should be accurate + * + * \returns > 0 : needs swap + * 0 : does not need swap + * < 0 : error condition + *----------------------------------------------------------------------*/ +static int need_nhdr_swap( short dim0, int hdrsize ) +{ + short d0 = dim0; /* so we won't have to swap them on the stack */ + int hsize = hdrsize; + + if( d0 != 0 ){ /* then use it for the check */ + if( d0 > 0 && d0 <= 7 ) return 0; + + nifti_swap_2bytes(1, &d0); /* swap? */ + if( d0 > 0 && d0 <= 7 ) return 1; + + if( g_opts.debug > 1 ){ + fprintf(stderr,"** NIFTI: bad swapped d0 = %d, unswapped = ", d0); + nifti_swap_2bytes(1, &d0); /* swap? */ + fprintf(stderr,"%d\n", d0); + } + + return -1; /* bad, naughty d0 */ + } + + /* dim[0] == 0 should not happen, but could, so try hdrsize */ + if( hsize == sizeof(nifti_1_header) ) return 0; + + nifti_swap_4bytes(1, &hsize); /* swap? */ + if( hsize == sizeof(nifti_1_header) ) return 1; + + if( g_opts.debug > 1 ){ + fprintf(stderr,"** NIFTI: bad swapped hsize = %d, unswapped = ", hsize); + nifti_swap_4bytes(1, &hsize); /* swap? */ + fprintf(stderr,"%d\n", hsize); + } + + return -2; /* bad, naughty hsize */ +} + + +/* use macro LNI_FILE_ERROR instead of ERREX() +#undef ERREX +#define ERREX(msg) \ + do{ fprintf(stderr,"** ERROR: nifti_image_read(%s): %s\n", \ + (hname != NULL) ? hname : "(null)" , (msg) ) ; \ + return NULL ; } while(0) +*/ + + +/*************************************************************** + * nifti_image_read + ***************************************************************/ +/*! \brief Read a nifti header and optionally the data, creating a nifti_image. + + - The data buffer will be byteswapped if necessary. + - The data buffer will not be scaled. + - The data buffer is allocated with calloc(). + + \param hname filename of the nifti dataset + \param read_data Flag, true=read data blob, false=don't read blob. + \return A pointer to the nifti_image data structure. + + \sa nifti_image_free, nifti_free_extensions, nifti_image_read_bricks +*/ +nifti_image *nifti_image_read( const char *hname , int read_data ) +{ + struct nifti_1_header nhdr ; + nifti_image *nim ; + znzFile fp ; + int rv, ii , filesize, remaining; + char fname[] = { "nifti_image_read" }; + char *hfile=NULL; + + if( g_opts.debug > 1 ){ + fprintf(stderr,"-d image_read from '%s', read_data = %d",hname,read_data); +#ifdef HAVE_ZLIB + fprintf(stderr,", HAVE_ZLIB = 1\n"); +#else + fprintf(stderr,", HAVE_ZLIB = 0\n"); +#endif + } + + /**- determine filename to use for header */ + hfile = nifti_findhdrname(hname); + if( hfile == NULL ){ + if(g_opts.debug > 0) + LNI_FERR(fname,"failed to find header file for", hname); + return NULL; /* check return */ + } else if( g_opts.debug > 1 ) + fprintf(stderr,"-d %s: found header filename '%s'\n",fname,hfile); + + if( nifti_is_gzfile(hfile) ) filesize = -1; /* unknown */ + else filesize = nifti_get_filesize(hfile); + + fp = znzopen(hfile, "rb", nifti_is_gzfile(hfile)); + if( znz_isnull(fp) ){ + if( g_opts.debug > 0 ) LNI_FERR(fname,"failed to open header file",hfile); + free(hfile); + return NULL; + } + + rv = has_ascii_header( fp ); + if( rv < 0 ){ + if( g_opts.debug > 0 ) LNI_FERR(fname,"short header read",hfile); + znzclose( fp ); + free(hfile); + return NULL; + } + else if ( rv == 1 ) /* process special file type */ + return nifti_read_ascii_image( fp, hfile, filesize, read_data ); + + /* else, just process normally */ + + /**- read binary header */ + + ii = (int)znzread( &nhdr , 1 , sizeof(nhdr) , fp ) ; /* read the thing */ + + /* keep file open so we can check for exts. after nifti_convert_nhdr2nim() */ + + if( ii < (int) sizeof(nhdr) ){ + if( g_opts.debug > 0 ){ + LNI_FERR(fname,"bad binary header read for file", hfile); + fprintf(stderr," - read %d of %d bytes\n",ii, (int)sizeof(nhdr)); + } + znzclose(fp) ; + free(hfile); + return NULL; + } + + /* create output image struct and set it up */ + + /**- convert all nhdr fields to nifti_image fields */ + nim = nifti_convert_nhdr2nim(nhdr,hfile); + + if( nim == NULL ){ + znzclose( fp ) ; /* close the file */ + if( g_opts.debug > 0 ) + LNI_FERR(fname,"cannot create nifti image from header",hfile); + free(hfile); /* had to save this for debug message */ + return NULL; + } + + if( g_opts.debug > 3 ){ + fprintf(stderr,"+d nifti_image_read(), have nifti image:\n"); + if( g_opts.debug > 2 ) nifti_image_infodump(nim); + } + + /**- check for extensions (any errors here means no extensions) */ + if( NIFTI_ONEFILE(nhdr) ) remaining = nim->iname_offset - sizeof(nhdr); + else remaining = filesize - sizeof(nhdr); + + (void)nifti_read_extensions(nim, fp, remaining); + + znzclose( fp ) ; /* close the file */ + free(hfile); + + /**- read the data if desired, then bug out */ + if( read_data ){ + if( nifti_image_load( nim ) < 0 ){ + nifti_image_free(nim); /* take ball, go home. */ + return NULL; + } + } + else nim->data = NULL ; + + return nim ; +} + + +/*---------------------------------------------------------------------- + * has_ascii_header - see if the NIFTI header is an ASCII format + * + * If the file starts with the ASCII string " 1 ) + fprintf(stderr,"-d %s: have ASCII NIFTI file of size %d\n",fname,slen); + + if( slen > 65530 ) slen = 65530 ; + sbuf = (char *)calloc(sizeof(char),slen+1) ; + if( !sbuf ){ + fprintf(stderr,"** %s: failed to alloc %d bytes for sbuf",lfunc,65530); + free(fname); znzclose(fp); return NULL; + } + znzread( sbuf , 1 , slen , fp ) ; + nim = nifti_image_from_ascii( sbuf, &txt_size ) ; free( sbuf ) ; + if( nim == NULL ){ + LNI_FERR(lfunc,"failed nifti_image_from_ascii()",fname); + free(fname); znzclose(fp); return NULL; + } + nim->nifti_type = NIFTI_FTYPE_ASCII ; + + /* compute remaining space for extensions */ + remain = flen - txt_size - (int)nifti_get_volsize(nim); + if( remain > 4 ){ + /* read extensions (reposition file pointer, first) */ + znzseek(fp, txt_size, SEEK_SET); + (void) nifti_read_extensions(nim, fp, remain); + } + + free(fname); + znzclose( fp ) ; + + nim->iname_offset = -1 ; /* check from the end of the file */ + + if( read_data ) rv = nifti_image_load( nim ) ; + else nim->data = NULL ; + + /* check for nifti_image_load() failure, maybe bail out */ + if( read_data && rv != 0 ){ + if( g_opts.debug > 1 ) + fprintf(stderr,"-d failed image_load, free nifti image struct\n"); + free(nim); + return NULL; + } + + return nim ; +} + + +/*---------------------------------------------------------------------- + * Read the extensions into the nifti_image struct 08 Dec 2004 [rickr] + * + * This function is called just after the header struct is read in, and + * it is assumed the file pointer has not moved. The value in remain + * is assumed to be accurate, reflecting the bytes of space for potential + * extensions. + * + * return the number of extensions read in, or < 0 on error + *----------------------------------------------------------------------*/ +static int nifti_read_extensions( nifti_image *nim, znzFile fp, int remain ) +{ + nifti1_extender extdr; /* defines extension existence */ + nifti1_extension extn; /* single extension to process */ + nifti1_extension * Elist; /* list of processed extensions */ + int posn, count; + + if( !nim || znz_isnull(fp) ) { + if( g_opts.debug > 0 ) + fprintf(stderr,"** nifti_read_extensions: bad inputs (%p,%p)\n", + (void *)nim, (void *)fp); + return -1; + } + + posn = znztell(fp); + + if( (posn != sizeof(nifti_1_header)) && + (nim->nifti_type != NIFTI_FTYPE_ASCII) ) + fprintf(stderr,"** WARNING: posn not header size (%d, %d)\n", + posn, (int)sizeof(nifti_1_header)); + + if( g_opts.debug > 2 ) + fprintf(stderr,"-d nre: posn = %d, offset = %d, type = %d, remain = %d\n", + posn, nim->iname_offset, nim->nifti_type, remain); + + if( remain < 16 ){ + if( g_opts.debug > 2 ){ + if( g_opts.skip_blank_ext ) + fprintf(stderr,"-d no extender in '%s' is okay, as " + "skip_blank_ext is set\n",nim->fname); + else + fprintf(stderr,"-d remain=%d, no space for extensions\n",remain); + } + return 0; + } + + count = (int)znzread( extdr.extension, 1, 4, fp ); /* get extender */ + + if( count < 4 ){ + if( g_opts.debug > 1 ) + fprintf(stderr,"-d file '%s' is too short for an extender\n", + nim->fname); + return 0; + } + + if( extdr.extension[0] != 1 ){ + if( g_opts.debug > 2 ) + fprintf(stderr,"-d extender[0] (%d) shows no extensions for '%s'\n", + extdr.extension[0], nim->fname); + return 0; + } + + remain -= 4; + if( g_opts.debug > 2 ) + fprintf(stderr,"-d found valid 4-byte extender, remain = %d\n", remain); + + /* so we expect extensions, but have no idea of how many there may be */ + + count = 0; + Elist = NULL; + while (nifti_read_next_extension(&extn, nim, remain, fp) > 0) + { + if( nifti_add_exten_to_list(&extn, &Elist, count+1) < 0 ){ + if( g_opts.debug > 0 ) + fprintf(stderr,"** failed adding ext %d to list\n", count); + return -1; + } + + /* we have a new extension */ + if( g_opts.debug > 1 ){ + fprintf(stderr,"+d found extension #%d, code = 0x%x, size = %d\n", + count, extn.ecode, extn.esize); + if( extn.ecode == NIFTI_ECODE_AFNI && g_opts.debug > 2 ) /* ~XML */ + fprintf(stderr," AFNI extension: %.*s\n", + extn.esize-8,extn.edata); + else if( extn.ecode == NIFTI_ECODE_COMMENT && g_opts.debug > 2 ) + fprintf(stderr," COMMENT extension: %.*s\n", /* TEXT */ + extn.esize-8,extn.edata); + } + remain -= extn.esize; + count++; + } + + if( g_opts.debug > 2 ) fprintf(stderr,"+d found %d extension(s)\n", count); + + nim->num_ext = count; + nim->ext_list = Elist; + + return count; +} + + +/*----------------------------------------------------------------------*/ +/*! nifti_add_extension - add an extension, with a copy of the data + + Add an extension to the nim->ext_list array. + Fill this extension with a copy of the data, noting the + length and extension code. + + \param nim - nifti_image to add extension to + \param data - raw extension data + \param length - length of raw extension data + \param ecode - extension code + + \sa extension codes NIFTI_ECODE_* in nifti1_io.h + \sa nifti_free_extensions, valid_nifti_extensions, nifti_copy_extensions + + \return 0 on success, -1 on error (and free the entire list) +*//*--------------------------------------------------------------------*/ +int nifti_add_extension(nifti_image *nim, const char * data, int len, int ecode) +{ + nifti1_extension ext; + + /* error are printed in functions */ + if( nifti_fill_extension(&ext, data, len, ecode) ) return -1; + if( nifti_add_exten_to_list(&ext, &nim->ext_list, nim->num_ext+1)) return -1; + + nim->num_ext++; /* success, so increment */ + + return 0; +} + + +/*----------------------------------------------------------------------*/ +/* nifti_add_exten_to_list - add a new nifti1_extension to the list + + We will append via "malloc, copy and free", because on an error, + the list will revert to the previous one (sorry realloc(), only + quality dolphins get to become part of St@rk!st brand tunafish). + + return 0 on success, -1 on error (and free the entire list) +*//*--------------------------------------------------------------------*/ +static int nifti_add_exten_to_list( nifti1_extension * new_ext, + nifti1_extension ** list, int new_length ) +{ + nifti1_extension * tmplist; + + tmplist = *list; + *list = (nifti1_extension *)malloc(new_length * sizeof(nifti1_extension)); + + /* check for failure first */ + if( ! *list ){ + fprintf(stderr,"** failed to alloc %d extension structs (%d bytes)\n", + new_length, new_length*(int)sizeof(nifti1_extension)); + if( !tmplist ) return -1; /* no old list to lose */ + + *list = tmplist; /* reset list to old one */ + return -1; + } + + /* if an old list exists, copy the pointers and free the list */ + if( tmplist ){ + memcpy(*list, tmplist, (new_length-1)*sizeof(nifti1_extension)); + free(tmplist); + } + + /* for some reason, I just don't like struct copy... */ + (*list)[new_length-1].esize = new_ext->esize; + (*list)[new_length-1].ecode = new_ext->ecode; + (*list)[new_length-1].edata = new_ext->edata; + + if( g_opts.debug > 2 ) + fprintf(stderr,"+d allocated and appended extension #%d to list\n", + new_length); + + return 0; +} + + +/*----------------------------------------------------------------------*/ +/* nifti_fill_extension - given data and length, fill an extension struct + + Allocate memory for data, copy data, set the size and code. + + return 0 on success, -1 on error (and free the entire list) +*//*--------------------------------------------------------------------*/ +static int nifti_fill_extension( nifti1_extension *ext, const char * data, + int len, int ecode) +{ + int esize; + + if( !ext || !data || len < 0 ){ + fprintf(stderr,"** fill_ext: bad params (%p,%p,%d)\n", + (void *)ext, data, len); + return -1; + } else if( ! nifti_is_valid_ecode(ecode) ){ + fprintf(stderr,"** fill_ext: invalid ecode %d\n", ecode); + return -1; + } + + /* compute esize, first : len+8, and take ceiling up to a mult of 16 */ + esize = len+8; + if( esize & 0xf ) esize = (esize + 0xf) & ~0xf; + ext->esize = esize; + + /* allocate esize-8 (maybe more than len), using calloc for fill */ + ext->edata = (char *)calloc(esize-8, sizeof(char)); + if( !ext->edata ){ + fprintf(stderr,"** NFE: failed to alloc %d bytes for extension\n",len); + return -1; + } + + memcpy(ext->edata, data, len); /* copy the data, using len */ + ext->ecode = ecode; /* set the ecode */ + + if( g_opts.debug > 2 ) + fprintf(stderr,"+d alloc %d bytes for ext len %d, ecode %d, esize %d\n", + esize-8, len, ecode, esize); + + return 0; +} + + +/*---------------------------------------------------------------------- + * nifti_read_next_extension - read a single extension from the file + * + * return (>= 0 is okay): + * + * success : esize + * no extension : 0 + * error : -1 + *----------------------------------------------------------------------*/ +static int nifti_read_next_extension( nifti1_extension * nex, nifti_image *nim, + int remain, znzFile fp ) +{ + int swap = nim->byteorder != nifti_short_order(); + int count, size, code; + + /* first clear nex */ + nex->esize = nex->ecode = 0; + nex->edata = NULL; + + if( remain < 16 ){ + if( g_opts.debug > 2 ) + fprintf(stderr,"-d only %d bytes remain, so no extension\n", remain); + return 0; + } + + /* must start with 4-byte size and code */ + count = (int)znzread( &size, 4, 1, fp ); + if( count == 1 ) count += (int)znzread( &code, 4, 1, fp ); + + if( count != 2 ){ + if( g_opts.debug > 2 ) + fprintf(stderr,"-d current extension read failed\n"); + znzseek(fp, -4*count, SEEK_CUR); /* back up past any read */ + return 0; /* no extension, no error condition */ + } + + if( swap ){ + if( g_opts.debug > 2 ) + fprintf(stderr,"-d pre-swap exts: code %d, size %d\n", code, size); + + nifti_swap_4bytes(1, &size); + nifti_swap_4bytes(1, &code); + } + + if( g_opts.debug > 2 ) + fprintf(stderr,"-d potential extension: code %d, size %d\n", code, size); + + if( !nifti_check_extension(nim, size, code, remain) ){ + if( znzseek(fp, -8, SEEK_CUR) < 0 ){ /* back up past any read */ + fprintf(stderr,"** failure to back out of extension read!\n"); + return -1; + } + return 0; + } + + /* now get the actual data */ + nex->esize = size; + nex->ecode = code; + + size -= 8; /* subtract space for size and code in extension */ + nex->edata = (char *)malloc(size * sizeof(char)); + if( !nex->edata ){ + fprintf(stderr,"** failed to allocate %d bytes for extension\n",size); + return -1; + } + + count = (int)znzread(nex->edata, 1, size, fp); + if( count < size ){ + if( g_opts.debug > 0 ) + fprintf(stderr,"-d read only %d (of %d) bytes for extension\n", + count, size); + free(nex->edata); + nex->edata = NULL; + return -1; + } + + /* success! */ + if( g_opts.debug > 2 ) + fprintf(stderr,"+d successfully read extension, code %d, size %d\n", + nex->ecode, nex->esize); + + return nex->esize; +} + + +/*----------------------------------------------------------------------*/ +/*! for each extension, check code, size and data pointer +*//*--------------------------------------------------------------------*/ +int valid_nifti_extensions(const nifti_image * nim) +{ + nifti1_extension * ext; + int c, errs; + + if( nim->num_ext <= 0 || nim->ext_list == NULL ){ + if( g_opts.debug > 2 ) fprintf(stderr,"-d empty extension list\n"); + return 0; + } + + /* for each extension, check code, size and data pointer */ + ext = nim->ext_list; + errs = 0; + for ( c = 0; c < nim->num_ext; c++ ){ + if( ! nifti_is_valid_ecode(ext->ecode) ) { + if( g_opts.debug > 1 ) + fprintf(stderr,"-d ext %d, invalid code %d\n", c, ext->ecode); + errs++; + } + + if( ext->esize <= 0 ){ + if( g_opts.debug > 1 ) + fprintf(stderr,"-d ext %d, bad size = %d\n", c, ext->esize); + errs++; + } else if( ext->esize & 0xf ){ + if( g_opts.debug > 1 ) + fprintf(stderr,"-d ext %d, size %d not multiple of 16\n", + c, ext->esize); + errs++; + } + + if( ext->edata == NULL ){ + if( g_opts.debug > 1 ) fprintf(stderr,"-d ext %d, missing data\n", c); + errs++; + } + + ext++; + } + + if( errs > 0 ){ + if( g_opts.debug > 0 ) + fprintf(stderr,"-d had %d extension errors, none will be written\n", + errs); + return 0; + } + + /* if we're here, we're good */ + return 1; +} + + +/*----------------------------------------------------------------------*/ +/*! check whether the extension code is valid + + \return 1 if valid, 0 otherwise +*//*--------------------------------------------------------------------*/ +int nifti_is_valid_ecode( int ecode ) +{ + if( ecode < NIFTI_ECODE_IGNORE || /* minimum code number (0) */ + ecode > NIFTI_MAX_ECODE || /* maximum code number */ + ecode & 1 ) /* cannot be odd */ + return 0; + + return 1; +} + + +/*---------------------------------------------------------------------- + * check for valid size and code, as well as can be done + *----------------------------------------------------------------------*/ +static int nifti_check_extension(nifti_image *nim, int size, int code, int rem) +{ + /* check for bad code before bad size */ + if( ! nifti_is_valid_ecode(code) ) { + if( g_opts.debug > 2 ) + fprintf(stderr,"-d invalid extension code %d\n",code); + return 0; + } + + if( size < 16 ){ + if( g_opts.debug > 2 ) + fprintf(stderr,"-d ext size %d, no extension\n",size); + return 0; + } + + if( size > rem ){ + if( g_opts.debug > 2 ) + fprintf(stderr,"-d ext size %d, space %d, no extension\n", size, rem); + return 0; + } + + if( size & 0xf ){ + if( g_opts.debug > 2 ) + fprintf(stderr,"-d nifti extension size %d not multiple of 16\n",size); + return 0; + } + + if( nim->nifti_type == NIFTI_FTYPE_ASCII && size > LNI_MAX_NIA_EXT_LEN ){ + if( g_opts.debug > 2 ) + fprintf(stderr,"-d NVE, bad nifti_type 3 size %d\n", size); + return 0; + } + + return 1; +} + + +/*---------------------------------------------------------------------- + * nifti_image_load_prep - prepare to read data + * + * Check nifti_image fields, open the file and seek to the appropriate + * offset for reading. + * + * return NULL on failure + *----------------------------------------------------------------------*/ +static znzFile nifti_image_load_prep( nifti_image *nim ) +{ + /* set up data space, open data file and seek, then call nifti_read_buffer */ + size_t ntot , ii , ioff; + znzFile fp; + char *tmpimgname; + char fname[] = { "nifti_image_load_prep" }; + + /**- perform sanity checks */ + if( nim == NULL || nim->iname == NULL || + nim->nbyper <= 0 || nim->nvox <= 0 ) + { + if ( g_opts.debug > 0 ){ + if( !nim ) fprintf(stderr,"** ERROR: N_image_load: no nifti image\n"); + else fprintf(stderr,"** ERROR: N_image_load: bad params (%p,%d,%u)\n", + nim->iname, nim->nbyper, (unsigned)nim->nvox); + } + return NULL; + } + + ntot = nifti_get_volsize(nim) ; /* total bytes to read */ + + /**- open image data file */ + + tmpimgname = nifti_findimgname(nim->iname , nim->nifti_type); + if( tmpimgname == NULL ){ + if( g_opts.debug > 0 ) + fprintf(stderr,"** no image file found for '%s'\n",nim->iname); + return NULL; + } + + fp = znzopen(tmpimgname, "rb", nifti_is_gzfile(tmpimgname)); + if (znz_isnull(fp)){ + if(g_opts.debug > 0) LNI_FERR(fname,"cannot open data file",tmpimgname); + free(tmpimgname); + return NULL; /* bad open? */ + } + free(tmpimgname); + + /**- get image offset: a negative offset means to figure from end of file */ + if( nim->iname_offset < 0 ){ + if( nifti_is_gzfile(nim->iname) ){ + if( g_opts.debug > 0 ) + LNI_FERR(fname,"negative offset for compressed file",nim->iname); + znzclose(fp); + return NULL; + } + ii = nifti_get_filesize( nim->iname ) ; + if( ii <= 0 ){ + if( g_opts.debug > 0 ) LNI_FERR(fname,"empty data file",nim->iname); + znzclose(fp); + return NULL; + } + ioff = (ii > ntot) ? ii-ntot : 0 ; + } else { /* non-negative offset */ + ioff = nim->iname_offset ; /* means use it directly */ + } + + /**- seek to the appropriate read position */ + if( znzseek(fp , (long)ioff , SEEK_SET) < 0 ){ + fprintf(stderr,"** could not seek to offset %u in file '%s'\n", + (unsigned)ioff, nim->iname); + znzclose(fp); + return NULL; + } + + /**- and return the File pointer */ + return fp; +} + + +/*---------------------------------------------------------------------- + * nifti_image_load + *----------------------------------------------------------------------*/ +/*! \fn int nifti_image_load( nifti_image *nim ) + \brief Load the image blob into a previously initialized nifti_image. + + - If not yet set, the data buffer is allocated with calloc(). + - The data buffer will be byteswapped if necessary. + - The data buffer will not be scaled. + + This function is used to read the image from disk. It should be used + after a function such as nifti_image_read(), so that the nifti_image + structure is already initialized. + + \param nim pointer to a nifti_image (previously initialized) + \return 0 on success, -1 on failure + \sa nifti_image_read, nifti_image_free, nifti_image_unload +*/ +int nifti_image_load( nifti_image *nim ) +{ + /* set up data space, open data file and seek, then call nifti_read_buffer */ + size_t ntot , ii ; + znzFile fp ; + + /**- open the file and position the FILE pointer */ + fp = nifti_image_load_prep( nim ); + + if( fp == NULL ){ + if( g_opts.debug > 0 ) + fprintf(stderr,"** nifti_image_load, failed load_prep\n"); + return -1; + } + + ntot = nifti_get_volsize(nim); + + /**- if the data pointer is not yet set, get memory space for the image */ + + if( nim->data == NULL ) + { + nim->data = (void *)calloc(1,ntot) ; /* create image memory */ + if( nim->data == NULL ){ + if( g_opts.debug > 0 ) + fprintf(stderr,"** failed to alloc %d bytes for image data\n", + (int)ntot); + znzclose(fp); + return -1; + } + } + + /**- now that everything is set up, do the reading */ + ii = nifti_read_buffer(fp,nim->data,ntot,nim); + if( ii < ntot ){ + znzclose(fp) ; + free(nim->data) ; + nim->data = NULL ; + return -1 ; /* errors were printed in nifti_read_buffer() */ + } + + /**- close the file */ + znzclose( fp ) ; + + return 0 ; +} + + +/* 30 Nov 2004 [rickr] +#undef ERREX +#define ERREX(msg) \ + do{ fprintf(stderr,"** ERROR: nifti_read_buffer: %s\n",(msg)) ; \ + return 0; } while(0) +*/ + +/*----------------------------------------------------------------------*/ +/*! read ntot bytes of data from an open file and byte swaps if necessary + + note that nifti_image is required for information on datatype, bsize + (for any needed byte swapping), etc. + + This function does not allocate memory, so dataptr must be valid. +*//*--------------------------------------------------------------------*/ +size_t nifti_read_buffer(znzFile fp, void* dataptr, size_t ntot, + nifti_image *nim) +{ + size_t ii; + + if( dataptr == NULL ){ + if( g_opts.debug > 0 ) + fprintf(stderr,"** ERROR: nifti_read_buffer: NULL dataptr\n"); + return -1; + } + + ii = znzread( dataptr , 1 , ntot , fp ) ; /* data input */ + + /* if read was short, fail */ + if( ii < ntot ){ + if( g_opts.debug > 0 ) + fprintf(stderr,"++ WARNING: nifti_read_buffer(%s):\n" + " data bytes needed = %u\n" + " data bytes input = %u\n" + " number missing = %u (set to 0)\n", + nim->iname , (unsigned int)ntot , + (unsigned int)ii , (unsigned int)(ntot-ii) ) ; + /* memset( (char *)(dataptr)+ii , 0 , ntot-ii ) ; now failure [rickr] */ + return -1 ; + } + + if( g_opts.debug > 2 ) + fprintf(stderr,"+d nifti_read_buffer: read %u bytes\n", (unsigned)ii); + + /* byte swap array if needed */ + + /* ntot/swapsize might not fit as int, use size_t 6 Jul 2010 [rickr] */ + if( nim->swapsize > 1 && nim->byteorder != nifti_short_order() ) { + if( g_opts.debug > 1 ) + fprintf(stderr,"+d nifti_read_buffer: swapping data bytes...\n"); + nifti_swap_Nbytes( ntot / nim->swapsize, nim->swapsize , dataptr ) ; + } +#ifndef USE_NII_NAN +#ifdef isfinite +{ + /* check input float arrays for goodness, and fix bad floats */ + int fix_count = 0 ; + + switch( nim->datatype ){ + + case NIFTI_TYPE_FLOAT32: + case NIFTI_TYPE_COMPLEX64:{ + register float *far = (float *)dataptr ; register size_t jj,nj ; + nj = ntot / sizeof(float) ; + for( jj=0 ; jj < nj ; jj++ ) /* count fixes 30 Nov 2004 [rickr] */ + if( !IS_GOOD_FLOAT(far[jj]) ){ + far[jj] = 0 ; + fix_count++ ; + } + } + break ; + + case NIFTI_TYPE_FLOAT64: + case NIFTI_TYPE_COMPLEX128:{ + register double *far = (double *)dataptr ; register size_t jj,nj ; + nj = ntot / sizeof(double) ; + for( jj=0 ; jj < nj ; jj++ ) /* count fixes 30 Nov 2004 [rickr] */ + if( !IS_GOOD_FLOAT(far[jj]) ){ + far[jj] = 0 ; + fix_count++ ; + } + } + break ; + + } + + if( g_opts.debug > 1 ) + fprintf(stderr,"+d in image, %d bad floats were set to 0\n", fix_count); +} +#endif +#endif + + return ii; +} + +/*--------------------------------------------------------------------------*/ +/*! Unload the data in a nifti_image struct, but keep the metadata. +*//*------------------------------------------------------------------------*/ +void nifti_image_unload( nifti_image *nim ) +{ + if( nim != NULL && nim->data != NULL ){ + free(nim->data) ; nim->data = NULL ; + } + return ; +} + +/*--------------------------------------------------------------------------*/ +/*! free 'everything' about a nifti_image struct (including the passed struct) + + free (only fields which are not NULL): + - fname and iname + - data + - any ext_list[i].edata + - ext_list + - nim +*//*------------------------------------------------------------------------*/ +void nifti_image_free( nifti_image *nim ) +{ + if( nim == NULL ) return ; + if( nim->fname != NULL ) free(nim->fname) ; + if( nim->iname != NULL ) free(nim->iname) ; + if( nim->data != NULL ) free(nim->data ) ; + (void)nifti_free_extensions( nim ) ; + free(nim) ; return ; +} + + +/*--------------------------------------------------------------------------*/ +/*! free the nifti extensions + + - If any edata pointer is set in the extension list, free() it. + - Free ext_list, if it is set. + - Clear num_ext and ext_list from nim. + + \return 0 on success, -1 on error + + \sa nifti_add_extension, nifti_copy_extensions +*//*------------------------------------------------------------------------*/ +int nifti_free_extensions( nifti_image *nim ) +{ + int c ; + if( nim == NULL ) return -1; + if( nim->num_ext > 0 && nim->ext_list ){ + for( c = 0; c < nim->num_ext; c++ ) + if ( nim->ext_list[c].edata ) free(nim->ext_list[c].edata); + free(nim->ext_list); + } + /* or if it is inconsistent, warn the user (if we are not in quiet mode) */ + else if ( (nim->num_ext > 0 || nim->ext_list != NULL) && (g_opts.debug > 0) ) + fprintf(stderr,"** warning: nifti extension num/ptr mismatch (%d,%p)\n", + nim->num_ext, (void *)nim->ext_list); + + if( g_opts.debug > 2 ) + fprintf(stderr,"+d free'd %d extension(s)\n", nim->num_ext); + + nim->num_ext = 0; + nim->ext_list = NULL; + + return 0; +} + + +/*--------------------------------------------------------------------------*/ +/*! Print to stdout some info about a nifti_image struct. +*//*------------------------------------------------------------------------*/ +void nifti_image_infodump( const nifti_image *nim ) +{ + char *str = nifti_image_to_ascii( nim ) ; + /* stdout -> stderr 2 Dec 2004 [rickr] */ + if( str != NULL ){ fputs(str,stderr) ; free(str) ; } + return ; +} + + +/*-------------------------------------------------------------------------- + * nifti_write_buffer just check for a null znzFile and call znzwrite + *--------------------------------------------------------------------------*/ +/*! \fn size_t nifti_write_buffer(znzFile fp, void *buffer, size_t numbytes) + \brief write numbytes of buffer to file, fp + + \param fp File pointer (from znzopen) to gzippable nifti datafile + \param buffer data buffer to be written + \param numbytes number of bytes in buffer to write + \return number of bytes successfully written +*/ +size_t nifti_write_buffer(znzFile fp, const void *buffer, size_t numbytes) +{ + /* Write all the image data at once (no swapping here) */ + size_t ss; + if (znz_isnull(fp)){ + fprintf(stderr,"** ERROR: nifti_write_buffer: null file pointer\n"); + return 0; + } + ss = znzwrite( (void*)buffer , 1 , numbytes , fp ) ; + return ss; +} + + +/*----------------------------------------------------------------------*/ +/*! write the nifti_image data to file (from nim->data or from NBL) + + If NBL is not NULL, write the data from that structure. Otherwise, + write it out from nim->data. No swapping is done here. + + \param fp : File pointer + \param nim : nifti_image corresponding to the data + \param NBL : optional source of write data (if NULL use nim->data) + + \return 0 on success, -1 on failure + + Note: the nifti_image byte_order is set as that of the current CPU. + This is because such a conversion was made to the data upon + reading, while byte_order was not set (so the programs would + know what format the data was on disk). Effectively, since + byte_order should match what is on disk, it should bet set to + that of the current CPU whenever new filenames are assigned. +*//*--------------------------------------------------------------------*/ +int nifti_write_all_data(znzFile fp, nifti_image * nim, + const nifti_brick_list * NBL) +{ + size_t ss; + int bnum; + + if( !NBL ){ /* just write one buffer and get out of here */ + if( nim->data == NULL ){ + fprintf(stderr,"** NWAD: no image data to write\n"); + return -1; + } + + ss = nifti_write_buffer(fp,nim->data,nim->nbyper * nim->nvox); + if (ss < nim->nbyper * nim->nvox){ + fprintf(stderr, + "** ERROR: NWAD: wrote only %u of %u bytes to file\n", + (unsigned)ss, (unsigned)(nim->nbyper * nim->nvox)); + return -1; + } + + if( g_opts.debug > 1 ) + fprintf(stderr,"+d wrote single image of %u bytes\n", (unsigned)ss); + } else { + if( ! NBL->bricks || NBL->nbricks <= 0 || NBL->bsize <= 0 ){ + fprintf(stderr,"** NWAD: no brick data to write (%p,%d,%u)\n", + (void *)NBL->bricks, NBL->nbricks, (unsigned)NBL->bsize); + return -1; + } + + for( bnum = 0; bnum < NBL->nbricks; bnum++ ){ + ss = nifti_write_buffer(fp, NBL->bricks[bnum], NBL->bsize); + if( ss < NBL->bsize ){ + fprintf(stderr, + "** NWAD ERROR: wrote %u of %u bytes of brick %d of %d to file", + (unsigned)ss, (unsigned)NBL->bsize, bnum+1, NBL->nbricks); + return -1; + } + } + if( g_opts.debug > 1 ) + fprintf(stderr,"+d wrote image of %d brick(s), each of %u bytes\n", + NBL->nbricks, (unsigned int)NBL->bsize); + } + + /* mark as being in this CPU byte order */ + nim->byteorder = nifti_short_order() ; + + return 0; +} + +/* return number of extensions written, or -1 on error */ +static int nifti_write_extensions(znzFile fp, nifti_image *nim) +{ + nifti1_extension * list; + char extdr[4] = { 0, 0, 0, 0 }; + int c, size, ok = 1; + + if( znz_isnull(fp) || !nim || nim->num_ext < 0 ){ + if( g_opts.debug > 0 ) + fprintf(stderr,"** nifti_write_extensions, bad params\n"); + return -1; + } + + /* if no extensions and user requests it, skip extender */ + if( g_opts.skip_blank_ext && (nim->num_ext == 0 || ! nim->ext_list ) ){ + if( g_opts.debug > 1 ) + fprintf(stderr,"-d no exts and skip_blank_ext set, " + "so skipping 4-byte extender\n"); + return 0; + } + + /* if invalid extension list, clear num_ext */ + if( ! valid_nifti_extensions(nim) ) nim->num_ext = 0; + + /* write out extender block */ + if( nim->num_ext > 0 ) extdr[0] = 1; + if( nifti_write_buffer(fp, extdr, 4) != 4 ){ + fprintf(stderr,"** failed to write extender\n"); + return -1; + } + + list = nim->ext_list; + for ( c = 0; c < nim->num_ext; c++ ){ + size = (int)nifti_write_buffer(fp, &list->esize, sizeof(int)); + ok = (size == (int)sizeof(int)); + if( ok ){ + size = (int)nifti_write_buffer(fp, &list->ecode, sizeof(int)); + ok = (size == (int)sizeof(int)); + } + if( ok ){ + size = (int)nifti_write_buffer(fp, list->edata, list->esize - 8); + ok = (size == list->esize - 8); + } + + if( !ok ){ + fprintf(stderr,"** failed while writing extension #%d\n",c); + return -1; + } else if ( g_opts.debug > 2 ) + fprintf(stderr,"+d wrote extension %d of %d bytes\n", c, size); + + list++; + } + + if( g_opts.debug > 1 ) + fprintf(stderr,"+d wrote out %d extension(s)\n", nim->num_ext); + + return nim->num_ext; +} + + +/*----------------------------------------------------------------------*/ +/*! basic initialization of a nifti_image struct (to a 1x1x1 image) +*//*--------------------------------------------------------------------*/ +nifti_image *nifti_simple_init_nim(void) +{ + nifti_image *nim; + struct nifti_1_header nhdr; + int nbyper, swapsize; + + memset(&nhdr,0,sizeof(nhdr)) ; /* zero out header, to be safe */ + + nhdr.sizeof_hdr = sizeof(nhdr) ; + nhdr.regular = 'r' ; /* for some stupid reason */ + + nhdr.dim[0] = 3 ; + nhdr.dim[1] = 1 ; nhdr.dim[2] = 1 ; nhdr.dim[3] = 1 ; + nhdr.dim[4] = 0 ; + + nhdr.pixdim[0] = 0.0 ; + nhdr.pixdim[1] = 1.0 ; nhdr.pixdim[2] = 1.0 ; + nhdr.pixdim[3] = 1.0 ; + + nhdr.datatype = DT_FLOAT32 ; + nifti_datatype_sizes( nhdr.datatype , &nbyper, &swapsize ); + nhdr.bitpix = 8 * nbyper ; + + strcpy(nhdr.magic, "n+1"); /* init to single file */ + + nim = nifti_convert_nhdr2nim(nhdr,NULL); + nim->fname = NULL; + nim->iname = NULL; + return nim; +} + + +/*----------------------------------------------------------------------*/ +/*! basic initialization of a nifti_1_header struct (with given dimensions) + + Return an allocated nifti_1_header struct, based on the given + dimensions and datatype. + + \param arg_dims : optional dim[8] array (default {3,1,1,1,0,0,0,0}) + \param arg_dtype : optional datatype (default DT_FLOAT32) + + \return pointer to allocated nifti_1_header struct +*//*--------------------------------------------------------------------*/ +nifti_1_header * nifti_make_new_header(const int arg_dims[], int arg_dtype) +{ + nifti_1_header * nhdr; + const int default_dims[8] = { 3, 1, 1, 1, 0, 0, 0, 0 }; + const int * dim; /* either passed or default dims */ + int dtype; /* either passed or default dtype */ + int c, nbyper, swapsize; + + /* if arg_dims is passed, apply it */ + if( arg_dims ) dim = arg_dims; + else dim = default_dims; + + /* validate dim: if there is any problem, apply default_dims */ + if( dim[0] < 1 || dim[0] > 7 ) { + fprintf(stderr,"** nifti_simple_hdr_with_dims: bad dim[0]=%d\n",dim[0]); + dim = default_dims; + } else { + for( c = 1; c <= dim[0]; c++ ) + if( dim[c] < 1 ) + { + fprintf(stderr, + "** nifti_simple_hdr_with_dims: bad dim[%d]=%d\n",c,dim[c]); + dim = default_dims; + break; + } + } + + /* validate dtype, too */ + dtype = arg_dtype; + if( ! nifti_is_valid_datatype(dtype) ) { + fprintf(stderr,"** nifti_simple_hdr_with_dims: bad dtype %d\n",dtype); + dtype = DT_FLOAT32; + } + + /* now populate the header struct */ + + if( g_opts.debug > 1 ) + fprintf(stderr,"+d nifti_make_new_header, dim[0] = %d, datatype = %d\n", + dim[0], dtype); + + nhdr = (nifti_1_header *)calloc(1,sizeof(nifti_1_header)); + if( !nhdr ){ + fprintf(stderr,"** nifti_make_new_header: failed to alloc hdr\n"); + return NULL; + } + + nhdr->sizeof_hdr = sizeof(nifti_1_header) ; + nhdr->regular = 'r' ; /* for some stupid reason */ + + /* init dim and pixdim */ + nhdr->dim[0] = dim[0] ; + nhdr->pixdim[0] = 0.0; + for( c = 1; c <= dim[0]; c++ ) { + nhdr->dim[c] = dim[c]; + nhdr->pixdim[c] = 1.0; + } + + nhdr->datatype = dtype ; + nifti_datatype_sizes( nhdr->datatype , &nbyper, &swapsize ); + nhdr->bitpix = 8 * nbyper ; + + strcpy(nhdr->magic, "n+1"); /* init to single file */ + + return nhdr; +} + + +/*----------------------------------------------------------------------*/ +/*! basic creation of a nifti_image struct + + Create a nifti_image from the given dimensions and data type. + Optinally, allocate zero-filled data. + + \param dims : optional dim[8] (default {3,1,1,1,0,0,0,0}) + \param datatype : optional datatype (default DT_FLOAT32) + \param data_fill : if flag is set, allocate zero-filled data for image + + \return pointer to allocated nifti_image struct +*//*--------------------------------------------------------------------*/ +nifti_image * nifti_make_new_nim(const int dims[], int datatype, int data_fill) +{ + nifti_image * nim; + nifti_1_header * nhdr; + + nhdr = nifti_make_new_header(dims, datatype); + if( !nhdr ) return NULL; /* error already printed */ + + nim = nifti_convert_nhdr2nim(*nhdr,NULL); + free(nhdr); /* in any case, we are done with this */ + if( !nim ){ + fprintf(stderr,"** NMNN: nifti_convert_nhdr2nim failure\n"); + return NULL; + } + + if( g_opts.debug > 1 ) + fprintf(stderr,"+d nifti_make_new_nim, data_fill = %d\n",data_fill); + + if( data_fill ) { + nim->data = calloc(nim->nvox, nim->nbyper); + + /* if we cannot allocate data, take ball and go home */ + if( !nim->data ) { + fprintf(stderr,"** NMNN: failed to alloc %u bytes for data\n", + (unsigned)(nim->nvox*nim->nbyper)); + nifti_image_free(nim); + nim = NULL; + } + } + + return nim; +} + + +/*----------------------------------------------------------------------*/ +/*! convert a nifti_image structure to a nifti_1_header struct + + No allocation is done, this should be used via structure copy. + As in: +
+    nifti_1_header my_header;
+    my_header = nifti_convert_nim2nhdr(my_nim_pointer);
+    
+*//*--------------------------------------------------------------------*/ +struct nifti_1_header nifti_convert_nim2nhdr(const nifti_image * nim) +{ + struct nifti_1_header nhdr; + + memset(&nhdr,0,sizeof(nhdr)) ; /* zero out header, to be safe */ + + + /**- load the ANALYZE-7.5 generic parts of the header struct */ + + nhdr.sizeof_hdr = sizeof(nhdr) ; + nhdr.regular = 'r' ; /* for some stupid reason */ + + nhdr.dim[0] = nim->ndim ; + nhdr.dim[1] = nim->nx ; nhdr.dim[2] = nim->ny ; nhdr.dim[3] = nim->nz ; + nhdr.dim[4] = nim->nt ; nhdr.dim[5] = nim->nu ; nhdr.dim[6] = nim->nv ; + nhdr.dim[7] = nim->nw ; + + nhdr.pixdim[0] = 0.0 ; + nhdr.pixdim[1] = nim->dx ; nhdr.pixdim[2] = nim->dy ; + nhdr.pixdim[3] = nim->dz ; nhdr.pixdim[4] = nim->dt ; + nhdr.pixdim[5] = nim->du ; nhdr.pixdim[6] = nim->dv ; + nhdr.pixdim[7] = nim->dw ; + + nhdr.datatype = nim->datatype ; + nhdr.bitpix = 8 * nim->nbyper ; + + if( nim->cal_max > nim->cal_min ){ + nhdr.cal_max = nim->cal_max ; + nhdr.cal_min = nim->cal_min ; + } + + if( nim->scl_slope != 0.0 ){ + nhdr.scl_slope = nim->scl_slope ; + nhdr.scl_inter = nim->scl_inter ; + } + + if( nim->descrip[0] != '\0' ){ + memcpy(nhdr.descrip ,nim->descrip ,79) ; nhdr.descrip[79] = '\0' ; + } + if( nim->aux_file[0] != '\0' ){ + memcpy(nhdr.aux_file ,nim->aux_file ,23) ; nhdr.aux_file[23] = '\0' ; + } + + /**- Load NIFTI specific stuff into the header */ + + if( nim->nifti_type > NIFTI_FTYPE_ANALYZE ){ /* then not ANALYZE */ + + if( nim->nifti_type == NIFTI_FTYPE_NIFTI1_1 ) strcpy(nhdr.magic,"n+1") ; + else strcpy(nhdr.magic,"ni1") ; + + nhdr.pixdim[1] = fabs(nhdr.pixdim[1]) ; nhdr.pixdim[2] = fabs(nhdr.pixdim[2]) ; + nhdr.pixdim[3] = fabs(nhdr.pixdim[3]) ; nhdr.pixdim[4] = fabs(nhdr.pixdim[4]) ; + nhdr.pixdim[5] = fabs(nhdr.pixdim[5]) ; nhdr.pixdim[6] = fabs(nhdr.pixdim[6]) ; + nhdr.pixdim[7] = fabs(nhdr.pixdim[7]) ; + + nhdr.intent_code = nim->intent_code ; + nhdr.intent_p1 = nim->intent_p1 ; + nhdr.intent_p2 = nim->intent_p2 ; + nhdr.intent_p3 = nim->intent_p3 ; + if( nim->intent_name[0] != '\0' ){ + memcpy(nhdr.intent_name,nim->intent_name,15) ; + nhdr.intent_name[15] = '\0' ; + } + + nhdr.vox_offset = (float) nim->iname_offset ; + nhdr.xyzt_units = SPACE_TIME_TO_XYZT( nim->xyz_units, nim->time_units ) ; + nhdr.toffset = nim->toffset ; + + if( nim->qform_code > 0 ){ + nhdr.qform_code = nim->qform_code ; + nhdr.quatern_b = nim->quatern_b ; + nhdr.quatern_c = nim->quatern_c ; + nhdr.quatern_d = nim->quatern_d ; + nhdr.qoffset_x = nim->qoffset_x ; + nhdr.qoffset_y = nim->qoffset_y ; + nhdr.qoffset_z = nim->qoffset_z ; + nhdr.pixdim[0] = (nim->qfac >= 0.0) ? 1.0 : -1.0 ; + } + + if( nim->sform_code > 0 ){ + nhdr.sform_code = nim->sform_code ; + nhdr.srow_x[0] = nim->sto_xyz.m[0][0] ; + nhdr.srow_x[1] = nim->sto_xyz.m[0][1] ; + nhdr.srow_x[2] = nim->sto_xyz.m[0][2] ; + nhdr.srow_x[3] = nim->sto_xyz.m[0][3] ; + nhdr.srow_y[0] = nim->sto_xyz.m[1][0] ; + nhdr.srow_y[1] = nim->sto_xyz.m[1][1] ; + nhdr.srow_y[2] = nim->sto_xyz.m[1][2] ; + nhdr.srow_y[3] = nim->sto_xyz.m[1][3] ; + nhdr.srow_z[0] = nim->sto_xyz.m[2][0] ; + nhdr.srow_z[1] = nim->sto_xyz.m[2][1] ; + nhdr.srow_z[2] = nim->sto_xyz.m[2][2] ; + nhdr.srow_z[3] = nim->sto_xyz.m[2][3] ; + } + + nhdr.dim_info = FPS_INTO_DIM_INFO( nim->freq_dim , + nim->phase_dim , nim->slice_dim ) ; + nhdr.slice_code = nim->slice_code ; + nhdr.slice_start = nim->slice_start ; + nhdr.slice_end = nim->slice_end ; + nhdr.slice_duration = nim->slice_duration ; + } + + return nhdr; +} + + +/*----------------------------------------------------------------------*/ +/*! \fn int nifti_copy_extensions(nifti_image * nim_dest, nifti_image * nim_src) + \brief copy the nifti1_extension list from src to dest + + Duplicate the list of nifti1_extensions. The dest structure must + be clear of extensions. + \return 0 on success, -1 on failure + + \sa nifti_add_extension, nifti_free_extensions +*/ +int nifti_copy_extensions(nifti_image * nim_dest, const nifti_image * nim_src) +{ + char * data; + size_t bytes; + int c, size, old_size; + + if( nim_dest->num_ext > 0 || nim_dest->ext_list != NULL ){ + fprintf(stderr,"** will not copy extensions over existing ones\n"); + return -1; + } + + if( g_opts.debug > 1 ) + fprintf(stderr,"+d duplicating %d extension(s)\n", nim_src->num_ext); + + if( nim_src->num_ext <= 0 ) return 0; + + bytes = nim_src->num_ext * sizeof(nifti1_extension); /* I'm lazy */ + nim_dest->ext_list = (nifti1_extension *)malloc(bytes); + if( !nim_dest->ext_list ){ + fprintf(stderr,"** failed to allocate %d nifti1_extension structs\n", + nim_src->num_ext); + return -1; + } + + /* copy the extension data */ + nim_dest->num_ext = 0; + for( c = 0; c < nim_src->num_ext; c++ ){ + size = old_size = nim_src->ext_list[c].esize; + if( size & 0xf ) size = (size + 0xf) & ~0xf; /* make multiple of 16 */ + if( g_opts.debug > 2 ) + fprintf(stderr,"+d dup'ing ext #%d of size %d (from size %d)\n", + c, size, old_size); + /* data length is size-8, as esize includes space for esize and ecode */ + data = (char *)calloc(size-8,sizeof(char)); /* maybe size > old */ + if( !data ){ + fprintf(stderr,"** failed to alloc %d bytes for extention\n", size); + if( c == 0 ) { free(nim_dest->ext_list); nim_dest->ext_list = NULL; } + /* otherwise, keep what we have (a.o.t. deleting them all) */ + return -1; + } + /* finally, fill the new structure */ + nim_dest->ext_list[c].esize = size; + nim_dest->ext_list[c].ecode = nim_src->ext_list[c].ecode; + nim_dest->ext_list[c].edata = data; + memcpy(data, nim_src->ext_list[c].edata, old_size-8); + + nim_dest->num_ext++; + } + + return 0; +} + + +/*----------------------------------------------------------------------*/ +/*! compute the total size of all extensions + + \return the total of all esize fields + + Note that each esize includes 4 bytes for ecode, 4 bytes for esize, + and the bytes used for the data. Each esize also needs to be a + multiple of 16, so it may be greater than the sum of its 3 parts. +*//*--------------------------------------------------------------------*/ +int nifti_extension_size(nifti_image *nim) +{ + int c, size = 0; + + if( !nim || nim->num_ext <= 0 ) return 0; + + if( g_opts.debug > 2 ) fprintf(stderr,"-d ext sizes:"); + + for ( c = 0; c < nim->num_ext; c++ ){ + size += nim->ext_list[c].esize; + if( g_opts.debug > 2 ) fprintf(stderr," %d",nim->ext_list[c].esize); + } + + if( g_opts.debug > 2 ) fprintf(stderr," (total = %d)\n",size); + + return size; +} + + +/*----------------------------------------------------------------------*/ +/*! set the nifti_image iname_offset field, based on nifti_type + + - if writing to 2 files, set offset to 0 + - if writing to a single NIFTI-1 file, set the offset to + 352 + total extension size, then align to 16-byte boundary + - if writing an ASCII header, set offset to -1 +*//*--------------------------------------------------------------------*/ +void nifti_set_iname_offset(nifti_image *nim) +{ + int offset; + + switch( nim->nifti_type ){ + + default: /* writing into 2 files */ + /* we only write files with 0 offset in the 2 file format */ + nim->iname_offset = 0 ; + break ; + + /* NIFTI-1 single binary file - always update */ + case NIFTI_FTYPE_NIFTI1_1: + offset = nifti_extension_size(nim)+sizeof(struct nifti_1_header)+4; + /* be sure offset is aligned to a 16 byte boundary */ + if ( ( offset % 16 ) != 0 ) offset = ((offset + 0xf) & ~0xf); + if( nim->iname_offset != offset ){ + if( g_opts.debug > 1 ) + fprintf(stderr,"+d changing offset from %d to %d\n", + nim->iname_offset, offset); + nim->iname_offset = offset; + } + break ; + + /* non-standard case: NIFTI-1 ASCII header + binary data (single file) */ + case NIFTI_FTYPE_ASCII: + nim->iname_offset = -1 ; /* compute offset from filesize */ + break ; + } +} + + +/*----------------------------------------------------------------------*/ +/*! write the nifti_image dataset to disk, optionally including data + + This is just a front-end for nifti_image_write_hdr_img2. + + \param nim nifti_image to write to disk + \param write_data write options (see nifti_image_write_hdr_img2) + \param opts file open options ("wb" from nifti_image_write) + + \sa nifti_image_write, nifti_image_write_hdr_img2, nifti_image_free, + nifti_set_filenames +*//*--------------------------------------------------------------------*/ +znzFile nifti_image_write_hdr_img( nifti_image *nim , int write_data , + const char* opts ) +{ + return nifti_image_write_hdr_img2(nim,write_data,opts,NULL,NULL); +} + + +#undef ERREX +#define ERREX(msg) \ + do{ fprintf(stderr,"** ERROR: nifti_image_write_hdr_img: %s\n",(msg)) ; \ + return fp ; } while(0) + + +/* ----------------------------------------------------------------------*/ +/*! This writes the header (and optionally the image data) to file + * + * If the image data file is left open it returns a valid znzFile handle. + * It also uses imgfile as the open image file is not null, and modifies + * it inside. + * + * \param nim nifti_image to write to disk + * \param write_opts flags whether to write data and/or close file (see below) + * \param opts file-open options, probably "wb" from nifti_image_write() + * \param imgfile optional open znzFile struct, for writing image data + (may be NULL) + * \param NBL optional nifti_brick_list, containing the image data + (may be NULL) + * + * Values for write_opts mode are based on two binary flags + * ( 0/1 for no-write/write data, and 0/2 for close/leave-open files ) : + * - 0 = do not write data and close (do not open data file) + * - 1 = write data and close + * - 2 = do not write data and leave data file open + * - 3 = write data and leave data file open + * + * \sa nifti_image_write, nifti_image_write_hdr_img, nifti_image_free, + * nifti_set_filenames +*//*---------------------------------------------------------------------*/ +znzFile nifti_image_write_hdr_img2(nifti_image *nim, int write_opts, + const char * opts, znzFile imgfile, const nifti_brick_list * NBL) +{ + struct nifti_1_header nhdr ; + znzFile fp=NULL; + size_t ss ; + int write_data, leave_open; + char func[] = { "nifti_image_write_hdr_img2" }; + + write_data = write_opts & 1; /* just separate the bits now */ + leave_open = write_opts & 2; + + if( ! nim ) ERREX("NULL input") ; + if( ! nifti_validfilename(nim->fname) ) ERREX("bad fname input") ; + if( write_data && ! nim->data && ! NBL ) ERREX("no image data") ; + + if( write_data && NBL && ! nifti_NBL_matches_nim(nim, NBL) ) + ERREX("NBL does not match nim"); + + nifti_set_iname_offset(nim); + + if( g_opts.debug > 1 ){ + fprintf(stderr,"-d writing nifti file '%s'...\n", nim->fname); + if( g_opts.debug > 2 ) + fprintf(stderr,"-d nifti type %d, offset %d\n", + nim->nifti_type, nim->iname_offset); + } + + if( nim->nifti_type == NIFTI_FTYPE_ASCII ) /* non-standard case */ + return nifti_write_ascii_image(nim,NBL,opts,write_data,leave_open); + + nhdr = nifti_convert_nim2nhdr(nim); /* create the nifti1_header struct */ + + /* if writing to 2 files, make sure iname is set and different from fname */ + if( nim->nifti_type != NIFTI_FTYPE_NIFTI1_1 ){ + if( nim->iname && strcmp(nim->iname,nim->fname) == 0 ){ + free(nim->iname) ; nim->iname = NULL ; + } + if( nim->iname == NULL ){ /* then make a new one */ + nim->iname = nifti_makeimgname(nim->fname,nim->nifti_type,0,0); + if( nim->iname == NULL ) return NULL; + } + } + + /* if we have an imgfile and will write the header there, use it */ + if( ! znz_isnull(imgfile) && nim->nifti_type == NIFTI_FTYPE_NIFTI1_1 ){ + if( g_opts.debug > 2 ) fprintf(stderr,"+d using passed file for hdr\n"); + fp = imgfile; + } + else { + if( g_opts.debug > 2 ) + fprintf(stderr,"+d opening output file %s [%s]\n",nim->fname,opts); + fp = znzopen( nim->fname , opts , nifti_is_gzfile(nim->fname) ) ; + if( znz_isnull(fp) ){ + LNI_FERR(func,"cannot open output file",nim->fname); + return fp; + } + } + + /* write the header and extensions */ + + ss = znzwrite(&nhdr , 1 , sizeof(nhdr) , fp); /* write header */ + if( ss < sizeof(nhdr) ){ + LNI_FERR(func,"bad header write to output file",nim->fname); + znzclose(fp); return fp; + } + + /* partial file exists, and errors have been printed, so ignore return */ + if( nim->nifti_type != NIFTI_FTYPE_ANALYZE ) + (void)nifti_write_extensions(fp,nim); + + /* if the header is all we want, we are done */ + if( ! write_data && ! leave_open ){ + if( g_opts.debug > 2 ) fprintf(stderr,"-d header is all we want: done\n"); + znzclose(fp); return(fp); + } + + if( nim->nifti_type != NIFTI_FTYPE_NIFTI1_1 ){ /* get a new file pointer */ + znzclose(fp); /* first, close header file */ + if( ! znz_isnull(imgfile) ){ + if(g_opts.debug > 2) fprintf(stderr,"+d using passed file for img\n"); + fp = imgfile; + } + else { + if( g_opts.debug > 2 ) + fprintf(stderr,"+d opening img file '%s'\n", nim->iname); + fp = znzopen( nim->iname , opts , nifti_is_gzfile(nim->iname) ) ; + if( znz_isnull(fp) ) ERREX("cannot open image file") ; + } + } + + znzseek(fp, nim->iname_offset, SEEK_SET); /* in any case, seek to offset */ + + if( write_data ) nifti_write_all_data(fp,nim,NBL); + if( ! leave_open ) znzclose(fp); + + return fp; +} + + +/*----------------------------------------------------------------------*/ +/*! write a nifti_image to disk in ASCII format +*//*--------------------------------------------------------------------*/ +znzFile nifti_write_ascii_image(nifti_image *nim, const nifti_brick_list * NBL, + const char *opts, int write_data, int leave_open) +{ + znzFile fp; + char * hstr; + + hstr = nifti_image_to_ascii( nim ) ; /* get header in ASCII form */ + if( ! hstr ){ fprintf(stderr,"** failed image_to_ascii()\n"); return NULL; } + + fp = znzopen( nim->fname , opts , nifti_is_gzfile(nim->fname) ) ; + if( znz_isnull(fp) ){ + free(hstr); + fprintf(stderr,"** failed to open '%s' for ascii write\n",nim->fname); + return fp; + } + + znzputs(hstr,fp); /* header */ + nifti_write_extensions(fp,nim); /* extensions */ + + if ( write_data ) { nifti_write_all_data(fp,nim,NBL); } /* data */ + if ( ! leave_open ) { znzclose(fp); } + free(hstr); + return fp; /* returned but may be closed */ +} + + +/*--------------------------------------------------------------------------*/ +/*! Write a nifti_image to disk. + + Since data is properly byte-swapped upon reading, it is assumed + to be in the byte-order of the current CPU at write time. Thus, + nim->byte_order should match that of the current CPU. Note that + the nifti_set_filenames() function takes the flag, set_byte_order. + + The following fields of nim affect how the output appears: + - nifti_type = 0 ==> ANALYZE-7.5 format file pair will be written + - nifti_type = 1 ==> NIFTI-1 format single file will be written + (data offset will be 352+extensions) + - nifti_type = 2 ==> NIFTI_1 format file pair will be written + - nifti_type = 3 ==> NIFTI_1 ASCII single file will be written + - fname is the name of the output file (header or header+data) + - if a file pair is being written, iname is the name of the data file + - existing files WILL be overwritten with extreme prejudice + - if qform_code > 0, the quatern_*, qoffset_*, and qfac fields determine + the qform output, NOT the qto_xyz matrix; if you want to compute these + fields from the qto_xyz matrix, you can use the utility function + nifti_mat44_to_quatern() + + \sa nifti_image_write_bricks, nifti_image_free, nifti_set_filenames, + nifti_image_write_hdr_img +*//*------------------------------------------------------------------------*/ +void nifti_image_write( nifti_image *nim ) +{ + znzFile fp = nifti_image_write_hdr_img(nim,1,"wb"); + if( fp ){ + if( g_opts.debug > 2 ) fprintf(stderr,"-d niw: done with znzFile\n"); + free(fp); + } + if( g_opts.debug > 1 ) fprintf(stderr,"-d nifti_image_write: done\n"); +} + + +/*----------------------------------------------------------------------*/ +/*! similar to nifti_image_write, but data is in NBL struct, not nim->data + + \sa nifti_image_write, nifti_image_free, nifti_set_filenames, nifti_free_NBL +*//*--------------------------------------------------------------------*/ +void nifti_image_write_bricks( nifti_image *nim, const nifti_brick_list * NBL ) +{ + znzFile fp = nifti_image_write_hdr_img2(nim,1,"wb",NULL,NBL); + if( fp ){ + if( g_opts.debug > 2 ) fprintf(stderr,"-d niwb: done with znzFile\n"); + free(fp); + } + if( g_opts.debug > 1 ) fprintf(stderr,"-d niwb: done writing bricks\n"); +} + + +/*----------------------------------------------------------------------*/ +/*! copy the nifti_image structure, without data + + Duplicate the structure, including fname, iname and extensions. + Leave the data pointer as NULL. +*//*--------------------------------------------------------------------*/ +nifti_image * nifti_copy_nim_info(const nifti_image * src) +{ + nifti_image *dest; + dest = (nifti_image *)calloc(1,sizeof(nifti_image)); + if( !dest ){ + fprintf(stderr,"** NCNI: failed to alloc nifti_image\n"); + return NULL; + } + memcpy(dest, src, sizeof(nifti_image)); + if( src->fname ) dest->fname = nifti_strdup(src->fname); + if( src->iname ) dest->iname = nifti_strdup(src->iname); + dest->num_ext = 0; + dest->ext_list = NULL; + /* errors will be printed in NCE(), continue in either case */ + (void)nifti_copy_extensions(dest, src); + + dest->data = NULL; + + return dest; +} + + +/*------------------------------------------------------------------------*/ +/* Un-escape a C string in place -- that is, convert XML escape sequences + back into their characters. (This can be done in place since the + replacement is always smaller than the input.) Escapes recognized are: + - < -> < + - > -> > + - " -> " + - ' -> ' + - & -> & + Also replace CR LF pair (Microsoft), or CR alone (Macintosh) with + LF (Unix), per the XML standard. + Return value is number of replacements made (if you care). +--------------------------------------------------------------------------*/ + +#undef CR +#undef LF +#define CR 0x0D +#define LF 0x0A + +static int unescape_string( char *str ) +{ + int ii,jj , nn,ll ; + + if( str == NULL ) return 0 ; /* no string? */ + ll = (int)strlen(str) ; if( ll == 0 ) return 0 ; + + /* scan for escapes: &something; */ + + for( ii=jj=nn=0 ; ii': lout += 4 ; break ; /* replace '<' with "<" */ + + case '"' : + case '\'': lout += 6 ; break ; /* replace '"' with """ */ + + case CR: + case LF: lout += 6 ; break ; /* replace CR with " " + LF with " " */ + + default: lout++ ; break ; /* copy all other chars */ + } + } + out = (char *)calloc(1,lout) ; /* allocate output string */ + if( !out ){ + fprintf(stderr,"** escapize_string: failed to alloc %d bytes\n",lout); + return NULL; + } + out[0] = '\'' ; /* opening quote mark */ + for( ii=0,jj=1 ; ii < lstr ; ii++ ){ + switch( str[ii] ){ + default: out[jj++] = str[ii] ; break ; /* normal characters */ + + case '&': memcpy(out+jj,"&",5) ; jj+=5 ; break ; + + case '<': memcpy(out+jj,"<",4) ; jj+=4 ; break ; + case '>': memcpy(out+jj,">",4) ; jj+=4 ; break ; + + case '"' : memcpy(out+jj,""",6) ; jj+=6 ; break ; + + case '\'': memcpy(out+jj,"'",6) ; jj+=6 ; break ; + + case CR: memcpy(out+jj," ",6) ; jj+=6 ; break ; + case LF: memcpy(out+jj," ",6) ; jj+=6 ; break ; + } + } + out[jj++] = '\'' ; /* closing quote mark */ + out[jj] = '\0' ; /* terminate the string */ + return out ; +} + +/*---------------------------------------------------------------------------*/ +/*! Dump the information in a NIFTI image header to an XML-ish ASCII string + that can later be converted back into a NIFTI header in + nifti_image_from_ascii(). + + The resulting string can be free()-ed when you are done with it. +*//*-------------------------------------------------------------------------*/ +char *nifti_image_to_ascii( const nifti_image *nim ) +{ + char *buf , *ebuf ; int nbuf ; + + if( nim == NULL ) return NULL ; /* stupid caller */ + + buf = (char *)calloc(1,65534); nbuf = 0; /* longer than needed, to be safe */ + if( !buf ){ + fprintf(stderr,"** NITA: failed to alloc %d bytes\n",65534); + return NULL; + } + + sprintf( buf , "nifti_type == NIFTI_FTYPE_NIFTI1_1) ? "NIFTI-1+" + :(nim->nifti_type == NIFTI_FTYPE_NIFTI1_2) ? "NIFTI-1" + :(nim->nifti_type == NIFTI_FTYPE_ASCII ) ? "NIFTI-1A" + : "ANALYZE-7.5" ) ; + + /** Strings that we don't control (filenames, etc.) that might + contain "weird" characters (like quotes) are "escaped": + - A few special characters are replaced by XML-style escapes, using + the function escapize_string(). + - On input, function unescape_string() reverses this process. + - The result is that the NIFTI ASCII-format header is XML-compliant. */ + + ebuf = escapize_string(nim->fname) ; + sprintf( buf+strlen(buf) , " header_filename = %s\n",ebuf); free(ebuf); + + ebuf = escapize_string(nim->iname) ; + sprintf( buf+strlen(buf) , " image_filename = %s\n", ebuf); free(ebuf); + + sprintf( buf+strlen(buf) , " image_offset = '%d'\n" , nim->iname_offset ); + + sprintf( buf+strlen(buf), " ndim = '%d'\n", nim->ndim); + sprintf( buf+strlen(buf), " nx = '%d'\n", nim->nx ); + if( nim->ndim > 1 ) sprintf( buf+strlen(buf), " ny = '%d'\n", nim->ny ); + if( nim->ndim > 2 ) sprintf( buf+strlen(buf), " nz = '%d'\n", nim->nz ); + if( nim->ndim > 3 ) sprintf( buf+strlen(buf), " nt = '%d'\n", nim->nt ); + if( nim->ndim > 4 ) sprintf( buf+strlen(buf), " nu = '%d'\n", nim->nu ); + if( nim->ndim > 5 ) sprintf( buf+strlen(buf), " nv = '%d'\n", nim->nv ); + if( nim->ndim > 6 ) sprintf( buf+strlen(buf), " nw = '%d'\n", nim->nw ); + sprintf( buf+strlen(buf), " dx = '%g'\n", nim->dx ); + if( nim->ndim > 1 ) sprintf( buf+strlen(buf), " dy = '%g'\n", nim->dy ); + if( nim->ndim > 2 ) sprintf( buf+strlen(buf), " dz = '%g'\n", nim->dz ); + if( nim->ndim > 3 ) sprintf( buf+strlen(buf), " dt = '%g'\n", nim->dt ); + if( nim->ndim > 4 ) sprintf( buf+strlen(buf), " du = '%g'\n", nim->du ); + if( nim->ndim > 5 ) sprintf( buf+strlen(buf), " dv = '%g'\n", nim->dv ); + if( nim->ndim > 6 ) sprintf( buf+strlen(buf), " dw = '%g'\n", nim->dw ); + + sprintf( buf+strlen(buf) , " datatype = '%d'\n" , nim->datatype ) ; + sprintf( buf+strlen(buf) , " datatype_name = '%s'\n" , + nifti_datatype_string(nim->datatype) ) ; + + sprintf( buf+strlen(buf) , " nvox = '%u'\n" , (unsigned)nim->nvox ) ; + sprintf( buf+strlen(buf) , " nbyper = '%d'\n" , nim->nbyper ) ; + + sprintf( buf+strlen(buf) , " byteorder = '%s'\n" , + (nim->byteorder==MSB_FIRST) ? "MSB_FIRST" : "LSB_FIRST" ) ; + + if( nim->cal_min < nim->cal_max ){ + sprintf( buf+strlen(buf) , " cal_min = '%g'\n", nim->cal_min ) ; + sprintf( buf+strlen(buf) , " cal_max = '%g'\n", nim->cal_max ) ; + } + + if( nim->scl_slope != 0.0 ){ + sprintf( buf+strlen(buf) , " scl_slope = '%g'\n" , nim->scl_slope ) ; + sprintf( buf+strlen(buf) , " scl_inter = '%g'\n" , nim->scl_inter ) ; + } + + if( nim->intent_code > 0 ){ + sprintf( buf+strlen(buf) , " intent_code = '%d'\n", nim->intent_code ) ; + sprintf( buf+strlen(buf) , " intent_code_name = '%s'\n" , + nifti_intent_string(nim->intent_code) ) ; + sprintf( buf+strlen(buf) , " intent_p1 = '%g'\n" , nim->intent_p1 ) ; + sprintf( buf+strlen(buf) , " intent_p2 = '%g'\n" , nim->intent_p2 ) ; + sprintf( buf+strlen(buf) , " intent_p3 = '%g'\n" , nim->intent_p3 ) ; + + if( nim->intent_name[0] != '\0' ){ + ebuf = escapize_string(nim->intent_name) ; + sprintf( buf+strlen(buf) , " intent_name = %s\n",ebuf) ; + free(ebuf) ; + } + } + + if( nim->toffset != 0.0 ) + sprintf( buf+strlen(buf) , " toffset = '%g'\n",nim->toffset ) ; + + if( nim->xyz_units > 0 ) + sprintf( buf+strlen(buf) , + " xyz_units = '%d'\n" + " xyz_units_name = '%s'\n" , + nim->xyz_units , nifti_units_string(nim->xyz_units) ) ; + + if( nim->time_units > 0 ) + sprintf( buf+strlen(buf) , + " time_units = '%d'\n" + " time_units_name = '%s'\n" , + nim->time_units , nifti_units_string(nim->time_units) ) ; + + if( nim->freq_dim > 0 ) + sprintf( buf+strlen(buf) , " freq_dim = '%d'\n",nim->freq_dim ) ; + if( nim->phase_dim > 0 ) + sprintf( buf+strlen(buf) , " phase_dim = '%d'\n",nim->phase_dim ) ; + if( nim->slice_dim > 0 ) + sprintf( buf+strlen(buf) , " slice_dim = '%d'\n",nim->slice_dim ) ; + if( nim->slice_code > 0 ) + sprintf( buf+strlen(buf) , + " slice_code = '%d'\n" + " slice_code_name = '%s'\n" , + nim->slice_code , nifti_slice_string(nim->slice_code) ) ; + if( nim->slice_start >= 0 && nim->slice_end > nim->slice_start ) + sprintf( buf+strlen(buf) , + " slice_start = '%d'\n" + " slice_end = '%d'\n" , nim->slice_start , nim->slice_end ) ; + if( nim->slice_duration != 0.0 ) + sprintf( buf+strlen(buf) , " slice_duration = '%g'\n", + nim->slice_duration ) ; + + if( nim->descrip[0] != '\0' ){ + ebuf = escapize_string(nim->descrip) ; + sprintf( buf+strlen(buf) , " descrip = %s\n",ebuf) ; + free(ebuf) ; + } + + if( nim->aux_file[0] != '\0' ){ + ebuf = escapize_string(nim->aux_file) ; + sprintf( buf+strlen(buf) , " aux_file = %s\n",ebuf) ; + free(ebuf) ; + } + + if( nim->qform_code > 0 ){ + int i,j,k ; + + sprintf( buf+strlen(buf) , + " qform_code = '%d'\n" + " qform_code_name = '%s'\n" + " qto_xyz_matrix = '%g %g %g %g %g %g %g %g %g %g %g %g %g %g %g %g'\n" , + nim->qform_code , nifti_xform_string(nim->qform_code) , + nim->qto_xyz.m[0][0] , nim->qto_xyz.m[0][1] , + nim->qto_xyz.m[0][2] , nim->qto_xyz.m[0][3] , + nim->qto_xyz.m[1][0] , nim->qto_xyz.m[1][1] , + nim->qto_xyz.m[1][2] , nim->qto_xyz.m[1][3] , + nim->qto_xyz.m[2][0] , nim->qto_xyz.m[2][1] , + nim->qto_xyz.m[2][2] , nim->qto_xyz.m[2][3] , + nim->qto_xyz.m[3][0] , nim->qto_xyz.m[3][1] , + nim->qto_xyz.m[3][2] , nim->qto_xyz.m[3][3] ) ; + + sprintf( buf+strlen(buf) , + " qto_ijk_matrix = '%g %g %g %g %g %g %g %g %g %g %g %g %g %g %g %g'\n" , + nim->qto_ijk.m[0][0] , nim->qto_ijk.m[0][1] , + nim->qto_ijk.m[0][2] , nim->qto_ijk.m[0][3] , + nim->qto_ijk.m[1][0] , nim->qto_ijk.m[1][1] , + nim->qto_ijk.m[1][2] , nim->qto_ijk.m[1][3] , + nim->qto_ijk.m[2][0] , nim->qto_ijk.m[2][1] , + nim->qto_ijk.m[2][2] , nim->qto_ijk.m[2][3] , + nim->qto_ijk.m[3][0] , nim->qto_ijk.m[3][1] , + nim->qto_ijk.m[3][2] , nim->qto_ijk.m[3][3] ) ; + + sprintf( buf+strlen(buf) , + " quatern_b = '%g'\n" + " quatern_c = '%g'\n" + " quatern_d = '%g'\n" + " qoffset_x = '%g'\n" + " qoffset_y = '%g'\n" + " qoffset_z = '%g'\n" + " qfac = '%g'\n" , + nim->quatern_b , nim->quatern_c , nim->quatern_d , + nim->qoffset_x , nim->qoffset_y , nim->qoffset_z , nim->qfac ) ; + + nifti_mat44_to_orientation( nim->qto_xyz , &i,&j,&k ) ; + if( i > 0 && j > 0 && k > 0 ) + sprintf( buf+strlen(buf) , + " qform_i_orientation = '%s'\n" + " qform_j_orientation = '%s'\n" + " qform_k_orientation = '%s'\n" , + nifti_orientation_string(i) , + nifti_orientation_string(j) , + nifti_orientation_string(k) ) ; + } + + if( nim->sform_code > 0 ){ + int i,j,k ; + + sprintf( buf+strlen(buf) , + " sform_code = '%d'\n" + " sform_code_name = '%s'\n" + " sto_xyz_matrix = '%g %g %g %g %g %g %g %g %g %g %g %g %g %g %g %g'\n" , + nim->sform_code , nifti_xform_string(nim->sform_code) , + nim->sto_xyz.m[0][0] , nim->sto_xyz.m[0][1] , + nim->sto_xyz.m[0][2] , nim->sto_xyz.m[0][3] , + nim->sto_xyz.m[1][0] , nim->sto_xyz.m[1][1] , + nim->sto_xyz.m[1][2] , nim->sto_xyz.m[1][3] , + nim->sto_xyz.m[2][0] , nim->sto_xyz.m[2][1] , + nim->sto_xyz.m[2][2] , nim->sto_xyz.m[2][3] , + nim->sto_xyz.m[3][0] , nim->sto_xyz.m[3][1] , + nim->sto_xyz.m[3][2] , nim->sto_xyz.m[3][3] ) ; + + sprintf( buf+strlen(buf) , + " sto_ijk matrix = '%g %g %g %g %g %g %g %g %g %g %g %g %g %g %g %g'\n" , + nim->sto_ijk.m[0][0] , nim->sto_ijk.m[0][1] , + nim->sto_ijk.m[0][2] , nim->sto_ijk.m[0][3] , + nim->sto_ijk.m[1][0] , nim->sto_ijk.m[1][1] , + nim->sto_ijk.m[1][2] , nim->sto_ijk.m[1][3] , + nim->sto_ijk.m[2][0] , nim->sto_ijk.m[2][1] , + nim->sto_ijk.m[2][2] , nim->sto_ijk.m[2][3] , + nim->sto_ijk.m[3][0] , nim->sto_ijk.m[3][1] , + nim->sto_ijk.m[3][2] , nim->sto_ijk.m[3][3] ) ; + + nifti_mat44_to_orientation( nim->sto_xyz , &i,&j,&k ) ; + if( i > 0 && j > 0 && k > 0 ) + sprintf( buf+strlen(buf) , + " sform_i_orientation = '%s'\n" + " sform_j_orientation = '%s'\n" + " sform_k_orientation = '%s'\n" , + nifti_orientation_string(i) , + nifti_orientation_string(j) , + nifti_orientation_string(k) ) ; + } + + sprintf( buf+strlen(buf) , " num_ext = '%d'\n", nim->num_ext ) ; + + sprintf( buf+strlen(buf) , "/>\n" ) ; /* XML-ish closer */ + + nbuf = (int)strlen(buf) ; + buf = (char *)realloc((void *)buf, nbuf+1); /* cut back to proper length */ + if( !buf ) fprintf(stderr,"** NITA: failed to realloc %d bytes\n",nbuf+1); + return buf ; +} + +/*---------------------------------------------------------------------------*/ + +/*----------------------------------------------------------------------*/ +/*! get the byte order for this CPU + + - LSB_FIRST means least significant byte, first (little endian) + - MSB_FIRST means most significant byte, first (big endian) +*//*--------------------------------------------------------------------*/ +int nifti_short_order(void) /* determine this CPU's byte order */ +{ + union { unsigned char bb[2] ; + short ss ; } fred ; + + fred.bb[0] = 1 ; fred.bb[1] = 0 ; + + return (fred.ss == 1) ? LSB_FIRST : MSB_FIRST ; +} + +/*---------------------------------------------------------------------------*/ + +#undef QQNUM +#undef QNUM +#undef QSTR + +/* macro to check lhs string against "n1"; if it matches, + interpret rhs string as a number, and put it into nim->"n2" */ + +#define QQNUM(n1,n2) if( strcmp(lhs,#n1)==0 ) nim->n2=strtod(rhs,NULL) + +/* same, but where "n1" == "n2" */ + +#define QNUM(nam) QQNUM(nam,nam) + +/* macro to check lhs string against "nam"; if it matches, + put rhs string into nim->"nam" string, with max length = "ml" */ + +#define QSTR(nam,ml) if( strcmp(lhs,#nam) == 0 ) \ + strncpy(nim->nam,rhs,ml), nim->nam[ml]='\0' + +/*---------------------------------------------------------------------------*/ +/*! Take an XML-ish ASCII string and create a NIFTI image header to match. + + NULL is returned if enough information isn't present in the input string. + - The image data can later be loaded with nifti_image_load(). + - The struct returned here can be liberated with nifti_image_free(). + - Not a lot of error checking is done here to make sure that the + input values are reasonable! +*//*-------------------------------------------------------------------------*/ +nifti_image *nifti_image_from_ascii( const char *str, int * bytes_read ) +{ + char lhs[1024] , rhs[1024] ; + int ii , spos, nn ; + nifti_image *nim ; /* will be output */ + + if( str == NULL || *str == '\0' ) return NULL ; /* bad input!? */ + + /* scan for opening string */ + + spos = 0 ; + if(!strlen(str)) return NULL; + ii = sscanf( str+spos , "%1023s%n" , lhs , &nn ) ; spos += nn ; + if( ii == 0 || strcmp(lhs,"nx = nim->ny = nim->nz = nim->nt + = nim->nu = nim->nv = nim->nw = 1 ; + nim->dx = nim->dy = nim->dz = nim->dt + = nim->du = nim->dv = nim->dw = 0 ; + nim->qfac = 1.0 ; + + nim->byteorder = nifti_short_order() ; + + /* starting at str[spos], scan for "equations" of the form + lhs = 'rhs' + and assign rhs values into the struct component named by lhs */ + + while(1){ + + while( isspace((int) str[spos]) ) spos++ ; /* skip whitespace */ + if( str[spos] == '\0' ) break ; /* end of string? */ + + /* get lhs string */ + + ii = sscanf( str+spos , "%1023s%n" , lhs , &nn ) ; spos += nn ; + if( ii == 0 || strcmp(lhs,"/>") == 0 ) break ; /* end of input? */ + + /* skip whitespace and the '=' marker */ + + while( isspace((int) str[spos]) || str[spos] == '=' ) spos++ ; + if( str[spos] == '\0' ) break ; /* end of string? */ + + /* if next character is a quote ', copy everything up to next ' + otherwise, copy everything up to next nonblank */ + + if( str[spos] == '\'' ){ + ii = spos+1 ; + while( str[ii] != '\0' && str[ii] != '\'' ) ii++ ; + nn = ii-spos-1 ; if( nn > 1023 ) nn = 1023 ; + memcpy(rhs,str+spos+1,nn) ; rhs[nn] = '\0' ; + spos = (str[ii] == '\'') ? ii+1 : ii ; + } else { + ii = sscanf( str+spos , "%1023s%n" , rhs , &nn ) ; spos += nn ; + if( ii == 0 ) break ; /* nothing found? */ + } + unescape_string(rhs) ; /* remove any XML escape sequences */ + + /* Now can do the assignment, based on lhs string. + Start with special cases that don't fit the QNUM/QSTR macros. */ + + if( strcmp(lhs,"nifti_type") == 0 ){ + if( strcmp(rhs,"ANALYZE-7.5") == 0 ) + nim->nifti_type = NIFTI_FTYPE_ANALYZE ; + else if( strcmp(rhs,"NIFTI-1+") == 0 ) + nim->nifti_type = NIFTI_FTYPE_NIFTI1_1 ; + else if( strcmp(rhs,"NIFTI-1") == 0 ) + nim->nifti_type = NIFTI_FTYPE_NIFTI1_2 ; + else if( strcmp(rhs,"NIFTI-1A") == 0 ) + nim->nifti_type = NIFTI_FTYPE_ASCII ; + } + else if( strcmp(lhs,"header_filename") == 0 ){ + nim->fname = nifti_strdup(rhs) ; + } + else if( strcmp(lhs,"image_filename") == 0 ){ + nim->iname = nifti_strdup(rhs) ; + } + else if( strcmp(lhs,"sto_xyz_matrix") == 0 ){ + sscanf( rhs , "%f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f" , + &(nim->sto_xyz.m[0][0]) , &(nim->sto_xyz.m[0][1]) , + &(nim->sto_xyz.m[0][2]) , &(nim->sto_xyz.m[0][3]) , + &(nim->sto_xyz.m[1][0]) , &(nim->sto_xyz.m[1][1]) , + &(nim->sto_xyz.m[1][2]) , &(nim->sto_xyz.m[1][3]) , + &(nim->sto_xyz.m[2][0]) , &(nim->sto_xyz.m[2][1]) , + &(nim->sto_xyz.m[2][2]) , &(nim->sto_xyz.m[2][3]) , + &(nim->sto_xyz.m[3][0]) , &(nim->sto_xyz.m[3][1]) , + &(nim->sto_xyz.m[3][2]) , &(nim->sto_xyz.m[3][3]) ) ; + } + else if( strcmp(lhs,"byteorder") == 0 ){ + if( strcmp(rhs,"MSB_FIRST") == 0 ) nim->byteorder = MSB_FIRST ; + if( strcmp(rhs,"LSB_FIRST") == 0 ) nim->byteorder = LSB_FIRST ; + } + else QQNUM(image_offset,iname_offset) ; + else QNUM(datatype) ; + else QNUM(ndim) ; + else QNUM(nx) ; + else QNUM(ny) ; + else QNUM(nz) ; + else QNUM(nt) ; + else QNUM(nu) ; + else QNUM(nv) ; + else QNUM(nw) ; + else QNUM(dx) ; + else QNUM(dy) ; + else QNUM(dz) ; + else QNUM(dt) ; + else QNUM(du) ; + else QNUM(dv) ; + else QNUM(dw) ; + else QNUM(cal_min) ; + else QNUM(cal_max) ; + else QNUM(scl_slope) ; + else QNUM(scl_inter) ; + else QNUM(intent_code) ; + else QNUM(intent_p1) ; + else QNUM(intent_p2) ; + else QNUM(intent_p3) ; + else QSTR(intent_name,15) ; + else QNUM(toffset) ; + else QNUM(xyz_units) ; + else QNUM(time_units) ; + else QSTR(descrip,79) ; + else QSTR(aux_file,23) ; + else QNUM(qform_code) ; + else QNUM(quatern_b) ; + else QNUM(quatern_c) ; + else QNUM(quatern_d) ; + else QNUM(qoffset_x) ; + else QNUM(qoffset_y) ; + else QNUM(qoffset_z) ; + else QNUM(qfac) ; + else QNUM(sform_code) ; + else QNUM(freq_dim) ; + else QNUM(phase_dim) ; + else QNUM(slice_dim) ; + else QNUM(slice_code) ; + else QNUM(slice_start) ; + else QNUM(slice_end) ; + else QNUM(slice_duration) ; + else QNUM(num_ext) ; + + } /* end of while loop */ + + if( bytes_read ) *bytes_read = spos+1; /* "process" last '\n' */ + + /* do miscellaneous checking and cleanup */ + + if( nim->ndim <= 0 ){ nifti_image_free(nim); return NULL; } /* bad! */ + + nifti_datatype_sizes( nim->datatype, &(nim->nbyper), &(nim->swapsize) ); + if( nim->nbyper == 0 ){ nifti_image_free(nim); return NULL; } /* bad! */ + + nim->dim[0] = nim->ndim ; + nim->dim[1] = nim->nx ; nim->pixdim[1] = nim->dx ; + nim->dim[2] = nim->ny ; nim->pixdim[2] = nim->dy ; + nim->dim[3] = nim->nz ; nim->pixdim[3] = nim->dz ; + nim->dim[4] = nim->nt ; nim->pixdim[4] = nim->dt ; + nim->dim[5] = nim->nu ; nim->pixdim[5] = nim->du ; + nim->dim[6] = nim->nv ; nim->pixdim[6] = nim->dv ; + nim->dim[7] = nim->nw ; nim->pixdim[7] = nim->dw ; + + nim->nvox = (size_t)nim->nx * nim->ny * nim->nz + * nim->nt * nim->nu * nim->nv * nim->nw ; + + if( nim->qform_code > 0 ) + nim->qto_xyz = nifti_quatern_to_mat44( + nim->quatern_b, nim->quatern_c, nim->quatern_d, + nim->qoffset_x, nim->qoffset_y, nim->qoffset_z, + nim->dx , nim->dy , nim->dz , + nim->qfac ) ; + else + nim->qto_xyz = nifti_quatern_to_mat44( + 0.0 , 0.0 , 0.0 , 0.0 , 0.0 , 0.0 , + nim->dx , nim->dy , nim->dz , 0.0 ) ; + + + nim->qto_ijk = nifti_mat44_inverse( nim->qto_xyz ) ; + + if( nim->sform_code > 0 ) + nim->sto_ijk = nifti_mat44_inverse( nim->sto_xyz ) ; + + return nim ; +} + + +/*---------------------------------------------------------------------------*/ +/*! validate the nifti_image + + \return 1 if the structure seems valid, otherwise 0 + + \sa nifti_nim_has_valid_dims, nifti_hdr_looks_good +*//*-------------------------------------------------------------------------*/ +int nifti_nim_is_valid(nifti_image * nim, int complain) +{ + int errs = 0; + + if( !nim ){ + fprintf(stderr,"** is_valid_nim: nim is NULL\n"); + return 0; + } + + if( g_opts.debug > 2 ) fprintf(stderr,"-d nim_is_valid check...\n"); + + /**- check that dim[] matches the individual values ndim, nx, ny, ... */ + if( ! nifti_nim_has_valid_dims(nim,complain) ){ + if( !complain ) return 0; + errs++; + } + + /* might check nbyper, pixdim, q/sforms, swapsize, nifti_type, ... */ + + /**- be explicit in return of 0 or 1 */ + if( errs > 0 ) return 0; + else return 1; +} + +/*---------------------------------------------------------------------------*/ +/*! validate nifti dimensions + + \return 1 if valid, 0 if not + + \sa nifti_nim_is_valid, nifti_hdr_looks_good + + rely on dim[] as the master +*//*-------------------------------------------------------------------------*/ +int nifti_nim_has_valid_dims(nifti_image * nim, int complain) +{ + size_t prod; + int c, errs = 0; + + /**- start with dim[0]: failure here is considered terminal */ + if( nim->dim[0] <= 0 || nim->dim[0] > 7 ){ + errs++; + if( complain ) + fprintf(stderr,"** NVd: dim[0] (%d) out of range [1,7]\n",nim->dim[0]); + return 0; + } + + /**- check whether ndim equals dim[0] */ + if( nim->ndim != nim->dim[0] ){ + errs++; + if( ! complain ) return 0; + fprintf(stderr,"** NVd: ndim != dim[0] (%d,%d)\n",nim->ndim,nim->dim[0]); + } + + /**- compare each dim[i] to the proper nx, ny, ... */ + if( ( (nim->dim[0] >= 1) && (nim->dim[1] != nim->nx) ) || + ( (nim->dim[0] >= 2) && (nim->dim[2] != nim->ny) ) || + ( (nim->dim[0] >= 3) && (nim->dim[3] != nim->nz) ) || + ( (nim->dim[0] >= 4) && (nim->dim[4] != nim->nt) ) || + ( (nim->dim[0] >= 5) && (nim->dim[5] != nim->nu) ) || + ( (nim->dim[0] >= 6) && (nim->dim[6] != nim->nv) ) || + ( (nim->dim[0] >= 7) && (nim->dim[7] != nim->nw) ) ){ + errs++; + if( !complain ) return 0; + fprintf(stderr,"** NVd mismatch: dims = %d,%d,%d,%d,%d,%d,%d\n" + " nxyz... = %d,%d,%d,%d,%d,%d,%d\n", + nim->dim[1], nim->dim[2], nim->dim[3], + nim->dim[4], nim->dim[5], nim->dim[6], nim->dim[7], + nim->nx, nim->ny, nim->nz, + nim->nt, nim->nu, nim->nv, nim->nw ); + } + + if( g_opts.debug > 2 ){ + fprintf(stderr,"-d check dim[%d] =", nim->dim[0]); + for( c = 0; c < 7; c++ ) fprintf(stderr," %d", nim->dim[c]); + fputc('\n', stderr); + } + + /**- check the dimensions, and that their product matches nvox */ + prod = 1; + for( c = 1; c <= nim->dim[0]; c++ ){ + if( nim->dim[c] > 0) + prod *= nim->dim[c]; + else if( nim->dim[c] <= 0 ){ + if( !complain ) return 0; + fprintf(stderr,"** NVd: dim[%d] (=%d) <= 0\n",c, nim->dim[c]); + errs++; + } + } + if( prod != nim->nvox ){ + if( ! complain ) return 0; + fprintf(stderr,"** NVd: nvox does not match %d-dim product (%u, %u)\n", + nim->dim[0], (unsigned)nim->nvox, (unsigned)prod); + errs++; + } + + /**- if debug, warn about any remaining dim that is neither 0, nor 1 */ + /* (values in dims above dim[0] are undefined, as reminded by Cinly + Ooi and Alle Meije Wink) 16 Nov 2005 [rickr] */ + if( g_opts.debug > 1 ) + for( c = nim->dim[0]+1; c <= 7; c++ ) + if( nim->dim[c] != 0 && nim->dim[c] != 1 ) + fprintf(stderr,"** NVd warning: dim[%d] = %d, but ndim = %d\n", + c, nim->dim[c], nim->dim[0]); + + if( g_opts.debug > 2 ) + fprintf(stderr,"-d nim_has_valid_dims check, errs = %d\n", errs); + + /**- return invalid or valid */ + if( errs > 0 ) return 0; + else return 1; +} + + +/*---------------------------------------------------------------------------*/ +/*! read a nifti image, collapsed across dimensions according to dims[8]
+
+    This function may be used to read parts of a nifti dataset, such as
+    the time series for a single voxel, or perhaps a slice.  It is similar
+    to nifti_image_load(), though the passed 'data' parameter is used for
+    returning the image, not nim->data.
+
+    \param nim  given nifti_image struct, corresponding to the data file
+    \param dims given list of dimensions (see below)
+    \param data pointer to data pointer (if *data is NULL, data will be
+                allocated, otherwise not)
+
+    Here, dims is an array of 8 ints, similar to nim->dim[8].  While dims[0]
+    is unused at this point, the other indices specify which dimensions to
+    collapse (and at which index), and which not to collapse.  If dims[i] is
+    set to -1, then that entire dimension will be read in, from index 0 to
+    index (nim->dim[i] - 1).  If dims[i] >= 0, then only that index will be
+    read in (so dims[i] must also be < nim->dim[i]).
+
+    Example: given  nim->dim[8] = { 4, 64, 64, 21, 80, 1, 1, 1 } (4-D dataset)
+
+      if dims[8] = { 0,  5,  4, 17, -1, -1, -1, -1 }
+         -> read time series for voxel i,j,k = 5,4,17
+
+      if dims[8] = { 0, -1, -1, -1, 17, -1, -1, -1 }
+         -> read single volume at time point 17
+
+    Example: given  nim->dim[8] = { 6, 64, 64, 21, 80, 4, 3, 1 } (6-D dataset)
+
+      if dims[8] = { 0, 5, 4, 17, -1, 2, 1, 0 }
+         -> read time series for the voxel i,j,k = 5,4,17, and dim 5,6 = 2,1
+
+      if dims[8] = { 0, 5, 4, -1, -1, 0, 0, 0 }
+         -> read time series for slice at i,j = 5,4, and dim 5,6,7 = 0,0,0
+            (note that dims[7] is not relevant, but must be 0 or -1)
+
+    If *data is NULL, then *data will be set as a pointer to new memory,
+    allocated here for the resulting collapsed image data.
+
+      e.g. { int    dims[8] = { 0,  5,  4, 17, -1, -1, -1, -1 };
+             void * data    = NULL;
+             ret_val = nifti_read_collapsed_image(nim, dims, &data);
+             if( ret_val > 0 ){
+                process_time_series(data);
+                if( data != NULL ) free(data);
+             }
+           }
+
+    NOTE: If *data is not NULL, then it will be assumed that it points to
+          valid memory, sufficient to hold the results.  This is done for
+          speed and possibly repeated calls to this function.
+
+      e.g. { int    dims[8] = { 0,  -1, -1, -1, -1, -1, -1, -1 };
+             void * data    = NULL;
+             for( zslice = 0; zslice < nzslices; zslice++ ){
+                dims[3] = zslice;
+                ret_val = nifti_read_collapsed_image(nim, dims, &data);
+                if( ret_val > 0 ) process_slice(zslice, data);
+             }
+             if( data != NULL ) free(data);
+           }
+
+    \return
+        -  the total number of bytes read, or < 0 on failure
+        -  the read and byte-swapped data, in 'data'            
+ + \sa nifti_image_read, nifti_image_free, nifti_image_read_bricks + nifti_image_load +*//*-------------------------------------------------------------------------*/ +int nifti_read_collapsed_image( nifti_image * nim, const int dims [8], + void ** data ) +{ + znzFile fp; + int pivots[8], prods[8], nprods; /* sizes are bounded by dims[], so 8 */ + int c, bytes; + + /** - check pointers for sanity */ + if( !nim || !dims || !data ){ + fprintf(stderr,"** nifti_RCI: bad params %p, %p, %p\n", + (void *)nim, (void *)dims, (void *)data); + return -1; + } + + if( g_opts.debug > 2 ){ + fprintf(stderr,"-d read_collapsed_image:\n dims ="); + for(c = 0; c < 8; c++) fprintf(stderr," %3d", dims[c]); + fprintf(stderr,"\n nim->dims ="); + for(c = 0; c < 8; c++) fprintf(stderr," %3d", nim->dim[c]); + fputc('\n', stderr); + } + + /** - verify that dim[] makes sense */ + if( ! nifti_nim_is_valid(nim, g_opts.debug > 0) ){ + fprintf(stderr,"** invalid nim (file is '%s')\n", nim->fname ); + return -1; + } + + /** - verify that dims[] makes sense for this dataset */ + for( c = 1; c <= nim->dim[0]; c++ ){ + if( dims[c] >= nim->dim[c] ){ + fprintf(stderr,"** nifti_RCI: dims[%d] >= nim->dim[%d] (%d,%d)\n", + c, c, dims[c], nim->dim[c]); + return -1; + } + } + + /** - prepare pivot list - pivots are fixed indices */ + if( make_pivot_list(nim, dims, pivots, prods, &nprods) < 0 ) return -1; + + bytes = rci_alloc_mem(data, prods, nprods, nim->nbyper); + if( bytes < 0 ) return -1; + + /** - open the image file for reading at the appropriate offset */ + fp = nifti_image_load_prep( nim ); + if( ! fp ){ free(*data); *data = NULL; return -1; } /* failure */ + + /** - call the recursive reading function, passing nim, the pivot info, + location to store memory, and file pointer and position */ + c = rci_read_data(nim, pivots,prods,nprods,dims, + (char *)*data, fp, znztell(fp)); + znzclose(fp); /* in any case, close the file */ + if( c < 0 ){ free(*data); *data = NULL; return -1; } /* failure */ + + if( g_opts.debug > 1 ) + fprintf(stderr,"+d read %d bytes of collapsed image from %s\n", + bytes, nim->fname); + + return bytes; +} + + +/* local function to find strides per dimension. assumes 7D size and +** stride array. +*/ +static void +compute_strides(int *strides,const int *size,int nbyper) +{ + int i; + strides[0] = nbyper; + for(i = 1; i < 7; i++) + { + strides[i] = size[i-1] * strides[i-1]; + } +} + +/*---------------------------------------------------------------------------*/ +/*! read an arbitrary subregion from a nifti image + + This function may be used to read a single arbitary subregion of any + rectangular size from a nifti dataset, such as a small 5x5x5 subregion + around the center of a 3D image. + + \param nim given nifti_image struct, corresponding to the data file + \param start_index the index location of the first voxel that will be returned + \param region_size the size of the subregion to be returned + \param data pointer to data pointer (if *data is NULL, data will be + allocated, otherwise not) + + Example: given nim->dim[8] = {3, 64, 64, 64, 1, 1, 1, 1 } (3-D dataset) + + if start_index[7] = { 29, 29, 29, 0, 0, 0, 0 } and + region_size[7] = { 5, 5, 5, 1, 1, 1, 1 } + -> read 5x5x5 region starting with the first voxel location at (29,29,29) + + NOTE: If *data is not NULL, then it will be assumed that it points to + valid memory, sufficient to hold the results. This is done for + speed and possibly repeated calls to this function. + \return + - the total number of bytes read, or < 0 on failure + - the read and byte-swapped data, in 'data' + + \sa nifti_image_read, nifti_image_free, nifti_image_read_bricks + nifti_image_load, nifti_read_collapsed_image +*//*-------------------------------------------------------------------------*/ +int nifti_read_subregion_image( nifti_image * nim, + int *start_index, + int *region_size, + void ** data ) +{ + znzFile fp; /* file to read */ + int i,j,k,l,m,n; /* indices for dims */ + long int bytes = 0; /* total # bytes read */ + int total_alloc_size; /* size of buffer allocation */ + char *readptr; /* where in *data to read next */ + int strides[7]; /* strides between dimensions */ + int collapsed_dims[8]; /* for read_collapsed_image */ + int *image_size; /* pointer to dimensions in header */ + long int initial_offset; + long int offset; /* seek offset for reading current row */ + + /* probably ignored, but set to ndim for consistency*/ + collapsed_dims[0] = nim->ndim; + + /* build a dims array for collapsed image read */ + for(i = 0; i < nim->ndim; i++) + { + /* if you take the whole extent in this dimension */ + if(start_index[i] == 0 && + region_size[i] == nim->dim[i+1]) + { + collapsed_dims[i+1] = -1; + } + /* if you specify a single element in this dimension */ + else if(region_size[i] == 1) + { + collapsed_dims[i+1] = start_index[i]; + } + else + { + collapsed_dims[i+1] = -2; /* sentinel value */ + } + } + /* fill out end of collapsed_dims */ + for(i = nim->ndim ; i < 7; i++) + { + collapsed_dims[i+1] = -1; + } + + /* check to see whether collapsed read is possible */ + for(i = 1; i <= nim->ndim; i++) + { + if(collapsed_dims[i] == -2) + { + break; + } + } + + /* if you get through all the dimensions without hitting + ** a subrange of size > 1, a collapsed read is possible + */ + if(i > nim->ndim) + { + return nifti_read_collapsed_image(nim, collapsed_dims, data); + } + + /* point past first element of dim, which holds nim->ndim */ + image_size = &(nim->dim[1]); + + /* check region sizes for sanity */ + for(i = 0; i < nim->ndim; i++) + { + if(start_index[i] + region_size[i] > image_size[i]) + { + if(g_opts.debug > 1) + { + fprintf(stderr,"region doesn't fit within image size\n"); + } + return -1; + } + } + + /* get the file open */ + fp = nifti_image_load_prep( nim ); + /* the current offset is just past the nifti header, save + * location so that SEEK_SET can be used below + */ + initial_offset = znztell(fp); + /* get strides*/ + compute_strides(strides,image_size,nim->nbyper); + + total_alloc_size = nim->nbyper; /* size of pixel */ + + /* find alloc size */ + for(i = 0; i < nim->ndim; i++) + { + total_alloc_size *= region_size[i]; + } + /* allocate buffer, if necessary */ + if(*data == 0) + { + *data = (void *)malloc(total_alloc_size); + } + + if(*data == 0) + { + if(g_opts.debug > 1) + { + fprintf(stderr,"allocation of %d bytes failed\n",total_alloc_size); + return -1; + } + } + + /* point to start of data buffer as char * */ + readptr = *((char **)data); + { + /* can't assume that start_index and region_size have any more than + ** nim->ndim elements so make local copies, filled out to seven elements + */ + int si[7], rs[7]; + for(i = 0; i < nim->ndim; i++) + { + si[i] = start_index[i]; + rs[i] = region_size[i]; + } + for(i = nim->ndim; i < 7; i++) + { + si[i] = 0; + rs[i] = 1; + } + /* loop through subregion and read a row at a time */ + for(i = si[6]; i < (si[6] + rs[6]); i++) + { + for(j = si[5]; j < (si[5] + rs[5]); j++) + { + for(k = si[4]; k < (si[4] + rs[4]); k++) + { + for(l = si[3]; l < (si[3] + rs[3]); l++) + { + for(m = si[2]; m < (si[2] + rs[2]); m++) + { + for(n = si[1]; n < (si[1] + rs[1]); n++) + { + int nread,read_amount; + offset = initial_offset + + (i * strides[6]) + + (j * strides[5]) + + (k * strides[4]) + + (l * strides[3]) + + (m * strides[2]) + + (n * strides[1]) + + (si[0] * strides[0]); + znzseek(fp, offset, SEEK_SET); /* seek to current row */ + read_amount = rs[0] * nim->nbyper; /* read a row of the subregion*/ + nread = (int)nifti_read_buffer(fp, readptr, read_amount, nim); + if(nread != read_amount) + { + if(g_opts.debug > 1) + { + fprintf(stderr,"read of %d bytes failed\n",read_amount); + return -1; + } + } + bytes += nread; + readptr += read_amount; + } + } + } + } + } + } + } + return bytes; +} + + +/* read the data from the file pointed to by fp + + - this a recursive function, so start with the base case + - data is now (char *) for easy incrementing + + return 0 on success, < 0 on failure +*/ +static int rci_read_data(nifti_image * nim, int * pivots, int * prods, + int nprods, const int dims[], char * data, znzFile fp, size_t base_offset) +{ + size_t sublen, offset, read_size; + int c; + + /* bad check first - base_offset may not have been checked */ + if( nprods <= 0 ){ + fprintf(stderr,"** rci_read_data, bad prods, %d\n", nprods); + return -1; + } + + /* base case: actually read the data */ + if( nprods == 1 ){ + size_t nread, bytes; + + /* make sure things look good here */ + if( *pivots != 0 ){ + fprintf(stderr,"** rciRD: final pivot == %d!\n", *pivots); + return -1; + } + + /* so just seek and read (prods[0] * nbyper) bytes from the file */ + znzseek(fp, (long)base_offset, SEEK_SET); + bytes = (size_t)prods[0] * nim->nbyper; + nread = nifti_read_buffer(fp, data, bytes, nim); + if( nread != bytes ){ + fprintf(stderr,"** rciRD: read only %u of %u bytes from '%s'\n", + (unsigned)nread, (unsigned)bytes, nim->fname); + return -1; + } else if( g_opts.debug > 3 ) + fprintf(stderr,"+d successful read of %u bytes at offset %u\n", + (unsigned)bytes, (unsigned)base_offset); + + return 0; /* done with base case - return success */ + } + + /* not the base case, so do a set of reduced reads */ + + /* compute size of sub-brick: all dimensions below pivot */ + for( c = 1, sublen = 1; c < *pivots; c++ ) sublen *= nim->dim[c]; + + /* compute number of values to read, i.e. remaining prods */ + for( c = 1, read_size = 1; c < nprods; c++ ) read_size *= prods[c]; + read_size *= nim->nbyper; /* and multiply by bytes per voxel */ + + /* now repeatedly compute offsets, and recursively read */ + for( c = 0; c < prods[0]; c++ ){ + /* offset is (c * sub-block size (including pivot dim)) */ + /* + (dims[] index into pivot sub-block) */ + /* the unneeded multiplication is to make this more clear */ + offset = (size_t)c * sublen * nim->dim[*pivots] + + (size_t)sublen * dims[*pivots]; + offset *= nim->nbyper; + + if( g_opts.debug > 3 ) + fprintf(stderr,"-d reading %u bytes, foff %u + %u, doff %u\n", + (unsigned)read_size, (unsigned)base_offset, (unsigned)offset, + (unsigned)(c*read_size)); + + /* now read the next level down, adding this offset */ + if( rci_read_data(nim, pivots+1, prods+1, nprods-1, dims, + data + c * read_size, fp, base_offset + offset) < 0 ) + return -1; + } + + return 0; +} + + +/* allocate memory for all collapsed image data + + If *data is already set, do not allocate, but still calculate + size for debug report. + + return total size on success, and < 0 on failure +*/ +static int rci_alloc_mem(void ** data, int prods[8], int nprods, int nbyper ) +{ + int size, index; + + if( nbyper < 0 || nprods < 1 || nprods > 8 ){ + fprintf(stderr,"** rci_am: bad params, %d, %d\n", nbyper, nprods); + return -1; + } + + for( index = 0, size = 1; index < nprods; index++ ) + size *= prods[index]; + + size *= nbyper; + + if( ! *data ){ /* then allocate what is needed */ + if( g_opts.debug > 1 ) + fprintf(stderr,"+d alloc %d (= %d x %d) bytes for collapsed image\n", + size, size/nbyper, nbyper); + + *data = malloc(size); /* actually allocate the memory */ + if( ! *data ){ + fprintf(stderr,"** rci_am: failed to alloc %d bytes for data\n", size); + return -1; + } + } else if( g_opts.debug > 1 ) + fprintf(stderr,"-d rci_am: *data already set, need %d (%d x %d) bytes\n", + size, size/nbyper, nbyper); + + return size; +} + + +/* prepare a pivot list for reading + + The pivot points are the indices into dims where the calling function + wants to collapse a dimension. The last pivot should always be zero + (note that we have space for that in the lists). +*/ +static int make_pivot_list(nifti_image * nim, const int dims[], int pivots[], + int prods[], int * nprods ) +{ + int len, index; + + len = 0; + index = nim->dim[0]; + while( index > 0 ){ + prods[len] = 1; + while( index > 0 && (nim->dim[index] == 1 || dims[index] == -1) ){ + prods[len] *= nim->dim[index]; + index--; + } + pivots[len] = index; + len++; + index--; /* fine, let it drop out at -1 */ + } + + /* make sure to include 0 as a pivot (instead of just 1, if it is) */ + if( pivots[len-1] != 0 ){ + pivots[len] = 0; + prods[len] = 1; + len++; + } + + *nprods = len; + + if( g_opts.debug > 2 ){ + fprintf(stderr,"+d pivot list created, pivots :"); + for(index = 0; index < len; index++) fprintf(stderr," %d", pivots[index]); + fprintf(stderr,", prods :"); + for(index = 0; index < len; index++) fprintf(stderr," %d", prods[index]); + fputc('\n',stderr); + } + + return 0; +} + + +#undef ISEND +#define ISEND(c) ( (c)==']' || (c)=='}' || (c)=='\0' ) + +/*---------------------------------------------------------------------*/ +/*! Get an integer list in the range 0..(nvals-1), from the + character string str. If we call the output pointer fred, + then fred[0] = number of integers in the list (> 0), and + fred[i] = i-th integer in the list for i=1..fred[0]. + If on return, fred == NULL or fred[0] == 0, then something is + wrong, and the caller must deal with that. + + Syntax of input string: + - initial '{' or '[' is skipped, if present + - ends when '}' or ']' or end of string is found + - contains entries separated by commas + - entries have one of these forms: + - a single number + - a dollar sign '$', which means nvals-1 + - a sequence of consecutive numbers in the form "a..b" or + "a-b", where "a" and "b" are single numbers (or '$') + - a sequence of evenly spaced numbers in the form + "a..b(c)" or "a-b(c)", where "c" encodes the step + - Example: "[2,7..4,3..9(2)]" decodes to the list + 2 7 6 5 4 3 5 7 9 + - entries should be in the range 0..nvals-1 + + (borrowed, with permission, from thd_intlist.c) +*//*-------------------------------------------------------------------*/ +int * nifti_get_intlist( int nvals , const char * str ) +{ + int *subv = NULL ; + int ii , ipos , nout , slen ; + int ibot,itop,istep , nused ; + char *cpt ; + + /* Meaningless input? */ + if( nvals < 1 ) return NULL ; + + /* No selection list? */ + if( str == NULL || str[0] == '\0' ) return NULL ; + + /* skip initial '[' or '{' */ + subv = (int *)malloc( sizeof(int) * 2 ) ; + if( !subv ) { + fprintf(stderr,"** nifti_get_intlist: failed alloc of 2 ints\n"); + return NULL; + } + subv[0] = nout = 0 ; + + ipos = 0 ; + if( str[ipos] == '[' || str[ipos] == '{' ) ipos++ ; + + if( g_opts.debug > 1 ) + fprintf(stderr,"-d making int_list (vals = %d) from '%s'\n", nvals, str); + + /**- for each sub-selector until end of input... */ + + slen = (int)strlen(str) ; + while( ipos < slen && !ISEND(str[ipos]) ){ + + while( isspace((int) str[ipos]) ) ipos++ ; /* skip blanks */ + if( ISEND(str[ipos]) ) break ; /* done */ + + /**- get starting value */ + + if( str[ipos] == '$' ){ /* special case */ + ibot = nvals-1 ; ipos++ ; + } else { /* decode an integer */ + ibot = strtol( str+ipos , &cpt , 10 ) ; + if( ibot < 0 ){ + fprintf(stderr,"** ERROR: list index %d is out of range 0..%d\n", + ibot,nvals-1) ; + free(subv) ; return NULL ; + } + if( ibot >= nvals ){ + fprintf(stderr,"** ERROR: list index %d is out of range 0..%d\n", + ibot,nvals-1) ; + free(subv) ; return NULL ; + } + nused = (cpt-(str+ipos)) ; + if( ibot == 0 && nused == 0 ){ + fprintf(stderr,"** ERROR: list syntax error '%s'\n",str+ipos) ; + free(subv) ; return NULL ; + } + ipos += nused ; + } + + while( isspace((int) str[ipos]) ) ipos++ ; /* skip blanks */ + + /**- if that's it for this sub-selector, add one value to list */ + + if( str[ipos] == ',' || ISEND(str[ipos]) ){ + nout++ ; + subv = (int *)realloc( (char *)subv , sizeof(int) * (nout+1) ) ; + if( !subv ) { + fprintf(stderr,"** nifti_get_intlist: failed realloc of %d ints\n", + nout+1); + return NULL; + } + subv[0] = nout ; + subv[nout] = ibot ; + if( ISEND(str[ipos]) ) break ; /* done */ + ipos++ ; continue ; /* re-start loop at next sub-selector */ + } + + /**- otherwise, must have '..' or '-' as next inputs */ + + if( str[ipos] == '-' ){ + ipos++ ; + } else if( str[ipos] == '.' && str[ipos+1] == '.' ){ + ipos++ ; ipos++ ; + } else { + fprintf(stderr,"** ERROR: index list syntax is bad: '%s'\n", + str+ipos) ; + free(subv) ; return NULL ; + } + + /**- get ending value for loop now */ + + if( str[ipos] == '$' ){ /* special case */ + itop = nvals-1 ; ipos++ ; + } else { /* decode an integer */ + itop = strtol( str+ipos , &cpt , 10 ) ; + if( itop < 0 ){ + fprintf(stderr,"** ERROR: index %d is out of range 0..%d\n", + itop,nvals-1) ; + free(subv) ; return NULL ; + } + if( itop >= nvals ){ + fprintf(stderr,"** ERROR: index %d is out of range 0..%d\n", + itop,nvals-1) ; + free(subv) ; return NULL ; + } + nused = (cpt-(str+ipos)) ; + if( itop == 0 && nused == 0 ){ + fprintf(stderr,"** ERROR: index list syntax error '%s'\n",str+ipos) ; + free(subv) ; return NULL ; + } + ipos += nused ; + } + + /**- set default loop step */ + + istep = (ibot <= itop) ? 1 : -1 ; + + while( isspace((int) str[ipos]) ) ipos++ ; /* skip blanks */ + + /**- check if we have a non-default loop step */ + + if( str[ipos] == '(' ){ /* decode an integer */ + ipos++ ; + istep = strtol( str+ipos , &cpt , 10 ) ; + if( istep == 0 ){ + fprintf(stderr,"** ERROR: index loop step is 0!\n") ; + free(subv) ; return NULL ; + } + nused = (cpt-(str+ipos)) ; + ipos += nused ; + if( str[ipos] == ')' ) ipos++ ; + if( (ibot-itop)*istep > 0 ){ + fprintf(stderr,"** WARNING: index list '%d..%d(%d)' means nothing\n", + ibot,itop,istep ) ; + } + } + + /**- add values to output */ + + for( ii=ibot ; (ii-itop)*istep <= 0 ; ii += istep ){ + nout++ ; + subv = (int *)realloc( (char *)subv , sizeof(int) * (nout+1) ) ; + if( !subv ) { + fprintf(stderr,"** nifti_get_intlist: failed realloc of %d ints\n", + nout+1); + return NULL; + } + subv[0] = nout ; + subv[nout] = ii ; + } + + /**- check if we have a comma to skip over */ + + while( isspace((int) str[ipos]) ) ipos++ ; /* skip blanks */ + if( str[ipos] == ',' ) ipos++ ; /* skip commas */ + + } /* end of loop through selector string */ + + if( g_opts.debug > 1 ) { + fprintf(stderr,"+d int_list (vals = %d): ", subv[0]); + for( ii = 1; ii <= subv[0]; ii++ ) fprintf(stderr,"%d ", subv[ii]); + fputc('\n',stderr); + } + + if( subv[0] == 0 ){ free(subv); subv = NULL; } + return subv ; +} + +/*---------------------------------------------------------------------*/ +/*! Given a NIFTI_TYPE string, such as "NIFTI_TYPE_INT16", return the + * corresponding integral type code. The type code is the macro + * value defined in nifti1.h. +*//*-------------------------------------------------------------------*/ +int nifti_datatype_from_string( const char * name ) +{ + int tablen = sizeof(nifti_type_list)/sizeof(nifti_type_ele); + int c; + + if( !name ) return DT_UNKNOWN; + + for( c = tablen-1; c > 0; c-- ) + if( !strcmp(name, nifti_type_list[c].name) ) + break; + + return nifti_type_list[c].type; +} + + +/*---------------------------------------------------------------------*/ +/*! Given a NIFTI_TYPE value, such as NIFTI_TYPE_INT16, return the + * corresponding macro label as a string. The dtype code is the + * macro value defined in nifti1.h. +*//*-------------------------------------------------------------------*/ +char * nifti_datatype_to_string( int dtype ) +{ + int tablen = sizeof(nifti_type_list)/sizeof(nifti_type_ele); + int c; + + for( c = tablen-1; c > 0; c-- ) + if( nifti_type_list[c].type == dtype ) + break; + + return nifti_type_list[c].name; +} + + +/*---------------------------------------------------------------------*/ +/*! Determine whether dtype is a valid NIFTI_TYPE. + * + * DT_UNKNOWN is considered invalid + * + * The only difference 'for_nifti' makes is that DT_BINARY + * should be invalid for a NIfTI dataset. +*//*-------------------------------------------------------------------*/ +int nifti_datatype_is_valid( int dtype, int for_nifti ) +{ + int tablen = sizeof(nifti_type_list)/sizeof(nifti_type_ele); + int c; + + /* special case */ + if( for_nifti && dtype == DT_BINARY ) return 0; + + for( c = tablen-1; c > 0; c-- ) + if( nifti_type_list[c].type == dtype ) + return 1; + + return 0; +} + + +/*---------------------------------------------------------------------*/ +/*! Only as a test, verify that the new nifti_type_list table matches + * the the usage of nifti_datatype_sizes (which could be changed to + * use the table, if there were interest). + * + * return the number of errors (so 0 is success, as usual) +*//*-------------------------------------------------------------------*/ +int nifti_test_datatype_sizes(int verb) +{ + int tablen = sizeof(nifti_type_list)/sizeof(nifti_type_ele); + int nbyper, ssize; + int c, errs = 0; + + for( c = 0; c < tablen; c++ ) + { + nbyper = ssize = -1; + nifti_datatype_sizes(nifti_type_list[c].type, &nbyper, &ssize); + if( nbyper < 0 || ssize < 0 || + nbyper != nifti_type_list[c].nbyper || + ssize != nifti_type_list[c].swapsize ) + { + if( verb || g_opts.debug > 2 ) + fprintf(stderr, "** type mismatch: %s, %d, %d, %d : %d, %d\n", + nifti_type_list[c].name, nifti_type_list[c].type, + nifti_type_list[c].nbyper, nifti_type_list[c].swapsize, + nbyper, ssize); + errs++; + } + } + + if( errs ) + fprintf(stderr,"** nifti_test_datatype_sizes: found %d errors\n",errs); + else if( verb || g_opts.debug > 1 ) + fprintf(stderr,"-- nifti_test_datatype_sizes: all OK\n"); + + return errs; +} + + +/*---------------------------------------------------------------------*/ +/*! Display the nifti_type_list table. + * + * if which == 1 : display DT_* + * if which == 2 : display NIFTI_TYPE* + * else : display all +*//*-------------------------------------------------------------------*/ +int nifti_disp_type_list( int which ) +{ + char * style; + int tablen = sizeof(nifti_type_list)/sizeof(nifti_type_ele); + int lwhich, c; + + if ( which == 1 ){ lwhich = 1; style = "DT_"; } + else if( which == 2 ){ lwhich = 2; style = "NIFTI_TYPE_"; } + else { lwhich = 3; style = "ALL"; } + + printf("nifti_type_list entries (%s) :\n" + " name type nbyper swapsize\n" + " --------------------- ---- ------ --------\n", style); + + for( c = 0; c < tablen; c++ ) + if( (lwhich & 1 && nifti_type_list[c].name[0] == 'D') || + (lwhich & 2 && nifti_type_list[c].name[0] == 'N') ) + printf(" %-22s %5d %3d %5d\n", + nifti_type_list[c].name, + nifti_type_list[c].type, + nifti_type_list[c].nbyper, + nifti_type_list[c].swapsize); + + return 0; +} + + diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/nifti/nifti1_io.h b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/nifti/nifti1_io.h new file mode 100755 index 00000000..2cdd077b --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/nifti/nifti1_io.h @@ -0,0 +1,551 @@ +/** \file nifti1_io.h + \brief Data structures for using nifti1_io API. + - Written by Bob Cox, SSCC NIMH + - Revisions by Rick Reynolds, SSCC NIMH + */ +#ifndef _NIFTI_IO_HEADER_ +#define _NIFTI_IO_HEADER_ + +#include +#include +#include +#include +#include + +#ifndef DONT_INCLUDE_ANALYZE_STRUCT +#define DONT_INCLUDE_ANALYZE_STRUCT /*** not needed herein ***/ +#endif +#include "nifti1.h" /*** NIFTI-1 header specification ***/ + +#include + +/*=================*/ +#ifdef __cplusplus +extern "C" { +#endif + /*=================*/ + + /*****===================================================================*****/ + /***** File nifti1_io.h == Declarations for nifti1_io.c *****/ + /*****...................................................................*****/ + /***** This code is released to the public domain. *****/ + /*****...................................................................*****/ + /***** Author: Robert W Cox, SSCC/DIRP/NIMH/NIH/DHHS/USA/EARTH *****/ + /***** Date: August 2003 *****/ + /*****...................................................................*****/ + /***** Neither the National Institutes of Health (NIH), nor any of its *****/ + /***** employees imply any warranty of usefulness of this software for *****/ + /***** any purpose, and do not assume any liability for damages, *****/ + /***** incidental or otherwise, caused by any use of this document. *****/ + /*****===================================================================*****/ + + /* + Modified by: Mark Jenkinson (FMRIB Centre, University of Oxford, UK) + Date: July/August 2004 + + Mainly adding low-level IO and changing things to allow gzipped files + to be read and written + Full backwards compatability should have been maintained + + Modified by: Rick Reynolds (SSCC/DIRP/NIMH, National Institutes of Health) + Date: December 2004 + + Modified and added many routines for I/O. + */ + + /********************** Some sample data structures **************************/ + + typedef struct /** 4x4 matrix struct **/ + { + float m[4][4] ; + } mat44 ; + + typedef struct /** 3x3 matrix struct **/ + { + float m[3][3] ; + } mat33 ; + + /*...........................................................................*/ + + /*! \enum analyze_75_orient_code + * \brief Old-style analyze75 orientation + * codes. + */ + typedef enum _analyze75_orient_code + { + a75_transverse_unflipped = 0, + a75_coronal_unflipped = 1, + a75_sagittal_unflipped = 2, + a75_transverse_flipped = 3, + a75_coronal_flipped = 4, + a75_sagittal_flipped = 5, + a75_orient_unknown = 6 + } analyze_75_orient_code; + + /*! \struct nifti_image + \brief High level data structure for open nifti datasets in the + nifti1_io API. Note that this structure is not part of the + nifti1 format definition; it is used to implement one API + for reading/writing formats in the nifti1 format. + */ + typedef struct /*!< Image storage struct **/ + _nifti_image { + + int ndim ; /*!< last dimension greater than 1 (1..7) */ + int nx ; /*!< dimensions of grid array */ + int ny ; /*!< dimensions of grid array */ + int nz ; /*!< dimensions of grid array */ + int nt ; /*!< dimensions of grid array */ + int nu ; /*!< dimensions of grid array */ + int nv ; /*!< dimensions of grid array */ + int nw ; /*!< dimensions of grid array */ + int dim[8] ; /*!< dim[0]=ndim, dim[1]=nx, etc. */ + size_t nvox ; /*!< number of voxels = nx*ny*nz*...*nw */ + int nbyper ; /*!< bytes per voxel, matches datatype */ + int datatype ; /*!< type of data in voxels: DT_* code */ + + float dx ; /*!< grid spacings */ + float dy ; /*!< grid spacings */ + float dz ; /*!< grid spacings */ + float dt ; /*!< grid spacings */ + float du ; /*!< grid spacings */ + float dv ; /*!< grid spacings */ + float dw ; /*!< grid spacings */ + float pixdim[8] ; /*!< pixdim[1]=dx, etc. */ + + float scl_slope ; /*!< scaling parameter - slope */ + float scl_inter ; /*!< scaling parameter - intercept */ + + float cal_min ; /*!< calibration parameter, minimum */ + float cal_max ; /*!< calibration parameter, maximum */ + + int qform_code ; /*!< codes for (x,y,z) space meaning */ + int sform_code ; /*!< codes for (x,y,z) space meaning */ + + int freq_dim ; /*!< indexes (1,2,3, or 0) for MRI */ + int phase_dim ; /*!< directions in dim[]/pixdim[] */ + int slice_dim ; /*!< directions in dim[]/pixdim[] */ + + int slice_code ; /*!< code for slice timing pattern */ + int slice_start ; /*!< index for start of slices */ + int slice_end ; /*!< index for end of slices */ + float slice_duration ; /*!< time between individual slices */ + + /*! quaternion transform parameters + [when writing a dataset, these are used for qform, NOT qto_xyz] */ + float quatern_b , quatern_c , quatern_d , + qoffset_x , qoffset_y , qoffset_z , + qfac ; + + mat44 qto_xyz ; /*!< qform: transform (i,j,k) to (x,y,z) */ + mat44 qto_ijk ; /*!< qform: transform (x,y,z) to (i,j,k) */ + + mat44 sto_xyz ; /*!< sform: transform (i,j,k) to (x,y,z) */ + mat44 sto_ijk ; /*!< sform: transform (x,y,z) to (i,j,k) */ + + float toffset ; /*!< time coordinate offset */ + + int xyz_units ; /*!< dx,dy,dz units: NIFTI_UNITS_* code */ + int time_units ; /*!< dt units: NIFTI_UNITS_* code */ + + int nifti_type ; /*!< 0==ANALYZE, 1==NIFTI-1 (1 file), + 2==NIFTI-1 (2 files), + 3==NIFTI-ASCII (1 file) */ + int intent_code ; /*!< statistic type (or something) */ + float intent_p1 ; /*!< intent parameters */ + float intent_p2 ; /*!< intent parameters */ + float intent_p3 ; /*!< intent parameters */ + char intent_name[16] ; /*!< optional description of intent data */ + + char descrip[80] ; /*!< optional text to describe dataset */ + char aux_file[24] ; /*!< auxiliary filename */ + + char *fname ; /*!< header filename (.hdr or .nii) */ + char *iname ; /*!< image filename (.img or .nii) */ + int iname_offset ; /*!< offset into iname where data starts */ + int swapsize ; /*!< swap unit in image data (might be 0) */ + int byteorder ; /*!< byte order on disk (MSB_ or LSB_FIRST) */ + void *data ; /*!< pointer to data: nbyper*nvox bytes */ + + int num_ext ; /*!< number of extensions in ext_list */ + nifti1_extension * ext_list ; /*!< array of extension structs (with data) */ + analyze_75_orient_code analyze75_orient; /*!< for old analyze files, orient */ + + } nifti_image ; + + + + /* struct for return from nifti_image_read_bricks() */ + typedef struct + { + int nbricks; /* the number of allocated pointers in 'bricks' */ + size_t bsize; /* the length of each data block, in bytes */ + void ** bricks; /* array of pointers to data blocks */ + } nifti_brick_list; + + + /*****************************************************************************/ + /*------------------ NIfTI version of ANALYZE 7.5 structure -----------------*/ + + /* (based on fsliolib/dbh.h, but updated for version 7.5) */ + + typedef struct + { + /* header info fields - describes the header overlap with NIfTI */ + /* ------------------ */ + int sizeof_hdr; /* 0 + 4 same */ + char data_type[10]; /* 4 + 10 same */ + char db_name[18]; /* 14 + 18 same */ + int extents; /* 32 + 4 same */ + short int session_error; /* 36 + 2 same */ + char regular; /* 38 + 1 same */ + char hkey_un0; /* 39 + 1 40 bytes */ + + /* image dimension fields - describes image sizes */ + short int dim[8]; /* 0 + 16 same */ + short int unused8; /* 16 + 2 intent_p1... */ + short int unused9; /* 18 + 2 ... */ + short int unused10; /* 20 + 2 intent_p2... */ + short int unused11; /* 22 + 2 ... */ + short int unused12; /* 24 + 2 intent_p3... */ + short int unused13; /* 26 + 2 ... */ + short int unused14; /* 28 + 2 intent_code */ + short int datatype; /* 30 + 2 same */ + short int bitpix; /* 32 + 2 same */ + short int dim_un0; /* 34 + 2 slice_start */ + float pixdim[8]; /* 36 + 32 same */ + + float vox_offset; /* 68 + 4 same */ + float funused1; /* 72 + 4 scl_slope */ + float funused2; /* 76 + 4 scl_inter */ + float funused3; /* 80 + 4 slice_end, */ + /* slice_code, */ + /* xyzt_units */ + float cal_max; /* 84 + 4 same */ + float cal_min; /* 88 + 4 same */ + float compressed; /* 92 + 4 slice_duration */ + float verified; /* 96 + 4 toffset */ + int glmax,glmin; /* 100 + 8 108 bytes */ + + /* data history fields - optional */ + char descrip[80]; /* 0 + 80 same */ + char aux_file[24]; /* 80 + 24 same */ + char orient; /* 104 + 1 NO GOOD OVERLAP */ + char originator[10]; /* 105 + 10 FROM HERE DOWN... */ + char generated[10]; /* 115 + 10 */ + char scannum[10]; /* 125 + 10 */ + char patient_id[10]; /* 135 + 10 */ + char exp_date[10]; /* 145 + 10 */ + char exp_time[10]; /* 155 + 10 */ + char hist_un0[3]; /* 165 + 3 */ + int views; /* 168 + 4 */ + int vols_added; /* 172 + 4 */ + int start_field; /* 176 + 4 */ + int field_skip; /* 180 + 4 */ + int omax, omin; /* 184 + 8 */ + int smax, smin; /* 192 + 8 200 bytes */ + } nifti_analyze75; /* total: 348 bytes */ + + + /*****************************************************************************/ + /*--------------- Prototypes of functions defined in this file --------------*/ + + char *nifti_datatype_string ( int dt ) ; + char *nifti_units_string ( int uu ) ; + char *nifti_intent_string ( int ii ) ; + char *nifti_xform_string ( int xx ) ; + char *nifti_slice_string ( int ss ) ; + char *nifti_orientation_string( int ii ) ; + + int nifti_is_inttype( int dt ) ; + + mat44 nifti_mat44_inverse( mat44 R ) ; + + mat33 nifti_mat33_inverse( mat33 R ) ; + mat33 nifti_mat33_polar ( mat33 A ) ; + float nifti_mat33_rownorm( mat33 A ) ; + float nifti_mat33_colnorm( mat33 A ) ; + float nifti_mat33_determ ( mat33 R ) ; + mat33 nifti_mat33_mul ( mat33 A , mat33 B ) ; + + void nifti_swap_2bytes ( size_t n , void *ar ) ; + void nifti_swap_4bytes ( size_t n , void *ar ) ; + void nifti_swap_8bytes ( size_t n , void *ar ) ; + void nifti_swap_16bytes( size_t n , void *ar ) ; + void nifti_swap_Nbytes ( size_t n , int siz , void *ar ) ; + + int nifti_datatype_is_valid (int dtype, int for_nifti); + int nifti_datatype_from_string(const char * name); + char * nifti_datatype_to_string (int dtype); + + int nifti_get_filesize( const char *pathname ) ; + void swap_nifti_header ( struct nifti_1_header *h , int is_nifti ) ; + void old_swap_nifti_header( struct nifti_1_header *h , int is_nifti ); + int nifti_swap_as_analyze( nifti_analyze75 *h ); + + + /* main read/write routines */ + + nifti_image *nifti_image_read_bricks(const char *hname , int nbricks, + const int *blist, nifti_brick_list * NBL); + int nifti_image_load_bricks(nifti_image *nim , int nbricks, + const int *blist, nifti_brick_list * NBL); + void nifti_free_NBL( nifti_brick_list * NBL ); + + nifti_image *nifti_image_read ( const char *hname , int read_data ) ; + int nifti_image_load ( nifti_image *nim ) ; + void nifti_image_unload ( nifti_image *nim ) ; + void nifti_image_free ( nifti_image *nim ) ; + + int nifti_read_collapsed_image( nifti_image * nim, const int dims [8], + void ** data ); + + int nifti_read_subregion_image( nifti_image * nim, + int *start_index, int *region_size, + void ** data ); + + void nifti_image_write ( nifti_image * nim ) ; + void nifti_image_write_bricks(nifti_image * nim, + const nifti_brick_list * NBL); + void nifti_image_infodump( const nifti_image * nim ) ; + + void nifti_disp_lib_hist( void ) ; /* to display library history */ + void nifti_disp_lib_version( void ) ; /* to display library version */ + int nifti_disp_matrix_orient( const char * mesg, mat44 mat ); + int nifti_disp_type_list( int which ); + + + char * nifti_image_to_ascii ( const nifti_image * nim ) ; + nifti_image *nifti_image_from_ascii( const char * str, int * bytes_read ) ; + + size_t nifti_get_volsize(const nifti_image *nim) ; + + /* basic file operations */ + int nifti_set_filenames(nifti_image * nim, const char * prefix, int check, + int set_byte_order); + char * nifti_makehdrname (const char * prefix, int nifti_type, int check, + int comp); + char * nifti_makeimgname (const char * prefix, int nifti_type, int check, + int comp); + int is_nifti_file (const char *hname); + char * nifti_find_file_extension(const char * name); + int nifti_is_complete_filename(const char* fname); + int nifti_validfilename(const char* fname); + + int disp_nifti_1_header(const char * info, const nifti_1_header * hp ) ; + void nifti_set_debug_level( int level ) ; + void nifti_set_skip_blank_ext( int skip ) ; + void nifti_set_allow_upper_fext( int allow ) ; + + int valid_nifti_brick_list(nifti_image * nim , int nbricks, + const int * blist, int disp_error); + + /* znzFile operations */ + znzFile nifti_image_open(const char * hname, char * opts, nifti_image ** nim); + znzFile nifti_image_write_hdr_img(nifti_image *nim, int write_data, + const char* opts); + znzFile nifti_image_write_hdr_img2( nifti_image *nim , int write_opts , + const char* opts, znzFile imgfile, const nifti_brick_list * NBL); + size_t nifti_read_buffer(znzFile fp, void* datatptr, size_t ntot, + nifti_image *nim); + int nifti_write_all_data(znzFile fp, nifti_image * nim, + const nifti_brick_list * NBL); + size_t nifti_write_buffer(znzFile fp, const void * buffer, size_t numbytes); + nifti_image *nifti_read_ascii_image(znzFile fp, char *fname, int flen, + int read_data); + znzFile nifti_write_ascii_image(nifti_image *nim, const nifti_brick_list * NBL, + const char * opts, int write_data, int leave_open); + + + void nifti_datatype_sizes( int datatype , int *nbyper, int *swapsize ) ; + + void nifti_mat44_to_quatern( mat44 R , + float *qb, float *qc, float *qd, + float *qx, float *qy, float *qz, + float *dx, float *dy, float *dz, float *qfac ) ; + + mat44 nifti_quatern_to_mat44( float qb, float qc, float qd, + float qx, float qy, float qz, + float dx, float dy, float dz, float qfac ); + + mat44 nifti_make_orthog_mat44( float r11, float r12, float r13 , + float r21, float r22, float r23 , + float r31, float r32, float r33 ) ; + + int nifti_short_order(void) ; /* CPU byte order */ + + + /* Orientation codes that might be returned from nifti_mat44_to_orientation().*/ + +#define NIFTI_L2R 1 /* Left to Right */ +#define NIFTI_R2L 2 /* Right to Left */ +#define NIFTI_P2A 3 /* Posterior to Anterior */ +#define NIFTI_A2P 4 /* Anterior to Posterior */ +#define NIFTI_I2S 5 /* Inferior to Superior */ +#define NIFTI_S2I 6 /* Superior to Inferior */ + + void nifti_mat44_to_orientation( mat44 R , int *icod, int *jcod, int *kcod ) ; + + /*--------------------- Low level IO routines ------------------------------*/ + + char * nifti_findhdrname (const char* fname); + char * nifti_findimgname (const char* fname , int nifti_type); + int nifti_is_gzfile (const char* fname); + + char * nifti_makebasename(const char* fname); + + + /* other routines */ + struct nifti_1_header nifti_convert_nim2nhdr(const nifti_image *nim); + nifti_1_header * nifti_make_new_header(const int arg_dims[], int arg_dtype); + nifti_1_header * nifti_read_header(const char *hname, int *swapped, int check); + nifti_image * nifti_copy_nim_info(const nifti_image * src); + nifti_image * nifti_make_new_nim(const int dims[], int datatype, + int data_fill); + nifti_image * nifti_simple_init_nim(void); + nifti_image * nifti_convert_nhdr2nim(struct nifti_1_header nhdr, + const char * fname); + + int nifti_hdr_looks_good (const nifti_1_header * hdr); + int nifti_is_valid_datatype (int dtype); + int nifti_is_valid_ecode (int ecode); + int nifti_nim_is_valid (nifti_image * nim, int complain); + int nifti_nim_has_valid_dims (nifti_image * nim, int complain); + int is_valid_nifti_type (int nifti_type); + int nifti_test_datatype_sizes (int verb); + int nifti_type_and_names_match (nifti_image * nim, int show_warn); + int nifti_update_dims_from_array(nifti_image * nim); + void nifti_set_iname_offset (nifti_image *nim); + int nifti_set_type_from_names (nifti_image * nim); + int nifti_add_extension(nifti_image * nim, const char * data, int len, + int ecode ); + int nifti_compiled_with_zlib (void); + int nifti_copy_extensions (nifti_image *nim_dest,const nifti_image *nim_src); + int nifti_free_extensions (nifti_image *nim); + int * nifti_get_intlist (int nvals , const char *str); + char * nifti_strdup (const char *str); + int valid_nifti_extensions(const nifti_image *nim); + + + /*-------------------- Some C convenience macros ----------------------------*/ + + /* NIfTI-1.1 extension codes: + see http://nifti.nimh.nih.gov/nifti-1/documentation/faq#Q21 */ + +#define NIFTI_ECODE_IGNORE 0 /* changed from UNKNOWN, 29 June 2005 */ + +#define NIFTI_ECODE_DICOM 2 /* intended for raw DICOM attributes */ + +#define NIFTI_ECODE_AFNI 4 /* Robert W Cox: rwcox@nih.gov +http://afni.nimh.nih.gov/afni */ + +#define NIFTI_ECODE_COMMENT 6 /* plain ASCII text only */ + +#define NIFTI_ECODE_XCEDE 8 /* David B Keator: dbkeator@uci.edu +http://www.nbirn.net/Resources + /Users/Applications/ + /xcede/index.htm */ + +#define NIFTI_ECODE_JIMDIMINFO 10 /* Mark A Horsfield: + mah5@leicester.ac.uk +http://someplace/something */ + +#define NIFTI_ECODE_WORKFLOW_FWDS 12 /* Kate Fissell: fissell@pitt.edu +http://kraepelin.wpic.pitt.edu + /~fissell/NIFTI_ECODE_WORKFLOW_FWDS + /NIFTI_ECODE_WORKFLOW_FWDS.html */ + +#define NIFTI_ECODE_FREESURFER 14 /* http://surfer.nmr.mgh.harvard.edu */ + +#define NIFTI_ECODE_PYPICKLE 16 /* embedded Python objects +http://niftilib.sourceforge.net + /pynifti */ + + /* LONI MiND codes: http://www.loni.ucla.edu/twiki/bin/view/Main/MiND */ +#define NIFTI_ECODE_MIND_IDENT 18 /* Vishal Patel: vishal.patel@ucla.edu*/ +#define NIFTI_ECODE_B_VALUE 20 +#define NIFTI_ECODE_SPHERICAL_DIRECTION 22 +#define NIFTI_ECODE_DT_COMPONENT 24 +#define NIFTI_ECODE_SHC_DEGREEORDER 26 /* end LONI MiND codes */ + +#define NIFTI_ECODE_VOXBO 28 /* Dan Kimberg: www.voxbo.org */ + +#define NIFTI_ECODE_CARET 30 /* John Harwell: john@brainvis.wustl.edu +http://brainvis.wustl.edu/wiki +/index.php/Caret:Documentation +:CaretNiftiExtension */ + +#define NIFTI_MAX_ECODE 30 /******* maximum extension code *******/ + + /* nifti_type file codes */ +#define NIFTI_FTYPE_ANALYZE 0 +#define NIFTI_FTYPE_NIFTI1_1 1 +#define NIFTI_FTYPE_NIFTI1_2 2 +#define NIFTI_FTYPE_ASCII 3 +#define NIFTI_MAX_FTYPE 3 /* this should match the maximum code */ + + /*------------------------------------------------------------------------*/ + /*-- the rest of these apply only to nifti1_io.c, check for _NIFTI1_IO_C_ */ + /* Feb 9, 2005 [rickr] */ +#ifdef _NIFTI1_IO_C_ + + typedef struct + { + int debug; /*!< debug level for status reports */ + int skip_blank_ext; /*!< skip extender if no extensions */ + int allow_upper_fext; /*!< allow uppercase file extensions */ + } nifti_global_options; + + typedef struct + { + int type; /* should match the NIFTI_TYPE_ #define */ + int nbyper; /* bytes per value, matches nifti_image */ + int swapsize; /* bytes per swap piece, matches nifti_image */ + char * name; /* text string to match #define */ + } nifti_type_ele; + +#undef LNI_FERR /* local nifti file error, to be compact and repetative */ +#define LNI_FERR(func,msg,file) \ + fprintf(stderr,"** ERROR (%s): %s '%s'\n",func,msg,file) + +#undef swap_2 +#undef swap_4 +#define swap_2(s) nifti_swap_2bytes(1,&(s)) /* s: 2-byte short; swap in place */ +#define swap_4(v) nifti_swap_4bytes(1,&(v)) /* v: 4-byte value; swap in place */ + + /***** isfinite() is a C99 macro, which is + present in many C implementations already *****/ + +#undef IS_GOOD_FLOAT +#undef FIXED_FLOAT + +#ifdef isfinite /* use isfinite() to check floats/doubles for goodness */ +# define IS_GOOD_FLOAT(x) isfinite(x) /* check if x is a "good" float */ +# define FIXED_FLOAT(x) (isfinite(x) ? (x) : 0) /* fixed if bad */ +#else +# define IS_GOOD_FLOAT(x) 1 /* don't check it */ +# define FIXED_FLOAT(x) (x) /* don't fix it */ +#endif + +#undef ASSIF /* assign v to *p, if possible */ +#define ASSIF(p,v) if( (p)!=NULL ) *(p) = (v) + +#undef MSB_FIRST +#undef LSB_FIRST +#undef REVERSE_ORDER +#define LSB_FIRST 1 +#define MSB_FIRST 2 +#define REVERSE_ORDER(x) (3-(x)) /* convert MSB_FIRST <--> LSB_FIRST */ + +#define LNI_MAX_NIA_EXT_LEN 100000 /* consider a longer extension invalid */ + +#endif /* _NIFTI1_IO_C_ section */ + /*------------------------------------------------------------------------*/ + + /*=================*/ +#ifdef __cplusplus +} +#endif +/*=================*/ + +#endif /* _NIFTI_IO_HEADER_ */ diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/nifti/znzlib.c b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/nifti/znzlib.c new file mode 100755 index 00000000..7364568c --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/nifti/znzlib.c @@ -0,0 +1,322 @@ +/** \file znzlib.c + \brief Low level i/o interface to compressed and noncompressed files. + Written by Mark Jenkinson, FMRIB + +This library provides an interface to both compressed (gzip/zlib) and +uncompressed (normal) file IO. The functions are written to have the +same interface as the standard file IO functions. + +To use this library instead of normal file IO, the following changes +are required: + - replace all instances of FILE* with znzFile + - change the name of all function calls, replacing the initial character + f with the znz (e.g. fseek becomes znzseek) + one exception is rewind() -> znzrewind() + - add a third parameter to all calls to znzopen (previously fopen) + that specifies whether to use compression (1) or not (0) + - use znz_isnull rather than any (pointer == NULL) comparisons in the code + for znzfile types (normally done after a return from znzopen) + +NB: seeks for writable files with compression are quite restricted + + */ + +#include "znzlib.h" + +/* +znzlib.c (zipped or non-zipped library) + +***** This code is released to the public domain. ***** + +***** Author: Mark Jenkinson, FMRIB Centre, University of Oxford ***** +***** Date: September 2004 ***** + +***** Neither the FMRIB Centre, the University of Oxford, nor any of ***** +***** its employees imply any warranty of usefulness of this software ***** +***** for any purpose, and do not assume any liability for damages, ***** +***** incidental or otherwise, caused by any use of this document. ***** + +*/ + + +/* Note extra argument (use_compression) where + use_compression==0 is no compression + use_compression!=0 uses zlib (gzip) compression +*/ + +znzFile znzopen(const char *path, const char *mode, int use_compression) +{ + znzFile file; + file = (znzFile) calloc(1,sizeof(struct znzptr)); + if( file == NULL ){ + fprintf(stderr,"** ERROR: znzopen failed to alloc znzptr\n"); + return NULL; + } + + file->nzfptr = NULL; + +#ifdef HAVE_ZLIB + file->zfptr = NULL; + + if (use_compression) { + file->withz = 1; + if((file->zfptr = gzopen(path,mode)) == NULL) { + free(file); + file = NULL; + } + } else { +#endif + + file->withz = 0; + if((file->nzfptr = fopen(path,mode)) == NULL) { + free(file); + file = NULL; + } + +#ifdef HAVE_ZLIB + } +#endif + + return file; +} + + +znzFile znzdopen(int fd, const char *mode, int use_compression) +{ + znzFile file; + file = (znzFile) calloc(1,sizeof(struct znzptr)); + if( file == NULL ){ + fprintf(stderr,"** ERROR: znzdopen failed to alloc znzptr\n"); + return NULL; + } +#ifdef HAVE_ZLIB + if (use_compression) { + file->withz = 1; + file->zfptr = gzdopen(fd,mode); + file->nzfptr = NULL; + } else { +#endif + file->withz = 0; +#ifdef HAVE_FDOPEN + file->nzfptr = fdopen(fd,mode); +#endif +#ifdef HAVE_ZLIB + file->zfptr = NULL; + }; +#endif + return file; +} + + +int Xznzclose(znzFile * file) +{ + int retval = 0; + if (*file!=NULL) { +#ifdef HAVE_ZLIB + if ((*file)->zfptr!=NULL) { retval = gzclose((*file)->zfptr); } +#endif + if ((*file)->nzfptr!=NULL) { retval = fclose((*file)->nzfptr); } + + free(*file); + *file = NULL; + } + return retval; +} + + +/* we already assume ints are 4 bytes */ +#undef ZNZ_MAX_BLOCK_SIZE +#define ZNZ_MAX_BLOCK_SIZE (1<<30) + +size_t znzread(void* buf, size_t size, size_t nmemb, znzFile file) +{ + size_t remain = size*nmemb; + char * cbuf = (char *)buf; + unsigned n2read; + int nread; + + if (file==NULL) { return 0; } +#ifdef HAVE_ZLIB + if (file->zfptr!=NULL) { + /* gzread/write take unsigned int length, so maybe read in int pieces + (noted by M Hanke, example given by M Adler) 6 July 2010 [rickr] */ + while( remain > 0 ) { + n2read = (remain < ZNZ_MAX_BLOCK_SIZE) ? remain : ZNZ_MAX_BLOCK_SIZE; + nread = gzread(file->zfptr, (void *)cbuf, n2read); + if( nread < 0 ) return nread; /* returns -1 on error */ + + remain -= nread; + cbuf += nread; + + /* require reading n2read bytes, so we don't get stuck */ + if( nread < (int)n2read ) break; /* return will be short */ + } + + /* warn of a short read that will seem complete */ + if( remain > 0 && remain < size ) + fprintf(stderr,"** znzread: read short by %u bytes\n",(unsigned)remain); + + return nmemb - remain/size; /* return number of members processed */ + } +#endif + return fread(buf,size,nmemb,file->nzfptr); +} + +size_t znzwrite(const void* buf, size_t size, size_t nmemb, znzFile file) +{ + size_t remain = size*nmemb; + char * cbuf = (char *)buf; + unsigned n2write; + int nwritten; + + if (file==NULL) { return 0; } +#ifdef HAVE_ZLIB + if (file->zfptr!=NULL) { + while( remain > 0 ) { + n2write = (remain < ZNZ_MAX_BLOCK_SIZE) ? remain : ZNZ_MAX_BLOCK_SIZE; + nwritten = gzwrite(file->zfptr, (void *)cbuf, n2write); + + /* gzread returns 0 on error, but in case that ever changes... */ + if( nwritten < 0 ) return nwritten; + + remain -= nwritten; + cbuf += nwritten; + + /* require writing n2write bytes, so we don't get stuck */ + if( nwritten < (int)n2write ) break; + } + + /* warn of a short write that will seem complete */ + if( remain > 0 && remain < size ) + fprintf(stderr,"** znzwrite: write short by %u bytes\n",(unsigned)remain); + + return nmemb - remain/size; /* return number of members processed */ + } +#endif + return fwrite(buf,size,nmemb,file->nzfptr); +} + +long znzseek(znzFile file, long offset, int whence) +{ + if (file==NULL) { return 0; } +#ifdef HAVE_ZLIB + if (file->zfptr!=NULL) return (long) gzseek(file->zfptr,offset,whence); +#endif + return fseek(file->nzfptr,offset,whence); +} + +int znzrewind(znzFile stream) +{ + if (stream==NULL) { return 0; } +#ifdef HAVE_ZLIB + /* On some systems, gzrewind() fails for uncompressed files. + Use gzseek(), instead. 10, May 2005 [rickr] + + if (stream->zfptr!=NULL) return gzrewind(stream->zfptr); + */ + + if (stream->zfptr!=NULL) return (int)gzseek(stream->zfptr, 0L, SEEK_SET); +#endif + rewind(stream->nzfptr); + return 0; +} + +long znztell(znzFile file) +{ + if (file==NULL) { return 0; } +#ifdef HAVE_ZLIB + if (file->zfptr!=NULL) return (long) gztell(file->zfptr); +#endif + return ftell(file->nzfptr); +} + +int znzputs(const char * str, znzFile file) +{ + if (file==NULL) { return 0; } +#ifdef HAVE_ZLIB + if (file->zfptr!=NULL) return gzputs(file->zfptr,str); +#endif + return fputs(str,file->nzfptr); +} + + +char * znzgets(char* str, int size, znzFile file) +{ + if (file==NULL) { return NULL; } +#ifdef HAVE_ZLIB + if (file->zfptr!=NULL) return gzgets(file->zfptr,str,size); +#endif + return fgets(str,size,file->nzfptr); +} + + +int znzflush(znzFile file) +{ + if (file==NULL) { return 0; } +#ifdef HAVE_ZLIB + if (file->zfptr!=NULL) return gzflush(file->zfptr,Z_SYNC_FLUSH); +#endif + return fflush(file->nzfptr); +} + + +int znzeof(znzFile file) +{ + if (file==NULL) { return 0; } +#ifdef HAVE_ZLIB + if (file->zfptr!=NULL) return gzeof(file->zfptr); +#endif + return feof(file->nzfptr); +} + + +int znzputc(int c, znzFile file) +{ + if (file==NULL) { return 0; } +#ifdef HAVE_ZLIB + if (file->zfptr!=NULL) return gzputc(file->zfptr,c); +#endif + return fputc(c,file->nzfptr); +} + + +int znzgetc(znzFile file) +{ + if (file==NULL) { return 0; } +#ifdef HAVE_ZLIB + if (file->zfptr!=NULL) return gzgetc(file->zfptr); +#endif + return fgetc(file->nzfptr); +} + +#if !defined (WIN32) +int znzprintf(znzFile stream, const char *format, ...) +{ + int retval=0; + char *tmpstr; + va_list va; + if (stream==NULL) { return 0; } + va_start(va, format); +#ifdef HAVE_ZLIB + if (stream->zfptr!=NULL) { + int size; /* local to HAVE_ZLIB block */ + size = strlen(format) + 1000000; /* overkill I hope */ + tmpstr = (char *)calloc(1, size); + if( tmpstr == NULL ){ + fprintf(stderr,"** ERROR: znzprintf failed to alloc %d bytes\n", size); + return retval; + } + vsprintf(tmpstr,format,va); + retval=gzprintf(stream->zfptr,"%s",tmpstr); + free(tmpstr); + } else +#endif + { + retval=vfprintf(stream->nzfptr,format,va); + } + va_end(va); + return retval; +} + +#endif + diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/nifti/znzlib.h b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/nifti/znzlib.h new file mode 100755 index 00000000..cdbb47f6 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/nifti/znzlib.h @@ -0,0 +1,124 @@ +#ifndef _ZNZLIB_H_ +#define _ZNZLIB_H_ + +/* +znzlib.h (zipped or non-zipped library) + +***** This code is released to the public domain. ***** + +***** Author: Mark Jenkinson, FMRIB Centre, University of Oxford ***** +***** Date: September 2004 ***** + +***** Neither the FMRIB Centre, the University of Oxford, nor any of ***** +***** its employees imply any warranty of usefulness of this software ***** +***** for any purpose, and do not assume any liability for damages, ***** +***** incidental or otherwise, caused by any use of this document. ***** + +*/ + +/* + +This library provides an interface to both compressed (gzip/zlib) and +uncompressed (normal) file IO. The functions are written to have the +same interface as the standard file IO functions. + +To use this library instead of normal file IO, the following changes +are required: + - replace all instances of FILE* with znzFile + - change the name of all function calls, replacing the initial character + f with the znz (e.g. fseek becomes znzseek) + - add a third parameter to all calls to znzopen (previously fopen) + that specifies whether to use compression (1) or not (0) + - use znz_isnull rather than any (pointer == NULL) comparisons in the code + +NB: seeks for writable files with compression are quite restricted + +*/ + + +/*=================*/ +#ifdef __cplusplus +extern "C" { +#endif + /*=================*/ + +#include +#include +#include +#include + + /* include optional check for HAVE_FDOPEN here, from deleted config.h: + + uncomment the following line if fdopen() exists for your compiler and + compiler options + */ + /* #define HAVE_FDOPEN */ + + +#ifdef HAVE_ZLIB +#if defined(ITKZLIB) +#include "itk_zlib.h" +#else +#include "zlib.h" +#endif +#endif + + + struct znzptr + { + int withz; + FILE* nzfptr; +#ifdef HAVE_ZLIB + gzFile zfptr; +#endif + } ; + + /* the type for all file pointers */ + typedef struct znzptr * znzFile; + + + /* int znz_isnull(znzFile f); */ + /* int znzclose(znzFile f); */ +#define znz_isnull(f) ((f) == NULL) +#define znzclose(f) Xznzclose(&(f)) + + /* Note extra argument (use_compression) where + use_compression==0 is no compression + use_compression!=0 uses zlib (gzip) compression + */ + + znzFile znzopen(const char *path, const char *mode, int use_compression); + + znzFile znzdopen(int fd, const char *mode, int use_compression); + + int Xznzclose(znzFile * file); + + size_t znzread(void* buf, size_t size, size_t nmemb, znzFile file); + + size_t znzwrite(const void* buf, size_t size, size_t nmemb, znzFile file); + + long znzseek(znzFile file, long offset, int whence); + + int znzrewind(znzFile stream); + + long znztell(znzFile file); + + int znzputs(const char *str, znzFile file); + + char * znzgets(char* str, int size, znzFile file); + + int znzputc(int c, znzFile file); + + int znzgetc(znzFile file); + +#if !defined(WIN32) + int znzprintf(znzFile stream, const char *format, ...); +#endif + + /*=================*/ +#ifdef __cplusplus +} +#endif +/*=================*/ + +#endif diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_cpu_resample_gradient_op.cpp b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_cpu_resample_gradient_op.cpp new file mode 100644 index 00000000..db197753 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_cpu_resample_gradient_op.cpp @@ -0,0 +1,53 @@ +#include "niftyreg_cpu_resample_gradient_op.h" +#include "niftyreg_cpu_resample_op.h" + +#include "_reg_resampling.h" + +void NiftyRegCPUResampleGradientOp::Compute(tf::OpKernelContext *p_context) { + float const *pc_floating = this->extract_floating_image(p_context).flat().data(); + float const *pc_deformation = this->extract_deformation_image(p_context).flat().data(); + float *p_gradient = nullptr; + nifti_image *p_img_nim = nifti_simple_init_nim(); + nifti_image *p_deformation_nim = nifti_simple_init_nim(); + nifti_image gradient; + int image_size; + int displacement_size; + tf::Tensor *p_output; + + this->populate_nifti_headers_from_context(*p_img_nim, *p_deformation_nim, p_context); + + image_size = p_img_nim->nvox; + displacement_size = p_deformation_nim->nvox; + + p_context->allocate_output(0, this->compute_gradient_shape(p_context), &p_output); + this->load_nifti_dimensions_from_tensor(gradient, *p_output); + p_gradient = p_output->flat().data(); + if (gradient.nu != p_deformation_nim->nu) { + gradient.nu = gradient.dim[5] = p_deformation_nim->nu; + gradient.nvox = p_deformation_nim->nvox; + } + + for (int b = 0; b < this->batch_size(p_context); ++b) { + for (int m = 0; m < p_img_nim->nu*p_img_nim->nt; ++m) { + p_deformation_nim->data = const_cast(pc_deformation); + p_img_nim->data = const_cast(pc_floating); + gradient.data = p_gradient; + + reg_getImageGradient(p_img_nim, + &gradient, + p_deformation_nim, + this->interpolation(), + this->boundary(), + m, + nullptr); + + p_gradient += displacement_size; + } + pc_deformation += displacement_size; + pc_floating += image_size; + } + + p_img_nim->data = p_deformation_nim->data = nullptr; + nifti_image_free(p_img_nim); + nifti_image_free(p_deformation_nim); +} diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_cpu_resample_gradient_op.h b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_cpu_resample_gradient_op.h new file mode 100644 index 00000000..07019e87 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_cpu_resample_gradient_op.h @@ -0,0 +1,32 @@ +#pragma once + +#include "niftyreg_cpu_resample_op.h" + +/** + * \brief CPU implementation of gradient wrt. deformation + */ +class NiftyRegCPUResampleGradientOp : public NiftyRegCPUResampleOp { + /** + * \name Typedefs + * @{ + */ +public: + typedef NiftyRegCPUResampleOp Superclass; + /** @} */ + + /** + * \name Op API + * @{ + */ +public: + virtual void Compute(tf::OpKernelContext *p_context) override; + /** @} */ + + /** + * \name Instantiation + * @{ + */ +public: + explicit NiftyRegCPUResampleGradientOp(tf::OpKernelConstruction* context) : Superclass(context) {} + /** @} */ +}; diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_cpu_resample_image_gradient_op.cpp b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_cpu_resample_image_gradient_op.cpp new file mode 100644 index 00000000..9c93cf28 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_cpu_resample_image_gradient_op.cpp @@ -0,0 +1,36 @@ +#include "niftyreg_cpu_resample_image_gradient_op.h" +#include "_reg_resampling.h" + +tf::TensorShape NiftyRegCPUResampleImageGradientOp::compute_image_gradient_product_shape(tf::OpKernelContext *p_context) { + return NiftyRegCPUResampleImageGradientOp::extract_image_shape(p_context); +} + +tf::shape_inference::ShapeHandle NiftyRegCPUResampleImageGradientOp::compute_image_gradient_product_shape(tf::shape_inference::InferenceContext *p_context) { + return p_context->input(0); +} + +void NiftyRegCPUResampleImageGradientOp::Compute(tf::OpKernelContext *p_context) { + const float *pc_image = p_context->input(0).flat().data(); + const float *pc_deformation = p_context->input(1).flat().data(); + const tf::Tensor &gradient_out = p_context->input(2); + + nifti_image image_nim; + nifti_image deformation_nim; + nifti_image gradient_out_nim; + nifti_image gradient_image_nim; + tf::Tensor *p_output; + + this->populate_nifti_headers_from_context(image_nim, deformation_nim, p_context); + p_context->allocate_output(0, this->compute_image_gradient_product_shape(p_context), &p_output); + this->load_nifti_dimensions_from_tensor(gradient_out_nim, gradient_out); + this->load_nifti_dimensions_from_tensor(gradient_image_nim, *p_output); + + for (int b = 0; b < this->batch_size(p_context); ++b) { + image_nim.data = const_cast(pc_image + b*image_nim.nvox); + deformation_nim.data = const_cast(pc_deformation + b*deformation_nim.nvox); + gradient_out_nim.data = const_cast(gradient_out.flat().data() + b*gradient_out_nim.nvox); + gradient_image_nim.data = p_output->flat().data() + b*gradient_image_nim.nvox; + + compute_gradient_product(gradient_image_nim, image_nim, deformation_nim, gradient_out_nim, this->boundary(), this->interpolation()); + } +} diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_cpu_resample_image_gradient_op.h b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_cpu_resample_image_gradient_op.h new file mode 100644 index 00000000..5998ac0a --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_cpu_resample_image_gradient_op.h @@ -0,0 +1,38 @@ +#pragma once + +#include "niftyreg_cpu_resample_op.h" + +/** + * \brief CPU implementation of NiftyRegResampleOp + */ +class NiftyRegCPUResampleImageGradientOp : public NiftyRegCPUResampleOp { + /** + * \name Typedefs + * @{ + */ +public: + typedef NiftyRegCPUResampleOp Superclass; + /** @} */ + + /** + * \name Op API + * @{ + */ +public: + /** \returns the size of the gradient outputted by this operation, at execution time */ + static tf::TensorShape compute_image_gradient_product_shape(tf::OpKernelContext *p_context); + + /** \returns the size of the gradient outputted by this operation, at graph-compilation time */ + static tf::shape_inference::ShapeHandle compute_image_gradient_product_shape(tf::shape_inference::InferenceContext *p_context); + + virtual void Compute(tf::OpKernelContext *p_context) override; + /** @} */ + + /** + * \name Instantiation + * @{ + */ +public: + explicit NiftyRegCPUResampleImageGradientOp(tf::OpKernelConstruction* context) : Superclass(context) {} + /** @} */ +}; diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_cpu_resample_op.cpp b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_cpu_resample_op.cpp new file mode 100644 index 00000000..0b475780 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_cpu_resample_op.cpp @@ -0,0 +1,129 @@ +#include "niftyreg_cpu_resample_op.h" +#include "niftyreg_cpu_resample_gradient_op.h" +#include "niftyreg_cpu_resample_image_gradient_op.h" +#ifdef GOOGLE_CUDA +#include "niftyreg_gpu_resample_op.h" +#include "niftyreg_gpu_resample_gradient_op.h" +#endif + +#include "_reg_resampling.h" +#undef DT_INT32 +#undef DT_FLOAT + +#include +#include + +void NiftyRegCPUResampleOp::populate_nifti_headers_from_context(nifti_image &r_image, nifti_image &r_deformation, tf::OpKernelContext *p_context) const { + const tf::Tensor& input_image = this->extract_floating_image(p_context); + const tf::Tensor& input_deformation = this->extract_deformation_image(p_context); + + this->load_nifti_dimensions_from_tensors(r_image, r_deformation, input_image, input_deformation); + + r_deformation.datatype = r_image.datatype = NIFTI_TYPE_FLOAT32; + r_deformation.nbyper = r_image.nbyper = sizeof(float); +} + +void NiftyRegCPUResampleOp::Compute(tf::OpKernelContext *p_context) { + float const *pc_floating = this->extract_floating_image(p_context).flat().data(); + float const *pc_deformation = this->extract_deformation_image(p_context).flat().data(); + float *p_warped = nullptr; + nifti_image *p_img_nim = nifti_simple_init_nim(); + nifti_image *p_deformation_nim = nifti_simple_init_nim(); + nifti_image warped; + tf::Tensor *p_output; + + this->populate_nifti_headers_from_context(*p_img_nim, *p_deformation_nim, p_context); + OP_REQUIRES(p_context, p_context->input(0).dims() == p_context->input(1).dims(), tf::errors::InvalidArgument("Image and sample index tensors must have the same dimensionality.")); + + p_context->allocate_output(0, this->compute_output_shape(p_context), &p_output); + this->load_nifti_dimensions_from_tensor(warped, *p_output); + p_warped = p_output->flat().data(); + + for (int b = 0; b < this->batch_size(p_context); ++b) { + p_deformation_nim->data = const_cast(pc_deformation); + p_img_nim->data = const_cast(pc_floating); + warped.data = p_warped; + + reg_resampleImage(p_img_nim, + &warped, + p_deformation_nim, + this->interpolation(), + this->boundary()); + + pc_deformation += p_deformation_nim->nvox; + pc_floating += p_img_nim->nvox; + p_warped += warped.nvox; + } + + p_img_nim->data = p_deformation_nim->data = nullptr; + nifti_image_free(p_img_nim); + nifti_image_free(p_deformation_nim); +} + +static const char gc_opname[] = "NiftyregImageResampling"; + +REGISTER_OP(gc_opname) +.Attr("interpolation: int = 1") +.Attr("boundary: int = 1") +.Input("image: float") +.Input("deformation: float") +.Output("warped: float") +.SetShapeFn([](tf::shape_inference::InferenceContext *p_context) { + p_context->set_output(0, NiftyRegCPUResampleOp::compute_output_shape(p_context)); + + return tf::Status::OK(); + }); + +REGISTER_KERNEL_BUILDER(Name(gc_opname) + .Device(tf::DEVICE_CPU), + NiftyRegCPUResampleOp); + +#ifdef GOOGLE_CUDA +REGISTER_KERNEL_BUILDER(Name(gc_opname) + .Device(tf::DEVICE_GPU), + NiftyRegGPUResampleOp); +#endif + +static const char gc_gradient_opname[] = "NiftyregImageResamplingGradient"; + +REGISTER_OP(gc_gradient_opname) +.Attr("interpolation: int = 1") +.Attr("boundary: int = 1") +.Input("image: float") +.Input("deformation: float") +.Output("gradient: float") +.SetShapeFn([](tf::shape_inference::InferenceContext *p_context) { + p_context->set_output(0, NiftyRegCPUResampleGradientOp::compute_gradient_shape(p_context)); + + return tf::Status::OK(); + }); + +REGISTER_KERNEL_BUILDER(Name(gc_gradient_opname) + .Device(tf::DEVICE_CPU), + NiftyRegCPUResampleGradientOp); + +#ifdef GOOGLE_CUDA +REGISTER_KERNEL_BUILDER(Name(gc_gradient_opname) + .Device(tf::DEVICE_GPU), + NiftyRegGPUResampleGradientOp); +#endif + +static const char gc_image_gradient_opname[] = "NiftyregImageResamplingImageGradient"; + +REGISTER_OP(gc_image_gradient_opname) +.Attr("interpolation: int = 1") +.Attr("boundary: int = 1") +.Input("image: float") +.Input("deformation: float") +.Input("loss_gradient: float") +.Output("image_gradient: float") +.SetShapeFn([](tf::shape_inference::InferenceContext *p_context) { + p_context->set_output(0, NiftyRegCPUResampleImageGradientOp::compute_image_gradient_product_shape(p_context)); + + return tf::Status::OK(); + }); + +REGISTER_KERNEL_BUILDER(Name(gc_image_gradient_opname) + .Device(tf::DEVICE_CPU), + NiftyRegCPUResampleImageGradientOp); + diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_cpu_resample_op.h b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_cpu_resample_op.h new file mode 100644 index 00000000..74846621 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_cpu_resample_op.h @@ -0,0 +1,36 @@ +#pragma once + +#include "niftyreg_resample_op.h" + +/** + * \brief CPU implementation of NiftyRegResampleOp + */ +class NiftyRegCPUResampleOp : public NiftyRegResampleOp { + /** + * \name Typedefs + * @{ + */ +public: + typedef NiftyRegResampleOp Superclass; + /** @} */ + + /** + * \name Op API + * @{ + */ +protected: + /** \brief Fully populates the image and deformation-field Nifti-headers given the kernel context */ + void populate_nifti_headers_from_context(nifti_image &r_image, nifti_image &r_deformation, tf::OpKernelContext *p_context) const; + +public: + virtual void Compute(tf::OpKernelContext *p_context) override; + /** @} */ + + /** + * \name Instantiation + * @{ + */ +public: + explicit NiftyRegCPUResampleOp(tf::OpKernelConstruction* context) : Superclass(context) {} + /** @} */ +}; diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_gpu_resample_gradient_op.cu b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_gpu_resample_gradient_op.cu new file mode 100644 index 00000000..1b432f60 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_gpu_resample_gradient_op.cu @@ -0,0 +1,41 @@ +#include "niftyreg_gpu_resample_gradient_op.h" +#include "niftyreg_gpu_resample_op.h" +#include "_reg_resampling_gpu.h" + +#include + +void NiftyRegGPUResampleGradientOp::Compute(tf::OpKernelContext *p_context) { + const auto &input_image = this->extract_floating_image(p_context); + const auto &input_deformation = this->extract_deformation_image(p_context); + + nifti_image img_nim; + nifti_image deformation_nim; + float *dp_gradient; + tf::Tensor *p_output; + + this->load_nifti_dimensions_from_tensors(img_nim, deformation_nim, input_image, input_deformation); + + p_context->allocate_output(0, this->compute_gradient_shape(p_context), &p_output); + dp_gradient = p_output->flat().data(); + + if (this->interpolation() != 1 && this->interpolation() != 3) { + std::cerr << "WARNING: gradient is only available for cubic/linear interpolation.\n"; + } + + for (int b = 0; b < this->batch_size(p_context); ++b) { + for (int m = 0; m < img_nim.nu; ++m) { + const float *dpc_source = input_image.flat().data() + (b + m)*img_nim.nx*img_nim.ny*img_nim.nz; + const float *dpc_deformation = input_deformation.flat().data() + b*deformation_nim.nvox; + + reg_getImageGradient_gpu(img_nim, + deformation_nim, + dpc_source, + dpc_deformation, + dp_gradient, + this->boundary(), + this->interpolation()); + + dp_gradient += deformation_nim.nvox; + } + } +} diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_gpu_resample_gradient_op.h b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_gpu_resample_gradient_op.h new file mode 100644 index 00000000..4e5c9cea --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_gpu_resample_gradient_op.h @@ -0,0 +1,32 @@ +#pragma once + +#include "niftyreg_gpu_resample_op.h" + +/** + * \brief GPU implementation of gradient wrt. deformation + */ +class NiftyRegGPUResampleGradientOp : public NiftyRegGPUResampleOp { + /** + * \name Typedefs + * @{ + */ +public: + typedef NiftyRegGPUResampleOp Superclass; + /** @} */ + + /** + * \name Op API + * @{ + */ +public: + virtual void Compute(tf::OpKernelContext *p_context) override; + /** @} */ + + /** + * \name Instantiation + * @{ + */ +public: + explicit NiftyRegGPUResampleGradientOp(tf::OpKernelConstruction* context) : Superclass(context) {} + /** @} */ +}; diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_gpu_resample_op.cu b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_gpu_resample_op.cu new file mode 100644 index 00000000..28641785 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_gpu_resample_op.cu @@ -0,0 +1,38 @@ +#include "niftyreg_gpu_resample_op.h" +#include "resampleKernel.h" +#include "_reg_common_cuda.h" +#ifdef DT_INT32 +#undef DT_INT32 +#endif +#ifdef DT_FLOAT +#undef DT_FLOAT +#endif + +#include +#include +#include +#include + +void NiftyRegGPUResampleOp::Compute(tf::OpKernelContext *p_context) { + const auto &input_image = this->extract_floating_image(p_context); + const auto &input_deformation = this->extract_deformation_image(p_context); + + tf::Tensor *p_destination; + nifti_image floating_image; + nifti_image warped_image; + nifti_image deformation_image; + + this->load_nifti_dimensions_from_tensors(floating_image, deformation_image, input_image, input_deformation); + + p_context->allocate_output(0, this->compute_output_shape(p_context), &p_destination); + this->load_nifti_dimensions_from_tensor(warped_image, *p_destination); + + for (int b = 0; b < this->batch_size(p_context); ++b) { + const float *dpc_floating = input_image.flat().data() + b*floating_image.nvox; + const float *dpc_deformation = input_deformation.flat().data() + b*deformation_image.nvox; + + float *dp_warped = p_destination->flat().data() + b*warped_image.nvox; + + launchResample(&floating_image, &warped_image, this->interpolation(), this->boundary(), dpc_floating, dp_warped, dpc_deformation); + } +} diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_gpu_resample_op.h b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_gpu_resample_op.h new file mode 100644 index 00000000..a9622b78 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_gpu_resample_op.h @@ -0,0 +1,32 @@ +#pragma once + +#include "niftyreg_resample_op.h" + +/** + * \brief GPU Implementation of NiftyReg-powered image resampling operation. + */ +class NiftyRegGPUResampleOp : public NiftyRegResampleOp { + /** + * \name Typedefs + * @{ + */ +public: + typedef NiftyRegResampleOp Superclass; + /** @} */ + + /** + * \name Op API + * @{ + */ +public: + virtual void Compute(tf::OpKernelContext *p_context) override; + /** @} */ + + /** + * \name Instantiation + * @{ + */ +public: + explicit NiftyRegGPUResampleOp(tf::OpKernelConstruction* context) : Superclass(context) {} + /** @} */ +}; diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_resample_op.h b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_resample_op.h new file mode 100644 index 00000000..1c9552f0 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_resample_op.h @@ -0,0 +1,158 @@ +#pragma once + +#ifdef GOOGLE_CUDA +#define EIGEN_USE_GPU +#endif + + +#include "resampler_boundary.h" + +#include +#include + +struct _nifti_image; + +namespace tf = tensorflow; + +/** + * \brief Base class for NiftyReg image resampling operations. + * + * Assumes that the input data (and subsequently the output data) have been transposed. I.e., the displacement-component index is assumed + * to be the slowest moving index. + */ +template +class NiftyRegResampleOp : public tf::OpKernel { + /** + * \name Typedefs + * @{ + */ +public: + typedef tf::OpKernel Superclass; + /** @} */ + + /** + * \name Settings + * @{ + */ +private: + int m_interpolation; + resampler_boundary_e m_boundary; + +protected: + /** \returns the NiftyReg interpolation code */ + int interpolation(void) const { + return m_interpolation; + } + + /** \returns the padding value appropriate for the requested boundary treatment */ + float padding(void) const { + return m_boundary == resampler_boundary_e::ZEROPAD? 0.f : std::numeric_limits::quiet_NaN(); + } + + /** \returns the requested boundary treatment */ + resampler_boundary_e boundary(void) const { + return m_boundary; + } + /** @} */ + + /** + * \name Utility functions + * @{ + */ +private: + static tf::TensorShape _make_fake_dim(tf::shape_inference::InferenceContext *p_context, const int input_idx); + +protected: + /** \returns the deformation field input */ + static const tf::Tensor& extract_floating_image(tf::OpKernelContext *p_context) { + return p_context->input(0); + } + + /** \returns the deformation field input */ + static const tf::Tensor& extract_deformation_image(tf::OpKernelContext *p_context) { + return p_context->input(1); + } + + /** \returns the shape of the i-th input */ + static const tf::TensorShape& extract_input_shape(tf::OpKernelContext *p_context, const int i) { + return p_context->input(i).shape(); + } + + /** \returns the shape of the input image */ + static const tf::TensorShape& extract_image_shape(tf::OpKernelContext *p_context) { + return NiftyRegResampleOp::extract_input_shape(p_context, 0); + } + + /** \returns the partial shape of the input image (all but first dimension must be known in advance) */ + static tf::TensorShape extract_image_shape(tf::shape_inference::InferenceContext *p_context) { + return _make_fake_dim(p_context, 0); + } + + /** \returns the shape of the deformation image */ + static const tf::TensorShape& extract_deformation_shape(tf::OpKernelContext *p_context) { + return NiftyRegResampleOp::extract_input_shape(p_context, 1); + } + + /** \returns the partial shape of the input deformation (all but first dimension must be known in advance) */ + static tf::TensorShape extract_deformation_shape(tf::shape_inference::InferenceContext *p_context) { + return _make_fake_dim(p_context, 1); + } + + /** \returns the spatial rank of the image */ + static int infer_spatial_rank(tf::OpKernelContext *p_context) { + return NiftyRegResampleOp::extract_deformation_shape(p_context).dim_size(1); + } + + /** \returns the batch size */ + static int batch_size(tf::OpKernelContext *p_context) { + return NiftyRegResampleOp::extract_floating_image(p_context).dim_size(0); + } + + /** \brief Converts tensor dimensions into Nifti-image dimension arrays */ + static void load_nifti_dimensions_from_tensors(_nifti_image &r_image, _nifti_image &r_deformation, const tf::Tensor &image_tensor, const tf::Tensor &deformation_tensor); + + /** \brief Converts tensor dimensions into Nifti-image dimension arrays */ + static void load_nifti_dimensions_from_tensor(_nifti_image &r_image, const tf::Tensor &tensor); + +public: + /** + * \returns the shape of the output image + */ + static tf::TensorShape compute_output_shape(tf::OpKernelContext *p_context); + + /** + * \returns the shape of the output image at Op-registration time + */ + static tf::shape_inference::ShapeHandle compute_output_shape(tf::shape_inference::InferenceContext *p_context); + + /** + * \returns the shape of the output image + */ + static tf::TensorShape compute_output_shape(const tf::TensorShape &image, const tf::TensorShape &deformation); + + /** + * \returns the shape of the gradient image + */ + static tf::TensorShape compute_gradient_shape(tf::OpKernelContext *p_context); + + /** + * \returns the shape of the gradient image at Op-registration time + */ + static tf::shape_inference::ShapeHandle compute_gradient_shape(tf::shape_inference::InferenceContext *p_context); + + /** + * \returns the shape of the gradient image + */ + static tf::TensorShape compute_gradient_shape(const tf::TensorShape &image, const tf::TensorShape &deformation); + /** @} */ + + /** + * \name Instantiation + * @{ + */ +public: + NiftyRegResampleOp(tf::OpKernelConstruction* p_context); + /** @} */ +}; + +#include "niftyreg_resample_op.tpp" diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_resample_op.tpp b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_resample_op.tpp new file mode 100644 index 00000000..ec44f577 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/niftyreg_resample_op.tpp @@ -0,0 +1,141 @@ +#pragma once + +#include "niftyreg_resample_op.h" +#include "_reg_resampling.h" + +#include +#include +#include + +template +tf::TensorShape NiftyRegResampleOp::compute_output_shape(const tf::TensorShape &image, const tf::TensorShape &deformation) { + tf::TensorShape output_shape; + + output_shape.AddDim(image.dim_size(0)); + output_shape.AddDim(image.dim_size(1)); + for (int d = 2; d < deformation.dims(); ++d) { + output_shape.AddDim(deformation.dim_size(d)); + } + + return output_shape; +} + +template +tf::TensorShape NiftyRegResampleOp::compute_output_shape(tf::OpKernelContext *p_context) { + return NiftyRegResampleOp::compute_output_shape(NiftyRegResampleOp::extract_image_shape(p_context), NiftyRegResampleOp::extract_deformation_shape(p_context)); +} + +template +tf::TensorShape NiftyRegResampleOp::_make_fake_dim(tf::shape_inference::InferenceContext *p_context, const int iidx) { + const auto &known_shape = p_context->input(iidx); + + tf::TensorShape shape; + + shape.AddDim(1); + for (int i = 1; i < p_context->Rank(known_shape); ++i) { + if (!p_context->ValueKnown(p_context->Dim(known_shape, i)) || p_context->Value(p_context->Dim(known_shape, i)) < 0) { + shape.AddDim(0); + std::cerr << "Warning: unknown dimension size " << i << " in input " << iidx << std::endl; + } else { + shape.AddDim(p_context->Value(p_context->Dim(known_shape, i))); + } + } + + return shape; +} + +template +tf::shape_inference::ShapeHandle NiftyRegResampleOp::compute_output_shape(tf::shape_inference::InferenceContext *p_context) { + tf::TensorShape inferred_shape; + tf::TensorShape image_shape; + tf::TensorShape deformation_shape; + tf::shape_inference::ShapeHandle inferred_shape_handle; + + if (p_context->input_tensor(0) == nullptr || p_context->input_tensor(1) == nullptr) { + image_shape = NiftyRegResampleOp::extract_image_shape(p_context); + deformation_shape = NiftyRegResampleOp::extract_deformation_shape(p_context); + } else { + image_shape = p_context->input_tensor(0)->shape(); + deformation_shape = p_context->input_tensor(1)->shape(); + } + + inferred_shape = NiftyRegResampleOp::compute_output_shape(image_shape, deformation_shape); + p_context->MakeShapeFromTensorShape(inferred_shape, &inferred_shape_handle); + p_context->ReplaceDim(inferred_shape_handle, 0, p_context->UnknownDim(), &inferred_shape_handle); + for (int d = 1; d < inferred_shape.dims(); ++d) { + if (inferred_shape.dim_size(d) <= 0) { + p_context->ReplaceDim(inferred_shape_handle, d, p_context->UnknownDim(), &inferred_shape_handle); + } + } + + return inferred_shape_handle; +} + +template +tf::TensorShape NiftyRegResampleOp::compute_gradient_shape(const tf::TensorShape &image_shape, const tf::TensorShape &deformation_shape) { + tf::TensorShape shape = deformation_shape; + + shape.set_dim(1, image_shape.dim_size(1)*deformation_shape.dim_size(1)); + + return shape; +} + +template +tf::TensorShape NiftyRegResampleOp::compute_gradient_shape(tf::OpKernelContext *p_context) { + return NiftyRegResampleOp::compute_gradient_shape(NiftyRegResampleOp::extract_image_shape(p_context), NiftyRegResampleOp::extract_deformation_shape(p_context)); +} + +template +tf::shape_inference::ShapeHandle NiftyRegResampleOp::compute_gradient_shape(tf::shape_inference::InferenceContext *p_context) { + tf::shape_inference::ShapeHandle inferred_shape = p_context->input(1); + + if (p_context->ValueKnown(p_context->Dim(inferred_shape, 1)) && p_context->Value(p_context->Dim(inferred_shape, 1)) > 0 + && p_context->ValueKnown(p_context->Dim(p_context->input(0), 1)) && p_context->Value(p_context->Dim(p_context->input(0), 1)) > 0) { + const int dim_size = p_context->Value(p_context->Dim(inferred_shape, 1))*p_context->Value(p_context->Dim(p_context->input(0), 1)); + + p_context->ReplaceDim(inferred_shape, 1, p_context->MakeDim(dim_size), &inferred_shape); + } else { + std::cerr << "Warning: computing gradient with unknown number of per-voxel components." << std::endl; + p_context->ReplaceDim(inferred_shape, 1, p_context->UnknownDim(), &inferred_shape); + } + + return inferred_shape; +} + +template +void NiftyRegResampleOp::load_nifti_dimensions_from_tensors(nifti_image &r_image, nifti_image &r_deformation, const tf::Tensor &image_tensor, const tf::Tensor &deformation_tensor) { + NiftyRegResampleOp::load_nifti_dimensions_from_tensor(r_image, image_tensor); + NiftyRegResampleOp::load_nifti_dimensions_from_tensor(r_deformation, deformation_tensor); +} + +template +void NiftyRegResampleOp::load_nifti_dimensions_from_tensor(nifti_image &r_image, const tf::Tensor &tensor) { + std::fill(r_image.dim + 1, r_image.dim + 8, 1.f); + std::fill(r_image.pixdim, r_image.pixdim + 8, 1.f); + + if (tensor.dim_size(1) != 1) { + r_image.dim[0] = 5; + r_image.dim[5] = tensor.dim_size(1); + } else { + r_image.dim[0] = tensor.dims() - 2; + } + + for (int i = 1; i <= tensor.dims() - 2; ++i) { + r_image.dim[i] = tensor.dim_size(tensor.dims() - i); + } + r_image.datatype = NIFTI_TYPE_FLOAT32; + r_image.nbyper = 4; + nifti_update_dims_from_array(&r_image); +} + +template +NiftyRegResampleOp::NiftyRegResampleOp(tf::OpKernelConstruction* p_context) : Superclass(p_context) { + int bdy_code = int(resampler_boundary_e::SENTINEL); + + p_context->GetAttr("interpolation", &m_interpolation); + p_context->GetAttr("boundary", &bdy_code); + + OP_REQUIRES(p_context, bdy_code >= int(resampler_boundary_e::ZEROPAD) && bdy_code < int(resampler_boundary_e::SENTINEL), + tf::errors::InvalidArgument("Invalid boundary code.")); + m_boundary = resampler_boundary_e(bdy_code); +} diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/resampleKernel.cu b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/resampleKernel.cu new file mode 100644 index 00000000..a812b5e4 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/resampleKernel.cu @@ -0,0 +1,507 @@ +#include +#include +#include "cuda_runtime.h" +#include "cuda.h" +#include"_reg_resampling.h" +#include"_reg_maths.h" +#include "resampleKernel.h" +#include "_reg_common_cuda.h" +#include"_reg_tools.h" +#include "interpolations.h" + +#define SINC_KERNEL_RADIUS 3 +#define SINC_KERNEL_SIZE SINC_KERNEL_RADIUS*2 + +/* *************************************************************** */ +unsigned int min1(unsigned int a, unsigned int b) +{ + return (a < b) ? a : b; +} +/* *************************************************************** */ +template +__device__ __inline__ void reg_mat44_mul_cuda(DTYPE const* mat, DTYPE const* in, DTYPE *out) +{ + out[0] = (DTYPE)((double)mat[0 * 4 + 0] * (double)in[0] + (double)mat[0 * 4 + 1] * (double)in[1] + (double)mat[0 * 4 + 2] * (double)in[2] + (double)mat[0 * 4 + 3]); + out[1] = (DTYPE)((double)mat[1 * 4 + 0] * (double)in[0] + (double)mat[1 * 4 + 1] * (double)in[1] + (double)mat[1 * 4 + 2] * (double)in[2] + (double)mat[1 * 4 + 3]); + out[2] = (DTYPE)((double)mat[2 * 4 + 0] * (double)in[0] + (double)mat[2 * 4 + 1] * (double)in[1] + (double)mat[2 * 4 + 2] * (double)in[2] + (double)mat[2 * 4 + 3]); + return; +} +/* *************************************************************** */ +template +__device__ __inline__ void reg_mat44_mul_cuda(float* mat, DTYPE const* in, DTYPE *out) +{ + out[0] = (DTYPE)((double)mat[0 * 4 + 0] * (double)in[0] + (double)mat[0 * 4 + 1] * (double)in[1] + (double)mat[0 * 4 + 2] * (double)in[2] + (double)mat[0 * 4 + 3]); + out[1] = (DTYPE)((double)mat[1 * 4 + 0] * (double)in[0] + (double)mat[1 * 4 + 1] * (double)in[1] + (double)mat[1 * 4 + 2] * (double)in[2] + (double)mat[1 * 4 + 3]); + out[2] = (DTYPE)((double)mat[2 * 4 + 0] * (double)in[0] + (double)mat[2 * 4 + 1] * (double)in[1] + (double)mat[2 * 4 + 2] * (double)in[2] + (double)mat[2 * 4 + 3]); + return; +} +/* *************************************************************** */ +__device__ __inline__ int cuda_reg_floor(double a) +{ + return (int) (floor(a)); +} +/* *************************************************************** */ +template +__device__ __inline__ void interpolantCubicSpline(FieldTYPE ratio, FieldTYPE *basis) +{ + if (ratio < 0.0) + ratio = 0.0; //reg_rounding error + double FF = (double) ratio * ratio; + basis[0] = (FieldTYPE) ((ratio * (((double)2.0 - ratio) * ratio - (double)1.0)) / (double)2.0); + basis[1] = (FieldTYPE) ((FF * ((double)3.0 * ratio - 5.0) + 2.0) / (double)2.0); + basis[2] = (FieldTYPE) ((ratio * (((double)4.0 - (double)3.0 * ratio) * ratio + (double)1.0)) / (double)2.0); + basis[3] = (FieldTYPE) ((ratio - (double)1.0) * FF / (double)2.0); +} +/* *************************************************************** */ +__inline__ __device__ void interpWindowedSincKernel(double relative, double *basis) +{ + if (relative < 0.0) + relative = 0.0; //reg_rounding error + int j = 0; + double sum = 0.; + for (int i = -SINC_KERNEL_RADIUS; i < SINC_KERNEL_RADIUS; ++i) { + double x = relative - (double) (i); + if (x == 0.0) + basis[j] = 1.0; + else if (abs(x) >= (double) (SINC_KERNEL_RADIUS)) + basis[j] = 0; + else { + double pi_x = M_PI * x; + basis[j] = (SINC_KERNEL_RADIUS) * sin(pi_x) * sin(pi_x / SINC_KERNEL_RADIUS) / (pi_x * pi_x); + } + sum += basis[j]; + j++; + } + for (int i = 0; i < SINC_KERNEL_SIZE; ++i) + basis[i] /= sum; +} +/* *************************************************************** */ +__inline__ __device__ void interpCubicSplineKernel(double relative, double *basis) +{ + // if (relative < 0.0) + // relative = 0.0; //reg_rounding error + double FF = relative * relative; + basis[0] = (relative * ((2.0 - relative) * relative - 1.0)) / 2.0; + basis[1] = (FF * (3.0 * relative - 5.0) + 2.0) / 2.0; + basis[2] = (relative * ((4.0 - 3.0 * relative) * relative + 1.0)) / 2.0; + basis[3] = (relative - 1.0) * FF / 2.0; +} +/* *************************************************************** */ +__inline__ __device__ void interpLinearKernel(double relative, double *basis) +{ + if (relative < 0.0) + relative = 0.0; //reg_rounding error + basis[1] = relative; + basis[0] = 1.0 - relative; +} +/* *************************************************************** */ +__inline__ __device__ void interpNearestNeighKernel(double relative, double *basis) +{ + if (relative < 0.0) + relative = 0.0; //reg_rounding error + basis[0] = basis[1] = 0.0; + if (relative >= 0.5) + basis[1] = 1; + else + basis[0] = 1; +} +/* *************************************************************** */ +__inline__ __device__ double interpLoop2D(const float* floatingIntensity, + const double* xBasis, + const double* yBasis, + int *previous, + uint3 fi_xyz, + const float paddingValue, + const unsigned int kernel_size) +{ + double intensity = (double)(0.0); + + for (int b = 0; b < kernel_size; b++) { + int Y = previous[1] + b; + bool yInBounds = -1 < Y && Y < fi_xyz.y; + double xTempNewValue = 0.0; + + for (int a = 0; a < kernel_size; a++) { + int X = previous[0] + a; + bool xInBounds = -1 < X && X < fi_xyz.x; + + const unsigned int idx = Y * fi_xyz.x + X; + + xTempNewValue += (xInBounds && yInBounds) ? floatingIntensity[idx] * xBasis[a] : paddingValue * xBasis[a]; + } + intensity += xTempNewValue * yBasis[b]; + } + return intensity; +} +/* *************************************************************** */ +template +__inline__ __device__ double interpLoop2DBoundary(const float* floatingIntensity, + const double* xBasis, + const double* yBasis, + int *previous, + uint3 fi_xyz, + const unsigned int kernel_size) +{ + double intensity = (double)(0.0); + + for (int b = 0; b < kernel_size; b++) { + const int offset_x = reg_applyBoundary(previous[1] + b, fi_xyz.y)*fi_xyz.x; + + double xTempNewValue = 0.0; + + for (int a = 0; a < kernel_size; a++) { + const unsigned int idx = offset_x + reg_applyBoundary(previous[0] + a, fi_xyz.x); + + xTempNewValue += floatingIntensity[idx]*xBasis[a]; + } + intensity += xTempNewValue*yBasis[b]; + } + + return intensity; +} +/* *************************************************************** */ +__inline__ __device__ double interpLoop3D(const float* floatingIntensity, + const double* xBasis, + const double* yBasis, + const double* zBasis, + int *previous, + uint3 fi_xyz, + float paddingValue, + unsigned int kernel_size) +{ + double intensity = (double)(0.0); + for (int c = 0; c < kernel_size; c++) { + int Z = previous[2] + c; + bool zInBounds = -1 < Z && Z < fi_xyz.z; + double yTempNewValue = 0.0; + for (int b = 0; b < kernel_size; b++) { + int Y = previous[1] + b; + bool yInBounds = -1 < Y && Y < fi_xyz.y; + double xTempNewValue = 0.0; + for (int a = 0; a < kernel_size; a++) { + int X = previous[0] + a; + bool xInBounds = -1 < X && X < fi_xyz.x; + const unsigned int idx = Z * fi_xyz.x * fi_xyz.y + Y * fi_xyz.x + X; + + xTempNewValue += (xInBounds && yInBounds && zInBounds) ? floatingIntensity[idx] * xBasis[a] : paddingValue * xBasis[a]; + } + yTempNewValue += xTempNewValue * yBasis[b]; + } + intensity += yTempNewValue * zBasis[c]; + } + return intensity; +} +/* *************************************************************** */ +template +__inline__ __device__ double interpLoop3DBoundary(const float* floatingIntensity, + const double* xBasis, + const double* yBasis, + const double* zBasis, + int *previous, + uint3 fi_xyz, + unsigned int kernel_size) +{ + double intensity = (double)(0.0); + + for (int c = 0; c < kernel_size; c++) { + const int offset_y = reg_applyBoundary(previous[2] + c, fi_xyz.z)*fi_xyz.y; + + double yTempNewValue = 0.0; + + for (int b = 0; b < kernel_size; b++) { + const int offset_x = (offset_y + reg_applyBoundary(previous[1] + b, fi_xyz.y))*fi_xyz.x; + + double xTempNewValue = 0.0; + + for (int a = 0; a < kernel_size; a++) { + const unsigned int idx = offset_x + reg_applyBoundary(previous[0] + a, fi_xyz.x); + + xTempNewValue += floatingIntensity[idx]*xBasis[a]; + } + yTempNewValue += xTempNewValue*yBasis[b]; + } + intensity += yTempNewValue*zBasis[c]; + } + + return intensity; +} +/* *************************************************************** */ +template +__global__ void ResampleImage2D(const float* floatingImage, + const float* deformationField, + float* warpedImage, + ulong2 voxelNumber, + uint3 fi_xyz, + uint2 wi_tu, + const float paddingValue, + const int kernelType) +{ + const float *sourceIntensityPtr = (floatingImage); + float *resultIntensityPtr = (warpedImage); + const float *deformationFieldPtrX = (deformationField); + const float *deformationFieldPtrY = &deformationFieldPtrX[voxelNumber.x]; + + long index = blockIdx.x * blockDim.x + threadIdx.x; + + while (index < voxelNumber.x) { + + for (unsigned int t = 0; t < wi_tu.x * wi_tu.y; t++) { + + float *resultIntensity = &resultIntensityPtr[t * voxelNumber.x]; + const float *floatingIntensity = &sourceIntensityPtr[t * voxelNumber.y]; + double intensity = paddingValue; + + int previous[3]; + float position[3]; + double relative[3]; + auto launchInterpLoop = [&](const double xBasis[], const double yBasis[], const int kernelSize) { + if (resampler_boundary_e(tBoundary) != resampler_boundary_e::ZEROPAD && resampler_boundary_e(tBoundary) != resampler_boundary_e::NANPAD) { + intensity = interpLoop2DBoundary(floatingIntensity, xBasis, yBasis, previous, fi_xyz, kernelSize); + } else { + intensity = interpLoop2D(floatingIntensity, xBasis, yBasis, previous, fi_xyz, paddingValue, kernelSize); + } + }; + + position[0] = (float)(deformationFieldPtrX[index]); + position[1] = (float)(deformationFieldPtrY[index]); + + previous[0] = cuda_reg_floor(position[0]); + previous[1] = cuda_reg_floor(position[1]); + + relative[0] = (double)(position[0]) - (double)(previous[0]); + relative[1] = (double)(position[1]) - (double)(previous[1]); + + if (kernelType == 0) { + + double xBasisIn[2], yBasisIn[2]; + interpNearestNeighKernel(relative[0], xBasisIn); + interpNearestNeighKernel(relative[1], yBasisIn); + launchInterpLoop(xBasisIn, yBasisIn, 2); + } + else if (kernelType == 1) { + + double xBasisIn[2], yBasisIn[2]; + interpLinearKernel(relative[0], xBasisIn); + interpLinearKernel(relative[1], yBasisIn); + launchInterpLoop(xBasisIn, yBasisIn, 2); + } + else if (kernelType == 4) { + + double xBasisIn[6], yBasisIn[6]; + + previous[0] -= SINC_KERNEL_RADIUS; + previous[1] -= SINC_KERNEL_RADIUS; + previous[2] -= SINC_KERNEL_RADIUS; + + interpWindowedSincKernel(relative[0], xBasisIn); + interpWindowedSincKernel(relative[1], yBasisIn); + launchInterpLoop(xBasisIn, yBasisIn, 6); + } + else { + + double xBasisIn[4], yBasisIn[4]; + + previous[0]--; + previous[1]--; + previous[2]--; + + reg_getNiftynetCubicSpline(relative[0], xBasisIn); + reg_getNiftynetCubicSpline(relative[1], yBasisIn); + launchInterpLoop(xBasisIn, yBasisIn, 4); + } + + resultIntensity[index] = (float)intensity; + } + index += blockDim.x * gridDim.x; + } +} +/* *************************************************************** */ +template +__global__ void ResampleImage3D(const float* floatingImage, + const float* deformationField, + float* warpedImage, + const ulong2 voxelNumber, + uint3 fi_xyz, + uint2 wi_tu, + const float paddingValue, + int kernelType) +{ + const float *sourceIntensityPtr = (floatingImage); + float *resultIntensityPtr = (warpedImage); + const float *deformationFieldPtrX = (deformationField); + const float *deformationFieldPtrY = &deformationFieldPtrX[voxelNumber.x]; + const float *deformationFieldPtrZ = &deformationFieldPtrY[voxelNumber.x]; + + long index = blockIdx.x * blockDim.x + threadIdx.x; + + while (index < voxelNumber.x) { + + for (unsigned int t = 0; t < wi_tu.x * wi_tu.y; t++) { + + float *resultIntensity = &resultIntensityPtr[t * voxelNumber.x]; + const float *floatingIntensity = &sourceIntensityPtr[t * voxelNumber.y]; + double intensity = paddingValue; + + int previous[3]; + float position[3]; + double relative[3]; + auto launchInterpLoop = [&](const double xBasisIn[], const double yBasisIn[], const double zBasisIn[], const int kernelSize) { + if (resampler_boundary_e(tBoundary) != resampler_boundary_e::ZEROPAD && resampler_boundary_e(tBoundary) != resampler_boundary_e::NANPAD) { + intensity = interpLoop3DBoundary(floatingIntensity, xBasisIn, yBasisIn, zBasisIn, previous, fi_xyz, kernelSize); + } else { + intensity = interpLoop3D(floatingIntensity, xBasisIn, yBasisIn, zBasisIn, previous, fi_xyz, paddingValue, kernelSize); + } + }; + + position[0] = (float) (deformationFieldPtrX[index]); + position[1] = (float) (deformationFieldPtrY[index]); + position[2] = (float) (deformationFieldPtrZ[index]); + + previous[0] = cuda_reg_floor(position[0]); + previous[1] = cuda_reg_floor(position[1]); + previous[2] = cuda_reg_floor(position[2]); + + relative[0] = (double)(position[0]) - (double)(previous[0]); + relative[1] = (double)(position[1]) - (double)(previous[1]); + relative[2] = (double)(position[2]) - (double)(previous[2]); + + if (kernelType == 0) { + + double xBasisIn[2], yBasisIn[2], zBasisIn[2]; + interpNearestNeighKernel(relative[0], xBasisIn); + interpNearestNeighKernel(relative[1], yBasisIn); + interpNearestNeighKernel(relative[2], zBasisIn); + launchInterpLoop(xBasisIn, yBasisIn, zBasisIn, 2); + } else if (kernelType == 1) { + + double xBasisIn[2], yBasisIn[2], zBasisIn[2]; + interpLinearKernel(relative[0], xBasisIn); + interpLinearKernel(relative[1], yBasisIn); + interpLinearKernel(relative[2], zBasisIn); + launchInterpLoop(xBasisIn, yBasisIn, zBasisIn, 2); + } else if (kernelType == 4) { + + double xBasisIn[6], yBasisIn[6], zBasisIn[6]; + + previous[0] -= SINC_KERNEL_RADIUS; + previous[1] -= SINC_KERNEL_RADIUS; + previous[2] -= SINC_KERNEL_RADIUS; + + interpWindowedSincKernel(relative[0], xBasisIn); + interpWindowedSincKernel(relative[1], yBasisIn); + interpWindowedSincKernel(relative[2], zBasisIn); + launchInterpLoop(xBasisIn, yBasisIn, zBasisIn, 6); + } else { + + double xBasisIn[4], yBasisIn[4], zBasisIn[4]; + + previous[0]--; + previous[1]--; + previous[2]--; + + reg_getNiftynetCubicSpline(relative[0], xBasisIn); + reg_getNiftynetCubicSpline(relative[1], yBasisIn); + reg_getNiftynetCubicSpline(relative[2], zBasisIn); + launchInterpLoop(xBasisIn, yBasisIn, zBasisIn, 4); + } + resultIntensity[index] = (float)intensity; + } + index += blockDim.x * gridDim.x; + } +} +/* *************************************************************** */ +void launchResample(const nifti_image *floatingImage, + const nifti_image *warpedImage, + const int interp, + const resampler_boundary_e boundary, + const float *floatingImage_d, + float *warpedImage_d, + const float *deformationFieldImage_d) { + const float paddingValue = reg_getPaddingValue(boundary); + + + long targetVoxelNumber = (long) warpedImage->nx * warpedImage->ny * warpedImage->nz; + ulong2 voxelNumber = make_ulong2(warpedImage->nx * warpedImage->ny * warpedImage->nz, floatingImage->nx * floatingImage->ny * floatingImage->nz); + dim3 mygrid; + dim3 myblocks; + uint3 fi_xyz = make_uint3(floatingImage->nx, floatingImage->ny, floatingImage->nz); + uint2 wi_tu = make_uint2(warpedImage->nt, warpedImage->nu); + + cudaCommon_computeGridConfiguration(myblocks, mygrid, targetVoxelNumber); + if (floatingImage->nz > 1 || warpedImage->nz > 1) { + switch (boundary) { + case resampler_boundary_e::CLAMPING: + ResampleImage3D <<>>(floatingImage_d, + deformationFieldImage_d, + warpedImage_d, + voxelNumber, + fi_xyz, + wi_tu, + paddingValue, + interp); + break; + + case resampler_boundary_e::REFLECTING: + ResampleImage3D <<>>(floatingImage_d, + deformationFieldImage_d, + warpedImage_d, + voxelNumber, + fi_xyz, + wi_tu, + paddingValue, + interp); + break; + + default: + ResampleImage3D <<>>(floatingImage_d, + deformationFieldImage_d, + warpedImage_d, + voxelNumber, + fi_xyz, + wi_tu, + paddingValue, + interp); + } + } else{ + switch (boundary) { + case resampler_boundary_e::CLAMPING: + ResampleImage2D <<>>(floatingImage_d, + deformationFieldImage_d, + warpedImage_d, + voxelNumber, + fi_xyz, + wi_tu, + paddingValue, + interp); + break; + + case resampler_boundary_e::REFLECTING: + ResampleImage2D <<>>(floatingImage_d, + deformationFieldImage_d, + warpedImage_d, + voxelNumber, + fi_xyz, + wi_tu, + paddingValue, + interp); + break; + + + default: + ResampleImage2D <<>>(floatingImage_d, + deformationFieldImage_d, + warpedImage_d, + voxelNumber, + fi_xyz, + wi_tu, + paddingValue, + interp); + } + } +#ifndef NDEBUG + NR_CUDA_CHECK_KERNEL(mygrid, myblocks) +#else + NR_CUDA_SAFE_CALL(cudaThreadSynchronize()); +#endif +} +/* *************************************************************** */ diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/resampleKernel.h b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/resampleKernel.h new file mode 100644 index 00000000..f61a39f7 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/resampleKernel.h @@ -0,0 +1,26 @@ +#pragma once + +#include "resampler_boundary.h" +#include "nifti1_io.h" + +/** + * \brief Runs a resampling operation on the image referenced by pc_floating. + * \param[in] pc_floating floating image description (header only) + * \param[in] pc_warped destination image description (header only) + * \param interp interpolation code (0, 1, 3) + * \param[out] dp_warped device base pointer of destination buffer. + */ +void launchResample(const nifti_image *pc_floating, const nifti_image *pc_warped, const int interp, + const resampler_boundary_e boundary, const float *floatingImage_d, float *dp_warped, + const float *dpc_deformation); + +/** + * \brief Resamples the floating argument image based on the displacement/deformation field passed in r_displacements + * \param r_displacements displacement/deformation field, if given as displacements it is converted to a deformation field in-place, thus destroying the original data. + * \param floating the image to resample + * \param interp_code a NiftyReg interpolation code (0, 1, 3, 4) + * \param is_displacement_argument indicates if the first argument is a displacement field or a deformation field (default: true) + * \returns the resampled image, to be freed by client + */ +nifti_image* resample(nifti_image &r_displacements, const nifti_image &floating, const int interp_code, const resampler_boundary_e boundary, const bool is_displacement_argument = true); + diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/resampler_boundary.h b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/resampler_boundary.h new file mode 100644 index 00000000..2eb4813b --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/resampler_boundary.h @@ -0,0 +1,38 @@ +#pragma once + +#ifdef __CUDACC__ +#define NR_HOST_DEV __host__ __device__ +#else +#define NR_HOST_DEV +#endif + +/** \brief Resampler boundary treatment options */ +enum class resampler_boundary_e { + ZEROPAD = 0, /**< Zero-padding */ + NANPAD, /**< NaN-padding */ + CLAMPING, /**< Clamp to nearest boundary voxel intensity */ + REFLECTING, /**< Reflect indices at boundaries */ + SENTINEL, /**< Sentinel code */ +}; + +/** + * \brief Boundary index modification function + * \tparam tBoundary boundary treatment enum value + * \returns an appropriately modified index + */ +template +NR_HOST_DEV int reg_applyBoundary(const int idx, const int bound); + +/** + * \param bound upper bound on index + * \tparam tBoundary boundary treatment enum value + * \returns true if the argument index lies between 0 (incl) and bound (excl), or the index is guearanteed to be valid by virtue of the applied boundary treatment. + */ +template +NR_HOST_DEV bool reg_checkImageDimensionIndex(const TIndex index, const TBound bound); + +/** \returns the appropriate padding value for a given boundary treatment enum value */ +template +NR_HOST_DEV constexpr TVoxel reg_getPaddingValue(const resampler_boundary_e boundary); + +#include "resampler_boundary.tpp" diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/resampler_boundary.tpp b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/resampler_boundary.tpp new file mode 100644 index 00000000..fc936e7a --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_gpu_resampler/resampler_boundary.tpp @@ -0,0 +1,39 @@ +#pragma once + +#include "resampler_boundary.h" + +#include +#include +#include + +template +NR_HOST_DEV int reg_applyBoundary(const int idx, const int bound) { + int bdyIdx = idx; + + switch (tBoundary) { + case resampler_boundary_e::CLAMPING: + bdyIdx = bdyIdx >= 0? bdyIdx : 0; + bdyIdx = bdyIdx < bound? bdyIdx : bound - 1; + break; + + case resampler_boundary_e::REFLECTING: { + const int wrap_size = 2*bound - 2; + + bdyIdx = bound - 1 - std::abs(bound - 1 - (bdyIdx%wrap_size + wrap_size)%wrap_size); + break; + } + } + + return bdyIdx; +} +/* *************************************************************** */ +template +NR_HOST_DEV bool reg_checkImageDimensionIndex(const TIndex index, const TBound bound) { + return resampler_boundary_e(tBoundary) == resampler_boundary_e::CLAMPING || resampler_boundary_e(tBoundary) == resampler_boundary_e::REFLECTING || (index >= 0 && index < bound); +} +/* *************************************************************** */ +template +NR_HOST_DEV constexpr TVoxel reg_getPaddingValue(const resampler_boundary_e boundary) { + return boundary == resampler_boundary_e::ZEROPAD? TVoxel(0) + : (std::is_integral::value? std::numeric_limits::lowest() : std::numeric_limits::quiet_NaN()); +} diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_image_resampling.py b/niftynet/contrib/niftyreg_image_resampling/niftyreg_image_resampling.py new file mode 100644 index 00000000..94dd256d --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_image_resampling.py @@ -0,0 +1,125 @@ +from __future__ import print_function, division + +import tensorflow as tf +from tensorflow.python.framework import ops +from niftynet.layer.base_layer import Layer +from niftynet.layer.layer_util import infer_spatial_rank + +from niftynet.contrib.niftyreg_image_resampling.niftyreg_module_loader import get_niftyreg_module + + +# NiftyNet boundary types to NiftyReg code mapping +__BOUNDARY_CODES__ = { + 'ZERO': 0, + 'NAN': 1, + 'REPLICATE': 2, + 'SYMMETRIC': 3 +} + + +# Exposure of supported boundary types for compat. w/ ResamplerLayer +SUPPORTED_BOUNDARY = {k for k in __BOUNDARY_CODES__} + + +# NiftyNet interpolation types to NiftyReg code mapping +__INTERPOLATION_CODES__ = {'NEAREST': 0, + 'LINEAR': 1, + 'BSPLINE': 3} + + +# Exposure of supported interpolation types for compat. w/ ResamplerLayer +SUPPORTED_INTERPOLATION = {k for k in __INTERPOLATION_CODES__} + + +# NiftyReg expects displacement components to be +# indexed w/ slowest index +def _transpose(data): + nof_dims = len(data.shape) - 1 + perm = [0] + list(range(nof_dims, 0, -1)) + perm += list(range(nof_dims + 1, len(data.shape))) + assert len(perm) == len(data.shape) + + return tf.transpose(data, perm) + + +@ops.RegisterGradient("NiftyregImageResampling") +def _niftyreg_resampling_grad(op, grad): + grad_op = get_niftyreg_module().niftyreg_image_resampling_gradient( + op.inputs[0], + op.inputs[1], + interpolation=op.get_attr('interpolation'), + boundary=op.get_attr('boundary')) + + chained_grad = None + + nof_modalities = op.inputs[0].shape.as_list()[1] + if not nof_modalities is None and nof_modalities != 1: + nof_dims = op.inputs[1].shape.as_list()[1] + + assert grad_op.shape.as_list()[1] == nof_modalities*nof_dims + + chained_grads = [] + for m in range(nof_modalities): + mod_grad = grad_op[:,(nof_dims*m):((m+1)*nof_dims),...] + out_mod_grad = tf.expand_dims(grad[:,m,...], axis=1) + + out_grad = tf.tile(out_mod_grad, [1] + [nof_dims] + + [1]*(len(grad_op.shape) - 2)) + + chained_grads.append(tf.multiply(mod_grad, out_grad)) + + chained_grad = tf.reduce_sum(tf.stack(chained_grads, axis=0), axis=0) + + else: + grad_rep = tf.tile(grad, [1] + [grad_op.shape[1]] + + [1]*(len(grad_op.shape) - 2)) + chained_grad = tf.multiply(grad_rep, grad_op) + + image_grad_op \ + = get_niftyreg_module().niftyreg_image_resampling_image_gradient( + op.inputs[0], + op.inputs[1], + grad, + interpolation=op.get_attr('interpolation'), + boundary=op.get_attr('boundary')) + + return [image_grad_op, chained_grad] + + +class NiftyregImageResamplingLayer(Layer): + def __init__(self, interpolation, boundary='ZERO', **kwargs): + super(NiftyregImageResamplingLayer, self).__init__(**kwargs) + + self._interpolation = __INTERPOLATION_CODES__[interpolation.upper()] + self._boundary = boundary.upper() + + def layer_op(self, inputs, deformation, **kwargs): + nof_dims = infer_spatial_rank(inputs) + nof_output_dims = infer_spatial_rank(deformation) + + batch_size = inputs.shape.as_list()[0] + if deformation.shape.as_list()[0] != batch_size: + deformation = tf.tile(deformation, + [batch_size] + [1]*(nof_output_dims + 1)) + + output_spatial_dims = deformation.shape.as_list()[1:-1] + input_dims = [d if d else -1 for d in inputs.shape.as_list()] + if len(output_spatial_dims) != nof_dims: + resample_def = deformation + while len(resample_def.shape) < len(inputs.shape): + resample_def = tf.expand_dims(resample_def, + axis=len(resample_def.shape) - 2) + else: + resample_def = deformation + assert infer_spatial_rank(resample_def) == nof_dims + + resampled = get_niftyreg_module().niftyreg_image_resampling( + _transpose(inputs), + _transpose(resample_def), + interpolation=self._interpolation, + boundary=__BOUNDARY_CODES__[self._boundary]) + + return tf.reshape( + _transpose(resampled), + [batch_size] + output_spatial_dims + [input_dims[-1]]) + diff --git a/niftynet/contrib/niftyreg_image_resampling/niftyreg_module_loader.py b/niftynet/contrib/niftyreg_image_resampling/niftyreg_module_loader.py new file mode 100644 index 00000000..0c0bf2f0 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/niftyreg_module_loader.py @@ -0,0 +1,27 @@ +import os.path as osp +import tensorflow as tf + +__MODULE_FILENAME__ = '$' +__INSTALL_LOCATION__ \ + = '$' +__BUILD_LOCATION__ \ + = '$' + + +def _find_and_load_op(): + for test_path in (osp.join(osp.dirname(__file__), __MODULE_FILENAME__), + osp.join('.', __INSTALL_LOCATION__, __MODULE_FILENAME__), + osp.join(__INSTALL_LOCATION__, __MODULE_FILENAME__), + osp.join(__BUILD_LOCATION__, __MODULE_FILENAME__)): + if osp.isfile(test_path): + return tf.load_op_library(test_path) + + # raise ImportError('Could not locate resampling library: %s' + # % __MODULE_FILENAME__) + + +__nr_module__ = _find_and_load_op() + + +def get_niftyreg_module(): + return __nr_module__ diff --git a/niftynet/contrib/niftyreg_image_resampling/setup b/niftynet/contrib/niftyreg_image_resampling/setup new file mode 100644 index 00000000..4a90aa12 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/setup @@ -0,0 +1,92 @@ +from __future__ import print_function + +import os +import os.path as osp +import platform +from setuptools import setup, Extension, Command +from setuptools.command.build_ext import build_ext +from shutil import which +import subprocess as sp +import sys + +__CMAKE_OVERRIDE_FLAGS__ = {} + + +class CMakeExtension(Extension): + def __init__(self, name): + super(CMakeExtension, self).__init__(name, sources=[]) + + +class CMakeOverride(Command): + description = 'Overrides CMake variables for build' + + user_options = [('settings=', 's', + 'CMake variable override: :::...')] + + def initialize_options(self): + self.settings = '' + + def finalize_options(self): + pass + + def run(self): + global __CMAKE_OVERRIDE_FLAGS__ + + overrides = self.settings.split(':') + for i in range(0, len(overrides), 2): + print('Overriding %s with %s' % (overrides[i], overrides[i+1])) + __CMAKE_OVERRIDE_FLAGS__[overrides[i]] = overrides[i+1] + + +class CMakeBuildExt(build_ext): + def run(self): + for ext in self.extensions: + self.build_extension(ext) + + def build_extension(self, ext): + print('Building ' + ext.name) + + outdir = osp.abspath(osp.dirname(self.get_ext_fullpath(ext.name))) + args = ['-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=' + outdir] + if not osp.isdir(outdir): + os.makedirs(outdir) + args += ['-DGPU_RESAMPLING_CONFIGFILE_DIR=' + outdir] + args += ['-DCMAKE_BUILD_TYPE=' + ('Debug' if self.debug else 'Release')] + + if platform.system() == 'Linux' \ + and any(dist in platform.dist() for dist in ('Debian', 'Ubuntu')): + # Need to find compilers that play nice with nvcc; + # this assumes compatible versions have been linked to + # /PATH/TO/cuda/bin/cc and /PATH/TO/cuda/bin/c++, and + # that they appear first on the search path. + if not 'CMAKE_C_COMPILER' in __CMAKE_OVERRIDE_FLAGS__: + args += ['-DCMAKE_C_COMPILER=' + which('cc')] + if not 'CMAKE_CXX_COMPILER' in __CMAKE_OVERRIDE_FLAGS__: + args += ['-DCMAKE_CXX_COMPILER=' + which('c++')] + + for key, val in __CMAKE_OVERRIDE_FLAGS__.items(): + args += ['-D' + key + '=' + val] + + args += [osp.join(osp.dirname(osp.abspath(__file__)), + 'niftyreg_gpu_resampler')] + + if not osp.isdir(self.build_temp): + os.makedirs(self.build_temp) + + print('Building in ' + str(self.build_temp) + + ': cmake ' + ' '.join(args)) + sp.call(['cmake'] + args, cwd=self.build_temp) + sp.call(['cmake'] + args, cwd=self.build_temp) + sp.call(['cmake', '--build', self.build_temp]) + + +setup( + name='niftyreg_gpu_resampler', + description='A NiftyNet image resampling sub-module powered by NiftyReg ' + 'GPU code.', + packages=['.'], + ext_modules=[CMakeExtension('niftyreg_gpu_resampler')], + cmdclass={'override': CMakeOverride, + 'build_ext': CMakeBuildExt}, + zip_safe=False, +) diff --git a/niftynet/contrib/niftyreg_image_resampling/tests/__init__.py b/niftynet/contrib/niftyreg_image_resampling/tests/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/niftynet/contrib/niftyreg_image_resampling/tests/test_python_wrapper.py b/niftynet/contrib/niftyreg_image_resampling/tests/test_python_wrapper.py new file mode 100644 index 00000000..10487505 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/tests/test_python_wrapper.py @@ -0,0 +1,185 @@ +from niftynet.contrib.niftyreg_image_resampling.niftyreg_image_resampling import NiftyregImageResamplingLayer +from niftynet.contrib.niftyreg_image_resampling.tests.test_resampler import ResamplerTest + +import numpy as np +import tensorflow as tf +import tensorflow.test as tft + +class WrapperResamplerTest(ResamplerTest): + """ + Unit test for NiftyregImageResamplingLayer + """ + + INTERPOLATIONS = ((0, 'NEAREST'), + (1, 'LINEAR'), + (3, 'BSPLINE')) + + def _test_differential(self, use_gpu, interpolations): + for floating in self._get_images(True): + floating_data = floating.get_data() + nof_dims = len(floating_data.shape) + nof_mods = 2 + + if nof_dims == 3: + # 3D test takes forever (at least with debug builds) + continue + + floating_shape = list(floating_data.shape) + image_batch_shape = [1] + floating_shape + [nof_mods] + disp_batch_shape = [1] + floating_shape + [nof_dims] + + disp_template \ + = self._make_constant_displacement_image(0, 0, + floating_data) + u = 2.5001 + + floating_data = np.stack([floating_data]*nof_mods, axis=-1) + for m in range(nof_mods): + floating_data[...,m] = (1 + m)*floating_data[...,m] + + for iname in interpolations: + d = 1 + with self.session(use_gpu=use_gpu) as sess: + img = tf.constant( + floating_data.reshape(image_batch_shape), + dtype=tf.float32) + disp = tf.constant(u, + dtype=tf.float32) + + base_field = tf.constant(disp_template\ + .reshape(disp_batch_shape), + dtype=tf.float32) + + disp_field = [] + for i in range(nof_dims): + if i == d: + disp_field.append(base_field[...,i] + disp) + else: + disp_field.append(base_field[...,i]) + disp_field = tf.stack(disp_field, axis=-1) + + warped = NiftyregImageResamplingLayer(interpolation=iname, + boundary='ZERO') + warped = warped(img, disp_field) + + tgrad, refgrad = tft.compute_gradient( + disp, + (), + warped, + tuple(image_batch_shape)) + + error = np.power(tgrad - refgrad, 2).sum() + refmag = np.power(refgrad, 2).sum() + + self.assertLessEqual(error, 1e-2*refmag) + + def test_cpu_differential(self): + self._test_differential(False, ('LINEAR', 'BSPLINE')) + + def test_gpu_differential(self): + if tft.is_gpu_available(cuda_only=True) and tft.is_built_with_cuda(): + self._test_differential(True, ('LINEAR', 'BSPLINE')) + else: + self.skipTest('No CUDA support available') + + def test_image_gradient(self): + for img in self._get_images(True): + basedata = img.get_data() + + imgshape = list(basedata.shape) + nof_dims = len(imgshape) + for nof_mods in range(1, 3): + image_batch_shape = [1] + imgshape + [nof_mods] + disp_batch_shape = [1] + imgshape + [nof_dims] + + multimod_data = [] + for j in range(nof_mods): + multimod_data.append((j + 1)*basedata) + imgdata = np.stack(multimod_data, axis=-1) + + for _, inter in self.INTERPOLATIONS: + for bdy in ('REPLICATE', 'ZERO', 'SYMMETRIC'): + u = 3.5001 + d = 1 + disp = self._make_constant_displacement_image( + u, d, imgdata[...,0].reshape(imgshape)) + + with self.session() as sess: + tfimg = tf.constant( + imgdata.reshape(image_batch_shape), + dtype=tf.float32) + tfdisp = tf.constant( + disp.reshape(disp_batch_shape), + dtype=tf.float32) + + warped = NiftyregImageResamplingLayer(interpolation=inter, + boundary=bdy) + warped = warped(tfimg, tfdisp) + dummy_cost = tf.reduce_mean(tf.pow(warped, 2)) + + tgrad, refgrad = tft.compute_gradient( + tfimg, + image_batch_shape, + dummy_cost, + ()) + + error = np.power(tgrad - refgrad, 2).sum() + refmag = np.power(refgrad, 2).sum() + + self.assertLessEqual(error, 5e-2*refmag) + + def _test_resampling(self, use_gpu): + for floating in self._get_images(): + floating_data = floating.get_data() + nof_dims = len(floating_data.shape) + + floating_shape = list(floating_data.shape) + if floating_shape[-1] == 1: + floating_shape = floating_shape[:-1] + image_batch_shape = [1] + floating_shape + [1] + disp_batch_shape = [1] + floating_shape + [nof_dims] + + for code, inter in self.INTERPOLATIONS: + with self.session(use_gpu=use_gpu) as sess: + img = tf.placeholder(tf.float32, + shape=image_batch_shape) + disp = tf.placeholder(tf.float32, + shape=disp_batch_shape) + + warped = NiftyregImageResamplingLayer(interpolation=inter, + boundary='NAN') + warped = warped(img, disp) + + for u in (0.5001, 3.50001): + for d in range(nof_dims): + displacement_data \ + = self._make_constant_displacement_image( + u, d, floating_data) + + resampled_data = sess.run( + warped, + feed_dict={ + img: floating_data\ + .reshape(image_batch_shape), + disp: displacement_data\ + .reshape(disp_batch_shape), + }) + + resampled_data = resampled_data.reshape(floating_shape) + + self._test_resampled(resampled_data, floating_data, + u, code, d) + + + def test_cpu_resampling(self): + self._test_resampling(False) + + def test_gpu_resampling(self): + if tft.is_gpu_available(cuda_only=True) and tft.is_built_with_cuda(): + self._test_resampling(True) + else: + self.skipTest('No CUDA support available') + + +if __name__ == '__main__': + tft.main() diff --git a/niftynet/contrib/niftyreg_image_resampling/tests/test_resampler.py b/niftynet/contrib/niftyreg_image_resampling/tests/test_resampler.py new file mode 100644 index 00000000..fdd72152 --- /dev/null +++ b/niftynet/contrib/niftyreg_image_resampling/tests/test_resampler.py @@ -0,0 +1,157 @@ +from glob import glob +import math +import os.path as osp +import nibabel as nib +import numpy as np +import copy + +import tensorflow as tf +import tensorflow.test as tft + +from niftynet.contrib.niftyreg_image_resampling.niftyreg_module_loader import get_niftyreg_module + + +res = get_niftyreg_module() + + +class ResamplerTest(tft.TestCase): + """ + Unit test for GPUImageResampling defined in python_wrapper.cpp + """ + + def _get_images(self, do_small=False): + data_dir = osp.join(osp.dirname(__file__), 'data') + filename_temp = 'test_image_' + ('vsmall' if do_small else 'large') + '_*.nii' + + for test_img_path in glob(osp.join(data_dir, filename_temp)): + yield nib.load(test_img_path) + + @staticmethod + def _dump_image(dst_path, img): + nib.save(nib.Nifti1Image(img, np.eye(4)), + str(dst_path)) + + def _test_resampled(self, resampled, floating, d, inter, axis): + use_nearest = inter == 0 + max_idx_disp = int(round(d)) if use_nearest else int(math.ceil(d)) + + self.assertAllEqual(resampled.shape, floating.shape) + + ref = float('nan')*np.zeros_like(resampled) + size = ref.shape[axis] + + ref_slice = slice(0, size - max_idx_disp) + flo_slice = slice(max_idx_disp, size) + + base_block = [slice(0, d) for d in floating.shape] + flo_block = copy.copy(base_block) + ref_block = copy.copy(base_block) + flo_block[axis] = flo_slice + ref_block[axis] = ref_slice + + ref[tuple(ref_block)] = floating[tuple(flo_block)] + + if not use_nearest: + flo_slice = slice(max_idx_disp - 1, size - 1) + flo_block = copy.copy(base_block) + flo_block[axis] = flo_slice + ref[tuple(ref_block)] += floating[tuple(flo_block)] + ref /= 2 + + mask = np.isfinite(ref + resampled) + max_err = abs(ref[mask] - resampled[mask]).max() + self.assertLessEqual(max_err, 1e-2) + + if len(resampled.shape) == 2: + bdy_size = 1 + max(max_idx_disp, 1)*(inter + 1)\ + *(resampled.shape[0] + resampled.shape[1]) + else: + bdy_size = 7 + (max(max_idx_disp, 1) + 2)*(inter + 1)\ + *max(resampled.shape)**2 + + nof_nans = resampled.size - mask.sum() + self.assertLess(nof_nans, bdy_size) + + def _make_constant_displacement_image(self, u, axis, image_data): + nof_dims = len(image_data.shape) + displacement_shape = list(image_data.shape) \ + + [1]*(3 - nof_dims) + [nof_dims] + + displacement_data = np.zeros(displacement_shape) + displacement_data[...,axis] = u + for dd in range(nof_dims): + idcs = np.arange(displacement_data.shape[dd]) + idcs = idcs.reshape( + [1]*dd + [displacement_shape[dd]] \ + + [1]*(nof_dims - dd - 1)) + + tile_dim = list(displacement_shape[:dd]) + [1] \ + + list(displacement_shape[dd+1:nof_dims]) + + idcs = np.tile(idcs, tile_dim) + if len(idcs.shape) < 3: + idcs = idcs.reshape(list(idcs.shape) + [1]) + + displacement_data[...,dd] += idcs + + return displacement_data + + def _test_resampling(self, use_gpu): + for floating in self._get_images(): + floating_data = floating.get_data() + nof_dims = len(floating_data.shape) + + transposed_shape = list(floating_data.shape) + transposed_shape.reverse() + image_batch_shape = [1]*2 + transposed_shape + disp_batch_shape = [1] + [nof_dims] + transposed_shape + + for inter in (0, 1, 3): + with self.session(use_gpu=use_gpu) as sess: + img = tf.placeholder(tf.float32, + shape=image_batch_shape) + disp = tf.placeholder(tf.float32, + shape=disp_batch_shape) + + warped = res.niftyreg_image_resampling(img, disp, + interpolation=inter) + + for u in (0.5001, 3.50001): + for d in range(nof_dims): + displacement_data \ + = self._make_constant_displacement_image( + u, d, floating_data) + + # NiftyReg expects displacement components to be + # indexed w/ slowest index + def _transpose(data): + return np.transpose( + data, range(len(data.shape) - 1, -1, -1)) + + resampled_data = sess.run( + warped, + feed_dict={ + img: _transpose(floating_data)\ + .reshape(image_batch_shape), + disp: _transpose(displacement_data)\ + .reshape(disp_batch_shape), + }) + + resampled_data = _transpose(resampled_data).reshape(floating_data.shape) + + self._test_resampled(resampled_data, floating_data, + u, inter, d) + + + def test_cpu_resampling(self): + self._test_resampling(False) + + def test_gpu_resampling(self): + if tft.is_gpu_available(cuda_only=True) and tft.is_built_with_cuda(): + self._test_resampling(True) + else: + self.skipTest('No CUDA support available') + + +if __name__ == '__main__': + tft.main() diff --git a/niftynet/contrib/preprocessors/preprocessing.py b/niftynet/contrib/preprocessors/preprocessing.py index 6199815b..bc8dca05 100644 --- a/niftynet/contrib/preprocessors/preprocessing.py +++ b/niftynet/contrib/preprocessors/preprocessing.py @@ -58,7 +58,8 @@ def prepare_augmentation_layers(self): augmentation_layers.append(RandomSpatialScalingLayer( min_percentage=self.action_param.scaling_percentage[0], max_percentage=self.action_param.scaling_percentage[1], - antialiasing=self.action_param.antialiasing)) + antialiasing=self.action_param.antialiasing, + isotropic=self.action_param.isotropic_scaling)) if self.action_param.rotation_angle or \ self.action_param.rotation_angle_x or \ self.action_param.rotation_angle_y or \ diff --git a/niftynet/contrib/sampler_pairwise/sampler_pairwise_resize_csv.py b/niftynet/contrib/sampler_pairwise/sampler_pairwise_resize_csv.py new file mode 100755 index 00000000..2145b420 --- /dev/null +++ b/niftynet/contrib/sampler_pairwise/sampler_pairwise_resize_csv.py @@ -0,0 +1,184 @@ +from __future__ import absolute_import, division, print_function + +import numpy as np +import tensorflow as tf +#from tensorflow.contrib.data.python.ops.dataset_ops import Dataset + +from niftynet.engine.image_window import ImageWindow +from niftynet.layer.base_layer import Layer +from niftynet.layer.grid_warper import AffineGridWarperLayer +from niftynet.layer.resampler import ResamplerLayer +from niftynet.layer.linear_resize import LinearResizeLayer as Resize + + +class PairwiseResizeSampler(Layer): + def __init__(self, + reader_0, + reader_1, + data_param, + batch_size=1): + Layer.__init__(self, name='pairwise_sampler_resize') + # reader for the fixed images + self.reader_0 = reader_0 + # reader for the moving images + self.reader_1 = reader_1 + + # TODO: + # 0) check the readers should have the same length file list + # 1) detect window shape mismatches or defaulting + # windows to the fixed image reader properties + # 2) reshape images to (supporting multi-modal data) + # [batch, x, y, channel] or [batch, x, y, z, channels] + # 3) infer spatial rank + # 4) make ``label`` optional + self.batch_size = int(batch_size) + assert self.batch_size > 0, "batch size must be greater than 0" + self.spatial_rank = 3 + self.window = ImageWindow.from_data_reader_properties( + self.reader_0.input_sources, + self.reader_0.shapes, + self.reader_0.tf_dtypes, + data_param) + if self.window.has_dynamic_shapes: + tf.logging.fatal('Dynamic shapes not supported.\nPlease specify ' + 'all spatial dims of the input data, for the ' + 'spatial_window_size parameter.') + raise NotImplementedError + # TODO: check spatial dims the same across input modalities + self.image_shape = \ + self.reader_0.shapes['fixed_image'][:self.spatial_rank] + self.moving_image_shape = \ + self.reader_1.shapes['moving_image'][:self.spatial_rank] + self.window_size = self.window.shapes['fixed_image'][1:] + + # initialise a dataset prefetching pairs of image and label volumes + n_subjects = len(self.reader_0.output_list) + int_seq = list(range(n_subjects)) + # make the list of sequence divisible by batch size + while len(int_seq) > 0 and len(int_seq) % self.batch_size != 0: + int_seq.append(int_seq[-1]) + + image_dataset = tf.data.Dataset.from_tensor_slices(int_seq) + # mapping random integer id to 4 volumes moving/fixed x image/label + # tf.py_func wrapper of ``get_pairwise_inputs`` + image_dataset = image_dataset.map( + lambda image_id: tuple(tf.py_func( + self.get_pairwise_inputs, [image_id], + [tf.int32, tf.float32, tf.float32, tf.int32, tf.int32])), + num_parallel_calls=4) # supported by tf 1.4? + # todo: sequential and no repeatition + image_dataset = image_dataset.batch(self.batch_size) + self.iterator = image_dataset.make_initializable_iterator() + + def get_pairwise_inputs(self, image_id): + # fetch fixed image + fixed_inputs = [] + fixed_inputs.append(self._get_image('fixed_image', image_id)[0]) + fixed_inputs.append(self._get_image('fixed_label', image_id)[0]) + fixed_inputs = np.concatenate(fixed_inputs, axis=-1) + fixed_shape = np.asarray(fixed_inputs.shape).T.astype(np.int32) + + # fetch moving image + moving_inputs = [] + moving_inputs.append(self._get_image('moving_image', image_id)[0]) + moving_inputs.append(self._get_image('moving_label', image_id)[0]) + moving_inputs = np.concatenate(moving_inputs, axis=-1) + moving_shape = np.asarray(moving_inputs.shape).T.astype(np.int32) + + return image_id, fixed_inputs, moving_inputs, fixed_shape, moving_shape + + def _get_image(self, image_source_type, image_id): + # returns a random image from either the list of fixed images + # or the list of moving images + try: + image_source_type = image_source_type.decode() + except AttributeError: + pass + if image_source_type.startswith('fixed'): + _, data, _ = self.reader_0(idx=image_id) + else: # image_source_type.startswith('moving'): + _, data, _ = self.reader_1(idx=image_id) + image = np.asarray(data[image_source_type]).astype(np.float32) + image_shape = list(image.shape) + image = np.reshape(image, image_shape[:self.spatial_rank] + [-1]) + image_shape = np.asarray(image.shape).astype(np.int32) + return image, image_shape + + def layer_op(self): + image_id, fixed_inputs, moving_inputs, fixed_shape, moving_shape = \ + self.iterator.get_next() + # TODO preprocessing layer modifying + # image shapes will not be supported + # assuming the same shape across modalities, using the first + image_id.set_shape((self.batch_size,)) + image_id = tf.to_float(image_id) + + fixed_inputs.set_shape( + (self.batch_size,) + (None,) * self.spatial_rank + (2,)) + # last dim is 1 image + 1 label + moving_inputs.set_shape( + (self.batch_size,) + self.moving_image_shape + (2,)) + fixed_shape.set_shape((self.batch_size, self.spatial_rank + 1)) + moving_shape.set_shape((self.batch_size, self.spatial_rank + 1)) + + # resizing the moving_inputs to match the target + # assumes the same shape across the batch + target_spatial_shape = \ + tf.unstack(fixed_shape[0], axis=0)[:self.spatial_rank] + moving_inputs = Resize(new_size=target_spatial_shape)(moving_inputs) + combined_volume = tf.concat([fixed_inputs, moving_inputs], axis=-1) + + # TODO affine data augmentation here + if self.spatial_rank == 3: + + window_channels = np.prod(self.window_size[self.spatial_rank:]) * 4 + # TODO if no affine augmentation: + img_spatial_shape = target_spatial_shape + win_spatial_shape = [tf.constant(dim) for dim in + self.window_size[:self.spatial_rank]] + + # scale the image to new space + batch_scale = [ + tf.reshape(tf.to_float(img) / tf.to_float(win), (1,1)) + for (win, img) in zip(win_spatial_shape, img_spatial_shape)] + batch_scale = tf.concat(batch_scale, axis=1) + affine_constraints = ((None, 0.0, 0.0, 0.0), + (0.0, None, 0.0, 0.0), + (0.0, 0.0, None, 0.0)) + computed_grid = AffineGridWarperLayer( + source_shape=(None, None, None), + output_shape=self.window_size[:self.spatial_rank], + constraints=affine_constraints)(batch_scale) + computed_grid.set_shape((1,) + + self.window_size[:self.spatial_rank] + + (self.spatial_rank,)) + resampler = ResamplerLayer( + interpolation='linear', boundary='replicate') + windows = resampler(combined_volume, computed_grid) + out_shape = [self.batch_size] + \ + list(self.window_size[:self.spatial_rank]) + \ + [window_channels] + windows.set_shape(out_shape) + + image_id = tf.reshape(image_id, (self.batch_size, 1)) + start_location = tf.zeros((self.batch_size, self.spatial_rank)) + end_location = tf.constant(self.window_size[:self.spatial_rank]) + end_location = tf.reshape(end_location, (1, self.spatial_rank)) + end_location = tf.to_float(tf.tile( + end_location, [self.batch_size, 1])) + locations = tf.concat([ + image_id, start_location, end_location], axis=1) + return windows, locations + #return windows, [tf.reduce_max(computed_grid), batch_scale] + + # overriding input buffers + def run_threads(self, session, *args, **argvs): + """ + To be called at the beginning of running graph variables + """ + session.run(self.iterator.initializer) + return + + def close_all(self): + # do nothing + pass diff --git a/niftynet/contrib/sampler_pairwise/sampler_pairwise_uniform_csv.py b/niftynet/contrib/sampler_pairwise/sampler_pairwise_uniform_csv.py new file mode 100755 index 00000000..c17f7265 --- /dev/null +++ b/niftynet/contrib/sampler_pairwise/sampler_pairwise_uniform_csv.py @@ -0,0 +1,196 @@ +from __future__ import absolute_import, division, print_function + +import numpy as np +import tensorflow as tf +#from tensorflow.contrib.data.python.ops.dataset_ops import Dataset + +from niftynet.engine.image_window import ImageWindow +from niftynet.layer.base_layer import Layer +from niftynet.layer.grid_warper import AffineGridWarperLayer +from niftynet.layer.resampler import ResamplerLayer +from niftynet.layer.linear_resize import LinearResizeLayer as Resize +#from niftynet.layer.approximated_smoothing import SmoothingLayer as Smooth + + +class PairwiseUniformSampler(Layer): + def __init__(self, + reader_0, + reader_1, + data_param, + batch_size=1): + Layer.__init__(self, name='pairwise_sampler_uniform') + # reader for the fixed images + self.reader_0 = reader_0 + # reader for the moving images + self.reader_1 = reader_1 + + # TODO: + # 0) check the readers should have the same length file list + # 1) detect window shape mismatches or defaulting + # windows to the fixed image reader properties + # 2) reshape images to (supporting multi-modal data) + # [batch, x, y, channel] or [batch, x, y, z, channels] + # 3) infer spatial rank + # 4) make ``label`` optional + self.batch_size = batch_size + self.spatial_rank = 3 + self.window = ImageWindow.from_data_reader_properties( + self.reader_0.input_sources, + self.reader_0.shapes, + self.reader_0.tf_dtypes, + data_param) + if self.window.has_dynamic_shapes: + tf.logging.fatal('Dynamic shapes not supported.\nPlease specify ' + 'all spatial dims of the input data, for the ' + 'spatial_window_size parameter.') + raise NotImplementedError + # TODO: check spatial dims the same across input modalities + self.image_shape = \ + self.reader_0.shapes['fixed_image'][:self.spatial_rank] + self.moving_image_shape = \ + self.reader_1.shapes['moving_image'][:self.spatial_rank] + self.window_size = self.window.shapes['fixed_image'][1:] + + # initialise a dataset prefetching pairs of image and label volumes + n_subjects = len(self.reader_0.output_list) + rand_ints = np.random.randint(n_subjects, size=[n_subjects]) + image_dataset = tf.data.Dataset.from_tensor_slices(rand_ints) + # mapping random integer id to 4 volumes moving/fixed x image/label + # tf.py_func wrapper of ``get_pairwise_inputs`` + image_dataset = image_dataset.map( + lambda image_id: tuple(tf.py_func( + self.get_pairwise_inputs, [image_id], + [tf.int64, tf.float32, tf.float32, tf.int32, tf.int32])), + num_parallel_calls=4) # supported by tf 1.4? + image_dataset = image_dataset.repeat() # num_epochs can be param + image_dataset = image_dataset.shuffle( + buffer_size=self.batch_size * 20) + image_dataset = image_dataset.batch(self.batch_size) + self.iterator = image_dataset.make_initializable_iterator() + + def get_pairwise_inputs(self, image_id): + # fetch fixed image + fixed_inputs = [] + fixed_inputs.append(self._get_image('fixed_image', image_id)[0]) + fixed_inputs.append(self._get_image('fixed_label', image_id)[0]) + fixed_inputs = np.concatenate(fixed_inputs, axis=-1) + fixed_shape = np.asarray(fixed_inputs.shape).T.astype(np.int32) + + # fetch moving image + moving_inputs = [] + moving_inputs.append(self._get_image('moving_image', image_id)[0]) + moving_inputs.append(self._get_image('moving_label', image_id)[0]) + moving_inputs = np.concatenate(moving_inputs, axis=-1) + moving_shape = np.asarray(moving_inputs.shape).T.astype(np.int32) + + return image_id, fixed_inputs, moving_inputs, fixed_shape, moving_shape + + def _get_image(self, image_source_type, image_id): + # returns a random image from either the list of fixed images + # or the list of moving images + try: + image_source_type = image_source_type.decode() + except AttributeError: + pass + if image_source_type.startswith('fixed'): + _, data, _ = self.reader_0(idx=image_id) + else: # image_source_type.startswith('moving'): + _, data, _ = self.reader_1(idx=image_id) + image = np.asarray(data[image_source_type]).astype(np.float32) + image_shape = list(image.shape) + image = np.reshape(image, image_shape[:self.spatial_rank] + [-1]) + image_shape = np.asarray(image.shape).astype(np.int32) + return image, image_shape + + def layer_op(self): + """ + This function concatenate image and label volumes at the last dim + and randomly cropping the volumes (also the cropping margins) + """ + image_id, fixed_inputs, moving_inputs, fixed_shape, moving_shape = \ + self.iterator.get_next() + # TODO preprocessing layer modifying + # image shapes will not be supported + # assuming the same shape across modalities, using the first + image_id.set_shape((self.batch_size,)) + image_id = tf.to_float(image_id) + + fixed_inputs.set_shape( + (self.batch_size,) + (None,) * self.spatial_rank + (2,)) + # last dim is 1 image + 1 label + moving_inputs.set_shape( + (self.batch_size,) + self.moving_image_shape + (2,)) + fixed_shape.set_shape((self.batch_size, self.spatial_rank + 1)) + moving_shape.set_shape((self.batch_size, self.spatial_rank + 1)) + + # resizing the moving_inputs to match the target + # assumes the same shape across the batch + target_spatial_shape = \ + tf.unstack(fixed_shape[0], axis=0)[:self.spatial_rank] + moving_inputs = Resize(new_size=target_spatial_shape)(moving_inputs) + combined_volume = tf.concat([fixed_inputs, moving_inputs], axis=-1) + + # smoothing_layer = Smoothing( + # sigma=1, truncate=3.0, type_str='gaussian') + # combined_volume = tf.unstack(combined_volume, axis=-1) + # combined_volume[0] = tf.expand_dims(combined_volume[0], axis=-1) + # combined_volume[1] = smoothing_layer( + # tf.expand_dims(combined_volume[1]), axis=-1) + # combined_volume[2] = tf.expand_dims(combined_volume[2], axis=-1) + # combined_volume[3] = smoothing_layer( + # tf.expand_dims(combined_volume[3]), axis=-1) + # combined_volume = tf.stack(combined_volume, axis=-1) + + # TODO affine data augmentation here + if self.spatial_rank == 3: + + window_channels = np.prod(self.window_size[self.spatial_rank:]) * 4 + # TODO if no affine augmentation: + img_spatial_shape = target_spatial_shape + win_spatial_shape = [tf.constant(dim) for dim in + self.window_size[:self.spatial_rank]] + # when img==win make sure shift => 0.0 + # otherwise interpolation is out of bound + batch_shift = [ + tf.random_uniform( + shape=(self.batch_size, 1), + minval=0, + maxval=tf.maximum(tf.to_float(img - win - 1), 0.01)) + for (win, img) in zip(win_spatial_shape, img_spatial_shape)] + batch_shift = tf.concat(batch_shift, axis=1) + affine_constraints = ((1.0, 0.0, 0.0, None), + (0.0, 1.0, 0.0, None), + (0.0, 0.0, 1.0, None)) + computed_grid = AffineGridWarperLayer( + source_shape=(None, None, None), + output_shape=self.window_size[:self.spatial_rank], + constraints=affine_constraints)(batch_shift) + computed_grid.set_shape((self.batch_size,) + + self.window_size[:self.spatial_rank] + + (self.spatial_rank,)) + resampler = ResamplerLayer( + interpolation='linear', boundary='replicate') + windows = resampler(combined_volume, computed_grid) + out_shape = [self.batch_size] + \ + list(self.window_size[:self.spatial_rank]) + \ + [window_channels] + windows.set_shape(out_shape) + + image_id = tf.reshape(image_id, (self.batch_size, 1)) + start_location = tf.zeros((self.batch_size, self.spatial_rank)) + locations = tf.concat([ + image_id, start_location, batch_shift], axis=1) + return windows, locations + # return windows, [tf.reduce_max(computed_grid), batch_shift] + + # overriding input buffers + def run_threads(self, session, *args, **argvs): + """ + To be called at the beginning of running graph variables + """ + session.run(self.iterator.initializer) + return + + def close_all(self): + # do nothing + pass diff --git a/niftynet/contrib/segmentation_bf_aug/segmentation_application_bfaug.py b/niftynet/contrib/segmentation_bf_aug/segmentation_application_bfaug.py index e09ebdb7..ffaf5b9a 100755 --- a/niftynet/contrib/segmentation_bf_aug/segmentation_application_bfaug.py +++ b/niftynet/contrib/segmentation_bf_aug/segmentation_application_bfaug.py @@ -120,11 +120,12 @@ def initialise_dataset_loader( self.action_param.bias_field_range) augmentation_layers.append(bias_field_layer) - volume_padding_layer = [] - if self.net_param.volume_padding_size: - volume_padding_layer.append(PadLayer( - image_name=SUPPORTED_INPUT, - border=self.net_param.volume_padding_size)) + volume_padding_layer = [PadLayer( + image_name=SUPPORTED_INPUT, + border=self.net_param.volume_padding_size, + mode=self.net_param.volume_padding_mode, + pad_to=self.net_param.volume_padding_to_size) + ] self.readers[0].add_preprocessing_layers( volume_padding_layer + normalisation_layers + augmentation_layers) diff --git a/niftynet/contrib/segmentation_selective_sampler/ss_app.py b/niftynet/contrib/segmentation_selective_sampler/ss_app.py index b18d5478..77d44f4c 100755 --- a/niftynet/contrib/segmentation_selective_sampler/ss_app.py +++ b/niftynet/contrib/segmentation_selective_sampler/ss_app.py @@ -130,11 +130,12 @@ def initialise_dataset_loader( self.action_param.rotation_angle_z) augmentation_layers.append(rotation_layer) - volume_padding_layer = [] - if self.net_param.volume_padding_size: - volume_padding_layer.append(PadLayer( - image_name=SUPPORTED_INPUT, - border=self.net_param.volume_padding_size)) + volume_padding_layer = [PadLayer( + image_name=SUPPORTED_INPUT, + border=self.net_param.volume_padding_size, + mode=self.net_param.volume_padding_mode, + pad_to=self.net_param.volume_padding_to_size) + ] for reader in self.readers: reader.add_preprocessing_layers( @@ -310,5 +311,6 @@ def switch_sampler(for_training): def interpret_output(self, batch_output): if not self.is_training: return self.output_decoder.decode_batch( - batch_output['window'], batch_output['location']) + {'window_image': batch_output['window']}, + batch_output['location']) return True diff --git a/niftynet/contrib/segmentation_selective_sampler/test_sampler_selective.py b/niftynet/contrib/segmentation_selective_sampler/test_sampler_selective.py index 7cef2d34..d69c14e8 100755 --- a/niftynet/contrib/segmentation_selective_sampler/test_sampler_selective.py +++ b/niftynet/contrib/segmentation_selective_sampler/test_sampler_selective.py @@ -11,6 +11,7 @@ from niftynet.io.image_reader import ImageReader from niftynet.io.image_sets_partitioner import ImageSetsPartitioner from niftynet.utilities.util_common import ParserNamespace +from tests.niftynet_testcase import NiftyNetTestCase ### utility function for testing purposes @@ -192,7 +193,7 @@ def get_dynamic_window_reader(): return reader -class SelectiveSamplerTest(tf.test.TestCase): +class SelectiveSamplerTest(NiftyNetTestCase): def test_3d_init(self): constraint_built = Constraint(compulsory_labels=[1], min_ratio=0.000001, @@ -203,7 +204,7 @@ def test_3d_init(self): constraint=constraint_built, windows_per_image=2, queue_length=10) - with self.test_session() as sess: + with self.cached_session() as sess: sampler.run_threads(sess, num_threads=1) out = sess.run(sampler.pop_batch_op()) self.assertTrue(check_constraint(out['label'], constraint_built)) @@ -218,7 +219,7 @@ def test_3d_init(self): # batch_size=2, # windows_per_image=10, # queue_length=10) - # with self.test_session() as sess: + # with self.cached_session() as sess: # sampler.run_threads(sess, num_threads=2) # out = sess.run(sampler.pop_batch_op()) # self.assertAllClose(out['image'].shape, (2, 10, 9, 1)) @@ -234,7 +235,7 @@ def test_3d_init(self): # min_num_labels=2), # windows_per_image=2, # queue_length=2) - # with self.test_session() as sess: + # with self.cached_session() as sess: # sampler.run_threads(sess, num_threads=2) # out = sess.run(sampler.pop_batch_op()) # test = np.zeros_like(out['label']) @@ -262,7 +263,7 @@ def test_3d_init(self): # sampler.close_all() -# class RandomCoordinatesTest(tf.test.TestCase): +# class RandomCoordinatesTest(NiftyNetTestCase): # def test_coordinates(self): # coords = rand_choice_coordinates( # subject_id=1, diff --git a/niftynet/contrib/ultrasound_simulator_gan/ultrasound_simulator_gan.py b/niftynet/contrib/ultrasound_simulator_gan/ultrasound_simulator_gan.py index fa4b4dc9..aac1ad55 100755 --- a/niftynet/contrib/ultrasound_simulator_gan/ultrasound_simulator_gan.py +++ b/niftynet/contrib/ultrasound_simulator_gan/ultrasound_simulator_gan.py @@ -64,13 +64,13 @@ def concat_cond(x, i): def conv(ch, x): with tf.name_scope('conv'): - conv_layer = ConvolutionalLayer(ch, 3, with_bn=False, w_initializer=w_init) + conv_layer = ConvolutionalLayer(ch, 3, feature_normalization=None, w_initializer=w_init) c = conv_layer(x, is_training=is_training) return tf.nn.relu(tf.contrib.layers.batch_norm(c)) def up(ch, x, hack=False): with tf.name_scope('up'): - deconv = DeconvolutionalLayer(ch, 3, with_bn=False, stride=2, w_initializer=w_init)(x, is_training=is_training) + deconv = DeconvolutionalLayer(ch, 3, feature_normalization=None, stride=2, w_initializer=w_init)(x, is_training=is_training) if hack: deconv = deconv[:, :, 1:, :] # hack to match Yipeng's image size return tf.nn.relu(tf.contrib.layers.batch_norm(deconv)) @@ -96,7 +96,7 @@ def up_block(ch, x, i, hack=False): if add_noise: noise = tf.random_normal(g_h5.shape.as_list()[0:-1] + [add_noise], 0, .1) g_h5 = tf.concat([g_h5, noise],axis=3) - x_sample = ConvolutionalLayer(1, 3, with_bn=False, with_bias=True, + x_sample = ConvolutionalLayer(1, 3, feature_normalization=None, with_bias=True, w_initializer=w_init, b_initializer=b_init)(g_h5, is_training=is_training) x_sample = tf.nn.dropout(tf.nn.tanh(x_sample), keep_prob_ph) @@ -144,18 +144,18 @@ def leaky_relu(x, alpha=0.2): def down(ch, x): with tf.name_scope('downsample'): - c = ConvolutionalLayer(ch, 3, stride=2, with_bn=False, + c = ConvolutionalLayer(ch, 3, stride=2, feature_normalization=None, w_initializer=w_init)(x, is_training=is_training) c=tf.contrib.layers.batch_norm(c) c = leaky_relu(c) return c def convr(ch, x): - c= ConvolutionalLayer(ch, 3, with_bn=False, + c= ConvolutionalLayer(ch, 3, feature_normalization=None, w_initializer=w_init)(x, is_training=is_training) return leaky_relu(tf.contrib.layers.batch_norm(c)) def conv(ch, x, s): - c = (ConvolutionalLayer(ch, 3, with_bn=False, + c = (ConvolutionalLayer(ch, 3, feature_normalization=None, w_initializer=w_init)(x, is_training=is_training)) return leaky_relu(tf.contrib.layers.batch_norm(c) + s) @@ -169,7 +169,7 @@ def down_block(ch, x): image = tf.concat([image, conditioning], axis=-1) with tf.name_scope('feature'): d_h1s = ConvolutionalLayer(ch[0], 5, with_bias=True, - with_bn=False, + feature_normalization=None, w_initializer=w_init, b_initializer=b_init)(image, is_training=is_training) diff --git a/niftynet/engine/application_factory.py b/niftynet/engine/application_factory.py index eedd7702..3afca6ed 100755 --- a/niftynet/engine/application_factory.py +++ b/niftynet/engine/application_factory.py @@ -49,6 +49,8 @@ 'niftynet.network.toynet.ToyNet', "unet": 'niftynet.network.unet.UNet3D', + "nonewnet": + 'niftynet.network.no_new_net.UNet3D', "vnet": 'niftynet.network.vnet.VNet', "dense_vnet": @@ -97,6 +99,8 @@ 'niftynet.layer.loss_segmentation.generalised_wasserstein_dice_loss', "SensSpec": 'niftynet.layer.loss_segmentation.sensitivity_specificity_loss', + "VolEnforcement": + 'niftynet.layer.loss_segmentation.volume_enforcement', # "L1Loss": # 'niftynet.layer.loss_segmentation.l1_loss', # "L2Loss": @@ -115,7 +119,11 @@ "MAE": 'niftynet.layer.loss_regression.mae_loss', "Huber": - 'niftynet.layer.loss_regression.huber_loss' + 'niftynet.layer.loss_regression.huber_loss', + "SmoothL1": + 'niftynet.layer.loss_regression.smooth_l1_loss', + "Cosine": + 'niftynet.layer.loss_regression.cosine_loss' } SUPPORTED_LOSS_CLASSIFICATION = { @@ -123,6 +131,15 @@ 'niftynet.layer.loss_classification.cross_entropy', } +SUPPORTED_LOSS_CLASSIFICATION_MULTI = { + "ConfusionMatrix": + 'niftynet.layer.loss_classification_multi.loss_confusion_matrix', + "Variability": + 'niftynet.layer.loss_classification_multi.loss_variability', + "Consistency": + 'niftynet.layer.loss_classification_multi.rmse_consistency' +} + SUPPORTED_LOSS_AUTOENCODER = { "VariationalLowerBound": @@ -217,7 +234,11 @@ 'console_logger': 'niftynet.engine.handler_console.ConsoleLogger', 'tensorboard_logger': - 'niftynet.engine.handler_tensorboard.TensorBoardLogger' + 'niftynet.engine.handler_tensorboard.TensorBoardLogger', + 'performance_logger': + 'niftynet.engine.handler_performance.PerformanceLogger', + 'early_stopper': + 'niftynet.engine.handler_early_stopping.EarlyStopper', } SUPPORTED_ITERATION_GENERATORS = { @@ -348,6 +369,15 @@ class LossClassificationFactory(ModuleFactory): type_str = 'classification loss' +class LossClassificationMultiFactory(ModuleFactory): + """ + Import a classification loss function from niftynet.layer or + from user specified string + """ + SUPPORTED = SUPPORTED_LOSS_CLASSIFICATION_MULTI + type_str = 'classification multi loss' + + class LossAutoencoderFactory(ModuleFactory): """ Import an autoencoder loss function from ``niftynet.layer`` or diff --git a/niftynet/engine/handler_early_stopping.py b/niftynet/engine/handler_early_stopping.py new file mode 100644 index 00000000..5adf1bc3 --- /dev/null +++ b/niftynet/engine/handler_early_stopping.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +""" +This module implements an early stopping handler +""" + +import numpy as np +import tensorflow as tf +from scipy.ndimage import median_filter + +from niftynet.engine.signal import ITER_FINISHED + + +class EarlyStopper(object): + """ + This class handles iteration events to store the current performance as + an attribute of the sender (i.e. application). + """ + + def __init__(self, **_unused): + ITER_FINISHED.connect(self.check_criteria) + + def check_criteria(self, _sender, **msg): + """ + Printing iteration message with ``tf.logging`` interface. + :param _sender: + :param msg: an iteration message instance + :return: + """ + msg = msg['iter_msg'] + if len(_sender.performance_history) == _sender.patience: + # Value / threshold based methods: + # Check the latest value of the performance history against a + # threshold calculated based on the performance history + msg.should_stop = \ + check_should_stop( + mode=_sender.mode, + performance_history=_sender.performance_history) + + +def compute_generalisation_loss(validation_his): + """ + This function computes the generalisation loss as + l[-1]-min(l)/max(l)-min(l) + :param validation_his: performance history + :return: generalisation loss + """ + min_val_loss = np.min(np.array(validation_his)) + max_val_loss = np.max(np.array(validation_his)) + last = validation_his[-1] + if min_val_loss == 0: + return last + return (last-min_val_loss)/(max_val_loss - min_val_loss) + + +def check_should_stop(performance_history, mode='mean', min_delta=0.03, + kernel_size=5, k_splits=5): + """ + This function takes in a mode, performance_history and patience and + returns True if the application should stop early. + :param mode: {'mean', 'robust_mean', 'median', 'generalisation_loss', + 'median_smoothing', 'validation_up'} the default mode is 'mean' + mean: + If your last loss is less than the average across the entire + performance history stop training + robust_mean: + Same as 'mean' but only loss values within 5th and 95th + percentile are considered + median: + As in mode='mean' but using the median + generalisation_loss: + Computes generalisation loss over the performance + history, and stops if it reaches an arbitrary threshold of 0.2. + validation_up: + This method check for performance increases in k sub-arrays of + length s, where k x s = patience. Because we cannot guarantee + patience to be divisible by both k and s, we define that k is + either 4 or 5, depending on which has the smallest remainder when + dividing. + :param performance_history: a list of size patience with the performance + history + :param min_delta: threshold for smoothness + :param kernel_size: hyperparameter for median smoothing + :param k_splits: number of splits if using 'validation_up' + :return: + """ + if mode == 'mean': + performance_to_consider = performance_history[:-1] + thresh = np.mean(performance_to_consider) + tf.logging.info("====Mean====") + tf.logging.info(thresh) + tf.logging.info(performance_history[-1]) + should_stop = performance_history[-1] > thresh + + elif mode == 'robust_mean': + performance_to_consider = performance_history[:-1] + perc = np.percentile(performance_to_consider, q=[5, 95]) + temp = [] + for perf_val in performance_to_consider: + if perc[0] < perf_val < perc[1]: + temp.append(perf_val) + should_stop = performance_history[-1] > np.mean(temp) + + elif mode == 'median': + performance_to_consider = performance_history[:-1] + should_stop = performance_history[-1] > np.median( + performance_to_consider) + + elif mode == 'generalisation_loss': + value = compute_generalisation_loss(performance_history) + should_stop = value > 0.2 + + elif mode == 'median_smoothing': + smoothed = median_filter(performance_history[:-1], + size=kernel_size) + gradient = np.gradient(smoothed) + thresholded = np.where(gradient < min_delta, 1, 0) + value = np.sum(thresholded) * 1.0 / len(gradient) + should_stop = value < 0.5 + elif mode == 'validation_up': + remainder = len(performance_history) % k_splits + performance_to_consider = performance_history[remainder:] + strips = np.split(np.array(performance_to_consider), k_splits) + gl_increase = [] + for strip in strips: + generalisation_loss = compute_generalisation_loss(strip) + gl_increase.append(generalisation_loss >= min_delta) + tf.logging.info("====Validation_up====") + tf.logging.info(gl_increase) + should_stop = False not in gl_increase + else: + raise Exception('Mode: {} provided is not supported'.format(mode)) + return should_stop diff --git a/niftynet/engine/handler_performance.py b/niftynet/engine/handler_performance.py new file mode 100644 index 00000000..786d71e8 --- /dev/null +++ b/niftynet/engine/handler_performance.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" +This module tracks model validation performance over training +""" +import tensorflow as tf + +from niftynet.engine.application_variables import CONSOLE +from niftynet.engine.signal import ITER_FINISHED + + +class PerformanceLogger(object): + """ + This class handles iteration events to store the current performance as + an attribute of the sender (i.e. application). + """ + + def __init__(self, **_unused): + ITER_FINISHED.connect(self.update_performance_history) + + def update_performance_history(self, _sender, **msg): + """ + Printing iteration message with ``tf.logging`` interface. + :param _sender: + :param msg: an iteration message instance + :return: + """ + iter_msg = msg['iter_msg'] + + if iter_msg.is_validation: + try: + console_content = iter_msg.current_iter_output.get(CONSOLE, '') + current_loss = console_content['total_loss'] + + if len(_sender.performance_history) < _sender.patience: + _sender.performance_history.append(current_loss) + else: + _sender.performance_history = \ + _sender.performance_history[1:] + [current_loss] + except AttributeError: + tf.logging.warning("does not contain any performance field " + "called total loss.") diff --git a/niftynet/engine/image_window_dataset.py b/niftynet/engine/image_window_dataset.py index b51027b6..caa99152 100755 --- a/niftynet/engine/image_window_dataset.py +++ b/niftynet/engine/image_window_dataset.py @@ -320,11 +320,14 @@ def _dataset_from_generator(self): if self._num_threads < 2 or not self.shuffle: window_generator = self else: + # self._enqueuer = GeneratorEnqueuer( + # self(), + # use_multiprocessing=True, + # wait_time=0.01, + # seed=self._seed) self._enqueuer = GeneratorEnqueuer( self(), - use_multiprocessing=True, - wait_time=0.01, - seed=self._seed) + use_multiprocessing=True) self._enqueuer.start( workers=self._num_threads, max_queue_size=self.queue_length) window_generator = self._enqueuer.get diff --git a/niftynet/engine/sampler_weighted_v2.py b/niftynet/engine/sampler_weighted_v2.py index 4adbea68..3b4004bd 100755 --- a/niftynet/engine/sampler_weighted_v2.py +++ b/niftynet/engine/sampler_weighted_v2.py @@ -73,7 +73,9 @@ def weighted_spatial_coordinates( win_spatial_size = np.asarray(win_spatial_size, dtype=np.int32) cropped_map = crop_sampling_map(sampler_map, win_spatial_size) flatten_map = cropped_map.flatten() - flatten_map = flatten_map - np.min(flatten_map) + flatten_map_min = np.min(flatten_map) + if flatten_map_min < 0: + flatten_map -= flatten_map_min normaliser = flatten_map.sum() # get the sorting indexes to that we can invert the sorting later on. sorted_indexes = np.argsort(flatten_map) @@ -81,10 +83,10 @@ def weighted_spatial_coordinates( np.true_divide(flatten_map[sorted_indexes], normaliser)) middle_coords = np.zeros((n_samples, N_SPATIAL), dtype=np.int32) - for sample in range(0, n_samples): + for sample in range(n_samples): # get n_sample from the cumulative histogram, spaced by 1/n_samples, # plus a random perturbation to give us a stochastic sampler - sample_ratio = 1 - (np.random.random() + sample) / (n_samples + 1) + sample_ratio = 1 - (np.random.random() + sample) / n_samples # find the index where the cumulative it above the sample threshold try: if normaliser == 0: diff --git a/niftynet/engine/windows_aggregator_base.py b/niftynet/engine/windows_aggregator_base.py index 971caea6..d3e4e8b6 100755 --- a/niftynet/engine/windows_aggregator_base.py +++ b/niftynet/engine/windows_aggregator_base.py @@ -25,6 +25,8 @@ def __init__(self, image_reader=None, output_path='.'): self._image_id = None self.postfix = '' self.output_path = os.path.abspath(output_path) + if not os.path.exists(self.output_path): + os.makedirs(self.output_path) self.inferred_cleared = False @property @@ -72,6 +74,8 @@ def decode_batch(self, *args, **kwargs): @staticmethod def _is_stopping_signal(location_vector): + if location_vector is None: + return True return np.any(location_vector < 0) @staticmethod @@ -111,10 +115,11 @@ def crop_batch(window, location, border=None): tf.logging.fatal( 'network output window can be ' 'cropped by specifying the border parameter in config file, ' - 'but here the output window %s is already smaller ' - 'than the input window size minus padding: %s, ' - 'not supported by this aggregator', - spatial_shape, cropped_shape) + 'but here the output window content size %s is smaller ' + 'than the window coordinate size: %s -- ' + 'computed by input window size minus border size (%s)' + 'not supported by this aggregator. Please try larger border.)', + spatial_shape, cropped_shape, border) raise ValueError if n_spatial == 1: window = window[:, diff --git a/niftynet/engine/windows_aggregator_classifier.py b/niftynet/engine/windows_aggregator_classifier.py deleted file mode 100755 index 9d663c2e..00000000 --- a/niftynet/engine/windows_aggregator_classifier.py +++ /dev/null @@ -1,72 +0,0 @@ -# -*- coding: utf-8 -*- -""" -windows aggregator resize each item -in a batch output and save as an image -""" -from __future__ import absolute_import, print_function, division - -import os - -import numpy as np - -import niftynet.io.misc_io as misc_io -from niftynet.engine.windows_aggregator_base import ImageWindowsAggregator -from niftynet.layer.discrete_label_normalisation import \ - DiscreteLabelNormalisationLayer - - -class ClassifierSamplesAggregator(ImageWindowsAggregator): - """ - This class decodes each item in a batch by saving classification - labels to a new image volume. - """ - def __init__(self, - image_reader, - name='image', - output_path=os.path.join('.', 'output'), - postfix='_niftynet_out'): - ImageWindowsAggregator.__init__( - self, image_reader=image_reader, output_path=output_path) - self.name = name - self.output_interp_order = 0 - self.postfix = postfix - self.csv_path = os.path.join(self.output_path, self.postfix+'.csv') - if os.path.exists(self.csv_path): - os.remove(self.csv_path) - - def decode_batch(self, window, location): - """ - window holds the classifier labels - location is a holdover from segmentation and may be removed - in a later refactoring, but currently hold info about the stopping - signal from the sampler - """ - n_samples = window.shape[0] - for batch_id in range(n_samples): - if self._is_stopping_signal(location[batch_id]): - return False - self.image_id = location[batch_id, 0] - self._save_current_image(window[batch_id, ...]) - return True - - def _save_current_image(self, image_out): - if self.input_image is None: - return - window_shape = [1, 1, 1, 1, image_out.shape[-1]] - image_out = np.reshape(image_out, window_shape) - for layer in reversed(self.reader.preprocessors): - if isinstance(layer, DiscreteLabelNormalisationLayer): - image_out, _ = layer.inverse_op(image_out) - subject_name = self.reader.get_subject_id(self.image_id) - filename = "{}{}.nii.gz".format(subject_name, self.postfix) - source_image_obj = self.input_image[self.name] - misc_io.save_data_array(self.output_path, - filename, - image_out, - source_image_obj, - self.output_interp_order) - with open(self.csv_path, 'a') as csv_file: - data_str = ','.join([str(i) for i in image_out[0, 0, 0, 0, :]]) - csv_file.write(subject_name+','+data_str+'\n') - self.log_inferred(subject_name, filename) - return diff --git a/niftynet/engine/windows_aggregator_grid.py b/niftynet/engine/windows_aggregator_grid.py index d26341c7..82b1a0bb 100755 --- a/niftynet/engine/windows_aggregator_grid.py +++ b/niftynet/engine/windows_aggregator_grid.py @@ -3,12 +3,16 @@ windows aggregator decode sampling grid coordinates and image id from batch data, forms image level output and write to hard drive. """ -from __future__ import absolute_import, print_function, division +from __future__ import absolute_import, division, print_function import os +from collections import OrderedDict import numpy as np +import pandas as pd +# pylint: disable=too-many-nested-blocks +# pylint: disable=too-many-branches import niftynet.io.misc_io as misc_io from niftynet.engine.windows_aggregator_base import ImageWindowsAggregator from niftynet.layer.discrete_label_normalisation import \ @@ -22,70 +26,167 @@ class GridSamplesAggregator(ImageWindowsAggregator): initialised as all zeros, and the values are replaced by image window data decoded from batch. """ + def __init__(self, image_reader, name='image', output_path=os.path.join('.', 'output'), window_border=(), interp_order=0, - postfix='_niftynet_out'): + postfix='niftynet_out', + fill_constant=0.0): ImageWindowsAggregator.__init__( self, image_reader=image_reader, output_path=output_path) self.name = name self.image_out = None + self.csv_out = None self.window_border = window_border self.output_interp_order = interp_order self.postfix = postfix + self.fill_constant = fill_constant def decode_batch(self, window, location): + """ + Function used to save multiple outputs listed in the window + dictionary. For the fields that have the keyword 'window' in the + dictionary key, it will be saved as image. The rest will be saved as + csv. CSV files will contain at saving a first line of 0 (to be + changed into the header by the user), the first column being the + index of the window, followed by the list of output and the location + array for each considered window + + :param window: dictionary of output + :param location: location of the input + :return: + """ n_samples = location.shape[0] - window, location = self.crop_batch(window, location, self.window_border) + location_cropped = {} + for key in window: + if 'window' in key: # all outputs to be created as images should + # contained the keyword "window" + window[key], location_cropped[key] = self.crop_batch( + window[key], location, self.window_border) for batch_id in range(n_samples): - image_id, x_start, y_start, z_start, x_end, y_end, z_end = \ - location[batch_id, :] + image_id = location[batch_id, 0] if image_id != self.image_id: # image name changed: - # save current image and create an empty image + # save current result and create an empty result file self._save_current_image() + self._save_current_csv() if self._is_stopping_signal(location[batch_id]): return False - self.image_out = self._initialise_empty_image( - image_id=image_id, - n_channels=window.shape[-1], - dtype=window.dtype) - self.image_out[x_start:x_end, - y_start:y_end, - z_start:z_end, ...] = window[batch_id, ...] + + self.image_out, self.csv_out = {}, {} + for key in window: + if 'window' in key: + # to be saved as image + self.image_out[key] = self._initialise_empty_image( + image_id=image_id, + n_channels=window[key].shape[-1], + dtype=window[key].dtype) + else: + # to be saved as csv file + n_elements = np.int64( + np.asarray(window[key]).size / n_samples) + table_header = [ + '{}_{}'.format(key, idx) + for idx in range(n_elements) + ] if n_elements > 1 else ['{}'.format(key)] + table_header += [ + 'coord_{}'.format(idx) + for idx in range(location.shape[-1]) + ] + self.csv_out[key] = self._initialise_empty_csv( + key_names=table_header) + + for key in window: + if 'window' in key: + x_start, y_start, z_start, x_end, y_end, z_end = \ + location_cropped[key][batch_id, 1:] + self.image_out[key][ + x_start:x_end, y_start:y_end, z_start:z_end, ...] = \ + window[key][batch_id, ...] + else: + window[key] = np.asarray(window[key]).reshape( + [n_samples, -1]) + window_save = window[key][batch_id:batch_id + 1, :] + window_loc = location[batch_id:batch_id + 1, :] + csv_row = np.concatenate([window_save, window_loc], 1) + csv_row = csv_row.ravel() + key_names = self.csv_out[key].columns + self.csv_out[key] = self.csv_out[key].append( + OrderedDict(zip(key_names, csv_row)), + ignore_index=True) return True def _initialise_empty_image(self, image_id, n_channels, dtype=np.float): + """ + Initialise an empty image in which to populate the output + :param image_id: image_id to be used in the reader + :param n_channels: numbers of channels of the saved output (for + multimodal output) + :param dtype: datatype used for the saving + :return: the initialised empty image + """ self.image_id = image_id spatial_shape = self.input_image[self.name].shape[:3] - output_image_shape = spatial_shape + (n_channels,) + output_image_shape = spatial_shape + (n_channels, ) empty_image = np.zeros(output_image_shape, dtype=dtype) - for layer in self.reader.preprocessors: if isinstance(layer, PadLayer): empty_image, _ = layer(empty_image) + + if self.fill_constant != 0.0: + empty_image[:] = self.fill_constant + return empty_image + def _initialise_empty_csv(self, key_names): + """ + Initialise a csv output file with a first line of zeros + + :param n_channel: number of saved fields + :return: empty first line of the array to be saved as csv + """ + return pd.DataFrame(columns=key_names) + def _save_current_image(self): + """ + For all the outputs to be saved as images, go through the dictionary + and save the resulting output after reversing the initial preprocessing + :return: + """ if self.input_image is None: return - for layer in reversed(self.reader.preprocessors): if isinstance(layer, PadLayer): - self.image_out, _ = layer.inverse_op(self.image_out) + for i in self.image_out: + self.image_out[i], _ = layer.inverse_op(self.image_out[i]) if isinstance(layer, DiscreteLabelNormalisationLayer): - self.image_out, _ = layer.inverse_op(self.image_out) + for i in self.image_out: + self.image_out[i], _ = layer.inverse_op(self.image_out[i]) + subject_name = self.reader.get_subject_id(self.image_id) + for i in self.image_out: + filename = "{}_{}_{}.nii.gz".format(i, subject_name, self.postfix) + source_image_obj = self.input_image[self.name] + misc_io.save_data_array(self.output_path, filename, + self.image_out[i], source_image_obj, + self.output_interp_order) + self.log_inferred(subject_name, filename) + return + + def _save_current_csv(self): + """ + For all output to be saved as csv, loop through the dictionary of + output and create the csv + :return: + """ + if self.input_image is None: + return subject_name = self.reader.get_subject_id(self.image_id) - filename = "{}{}.nii.gz".format(subject_name, self.postfix) - source_image_obj = self.input_image[self.name] - misc_io.save_data_array(self.output_path, - filename, - self.image_out, - source_image_obj, - self.output_interp_order) - self.log_inferred(subject_name, filename) + for i in self.csv_out: + filename = "{}_{}_{}.csv".format(i, subject_name, self.postfix) + misc_io.save_csv_array(self.output_path, filename, self.csv_out[i]) + self.log_inferred(subject_name, filename) return diff --git a/niftynet/engine/windows_aggregator_identity.py b/niftynet/engine/windows_aggregator_identity.py index 00d45866..a8623c58 100755 --- a/niftynet/engine/windows_aggregator_identity.py +++ b/niftynet/engine/windows_aggregator_identity.py @@ -2,13 +2,19 @@ """ windows aggregator saves each item in a batch output as an image. """ -from __future__ import absolute_import, print_function, division +from __future__ import absolute_import, division, print_function import os +from collections import OrderedDict + +import numpy as np +import pandas as pd import niftynet.io.misc_io as misc_io from niftynet.engine.windows_aggregator_base import ImageWindowsAggregator +# pylint: disable=too-many-branches + class WindowAsImageAggregator(ImageWindowsAggregator): """ @@ -31,6 +37,7 @@ class WindowAsImageAggregator(ImageWindowsAggregator): (indicates output image is from single input image) ``location[batch_id, 0]`` is used as the file name """ + def __init__(self, image_reader=None, output_path=os.path.join('.', 'output'), @@ -54,40 +61,77 @@ def _decode_subject_name(self, location=None): def decode_batch(self, window, location=None): if location is not None: n_samples = location.shape[0] - for batch_id in range(n_samples): - if self._is_stopping_signal(location[batch_id]): - return False - filename = self._decode_subject_name(location[batch_id, 0]) - # if base file name changed, reset relative name index - if filename != self.output_id['base_name']: - self.output_id['base_name'] = filename - self.output_id['relative_id'] = 0 - # when location has two component, the name should - # be constructed as a composite of two input filenames - if len(location[batch_id]) == 2: - output_name = '{}_{}'.format( - self.output_id['base_name'], - self._decode_subject_name(location[batch_id, 1])) - else: - output_name = self.output_id['base_name'] - self._save_current_image(self.output_id['relative_id'], - output_name, - window[batch_id, ...]) - self.output_id['relative_id'] += 1 - return True - n_samples = window.shape[0] + else: + n_samples = window[sorted(window)[0]].shape[0] + for batch_id in range(n_samples): - filename = self._decode_subject_name() - self._save_current_image( - batch_id, filename, window[batch_id, ...]) - return False + location_b = location[batch_id] if (location is not None) else None + if self._is_stopping_signal(location_b): + return False + filename = self._decode_subject_name(location_b[0]) \ + if (location_b is not None) else self._decode_subject_name() + # if base file name changed, reset relative name index + if filename != self.output_id['base_name']: + self.output_id['base_name'] = filename + self.output_id['relative_id'] = 0 + # when location has two component, the name should + # be constructed as a composite of two input filenames + if (location_b is not None) and (len(location_b) == 2): + output_name = '{}_{}'.format( + self.output_id['base_name'], + self._decode_subject_name(location_b[1])) + else: + output_name = self.output_id['base_name'] + + for key in window: + output_name_k = '{}_{}'.format(output_name, key) + if 'window' in key: + self._save_current_image(self.output_id['relative_id'], + output_name_k, + window[key][batch_id, ...]) + else: + window[key] = np.asarray(window[key]).reshape( + [n_samples, -1]) + n_elements = window[key].shape[-1] + table_header = [ + '{}_{}'.format(key, idx) for idx in range(n_elements) + ] if n_elements > 1 else ['{}'.format(key)] + csv_table = pd.DataFrame(columns=table_header) + csv_table = csv_table.append( + OrderedDict(zip(table_header, window[key].ravel())), + ignore_index=True) + self._save_current_csv(self.output_id['relative_id'], + output_name_k, csv_table) + self.output_id['relative_id'] += 1 + return True def _save_current_image(self, idx, filename, image): if image is None: return - uniq_name = "{}_{}{}.nii.gz".format(idx, filename, self.postfix) + if idx == 0: + uniq_name = "{}{}.nii.gz".format(filename, self.postfix) + else: + uniq_name = "{}_{}{}.nii.gz".format(idx, filename, self.postfix) misc_io.save_data_array(self.output_path, uniq_name, image, None) with open(self.inferred_csv, 'a') as csv_file: filename = os.path.join(self.output_path, filename) - csv_file.write('{},{}\n'.format(idx, uniq_name)) + csv_file.write('{},{}\n'.format(idx, filename)) + return + + def _save_current_csv(self, idx, filename, csv_data): + """ + Save all csv output present in the dictionary of csv_output. + :return: + """ + + if csv_data is None: + return + if idx == 0: + uniq_name = "{}{}.csv".format(filename, self.postfix) + else: + uniq_name = "{}_{}{}.csv".format(idx, filename, self.postfix) + misc_io.save_csv_array(self.output_path, uniq_name, csv_data) + with open(self.inferred_csv, 'a') as csv_file: + filename = os.path.join(self.output_path, filename) + csv_file.write('{},{}\n'.format(idx, filename)) return diff --git a/niftynet/engine/windows_aggregator_resize.py b/niftynet/engine/windows_aggregator_resize.py index 0aae6ff4..7c072ab7 100755 --- a/niftynet/engine/windows_aggregator_resize.py +++ b/niftynet/engine/windows_aggregator_resize.py @@ -3,11 +3,13 @@ Windows aggregator resize each item in a batch output and save as an image. """ -from __future__ import absolute_import, print_function, division +from __future__ import absolute_import, division, print_function import os +from collections import OrderedDict import numpy as np +import pandas as pd import niftynet.io.misc_io as misc_io from niftynet.engine.sampler_resize_v2 import zoom_3d @@ -20,84 +22,159 @@ class ResizeSamplesAggregator(ImageWindowsAggregator): """ This class decodes each item in a batch by resizing each image - window and save as a new image volume. + window and save as a new image volume. Multiple output image can be + proposed and csv output can be performed as well """ + def __init__(self, image_reader, name='image', output_path=os.path.join('.', 'output'), window_border=(), interp_order=0, - postfix='_niftynet_out'): + postfix='niftynet_out'): ImageWindowsAggregator.__init__( self, image_reader=image_reader, output_path=output_path) self.name = name + self.image_out = None + self.csv_out = None self.window_border = window_border self.output_interp_order = interp_order self.postfix = postfix + self.current_out = {} def decode_batch(self, window, location): """ Resizing each output image window in the batch as an image volume location specifies the original input image (so that the interpolation order, original shape information retained in the - generated outputs). + + generated outputs).For the fields that have the keyword 'window' in the + dictionary key, it will be saved as image. The rest will be saved as + csv. CSV files will contain at saving a first line of 0 (to be + changed into the header by the user), the first column being the + index of the window, followed by the list of output. + """ n_samples = location.shape[0] - window, location = self.crop_batch(window, location, self.window_border) for batch_id in range(n_samples): if self._is_stopping_signal(location[batch_id]): return False self.image_id = location[batch_id, 0] - resize_to_shape = self._initialise_image_shape( - image_id=self.image_id, - n_channels=window.shape[-1]) - self._save_current_image(window[batch_id, ...], resize_to_shape) + self.image_out, self.csv_out = {}, {} + for key in window: + if 'window' in key: + # saving image output + while window[key].ndim < 5: + window[key] = window[key][..., np.newaxis, :] + self.image_out[key] = window[key][batch_id, ...] + else: + # saving csv output + window[key] = np.asarray(window[key]).reshape( + [n_samples, -1]) + n_elements = window[key].shape[-1] + table_header = [ + '{}_{}'.format(key, idx) for idx in range(n_elements) + ] if n_elements > 1 else ['{}'.format(key)] + self.csv_out[key] = self._initialise_empty_csv( + key_names=table_header) + csv_row = window[key][batch_id:batch_id + 1, :].ravel() + self.csv_out[key] = self.csv_out[key].append( + OrderedDict(zip(table_header, csv_row)), + ignore_index=True) + self._save_current_image() + self._save_current_csv() + return True def _initialise_image_shape(self, image_id, n_channels): + """ + Return the shape of the empty image to be saved + :param image_id: index to find the appropriate input image from the + reader + :param n_channels: number of channels of the image + :return: shape of the empty image + """ self.image_id = image_id spatial_shape = self.input_image[self.name].shape[:3] - output_image_shape = spatial_shape + (1, n_channels,) + output_image_shape = spatial_shape + (1, n_channels) empty_image = np.zeros(output_image_shape, dtype=np.bool) for layer in self.reader.preprocessors: if isinstance(layer, PadLayer): empty_image, _ = layer(empty_image) return empty_image.shape - def _save_current_image(self, image_out, resize_to): + def _save_current_image(self): + """ + Loop through the dictionary of images output and resize and reverse + the preprocessing prior to saving + :return: + """ if self.input_image is None: return - window_shape = resize_to - while image_out.ndim < 5: - image_out = image_out[..., np.newaxis, :] - if self.window_border and any([b > 0 for b in self.window_border]): - np_border = self.window_border - while len(np_border) < 5: - np_border = np_border + (0,) - np_border = [(b,) for b in np_border] - image_out = np.pad(image_out, np_border, mode='edge') - image_shape = image_out.shape - zoom_ratio = \ - [float(p) / float(d) for p, d in zip(window_shape, image_shape)] - image_shape = list(image_shape[:3]) + [1, image_shape[-1]] - image_out = np.reshape(image_out, image_shape) - image_out = zoom_3d(image=image_out, - ratio=zoom_ratio, - interp_order=self.output_interp_order) + + self.current_out = {} + for i in self.image_out: + resize_to_shape = self._initialise_image_shape( + image_id=self.image_id, n_channels=self.image_out[i].shape[-1]) + window_shape = resize_to_shape + current_out = self.image_out[i] + while current_out.ndim < 5: + current_out = current_out[..., np.newaxis, :] + if self.window_border and any([b > 0 for b in self.window_border]): + np_border = self.window_border + while len(np_border) < 5: + np_border = np_border + (0, ) + np_border = [(b, ) for b in np_border] + current_out = np.pad(current_out, np_border, mode='edge') + image_shape = current_out.shape + zoom_ratio = \ + [float(p) / float(d) for p, d in zip(window_shape, image_shape)] + image_shape = list(image_shape[:3]) + [1, image_shape[-1]] + current_out = np.reshape(current_out, image_shape) + current_out = zoom_3d( + image=current_out, + ratio=zoom_ratio, + interp_order=self.output_interp_order) + self.current_out[i] = current_out for layer in reversed(self.reader.preprocessors): if isinstance(layer, PadLayer): - image_out, _ = layer.inverse_op(image_out) + for i in self.image_out: + self.current_out[i], _ = layer.inverse_op( + self.current_out[i]) if isinstance(layer, DiscreteLabelNormalisationLayer): - image_out, _ = layer.inverse_op(image_out) + for i in self.image_out: + self.image_out[i], _ = layer.inverse_op(self.image_out[i]) subject_name = self.reader.get_subject_id(self.image_id) - filename = "{}{}.nii.gz".format(subject_name, self.postfix) - source_image_obj = self.input_image[self.name] - misc_io.save_data_array(self.output_path, - filename, - image_out, - source_image_obj, - self.output_interp_order) - self.log_inferred(subject_name, filename) + for i in self.image_out: + filename = "{}_{}_{}.nii.gz".format(i, subject_name, self.postfix) + source_image_obj = self.input_image[self.name] + misc_io.save_data_array(self.output_path, filename, + self.current_out[i], source_image_obj, + self.output_interp_order) + self.log_inferred(subject_name, filename) return + + def _save_current_csv(self): + """ + Save all csv output present in the dictionary of csv_output. + :return: + """ + if self.input_image is None: + return + subject_name = self.reader.get_subject_id(self.image_id) + for i in self.csv_out: + filename = "{}_{}_{}.csv".format(i, subject_name, self.postfix) + misc_io.save_csv_array(self.output_path, filename, self.csv_out[i]) + self.log_inferred(subject_name, filename) + return + + def _initialise_empty_csv(self, key_names): + """ + Initialise the array to be saved as csv as a line of zeros according + to the number of elements to be saved + :param n_channel: + :return: + """ + return pd.DataFrame(columns=key_names) diff --git a/niftynet/evaluation/classification_evaluations.py b/niftynet/evaluation/classification_evaluations.py index 7a0ae3f9..b362ed26 100755 --- a/niftynet/evaluation/classification_evaluations.py +++ b/niftynet/evaluation/classification_evaluations.py @@ -35,4 +35,4 @@ def aggregate(self, df): return [agg] def get_aggregations(self): - return [DataFrameAggregator(('subject_id',), self.aggregate)] \ No newline at end of file + return [DataFrameAggregator(('subject_id',), self.aggregate)] diff --git a/niftynet/evaluation/segmentation_evaluations.py b/niftynet/evaluation/segmentation_evaluations.py index d3ad6ed7..79c622bd 100755 --- a/niftynet/evaluation/segmentation_evaluations.py +++ b/niftynet/evaluation/segmentation_evaluations.py @@ -343,7 +343,7 @@ def binarizer(data): out = np.argmax(data['inferred'], -1) else: out = data['inferred'] - return out == label, data['label'] + return out == label, data['label'] == label return binarizer diff --git a/niftynet/io/image_loader.py b/niftynet/io/image_loader.py index e0f36a50..a51396a6 100755 --- a/niftynet/io/image_loader.py +++ b/niftynet/io/image_loader.py @@ -94,7 +94,7 @@ def load_image_obj(filename, loader=None): tf.logging.debug('Using requested loader: {}'.format(loader)) loader_params = AVAILABLE_LOADERS[loader] return loader_params['func'](filename) - elif loader: + if loader: raise ValueError('Image Loader {} not supported. Supported loaders: {}' .format(loader, list(SUPPORTED_LOADERS.keys()))) @@ -207,6 +207,9 @@ def __init__(self, img, affine): if img.ndim == 3 and img.shape[2] <= 4: # Color Image img = img[:, :, None, None, :] + if img.dtype == np.bool: # bool is not a supported datatype by nibabel + img = img.astype(np.uint8) + nib.Nifti1Image.__init__(self, img, affine) diff --git a/niftynet/io/image_reader.py b/niftynet/io/image_reader.py index 7677a5f3..082defef 100755 --- a/niftynet/io/image_reader.py +++ b/niftynet/io/image_reader.py @@ -3,7 +3,6 @@ from __future__ import absolute_import, division, print_function from copy import deepcopy - import argparse import numpy as np import pandas @@ -21,8 +20,9 @@ DEFAULT_INTERP_ORDER = 1 SUPPORTED_DATA_SPEC = { - 'csv_file', 'path_to_search', - 'filename_contains', 'filename_not_contains', 'filename_removefromid', + + 'csv_file', 'path_to_search', 'csv_data_file', 'filename_removefromid', + 'filename_contains', 'filename_not_contains', 'to_ohe', 'interp_order', 'loader', 'pixdim', 'axcodes', 'spatial_window_size'} @@ -410,7 +410,8 @@ def _filename_to_image_list(file_list, mod_dict, data_param): if not volume_list: tf.logging.fatal( "Empty filename lists, please check the csv " - "files. (removing csv_file keyword if it is in the config file " + "files. (removing csv_file keyword if it is" + " in the config file " "to automatically search folders and generate new csv " "files again)\n\n" "Please note in the matched file names, each subject id are " diff --git a/niftynet/io/image_sets_partitioner.py b/niftynet/io/image_sets_partitioner.py index e5770427..97960828 100755 --- a/niftynet/io/image_sets_partitioner.py +++ b/niftynet/io/image_sets_partitioner.py @@ -110,7 +110,10 @@ def number_of_subjects(self, phase=ALL): if self._partition_ids is None: return 0 selector = self._partition_ids[COLUMN_PHASE] == phase - return self._partition_ids[selector].count()[COLUMN_UNIQ_ID] + selected = self._partition_ids[selector][[COLUMN_UNIQ_ID]] + subset = pandas.merge( + self._file_list, selected, on=COLUMN_UNIQ_ID, sort=True) + return subset.count()[COLUMN_UNIQ_ID] def get_file_list(self, phase=ALL, *section_names): """ @@ -195,6 +198,15 @@ def load_data_sections_by_subject(self): raise ValueError self._file_list = None for section_name in self.data_param: + + if isinstance(self.data_param[section_name], dict): + mod_spec = self.data_param[section_name] + else: + mod_spec = vars(self.data_param[section_name]) + if mod_spec.get('csv_data_file', None): # has csv_data_file + # skip file search + continue + modality_file_list = self.grep_files_by_data_section(section_name) if self._file_list is None: # adding all rows of the first modality diff --git a/niftynet/io/misc_io.py b/niftynet/io/misc_io.py index 8bd528cb..f3ced819 100755 --- a/niftynet/io/misc_io.py +++ b/niftynet/io/misc_io.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- """Utilities functions for file and path management""" - from __future__ import absolute_import, print_function, unicode_literals import errno @@ -13,6 +12,7 @@ import nibabel as nib import numpy as np +import pandas as pd import scipy.ndimage import tensorflow as tf # pylint: disable=no-name-in-module @@ -22,7 +22,7 @@ from niftynet.utilities.niftynet_global_config import NiftyNetGlobalConfig from niftynet.utilities.util_import import require_module -IS_PYTHON2 = False if sys.version_info[0] > 2 else True +IS_PYTHON2 = sys.version_info[0] == 2 warnings.simplefilter("ignore", UserWarning) @@ -30,9 +30,9 @@ CONSOLE_LOG_FORMAT = "\033[1m%(levelname)s:niftynet:\033[0m %(message)s" FILE_LOG_FORMAT = "%(levelname)s:niftynet:%(asctime)s: %(message)s" - # utilities for file headers # + def infer_ndims_from_file(file_path, loader=None): """ Get spatial rank of the image file. @@ -150,21 +150,21 @@ def rectify_header_sform_qform(img_nii): if img_nii.header['sform_code'] > 0: if not flag_sform_problem: return img_nii - elif not flag_qform_problem: + if not flag_qform_problem: # recover by copying the qform over the sform img_nii.set_sform(np.copy(img_nii.get_qform())) return img_nii elif img_nii.header['qform_code'] > 0: if not flag_qform_problem: return img_nii - elif not flag_sform_problem: + if not flag_sform_problem: # recover by copying the sform over the qform img_nii.set_qform(np.copy(img_nii.get_sform())) return img_nii affine = img_nii.affine pixdim = img_nii.header.get_zooms() while len(pixdim) < 3: - pixdim = pixdim + (1.0,) + pixdim = pixdim + (1.0, ) # assuming 3 elements new_affine = create_affine_pixdim(affine, pixdim[:3]) img_nii.set_sform(new_affine) @@ -192,11 +192,64 @@ def compute_orientation(init_axcodes, final_axcodes): ornt_transf = nib.orientations.ornt_transform(ornt_init, ornt_fin) return ornt_transf, ornt_init, ornt_fin except (ValueError, IndexError): - tf.logging.fatal( - 'reorientation transform error: %s, %s', ornt_init, ornt_fin) + tf.logging.fatal('reorientation transform error: %s, %s', ornt_init, + ornt_fin) raise ValueError +def do_resampling_idx(idx_array, init_pixdim, fin_pixdim): + """ + Performs the transformation of indices (for csv sampler) when resampling + is required (change of resolution enforced + :param idx_array: array of indices to modify + :param init_pixdim: initial pixdim + :param fin_pixdim: target pixdim + :return: new_idx transformed array of indices according to resampling + """ + if fin_pixdim is None: + new_idx = idx_array + return new_idx + factor_mult = np.asarray(init_pixdim) / np.asarray(fin_pixdim) + new_idx = idx_array.astype(np.float32) * factor_mult + new_idx = new_idx.astype(np.int32) + return new_idx + + +def do_reorientation_idx(idx_array, init_axcodes, final_axcodes, + init_spatial_size): + """ + Perform the indices change based on the the orientation transformation + :param idx_array: array of indices to transform when reorienting + :param init_axcodes: initial orientation codes + :param final_axcodes: target orientation codes + :param init_spatial_size: initial image spatial size + :return: new_idx the array of transformed indices and orientation transform + """ + new_idx = idx_array + if final_axcodes is None: + ornt_transf, ornt_init, ornt_fin = compute_orientation(init_axcodes, + init_axcodes) + return new_idx, ornt_transf + ornt_transf, ornt_init, ornt_fin = compute_orientation(init_axcodes, + final_axcodes) + # print(ornt_transf, ornt_init, "orientation prior idx transfo", + # idx_array.shape, init_spatial_size) + if np.array_equal(ornt_init, ornt_fin): + return new_idx, ornt_transf + else: + # print(ornt_transf[:, 0]) + # print(idx_array[:, np.asarray(ornt_transf[:,0],dtype=np.int32)]) + new_idx = idx_array[:, np.asarray(ornt_transf[:, 0], dtype=np.int32)] + # print("obtained new first", init_spatial_size) + reorder_axes = np.squeeze(np.asarray(ornt_transf[:, 0], dtype=np.int32)) + fin_spatial_size = [init_spatial_size[k] for k in reorder_axes] + for i in range(ornt_transf.shape[0]): + if ornt_transf[i, 1] < 0: + new_idx[:, i] = fin_spatial_size[i] - np.asarray(new_idx[:, i]) + print("Updated idx for %d" % i) + return new_idx, ornt_transf + + def do_reorientation(data_array, init_axcodes, final_axcodes): """ Performs the reorientation (changing order of axes) @@ -208,6 +261,7 @@ def do_reorientation(data_array, init_axcodes, final_axcodes): """ ornt_transf, ornt_init, ornt_fin = \ compute_orientation(init_axcodes, final_axcodes) + # print(ornt_transf, init_axcodes, final_axcodes) if np.array_equal(ornt_init, ornt_fin): return data_array try: @@ -231,7 +285,7 @@ def do_resampling(data_array, pixdim_init, pixdim_fin, interp_order): :return data_resampled: Array containing the resampled data """ if data_array is None: - return + return None if np.array_equal(pixdim_fin, pixdim_init): return data_array try: @@ -249,14 +303,40 @@ def do_resampling(data_array, pixdim_init, pixdim_fin, interp_order): for time_point in range(0, data_shape[3]): data_mod = [] for mod in range(0, data_shape[4]): - data_new = scipy.ndimage.zoom(data_array[..., time_point, mod], - to_multiply[0:3], - order=interp_order) + data_new = scipy.ndimage.zoom( + data_array[..., time_point, mod], + to_multiply[0:3], + order=interp_order) data_mod.append(data_new[..., np.newaxis, np.newaxis]) data_resampled.append(np.concatenate(data_mod, axis=-1)) return np.concatenate(data_resampled, axis=-2) +def save_csv_array(filefolder, filename, array_to_save): + """ + Save a np array as a csv + :param filefolder: Path to the folder where to save + :param filename: Name of the file to save + :param array_to_save: Array to save + :return: + """ + if array_to_save is None: + return + if not isinstance(array_to_save, pd.DataFrame): + array_to_save = pd.DataFrame(array_to_save) + touch_folder(filefolder) + output_name = os.path.join(filefolder, filename) + try: + if os.path.isfile(output_name): + tf.logging.warning('File %s exists, overwriting the file.', + output_name) + array_to_save.to_csv(output_name) + except OSError: + tf.logging.fatal("writing failed {}".format(output_name)) + raise + tf.logging.info('Saved {}'.format(output_name)) + + def save_data_array(filefolder, filename, array_to_save, @@ -294,7 +374,7 @@ def save_data_array(filefolder, # feature vector, should be saved with shape (1, 1, 1, 1, mod) while array_to_save.ndim < 5: array_to_save = np.expand_dims(array_to_save, axis=0) - elif input_ndim == 2 or input_ndim == 3: + elif input_ndim in (2, 3): # 2D or 3D images should be saved with shape (x, y, z, 1, 1) while array_to_save.ndim < 5: array_to_save = np.expand_dims(array_to_save, axis=-1) @@ -312,13 +392,14 @@ def save_data_array(filefolder, transf, _, _ = compute_orientation(image_axcodes, dst_axcodes) original_shape = tuple( original_shape[k] for k in transf[:, 0].astype(np.int)) - image_pixdim = dst_pixdim * np.divide(original_shape, spatial_shape) - array_to_save = do_resampling( - array_to_save, image_pixdim, dst_pixdim, interp_order) + image_pixdim = dst_pixdim * np.divide(original_shape, + spatial_shape) + array_to_save = do_resampling(array_to_save, image_pixdim, dst_pixdim, + interp_order) if image_axcodes and dst_axcodes: - array_to_save = do_reorientation( - array_to_save, image_axcodes, dst_axcodes) + array_to_save = do_reorientation(array_to_save, image_axcodes, + dst_axcodes) save_volume_5d(array_to_save, filename, filefolder, affine) @@ -345,7 +426,9 @@ def expand_to_5d(img_data): def save_volume_5d(img_data, filename, save_path, affine=np.eye(4)): """ - Save the img_data to nifti image + Save the img_data to nifti image, if the final dimensions of the 5D array + are 1's, save the lower dimensional image to disk by squeezing the trailing + single dimensional spaces away. :param img_data: 5d img to save :param filename: filename under which to save the img_data @@ -355,19 +438,29 @@ def save_volume_5d(img_data, filename, save_path, affine=np.eye(4)): """ if img_data is None: return + # 5D images are not well supported by many image processing tools + # (or are assumed to be time series) + # Squeeze 5d processing space into smaller image spatial size (3d or 2d) + # for improved compatibility with + # external visualization/processing tools like Slicer3D, ITK, + # SimpleITK, etc ... + sqeezed_shape = img_data.shape + while sqeezed_shape[-1] == 1: + sqeezed_shape = sqeezed_shape[0:-1] + img_data.shape = sqeezed_shape touch_folder(save_path) img_nii = nib.Nifti1Image(img_data, affine) # img_nii.set_data_dtype(np.dtype(np.float32)) output_name = os.path.join(save_path, filename) try: if os.path.isfile(output_name): - tf.logging.warning( - 'File %s exists, overwriting the file.', output_name) + tf.logging.warning('File %s exists, overwriting the file.', + output_name) nib.save(img_nii, output_name) except OSError: tf.logging.fatal("writing failed {}".format(output_name)) raise - print('Saved {}'.format(output_name)) + tf.logging.info('Saved {}'.format(output_name)) def split_filename(file_name): @@ -408,11 +501,11 @@ def squeeze_spatial_temporal_dim(tf_tensor): if tf_tensor.shape[4] != 1: if tf_tensor.shape[5] > 1: raise NotImplementedError("time sequences not currently supported") - else: # input shape [batch, x, y, z, t, 1]: swapping 't' and 1 - tf_tensor = tf.transpose(tf_tensor, [0, 1, 2, 3, 5, 4]) + # input shape [batch, x, y, z, t, 1]: swapping 't' and 1 + tf_tensor = tf.transpose(tf_tensor, [0, 1, 2, 3, 5, 4]) axis_to_squeeze = [] for (idx, axis) in enumerate(tf_tensor.shape.as_list()): - if idx == 0 or idx == 5: + if idx in (0, 5): continue if axis == 1: axis_to_squeeze.append(idx) @@ -498,20 +591,18 @@ def resolve_module_dir(module_dir_str, create_new=False): else: tf.logging.fatal( "trying to use '{}' as NiftyNet writing path, " - "however cannot write '{}'".format( - folder_path, init_file)) + "however cannot write '{}'".format(folder_path, init_file)) raise else: with os.fdopen(file_, 'w') as file_object: file_object.write("# Created automatically\n") return folder_path - else: - raise ValueError( - "Could not resolve [{}].\nMake sure it is a valid folder path " - "or a module name.\nIf it is string representing a module, " - "the parent folder of [{}] should be on " - "the system path.\n\nCurrent system path {}.".format( - module_dir_str, module_dir_str, sys.path)) + raise ValueError( + "Could not resolve [{}].\nMake sure it is a valid folder path " + "or a module name.\nIf it is string representing a module, " + "the parent folder of [{}] should be on " + "the system path.\n\nCurrent system path {}.".format( + module_dir_str, module_dir_str, sys.path)) def to_absolute_path(input_path, model_root): @@ -553,7 +644,7 @@ def resolve_file_name(file_name, paths): tf.logging.info('Resolving {} as {}'.format( file_name, path_file_name)) return os.path.abspath(path_file_name) - assert False, 'Could not resolve file name' + raise IOError('Could not resolve file name') except (TypeError, AssertionError, IOError): raise IOError('Could not resolve {}'.format(file_name)) @@ -574,8 +665,8 @@ def resolve_checkpoint(checkpoint_name): if os.path.isfile(checkpoint_name + '.index'): return checkpoint_name home_folder = NiftyNetGlobalConfig().get_niftynet_home_folder() - checkpoint_name = to_absolute_path(input_path=checkpoint_name, - model_root=home_folder) + checkpoint_name = to_absolute_path( + input_path=checkpoint_name, model_root=home_folder) if os.path.isfile(checkpoint_name + '.index'): return checkpoint_name raise ValueError('Invalid checkpoint {}'.format(checkpoint_name)) @@ -597,15 +688,17 @@ def get_latest_subfolder(parent_folder, create_new=False): except OSError: tf.logging.fatal('not a directory {}'.format(parent_folder)) raise OSError - log_sub_dirs = [name for name in log_sub_dirs - if re.findall('^[0-9]+$', name)] + log_sub_dirs = [ + name for name in log_sub_dirs if re.findall('^[0-9]+$', name) + ] if log_sub_dirs and create_new: latest_id = max([int(name) for name in log_sub_dirs]) log_sub_dir = '{}'.format(latest_id + 1) elif log_sub_dirs and not create_new: - latest_valid_id = max( - [int(name) for name in log_sub_dirs - if os.path.isdir(os.path.join(parent_folder, name))]) + latest_valid_id = max([ + int(name) for name in log_sub_dirs + if os.path.isdir(os.path.join(parent_folder, name)) + ]) log_sub_dir = '{}'.format(latest_valid_id) else: log_sub_dir = '{}'.format(0) @@ -618,8 +711,10 @@ def _image3_animated_gif(tag, ims): from PIL.GifImagePlugin import Image as GIF # x=numpy.random.randint(0,256,[10,10,10],numpy.uint8) - ims = [np.asarray((ims[i, :, :]).astype(np.uint8)) - for i in range(ims.shape[0])] + ims = [ + np.asarray((ims[i, :, :]).astype(np.uint8)) + for i in range(ims.shape[0]) + ] ims = [GIF.fromarray(im) for im in ims] img_str = b'' for b_data in PIL.GifImagePlugin.getheader(ims[0])[0]: @@ -634,16 +729,15 @@ def _image3_animated_gif(tag, ims): img_str = str(img_str) summary_image_str = summary_pb2.Summary.Image( height=10, width=10, colorspace=1, encoded_image_string=img_str) - image_summary = summary_pb2.Summary.Value( - tag=tag, image=summary_image_str) + image_summary = summary_pb2.Summary.Value(tag=tag, image=summary_image_str) return [summary_pb2.Summary(value=[image_summary]).SerializeToString()] def image3(name, tensor, max_out=3, - collections=(tf.GraphKeys.SUMMARIES,), - animation_axes=(1,), + collections=(tf.GraphKeys.SUMMARIES, ), + animation_axes=(1, ), image_axes=(2, 3), other_indices=None): """ @@ -681,15 +775,17 @@ def image3(name, if i not in axis_order] original_shape = tensor.shape.as_list() new_shape = [ - original_shape[0], -1, - original_shape[axis_order[-2]], - original_shape[axis_order[-1]]] + original_shape[0], -1, original_shape[axis_order[-2]], + original_shape[axis_order[-1]] + ] transposed_tensor = tf.transpose(tensor, axis_order_all) transposed_tensor = tf.reshape(transposed_tensor, new_shape) # split images with tf.device('/cpu:0'): for it_i in range(min(max_out, transposed_tensor.shape.as_list()[0])): - inp = [name + suffix.format(it_i), transposed_tensor[it_i, :, :, :]] + inp = [ + name + suffix.format(it_i), transposed_tensor[it_i, :, :, :] + ] summary_op = tf.py_func(_image3_animated_gif, inp, tf.string) for c in collections: tf.add_to_collection(c, summary_op) @@ -699,7 +795,7 @@ def image3(name, def image3_sagittal(name, tensor, max_outputs=3, - collections=(tf.GraphKeys.SUMMARIES,)): + collections=(tf.GraphKeys.SUMMARIES, )): """ Create 2D image summary in the sagittal view. @@ -715,7 +811,7 @@ def image3_sagittal(name, def image3_coronal(name, tensor, max_outputs=3, - collections=(tf.GraphKeys.SUMMARIES,)): + collections=(tf.GraphKeys.SUMMARIES, )): """ Create 2D image summary in the coronal view. @@ -731,7 +827,7 @@ def image3_coronal(name, def image3_axial(name, tensor, max_outputs=3, - collections=(tf.GraphKeys.SUMMARIES,)): + collections=(tf.GraphKeys.SUMMARIES, )): """ Create 2D image summary in the axial view. @@ -753,9 +849,18 @@ def set_logger(file_name=None): :return: """ # pylint: disable=no-name-in-module - from tensorflow.python.platform.tf_logging import _get_logger + # This is done so if the user had TF 1.12.1 or a new version the code + # does not brake. First part of the try is renaming the TF 1.12.1 to + # fit the TF 1.13.1>= naming scheme, while the second is just a normal + # import for TF 1.13.1>= + try: + # pylint: disable=no-name-in-module + from tensorflow.python.platform.tf_logging import \ + _get_logger as get_logger + except ImportError: + from tensorflow.python.platform.tf_logging import get_logger - logger = _get_logger() + logger = get_logger() tf.logging.set_verbosity(tf.logging.INFO) logger.handlers = [] @@ -780,9 +885,18 @@ def close_logger(): :return: """ # pylint: disable=no-name-in-module - from tensorflow.python.platform.tf_logging import _get_logger + # This is done so if the user had TF 1.12.1 or a new version the code + # does not brake. First part of the try is renaming the TF 1.12.1 to + # fit the TF 1.13.1>= naming scheme, while the second is just a normal + # import for TF 1.13.1>= + try: + # pylint: disable=no-name-in-module + from tensorflow.python.platform.tf_logging import \ + _get_logger as get_logger + except ImportError: + from tensorflow.python.platform.tf_logging import get_logger - logger = _get_logger() + logger = get_logger() for handler in reversed(logger.handlers): try: handler.flush() diff --git a/niftynet/layer/additive_upsample.py b/niftynet/layer/additive_upsample.py index d6b34d46..a302ae59 100755 --- a/niftynet/layer/additive_upsample.py +++ b/niftynet/layer/additive_upsample.py @@ -98,7 +98,7 @@ def layer_op(self, input_tensor, is_training=True): n_output_chns = check_divisible_channels(input_tensor, self.n_splits) # deconvolution path deconv_output = Deconv(n_output_chns=n_output_chns, - with_bias=False, with_bn=True, + with_bias=False, feature_normalization='batch', **self.deconv_param)(input_tensor, is_training) # additive upsampling path diff --git a/niftynet/layer/channel_sparse_convolution.py b/niftynet/layer/channel_sparse_convolution.py index ff7be7d7..95536a21 100755 --- a/niftynet/layer/channel_sparse_convolution.py +++ b/niftynet/layer/channel_sparse_convolution.py @@ -325,7 +325,7 @@ def __init__(self, dilation=1, padding='SAME', with_bias=False, - with_bn=True, + feature_normalization='batch', acti_func=None, w_initializer=None, w_regularizer=None, @@ -336,9 +336,9 @@ def __init__(self, name="conv"): self.acti_func = acti_func - self.with_bn = with_bn + self.feature_normalization = feature_normalization self.layer_name = '{}'.format(name) - if self.with_bn: + if self.feature_normalization == 'batch': self.layer_name += '_bn' if self.acti_func is not None: self.layer_name += '_{}'.format(self.acti_func) @@ -359,9 +359,9 @@ def __init__(self, self.initializers = { 'w': w_initializer if w_initializer else - niftynet.layer.convolution.default_w_initializer(), + niftynet.layer.convolution.default_w_initializer(), 'b': b_initializer if b_initializer else - niftynet.layer.convolution.default_b_initializer()} + niftynet.layer.convolution.default_b_initializer()} self.regularizers = {'w': w_regularizer, 'b': b_regularizer} @@ -395,10 +395,9 @@ def layer_op(self, output_tensor.set_shape( output_tensor.shape.as_list()[:-1] + [n_output_ch]) - if self.with_bn: + if self.feature_normalization == 'batch': if is_training is None: - raise ValueError('is_training argument should be ' - 'True or False unless with_bn is False') + raise ValueError('For batch norm, you must set the `is_training` argument.') bn_layer = ChannelSparseBNLayer( self.n_output_chns, regularizer=self.regularizers['w'], diff --git a/niftynet/layer/convolution.py b/niftynet/layer/convolution.py index 5280be94..f97a09ea 100755 --- a/niftynet/layer/convolution.py +++ b/niftynet/layer/convolution.py @@ -1,17 +1,18 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, print_function +import math import numpy as np import tensorflow as tf from niftynet.layer import layer_util from niftynet.layer.activation import ActiLayer from niftynet.layer.base_layer import TrainableLayer -from niftynet.layer.bn import BNLayer +from niftynet.layer.bn import BNLayer, InstanceNormLayer from niftynet.layer.gn import GNLayer from niftynet.utilities.util_common import look_up_operations -SUPPORTED_PADDING = set(['SAME', 'VALID']) +SUPPORTED_PADDING = set(['SAME', 'VALID', 'REFLECT', 'SYMMETRIC', 'CONSTANT']) def default_w_initializer(): @@ -47,7 +48,13 @@ def __init__(self, w_regularizer=None, b_initializer=None, b_regularizer=None, + padding_constant=0, name='conv'): + """ + :param padding_constant: a constant applied in padded convolution + (see also tf.pad) + """ + super(ConvLayer, self).__init__(name=name) self.padding = look_up_operations(padding.upper(), SUPPORTED_PADDING) @@ -56,6 +63,7 @@ def __init__(self, self.stride = stride self.dilation = dilation self.with_bias = with_bias + self.padding_constant = padding_constant self.initializers = { 'w': w_initializer if w_initializer else default_w_initializer(), @@ -82,12 +90,22 @@ def layer_op(self, input_tensor): 'w', shape=w_full_size, initializer=self.initializers['w'], regularizer=self.regularizers['w']) - output_tensor = tf.nn.convolution(input=input_tensor, - filter=conv_kernel, - strides=full_stride, - dilation_rate=full_dilation, - padding=self.padding, - name='conv') + if self.padding in ('VALID', 'SAME'): + output_tensor = tf.nn.convolution(input=input_tensor, + filter=conv_kernel, + strides=full_stride, + dilation_rate=full_dilation, + padding=self.padding, + name='conv') + else: + output_tensor = _extended_convolution( + input_tensor, + conv_kernel, + full_stride, + full_dilation, + self.padding, + constant=self.padding_constant) + if not self.with_bias: return output_tensor @@ -106,11 +124,11 @@ class ConvolutionalLayer(TrainableLayer): """ This class defines a composite layer with optional components:: - convolution -> batch_norm -> activation -> dropout + convolution -> feature_normalization (default batch norm) -> activation -> dropout The b_initializer and b_regularizer are applied to the ConvLayer The w_initializer and w_regularizer are applied to the ConvLayer, - the batch normalisation layer, and the activation layer (for 'prelu') + the feature normalization layer, and the activation layer (for 'prelu') """ def __init__(self, @@ -120,7 +138,7 @@ def __init__(self, dilation=1, padding='SAME', with_bias=False, - with_bn=True, + feature_normalization='batch', group_size=-1, acti_func=None, preactivation=False, @@ -130,19 +148,28 @@ def __init__(self, b_regularizer=None, moving_decay=0.9, eps=1e-5, + padding_constant=0, name="conv"): + """ + :param padding_constant: constant applied with CONSTANT padding + """ self.acti_func = acti_func - self.with_bn = with_bn + self.feature_normalization = feature_normalization self.group_size = group_size self.preactivation = preactivation self.layer_name = '{}'.format(name) - if self.with_bn and group_size > 0: - raise ValueError('only choose either batchnorm or groupnorm') - if self.with_bn: + if self.feature_normalization != 'group' and group_size > 0: + raise ValueError('You cannot have a group_size > 0 if not using group norm') + elif self.feature_normalization == 'group' and group_size <= 0: + raise ValueError('You cannot have a group_size <= 0 if using group norm') + + if self.feature_normalization == 'batch': self.layer_name += '_bn' - if self.group_size > 0: + elif self.feature_normalization == 'group': self.layer_name += '_gn' + elif self.feature_normalization == 'instance': + self.layer_name += '_in' if self.acti_func is not None: self.layer_name += '_{}'.format(self.acti_func) super(ConvolutionalLayer, self).__init__(name=self.layer_name) @@ -154,6 +181,7 @@ def __init__(self, self.dilation = dilation self.padding = padding self.with_bias = with_bias + self.padding_constant = padding_constant # for BNLayer self.moving_decay = moving_decay @@ -176,18 +204,21 @@ def layer_op(self, input_tensor, is_training=None, keep_prob=None): w_regularizer=self.regularizers['w'], b_initializer=self.initializers['b'], b_regularizer=self.regularizers['b'], + padding_constant=self.padding_constant, name='conv_') - if self.with_bn: + if self.feature_normalization == 'batch': if is_training is None: raise ValueError('is_training argument should be ' - 'True or False unless with_bn is False') + 'True or False unless feature_normalization is False') bn_layer = BNLayer( regularizer=self.regularizers['w'], moving_decay=self.moving_decay, eps=self.eps, name='bn_') - if self.group_size > 0: + elif self.feature_normalization == 'instance': + in_layer = InstanceNormLayer(eps=self.eps, name='in_') + elif self.feature_normalization == 'group': gn_layer = GNLayer( regularizer=self.regularizers['w'], group_size=self.group_size, @@ -203,9 +234,11 @@ def layer_op(self, input_tensor, is_training=None, keep_prob=None): dropout_layer = ActiLayer(func='dropout', name='dropout_') def activation(output_tensor): - if self.with_bn: + if self.feature_normalization == 'batch': output_tensor = bn_layer(output_tensor, is_training) - if self.group_size > 0: + elif self.feature_normalization == 'instance': + output_tensor = in_layer(output_tensor) + elif self.feature_normalization == 'group': output_tensor = gn_layer(output_tensor) if self.acti_func is not None: output_tensor = acti_layer(output_tensor) @@ -220,3 +253,106 @@ def activation(output_tensor): output_tensor = activation(conv_layer(input_tensor)) return output_tensor + + +def _compute_pad_size(input_dim_size, output_dim_size, kernel_dim_size, + stride, dilation): + """ + Computes the size of the pad using the formula given in TF's conv_ops.cc. + :return: the one-sided pad size + """ + + return ((output_dim_size - 1)*stride + (kernel_dim_size - 1)*dilation + 2 + - input_dim_size)//2 + + +def _extended_convolution(input_tensor, + kernel, + strides, + dilations, + padding, + constant=0, + name='extended_convolution'): + """ + A simple wrapper for tf.nn.convolution that first expands the input tensor + by sampling at discrete locations in the original tensor then invokes + the original convolution operation on the expanded tensor, and finally + extracts a suitable output tensor from the output of the convolution of + the expanded tensor. + :param input_tensor: original convolution input tensor + :param kernel: convolution kernel + :param strides: strided convolution strides (one per spatial dimension) + :param dilations: dilated convolution dilation factors + (one per spatial dimension) + :param padding: a string specifying the type of padding to apply + :param constant: a padding constant (only read in the case of constant + padding) + :param name: a name for the operation + :return: a convolution result of the same size as the input tensor + """ + + input_shape = input_tensor.shape.as_list() + batch_size = input_shape[0] + input_shape = input_shape[1:-1] + kernel_shape = kernel.shape.as_list() + nof_output_features = kernel_shape[-1] + kernel_shape = kernel_shape[:-2] + + if any(i is None or i < 0 or k is None or k < 0 + for i, k in zip(input_shape, kernel_shape)): + raise ValueError('The dimensions of the input tensor and the filter' + ' must be known in advance for this operation to ' + 'work.') + + output_shape = [int(math.ceil(i/s)) for i, s in zip(input_shape, strides)] + output_shape = [batch_size] + output_shape + [nof_output_features] + + dimpads = [0] + for i, k, s, d in zip(input_shape, kernel_shape, strides, dilations): + pad = _compute_pad_size(i, int(math.ceil(i/s)), k, s, d) + dimpads.append(pad) + dimpads += [0] + + # Cannot pad by more than 1 dimension size => repeatedly pad + if padding in ('REFLECT', 'SYMMETRIC'): + padded_input = input_tensor + offset = int(padding == 'REFLECT') + + while min(o - i - 2*p for o, i, p in zip( + padded_input.shape.as_list()[1:-1], + input_shape, + dimpads[1:-1])) < 0: + effective_pad = [(0, 0)] + padded_shape = padded_input.shape.as_list()[1:-1] + for i in range(len(input_shape)): + epad = min((input_shape[i] + 2*dimpads[1+i] - padded_shape[i])//2, + padded_shape[i] - offset) + epad = max(epad, 0) + effective_pad.append((epad, epad)) + effective_pad += [(0, 0)] + + assert max(e for e, _ in effective_pad) > 0 + + padded_input = tf.pad(padded_input, + effective_pad, + mode=padding) + else: + padded_input = tf.pad(input_tensor, + [(d, d) for d in dimpads], + mode=padding, + constant_values=constant) + + conv_output = tf.nn.convolution(input=padded_input, + filter=kernel, + strides=strides, + dilation_rate=dilations, + padding='SAME', + name='conv_' + name) + + conv_output_shape = conv_output.shape.as_list() + out_pad = [0] + out_pad += [(o - i)//2 for i, o in zip(output_shape[1:-1], conv_output_shape[1:-1])] + out_pad += [0] + + return tf.slice(conv_output, out_pad, output_shape) if max(out_pad) > 0 \ + else conv_output diff --git a/niftynet/layer/crf.py b/niftynet/layer/crf.py index 8495ec7d..88144180 100755 --- a/niftynet/layer/crf.py +++ b/niftynet/layer/crf.py @@ -281,18 +281,44 @@ def _simple_hash(key): hash_vector = tf.constant(hash_vector, dtype=tf.int64) return tf.reduce_sum(tf.to_int64(key) * hash_vector, 1) - hash_table = tf.contrib.lookup.MutableDenseHashTable( - tf.int64, tf.int64, - default_value=tf.constant([-1] * n_ch, dtype=tf.int64), - empty_key=-2, - initial_num_buckets=8, - checkpoint=False) - index_table = tf.contrib.lookup.MutableDenseHashTable( - tf.int64, tf.int64, - default_value=0, - empty_key=-1, - initial_num_buckets=8, - checkpoint=False) + # This is done so if the user had TF 1.12.1 or a new version the code + # does not brake. First part of the try is for TF 1.12.1 where the + # deleted_key keyword was missing, while the second is just a normal + # usage for TF 1.13.1>= + try: + hash_table = tf.contrib.lookup.MutableDenseHashTable( + tf.int64, tf.int64, + default_value=tf.constant([-1] * 100, dtype=tf.int64), + empty_key=-2, + initial_num_buckets=8, + checkpoint=False + ) + except TypeError: + hash_table = tf.contrib.lookup.MutableDenseHashTable( + tf.int64, tf.int64, + default_value=tf.constant([-1] * n_ch, dtype=tf.int64), + empty_key=-3, + deleted_key=-2, + initial_num_buckets=8, + checkpoint=False + ) + try: + index_table = tf.contrib.lookup.MutableDenseHashTable( + tf.int64, tf.int64, + default_value=0, + empty_key=-1, + initial_num_buckets=8, + checkpoint=False + ) + except TypeError: + index_table = tf.contrib.lookup.MutableDenseHashTable( + tf.int64, tf.int64, + default_value=0, + empty_key=-2, + deleted_key=-1, + initial_num_buckets=8, + checkpoint=False + ) # canonical simplex (p.4 in [Adams et al 2010]) canonical = \ @@ -327,8 +353,8 @@ def _simple_hash(key): # linearised [batch, spatial_dim] indices # where in the splat variable each simplex vertex is batch_index = tf.range(batch_size, dtype=tf.int32) - batch_index = tf.expand_dims(batch_index, 0) - batch_index = tf.tile(batch_index, [n_voxels, 1]) + batch_index = tf.expand_dims(batch_index, 1) + batch_index = tf.tile(batch_index, [1, n_voxels]) batch_index = tf.to_int64(tf.reshape(batch_index, [-1])) indices = [None] * n_ch_1 diff --git a/niftynet/layer/deconvolution.py b/niftynet/layer/deconvolution.py index 0e0b10b1..066cffb6 100755 --- a/niftynet/layer/deconvolution.py +++ b/niftynet/layer/deconvolution.py @@ -7,7 +7,7 @@ from niftynet.layer import layer_util from niftynet.layer.activation import ActiLayer from niftynet.layer.base_layer import TrainableLayer -from niftynet.layer.bn import BNLayer +from niftynet.layer.bn import BNLayer, InstanceNormLayer from niftynet.layer.gn import GNLayer from niftynet.utilities.util_common import look_up_operations @@ -168,7 +168,7 @@ def __init__(self, stride=1, padding='SAME', with_bias=False, - with_bn=True, + feature_normalization='batch', group_size=-1, acti_func=None, w_initializer=None, @@ -180,15 +180,17 @@ def __init__(self, name="deconv"): self.acti_func = acti_func - self.with_bn = with_bn + self.feature_normalization = feature_normalization self.group_size = group_size self.layer_name = '{}'.format(name) - if self.with_bn and self.group_size > 0: - raise ValueError('only choose either batchnorm or groupnorm') - if self.with_bn: - self.layer_name += '_bn' - if self.group_size > 0: - self.layer_name += '_gn' + if self.feature_normalization != 'group' and group_size > 0: + raise ValueError('You cannot have a group_size > 0 if not using group norm') + elif self.feature_normalization == 'group' and group_size <= 0: + raise ValueError('You cannot have a group_size <= 0 if using group norm') + + if self.feature_normalization is not None: + # appending, for example, '_bn' to the name + self.layer_name += '_' + self.feature_normalization[0] + 'n' if self.acti_func is not None: self.layer_name += '_{}'.format(self.acti_func) super(DeconvolutionalLayer, self).__init__(name=self.layer_name) @@ -224,17 +226,20 @@ def layer_op(self, input_tensor, is_training=None, keep_prob=None): name='deconv_') output_tensor = deconv_layer(input_tensor) - if self.with_bn: + if self.feature_normalization == 'batch': if is_training is None: raise ValueError('is_training argument should be ' - 'True or False unless with_bn is False') + 'True or False unless feature_normalization is False') bn_layer = BNLayer( regularizer=self.regularizers['w'], moving_decay=self.moving_decay, eps=self.eps, name='bn_') output_tensor = bn_layer(output_tensor, is_training) - if self.group_size > 0: + elif self.feature_normalization == 'instance': + in_layer = InstanceNormLayer(eps=self.eps, name='in_') + output_tensor = in_layer(output_tensor) + elif self.feature_normalization == 'group': gn_layer = GNLayer( regularizer=self.regularizers['w'], group_size=self.group_size, @@ -252,4 +257,5 @@ def layer_op(self, input_tensor, is_training=None, keep_prob=None): if keep_prob is not None: dropout_layer = ActiLayer(func='dropout', name='dropout_') output_tensor = dropout_layer(output_tensor, keep_prob=keep_prob) + return output_tensor diff --git a/niftynet/layer/downsample_res_block.py b/niftynet/layer/downsample_res_block.py index a903c5bc..791e5982 100755 --- a/niftynet/layer/downsample_res_block.py +++ b/niftynet/layer/downsample_res_block.py @@ -41,7 +41,7 @@ def layer_op(self, inputs, is_training=True): conv_0 = Conv(n_output_chns=self.n_output_chns, kernel_size=self.kernel_size, acti_func=self.acti_func, - with_bias=False, with_bn=True, + with_bias=False, feature_normalization='batch', **self.conv_param)(inputs, is_training) conv_res = ResUnit(n_output_chns=self.n_output_chns, kernel_size=self.kernel_size, diff --git a/niftynet/layer/fully_connected.py b/niftynet/layer/fully_connected.py index f689ec77..430ad382 100755 --- a/niftynet/layer/fully_connected.py +++ b/niftynet/layer/fully_connected.py @@ -8,7 +8,8 @@ # from . import layer_util from niftynet.layer.activation import ActiLayer from niftynet.layer.base_layer import TrainableLayer -from niftynet.layer.bn import BNLayer +from niftynet.layer.bn import BNLayer, InstanceNormLayer +from niftynet.layer.gn import GNLayer def default_w_initializer(): @@ -97,7 +98,8 @@ class FullyConnectedLayer(TrainableLayer): def __init__(self, n_output_chns, with_bias=True, - with_bn=True, + feature_normalization='batch', + group_size=-1, acti_func=None, w_initializer=None, w_regularizer=None, @@ -108,10 +110,18 @@ def __init__(self, name="fc"): self.acti_func = acti_func - self.with_bn = with_bn + self.feature_normalization = feature_normalization + self.group_size = group_size self.layer_name = '{}'.format(name) - if self.with_bn: - self.layer_name += '_bn' + + if self.feature_normalization != 'group' and group_size > 0: + raise ValueError('You cannot have a group_size > 0 if not using group norm') + elif self.feature_normalization == 'group' and group_size <= 0: + raise ValueError('You cannot have a group_size <= 0 if using group norm') + + if self.feature_normalization is not None: + # to append batch_norm as _bn and likewise for other norms + self.layer_name += '_' + self.feature_normalization[0] + 'n' if self.acti_func is not None: self.layer_name += '_{}'.format(self.acti_func) super(FullyConnectedLayer, self).__init__(name=self.layer_name) @@ -140,13 +150,26 @@ def layer_op(self, input_tensor, is_training=None, keep_prob=None): name='fc_') output_tensor = fc_layer(input_tensor) - if self.with_bn: + if self.feature_normalization == 'batch': + if is_training is None: + raise ValueError('is_training argument should be ' + 'True or False unless feature_normalization is False') bn_layer = BNLayer( regularizer=self.regularizers['w'], moving_decay=self.moving_decay, eps=self.eps, name='bn_') output_tensor = bn_layer(output_tensor, is_training) + elif self.feature_normalization == 'instance': + in_layer = InstanceNormLayer(eps=self.eps, name='in_') + output_tensor = in_layer(output_tensor) + elif self.feature_normalization == 'group': + gn_layer = GNLayer( + regularizer=self.regularizers['w'], + group_size=self.group_size, + eps=self.eps, + name='gn_') + output_tensor = gn_layer(output_tensor) if self.acti_func is not None: acti_layer = ActiLayer( diff --git a/niftynet/layer/layer_util.py b/niftynet/layer/layer_util.py index d487426c..d40b9e89 100755 --- a/niftynet/layer/layer_util.py +++ b/niftynet/layer/layer_util.py @@ -19,9 +19,10 @@ def check_spatial_dims(input_tensor, criteria): all_dims_satisfied = np.all([criteria(x) for x in spatial_dims]) if not all_dims_satisfied: import inspect - raise ValueError("input tensor's spatial dimensionality not" + raise ValueError("input tensor's spatial dimensionality" " not compatible, please tune " - "the input window sizes:\n{}".format( + "the input window sizes. " + "(e.g. lambda x : x % 8 == 0 checks whether each dimension is divisible by 8)\n{}".format( inspect.getsource(criteria))) return all_dims_satisfied diff --git a/niftynet/layer/loss_classification.py b/niftynet/layer/loss_classification.py index 852917c7..8a20f241 100755 --- a/niftynet/layer/loss_classification.py +++ b/niftynet/layer/loss_classification.py @@ -62,7 +62,7 @@ def layer_op(self, for pred in prediction: if self._loss_func_params: data_loss.append(self._data_loss_func( - pred, ground_truth, + pred, ground_truth, **self._loss_func_params)) else: data_loss.append(self._data_loss_func( @@ -79,5 +79,5 @@ def cross_entropy(prediction, :return: the loss """ ground_truth = tf.to_int64(ground_truth) - loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=prediction, labels=ground_truth) - return loss \ No newline at end of file + loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=prediction, labels=ground_truth) + return loss diff --git a/niftynet/layer/loss_classification_multi.py b/niftynet/layer/loss_classification_multi.py new file mode 100755 index 00000000..0a950198 --- /dev/null +++ b/niftynet/layer/loss_classification_multi.py @@ -0,0 +1,264 @@ +# -*- coding: utf-8 -*- +""" +Loss functions for multi-class classification +""" +from __future__ import absolute_import, print_function, division + +import numpy as np +import tensorflow as tf + +from niftynet.engine.application_factory import LossClassificationMultiFactory +from niftynet.layer.base_layer import Layer +#from niftynet.layer.loss_segmentation import labels_to_one_hot + + +class LossFunction(Layer): + def __init__(self, + n_class, + n_rater, + loss_type='CrossEntropy', + loss_func_params=None, + name='loss_function'): + + super(LossFunction, self).__init__(name=name) + self._num_classes = n_class + self._num_raters = n_rater + if loss_func_params is not None: + self._loss_func_params = loss_func_params + else: + self._loss_func_params = {} + self._data_loss_func = None + self.make_callable_loss_func(loss_type) + + def make_callable_loss_func(self, type_str): + self._data_loss_func = LossClassificationMultiFactory.create(type_str) + + def layer_op(self, + pred_ave=None, + pred_multi=None, + ground_truth=None, + weight_batch=None, + var_scope=None, ): + ''' + Compute the losses in the case of a multirater setting + :param pred_ave: average of the predictions over the different raters + :param pred_multi: prediction for each individual rater + :param ground_truth: ground truth classification for each individual + rater + :param weight_batch: + :param var_scope: + :return: + ''' + + + with tf.device('/cpu:0'): + if ground_truth is not None: + ground_truth = tf.reshape(ground_truth, [-1, + self._num_raters, + self._num_classes]) + if pred_ave is not None: + if not isinstance(pred_ave, (list, tuple)): + pred_ave = [pred_ave] + if self._num_classes > 0 and pred_ave is not None: + # reshape the prediction to [n_voxels , num_classes] + pred_ave = [tf.reshape(pred, [-1, self._num_classes]) + for pred in pred_ave] + if pred_multi is not None and not isinstance(pred_multi, (list, \ + tuple)): + pred_multi = [pred_multi] + if self._num_classes > 0 and pred_multi is not None: + + pred_multi = [tf.reshape(pred, [-1, + self._num_raters, + self._num_classes]) + for pred in pred_multi] + + + data_loss = [] + if ground_truth is not None: + if pred_multi is not None: + if pred_ave is not None: + for pred, pred_mul in zip(pred_ave, pred_multi): + if self._loss_func_params: + data_loss.append( + self._data_loss_func(ground_truth, + pred, pred_mul, + **self._loss_func_params)) + else: + data_loss.append(self._data_loss_func( + ground_truth, pred, pred_mul)) + else: + for pred_mul in pred_multi: + if self._loss_func_params: + data_loss.append( + self._data_loss_func(ground_truth, + pred_mul, + **self._loss_func_params)) + else: + data_loss.append(self._data_loss_func( + ground_truth, pred_mul)) + else: + for pred in pred_ave: + + if self._loss_func_params: + data_loss.append(self._data_loss_func( + pred, ground_truth, + **self._loss_func_params)) + else: + data_loss.append(self._data_loss_func( + pred, ground_truth)) + elif pred_multi is not None: + for pred, pred_mul in zip(pred_ave, pred_multi): + if self._loss_func_params: + data_loss.append(self._data_loss_func( + pred, pred_mul, + **self._loss_func_params)) + else: + data_loss.append(self._data_loss_func( + pred, pred_mul)) + + if weight_batch is not None: + return tf.reduce_mean(weight_batch/tf.reduce_sum( + weight_batch) * data_loss[0]) + else: + return tf.reduce_mean(data_loss) + +# +def labels_to_one_hot(ground_truth, num_classes=1): + """ + Converts ground truth labels to one-hot, sparse tensors. + Used extensively in segmentation losses. + + :param ground_truth: ground truth categorical labels (rank `N`) + :param num_classes: A scalar defining the depth of the one hot dimension + (see `depth` of `tf.one_hot`) + :return: one-hot sparse tf tensor + (rank `N+1`; new axis appended at the end) and the output shape + """ + # read input/output shapes + if isinstance(num_classes, tf.Tensor): + num_classes_tf = tf.to_int32(num_classes) + else: + num_classes_tf = tf.constant(num_classes, tf.int32) + input_shape = tf.shape(ground_truth) + output_shape = tf.concat( + [input_shape, tf.reshape(num_classes_tf, (1,))], 0) + + if num_classes == 1: + # need a sparse representation? + print('no need') + return tf.reshape(ground_truth, output_shape), output_shape + + # squeeze the spatial shape + + ground_truth = tf.reshape(ground_truth, (-1,)) + # shape of squeezed output + dense_shape = tf.stack([tf.shape(ground_truth)[0], + num_classes_tf], 0) + dense_shape = tf.Print(tf.cast(dense_shape, tf.int64), [dense_shape, + output_shape], message='check_shape_lohe') + # create a rank-2 sparse tensor + ground_truth = tf.to_int64(ground_truth) + ids = tf.range(tf.to_int64(tf.shape(ground_truth)[0]), dtype=tf.int64) + ids = tf.stack([ids, ground_truth], axis=1) + one_hot = tf.SparseTensor( + indices=ids, + values=tf.ones_like(ground_truth, dtype=tf.float32), + dense_shape=tf.to_int64(dense_shape)) + + # resume the spatial dims + one_hot = tf.sparse_reshape(one_hot, output_shape) + return one_hot, output_shape + + +def loss_confusion_matrix(ground_truth, pred_multi, num_classes=2, nrater=6): + ''' + Creates a loss over the two multi rater confusion matrices between the rater + :param ground_truth: multi rater classification + :param pred_multi: multi rater prediction (1 pred per class for each + rater and each observation - A softmax is performed during the loss + calculation + :param nrater: number of raters + :return: integration over the absolute differences between the confusion + matrices divided by number of raters + ''' + + one_hot_gt, output_shape = labels_to_one_hot(ground_truth, num_classes) + dense_one_hot = tf.reshape(tf.sparse_tensor_to_dense(one_hot_gt), + output_shape) + dense_one_hot = tf.reshape(dense_one_hot, tf.shape(pred_multi)) + nclasses=tf.shape(pred_multi)[-1] + nn_pred = tf.nn.softmax(pred_multi,-1) + error_fin = tf.zeros([nclasses, nclasses]) + error_fin = tf.Print(tf.cast(error_fin, tf.float32), [nn_pred, tf.shape( + pred_multi)], message='error') + nn_pred = tf.Print(tf.cast(nn_pred, tf.float32), [tf.shape( + dense_one_hot), nclasses, tf.shape(ground_truth), tf.shape(nn_pred)], + message='check_conf') + for i in range(0, nrater): + for j in range(i+1, nrater): + + confusion_pred = tf.matmul(tf.transpose(nn_pred[:, i, :]), + nn_pred[:, j, :]) + confusion_gt = tf.matmul(tf.transpose(dense_one_hot[:, i, :]), + dense_one_hot[:, j, :]) + error = tf.divide(np.abs(confusion_gt - confusion_pred), tf.cast( + tf.shape(ground_truth)[0], tf.float32)) + error_fin += error + error_fin = tf.Print(tf.cast(error,tf.float32), [tf.reduce_sum( + error_fin), tf.reduce_max(error_fin)], message='build_error') + return tf.reduce_sum(error_fin)/tf.cast(nrater, tf.float32) + + +def variability(pred_multi, num_classes=2, nrater=2): + one_hot_gt, output_shape = labels_to_one_hot(tf.cast(pred_multi, tf.int64), + num_classes) + dense_one_hot = tf.sparse_tensor_to_dense(one_hot_gt) + freq = tf.divide(tf.reduce_sum(dense_one_hot, 1), tf.cast(tf.shape( + pred_multi)[1],tf.float32)) + variability = tf.reduce_sum(tf.square(freq), -1) + return 1 - variability + + +def loss_variability(ground_truth, pred_multi, weight_map=None): + one_hot_gt, output_shape = labels_to_one_hot(tf.cast(ground_truth, + tf.int64), + tf.shape(pred_multi)[-1]) + dense_gt = tf.sparse_tensor_to_dense(one_hot_gt) + pred_hard = tf.argmax(pred_multi, -1) + + one_hot_pred, _ = labels_to_one_hot(tf.cast(pred_hard, tf.int64), + tf.shape(pred_multi)[-1]) + dense_pred = tf.sparse_tensor_to_dense(one_hot_pred) + freq_pred = tf.divide(tf.reduce_sum(dense_pred, 1), + tf.cast(tf.shape(pred_multi)[1],tf.float32)) + variability_pred = tf.reduce_sum(tf.square(freq_pred), -1) + freq_gt = tf.divide(tf.reduce_sum(dense_gt, 1), + tf.cast(tf.shape(pred_multi)[1],tf.float32)) + variability_gt = tf.reduce_sum(tf.square(freq_gt), -1) + + diff_square = tf.square(variability_gt-variability_pred) + if weight_map is not None: + diff_square = weight_map * diff_square + loss = tf.sqrt(tf.reduce_mean(diff_square)) + return loss + + +def rmse_consistency(pred_ave, + pred_multi, weight_map=None): + pred_multi = tf.nn.softmax(pred_multi, -1) + pred_multi_ave = tf.reduce_mean(pred_multi, axis=1) + pred_multi_ave = tf.Print(tf.cast(pred_multi_ave, tf.float32), [pred_ave[0], + pred_multi_ave[0], + tf.shape( + pred_ave), tf.shape(pred_multi_ave), + tf.reduce_max(pred_ave-pred_multi_ave)], + message='rmse_test') + diff_square = tf.square(pred_ave-pred_multi_ave) + if weight_map is not None: + diff_square = tf.multiply(weight_map, diff_square) / tf.reduce_sum( + weight_map) + + return tf.sqrt(tf.reduce_mean(diff_square)) + + diff --git a/niftynet/layer/loss_regression.py b/niftynet/layer/loss_regression.py index 27dc6a77..aded0188 100755 --- a/niftynet/layer/loss_regression.py +++ b/niftynet/layer/loss_regression.py @@ -22,6 +22,9 @@ def __init__(self, self._data_loss_func = LossRegressionFactory.create(loss_type) self._loss_func_params = \ loss_func_params if loss_func_params is not None else {} + self._reshape = True + if loss_type == 'Cosine': + self._reshape = False def layer_op(self, prediction, @@ -46,9 +49,16 @@ def layer_op(self, with tf.device('/cpu:0'): batch_size = ground_truth.shape[0].value - ground_truth = tf.reshape(ground_truth, [batch_size, -1]) - if weight_map is not None: - weight_map = tf.reshape(weight_map, [batch_size, -1]) + dir_size = 1 + if self._reshape: + ground_truth = tf.reshape(ground_truth, [batch_size, -1]) + if weight_map is not None: + weight_map = tf.reshape(weight_map, [batch_size, -1]) + else: + + dir_size = ground_truth.shape[-1].value + ground_truth = tf.reshape(ground_truth, [batch_size, -1, + dir_size]) if not isinstance(prediction, (list, tuple)): prediction = [prediction] @@ -63,7 +73,14 @@ def _batch_i_loss(*args): weight_map_b = None else: pred_b, ground_truth_b, weight_map_b = args[0] - pred_b = tf.reshape(pred_b, [-1]) + pred_b = tf.reshape(pred_b, tf.shape(ground_truth_b)) + # pred_b = tf.reshape(pred_b, [-1]) + # pred_b = tf.Print(tf.cast(pred_b, tf.float32), + # [tf.shape( + # pred_b), tf.shape( + # ground_truth_b)], + # message='pred_b_shape') + loss_params = { 'prediction': pred_b, @@ -178,3 +195,94 @@ def huber_loss(prediction, ground_truth, delta=1.0, weight_map=None): sum_weights = tf.to_float(tf.size(absolute_residuals)) sum_loss = tf.reduce_sum(voxelwise_loss) return tf.truediv(sum_loss, sum_weights) + + +def smooth_l1_loss(prediction, ground_truth, weight_map=None, value_thresh=0.5): + """ + Similarly to the Huber loss, the residuals are squared below a threshold + value. In addition they are square above the inverse of this threshold + :param prediction: the current prediction of the ground truth. + :param ground_truth: the measurement you are approximating with regression. + :param weight_map: + :return: mean of the l1 loss across all voxels. + """ + # Definition of thresholds + if value_thresh>1: + value_thresh_max = value_thresh + value_thresh = 1.0/value_thresh + else: + value_thresh_max = 1.0 / value_thresh + + value_correction = value_thresh ** 3 - value_thresh + + value_correction_max = value_thresh_max - value_thresh_max ** 2 + + prediction = tf.cast(prediction, dtype=tf.float32) + + ground_truth = tf.cast(ground_truth, dtype=tf.float32) + + absolute_residuals = tf.cast(tf.abs(tf.subtract(prediction, + ground_truth)), + dtype=tf.float32) + + absolute_residuals = tf.where(absolute_residuals < value_thresh, + value_thresh * + tf.square(absolute_residuals), + absolute_residuals + value_correction) + + absolute_residuals = tf.where(tf.greater(absolute_residuals,value_thresh_max), + tf.square( + absolute_residuals) + value_correction_max, absolute_residuals) + if weight_map is not None: + + absolute_residuals = tf.multiply(absolute_residuals, weight_map) + sum_residuals = tf.reduce_sum(absolute_residuals) + + sum_weights = tf.reduce_sum(weight_map) + + else: + sum_residuals = tf.reduce_sum(absolute_residuals) + sum_weights = tf.size(absolute_residuals) + return tf.truediv(tf.cast(sum_residuals, dtype=tf.float32), + tf.cast(sum_weights, dtype=tf.float32)) + + +def cosine_loss(prediction, ground_truth, weight_map=None, to_complete=True): + ''' + Cosine loss between predicted and ground_truth vectors. The predicted and + targeted vectors should be unit vectors + :param prediction: + :param ground_truth: + :param weight_map: + :param to_complete: if the unit vector is to be completed + :return: + ''' + if to_complete: + prediction_complete = tf.reshape(tf.sqrt(1 - tf.minimum(tf.reduce_sum( + tf.square( + prediction),-1),1)), [tf.shape(prediction)[0],1]) + ground_truth_complete = tf.reshape(tf.sqrt(1 - tf.minimum(tf.reduce_sum( + tf.square( + ground_truth),-1),1)),[tf.shape(prediction)[0],1]) + + pred_vect = tf.concat([prediction, prediction_complete], -1) + + gt_vect = tf.concat([ground_truth, ground_truth_complete], -1) + else: + pred_vect = prediction + gt_vect = ground_truth + + if weight_map is None: + weight_map = tf.ones([tf.shape(prediction)[0]]) + else: + weight_map = tf.reshape(weight_map, [tf.shape(prediction)[0]]) + + pred_vect = pred_vect / tf.maximum(tf.norm( + pred_vect,ord='euclidean',axis=-1, keep_dims=True), 0.00001) + gt_vect = gt_vect /tf.maximum(tf.norm( + gt_vect,ord='euclidean',axis=-1, keep_dims=True), 0.00001) + loss_init = 1 -tf.reduce_sum(gt_vect * pred_vect, -1) + weighted_loss = loss_init * weight_map + loss = tf.reduce_sum(weighted_loss) / tf.reduce_sum(weight_map) + + return loss diff --git a/niftynet/layer/loss_segmentation.py b/niftynet/layer/loss_segmentation.py index 40b2b17a..009e7be9 100755 --- a/niftynet/layer/loss_segmentation.py +++ b/niftynet/layer/loss_segmentation.py @@ -184,6 +184,98 @@ def labels_to_one_hot(ground_truth, num_classes=1): return one_hot +def undecided_loss(prediction, ground_truth, weight_map=None): + """ + + :param prediction: + :param ground_truth: + :param weight_map: + :return: + """ + ratio_undecided = 1.0/tf.cast(tf.shape(prediction)[-1], tf.float32) + res_undecided = tf.reciprocal(tf.reduce_mean(tf.abs(prediction - + ratio_undecided), -1) + 0.0001) + if weight_map is None: + return tf.reduce_mean(res_undecided) + else: + res_undecided = tf.Print(tf.cast(res_undecided, tf.float32), [tf.shape( + res_undecided), tf.shape(weight_map), tf.shape( + res_undecided*weight_map)], message='test_printshape_und') + return tf.reduce_sum(res_undecided * weight_map / + tf.reduce_sum(weight_map)) + + +def volume_enforcement(prediction, ground_truth, weight_map=None, eps=0.001, + hard=False): + """ + Computing a volume enforcement loss to ensure that the obtained volumes are + close and avoid empty results when something is expected + :param prediction: + :param ground_truth: labels + :param weight_map: potential weight map to apply + :param eps: epsilon to use as regulariser + :return: + """ + + prediction = tf.cast(prediction, tf.float32) + if len(ground_truth.shape) == len(prediction.shape): + ground_truth = ground_truth[..., -1] + one_hot = labels_to_one_hot(ground_truth, tf.shape(prediction)[-1]) + + gt_red = tf.sparse_reduce_sum(one_hot, 0) + pred_red = tf.reduce_sum(prediction, 0) + if hard: + pred_red = tf.sparse_reduce_sum(labels_to_one_hot(tf.argmax( + prediction,-1),tf.shape(prediction)[-1]), 0) + + if weight_map is not None: + n_classes = prediction.shape[1].value + weight_map_nclasses = tf.tile(tf.expand_dims(tf.reshape(weight_map, + [-1]), 1), + [1, n_classes]) + gt_red = tf.sparse_reduce_sum(weight_map_nclasses * one_hot, + reduction_axes=[0]) + pred_red = tf.reduce_sum(weight_map_nclasses * prediction, 0) + + return tf.reduce_mean(tf.sqrt(tf.square((gt_red+eps)/(pred_red+eps) - + (pred_red+eps)/(gt_red+eps)))) + + +def volume_enforcement_fin(prediction, ground_truth, weight_map=None, + eps=0.001): + """ + Computing a volume enforcement loss to ensure that the obtained volumes are + close and avoid empty results when something is expected + :param prediction: + :param ground_truth: + :param weight_map: + :param eps: + :return: + """ + + prediction = tf.cast(prediction, tf.float32) + if len(ground_truth.shape) == len(prediction.shape): + ground_truth = ground_truth[..., -1] + one_hot = labels_to_one_hot(ground_truth, tf.shape(prediction)[-1]) + gt_red = tf.sparse_reduce_sum(one_hot, 0) + pred_red = tf.sparse_reduce_sum(labels_to_one_hot(tf.argmax( + prediction,-1),tf.shape(prediction)[-1]), 0) + + if weight_map is not None: + n_classes = prediction.shape[1].value + weight_map_nclasses = tf.tile(tf.expand_dims(tf.reshape(weight_map, + [-1]), 1), + [1, n_classes]) + gt_red = tf.sparse_reduce_sum(weight_map_nclasses * one_hot, + reduction_axes=[0]) + pred_red = tf.sparse_reduce_sum(labels_to_one_hot(tf.argmax( + prediction, -1), tf.shape(prediction)[-1]) * weight_map_nclasses, 0) + + return tf.reduce_mean(tf.sqrt(tf.square((gt_red+eps)/(pred_red+eps) + - (pred_red+eps)/(gt_red+eps)))) + + + def generalised_dice_loss(prediction, ground_truth, weight_map=None, diff --git a/niftynet/layer/pad.py b/niftynet/layer/pad.py index d6036a09..4e80667c 100755 --- a/niftynet/layer/pad.py +++ b/niftynet/layer/pad.py @@ -17,71 +17,107 @@ class PadLayer(Layer, Invertible): therefore assumes the input has at least three spatial dims. """ - def __init__(self, image_name, border, name='pad', mode='minimum'): + def __init__(self, image_name, border, name='pad', mode='minimum', pad_to=(0,)): + """ + :param image_name: the name of the relevant key in the data dictionary + :param border: the dimensions of the desired border around the image. + :param name: name of the PadLayer in the tensorflow graph. + :param mode: how to choose the padding values for the np.pad operation. + :param pad_to: this determines a desired size of the padded image (useful + for inconsistent input sizes or for making inference efficient). If + it == (0, ) (DEFAULT), it will use the constant padding mode + determined by 'border' + """ super(PadLayer, self).__init__(name=name) try: spatial_border = tuple(map(lambda x: (x,), border)) except (ValueError, TypeError): - tf.logging.fatal("unknown padding param. {}".format(border)) + tf.logging.fatal("Unknown padding param. {}".format(border)) raise self.border = spatial_border self.image_name = set(image_name) self.mode = mode + self.pad_to = pad_to + self.full_border = None def layer_op(self, input_image, mask=None): if not isinstance(input_image, dict): - full_border = match_ndim(self.border, input_image.ndim) - input_image = np.pad(input_image, full_border, mode=self.mode) + self._set_full_border(input_image) + input_image = np.pad(input_image, self.full_border, mode=self.mode) return input_image, mask for name, image in input_image.items(): + self._set_full_border(image) if name not in self.image_name: - tf.logging.warning( - 'could not pad, dict name %s not in %s', - name, self.image_name) + tf.logging.warning('could not pad, dict name %s not in %s', name, self.image_name) continue - full_border = match_ndim(self.border, image.ndim) - input_image[name] = np.pad(image, full_border, mode=self.mode) + input_image[name] = np.pad(image, self.full_border, mode=self.mode) return input_image, mask def inverse_op(self, input_image, mask=None): if not isinstance(input_image, dict): - full_border = match_ndim(self.border, input_image.ndim) - outputs = _crop_numpy_array(input_image, full_border) + # you can run the cropping op without running the padding op, but only if you + # pad with a constant amount (not pad_to) + if self.full_border is None and self.pad_to == (0,): + self._set_full_border(input_image) + + outputs = self._crop_numpy_array(input_image, self.full_border) return outputs, mask for name, image in input_image.items(): + # you can run the cropping op without running the padding op, but only if you + # pad with a constant amount (not pad_to) + if self.full_border is None and self.pad_to == (0,): + self._set_full_border(image) + if name not in self.image_name: continue - full_border = match_ndim(self.border, image.ndim) - input_image[name] = _crop_numpy_array(image, full_border) + input_image[name] = self._crop_numpy_array(image, self.full_border) return input_image, mask + @staticmethod + def _crop_numpy_array(image, border): + try: + assert image.ndim >= 3, "input image must have at least 3 spatial dims" + if np.shape(border)[-1] < 2: + # same amount cropped from each side of array + border = np.hstack([np.array(border), np.array(border)]) + + x_ = border[0][0] if image.shape[0] / 2 > border[0][0] > 0 else 0 + y_ = border[1][0] if image.shape[1] / 2 > border[1][0] > 0 else 0 + z_ = border[2][0] if image.shape[2] / 2 > border[2][0] > 0 else 0 + _x = -border[0][1] if image.shape[0] / 2 > border[0][1] > 0 else image.shape[0] + _y = -border[1][1] if image.shape[1] / 2 > border[1][1] > 0 else image.shape[1] + _z = -border[2][1] if image.shape[2] / 2 > border[2][1] > 0 else image.shape[2] + return image[x_:_x, y_:_y, z_:_z, ...] + except (IndexError, AssertionError): + tf.logging.fatal( + "Unable to invert the padding. Input: {}, pad param. {}".format(image.shape, border)) + raise + + def _set_full_border(self, image): + """ + To calculate and set the border that is used to a) pad the image and b) invert the padding + :param image: the input image + """ + if self.pad_to == (0,): + full_border = self.border + while len(full_border) < image.ndim: + # here, we extend the tuple with zeros as all padding is symmetric (for each + # dimension, we pad 'in front' and 'behind' with the same number of values). + full_border = full_border + ((0,),) + else: + necessary_padding = np.array(self.pad_to) - image.shape[:len(self.pad_to)] + full_border = tuple() + + # do not pad if the dimension is bigger than self.pad_to + necessary_padding[necessary_padding < 0] = 0 + for pad in necessary_padding: + full_border += ((pad // 2, (pad + 1) // 2),) + + # no padding on the channel dimensions + while len(full_border) < image.ndim: + # in pad_to mode, we explicitly determine the padding at the front and back. + full_border += ((0, 0),) -def _crop_numpy_array(image, border): - try: - assert image.ndim >= 3, \ - "input image must have at least 3 spatial dims" - x_ = border[0][0] if image.shape[0] / 2 > border[0][0] > 0 else 0 - y_ = border[1][0] if image.shape[1] / 2 > border[1][0] > 0 else 0 - z_ = border[2][0] if image.shape[2] / 2 > border[2][0] > 0 else 0 - _x = -border[0][0] if image.shape[0] / 2 > border[0][0] > 0 \ - else image.shape[0] - _y = -border[1][0] if image.shape[1] / 2 > border[1][0] > 0 \ - else image.shape[1] - _z = -border[2][0] if image.shape[2] / 2 > border[2][0] > 0 \ - else image.shape[2] - return image[x_:_x, y_:_y, z_:_z, ...] - except (IndexError, AssertionError): - tf.logging.fatal( - "unable to inverse the padding " - "input: {}, pad param. {}".format( - image.shape, border)) - raise - - -def match_ndim(border, image_ndim): - full_border = border - while len(full_border) < image_ndim: - full_border = full_border + ((0,),) - return full_border[:image_ndim] + self.full_border = full_border diff --git a/niftynet/layer/rand_spatial_scaling.py b/niftynet/layer/rand_spatial_scaling.py index 8767feba..93ae9aff 100755 --- a/niftynet/layer/rand_spatial_scaling.py +++ b/niftynet/layer/rand_spatial_scaling.py @@ -21,19 +21,26 @@ def __init__(self, min_percentage=-10.0, max_percentage=10.0, antialiasing=True, + isotropic=False, name='random_spatial_scaling'): super(RandomSpatialScalingLayer, self).__init__(name=name) assert min_percentage <= max_percentage self._min_percentage = max(min_percentage, -99.9) self._max_percentage = max_percentage self.antialiasing = antialiasing + self.isotropic = isotropic self._rand_zoom = None def randomise(self, spatial_rank=3): spatial_rank = int(np.floor(spatial_rank)) - rand_zoom = np.random.uniform(low=self._min_percentage, - high=self._max_percentage, - size=(spatial_rank,)) + if self.isotropic: + one_rand_zoom = np.random.uniform(low=self._min_percentage, + high=self._max_percentage) + rand_zoom = np.repeat(one_rand_zoom, spatial_rank) + else: + rand_zoom = np.random.uniform(low=self._min_percentage, + high=self._max_percentage, + size=(spatial_rank,)) self._rand_zoom = (rand_zoom + 100.0) / 100.0 def _get_sigma(self, zoom): diff --git a/niftynet/layer/resampler.py b/niftynet/layer/resampler.py index 53fac609..683c87f9 100755 --- a/niftynet/layer/resampler.py +++ b/niftynet/layer/resampler.py @@ -23,7 +23,8 @@ class ResamplerLayer(Layer): def __init__(self, interpolation="LINEAR", boundary="REPLICATE", - name="resampler"): + name="resampler", + implementation="Fast"): super(ResamplerLayer, self).__init__(name=name) self.boundary = boundary.upper() self.boundary_func = look_up_operations( @@ -39,6 +40,36 @@ def __init__(self, tf.logging.warning('Zero padding is not supported for IDW mode') # raise NotImplementedError + self.FastResamplerLayer = None # + if implementation.lower() in ['niftyreg', 'fast']: + # check if niftyreg_resampling_layer is installed + try: + from niftyreg_image_resampling import NiftyregImageResamplingLayer + import niftyreg_image_resampling as resampler_module + except ImportError: + tf.logging.warning(''' + niftyreg_image_resampling is not installed; falling back onto + niftynet.layer.resampler.ResamplerLayer. To allow fast resampling, + please see installation instructions in + niftynet/contrib/niftyreg_image_resampling/README.md + ''') + return + + # Passthrough of supported boundary types for resampling + SUPPORTED_BOUNDARY_FAST = resampler_module.SUPPORTED_BOUNDARY + + # Passthrough of supported interpolation types for NiftyReg resampling + SUPPORTED_INTERPOLATION_FAST = resampler_module.SUPPORTED_INTERPOLATION + # check compatibility of the resampling options with niftyreg_image_resampling + try: + boundary_fast = look_up_operations(self.boundary, SUPPORTED_BOUNDARY_FAST) + interp_fast = look_up_operations(self.interpolation, SUPPORTED_INTERPOLATION_FAST) + self.FastResamplerLayer = NiftyregImageResamplingLayer(interp_fast, boundary_fast) + tf.logging.info('''NiftyReg image resampling is used.''') + except ValueError as e: + tf.logging.warning(e) + tf.logging.warning('''Falling back onto niftynet.layer.resampler.ResamplerLayer.''') + def layer_op(self, inputs, sample_coords): """ This layer resamples 2D or 3D data given the coordinates. @@ -119,6 +150,10 @@ def layer_op(self, inputs, sample_coords): if sample_coords.dtype not in SUPPORTED_INPUT_DTYPE: sample_coords = tf.to_float(sample_coords) + # use fast resampling layer if available + if self.FastResamplerLayer is not None: + return self.FastResamplerLayer.layer_op(inputs, sample_coords) + # otherwise compatibility resampling layer is used if self.interpolation == 'LINEAR': return self._resample_linear(inputs, sample_coords) if self.interpolation == 'NEAREST': @@ -464,7 +499,7 @@ def _binary_neighbour_ids(spatial_rank): for i in range(2 ** spatial_rank)] -try: # Some tf versions have this defined already +try: # Some tf versions have this defined already @tf.RegisterGradient('FloorMod') def _floormod_grad(op, grad): return [None, None] diff --git a/niftynet/layer/residual_unit.py b/niftynet/layer/residual_unit.py index dc0b59a0..81a31d3f 100755 --- a/niftynet/layer/residual_unit.py +++ b/niftynet/layer/residual_unit.py @@ -81,9 +81,9 @@ def layer_op(self, inputs, is_training=True): acti_0 = Acti(func=self.acti_func) acti_1 = Acti(func=self.acti_func) # convolutions - conv_0 = Conv(acti_func=None, with_bias=False, with_bn=False, + conv_0 = Conv(acti_func=None, with_bias=False, feature_normalization=None, **self.conv_param) - conv_1 = Conv(acti_func=None, with_bias=False, with_bn=False, + conv_1 = Conv(acti_func=None, with_bias=False, feature_normalization=None, **self.conv_param) if self.type_string == 'original': diff --git a/niftynet/layer/rgb_histogram_equilisation.py b/niftynet/layer/rgb_histogram_equilisation.py new file mode 100644 index 00000000..0edcfdda --- /dev/null +++ b/niftynet/layer/rgb_histogram_equilisation.py @@ -0,0 +1,64 @@ +from __future__ import absolute_import, print_function + +import numpy as np +from niftynet.layer.base_layer import Layer +from niftynet.utilities.util_import import require_module + +class RGBHistogramEquilisationLayer(Layer): + """ + RGB histogram equilisation. Unlike the multi-modality general + histogram normalisation this is done conventionally, on a + per-image basis. This layer requires OpenCV. + """ + + def __init__(self, + image_name, + name='rgb_normaliser'): + super(RGBHistogramEquilisationLayer, self).__init__(name=name) + + self.image_name = image_name + + def _normalise_image(self, image): + """ + Normalises a 2D RGB image, if necessary performs any type casting + and reshaping operations. + :param image: a 2D RGB image, possibly given as a 5D tensor + :return: the normalised image in its original shape + """ + + if isinstance(image.dtype, np.floating) and image.dtype != np.float32: + image = image.astype(np.float32) + elif isinstance(image.dtype, np.uint): + image = image.astype(np.float32)/255 + + orig_shape = list(image.shape) + if len(orig_shape) == 5 and (orig_shape[2] > 1 or orig_shape[3] > 1): + raise ValueError('Can only process 2D images.') + + if len(image.shape) != 3: + image = image.reshape(orig_shape[:2] + [orig_shape[-1]]) + + image = image[...,::-1] + + cv2 = require_module('cv2') + yuv_image = cv2.cvtColor(image, cv2.COLOR_BGR2YUV) + intensity = (255*yuv_image[...,0]).astype(np.uint8) + yuv_image[...,0] = cv2.equalizeHist(intensity).astype(np.float32)/255 + + return cv2.cvtColor(yuv_image, cv2.COLOR_YUV2BGR)[...,::-1]\ + .reshape(orig_shape) + + def layer_op(self, image, mask=None): + """ + :param image: a 3-channel tensor assumed to be an image in floating-point + RGB format (each channel in [0, 1]) + :return: the equilised image + """ + + if isinstance(image, dict): + image[self.image_name] = self._normalise_image( + image[self.image_name]) + + return image, mask + else: + return self._normalise_image(image), mask diff --git a/niftynet/layer/spatial_transformer.py b/niftynet/layer/spatial_transformer.py index 60c21a48..158f4a02 100755 --- a/niftynet/layer/spatial_transformer.py +++ b/niftynet/layer/spatial_transformer.py @@ -78,7 +78,6 @@ def layer_op(self,field): for d in [0, 1, 2]] resampled=tf.stack(resampled_list,5) permuted_shape=[batch_size]+[f-3 for f in self._coeff_shape]+self._knot_spacing+[spatial_rank] - print(permuted_shape) permuted=tf.transpose(tf.reshape(resampled,permuted_shape),[0,1,4,2,5,3,6,7]) valid_size=[(f-3)*k for f,k in zip(self._coeff_shape,self._knot_spacing)] reshaped=tf.reshape(permuted,[batch_size]+valid_size+[spatial_rank]) diff --git a/niftynet/layer/squeeze_excitation.py b/niftynet/layer/squeeze_excitation.py index ba1929cc..7685112c 100755 --- a/niftynet/layer/squeeze_excitation.py +++ b/niftynet/layer/squeeze_excitation.py @@ -49,12 +49,12 @@ def layer_op(self, input_tensor): num_channels_reduced = num_channels / reduction_ratio fc1 = FullyConnectedLayer(num_channels_reduced, with_bias=False, - with_bn=False, + feature_normalization=None, acti_func='relu', name='se_fc_1') fc2 = FullyConnectedLayer(num_channels, with_bias=False, - with_bn=False, + feature_normalization=None, acti_func='sigmoid', name='se_fc_2') @@ -86,7 +86,7 @@ def layer_op(self, input_tensor): # channel squeeze conv = ConvolutionalLayer(n_output_chns=1, kernel_size=1, - with_bn=False, + feature_normalization=None, acti_func='sigmoid', name="se_conv") diff --git a/niftynet/layer/subpixel.py b/niftynet/layer/subpixel.py new file mode 100644 index 00000000..b0381e98 --- /dev/null +++ b/niftynet/layer/subpixel.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function + +import tensorflow as tf + +from niftynet.layer import layer_util +from niftynet.layer.base_layer import TrainableLayer +from niftynet.layer.convolution import ConvolutionalLayer + + +class SubPixelLayer(TrainableLayer): + """ + Implementation of Shi et al.'s sub-pixel CNN single-image + upsampling method. + + Based on Shi et al.: "Real-Time Single Image and Video + Super-Resolution Using an Efficient Sub-Pixel Convolutional Neural + Network" + """ + + def __init__(self, + upsample_factor=3, + layer_configurations=((5, 64), + (3, 32), + (3, -1)), + acti_func='tanh', + feature_normalization=None, + group_size=-1, + with_bias=True, + padding='REFLECT', + w_initializer=None, + w_regularizer=None, + b_initializer=None, + b_regularizer=None, + name='subpixel_cnn'): + """ + :param upsample_factor: zoom-factor/image magnification factor + :param layer_configurations: N pairs consisting of a kernel size and + a feature-map size, where N is the number of layers in the net. The last + layer must have a feature-map size of -1. + :param padding: padding applied in convolutional layers + :param with_bias: incorporate bias parameters in convolutional layers + :param feature_normalization: the type of feature normalization (e.g. + batch, instance or group norm. Default None. + :param group_size: size of the groups if groupnorm is chosen. + :param acti_func: activation function applied to first N - 1 layers + """ + + super(SubPixelLayer, self).__init__(name=name) + + if layer_configurations[-1][1] != -1: + raise ValueError('The size of the last feature map must be -1') + if upsample_factor <= 0: + raise ValueError('The upsampling factor must be strictly positive.') + + self.upsample_factor = upsample_factor + self.layer_configurations = layer_configurations + self.acti_func = acti_func + + self.conv_layer_params = {'with_bias': with_bias, + 'feature_normalization': feature_normalization, + 'group_size': group_size, + 'padding': padding, + 'w_initializer': w_initializer, + 'b_initializer': b_initializer, + 'w_regularizer': w_regularizer, + 'b_regularizer': b_regularizer} + + def layer_op(self, lr_images, is_training=True, keep_prob=1.0): + input_shape = lr_images.shape.as_list() + batch_size = input_shape[0] + input_shape = input_shape[1:] + n_of_dims = len(input_shape) - 1 + + if batch_size is None: + raise ValueError('The batch size must be known and fixed.') + if any(i is None or i <= 0 for i in input_shape): + raise ValueError('The image shape must be known in advance.') + + n_of_channels = input_shape[-1] + + features = lr_images + for i, (ksize, n_of_features) in enumerate(self.layer_configurations): + name = 'fmap_{}'.format(i) + + if n_of_features > 0: + conv = ConvolutionalLayer(n_of_features, + kernel_size=ksize, + acti_func=self.acti_func, + name=name, + **self.conv_layer_params) + else: + n_of_features = n_of_channels * self.upsample_factor ** n_of_dims + conv = ConvolutionalLayer(n_of_features, + kernel_size=ksize, + acti_func=None, + name=name, + **self.conv_layer_params) + + features = conv(features, is_training=is_training, + keep_prob=keep_prob) + + # Setting the number of output features to the known value + # obtained from the input shape results in a ValueError as + # of TF 1.12 + output_shape = ([batch_size] + + [self.upsample_factor * i for i in input_shape[:-1]] + + [None]) + + return tf.contrib.periodic_resample.periodic_resample(features, + output_shape, + name='shuffle') diff --git a/niftynet/layer/upsample_res_block.py b/niftynet/layer/upsample_res_block.py index 3b6d183a..5448d6dd 100755 --- a/niftynet/layer/upsample_res_block.py +++ b/niftynet/layer/upsample_res_block.py @@ -53,7 +53,7 @@ def layer_op(self, inputs, forwarding=None, is_training=True): kernel_size=self.kernel_size, stride=self.upsample_stride, acti_func=self.acti_func, - with_bias=False, with_bn=True, + with_bias=False, feature_normalization='batch', **self.conv_param)(inputs, is_training) if forwarding is None: diff --git a/niftynet/network/README.md b/niftynet/network/README.md index 33bddaf6..ee3d8899 100644 --- a/niftynet/network/README.md +++ b/niftynet/network/README.md @@ -14,7 +14,19 @@ reimplemented from their original presentation with their default parameters. _See also: [NiftyNet model zoo](https://github.com/NifTK/NiftyNetModelZoo)._ -## UNet + +## UNet 2D +Reimplementation of + +Ronneberger, O., Fischer, P., and Brox, T. (2015). +[U-Net: Convolutional Networks for Buomedical Image +Segmentation](https://arxiv.org/pdf/1505.04597.pdf) +In MICCAI 2015 +##### Constraints +* input should be 2D + + +## UNet 3D Reimplementation of Çiçek, Ö., Abdulkadir, A., Lienkamp, S. S., Brox, T., and Ronneberger, O. @@ -26,6 +38,7 @@ In MICCAI 2016 * Label size should be more than 88 * border is 44 + ## VNet Reimplementation of @@ -34,7 +47,20 @@ neural networks for volumetric medical image segmentation](http://campar.in.tum.de/pub/milletari2016Vnet/milletari2016Vnet.pdf), In 3DV 2016 ##### Constraints -* Image size should be divisible by 8 +* Input size should be divisible by 8 +* Input should be either 2D or 3D + + +## DenseVNet +Reimplementation of + +Gibson, E., Giganti, F., Hu, Y., Bonmati, E., Bandula, S., Gurusamy, K., Davidson, B., +Pereira, S.P., Clarkson, M.J. & Barratt, D.C. (2018). [Automatic Multi-organ Segmentation +on Abdominal CT with Dense V-networks](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC6076994/), +in TMI 2018 +##### Constraints +* Input size should be divisible by 2*dilation_rates + ## ScaleNet Implementation of @@ -43,6 +69,7 @@ Fidon, L., Li, W., Garcia-Peraza-Herrera, L.C., Ekanayake, J., Kitchen, N., Ourselin, S., Vercauteren, T. (2017). [Scalable convolutional networks for brain tumour segmentation](https://arxiv.org/abs/1706.08124). In MICCAI 2017 ##### Constraints +* Image size should be divisible by 8 * More than one modality should be used @@ -56,6 +83,10 @@ task](https://link.springer.com/chapter/10.1007/978-3-319-59050-9_28), In IPMI 2017 ##### Constraints * Image size should be divisible by 8 +##### Versions +* default +* large (additional 3x3x3 convolution) +* small (initial stride-2 convolution) ## DeepMedic @@ -72,18 +103,122 @@ MedIA 36, 61-78 * Image size should be divisible by d_factor Example of appropriate configuration for training: - image spatial window size = 57, label spatial window size = 9, d_ factor = 3 and for inference: - image spatial window size = 105, label spatial window size = 57, d_ factor = 3 ## HolisticNet Implementation of -Fidon, L. et. al. (2017) Generalised Wasserstein Dice Score for Imbalanced -Multi-class Segmentation using Holistic Convolutional Networks. MICCAI 2017 +Fidon, L. et. al. (2017) [Generalised Wasserstein Dice Score for Imbalanced +Multi-class Segmentation using Holistic Convolutional Networks], +(https://arxiv.org/abs/1707.00478), MICCAI 2017 (BrainLes) +##### Constraints +* Image size should be divisible by 8 + + +## InterventionalAffineNet (INetAffine) +Implementation of the affine registration network presented in: + +Hu, Y., Modat, M., Gibson, E., Ghavami, N., Bonmati, E., Moore, C.M., +Emberton, M., Noble, J.A., Barratt, D.C. & Vercauteren, T. (2017) +[Label-driven weakly-supervised learning for multimodal deformable image +registration](https://arxiv.org/abs/1711.01666). In ISBI 2018 + +Hu, Y., Modat, M., Gibson, E., Li, W., Ghavami, N., Bonmati, E., Wang, G., +Bandula, S., Moore, C.M., Emberton, Ourselin, S., M., Noble, J.A., +Barratt, D.C. & Vercauteren, T. (2018) [Weakly-Supervised Convolutional +Neural Networks for Multimodal Image Registration] +(https://arxiv.org/abs/1807.03361). In MedIA 2018 +##### Constraints +* Only 2D or 3D input images supported + + +## InterventionalDenseNet (INetDense) +Implementation of the dense registration network presented in: + +Hu, Y., Modat, M., Gibson, E., Ghavami, N., Bonmati, E., Moore, C.M., +Emberton, M., Noble, J.A., Barratt, D.C. & Vercauteren, T. (2017) +[Label-driven weakly-supervised learning for multimodal deformable image +registration](https://arxiv.org/abs/1711.01666). In ISBI 2018 + +Hu, Y., Modat, M., Gibson, E., Li, W., Ghavami, N., Bonmati, E., Wang, G., +Bandula, S., Moore, C.M., Emberton, Ourselin, S., M., Noble, J.A., +Barratt, D.C. & Vercauteren, T. (2018) [Weakly-Supervised Convolutional +Neural Networks for Multimodal Image Registration] +(https://arxiv.org/abs/1807.03361). In MedIA 2018 +##### Constraints +- input spatial rank should be either 2 or 3 (2D or 3D images only) +* fixed image size should be divisible by 16 + + +## InterventionalHybrid (INetHybridPreWarp) +Implementation of the hybrid affine-dense registration network presented in: + +Hu, Y., Modat, M., Gibson, E., Ghavami, N., Bonmati, E., Moore, C.M., +Emberton, M., Noble, J.A., Barratt, D.C. & Vercauteren, T. (2017) +[Label-driven weakly-supervised learning for multimodal deformable image +registration](https://arxiv.org/abs/1711.01666). In ISBI 2018 + +Hu, Y., Modat, M., Gibson, E., Li, W., Ghavami, N., Bonmati, E., Wang, G., +Bandula, S., Moore, C.M., Emberton, Ourselin, S., M., Noble, J.A., +Barratt, D.C. & Vercauteren, T. (2018) [Weakly-Supervised Convolutional Neural Networks +for Multimodal Image Registration](https://arxiv.org/abs/1807.03361). In MedIA 2018 + +##### Constraints +- input spatial rank should be either 2 or 3 (2D or 3D images only) +* fixed image size should be divisible by 16 + + +## ResNet +Reimplementation of + +He, K., Zhang, X., Ren, S., Sun, J. (2016) [Identity Mappings in Deep +Residual Networks](https://arxiv.org/abs/1603.05027). In ECCV 2016 + + +## Squeeze-and-Excitation Net (SE_ResNet) +3D reimplementation of + +Hu, J., Shen, L., Albanie, S., Sun, G. & Wu, E. (2018) +[Squeeze-and-Excitation Networks](arXiv:1709.01507v2), +CVPR 2018 + + +## GenericGAN (simple_gan) +Demo for basic implementation of a generative adversarial network + +See for instance: +Goodfellow, I.J., Pouget-Abadie, J., Mirza, M., Xu, B., Warde-Farley, D., +Ozair, S., Courville, A. & Bengio, Y. (2014)[Generative Adversarial +Nets](http://papers.nips.cc/paper/5423-generative-adversarial-nets.pdf) +In NIPS 2014 + + +## SimulatorGAN +Reimplementation of + +Hu, Y., Gibson, E., Lee, L., Xie, W., Barratt, D.C., Vercauteren, T., Noble, A. (2017), +[Freehand Ultrasound Image Simulation with Spatially-Conditioned +Generative Adversarial Networks](https://arxiv.org/abs/1707.05392), +In: MICCAI RAMBO 2017 + + +## ToyNet +Basic two-layer convolutional neural network for simple testing. + + +## VAE +Implementation of a variational autoencoder network based on + +Kingma, D.P. & Welling, M. (2014)[Auto-Encoding Variational +Bayes](https://arxiv.org/abs/1312.6114) + + + + + diff --git a/niftynet/network/deepmedic.py b/niftynet/network/deepmedic.py index 560a6eea..bd3da2d9 100755 --- a/niftynet/network/deepmedic.py +++ b/niftynet/network/deepmedic.py @@ -12,9 +12,31 @@ class DeepMedic(BaseNet): """ + ### Description reimplementation of DeepMedic: Kamnitsas et al., "Efficient multi-scale 3D CNN with fully connected CRF for accurate brain lesion segmentation", MedIA '17 + + ### Building blocks + [CONV] - 3x3x3 convolutional layer + [denseCONV] - 1x1x1 convolutional layer + + ### Diagram + INPUT --> CROP -------> [CONV]x8 ------> [SUM] ----> [denseCONV]x3 --> OUTPUT + | | + DOWNSAMPLE ---> [CONV]x8 ---> UPSAMPLE + + + ### Constraints: + - The downsampling factor (d_factor) should be odd + - Label size = [(image_size / d_factor) - 16]* d_factor + - Image size should be divisible by d_factor + + # Examples: + - Appropriate configuration for training: + image spatial window size = 57, label spatial window size = 9, d_ factor = 3 + - Appropriate configuration for inference: + image spatial window size = 105, label spatial window size = 57, d_ factor = 3 """ def __init__(self, @@ -25,6 +47,16 @@ def __init__(self, b_regularizer=None, acti_func='prelu', name="DeepMedic"): + """ + + :param num_classes: int, number of channels of output + :param w_initializer: weight initialisation for network + :param w_regularizer: weight regularisation for network + :param b_initializer: bias initialisation for network + :param b_regularizer: bias regularisation for network + :param acti_func: activation function to use + :param name: layer name + """ super(DeepMedic, self).__init__( num_classes=num_classes, @@ -35,12 +67,20 @@ def __init__(self, acti_func=acti_func, name=name) - self.d_factor = 3 # downsampling factor + self.d_factor = 3 # downsampling factor - should be odd self.crop_diff = ((self.d_factor - 1) * 16) // 2 self.conv_features = [30, 30, 40, 40, 40, 40, 50, 50] self.fc_features = [150, 150, num_classes] def layer_op(self, images, is_training, layer_id=-1, **unused_kwargs): + """ + + :param images: tensor, input to the network, size should be divisible by d_factor + :param is_training: boolean, True if network is in training mode + :param layer_id: not in use + :param unused_kwargs: + :return: tensor, network output + """ # image_size is defined as the largest context, then: # downsampled path size: image_size / d_factor # downsampled path output: image_size / d_factor - 16 diff --git a/niftynet/network/dense_vnet.py b/niftynet/network/dense_vnet.py index d092e450..348d7d0e 100755 --- a/niftynet/network/dense_vnet.py +++ b/niftynet/network/dense_vnet.py @@ -20,6 +20,7 @@ class DenseVNet(BaseNet): """ + ### Description implementation of Dense-V-Net: Gibson et al., "Automatic multi-organ segmentation on abdominal CT with dense V-networks" 2018 @@ -47,6 +48,9 @@ class DenseVNet(BaseNet): - Skip layer: [ DFS + Conv] - Downsampled output: [ DFS + Down] + ### Constraints + - Input size has to be divisible by 2*dilation_rates + """ """ Default network hyperparameters @@ -80,7 +84,6 @@ class DenseVNet(BaseNet): use_dense_connections=True, use_coords=False) - def __init__(self, num_classes, hyperparams={}, @@ -90,6 +93,17 @@ def __init__(self, b_regularizer=None, acti_func='relu', name='DenseVNet'): + """ + + :param num_classes: int, number of channels of output + :param hyperparams: dictionary, network hyperparameters + :param w_initializer: weight initialisation for network + :param w_regularizer: weight regularisation for network + :param b_initializer: bias initialisation for network + :param b_regularizer: bias regularisation for network + :param acti_func: activation function to use + :param name: layer name + """ super(DenseVNet, self).__init__( num_classes=num_classes, @@ -106,7 +120,7 @@ def __init__(self, # Check for dilation rates if any([d != 1 for ds in self.hyperparams['dilation_rates'] - for d in ds]): + for d in ds]): raise NotImplementedError( 'Dilated convolutions are not yet implemented') # Check available modes @@ -117,8 +131,8 @@ def __init__(self, raise NotImplementedError( 'Image coordinate augmentation is not yet implemented') - def create_network(self): + hyperparams = self.hyperparams # Create initial convolutional layer @@ -126,10 +140,10 @@ def create_network(self): hyperparams['n_initial_conv_channels'], kernel_size=5, stride=2) - # name='initial_conv') + # name='initial_conv') # Create dense vblocks - num_blocks = len(hyperparams["n_dense_channels"]) # Num dense blocks + num_blocks = len(hyperparams["n_dense_channels"]) # Num dense blocks dense_ch = hyperparams["n_dense_channels"] seg_ch = hyperparams["n_seg_channels"] down_ch = hyperparams["n_down_channels"] @@ -152,25 +166,33 @@ def create_network(self): final_conv = ConvolutionalLayer( self.num_classes, kernel_size=hyperparams['seg_kernel_size'], - with_bn=False, + feature_normalization=None, with_bias=True) - # name='final_conv') + #  name='final_conv') # Create a structure with all the fields of a DenseVNet dense_vnet = namedtuple('DenseVNet', - ['initial_conv', 'dense_vblocks', 'final_conv']) + ['initial_conv', 'dense_vblocks', 'final_conv']) return dense_vnet(initial_conv=initial_conv, dense_vblocks=dense_vblocks, final_conv=final_conv) - def layer_op(self, input_tensor, is_training=True, layer_id=-1, keep_prob=0.5, **unused_kwargs): + """ + + :param input_tensor: tensor to input to the network, size has to be divisible by 2*dilation_rates + :param is_training: boolean, True if network is in training mode + :param layer_id: not in use + :param keep_prob: double, percentage of nodes to keep for drop-out + :param unused_kwargs: + :return: network prediction + """ hyperparams = self.hyperparams # Validate that dilation rates are compatible with input dimensions @@ -227,7 +249,6 @@ def layer_op(self, # Perform final convolution to segment structures output = dense_vnet.final_conv(all_features, is_training=is_training) - ###################### ### Postprocessing ### ###################### @@ -288,12 +309,23 @@ def __init__(self, output_shape, name='spatial_prior_block'): + """ + + :param prior_shape: shape of spatial prior + :param output_shape: target shape for resampling + :param name: layer name + """ + super(SpatialPriorBlock, self).__init__(name=name) self.prior_shape = prior_shape self.output_shape = output_shape def layer_op(self): + """ + + :return: spatial prior resampled to the target shape + """ # The internal representation is probabilities so # that resampling makes sense prior = tf.get_variable('prior', @@ -329,6 +361,15 @@ def __init__(self, use_bdo, name='dense_feature_stack_block', **kwargs): + """ + + :param n_dense_channels: int, number of dense channels in each block + :param kernel_size: kernel size for convolutional layers + :param dilation_rates: dilation rate of each layer in each vblock + :param use_bdo: boolean, set to True to use batch-wise drop-out + :param name: tensorflow scope name + :param kwargs: + """ super(DenseFeatureStackBlock, self).__init__(name=name) @@ -339,6 +380,10 @@ def __init__(self, self.kwargs = kwargs def create_block(self): + """ + + :return: dense feature stack block + """ dfs_block = [] for _ in self.dilation_rates: if self.use_bdo: @@ -357,6 +402,13 @@ def create_block(self): return dfs_block def layer_op(self, input_tensor, is_training=True, keep_prob=None): + """ + + :param input_tensor: tf tensor, input to the DenseFeatureStackBlock + :param is_training: boolean, True if network is in training mode + :param keep_prob: double, percentage of nodes to keep for drop-out + :return: feature stack + """ # Create dense feature stack block dfs_block = self.create_block() # Initialize feature stack for block @@ -438,6 +490,17 @@ def __init__(self, use_bdo, name='dense_feature_stack_block', **kwargs): + """ + + :param n_dense_channels: int, number of dense channels + :param kernel_size: kernel size for convolutional layers + :param dilation_rates: dilation rate of each layer in each vblock + :param n_seg_channels: int, number of segmentation channels + :param n_down_channels: int, number of output channels when downsampling + :param use_bdo: boolean, set to True to use batch-wise drop-out + :param name: layer name + :param kwargs: + """ super(DenseFeatureStackBlockWithSkipAndDownsample, self).__init__( name=name) @@ -451,6 +514,10 @@ def __init__(self, self.kwargs = kwargs def create_block(self): + """ + + :return: Dense Feature Stack with Skip Layer and Downsampling block + """ dfs_block = DenseFeatureStackBlock(self.n_dense_channels, self.kernel_size, self.dilation_rates, @@ -467,17 +534,24 @@ def create_block(self): down_conv = ConvolutionalLayer(self.n_down_channels, kernel_size=self.kernel_size, stride=2, - # name='down_conv', + #  name='down_conv', **self.kwargs) dfssd_block = namedtuple('DenseSDBlock', - ['dfs_block', 'skip_conv', 'down_conv']) + ['dfs_block', 'skip_conv', 'down_conv']) return dfssd_block(dfs_block=dfs_block, skip_conv=skip_conv, down_conv=down_conv) def layer_op(self, input_tensor, is_training=True, keep_prob=None): + """ + + :param input_tensor: tf tensor, input to the DenseFeatureStackBlock + :param is_training: boolean, True if network is in training mode + :param keep_prob: double, percentage of nodes to keep for drop-out + :return: feature stack after skip convolution, feature stack after downsampling + """ # Create dense feature stack block with skip and downsample dfssd_block = self.create_block() diff --git a/niftynet/network/highres3dnet.py b/niftynet/network/highres3dnet.py index 7e94e466..0366e34d 100755 --- a/niftynet/network/highres3dnet.py +++ b/niftynet/network/highres3dnet.py @@ -22,7 +22,7 @@ class HighRes3DNet(BaseNet): ### Building blocks { } - Residual connections: see He et al. "Deep residual learning for - image recogntion", in CVPR '16 + image recognition", in CVPR '16 [CONV] - Convolutional layer in form: Activation(Convolution(X)) where X = input tensor or output of previous layer @@ -52,6 +52,9 @@ class HighRes3DNet(BaseNet): INPUT --> [CONV] --> { (3)[D-CONV(1)] } --> { (3)[D-CONV(2)] } --> { (3)[D-CONV(4)] } -> [CONV*] -> Loss + ### Constraints + - Input image size should be divisible by 8 + """ def __init__(self, @@ -62,6 +65,16 @@ def __init__(self, b_regularizer=None, acti_func='prelu', name='HighRes3DNet'): + """ + + :param num_classes: int, number of channels of output + :param w_initializer: weight initialisation for network + :param w_regularizer: weight regularisation for network + :param b_initializer: bias initialisation for network + :param b_regularizer: bias regularisation for network + :param acti_func: activation function to use + :param name: layer name + """ super(HighRes3DNet, self).__init__( num_classes=num_classes, @@ -81,6 +94,14 @@ def __init__(self, {'name': 'conv_2', 'n_features': num_classes, 'kernel_size': 1}] def layer_op(self, images, is_training=True, layer_id=-1, **unused_kwargs): + """ + + :param images: tensor to input to the network. Size has to be divisible by 8 + :param is_training: boolean, True if network is in training mode + :param layer_id: int, index of the layer to return as output + :param unused_kwargs: + :return: output of layer indicated by layer_id + """ assert layer_util.check_spatial_dims( images, lambda x: x % 8 == 0) # go through self.layers, create an instance of each layer @@ -181,7 +202,7 @@ def _print(self, list_of_layers): class HighResBlock(TrainableLayer): """ - This class define a high-resolution block with residual connections + This class defines a high-resolution block with residual connections kernels - specify kernel sizes of each convolutional layer @@ -200,6 +221,16 @@ def __init__(self, w_regularizer=None, with_res=True, name='HighResBlock'): + """ + + :param n_output_chns: int, number of output channels + :param kernels: list of layer kernel sizes + :param acti_func: activation function to use + :param w_initializer: weight initialisation for network + :param w_regularizer: weight regularisation for network + :param with_res: boolean, set to True if residual connection are to use + :param name: layer name + """ super(HighResBlock, self).__init__(name=name) @@ -215,6 +246,12 @@ def __init__(self, self.regularizers = {'w': w_regularizer} def layer_op(self, input_tensor, is_training): + """ + + :param input_tensor: tensor, input to the network + :param is_training: boolean, True if network is in training mode + :return: tensor, output of the residual block + """ output_tensor = input_tensor for (i, k) in enumerate(self.kernels): # create parameterised layers diff --git a/niftynet/network/highres3dnet_large.py b/niftynet/network/highres3dnet_large.py index 2d332ca8..600c9ddc 100755 --- a/niftynet/network/highres3dnet_large.py +++ b/niftynet/network/highres3dnet_large.py @@ -18,6 +18,9 @@ class HighRes3DNetLarge(BaseNet): convolutional networks: Brain parcellation as a pretext task", IPMI '17 (This is a larger version with an additional 3x3x3 convolution) + + ### Constraints + - Input image size should be divisible by 8 """ def __init__(self, @@ -28,6 +31,16 @@ def __init__(self, b_regularizer=None, acti_func='relu', name='HighRes3DNet'): + """ + + :param num_classes: int, number of channels of output + :param w_initializer: weight initialisation for network + :param w_regularizer: weight regularisation for network + :param b_initializer: bias initialisation for network + :param b_regularizer: bias regularisation for network + :param acti_func: activation function to use + :param name: layer name + """ super(HighRes3DNetLarge, self).__init__( num_classes=num_classes, @@ -53,6 +66,15 @@ def layer_op(self, layer_id=-1, keep_prob=0.5, **unused_kwargs): + """ + + :param images: tensor to input to the network. Size has to be divisible by 8 + :param is_training: boolean, True if network is in training mode + :param layer_id: int, index of the layer to return as output + :param keep_prob: double, percentage of nodes to keep for drop-out + :param unused_kwargs: + :return: output of layer indicated by layer_id + """ assert (layer_util.check_spatial_dims( images, lambda x: x % 8 == 0)) # go through self.layers, create an instance of each layer diff --git a/niftynet/network/highres3dnet_small.py b/niftynet/network/highres3dnet_small.py index 9647c355..e862bbd9 100755 --- a/niftynet/network/highres3dnet_small.py +++ b/niftynet/network/highres3dnet_small.py @@ -19,6 +19,9 @@ class HighRes3DNetSmall(BaseNet): convolutional networks: Brain parcellation as a pretext task", IPMI '17 (This is smaller model with an initial stride-2 convolution) + + ### Constraints + - Input image size should be divisible by 8 """ def __init__(self, @@ -29,6 +32,16 @@ def __init__(self, b_regularizer=None, acti_func='relu', name='HighRes3DNetSmall'): + """ + + :param num_classes: int, number of channels of output + :param w_initializer: weight initialisation for network + :param w_regularizer: weight regularisation for network + :param b_initializer: bias initialisation for network + :param b_regularizer: bias regularisation for network + :param acti_func: activation function to use + :param name: layer name + """ super(HighRes3DNetSmall, self).__init__( num_classes=num_classes, @@ -48,6 +61,14 @@ def __init__(self, {'name': 'conv_2', 'n_features': num_classes, 'kernel_size': 1}] def layer_op(self, images, is_training=True, layer_id=-1, **unused_kwargs): + """ + + :param images: tensor to input to the network. Size has to be divisible by 8 + :param is_training: boolean, True if network is in training mode + :param layer_id: int, index of the layer to return as output + :param unused_kwargs: + :return: output of layer indicated by layer_id + """ assert (layer_util.check_spatial_dims( images, lambda x: x % 8 == 0)) # go through self.layers, create an instance of each layer diff --git a/niftynet/network/holistic_net.py b/niftynet/network/holistic_net.py index cce81f40..fa77c13c 100755 --- a/niftynet/network/holistic_net.py +++ b/niftynet/network/holistic_net.py @@ -15,10 +15,54 @@ class HolisticNet(BaseNet): """ + ### Description Implementation of HolisticNet detailed in Fidon, L. et. al. (2017) Generalised Wasserstein Dice Score for Imbalanced Multi-class Segmentation using Holistic Convolutional Networks. MICCAI 2017 (BrainLes) + + ### Diagram Blocks + [CONV] - 3x3x3 Convolutional layer in form: Activation(Convolution(X)) + where X = input tensor or output of previous layer + + and Activation is a function which includes: + + a) Batch-Norm + b) Activation Function (Elu, ReLu, PreLu, Sigmoid, Tanh etc.) + + [D-CONV(d)] - 3x3x3 Convolutional layer with dilated convolutions with blocks in + pre-activation mode: D-Convolution(Activation(X)) + see He et al., "Identity Mappings in Deep Residual Networks", ECCV '16 + + dilation factor = d + D-CONV(2) : dilated convolution with dilation factor 2 + + repeat factor = r + e.g. + (2)[D-CONV(d)] : 2 dilated convolutional layers in a row [D-CONV] -> [D-CONV] + { (2)[D-CONV(d)] } : 2 dilated convolutional layers within residual block + + [SCORE] - Batch-Norm + 3x3x3 Convolutional layer + Activation function + 1x1x1 Convolutional layer + + [MERGE] - Channel-wise merging + + + ### Diagram + + MULTIMODAL INPUT ----- [CONV]x3 -----[D-CONV(2)]x3 ----- MaxPooling ----- [CONV]x3 -----[D-CONV(2)]x3 + | | | | + [SCORE] [SCORE] [SCORE] [SCORE] + | | | | + ------------------------------------------------------------------- + | + [MERGE] --> OUTPUT + + ### Constraints + - Input image size should be divisible by 8 + + ### Comments + - The network returns only the merged output, so the loss will be applied only to this + (different from the referenced paper) """ def __init__(self, @@ -29,6 +73,16 @@ def __init__(self, b_regularizer=None, acti_func='elu', name='HolisticNet'): + """ + + :param num_classes: int, number of channels of output + :param w_initializer: weight initialisation for network + :param w_regularizer: weight regularisation for network + :param b_initializer: bias initialisation for network + :param b_regularizer: bias regularisation for network + :param acti_func: activation function to use + :param name: layer name + """ super(HolisticNet, self).__init__( num_classes=num_classes, acti_func=acti_func, @@ -49,11 +103,19 @@ def layer_op(self, is_training=True, layer_id=-1, **unused_kwargs): + """ + + :param input_tensor: tensor, input to the network + :param is_training: boolean, True if network is in training mode + :param layer_id: not in use + :param unused_kwargs: + :return: fused prediction from multiple scales + """ layer_instances = [] scores_instances = [] first_conv_layer = ConvolutionalLayer( n_output_chns=self.num_features[0], - with_bn=True, + feature_normalization='batch', kernel_size=3, w_initializer=self.initializers['w'], w_regularizer=self.regularizers['w'], @@ -202,6 +264,15 @@ def __init__(self, num_classes=1, acti_func='elu', name='ScoreLayer'): + """ + + :param num_features: int, number of features + :param w_initializer: weight initialisation for network + :param w_regularizer: weight regularisation for network + :param num_classes: int, number of prediction channels + :param acti_func: activation function to use + :param name: layer name + """ super(ScoreLayer, self).__init__(name=name) self.num_classes = num_classes self.acti_func = acti_func @@ -211,6 +282,13 @@ def __init__(self, self.regularizers = {'w': w_regularizer} def layer_op(self, input_tensor, is_training, layer_id=-1): + """ + + :param input_tensor: tensor, input to the layer + :param is_training: boolean, True if network is in training mode + :param layer_id: not is use + :return: tensor with number of channels to num_classes + """ rank = input_tensor.shape.ndims perm = [i for i in range(rank)] perm[-2], perm[-1] = perm[-1], perm[-2] @@ -223,7 +301,7 @@ def layer_op(self, input_tensor, is_training, layer_id=-1): for layer in range(n_layers - 1): layer_to_add = ConvolutionalLayer( n_output_chns=self.num_features[layer + 1], - with_bn=True, + feature_normalization='batch', kernel_size=3, w_initializer=self.initializers['w'], w_regularizer=self.regularizers['w'], @@ -248,6 +326,14 @@ def __init__(self, w_regularizer=None, acti_func='elu', name='MergeLayer'): + """ + + :param func: type of merging layer (SUPPORTED_OPS: AVERAGE, WEIGHTED_AVERAGE, MAXOUT) + :param w_initializer: weight initialisation for network + :param w_regularizer: weight regularisation for network + :param acti_func: activation function to use + :param name: layer name + """ super(MergeLayer, self).__init__(name=name) self.func = func self.acti_func = acti_func @@ -255,6 +341,11 @@ def __init__(self, self.regularizers = {'w': w_regularizer} def layer_op(self, roots): + """ + Performs channel-wise merging of input tensors + :param roots: tensors to be merged + :return: fused tensor + """ if self.func == 'MAXOUT': return tf.reduce_max(tf.stack(roots, axis=-1), axis=-1) elif self.func == 'AVERAGE': diff --git a/niftynet/network/interventional_affine_net.py b/niftynet/network/interventional_affine_net.py index 94aeae72..a87279e7 100755 --- a/niftynet/network/interventional_affine_net.py +++ b/niftynet/network/interventional_affine_net.py @@ -16,14 +16,9 @@ class INetAffine(BaseNet): - def __init__(self, - decay=1e-6, - affine_w_initializer=None, - affine_b_initializer=None, - acti_func='relu', - name='inet-affine'): - """ - This network estimates affine transformations from + """ + ### Description + This network estimates affine transformations from a pair of moving and fixed image: Hu et al., Label-driven weakly-supervised learning for @@ -37,11 +32,33 @@ def __init__(self, see also: https://github.com/YipengHu/label-reg - :param decay: - :param affine_w_initializer: - :param affine_b_initializer: - :param acti_func: - :param name: + ### Building blocks + [DOWN CONV] - Convolutional layer + Residual Unit + Max pooling + [CONV] - Convolutional layer + [FC] - Fully connected layer, outputs the affine matrix + [WARPER] - Grid resampling with the obtained affine matrix + + ### Diagram + + INPUT PAIR --> [DOWN CONV]x4 --> [CONV] --> [FC] --> [WARPER] --> DISPLACEMENT FIELD + + ### Constraints + - input spatial rank should be either 2 or 3 (2D or 3D images only) + + """ + def __init__(self, + decay=1e-6, + affine_w_initializer=None, + affine_b_initializer=None, + acti_func='relu', + name='inet-affine'): + """ + + :param decay: float, regularisation decay + :param affine_w_initializer: weight initialisation for affine registration network + :param affine_b_initializer: bias initialisation for affine registration network + :param acti_func: activation function to use + :param name: layer name """ BaseNet.__init__(self, name=name) @@ -65,9 +82,9 @@ def layer_op(self, **unused_kwargs): """ - :param fixed_image: - :param moving_image: - :param is_training: + :param fixed_image: tensor, fixed image for registration (defines reference space) + :param moving_image: tensor, moving image to be registered to fixed + :param is_training: boolean, True if network is in training mode :return: displacement fields transformed by estimating affine """ @@ -84,7 +101,7 @@ def layer_op(self, conv_5 = Conv(n_output_chns=self.fea[4], kernel_size=self.k_conv, - with_bias=False, with_bn=True, + with_bias=False, feature_normalization='batch', **self.res_param)(res_4, is_training) if spatial_rank == 2: @@ -99,7 +116,7 @@ def layer_op(self, self.affine_w_initializer = init_affine_w() if self.affine_b_initializer is None: self.affine_b_initializer = init_affine_b(spatial_rank) - affine = FC(n_output_chns=affine_size, with_bn=False, + affine = FC(n_output_chns=affine_size, feature_normalization=None, w_initializer=self.affine_w_initializer, b_initializer=self.affine_b_initializer, **self.affine_param)(conv_5) @@ -109,10 +126,21 @@ def layer_op(self, def init_affine_w(std=1e-8): + """ + + :param std: float, standard deviation of normal distribution for weight initialisation + :return: random weight initialisation from normal distribution with zero mean + """ return tf.random_normal_initializer(0, std) def init_affine_b(spatial_rank, initial_bias=0.0): + """ + + :param spatial_rank: int, rank of inputs (either 2D or 3D) + :param initial_bias: float, initial bias + :return: bias initialisation for the affine matrix + """ if spatial_rank == 2: identity = np.array([[1., 0., 0.], [0., 1., 0.]]).flatten() diff --git a/niftynet/network/interventional_dense_net.py b/niftynet/network/interventional_dense_net.py index 3d6dc360..9271d671 100755 --- a/niftynet/network/interventional_dense_net.py +++ b/niftynet/network/interventional_dense_net.py @@ -17,15 +17,8 @@ class INetDense(BaseNet): - def __init__(self, - decay=0.0, - smoothing=0, - disp_w_initializer=None, - disp_b_initializer=None, - acti_func='relu', - multi_scale_fusion=True, - name='inet-dense'): - """ + """ + ### Description The network estimates dense displacement fields from a pair of moving and fixed images: @@ -40,14 +33,48 @@ def __init__(self, see also: https://github.com/YipengHu/label-reg - :param decay: - :param smoothing: + ### Building blocks + [DOWN CONV] - Convolutional layer + Residual Unit + Downsampling (Max pooling) + [CONV] - Convolutional layer + [UP CONV] - Upsampling + Sum + Residual Unit + [FUSION] - Multi-scale displacement fields fusion + [DISPtoDEF] - (Smoothing if required) Conversion to deformation field (adding base grid) + + + ### Diagram + INPUT PAIR --> [DOWN CONV] [UP CONV] --> [CONV] --[FUSION] --> [DISPtoDEF] --> DENSE FIELD + | | | + [DOWN CONV] [UP CONV] --> [CONV] -----| + | | | + [DOWN CONV] [UP CONV] --> [CONV] -----| + | | | + [DOWN CONV] [UP CONV] --> [CONV] -----| + | | | + -------- [CONV]------------------ [CONV]------- + + + ### Constraints + - input spatial rank should be either 2 or 3 (2D or 3D images only) + - fixed image size should be divisible by 16 + """ + def __init__(self, + decay=0.0, + smoothing=0, + disp_w_initializer=None, + disp_b_initializer=None, + acti_func='relu', + multi_scale_fusion=True, + name='inet-dense'): + """ + + :param decay: float, regularisation decay + :param smoothing: float, smoothing factor for dense displacement field :param disp_w_initializer: initialisation of the displacement fields - :param disp_b_initializer: initialisation of the dis - :param acti_func: + :param disp_b_initializer: initialisation of the displacement fields + :param acti_func: activation function to use :param multi_scale_fusion: True/False indicating whether to use multiscale feature fusion. - :param name: + :param name: layer name """ BaseNet.__init__(self, name=name) @@ -96,10 +123,10 @@ def layer_op(self, **unused_kwargs): """ - :param fixed_image: - :param moving_image: - :param base_grid: - :param is_training: + :param fixed_image: tensor, fixed image for registration (defines reference space) + :param moving_image: tensor, moving image to be registered to fixed + :param base_grid: initial identity or affine displacement field + :param is_training: boolean, True if network is in training mode :return: estimated dense displacement fields """ @@ -143,7 +170,7 @@ def layer_op(self, field = Conv(n_output_chns=spatial_rank, kernel_size=self.k_conv, with_bias=True, - with_bn=False, + feature_normalization=None, acti_func=None, **self.disp_param)(scale_out) resized_field = Resize(new_size=spatial_shape)(field) @@ -178,6 +205,12 @@ def layer_op(self, def _get_smoothing_kernel(sigma, spatial_rank): + """ + + :param sigma: float, standard deviation for gaussian smoothing kernel + :param spatial_rank: int, rank of input + :return: smoothing kernel + """ # sigma defined in voxel not in freeform deformation grid if sigma <= 0: raise NotImplementedError @@ -195,6 +228,12 @@ def _get_smoothing_kernel(sigma, spatial_rank): def _smoothing_func(sigma): def smoothing(dense_field, spatial_rank): + """ + + :param dense_field: tensor, dense field to be smoothed + :param spatial_rank: int, rank of input images + :return: smoothed dense field + """ kernel = _get_smoothing_kernel(sigma, spatial_rank) kernel = tf.constant(kernel, dtype=dense_field.dtype) kernel = tf.expand_dims(kernel, axis=-1) @@ -208,6 +247,11 @@ def smoothing(dense_field, spatial_rank): def _computing_bending_energy(displacement): + """ + + :param displacement: tensor, displacement field + :return: bending energy + """ spatial_rank = infer_spatial_rank(displacement) if spatial_rank == 2: return _computing_bending_energy_2d(displacement) @@ -218,6 +262,11 @@ def _computing_bending_energy(displacement): def _computing_bending_energy_2d(displacement): + """ + + :param displacement: 2D tensor, displacement field + :return: bending energy + """ dTdx = ImgGrad(spatial_axis=0)(displacement) dTdy = ImgGrad(spatial_axis=1)(displacement) @@ -230,6 +279,11 @@ def _computing_bending_energy_2d(displacement): def _computing_bending_energy_3d(displacement): + """ + + :param displacement: 3D tensor, displacement field + :return: bending energy + """ dTdx = ImgGrad(spatial_axis=0)(displacement) dTdy = ImgGrad(spatial_axis=1)(displacement) dTdz = ImgGrad(spatial_axis=2)(displacement) @@ -249,6 +303,12 @@ def _computing_bending_energy_3d(displacement): def _computing_gradient_norm(displacement, flag_L1=False): + """ + + :param displacement: tensor, displacement field + :param flag_L1: boolean, True if L1 norm shoudl be used + :return: L2 (or L1) norm of gradients + """ norms = [] for spatial_ind in range(infer_spatial_rank(displacement)): dTdt = ImgGrad(spatial_axis=spatial_ind)(displacement) diff --git a/niftynet/network/interventional_hybrid_net.py b/niftynet/network/interventional_hybrid_net.py index 9ec5c7bd..78a3cfad 100755 --- a/niftynet/network/interventional_hybrid_net.py +++ b/niftynet/network/interventional_hybrid_net.py @@ -8,18 +8,9 @@ class INetHybridPreWarp(BaseNet): - def __init__(self, - decay, - affine_w_initializer=None, - affine_b_initializer=None, - disp_w_initializer=None, - disp_b_initializer=None, - acti_func='relu', - interp='linear', - boundary='replicate', - name='inet-hybrid-pre-warp'): - """ - Re-implementation of the registration network proposed in: + """ + ### Description + Re-implementation of the registration network proposed in: Hu et al., Label-driven weakly-supervised learning for multimodal deformable image registration, arXiv:1711.01666 @@ -32,15 +23,40 @@ def __init__(self, see also: https://github.com/YipengHu/label-reg - :param decay: - :param affine_w_initializer: - :param affine_b_initializer: - :param disp_w_initializer: - :param disp_b_initializer: - :param acti_func: - :param interp: - :param boundary: - :param name: + ### Building blocks + [GLOBAL] - INetAffine from interventional_affine_net.py + [RESAMPLER] - Layer to resample the moving image with estimated affine + [DENSE] - INetDense from intervetional_dense_net.py + + ### Diagram + + INPUT PAIR --> [GLOBAL] --> [RESAMPLER] --> [DENSE] --> DENSE FIELD, AFFINE FIELD + + ### Constraints + - input spatial rank should be either 2 or 3 (2D or 3D images only) + - fixed image size should be divisible by 16 + """ + def __init__(self, + decay, + affine_w_initializer=None, + affine_b_initializer=None, + disp_w_initializer=None, + disp_b_initializer=None, + acti_func='relu', + interp='linear', + boundary='replicate', + name='inet-hybrid-pre-warp'): + """ + + :param decay: float, regularisation decay + :param affine_w_initializer: weight initialisation for affine registration network + :param affine_b_initializer: bias initialisation for affine registration network + :param disp_w_initializer: weight initialisation for dense registration network + :param disp_b_initializer: bias initialisation for dense registration network + :param acti_func: activation function to use + :param interp: string, type of interpolation for the resampling [default:linear] + :param boundary: string, padding mode to deal with image boundary + :param name: layer name """ BaseNet.__init__(self, name=name) self.global_net = INetAffine(decay=decay, @@ -61,6 +77,14 @@ def layer_op(self, moving_image, is_training=True, **unused_kwargs): + """ + + :param fixed_image: tensor, fixed image for registration (defines reference space) + :param moving_image: tensor, moving image to be registered to fixed + :param is_training: boolean, True if network is in training mode + :param unused_kwargs: not in use + :return: estimated final dense and affine displacement fields + """ affine_field = self.global_net(fixed_image, moving_image, is_training) moving_image = resampler( interpolation=self.interp, @@ -71,6 +95,35 @@ def layer_op(self, class INetHybridTwoStream(BaseNet): + """ + ### Description + Re-implementation of the registration network proposed in: + + Hu et al., Label-driven weakly-supervised learning for + multimodal deformable image registration, arXiv:1711.01666 + https://arxiv.org/abs/1711.01666 + + Hu et al., Weakly-Supervised Convolutional Neural Networks for + Multimodal Image Registration, Medical Image Analysis (2018) + https://doi.org/10.1016/j.media.2018.07.002 + + see also: + https://github.com/YipengHu/label-reg + + ### Building blocks + [GLOBAL] - INetAffine from interventional_affine_net.py + [DENSE] - INetDense from intervetional_dense_net.py + + ### Diagram + + INPUT PAIR --> [GLOBAL] --> AFFINE FIELD --- DENSE + AFFINE FIELD + | | + -------> [DENSE] --> DENSE FIELD ------ + + ### Constraints + - input spatial rank should be either 2 or 3 (2D or 3D images only) + - fixed image size should be divisible by 16 + """ def __init__(self, decay, affine_w_initializer=None, @@ -81,6 +134,18 @@ def __init__(self, interp='linear', boundary='replicate', name='inet-hybrid-two-stream'): + """ + + :param decay: float, regularisation decay + :param affine_w_initializer: weight initialisation for affine registration network + :param affine_b_initializer: bias initialisation for affine registration network + :param disp_w_initializer: weight initialisation for dense registration network + :param disp_b_initializer: bias initialisation for dense registration network + :param acti_func: activation function to use + :param interp: string, type of interpolation for the resampling [default:linear] - not in use + :param boundary: string, padding mode to deal with image boundary [default: replicate] - not is use + :param name: layer name + """ BaseNet.__init__(self, name=name) self.global_net = INetAffine(decay=decay, affine_w_initializer=affine_w_initializer, @@ -100,6 +165,14 @@ def layer_op(self, moving_image, is_training=True, **unused_kwargs): + """ + + :param fixed_image: tensor, fixed image for registration (defines reference space) + :param moving_image: tensor, moving image to be registered to fixed + :param is_training: boolean, True if network is in training mode + :param unused_kwargs: not in use + :return: estimated total, dense and affine displacement fields + """ affine_field = self.global_net(fixed_image, moving_image, is_training) dense_field = self.local_net(fixed_image, moving_image, is_training) return dense_field + affine_field, dense_field, affine_field diff --git a/niftynet/network/no_new_net.py b/niftynet/network/no_new_net.py new file mode 100755 index 00000000..44db70e5 --- /dev/null +++ b/niftynet/network/no_new_net.py @@ -0,0 +1,214 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function + +from niftynet.layer import layer_util +from niftynet.layer.base_layer import TrainableLayer +from niftynet.layer.convolution import ConvolutionalLayer +from niftynet.layer.deconvolution import DeconvolutionalLayer +from niftynet.layer.downsample import DownSampleLayer +from niftynet.layer.elementwise import ElementwiseLayer +from niftynet.layer.crop import CropLayer +from niftynet.layer.linear_resize import LinearResizeLayer +from niftynet.utilities.util_common import look_up_operations + + +class UNet3D(TrainableLayer): + """ + Implementation of No New-Net + Isensee et al., "No New-Net", MICCAI BrainLesion Workshop 2018. + + The major changes between this and our standard 3d U-Net: + * input size == output size: padded convs are used + * leaky relu as non-linearity + * reduced number of filters before upsampling + * instance normalization (not batch) + * fits 128x128x128 with batch size of 2 on one TitanX GPU for + training + * no learned upsampling: linear resizing. + """ + + def __init__(self, + num_classes, + w_initializer=None, + w_regularizer=None, + b_initializer=None, + b_regularizer=None, + acti_func='leakyrelu', + name='NoNewNet'): + super(UNet3D, self).__init__(name=name) + + self.n_features = [30, 60, 120, 240, 480] + self.acti_func = acti_func + self.num_classes = num_classes + + self.initializers = {'w': w_initializer, 'b': b_initializer} + self.regularizers = {'w': w_regularizer, 'b': b_regularizer} + + print('using {}'.format(name)) + + def layer_op(self, thru_tensor, is_training=True, **unused_kwargs): + """ + + :param thru_tensor: the input is modified in-place as it goes through the network + :param is_training: + :param unused_kwargs: + :return: + """ + # image_size should be divisible by 16 because of max-pooling 4 times, 2x2x2 + assert layer_util.check_spatial_dims(thru_tensor, lambda x: x % 16 == 0) + block_layer = UNetBlock('DOWNSAMPLE', + (self.n_features[0], self.n_features[0]), + (3, 3), with_downsample_branch=True, + w_initializer=self.initializers['w'], + w_regularizer=self.regularizers['w'], + acti_func=self.acti_func, + name='L1') + thru_tensor, conv_1 = block_layer(thru_tensor, is_training) + print(block_layer) + + block_layer = UNetBlock('DOWNSAMPLE', + (self.n_features[1], self.n_features[1]), + (3, 3), with_downsample_branch=True, + w_initializer=self.initializers['w'], + w_regularizer=self.regularizers['w'], + acti_func=self.acti_func, + name='L2') + thru_tensor, conv_2 = block_layer(thru_tensor, is_training) + print(block_layer) + + block_layer = UNetBlock('DOWNSAMPLE', + (self.n_features[2], self.n_features[2]), + (3, 3), with_downsample_branch=True, + w_initializer=self.initializers['w'], + w_regularizer=self.regularizers['w'], + acti_func=self.acti_func, + name='L3') + thru_tensor, conv_3 = block_layer(thru_tensor, is_training) + print(block_layer) + + block_layer = UNetBlock('DOWNSAMPLE', + (self.n_features[3], self.n_features[3]), + (3, 3), with_downsample_branch=True, + w_initializer=self.initializers['w'], + w_regularizer=self.regularizers['w'], + acti_func=self.acti_func, + name='L4') + thru_tensor, conv_4 = block_layer(thru_tensor, is_training) + print(block_layer) + + block_layer = UNetBlock('UPSAMPLE', + (self.n_features[4], self.n_features[3]), + (3, 3), with_downsample_branch=False, + w_initializer=self.initializers['w'], + w_regularizer=self.regularizers['w'], + acti_func=self.acti_func, + name='bottom') + thru_tensor, _ = block_layer(thru_tensor, is_training) + print(block_layer) + + block_layer = UNetBlock('UPSAMPLE', + (self.n_features[3], self.n_features[2]), + (3, 3), with_downsample_branch=False, + w_initializer=self.initializers['w'], + w_regularizer=self.regularizers['w'], + acti_func=self.acti_func, + name='R4') + concat_4 = ElementwiseLayer('CONCAT')(conv_4, thru_tensor) + thru_tensor, _ = block_layer(concat_4, is_training) + print(block_layer) + + block_layer = UNetBlock('UPSAMPLE', + (self.n_features[2], self.n_features[1]), + (3, 3), with_downsample_branch=False, + w_initializer=self.initializers['w'], + w_regularizer=self.regularizers['w'], + acti_func=self.acti_func, + name='R3') + concat_3 = ElementwiseLayer('CONCAT')(conv_3, thru_tensor) + thru_tensor, _ = block_layer(concat_3, is_training) + print(block_layer) + + block_layer = UNetBlock('UPSAMPLE', + (self.n_features[1], self.n_features[0]), + (3, 3), with_downsample_branch=False, + w_initializer=self.initializers['w'], + w_regularizer=self.regularizers['w'], + acti_func=self.acti_func, + name='R2') + concat_2 = ElementwiseLayer('CONCAT')(conv_2, thru_tensor) + thru_tensor, _ = block_layer(concat_2, is_training) + print(block_layer) + + block_layer = UNetBlock('NONE', + (self.n_features[0], self.n_features[0], self.num_classes), + (3, 3, 1), + with_downsample_branch=False, + w_initializer=self.initializers['w'], + w_regularizer=self.regularizers['w'], + acti_func=self.acti_func, + name='R1_FC') + + concat_1 = ElementwiseLayer('CONCAT')(conv_1, thru_tensor) + thru_tensor, _ = block_layer(concat_1, is_training) + print(block_layer) + + return thru_tensor + + +SUPPORTED_OP = {'DOWNSAMPLE', 'UPSAMPLE', 'NONE'} + + +class UNetBlock(TrainableLayer): + def __init__(self, + func, + n_chns, + kernels, + w_initializer=None, + w_regularizer=None, + with_downsample_branch=False, + acti_func='leakyrelu', + name='UNet_block'): + + super(UNetBlock, self).__init__(name=name) + + self.func = look_up_operations(func.upper(), SUPPORTED_OP) + + self.kernels = kernels + self.n_chns = n_chns + self.with_downsample_branch = with_downsample_branch + self.acti_func = acti_func + + self.initializers = {'w': w_initializer} + self.regularizers = {'w': w_regularizer} + + def layer_op(self, thru_tensor, is_training): + for (kernel_size, n_features) in zip(self.kernels, self.n_chns): + # no activation before final 1x1x1 conv layer + acti_func = self.acti_func if kernel_size > 1 else None + feature_normalization = 'instance' if acti_func is not None else None + + conv_op = ConvolutionalLayer(n_output_chns=n_features, + kernel_size=kernel_size, + w_initializer=self.initializers['w'], + w_regularizer=self.regularizers['w'], + acti_func=acti_func, + name='{}'.format(n_features), + feature_normalization=feature_normalization) + thru_tensor = conv_op(thru_tensor, is_training) + + if self.with_downsample_branch: + branch_output = thru_tensor + else: + branch_output = None + + if self.func == 'DOWNSAMPLE': + downsample_op = DownSampleLayer('MAX', kernel_size=2, stride=2, name='down_2x2') + thru_tensor = downsample_op(thru_tensor) + elif self.func == 'UPSAMPLE': + up_shape = [2 * int(thru_tensor.shape[i]) for i in (1, 2, 3)] + upsample_op = LinearResizeLayer(up_shape) + thru_tensor = upsample_op(thru_tensor) + + elif self.func == 'NONE': + pass # do nothing + return thru_tensor, branch_output diff --git a/niftynet/network/resnet.py b/niftynet/network/resnet.py index dc708891..c2cf5996 100755 --- a/niftynet/network/resnet.py +++ b/niftynet/network/resnet.py @@ -19,8 +19,23 @@ ResNetDesc = namedtuple('ResNetDesc', ['bn', 'fc', 'conv1', 'blocks']) class ResNet(BaseNet): """ - implementation of Res-Net: - He et al., "Identity Mappings in Deep Residual Networks", arXiv:1603.05027v3 + ### Description + implementation of Res-Net: + He et al., "Identity Mappings in Deep Residual Networks", arXiv:1603.05027v3 + + ### Building Blocks + [CONV] - Convolutional layer, no activation, no batch norm + (s)[DOWNRES] - Downsample residual block. + Each block is composed of a first bottleneck block with stride s, + followed by n_blocks_per_resolution bottleneck blocks with stride 1. + [FC] - Fully connected layer with nr output channels == num_classes + + ### Diagram + + INPUT --> [CONV] -->(s=1)[DOWNRES] --> (s=2)[DOWNRES]x2 --> BN, ReLU, mean --> [FC] --> OUTPUT + + ### Constraints + """ def __init__(self, @@ -33,6 +48,18 @@ def __init__(self, b_regularizer=None, acti_func='relu', name='ResNet'): + """ + + :param num_classes: int, number of channels of output + :param n_features: array, number of features per ResNet block + :param n_blocks_per_resolution: int, number of BottleneckBlock per DownResBlock + :param w_initializer: weight initialisation for network + :param w_regularizer: weight regularisation for network + :param b_initializer: bias initialisation for network + :param b_regularizer: bias regularisation for network + :param acti_func: ctivation function to use + :param name: layer name + """ super(ResNet, self).__init__( num_classes=num_classes, @@ -54,9 +81,13 @@ def __init__(self, acti_func=acti_func) def create(self): + """ + + :return: tuple with batch norm layer, fully connected layer, first conv layer and all residual blocks + """ bn=BNLayer() fc=FCLayer(self.num_classes) - conv1=self.Conv(self.n_features[0], acti_func=None, with_bn=False) + conv1=self.Conv(self.n_features[0], acti_func=None, feature_normalization=None) blocks=[] blocks+=[DownResBlock(self.n_features[1], self.n_blocks_per_resolution, 1, self.Conv)] for n in self.n_features[2:]: @@ -64,6 +95,13 @@ def create(self): return ResNetDesc(bn=bn,fc=fc,conv1=conv1,blocks=blocks) def layer_op(self, images, is_training=True, **unused_kwargs): + """ + + :param images: tensor, input to the network + :param is_training: boolean, True if network is in training mode + :param unused_kwargs: not in use + :return: tensor, output of the final fully connected layer + """ layers = self.create() out = layers.conv1(images, is_training) for block in layers.blocks: @@ -77,6 +115,13 @@ def layer_op(self, images, is_training=True, **unused_kwargs): BottleneckBlockDesc2 = namedtuple('BottleneckBlockDesc2', ['common_bn', 'conv', 'conv_shortcut']) class BottleneckBlock(TrainableLayer): def __init__(self, n_output_chns, stride, Conv, name='bottleneck'): + """ + + :param n_output_chns: int, number of output channels + :param stride: int, stride to use in the convolutional layers + :param Conv: layer, convolutional layer + :param name: layer name + """ self.n_output_chns = n_output_chns self.stride=stride self.bottle_neck_chns = n_output_chns // 4 @@ -84,6 +129,11 @@ def __init__(self, n_output_chns, stride, Conv, name='bottleneck'): super(BottleneckBlock, self).__init__(name=name) def create(self, input_chns): + """ + + :param input_chns: int, number of input channel + :return: tuple, with series of convolutional layers + """ if self.n_output_chns == input_chns: b1 = self.Conv(self.bottle_neck_chns, kernel_size=1, stride=self.stride) @@ -93,15 +143,21 @@ def create(self, input_chns): else: b1 = BNLayer() b2 = self.Conv(self.bottle_neck_chns,kernel_size=1, - stride=self.stride, acti_func=None, with_bn=False) + stride=self.stride, acti_func=None, feature_normalization=None) b3 = self.Conv(self.bottle_neck_chns,kernel_size=3) b4 = self.Conv(self.n_output_chns,kernel_size=1) b5 = self.Conv(self.n_output_chns,kernel_size=1, - stride=self.stride, acti_func=None,with_bn=False) + stride=self.stride, acti_func=None,feature_normalization=None) return BottleneckBlockDesc2(common_bn=b1, conv=[b2, b3, b4], conv_shortcut=b5) def layer_op(self, images, is_training=True): + """ + + :param images: tensor, input to the BottleNeck block + :param is_training: boolean, True if network is in training mode + :return: tensor, output of the BottleNeck block + """ layers = self.create(images.shape[-1]) if self.n_output_chns == images.shape[-1]: out=layers.conv[0](images, is_training) @@ -120,6 +176,14 @@ def layer_op(self, images, is_training=True): DownResBlockDesc = namedtuple('DownResBlockDesc', ['blocks']) class DownResBlock(TrainableLayer): def __init__(self, n_output_chns, count, stride, Conv, name='downres'): + """ + + :param n_output_chns: int, number of output channels + :param count: int, number of BottleneckBlocks to generate + :param stride: int, stride for convolutional layer + :param Conv: Layer, convolutional layer + :param name: layer name + """ self.count = count self.stride = stride self.n_output_chns = n_output_chns @@ -127,6 +191,10 @@ def __init__(self, n_output_chns, count, stride, Conv, name='downres'): super(DownResBlock, self).__init__(name=name) def create(self): + """ + + :return: tuple, containing all the Bottleneck blocks composing the DownRes block + """ blocks=[] blocks+=[BottleneckBlock(self.n_output_chns, self.stride, self.Conv)] for it in range(1,self.count): @@ -134,6 +202,12 @@ def create(self): return DownResBlockDesc(blocks=blocks) def layer_op(self, images, is_training): + """ + + :param images: tensor, input to the DownRes block + :param is_training: is_training: boolean, True if network is in training mode + :return: tensor, output of the DownRes block + """ layers = self.create() out = images for l in layers.blocks: diff --git a/niftynet/network/scalenet.py b/niftynet/network/scalenet.py index 7231d7fd..1c11be0f 100755 --- a/niftynet/network/scalenet.py +++ b/niftynet/network/scalenet.py @@ -14,6 +14,17 @@ class ScaleNet(BaseNet): implementation of ScaleNet: Fidon et al., "Scalable multimodal convolutional networks for brain tumour segmentation", MICCAI '17 + + ### Diagram + + INPUT --> [BACKEND] ----> [MERGING] ----> [FRONTEND] ---> OUTPUT + + [BACKEND] and [MERGING] are provided by the ScaleBlock below + [FRONTEND]: it can be any NiftyNet network (default: HighRes3dnet) + + ### Constraints: + - Input image size should be divisible by 8 + - more than one modality should be used """ def __init__(self, @@ -24,6 +35,16 @@ def __init__(self, b_regularizer=None, acti_func='prelu', name='ScaleNet'): + """ + + :param num_classes: int, number of channels of output + :param w_initializer: weight initialisation for network + :param w_regularizer: weight regularisation for network + :param b_initializer: bias initialisation for network + :param b_regularizer: bias regularisation for network + :param acti_func: activation function to use + :param name: layer name + """ super(ScaleNet, self).__init__( num_classes=num_classes, @@ -37,6 +58,14 @@ def __init__(self, self.n_features = 16 def layer_op(self, images, is_training=True, layer_id=-1, **unused_kwargs): + """ + + :param images: tensor, concatenation of multiple input modalities + :param is_training: boolean, True if network is in training mode + :param layer_id: not in use + :param unused_kwargs: + :return: predicted tensor + """ n_modality = images.shape.as_list()[-1] rank = images.shape.ndims assert n_modality > 1 @@ -64,6 +93,14 @@ def layer_op(self, images, is_training=True, layer_id=-1, **unused_kwargs): class ScaleBlock(TrainableLayer): + """ + Implementation of the ScaleBlock described in + Fidon et al., "Scalable multimodal convolutional + networks for brain tumour segmentation", MICCAI '17 + + See Fig 2(a) for diagram details - SN BackEnd + + """ def __init__(self, func, n_layers=1, @@ -71,6 +108,14 @@ def __init__(self, w_regularizer=None, acti_func='relu', name='scaleblock'): + """ + :param func: merging function (SUPPORTED_OP: MAX, AVERAGE) + :param n_layers: int, number of layers + :param w_initializer: weight initialisation for network + :param w_regularizer: weight regularisation for network + :param acti_func: activation function to use + :param name: layer name + """ self.func = look_up_operations(func.upper(), SUPPORTED_OP) super(ScaleBlock, self).__init__(name=name) self.n_layers = n_layers @@ -80,6 +125,12 @@ def __init__(self, self.regularizers = {'w': w_regularizer} def layer_op(self, input_tensor, is_training): + """ + + :param input_tensor: tensor, input to the network + :param is_training: boolean, True if network is in training mode + :return: merged tensor after backend layers + """ n_modality = input_tensor.shape.as_list()[-1] n_chns = input_tensor.shape.as_list()[-2] rank = input_tensor.shape.ndims diff --git a/niftynet/network/se_resnet.py b/niftynet/network/se_resnet.py index eb6788b7..2edb6565 100644 --- a/niftynet/network/se_resnet.py +++ b/niftynet/network/se_resnet.py @@ -16,8 +16,24 @@ SE_ResNetDesc = namedtuple('SE_ResNetDesc', ['bn', 'fc', 'conv1', 'blocks']) class SE_ResNet(BaseNet): """ - 3D implementation of SE-ResNet: - Hu et al., "Squeeze-and-Excitation Networks", arXiv:1709.01507v2 + ### Description + implementation of Res-Net: + He et al., "Identity Mappings in Deep Residual Networks", arXiv:1603.05027v3 + + ### Building Blocks + [CONV] - Convolutional layer, no activation, no batch norm + (s)[DOWNRES] - Downsample residual block. + Each block is composed of a first bottleneck block with stride s, + followed by n_blocks_per_resolution bottleneck blocks with stride 1. + Bottleneck blocks include a squeeze-and-excitation block + [FC] - Fully connected layer with nr output channels == num_classes + + ### Diagram + + INPUT --> [CONV] -->(s=1)[DOWNRES] --> (s=2)[DOWNRES] --> BN, ReLU, mean --> [FC] --> OUTPUT + + ### Constraints + """ def __init__(self, @@ -30,6 +46,18 @@ def __init__(self, b_regularizer=None, acti_func='relu', name='SE_ResNet'): + """ + + :param num_classes: int, number of channels of output + :param n_features: array, number of features per ResNet block + :param n_blocks_per_resolution: int, number of BottleneckBlock per DownResBlock + :param w_initializer: weight initialisation for network + :param w_regularizer: weight regularisation for network + :param b_initializer: bias initialisation for network + :param b_regularizer: bias regularisation for network + :param acti_func: ctivation function to use + :param name: layer name + """ super(SE_ResNet, self).__init__( num_classes=num_classes, @@ -51,9 +79,13 @@ def __init__(self, acti_func=acti_func) def create(self): + """ + + :return: tuple with batch norm layer, fully connected layer, first conv layer and all residual blocks + """ bn=BNLayer() fc=FCLayer(self.num_classes) - conv1=self.Conv(self.n_features[0], acti_func=None, with_bn=False) + conv1=self.Conv(self.n_features[0], acti_func=None, feature_normalization=None) blocks=[] blocks+=[DownResBlock(self.n_features[1], self.n_blocks_per_resolution, 1, self.Conv)] for n in self.n_features[2:]: @@ -61,6 +93,13 @@ def create(self): return SE_ResNetDesc(bn=bn,fc=fc,conv1=conv1,blocks=blocks) def layer_op(self, images, is_training=True, **unused_kwargs): + """ + + :param images: tensor, input to the network + :param is_training: boolean, True if network is in training mode + :param unused_kwargs: not in use + :return: tensor, output of the final fully connected layer + """ layers = self.create() out = layers.conv1(images, is_training) for block in layers.blocks: @@ -72,6 +111,13 @@ def layer_op(self, images, is_training=True, **unused_kwargs): BottleneckBlockDesc2 = namedtuple('BottleneckBlockDesc2', ['common_bn', 'conv', 'conv_shortcut']) class BottleneckBlock(TrainableLayer): def __init__(self, n_output_chns, stride, Conv, name='bottleneck'): + """ + + :param n_output_chns: int, number of output channels + :param stride: int, stride to use in the convolutional layers + :param Conv: layer, convolutional layer + :param name: layer name + """ self.n_output_chns = n_output_chns self.stride=stride self.bottle_neck_chns = n_output_chns // 4 @@ -79,6 +125,12 @@ def __init__(self, n_output_chns, stride, Conv, name='bottleneck'): super(BottleneckBlock, self).__init__(name=name) def create(self, input_chns): + """ + + :param input_chns: int, number of input channel + :return: tuple, with series of convolutional layers + """ + if self.n_output_chns == input_chns: b1 = self.Conv(self.bottle_neck_chns, kernel_size=1, stride=self.stride) @@ -88,15 +140,21 @@ def create(self, input_chns): else: b1 = BNLayer() b2 = self.Conv(self.bottle_neck_chns,kernel_size=1, - stride=self.stride, acti_func=None, with_bn=False) + stride=self.stride, acti_func=None, feature_normalization=None) b3 = self.Conv(self.bottle_neck_chns,kernel_size=3) b4 = self.Conv(self.n_output_chns,kernel_size=1) b5 = self.Conv(self.n_output_chns,kernel_size=1, - stride=self.stride, acti_func=None,with_bn=False) + stride=self.stride, acti_func=None,feature_normalization=None) return BottleneckBlockDesc2(common_bn=b1, conv=[b2, b3, b4], conv_shortcut=b5) def layer_op(self, images, is_training=True): + """ + + :param images: tensor, input to the BottleNeck block + :param is_training: boolean, True if network is in training mode + :return: tensor, output of the BottleNeck block + """ layers = self.create(images.shape[-1]) se=ChannelSELayer() if self.n_output_chns == images.shape[-1]: @@ -118,6 +176,14 @@ def layer_op(self, images, is_training=True): DownResBlockDesc = namedtuple('DownResBlockDesc', ['blocks']) class DownResBlock(TrainableLayer): def __init__(self, n_output_chns, count, stride, Conv, name='downres'): + """ + + :param n_output_chns: int, number of output channels + :param count: int, number of BottleneckBlocks to generate + :param stride: int, stride for convolutional layer + :param Conv: Layer, convolutional layer + :param name: layer name + """ self.count = count self.stride = stride self.n_output_chns = n_output_chns @@ -125,6 +191,10 @@ def __init__(self, n_output_chns, count, stride, Conv, name='downres'): super(DownResBlock, self).__init__(name=name) def create(self): + """ + + :return: tuple, containing all the Bottleneck blocks composing the DownRes block + """ blocks=[] blocks+=[BottleneckBlock(self.n_output_chns, self.stride, self.Conv)] for it in range(1,self.count): @@ -132,6 +202,12 @@ def create(self): return DownResBlockDesc(blocks=blocks) def layer_op(self, images, is_training): + """ + + :param images: tensor, input to the DownRes block + :param is_training: is_training: boolean, True if network is in training mode + :return: tensor, output of the DownRes block + """ layers = self.create() out = images for l in layers.blocks: diff --git a/niftynet/network/simple_gan.py b/niftynet/network/simple_gan.py index d691a855..477513b9 100755 --- a/niftynet/network/simple_gan.py +++ b/niftynet/network/simple_gan.py @@ -7,8 +7,19 @@ from niftynet.layer.base_layer import TrainableLayer - class GenericGAN(TrainableLayer): + """ + ### Description + Generic Generative Adversarial Network + + ### Diagram + + RANDOM NOISE --> [GENERATOR] --> [DISCRIMINATOR] --> fake logits + TRAINING SET ------------------> [DISCRIMINATOR] --> real logits + + ### Constraints + + """ def __init__(self, generator, discriminator, name='generic_GAN'): self._generator=generator self._discriminator=discriminator @@ -23,6 +34,22 @@ def layer_op(self, random_source, population,is_training): return image, real_logits, fake_logits, diff class SimpleGAN(GenericGAN): + """ + ### Description + Specification of generator and discriminator for generic gan + + ### Building blocks + [GENERATOR] - See ImageGenerator class below + [DISCRIMINATOR] - See ImageDiscriminator class below + + ### Diagram + + RANDOM NOISE --> [GENERATOR] --> [DISCRIMINATOR] --> fake logits + TRAINING SET ------------------> [DISCRIMINATOR] --> real logits + + ### Constraints + + """ def __init__(self, name='simple_GAN'): generator=ImageGenerator(hidden_layer_channels=[128,64,32,16], name='generator') @@ -30,13 +57,32 @@ def __init__(self, name='simple_GAN'): super(SimpleGAN, self).__init__(generator, discriminator, name) class ImageGenerator(TrainableLayer): + """ + ### Description + + ### Diagram + + ### Constraints + """ def __init__(self, hidden_layer_channels, name): + """ + + :param hidden_layer_channels: + :param name: layer name + """ super(ImageGenerator, self).__init__(name=name) self._num_layers = len(hidden_layer_channels) self._layer_channels = hidden_layer_channels self.initializers = {'w':tf.contrib.layers.variance_scaling_initializer(),'b':tf.constant_initializer(0)} def layer_op(self, random_source, image_size,is_training): + """ + + :param random_source: tensor, random noise to start generation + :param image_size: output image size + :param is_training: boolean, True if network is in training mode + :return: tensor, generated image + """ spatial_rank = len(image_size)-1 batch_size = random_source.shape.as_list()[0] noise_size=random_source.shape.as_list()[1] @@ -91,12 +137,31 @@ def resize3(x,sz): return tf.nn.tanh(image) class ImageDiscriminator(TrainableLayer): + """ + ### Description + + ### Diagram + + ### Constraints + + """ def __init__(self, hidden_layer_channels,name): + """ + + :param hidden_layer_channels: array, number of output channels for each layer + :param name: layer name + """ super(ImageDiscriminator, self).__init__(name=name) self._layer_channels = hidden_layer_channels self.initializers = {'w':tf.contrib.layers.variance_scaling_initializer(),'b':tf.constant_initializer(0)} def layer_op(self, image,is_training): + """ + + :param image: tensor, input image to distriminator + :param is_training: boolean, True if network is in training mode + :return: tensor, classification logits + """ batch_size=image.shape.as_list()[0] spatial_rank=len(image.shape)-2 image_channels = image.shape.as_list()[-1] diff --git a/niftynet/network/simulator_gan.py b/niftynet/network/simulator_gan.py index 97a512a7..131ce140 100755 --- a/niftynet/network/simulator_gan.py +++ b/niftynet/network/simulator_gan.py @@ -15,10 +15,23 @@ class SimulatorGAN(GANImageBlock): """ - implementation of - Hu et al., "Freehand Ultrasound Image Simulation with Spatially-Conditioned - Generative Adversarial Networks", MICCAI RAMBO 2017 - https://arxiv.org/abs/1707.05392 + ### Description + implementation of + Hu et al., "Freehand Ultrasound Image Simulation with Spatially-Conditioned + Generative Adversarial Networks", MICCAI RAMBO 2017 + https://arxiv.org/abs/1707.05392 + + ### Building blocks + [GENERATOR] - See ImageGenerator below + [DISCRIMINATOR] - See ImageDiscriminator below + Note: See niftynet.layer.gan_blocks for layer_op + + ### Diagram + + RANDOM NOISE --> [GENERATOR] --> [DISCRIMINATOR] --> fake logits + TRAINING SET ------------------> [DISCRIMINATOR] --> real logits + + ### Constraints """ def __init__(self, name='simulator_GAN'): @@ -30,7 +43,33 @@ def __init__(self, name='simulator_GAN'): class ImageGenerator(BaseGenerator): + """ + ### Description + implementation of generator from + Hu et al., "Freehand Ultrasound Image Simulation with Spatially-Conditioned + Generative Adversarial Networks", MICCAI RAMBO 2017 + https://arxiv.org/abs/1707.05392 + + ### Building blocks + [FC] - Fully connected layer + [CONV] - Convolutional layer, conditioning is concatenated to input before the convolution + (if conditioning is used) + kernel size = 3, activation = relu + [UPCONV] - Upsampling block composed of upsampling deconvolution (stride 2, kernel size = 3, + activation = relu) and concatenation of conditioning + [fCONV] - Final convolutional layer, with no conditioning. Kernel size = 3, activation = tanh + + ### Diagram + + RANDOM NOISE --> [FC] --> [CONV] --> [UPCONV]x3 --> [fCONV] --> OUTPUT IMAGE + + ### Constraints + """ def __init__(self, name): + """ + + :param name: layer name + """ super(ImageGenerator, self).__init__(name=name) self.initializers = {'w': tf.random_normal_initializer(0, 0.02), 'b': tf.constant_initializer(0.001)} @@ -38,6 +77,14 @@ def __init__(self, name): self.with_conditionings = [True, True, True, True, False] def layer_op(self, random_source, image_size, conditioning, is_training): + """ + + :param random_source: tensor, random noise to start generation + :param image_size: output image size + :param conditioning: tensor, conditioning information (e.g. coordinates in physical space) + :param is_training: boolean, True if network is in training mode + :return: tensor, generated image + """ keep_prob_ph = 1 # not passed in as a placeholder add_noise = self.noise_channels_per_layer if conditioning is not None: @@ -74,6 +121,12 @@ def resize_func(x, sz): resize_func = tf.image.resize_bilinear def concat_cond(x, with_conditioning): + """ + + :param x: tensor, input + :param with_conditioning: boolean, True if conditioning is to be used + :return: tensor, concatenation of x and conditioning, with noise addition + """ noise = [] if add_noise: feature_shape = x.shape.as_list()[0:-1] @@ -88,42 +141,70 @@ def concat_cond(x, with_conditioning): return x def conv(ch, x): + """ + Generates and applies a convolutional layer with relu as activation, kernel size 3, + batch norm + :param ch: int, number of output channels for convolutional layer + :param x: tensor, input to convolutional layer + :return: tensor, output of convolutiona layer + """ with tf.name_scope('conv'): conv_layer = ConvolutionalLayer( n_output_chns=ch, kernel_size=3, - with_bn=True, + feature_normalization='batch', with_bias=False, acti_func='relu', w_initializer=self.initializers['w']) return conv_layer(x, is_training=is_training) def up(ch, x): + """ + Performs deconvolution operation with kernel size 3, stride 2, batch norm, and relu + :param ch: int, number of output channels for deconvolutional layer + :param x: tensor, input to deconvolutional layer + :return: tensor, output of deconvolutiona layer + """ with tf.name_scope('up'): deconv_layer = DeconvolutionalLayer( n_output_chns=ch, kernel_size=3, stride=2, - with_bn=True, + feature_normalization='batch', with_bias=False, acti_func='relu', w_initializer=self.initializers['w']) return deconv_layer(x, is_training=is_training) def up_block(ch, x, with_conditioning): + """ + Performs upsampling and concatenation with conditioning + :param ch: int, number of output channels for deconvolutional layer + :param x: tensor, input to deconvolutional layer + :param with_conditioning: boolean, True if conditioning is to be used + :return: tensor, output of upsampling and concatenation + """ with tf.name_scope('up_block'): u = up(ch, x) cond = concat_cond(u, with_conditioning) return conv(cond.shape.as_list()[-1], cond) def noise_to_image(sz, ch, rand_tensor, with_conditioning): + """ + Processes random noise with fully connected layer and then convolutional layer + :param sz: image size + :param ch: int, number of output channels + :param rand_tensor: tensor, input random noise to generate image + :param with_conditioning: boolean, True if conditioning is to be used + :return: tensor, output of convolutional layer + """ batch_size = rand_tensor.shape.as_list()[0] output_shape = [batch_size] + sz + [ch] with tf.name_scope('noise_to_image'): g_no_0 = np.prod(sz) * ch fc_layer = FullyConnectedLayer( n_output_chns=g_no_0, - with_bn=False, + feature_normalization=None, with_bias=True, w_initializer=self.initializers['w'], b_initializer=self.initializers['b']) @@ -133,6 +214,12 @@ def noise_to_image(sz, ch, rand_tensor, with_conditioning): return conv(ch + conditioning_channels, g_h1p) def final_image(n_chns, x): + """ + + :param n_chns: int, number of output channels + :param x: tensor, input tensor to layers + :return: tensor, generated image + """ with tf.name_scope('final_image'): if add_noise > 0: feature_shape = x.shape.as_list()[0:-1] @@ -143,7 +230,7 @@ def final_image(n_chns, x): n_output_chns=n_chns, kernel_size=3, acti_func='tanh', - with_bn=False, + feature_normalization=None, with_bias=True, w_initializer=self.initializers['w'], b_initializer=self.initializers['b']) @@ -164,7 +251,39 @@ def final_image(n_chns, x): class ImageDiscriminator(BaseDiscriminator): + """ + ### Description + implementation of discrimator from + Hu et al., "Freehand Ultrasound Image Simulation with Spatially-Conditioned + Generative Adversarial Networks", MICCAI RAMBO 2017 + https://arxiv.org/abs/1707.05392 + + ### Building blocks + [FEATURE BLOCK] - Convolutional layer (kernel size = 5, activation = selu) + + residual convolutiona layer (kernel size = 3, activation = selu, batch norm) + + convolutional layer (kernel size = 3, activation = selu, batch norm) + + [DOWN BLOCK] - Downsampling block with residual connections: + Downsampling convolutional layer (stride = 2, kernel size = 3, + activation = selu, batch norm) + + residual convolutiona layer (kernel size = 3, activation = selu, batch norm) + + convolutional layer (kernel size = 3, activation = selu, batch norm) + + [FC] - Fully connected layer + + If conditioning is used, it gets concatenated to the image at the discriminator input + + ### Diagram + + INPUT IMAGE --> [FEATURE BLOCK] --> [DOWN BLOCK]x5 --> [FC] --> OUTPUT LOGITS + + ### Constraints + """ def __init__(self, name): + """ + + :param name: layer name + """ super(ImageDiscriminator, self).__init__(name=name) w_init = tf.random_normal_initializer(0, 0.02) @@ -176,34 +295,61 @@ def __init__(self, name): self.chns = [32, 64, 128, 256, 512, 1024, 1] def layer_op(self, image, conditioning, is_training): + """ + + :param image: tensor, input to the network + :param conditioning: tensor, conditioning information (e.g. coordinates in physical space) + :param is_training: boolean, True if network is in training mode + :return: tensor, classification logits + """ batch_size = image.shape.as_list()[0] def down(ch, x): + """ + Downsampling convolutional layer (stride 2, kernel size 3, activation selu, batch norm) + :param ch: int, number of output channels + :param x: tensor, input to the convolutional layer + :return: tensor, output of the convolutional layer + """ with tf.name_scope('downsample'): conv_layer = ConvolutionalLayer( n_output_chns=ch, kernel_size=3, stride=2, - with_bn=True, + feature_normalization='batch', acti_func='selu', w_initializer=self.initializers['w']) return conv_layer(x, is_training=is_training) def convr(ch, x): + """ + Convolutional layer for residuals (stride 1, kernel size 3, activation selu, batch norm) + :param ch: int, number of output channels + :param x: tensor, input to the convolutional layer + :return: tensor, output of the convolutional layer + """ conv_layer = ConvolutionalLayer( n_output_chns=ch, kernel_size=3, - with_bn=True, + feature_normalization='batch', acti_func='selu', w_initializer=self.initializers['w']) return conv_layer(x, is_training=is_training) def conv(ch, x, s): + """ + Convolutional layer (stride 1, kernel size 3, batch norm) + combining two flows + :param ch: int, number of output channels + :param x: tensor, input to the convolutional layer + :param s: flow to be added after convolution + :return: tensor, output of selu activation layer (selu(conv(x) + s)) + """ conv_layer = ConvolutionalLayer( n_output_chns=ch, kernel_size=3, - with_bn=True, + feature_normalization='batch', w_initializer=self.initializers['w']) acti_layer = ActiLayer(func='selu') @@ -212,18 +358,30 @@ def conv(ch, x, s): return acti_layer(res_flow) def down_block(ch, x): + """ + Downsampling block with residual connections + :param ch: int, number of output channels + :param x: tensor, input to the convolutional layer + :return: tensor, output of downsampling + conv + conv + """ with tf.name_scope('down_resnet'): s = down(ch, x) r = convr(ch, s) return conv(ch, r, s) def feature_block(ch, image): + """ + First discriminator processing block + :param ch: int, number of output channels + :param image: tensor, input image to discriminator + :return: tensor, output of conv(image) --> conv --> conv + """ with tf.name_scope('feature'): conv_layer = ConvolutionalLayer( n_output_chns=ch, kernel_size=5, with_bias=True, - with_bn=False, + feature_normalization=None, acti_func='selu', w_initializer=self.initializers['w'], b_initializer=self.initializers['b']) @@ -232,10 +390,16 @@ def feature_block(ch, image): return conv(ch, d_h1r, d_h1s) def fully_connected(ch, features): + """ + Final discriminator processing block + :param ch: int, number of output channels + :param features: tensor, input features for final classification + :return: tensor, output logits of discriminator classification + """ with tf.name_scope('fully_connected'): # with bn? fc_layer = FullyConnectedLayer( - n_output_chns=ch, with_bn=False, with_bias=True) + n_output_chns=ch, feature_normalization=None, with_bias=True) return fc_layer(features, is_training=is_training) if conditioning is not None: diff --git a/niftynet/network/toynet.py b/niftynet/network/toynet.py index 9edaa1c0..d0450b31 100755 --- a/niftynet/network/toynet.py +++ b/niftynet/network/toynet.py @@ -6,6 +6,16 @@ class ToyNet(BaseNet): + """ + ### Description + Toy net for testing + + ### Diagram + INPUT --> CONV(kernel = 3, activation = relu) --> CONV(kernel = 1, activation = None) --> MULTICLASS OUTPUT + + ### Constraints + None + """ def __init__(self, num_classes, w_initializer=None, @@ -14,6 +24,16 @@ def __init__(self, b_regularizer=None, acti_func='prelu', name='ToyNet'): + """ + + :param num_classes: int, number of final output channels + :param w_initializer: weight initialisation for network + :param w_regularizer: weight regularisation for network + :param b_initializer: bias initialisation for network + :param b_regularizer: bias regularisation for network + :param acti_func: ctivation function to use + :param name: layer name + """ super(ToyNet, self).__init__( num_classes=num_classes, @@ -27,6 +47,13 @@ def __init__(self, self.hidden_features = 10 def layer_op(self, images, is_training=True, **unused_kwargs): + """ + + :param images: tensor, input to the network + :param is_training: boolean, True if network is in training mode + :param unused_kwargs: other arguments, not in use + :return: tensor, network output + """ conv_1 = ConvolutionalLayer(self.hidden_features, kernel_size=3, w_initializer=self.initializers['w'], diff --git a/niftynet/network/unet.py b/niftynet/network/unet.py index 6f49c358..b17019fc 100755 --- a/niftynet/network/unet.py +++ b/niftynet/network/unet.py @@ -13,9 +13,31 @@ class UNet3D(TrainableLayer): """ - reimplementation of 3D U-net - Çiçek et al., "3D U-Net: Learning dense Volumetric segmentation from - sparse annotation", MICCAI '16 + ### Description + reimplementation of 3D U-net + Çiçek et al., "3D U-Net: Learning dense Volumetric segmentation from + sparse annotation", MICCAI '16 + + ### Building blocks + [dBLOCK] - Downsampling UNet Block + [uBLOCK] - Upsampling UNet Block + [nBLOCK] - UNet Block with no final operation + [CROP] - Cropping layer + + ### Diagram + + INPUT --> [dBLOCK] - - - - - - - - - - - - - - - - [nBLOCK] --> [CROP] --> OUTPUT + | | + [dBLOCK] - - - - - - - - - - - - [uBLOCK] + | | + [dBLOCK] - - - - - - - [uBLOCK] + | | + --------[uBLOCk] ------ + + ### Constraints + - Image size - 4 should be divisible by 8 + - Label size should be more than 88 + - border is 44 """ def __init__(self, @@ -26,6 +48,16 @@ def __init__(self, b_regularizer=None, acti_func='prelu', name='UNet'): + """ + + :param num_classes: int, number of final output channels + :param w_initializer: weight initialisation for network + :param w_regularizer: weight regularisation for network + :param b_initializer: bias initialisation for network + :param b_regularizer: bias regularisation for network + :param acti_func: activation function to use + :param name: layer name + """ super(UNet3D, self).__init__(name=name) self.n_features = [32, 64, 128, 256, 512] @@ -38,6 +70,14 @@ def __init__(self, print('using {}'.format(name)) def layer_op(self, images, is_training=True, layer_id=-1, **unused_kwargs): + """ + + :param images: tensor, input to the network + :param is_training: boolean, True if network is in training mode + :param layer_id: int, not in use + :param unused_kwargs: other arguments, not in use + :return: tensor, output of the network + """ # image_size should be divisible by 8 assert layer_util.check_spatial_dims(images, lambda x: x % 8 == 0) assert layer_util.check_spatial_dims(images, lambda x: x >= 89) @@ -137,6 +177,17 @@ def __init__(self, with_downsample_branch=False, acti_func='relu', name='UNet_block'): + """ + + :param func: string, type of operation to perform after convolution (Downsampling, Upsampling, None) + :param n_chns: array, number of output channels for each convolutional layer of the block + :param kernels: array, kernel sizes for each convolutional layer of the block + :param w_initializer: weight initialisation of convolutional layers + :param w_regularizer: weight regularisation of convolutional layers + :param with_downsample_branch: boolean, returns also the tensor before func is applied + :param acti_func: activation function to use + :param name: layer name + """ super(UNetBlock, self).__init__(name=name) @@ -151,6 +202,12 @@ def __init__(self, self.regularizers = {'w': w_regularizer} def layer_op(self, input_tensor, is_training): + """ + + :param input_tensor: tensor, input to the UNet block + :param is_training: boolean, True if network is in training mode + :return: output tensor of the UNet block and branch before downsampling (if required) + """ output_tensor = input_tensor for (kernel_size, n_features) in zip(self.kernels, self.n_chns): conv_op = ConvolutionalLayer(n_output_chns=n_features, diff --git a/niftynet/network/unet_2d.py b/niftynet/network/unet_2d.py index 551e189d..6cc183d9 100755 --- a/niftynet/network/unet_2d.py +++ b/niftynet/network/unet_2d.py @@ -15,9 +15,30 @@ class UNet2D(BaseNet): """ - A reimplementation of 2D UNet: - Ronneberger et al., U-Net: Convolutional Networks for Biomedical - Image Segmentation, MICCAI '15 + ### Description + A reimplementation of 2D UNet: + Ronneberger et al., U-Net: Convolutional Networks for Biomedical + Image Segmentation, MICCAI '15 + + ### Building blocks + [dBLOCK] - Downsampling Block (conv 3x3, Relu + conv 3x3, Relu + Max pooling) + [BLOCK] - Two layer Block (conv 3x3, Relu + conv 3x3, Relu) + [uBLOCK] - Upsampling Block (deconv 2x2 + crop and concat + conv 3x3, Relu + conv 3x3, Relu) + [CONV] - Classification block (conv 1x1) + + ### Diagram + + INPUT --> [dBLOCK] - - - - - - - - - - - - - - - - [BLOCK] --> [CONV] --> OUTPUT + | | + [dBLOCK] - - - - - - - - - - - - [uBLOCK] + | | + [dBLOCK] - - - - - - - [uBLOCK] + | | + [dBLOCK] - - - [uBLOCK] + | | + ----[BLOCk] ---- + + ### Constraints """ def __init__(self, @@ -35,7 +56,8 @@ def __init__(self, net_params = {'padding': 'VALID', 'with_bias': True, - 'with_bn': False, + 'feature_normalization': 'batch', + 'group_size': -1, 'acti_func': acti_func, 'w_initializer': w_initializer, 'b_initializer': b_initializer, @@ -50,43 +72,50 @@ def __init__(self, self.deconv_params.update(net_params) def layer_op(self, images, is_training=True, **unused_kwargs): + """ + + :param images: tensor, input to the network + :param is_training: boolean, True if network is in training mode + :param unused_kwargs: other conditional arguments, not in use + :return: tensor, output of the network + """ # contracting path - output_1 = TwoLayerConv(self.n_fea[0], self.conv_params)(images) + output_1 = TwoLayerConv(self.n_fea[0], self.conv_params)(images, is_training=is_training) down_1 = Pooling(func='MAX', **self.pooling_params)(output_1) - output_2 = TwoLayerConv(self.n_fea[1], self.conv_params)(down_1) + output_2 = TwoLayerConv(self.n_fea[1], self.conv_params)(down_1, is_training=is_training) down_2 = Pooling(func='MAX', **self.pooling_params)(output_2) - output_3 = TwoLayerConv(self.n_fea[2], self.conv_params)(down_2) + output_3 = TwoLayerConv(self.n_fea[2], self.conv_params)(down_2, is_training=is_training) down_3 = Pooling(func='MAX', **self.pooling_params)(output_3) - output_4 = TwoLayerConv(self.n_fea[3], self.conv_params)(down_3) + output_4 = TwoLayerConv(self.n_fea[3], self.conv_params)(down_3, is_training=is_training) down_4 = Pooling(func='MAX', **self.pooling_params)(output_4) - output_5 = TwoLayerConv(self.n_fea[4], self.conv_params)(down_4) + output_5 = TwoLayerConv(self.n_fea[4], self.conv_params)(down_4, is_training=is_training) # expansive path - up_4 = DeConv(self.n_fea[3], **self.deconv_params)(output_5) + up_4 = DeConv(self.n_fea[3], **self.deconv_params)(output_5, is_training=is_training) output_4 = CropConcat()(output_4, up_4) - output_4 = TwoLayerConv(self.n_fea[3], self.conv_params)(output_4) + output_4 = TwoLayerConv(self.n_fea[3], self.conv_params)(output_4, is_training=is_training) - up_3 = DeConv(self.n_fea[2], **self.deconv_params)(output_4) + up_3 = DeConv(self.n_fea[2], **self.deconv_params)(output_4, is_training=is_training) output_3 = CropConcat()(output_3, up_3) - output_3 = TwoLayerConv(self.n_fea[2], self.conv_params)(output_3) + output_3 = TwoLayerConv(self.n_fea[2], self.conv_params)(output_3, is_training=is_training) - up_2 = DeConv(self.n_fea[1], **self.deconv_params)(output_3) + up_2 = DeConv(self.n_fea[1], **self.deconv_params)(output_3, is_training=is_training) output_2 = CropConcat()(output_2, up_2) - output_2 = TwoLayerConv(self.n_fea[1], self.conv_params)(output_2) + output_2 = TwoLayerConv(self.n_fea[1], self.conv_params)(output_2, is_training=is_training) - up_1 = DeConv(self.n_fea[0], **self.deconv_params)(output_2) + up_1 = DeConv(self.n_fea[0], **self.deconv_params)(output_2, is_training=is_training) output_1 = CropConcat()(output_1, up_1) - output_1 = TwoLayerConv(self.n_fea[0], self.conv_params)(output_1) + output_1 = TwoLayerConv(self.n_fea[0], self.conv_params)(output_1, is_training=is_training) # classification layer classifier = Conv(n_output_chns=self.num_classes, kernel_size=1, with_bias=True, - with_bn=False) + feature_normalization=None) output_tensor = classifier(output_1) tf.logging.info('output shape %s', output_tensor.shape) return output_tensor @@ -105,9 +134,16 @@ def __init__(self, n_chns, conv_params): self.n_chns = n_chns self.conv_params = conv_params - def layer_op(self, input_tensor): - output_tensor = Conv(self.n_chns, **self.conv_params)(input_tensor) - output_tensor = Conv(self.n_chns, **self.conv_params)(output_tensor) + def layer_op(self, input_tensor, is_training=None): + """ + + :param input_tensor: tensor, input to the two layer convolution + :param is_training: flag for training + :return: tensor, output of --conv--conv + """ + output_tensor = Conv(self.n_chns, **self.conv_params)(input_tensor, is_training=is_training) + output_tensor = Conv(self.n_chns, **self.conv_params)(output_tensor, is_training=is_training) + return output_tensor @@ -128,8 +164,8 @@ def layer_op(self, tensor_a, tensor_b): match the spatial shape and concatenate the tensors tensor_a will be cropped and resized to match tensor_b. - :param tensor_a: - :param tensor_b: + :param tensor_a: tensor, input + :param tensor_b: tensor, input :return: concatenated tensor """ crop_border = (tensor_a.shape[1] - tensor_b.shape[1]) // 2 diff --git a/niftynet/network/vae.py b/niftynet/network/vae.py index 3532eb9c..ce83f309 100755 --- a/niftynet/network/vae.py +++ b/niftynet/network/vae.py @@ -14,13 +14,27 @@ class VAE(TrainableLayer): """ - This is a denoising, convolutional, variational autoencoder (VAE), - composed of a sequence of {convolutions then downsampling} blocks, - followed by a sequence of fully-connected layers, - followed by a sequence of {transpose convolutions then upsampling} blocks. - See Auto-Encoding Variational Bayes, Kingma & Welling, 2014. - 2DO: share the fully-connected parameters - between the mean and logvar decoders. + ### Description + This is a denoising, convolutional, variational autoencoder (VAE), + composed of a sequence of {convolutions then downsampling} blocks, + followed by a sequence of fully-connected layers, + followed by a sequence of {transpose convolutions then upsampling} blocks. + See Auto-Encoding Variational Bayes, Kingma & Welling, 2014. + 2DO: share the fully-connected parameters + between the mean and logvar decoders. + + ### Building Blocks + [ENCODER] - See ConvEncoder class below + [GAUSSIAN SAMPLER] - See GaussianSampler class below + [DECODER] - See ConvDecoder class below + + ### Diagram + + INPUT --> [ENCODER] --> [GAUSSIAN SAMPLER] --> [FCDEC] ---> [DECODER] for means --- OUTPUTS + | | + ------> [DECODER] for logvars --- + + ### Constraints """ def __init__(self, @@ -29,6 +43,14 @@ def __init__(self, b_initializer=None, b_regularizer=None, name='VAE'): + """ + + :param w_initializer: weight initialisation for network + :param w_regularizer: weight regularisation for network + :param b_initializer: bias initialisation for network + :param b_regularizer: bias regularisation for network + :param name: layer name + """ super(VAE, self).__init__(name=name) @@ -130,19 +152,49 @@ def __init__(self, self.regularizers = {'w': w_regularizer, 'b': b_regularizer} def layer_op(self, images, is_training=True, **unused_kwargs): + """ + + :param images: tensor, input to the network + :param is_training: boolean, True if network is in training mode + :param unused_kwargs: other conditional arguments, not in use + :return: posterior_means: means output by gaussian sampler for KL divergence + posterior_logvars: log of variances output by gaussian sampler for KL divergence + data_means: output of means decoder branch (predicted image) + data_logvars: output of variances decoder branch (capturing aleatoric uncertainty) + images: input + data_variances: exp of data_logvars + posterior_variances: exp of posterior_logvars + sample: random sample from latent space (from gaussian sampler) + """ def clip(x): + """ + Clip input tensor using the lower and upper bounds + :param x: tensor, input + :return: clipped tensor + """ return tf.clip_by_value(x, self.logvars_lower_bound, self.logvars_upper_bound) def normalise(x): + """ + Normalise input to [0, 255] + :param x: tensor, input to normalise + :return: normalised tensor in [0, 255] + """ min_val = tf.reduce_min(x) max_val = tf.reduce_max(x) return 255 * (x - min_val) / (max_val - min_val) def infer_downsampled_shape(x, output_channels, pooling_factors): - # Calculate the shape of the data as it emerges from - # the convolutional part of the encoder + """ + Calculate the shape of the data as it emerges from + the convolutional part of the encoder + :param x: tensor, input + :param output_channels: int, number of output channels + :param pooling_factors: array, pooling factors + :return: array, shape of downsampled image + """ downsampled_shape = x.shape[1::].as_list() downsampled_shape[-1] = output_channels[-1] downsampled_shape[0:-1] = \ @@ -259,10 +311,11 @@ def infer_downsampled_shape(x, output_channels, pooling_factors): class ConvEncoder(TrainableLayer): """ + ### Description This is a generic encoder composed of {convolutions then downsampling} blocks followed by fully-connected layers. - """ + """ def __init__(self, denoising_variance, @@ -278,6 +331,22 @@ def __init__(self, b_initializer=None, b_regularizer=None, name='ConvEncoder'): + """ + + :param denoising_variance: variance of gaussian noise to add to the input + :param conv_output_channels: array, number of output channels for each conv layer + :param conv_kernel_sizes: array, kernel sizes for each conv layer + :param conv_pooling_factors: array, stride values for downsampling convolutions + :param acti_func_conv: array, activation functions of each layer + :param layer_sizes_encoder: array, number of output channels for each encoding FC layer + :param acti_func_encoder: array, activation functions for each encoding FC layer + :param serialised_shape: array, flatten shape to enter the FC layers + :param w_initializer: weight initialisation for network + :param w_regularizer: weight regularisation for network + :param b_initializer: bias initialisation for network + :param b_regularizer: bias regularisation for network + :param name: layer name + """ super(ConvEncoder, self).__init__(name=name) @@ -294,6 +363,12 @@ def __init__(self, self.regularizers = {'w': w_regularizer, 'b': b_regularizer} def layer_op(self, images, is_training): + """ + + :param images: tensor, input to the network + :param is_training: boolean, True if network is in training mode + :return: tensor, output of the encoder branch + """ # Define the encoding convolutional layers encoders_cnn = [] @@ -304,7 +379,7 @@ def layer_op(self, images, is_training): kernel_size=self.conv_kernel_sizes[i], padding='SAME', with_bias=True, - with_bn=True, + feature_normalization='batch', w_initializer=self.initializers['w'], w_regularizer=None, acti_func=self.acti_func_conv[i], @@ -319,7 +394,7 @@ def layer_op(self, images, is_training): stride=self.conv_pooling_factors[i], padding='SAME', with_bias=False, - with_bn=True, + feature_normalization='batch', w_initializer=self.initializers['w'], w_regularizer=None, acti_func=self.acti_func_conv[i], @@ -335,7 +410,7 @@ def layer_op(self, images, is_training): encoders_fc.append(FullyConnectedLayer( n_output_chns=self.layer_sizes_encoder[i], with_bias=True, - with_bn=True, + feature_normalization='batch', acti_func=self.acti_func_encoder[i], w_initializer=self.initializers['w'], w_regularizer=self.regularizers['w'], @@ -366,8 +441,14 @@ def layer_op(self, images, is_training): class GaussianSampler(TrainableLayer): """ + ### Description This predicts the mean and logvariance parameters, then generates an approximate sample from the posterior. + + ### Diagram + + ### Constraints + """ def __init__(self, @@ -380,6 +461,18 @@ def __init__(self, b_initializer=None, b_regularizer=None, name='gaussian_sampler'): + """ + + :param number_of_latent_variables: int, number of output channels for FC layer + :param number_of_samples_from_posterior: int, number of samples to draw from standard gaussian + :param logvars_upper_bound: upper bound of log of variances for clipping + :param logvars_lower_bound: lower bound of log of variances for clipping + :param w_initializer: weight initialisation for network + :param w_regularizer: weight regularisation for network + :param b_initializer: bias initialisation for network + :param b_regularizer: bias regularisation for network + :param name: layer name + """ super(GaussianSampler, self).__init__(name=name) @@ -392,6 +485,12 @@ def __init__(self, self.regularizers = {'w': w_regularizer, 'b': b_regularizer} def layer_op(self, codes, is_training): + """ + + :param codes: tensor, input latent space + :param is_training: boolean, True if network is in training mode + :return: samples from posterior distribution, means and log variances of the posterior distribution + """ def clip(input): # This is for clipping logvars, @@ -402,7 +501,7 @@ def clip(input): encoder_means = FullyConnectedLayer( n_output_chns=self.number_of_latent_variables, - with_bn=False, + feature_normalization=None, acti_func=None, w_initializer=self.initializers['w'], w_regularizer=self.regularizers['w'], @@ -411,7 +510,7 @@ def clip(input): encoder_logvars = FullyConnectedLayer( n_output_chns=self.number_of_latent_variables, - with_bn=False, + feature_normalization=None, acti_func=None, w_initializer=self.initializers['w'], w_regularizer=self.regularizers['w'], @@ -441,11 +540,12 @@ def clip(input): class ConvDecoder(TrainableLayer): """ - This is a generic decoder composed of - fully-connected layers followed by - {upsampling then transpose convolution} blocks. - There is no batch normalisation on - the final transpose convolutional layer. + ### Description + This is a generic decoder composed of + fully-connected layers followed by + {upsampling then transpose convolution} blocks. + There is no batch normalisation on + the final transpose convolutional layer. """ def __init__(self, @@ -462,6 +562,22 @@ def __init__(self, b_initializer=None, b_regularizer=None, name='ConvDecoder'): + """ + + :param layer_sizes_decoder: array, number of output channels for each decoding FC layer + :param acti_func_decoder: array, activation functions for each decoding FC layer + :param trans_conv_output_channels: array, number of output channels for each transpose conv layer + :param trans_conv_kernel_sizes: array, kernel sizes for each transpose conv layer + :param trans_conv_unpooling_factors: array, stride values for upsampling transpose convolutions + :param acti_func_trans_conv: array, activation functions for each transpose conv layer + :param upsampling_mode: string, type of upsampling (Deconvolution, channelwise deconvolution, replicate) + :param downsampled_shape: array, final encoded shape before FC in encoder + :param w_initializer: weight initialisation for network + :param w_regularizer: weight regularisation for network + :param b_initializer: bias initialisation for network + :param b_regularizer: bias regularisation for network + :param name: layer name + """ super(ConvDecoder, self).__init__(name=name) @@ -478,6 +594,12 @@ def __init__(self, self.regularizers = {'w': w_regularizer, 'b': b_regularizer} def layer_op(self, codes, is_training): + """ + + :param codes: tensor, input latent space after gaussian sampling + :param is_training: boolean, True if network is in training mode + :return: tensor, output of decoding branch + """ # Define the decoding fully-connected layers decoders_fc = [] @@ -485,7 +607,7 @@ def layer_op(self, codes, is_training): decoders_fc.append(FullyConnectedLayer( n_output_chns=self.layer_sizes_decoder[i], with_bias=True, - with_bn=True, + feature_normalization='batch', acti_func=self.acti_func_decoder[i], w_initializer=self.initializers['w'], w_regularizer=self.regularizers['w'], @@ -503,7 +625,7 @@ def layer_op(self, codes, is_training): stride=self.trans_conv_unpooling_factors[i], padding='SAME', with_bias=True, - with_bn=True, + feature_normalization='batch', w_initializer=self.initializers['w'], w_regularizer=None, acti_func=None, @@ -518,8 +640,8 @@ def layer_op(self, codes, is_training): stride=1, padding='SAME', with_bias=True, - with_bn=True, - #with_bn=not (i == len(self.trans_conv_output_channels) - 1), + feature_normalization='batch', + #feature_normalization=not (i == len(self.trans_conv_output_channels) - 1), # No BN on output w_initializer=self.initializers['w'], w_regularizer=None, @@ -558,8 +680,9 @@ def layer_op(self, codes, is_training): class FCDecoder(TrainableLayer): """ + ### Description This is a generic fully-connected decoder. - """ + """ def __init__(self, layer_sizes_decoder, @@ -569,6 +692,16 @@ def __init__(self, b_initializer=None, b_regularizer=None, name='FCDecoder'): + """ + + :param layer_sizes_decoder: array, number of output channels for each decoding FC layer + :param acti_func_decoder: array, activation functions for each decoding FC layer + :param w_initializer: weight initialisation for network + :param w_regularizer: weight regularisation for network + :param b_initializer: bias initialisation for network + :param b_regularizer: bias regularisation for network + :param name: layer name + """ super(FCDecoder, self).__init__(name=name) @@ -579,6 +712,12 @@ def __init__(self, self.regularizers = {'w': w_regularizer, 'b': b_regularizer} def layer_op(self, codes, is_training): + """ + + :param codes: tensor, input latent codes + :param is_training: boolean, True if network is in training mode + :return: tensor, output of series of FC layers + """ # Define the decoding fully-connected layers decoders_fc = [] @@ -586,7 +725,7 @@ def layer_op(self, codes, is_training): decoders_fc.append(FullyConnectedLayer( n_output_chns=self.layer_sizes_decoder[i], with_bias=True, - with_bn=True, + feature_normalization='batch', acti_func=self.acti_func_decoder[i], w_initializer=self.initializers['w'], w_regularizer=self.regularizers['w'], diff --git a/niftynet/network/vnet.py b/niftynet/network/vnet.py index 1f644bfa..69a447ac 100755 --- a/niftynet/network/vnet.py +++ b/niftynet/network/vnet.py @@ -15,9 +15,41 @@ class VNet(BaseNet): """ - implementation of V-Net: - Milletari et al., "V-Net: Fully convolutional neural networks for - volumetric medical image segmentation", 3DV '16 + ### Description + implementation of V-Net: + Milletari et al., "V-Net: Fully convolutional neural networks for + volumetric medical image segmentation", 3DV '16 + + ### Building Blocks + (n)[dBLOCK] - Downsampling VNet block with n conv layers (kernel size = 5, + with residual connections, activation = relu as default) + followed by downsampling conv layer (kernel size = 2, + stride = 2) + final activation + (n)[uBLOCK] - Upsampling VNet block with n conv layers (kernel size = 5, + with residual connections, activation = relu as default) + followed by deconv layer (kernel size = 2, + stride = 2) + final activation + (n)[sBLOCK] - VNet block with n conv layers (kernel size = 5, + with residual connections, activation = relu as default) + followed by 1x1x1 conv layer (kernel size = 1, + stride = 1) + final activation + + ### Diagram + + INPUT --> (1)[dBLOCK] - - - - - - - - - - - - - - - - (1)[sBLOCK] --> OUTPUT + | | + (2)[dBLOCK] - - - - - - - - - - - - (2)[uBLOCK] + | | + (3)[dBLOCK] - - - - - - - (3)[uBLOCK] + | | + (3)[dBLOCK] - - - (3)[uBLOCK] + | | + ----(3)[uBLOCk] ---- + + + ### Constraints + - Input size should be divisible by 8 + - Input should be either 2D or 3D """ def __init__(self, @@ -28,6 +60,16 @@ def __init__(self, b_regularizer=None, acti_func='relu', name='VNet'): + """ + + :param num_classes: int, number of channels of output + :param w_initializer: weight initialisation for network + :param w_regularizer: weight regularisation for network + :param b_initializer: bias initialisation for network + :param b_regularizer: bias regularisation for network + :param acti_func: activation function to use + :param name: layer name + """ super(VNet, self).__init__( num_classes=num_classes, @@ -41,6 +83,14 @@ def __init__(self, self.n_features = [16, 32, 64, 128, 256] def layer_op(self, images, is_training=True, layer_id=-1, **unused_kwargs): + """ + + :param images: tensor to input to the network. Size has to be divisible by 8 + :param is_training: boolean, True if network is in training mode + :param layer_id: not in use + :param unused_kwargs: other conditional arguments, not in use + :return: tensor, network output + """ assert layer_util.check_spatial_dims(images, lambda x: x % 8 == 0) if layer_util.infer_spatial_rank(images) == 2: @@ -137,6 +187,19 @@ def __init__(self, b_regularizer=None, acti_func='relu', name='vnet_block'): + """ + + :param func: string, defines final block operation (Downsampling, upsampling, same) + :param n_conv: int, number of conv layers to apply + :param n_feature_chns: int, number of feature channels (output channels) for each conv layer + :param n_output_chns: int, number of output channels of the final block operation (func) + :param w_initializer: weight initialisation of convolutional layers + :param w_regularizer: weight regularisation of convolutional layers + :param b_initializer: bias initialisation of convolutional layers + :param b_regularizer: bias regularisation of convolutional layers + :param acti_func: activation function to use + :param name: layer name + """ super(VNetBlock, self).__init__(name=name) @@ -150,6 +213,13 @@ def __init__(self, self.regularizers = {'w': w_regularizer, 'b': b_regularizer} def layer_op(self, main_flow, bypass_flow): + """ + + :param main_flow: tensor, input to the VNet block + :param bypass_flow: tensor, input from skip connection + :return: res_flow is tensor before final block operation (for residual connections), + main_flow is final output tensor + """ for i in range(self.n_conv): main_flow = ConvLayer(name='conv_{}'.format(i), n_output_chns=self.n_feature_chns, diff --git a/niftynet/utilities/filename_matching.py b/niftynet/utilities/filename_matching.py index 191e3cd3..177cfb6e 100755 --- a/niftynet/utilities/filename_matching.py +++ b/niftynet/utilities/filename_matching.py @@ -110,6 +110,10 @@ def matching_subjects_and_filenames(self): [os.path.join(p, filename) for p, filename in matching_path_file] subjectname_list = [self.__extract_subject_id_from(filename) for p, filename in matching_path_file] + for sname, fname in zip(subjectname_list, filename_list): + if not sname: + subjectname_list.remove(sname) + filename_list.remove(fname) self.__check_unique_names(filename_list, subjectname_list) if not filename_list or not subjectname_list: tf.logging.fatal('no file matched based on this matcher: %s', self) @@ -161,6 +165,8 @@ def __extract_subject_id_from(self, fullname): def __check_unique_names(self, file_list, id_list): uniq_dict = dict() for idx, subject_id in enumerate(id_list): + if not subject_id: + continue id_string = subject_id[0] if id_string in uniq_dict: tf.logging.fatal( diff --git a/niftynet/utilities/user_parameters_custom.py b/niftynet/utilities/user_parameters_custom.py index 77fbe4cc..df0e22e0 100755 --- a/niftynet/utilities/user_parameters_custom.py +++ b/niftynet/utilities/user_parameters_custom.py @@ -2,14 +2,11 @@ """ This module defines task specific parameters """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals +from __future__ import (absolute_import, division, print_function, + unicode_literals) -from niftynet.utilities.user_parameters_helper import add_input_name_args -from niftynet.utilities.user_parameters_helper import int_array -from niftynet.utilities.user_parameters_helper import str2boolean +from niftynet.utilities.user_parameters_helper import (add_input_name_args, + int_array, str2boolean) ####################################################################### @@ -45,8 +42,7 @@ def add_customised_args(parser, task_name): task_name = task_name.upper() if task_name in SUPPORTED_ARG_SECTIONS: return SUPPORTED_ARG_SECTIONS[task_name](parser) - else: - raise NotImplementedError + raise NotImplementedError def __add_regression_args(parser): @@ -121,8 +117,7 @@ def __add_segmentation_args(parser): "selective sampler", metavar='', type=float, - default=0 - ) + default=0) # for selective sampling only parser.add_argument( @@ -131,8 +126,7 @@ def __add_segmentation_args(parser): "selective sampling", metavar='', type=int_array, - default=(0, 1) - ) + default=(0, 1)) # for selective sampling only parser.add_argument( @@ -141,8 +135,7 @@ def __add_segmentation_args(parser): "when using selective sampler", metavar='', type=int, - default=0 - ) + default=0) # for selective sampling only parser.add_argument( @@ -151,8 +144,7 @@ def __add_segmentation_args(parser): "selective sampler", metavar='', type=int, - default=1 - ) + default=1) # for selective sampling only parser.add_argument( @@ -161,8 +153,7 @@ def __add_segmentation_args(parser): "selective sampler", metavar='', type=str2boolean, - default=True - ) + default=True) parser.add_argument( "--evaluation_units", @@ -171,6 +162,29 @@ def __add_segmentation_args(parser): choices=['foreground', 'label', 'cc'], default='foreground') + # for mixup augmentation + parser.add_argument( + "--do_mixup", + help="Use the 'mixup' option.", + type=str2boolean, + default=False) + + # for mixup augmentation + parser.add_argument( + "--mixup_alpha", + help="The alpha value to parametrise the beta distribution " + "(alpha, alpha). Default: 0.2.", + type=float, + default=0.2) + + # for mixup augmentation + parser.add_argument( + "--mix_match", + help="If true, matches bigger segmentations with " + "smaller segmentations.", + type=str2boolean, + default=False) + from niftynet.application.segmentation_application import SUPPORTED_INPUT parser = add_input_name_args(parser, SUPPORTED_INPUT) return parser @@ -231,7 +245,6 @@ def __add_classification_args(parser): type=str2boolean, default=False) - from niftynet.application.classification_application import SUPPORTED_INPUT parser = add_input_name_args(parser, SUPPORTED_INPUT) return parser diff --git a/niftynet/utilities/user_parameters_default.py b/niftynet/utilities/user_parameters_default.py index 28271997..5ba8ce60 100755 --- a/niftynet/utilities/user_parameters_default.py +++ b/niftynet/utilities/user_parameters_default.py @@ -2,22 +2,17 @@ """ This module defines niftynet parameters and their defaults. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals +from __future__ import (absolute_import, division, print_function, + unicode_literals) import os +from niftynet.engine.image_window_dataset import SMALLER_FINAL_BATCH_MODE from niftynet.io.image_loader import SUPPORTED_LOADERS from niftynet.io.image_sets_partitioner import SUPPORTED_PHASES -from niftynet.engine.image_window_dataset import SMALLER_FINAL_BATCH_MODE -from niftynet.utilities.user_parameters_helper import float_array -from niftynet.utilities.user_parameters_helper import int_array -from niftynet.utilities.user_parameters_helper import spatial_atleast3d -from niftynet.utilities.user_parameters_helper import spatialnumarray -from niftynet.utilities.user_parameters_helper import str2boolean -from niftynet.utilities.user_parameters_helper import str_array +from niftynet.utilities.user_parameters_helper import ( + float_array, int_array, spatial_atleast3d, spatialnumarray, str2boolean, + str_array) from niftynet.utilities.util_import import require_module DEFAULT_INFERENCE_OUTPUT = os.path.join('.', 'output') @@ -25,14 +20,10 @@ DEFAULT_DATASET_SPLIT_FILE = os.path.join('.', 'dataset_split.csv') DEFAULT_HISTOGRAM_REF_FILE = os.path.join('.', 'histogram_ref_file.txt') DEFAULT_MODEL_DIR = None -DEFAULT_EVENT_HANDLERS = ( - 'model_saver', - 'model_restorer', - 'sampler_threading', - 'apply_gradients', - 'output_interpreter', - 'console_logger', - 'tensorboard_logger') +DEFAULT_EVENT_HANDLERS = ('model_saver', 'model_restorer', 'sampler_threading', + 'apply_gradients', 'output_interpreter', + 'console_logger', 'tensorboard_logger', + 'performance_logger') DEFAULT_ITERATION_GENERATOR = 'iteration_generator' @@ -91,6 +82,7 @@ def add_application_args(parser): help='String representing an iteration generator class', type=str, default=DEFAULT_ITERATION_GENERATOR) + return parser @@ -148,6 +140,13 @@ def add_inference_args(parser): type=spatialnumarray, default=(0, 0, 0)) + parser.add_argument( + "--fill_constant", + help="[Inference only] Output fill value " + "used fill borders of output images.", + type=float, + default=0.0) + return parser @@ -187,6 +186,22 @@ def add_input_data_args(parser): help="Input list of subjects in csv files", default='') + parser.add_argument( + "--csv_data_file", + metavar='', + type=str, + help="Path to a csv with data; labels, features or coordinates for" + "the patch based sampler", + default='') + + parser.add_argument( + "--to_ohe", + help="Indicates if the data provided in the csv should be " + "one-hot-encoded." + "This is only valid when the csv_data_file has 2 columns", + type=str2boolean, + default=False) + parser.add_argument( "--path_to_search", metavar='', @@ -323,15 +338,31 @@ def add_network_args(parser): type=str, default='minimum') + parser.add_argument( + "--volume_padding_to_size", + help="Choose size to pad all input volumes to. Any dimensions " + "that exceed the desired size will be kept the same. Default: " + "(0, ) which indicates not to use this mode. ", + type=spatialnumarray, + default=(0,) + ) + parser.add_argument( "--window_sampling", metavar='TYPE_STR', help="How to sample patches from each loaded image:" " 'uniform': fixed size uniformly distributed," " 'resize': resize image to the patch size.", - choices=['uniform', 'resize', 'balanced', 'weighted'], + choices=['uniform', 'resize', 'balanced', 'weighted', 'patch'], default='uniform') + parser.add_argument( + "--force_output_identity_resizing", + metavar=str2boolean, + help="Forces the shape of the inferred output to match the " + "input label shape rather than be resized to input image shape.", + default=False) + parser.add_argument( "--queue_length", help="Set size of preprocessing buffer queue", @@ -370,8 +401,7 @@ def add_network_args(parser): parser.add_argument( "--foreground_type", - choices=list( - niftynet.layer.binary_masking.SUPPORTED_MASK_TYPES), + choices=list(niftynet.layer.binary_masking.SUPPORTED_MASK_TYPES), help="type_str of foreground masking strategy used", default='otsu_plus') @@ -381,6 +411,12 @@ def add_network_args(parser): type=str2boolean, default=False) + parser.add_argument( + "--rgb_normalisation", + help="Indicates if RGB histogram equilisation should be performed", + type=str2boolean, + default=False) + parser.add_argument( "--whitening", help="Indicates if the whitening of the data should be applied", @@ -484,6 +520,13 @@ def add_training_args(parser): type=float_array, default=()) + parser.add_argument( + "--isotropic_scaling", + help="Indicates if the same random scaling factor should be applied " + "to each dimension", + type=str2boolean, + default=False) + parser.add_argument( "--antialiasing", help="Indicates if antialiasing must be performed " @@ -621,6 +664,22 @@ def add_training_args(parser): type=str, default='') + parser.add_argument( + "--patience", + metavar='', + help='Number of iterations to wait before starting ' + 'performance monitoring', + type=int, + default=100) + + parser.add_argument( + "--early_stopping_mode", + metavar='', + help="Choose between {'mean', 'robust_mean', 'median', " + "'generalisation_loss', 'median_smoothing', 'validation_up'}", + type=str, + default='mean') + return parser diff --git a/niftynet/utilities/user_parameters_helper.py b/niftynet/utilities/user_parameters_helper.py index b6fa90ae..aafab649 100755 --- a/niftynet/utilities/user_parameters_helper.py +++ b/niftynet/utilities/user_parameters_helper.py @@ -26,11 +26,10 @@ def str2boolean(string_input): """ if string_input.lower() in TRUE_VALUE: return True - elif string_input.lower() in FALSE_VALUE: + if string_input.lower() in FALSE_VALUE: return False - else: - raise argparse.ArgumentTypeError( - 'Boolean value expected, received {}'.format(string_input)) + raise argparse.ArgumentTypeError( + 'Boolean value expected, received {}'.format(string_input)) def int_array(string_input): diff --git a/niftynet/utilities/user_parameters_parser.py b/niftynet/utilities/user_parameters_parser.py index ab38d1a4..48fb7bcf 100755 --- a/niftynet/utilities/user_parameters_parser.py +++ b/niftynet/utilities/user_parameters_parser.py @@ -313,7 +313,6 @@ def _raises_bad_keys(keys, error_info='config file'): 'did you mean "{1}"?\n "{0}" is ' 'not a valid option.{2}'.format( key, closest, EPILOG_STRING, error_info)) - return def __resolve_config_file_path(cmdline_arg): diff --git a/niftynet/utilities/user_parameters_regex.py b/niftynet/utilities/user_parameters_regex.py index 337dbdcc..51046d01 100755 --- a/niftynet/utilities/user_parameters_regex.py +++ b/niftynet/utilities/user_parameters_regex.py @@ -72,7 +72,5 @@ def match_array(string_input, type_str): return tuple(float(val) for val in values) if type_str == 'str': return tuple(values) - else: - raise ValueError("unknown array type_str {}".format(string_input)) - else: - raise ValueError("invalid parameter {}".format(string_input)) + raise ValueError("unknown array type_str {}".format(string_input)) + raise ValueError("invalid parameter {}".format(string_input)) diff --git a/requirements-cpu.txt b/requirements-cpu.txt index 6e44117a..a4392879 100644 --- a/requirements-cpu.txt +++ b/requirements-cpu.txt @@ -1,9 +1,9 @@ six>=1.10 nibabel>=2.1.0 -numpy>=1.13.3, <= 1.14.5 +numpy>=1.13.3 scipy>=0.18 configparser -tensorflow==1.12 +tensorflow>=1.13.2, <=1.14 pandas pillow blinker diff --git a/requirements-gpu.txt b/requirements-gpu.txt index 75502c25..1ccbc5a5 100644 --- a/requirements-gpu.txt +++ b/requirements-gpu.txt @@ -1,9 +1,9 @@ six>=1.10 nibabel>=2.1.0 -numpy>=1.13.3, <= 1.14.5 +numpy>=1.13.3 scipy>=0.18 configparser -tensorflow-gpu==1.12 +tensorflow-gpu>=1.13.2, <=1.14 pandas pillow blinker diff --git a/run_test.sh b/run_test.sh index f1ff4596..1b7b6a75 100755 --- a/run_test.sh +++ b/run_test.sh @@ -1,7 +1,12 @@ wget -q https://www.dropbox.com/s/lioecnpv82r5n6e/example_volumes_v0_2.tar.gz tar -xzvf example_volumes_v0_2.tar.gz +rm example_volumes_v0_2.tar.gz wget -N https://www.dropbox.com/s/p7b3t2c3mewtree/testing_data_v0_2.tar.gz tar -xzvf testing_data_v0_2.tar.gz +rm testing_data_v0_2.tar.gz +wget -N https://www.dropbox.com/s/gt0hm6o61rlsfcc/csv_data.tar.gz +tar -C data -xzvf csv_data.tar.gz +rm csv_data.tar.gz python -m unittest discover -s "tests" -p "*_test.py" diff --git a/setup.py b/setup.py index e53f71d8..3c062a85 100755 --- a/setup.py +++ b/setup.py @@ -64,7 +64,7 @@ install_requires=[ 'six>=1.10', 'nibabel>=2.1.0', - 'numpy>=1.13.3, <= 1.14.5', + 'numpy>=1.13.3', 'scipy>=0.18', 'configparser', 'pandas', diff --git a/tests/activation_test.py b/tests/activation_test.py index 42b7e180..c23165e5 100755 --- a/tests/activation_test.py +++ b/tests/activation_test.py @@ -5,9 +5,9 @@ from tensorflow.contrib.layers.python.layers import regularizers from niftynet.layer.activation import ActiLayer +from tests.niftynet_testcase import NiftyNetTestCase - -class ActivationTest(tf.test.TestCase): +class ActivationTest(NiftyNetTestCase): def get_3d_input(self): input_shape = (2, 16, 16, 16, 8) x = tf.ones(input_shape) @@ -26,7 +26,7 @@ def run_test(self, is_3d, type_str, expected_shape): activation_layer = ActiLayer(func=type_str) out_acti = activation_layer(x) print(activation_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out_acti) self.assertAllClose(out.shape, expected_shape) @@ -66,7 +66,7 @@ def test_3d_prelu_reg_shape(self): name='regularized') out_prelu = prelu_layer(x) print(prelu_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out_prelu) self.assertAllClose((2, 16, 16, 16, 8), out.shape) @@ -76,7 +76,7 @@ def test_3d_dropout_shape(self): dropout_layer = ActiLayer(func='dropout') out_dropout = dropout_layer(x, keep_prob=0.8) print(dropout_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out_dropout) self.assertAllClose((2, 16, 16, 16, 8), out.shape) @@ -119,7 +119,7 @@ def test_2d_prelu_reg_shape(self): name='regularized') out_prelu = prelu_layer(x) print(prelu_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out_prelu) self.assertAllClose((2, 16, 16, 8), out.shape) @@ -129,7 +129,7 @@ def test_2d_dropout_shape(self): dropout_layer = ActiLayer(func='dropout') out_dropout = dropout_layer(x, keep_prob=0.8) print(dropout_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out_dropout) self.assertAllClose((2, 16, 16, 8), out.shape) diff --git a/tests/additive_upsample_test.py b/tests/additive_upsample_test.py index bb8e0bb2..a02943d5 100755 --- a/tests/additive_upsample_test.py +++ b/tests/additive_upsample_test.py @@ -4,6 +4,7 @@ import tensorflow as tf from niftynet.layer.additive_upsample import AdditiveUpsampleLayer +from tests.niftynet_testcase import NiftyNetTestCase def get_3d_input(): input_shape = (2, 16, 16, 16, 4) @@ -15,7 +16,7 @@ def get_2d_input(): x = tf.ones(input_shape) return x -class AdditiveUpsampleTest(tf.test.TestCase): +class AdditiveUpsampleTest(NiftyNetTestCase): def run_test(self, new_size, n_splits, expected_shape, is_3d=True): if is_3d: x = get_3d_input() @@ -26,7 +27,7 @@ def run_test(self, new_size, n_splits, expected_shape, is_3d=True): new_size=new_size, n_splits=n_splits) resized = resize_layer(x) print(resize_layer) - with self.test_session() as sess: + with self.cached_session() as sess: out = sess.run(resized) self.assertAllClose(out.shape, expected_shape) diff --git a/tests/affine_augmentation_test.py b/tests/affine_augmentation_test.py index 0343d09b..786c6557 100755 --- a/tests/affine_augmentation_test.py +++ b/tests/affine_augmentation_test.py @@ -5,9 +5,9 @@ import tensorflow as tf from niftynet.layer.affine_augmentation import AffineAugmentationLayer +from tests.niftynet_testcase import NiftyNetTestCase - -class RandRotationTest(tf.test.TestCase): +class RandRotationTest(NiftyNetTestCase): def get_3d_image(self): image_shape = (1, 200, 200, 1) elements = tf.range(np.prod(image_shape), dtype=tf.float32) @@ -28,7 +28,7 @@ def test_2d_shape(self): inverse_augment_layer = augment_layer.inverse() inverse = inverse_augment_layer(deformed) - with self.test_session() as sess: + with self.cached_session() as sess: test_out = sess.run([input_tensor, deformed, inverse]) original, deformed_image, resumed_image = test_out to_compare = resumed_image > 0 @@ -49,7 +49,7 @@ def test_3d_shape(self): inverse = inverse_augment_layer(deformed) # with tf.Session() as sess: - with self.test_session() as sess: + with self.cached_session() as sess: test_out = sess.run([input_tensor, deformed, inverse]) original, deformed_image, resumed_image = test_out to_compare = resumed_image > 0 diff --git a/tests/application_driver_test.py b/tests/application_driver_test.py index 56125226..1a6c5609 100755 --- a/tests/application_driver_test.py +++ b/tests/application_driver_test.py @@ -13,7 +13,7 @@ from niftynet.io.misc_io import set_logger from niftynet.utilities.util_common import ParserNamespace from niftynet.engine.signal import SESS_FINISHED, SESS_STARTED - +from tests.niftynet_testcase import NiftyNetTestCase # def _run_test_application(): # test_driver = get_initialised_driver() @@ -58,6 +58,7 @@ def get_initialised_driver(starting_iter=0, exclude_fraction_for_validation=0.1, exclude_fraction_for_inference=0.1, vars_to_restore=vars_to_restore, + patience=100, lr=0.01), 'CUSTOM': ParserNamespace( vector_size=100, @@ -74,7 +75,7 @@ def get_initialised_driver(starting_iter=0, return app_driver -class ApplicationDriverTest(tf.test.TestCase): +class ApplicationDriverTest(NiftyNetTestCase): def test_wrong_init(self): app_driver = ApplicationDriver() with self.assertRaisesRegexp(AttributeError, ''): @@ -94,7 +95,7 @@ def test_wrong_init(self): # test_driver = get_initialised_driver() # graph = test_driver.create_graph( # test_driver.app, test_driver.num_gpus, True) - # with self.test_session(graph=graph) as sess: + # with self.cached_session(graph=graph) as sess: # sess.run(global_vars_init_or_restore()) # GRAPH_CREATED.send(test_driver.app, iter_msg=None) # SESS_STARTED.send(test_driver.app, iter_msg=None) @@ -111,7 +112,7 @@ def test_wrong_init(self): def test_training_update(self): test_driver = get_initialised_driver() graph = test_driver.create_graph(test_driver.app, 1, True) - with self.test_session(graph=graph) as sess: + with self.cached_session(graph=graph) as sess: SESS_STARTED.send(test_driver.app, iter_msg=None) train_op = test_driver.app.gradient_op @@ -130,7 +131,7 @@ def test_multi_device_inputs(self): test_driver = get_initialised_driver() graph = test_driver.create_graph( test_driver.app, test_driver.num_gpus, True) - with self.test_session(graph=graph) as sess: + with self.cached_session(graph=graph) as sess: SESS_STARTED.send(test_driver.app, iter_msg=None) for i in range(2): sess.run(test_driver.app.gradient_op) @@ -158,7 +159,7 @@ def test_multi_device_gradients(self): test_driver = get_initialised_driver() graph = test_driver.create_graph( test_driver.app, test_driver.num_gpus, True) - with self.test_session(graph=graph) as sess: + with self.cached_session(graph=graph) as sess: SESS_STARTED.send(test_driver.app, iter_msg=None) for i in range(2): sess.run(test_driver.app.gradient_op) @@ -183,7 +184,7 @@ def test_multi_device_multi_optimiser_gradients(self): application='tests.toy_application.ToyApplicationMultOpti') graph = test_driver.create_graph( test_driver.app, test_driver.num_gpus, True) - with self.test_session(graph=graph) as sess: + with self.cached_session(graph=graph) as sess: SESS_STARTED.send(test_driver.app, iter_msg=None) for i in range(2): sess.run(test_driver.app.gradient_op) @@ -238,7 +239,7 @@ def check_gradients(self, g_0, g_1, g_2, g_3, g_ave): def test_rand_initialisation(self): test_driver = get_initialised_driver(0, True) graph = test_driver.create_graph(test_driver.app, 1, True) - with self.test_session(graph=graph) as sess: + with self.cached_session(graph=graph) as sess: test_tensor = graph.get_tensor_by_name( "G/conv_bn_selu/conv_/w:0") with self.assertRaisesRegexp( @@ -258,7 +259,7 @@ def test_from_latest_file_initialisation(self): -0.10836645, 0.06488426, 0.0746650, -0.188567, -0.64652514]], dtype=np.float32) graph = test_driver.create_graph(test_driver.app, 1, True) - with self.test_session(graph=graph) as sess: + with self.cached_session(graph=graph) as sess: test_tensor = graph.get_tensor_by_name( "G/conv_bn_selu/conv_/w:0") with self.assertRaisesRegexp( @@ -273,7 +274,7 @@ def test_from_latest_file_initialisation(self): # def test_not_found_file_initialisation(self): # test_driver = get_initialised_driver(42, False) # graph = test_driver.create_graph(test_driver.app, 1, True) - # with self.test_session(graph=graph) as sess: + # with self.cached_session(graph=graph) as sess: # with self.assertRaisesRegexp( # ValueError, ''): # ModelRestorer(**vars(test_driver)).restore_model(None) @@ -290,7 +291,7 @@ def test_from_file_initialisation(self): -0.43854219, 0.40412974, 0.0396539, -0.1590578, -0.53759819]], dtype=np.float32) graph = test_driver.create_graph(test_driver.app, 1, True) - with self.test_session(graph=graph) as sess: + with self.cached_session(graph=graph) as sess: test_tensor = graph.get_tensor_by_name( "G/conv_bn_selu/conv_/w:0") with self.assertRaisesRegexp( @@ -312,7 +313,7 @@ def test_from_file_finetuning(self): -0.43854219, 0.40412974, 0.0396539, -0.1590578, -0.53759819]], dtype=np.float32) graph = test_driver.create_graph(test_driver.app, 1, True) - with self.test_session(graph=graph) as sess: + with self.cached_session(graph=graph) as sess: test_tensor = graph.get_tensor_by_name( "G/conv_bn_selu/conv_/w:0") test_negative_tensor = graph.get_tensor_by_name( diff --git a/tests/application_factory_test.py b/tests/application_factory_test.py index c36f4f45..3f803a39 100755 --- a/tests/application_factory_test.py +++ b/tests/application_factory_test.py @@ -2,9 +2,9 @@ import tensorflow as tf import niftynet.engine.application_factory as Factory +from tests.niftynet_testcase import NiftyNetTestCase - -class FactoryTest(tf.test.TestCase): +class FactoryTest(NiftyNetTestCase): def test_import(self): var_names = [ item for item in list(dir(Factory)) if item.startswith("SUPPORTED")] diff --git a/tests/approximated_smoothing_test.py b/tests/approximated_smoothing_test.py index 225e78c3..d55ed539 100755 --- a/tests/approximated_smoothing_test.py +++ b/tests/approximated_smoothing_test.py @@ -5,9 +5,9 @@ import tensorflow as tf from niftynet.layer.approximated_smoothing import SmoothingLayer as Smoothing +from tests.niftynet_testcase import NiftyNetTestCase - -class SmoothingTest(tf.test.TestCase): +class SmoothingTest(NiftyNetTestCase): def get_1d_input(self): input_shape = (2, 16, 8) x = tf.ones(input_shape) @@ -34,7 +34,7 @@ def run_test(self, ndim, sigma, type_str): smoothing_layer = Smoothing(sigma=sigma, type_str=type_str) smoothed = smoothing_layer(x) print(smoothing_layer) - with self.test_session() as sess: + with self.cached_session() as sess: out = sess.run(smoothed) self.assertAllClose(out.shape, x.shape.as_list()) return out diff --git a/tests/binary_masking_test.py b/tests/binary_masking_test.py index 0ca88692..845d19f2 100755 --- a/tests/binary_masking_test.py +++ b/tests/binary_masking_test.py @@ -4,9 +4,10 @@ import tensorflow as tf from niftynet.layer.binary_masking import BinaryMaskingLayer +from tests.niftynet_testcase import NiftyNetTestCase -class BinaryMaskingTest(tf.test.TestCase): +class BinaryMaskingTest(NiftyNetTestCase): def get_3d_input(self): input_shape = (16, 16, 16) x = np.random.randint(-10, 10, size=input_shape) diff --git a/tests/bn_test.py b/tests/bn_test.py index d5a859d9..e6a9469f 100755 --- a/tests/bn_test.py +++ b/tests/bn_test.py @@ -6,9 +6,10 @@ from niftynet.layer.bn import BNLayer from niftynet.layer.bn import InstanceNormLayer +from tests.niftynet_testcase import NiftyNetTestCase -class BNTest(tf.test.TestCase): +class BNTest(NiftyNetTestCase): def get_3d_input(self): input_shape = (2, 16, 16, 16, 8) x = tf.ones(input_shape) @@ -26,7 +27,7 @@ def test_3d_bn_shape(self): out_bn = bn_layer(x, is_training=True) print(bn_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out_bn) @@ -41,7 +42,7 @@ def test_3d_instnorm_shape(self): out_inst = instnorm_layer(x) print(instnorm_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out_inst) @@ -56,7 +57,7 @@ def test_3d_bn_reg_shape(self): test_bn = bn_layer(x, is_training=False) print(bn_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out_bn) @@ -74,7 +75,7 @@ def test_2d_bn_shape(self): out_bn = bn_layer(x, is_training=True) print(bn_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out_bn) @@ -88,7 +89,7 @@ def test_2d_instnorm_shape(self): out_inst = instnorm_layer(x) print(instnorm_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out_inst) @@ -104,7 +105,7 @@ def test_2d_bn_reg_shape(self): print(bn_layer) reg_loss = tf.add_n(bn_layer.regularizer_loss()) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out_bn) diff --git a/tests/channel_sparse_convolution_test.py b/tests/channel_sparse_convolution_test.py index ecf55f17..ded526bd 100755 --- a/tests/channel_sparse_convolution_test.py +++ b/tests/channel_sparse_convolution_test.py @@ -4,6 +4,7 @@ import numpy as np from niftynet.layer.channel_sparse_convolution import ChannelSparseConvolutionalLayer +from tests.niftynet_testcase import NiftyNetTestCase from tensorflow.core.protobuf import config_pb2 from tensorflow.core.protobuf import rewriter_config_pb2 @@ -17,13 +18,13 @@ def get_config(): return config -class ChannelSparseConvolutionalLayerTest(tf.test.TestCase): +class ChannelSparseConvolutionalLayerTest(NiftyNetTestCase): def test_3d_shape(self): x = tf.random_normal(shape=[2,4,5,6,4]) conv1 = ChannelSparseConvolutionalLayer(4) conv2 = ChannelSparseConvolutionalLayer(8, kernel_size=[1,1,3]) conv3 = ChannelSparseConvolutionalLayer(4, acti_func='relu') - conv4 = ChannelSparseConvolutionalLayer(8, with_bn=False) + conv4 = ChannelSparseConvolutionalLayer(8, feature_normalization=None) conv5 = ChannelSparseConvolutionalLayer(4, with_bias=True) x1, mask1=conv1(x, None, True, 1.) x2, mask2=conv2(x1, mask1, True, 1.) @@ -31,7 +32,7 @@ def test_3d_shape(self): x4, mask4=conv4(x3, mask3, True, .75) x5, mask5=conv5(x4, mask4, True, 1.) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out1, out2, out3, out4, out5 = sess.run([x1,x2,x3,x4,x5]) self.assertAllClose([2,4,5,6,4], out1.shape) @@ -45,7 +46,7 @@ def test_2d_shape(self): conv1 = ChannelSparseConvolutionalLayer(4) conv2 = ChannelSparseConvolutionalLayer(8, kernel_size=[1,1,3]) conv3 = ChannelSparseConvolutionalLayer(4, acti_func='relu') - conv4 = ChannelSparseConvolutionalLayer(8, with_bn=False) + conv4 = ChannelSparseConvolutionalLayer(8, feature_normalization=None) conv5 = ChannelSparseConvolutionalLayer(4, with_bias=True) x1, mask1=conv1(x, None, True, 1.) x2, mask2=conv2(x1, mask1, True, 1.) @@ -53,7 +54,7 @@ def test_2d_shape(self): x4, mask4=conv4(x3, mask3, True, .75) x5, mask5=conv5(x4, mask4, True, 1.) - with self.test_session(config=get_config()) as sess: + with self.cached_session(config=get_config()) as sess: sess.run(tf.global_variables_initializer()) out1, out2, out3, out4, out5 = sess.run([x1,x2,x3,x4,x5]) self.assertAllClose([2,4,5,4], out1.shape) @@ -73,7 +74,7 @@ def test_masks(self): x3, mask3=conv3(x2, mask2, True, .2) x4, mask4=conv4(x3, mask3, True, 1.) - with self.test_session(config=get_config()) as sess: + with self.cached_session(config=get_config()) as sess: sess.run(tf.global_variables_initializer()) out1, out2, out3, out4 = sess.run([mask1, mask2, mask3, mask4]) self.assertAllClose([10, 5, 2, 10], [np.sum(out1), diff --git a/tests/classification_evaluator_test.py b/tests/classification_evaluator_test.py index f4d0fe50..41a34409 100755 --- a/tests/classification_evaluator_test.py +++ b/tests/classification_evaluator_test.py @@ -7,8 +7,9 @@ from niftynet.evaluation.classification_evaluator import ClassificationEvaluator from niftynet.io.misc_io import set_logger +from tests.niftynet_testcase import NiftyNetTestCase -class ClassificationEvaluatorTest(tf.test.TestCase): +class ClassificationEvaluatorTest(NiftyNetTestCase): def test_basic(self): class NS(object): def __init__(self, dict): diff --git a/tests/convolution_test.py b/tests/convolution_test.py index f6a22bea..ae4019cc 100755 --- a/tests/convolution_test.py +++ b/tests/convolution_test.py @@ -1,13 +1,15 @@ -from __future__ import absolute_import, print_function +from __future__ import division, absolute_import, print_function +import functools as ft +import numpy as np import tensorflow as tf from tensorflow.contrib.layers.python.layers import regularizers from niftynet.layer.convolution import ConvLayer from niftynet.layer.convolution import ConvolutionalLayer +from tests.niftynet_testcase import NiftyNetTestCase - -class ConvTest(tf.test.TestCase): +class ConvTest(NiftyNetTestCase): def get_3d_input(self): input_shape = (2, 16, 16, 16, 8) x_3d = tf.ones(input_shape) @@ -30,7 +32,7 @@ def _test_conv_output_shape(self, conv_layer = ConvLayer(**param_dict) output_data = conv_layer(input_data) print(conv_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) output_value = sess.run(output_data) self.assertAllClose(output_shape, output_value.shape) @@ -51,11 +53,134 @@ def _test_conv_layer_output_shape(self, is_training=is_training, keep_prob=dropout_prob) print(conv_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) output_value = sess.run(output_data) self.assertAllClose(output_shape, output_value.shape) + def _test_extended_conv(self, orig_input, init_dict): + """ + Tests the extended padding options of ConvLayer + """ + + def _w_init(shape, dtype=tf.float32, **kwargs): + data = np.arange(ft.reduce(lambda prod, x: prod*x, shape, 1))\ + .astype(np.float32) + data *= 2.374/data.mean() + data -= data.mean() + + return tf.constant(data.reshape(shape), dtype=dtype) + + def _b_init(shape, dtype=tf.float32, **kwargs): + data = np.arange(shape[0]).astype(np.float32) + data *= 0.273/data.mean() + data -= data.mean() + + return tf.constant(data.reshape(shape), dtype=dtype) + + init_dict['w_initializer'] = _w_init + init_dict['b_initializer'] = _b_init + + conv_layer = ConvLayer(**init_dict) + small_output = conv_layer(tf.constant(orig_input)) + + input_shape = orig_input.shape + multiplier = init_dict['kernel_size'] + init_dict['dilation'] \ + + init_dict['stride'] + pad = [d*multiplier for d in input_shape[1:-1]] + paddings = [(0, 0)] + [(p, p) for p in pad] + [(0, 0)] + + if init_dict['padding'] == 'CONSTANT': + opts = {'constant_values': init_dict.get('padding_constant', 0)} + else: + opts = {} + + enlarged_input = np.pad(orig_input, + paddings, + init_dict['padding'].lower(), + **opts) + + conv_layer.padding = 'SAME' + large_output = conv_layer(tf.constant(enlarged_input)) + + def _extract_valid_region(output_tensor, target_tensor): + output_shape = output_tensor.shape + target_shape = target_tensor.shape + extr_slices = [] + for d in range(len(target_shape)): + opad = (output_shape[d] - target_shape[d])//2 + extr_slices.append(slice( + opad, opad + target_shape[d])) + + return output_tensor[tuple(extr_slices)] + + assert np.square( + _extract_valid_region(enlarged_input, orig_input) - orig_input).sum() \ + <= 1e-6*np.square(orig_input).sum() + + with self.cached_session() as sess: + sess.run(tf.global_variables_initializer()) + + small_value = sess.run(small_output) + large_value = sess.run(large_output) + + extr_value = _extract_valid_region(large_value, small_value) + + print(np.square(small_value - extr_value).sum()/np.square(extr_value).sum()) + + self.assertAllClose(small_value, extr_value, rtol=1e-3) + + def _get_pad_test_input_3d(self): + data = np.arange(1024, dtype=np.float32) + + return data.reshape([1, 16, 4, 4, 4]) + + def _get_pad_test_input_2d(self): + data = np.arange(256, dtype=np.float32) + + return data.reshape([4, 8, 4, 2]) + + # padding tests + def _test_extended_padding(self, pad, do_2d): + batch = self._get_pad_test_input_2d() if do_2d \ + else self._get_pad_test_input_3d() + + const = 127.23 + min_dim = min(batch.shape[1:-1]) - 1 + for ks in (2, min_dim): + for ds in (1, min_dim): + name = 'pad_test_conv' + ('2' if do_2d else '3') + name += "%i_%i" % (ks, ds) + init_dict = {'n_output_chns': 4, + 'kernel_size': ks, + 'stride': 1, + 'dilation': ds, + 'padding': pad, + 'name': name} + + if ds%2 == 0: + init_dict['padding_constant'] = const + + self._test_extended_conv(batch, init_dict) + + def test_2d_const_padding(self): + self._test_extended_padding('CONSTANT', True) + + def test_2d_reflect_padding(self): + self._test_extended_padding('REFLECT', True) + + def test_2d_symmetric_padding(self): + self._test_extended_padding('SYMMETRIC', True) + + def test_3d_const_padding(self): + self._test_extended_padding('CONSTANT', False) + + def test_3d_reflect_padding(self): + self._test_extended_padding('REFLECT', False) + + def test_3d_symmetric_padding(self): + self._test_extended_padding('SYMMETRIC', False) + # 3d tests def test_3d_conv_default_shape(self): input_param = {'n_output_chns': 10, @@ -131,7 +256,7 @@ def test_3d_convlayer_bias_shape(self): 'kernel_size': 3, 'stride': 1, 'with_bias': True, - 'with_bn': False} + 'feature_normalization': None} self._test_conv_layer_output_shape(rank=3, param_dict=input_param, output_shape=(2, 16, 16, 16, 10)) @@ -141,7 +266,7 @@ def test_convlayer_3d_bias_reg_shape(self): 'kernel_size': 3, 'stride': 1, 'with_bias': True, - 'with_bn': False, + 'feature_normalization': None, 'w_regularizer': regularizers.l2_regularizer(0.5), 'b_regularizer': regularizers.l2_regularizer(0.5)} self._test_conv_layer_output_shape(rank=3, @@ -153,7 +278,7 @@ def test_convlayer_3d_bn_reg_shape(self): 'kernel_size': [5, 1, 2], 'stride': 1, 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'w_regularizer': regularizers.l2_regularizer(0.5), 'b_regularizer': regularizers.l2_regularizer(0.5)} self._test_conv_layer_output_shape(rank=3, @@ -166,7 +291,7 @@ def test_convlayer_3d_bn_reg_prelu_shape(self): 'kernel_size': [5, 1, 2], 'stride': [1, 1, 2], 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'acti_func': 'prelu', 'w_regularizer': regularizers.l2_regularizer(0.5), 'b_regularizer': regularizers.l2_regularizer(0.5)} @@ -180,7 +305,7 @@ def test_convlayer_3d_relu_shape(self): 'kernel_size': [5, 1, 2], 'stride': [1, 2, 2], 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'acti_func': 'relu', 'w_regularizer': regularizers.l2_regularizer(0.5), 'b_regularizer': regularizers.l2_regularizer(0.5)} @@ -194,7 +319,7 @@ def test_convlayer_3d_bn_reg_dropout_shape(self): 'kernel_size': [5, 1, 2], 'stride': [1, 2, 2], 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'acti_func': 'prelu'} self._test_conv_layer_output_shape(rank=3, param_dict=input_param, @@ -207,7 +332,7 @@ def test_convlayer_3d_bn_reg_dropout_valid_shape(self): 'kernel_size': [5, 3, 2], 'stride': [2, 2, 3], 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'w_regularizer': regularizers.l2_regularizer(0.5), 'acti_func': 'prelu', 'padding': 'VALID'} @@ -222,7 +347,7 @@ def test_convlayer_3d_group_reg_dropout_valid_shape(self): 'kernel_size': [5, 3, 2], 'stride': [2, 2, 3], 'with_bias': False, - 'with_bn': False, + 'feature_normalization': 'group', 'group_size': 4, 'w_regularizer': regularizers.l2_regularizer(0.5)} self._test_conv_layer_output_shape(rank=3, @@ -284,7 +409,7 @@ def test_2d_convlayer_bias_shape(self): 'kernel_size': 2, 'stride': [2, 1], 'with_bias': True, - 'with_bn': False} + 'feature_normalization': None} self._test_conv_layer_output_shape(rank=2, param_dict=input_param, output_shape=(2, 8, 16, 10)) @@ -294,7 +419,7 @@ def test_convlayer_2d_bias_reg_shape(self): 'kernel_size': [3, 5], 'stride': [2, 1], 'with_bias': True, - 'with_bn': False, + 'feature_normalization': None, 'w_regularizer': regularizers.l2_regularizer(0.5), 'b_regularizer': regularizers.l2_regularizer(0.5)} self._test_conv_layer_output_shape(rank=2, @@ -306,7 +431,7 @@ def test_convlayer_2d_bn_reg_shape(self): 'kernel_size': [3, 5], 'stride': [2, 1], 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'w_regularizer': regularizers.l2_regularizer(0.5), 'b_regularizer': regularizers.l2_regularizer(0.5)} self._test_conv_layer_output_shape(rank=2, @@ -319,7 +444,7 @@ def test_convlayer_2d_bn_reg_prelu_2_shape(self): 'kernel_size': 3, 'stride': [2, 1], 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'acti_func': 'prelu'} self._test_conv_layer_output_shape(rank=2, param_dict=input_param, @@ -331,7 +456,7 @@ def test_convlayer_2d_relu_shape(self): 'kernel_size': 3, 'stride': [3, 1], 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'acti_func': 'relu'} self._test_conv_layer_output_shape(rank=2, param_dict=input_param, @@ -343,7 +468,7 @@ def test_convlayer_2d_bn_reg_prelu_shape(self): 'kernel_size': 3, 'stride': 1, 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'acti_func': 'prelu', 'w_regularizer': regularizers.l2_regularizer(0.5)} self._test_conv_layer_output_shape(rank=2, @@ -356,7 +481,7 @@ def test_convlayer_2d_bn_reg_valid_shape(self): 'kernel_size': [3, 2], 'stride': [2, 3], 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'acti_func': 'prelu', 'padding': 'VALID', 'w_regularizer': regularizers.l2_regularizer(0.5)} diff --git a/tests/crf_test.py b/tests/crf_test.py index 900b6947..97961122 100755 --- a/tests/crf_test.py +++ b/tests/crf_test.py @@ -5,9 +5,11 @@ import tensorflow as tf from niftynet.layer.crf import CRFAsRNNLayer +from niftynet.layer.crf import permutohedral_prepare, permutohedral_compute +from tests.niftynet_testcase import NiftyNetTestCase -class CRFTest(tf.test.TestCase): +class CRFTest(NiftyNetTestCase): def test_2d3d_shape(self): tf.reset_default_graph() I = tf.random_normal(shape=[2, 4, 5, 6, 3]) @@ -17,7 +19,7 @@ def test_2d3d_shape(self): out1 = crf_layer(I, U) out2 = crf_layer2(I[:, :, :, 0, :], out1[:, :, :, 0, :]) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out1, out2 = sess.run([out1, out2]) @@ -42,7 +44,7 @@ def test_training_3d(self): loss = tf.reduce_mean(tf.abs(smoothed_logits - gt)) opt = tf.train.GradientDescentOptimizer(0.5).minimize(loss) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) params = sess.run(tf.trainable_variables()) for param in params: @@ -73,7 +75,7 @@ def test_training_2d(self): loss = tf.reduce_mean(tf.abs(smoothed_logits - gt)) opt = tf.train.GradientDescentOptimizer(0.5).minimize(loss) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) params = sess.run(tf.trainable_variables()) for param in params: @@ -119,6 +121,25 @@ def test_training_4d(self): params_1 = sess.run(tf.trainable_variables()) self.assertGreater(np.sum(np.abs(params_1[0] - params[0])), 0.0) + def test_batch_mix(self): + feat = tf.random.uniform(shape=[2, 64, 5]) + desc = tf.ones(shape=[1, 64, 1]) + desc_ = tf.zeros(shape=[1, 64, 1]) + desc = tf.concat([desc, desc_], axis=0) + barycentric, blur_neighbours1, blur_neighbours2, indices = permutohedral_prepare(feat) + sliced = permutohedral_compute(desc, + barycentric, + blur_neighbours1, + blur_neighbours2, + indices, + "test", + True) + with tf.Session() as sess: + sess.run(tf.global_variables_initializer()) + sliced_np = sess.run(sliced) + self.assertAllClose(sliced_np[1:], np.zeros(shape=[1, 64, 1])) + + if __name__ == "__main__": tf.test.main() diff --git a/tests/crop_test.py b/tests/crop_test.py index d15dfb06..cafdace5 100755 --- a/tests/crop_test.py +++ b/tests/crop_test.py @@ -2,9 +2,9 @@ import tensorflow as tf from niftynet.layer.crop import CropLayer +from tests.niftynet_testcase import NiftyNetTestCase - -class CropTest(tf.test.TestCase): +class CropTest(NiftyNetTestCase): def test_3d_shape(self): input_shape = (2, 16, 16, 16, 8) test_border = 3 @@ -22,7 +22,7 @@ def test_3d_shape(self): out_crop_1 = crop_layer(x) print(crop_layer) - with self.test_session() as sess: + with self.cached_session() as sess: out = sess.run(out_crop) out_1 = sess.run(out_crop_1) self.assertAllClose((2, 10, 10, 10, 8), out.shape) @@ -45,7 +45,7 @@ def test_2d_shape(self): out_crop_1 = crop_layer(x) print(crop_layer) - with self.test_session() as sess: + with self.cached_session() as sess: out = sess.run(out_crop) out_1 = sess.run(out_crop_1) self.assertAllClose((2, 10, 10, 8), out.shape) diff --git a/tests/deconvolution_test.py b/tests/deconvolution_test.py index 4e92e8ef..13095fbe 100755 --- a/tests/deconvolution_test.py +++ b/tests/deconvolution_test.py @@ -5,9 +5,10 @@ from niftynet.layer.deconvolution import DeconvLayer from niftynet.layer.deconvolution import DeconvolutionalLayer +from tests.niftynet_testcase import NiftyNetTestCase -class DeconvTest(tf.test.TestCase): +class DeconvTest(NiftyNetTestCase): def get_2d_input(self): input_shape = (2, 16, 16, 8) x_2d = tf.ones(input_shape) @@ -30,7 +31,7 @@ def _test_deconv_output_shape(self, deconv_layer = DeconvLayer(**param_dict) output_data = deconv_layer(input_data) print(deconv_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) output_value = sess.run(output_data) self.assertAllClose(output_shape, output_value.shape) @@ -51,7 +52,7 @@ def _test_deconv_layer_output_shape(self, is_training=is_training, keep_prob=dropout_prob) print(deconv_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) output_value = sess.run(output_data) self.assertAllClose(output_shape, output_value.shape) @@ -98,7 +99,7 @@ def test_3d_deconvlayer_bias_shape(self): 'kernel_size': 3, 'stride': 2, 'with_bias': True, - 'with_bn': False} + 'feature_normalization': None} self._test_deconv_layer_output_shape(rank=3, param_dict=input_param, output_shape=(2, 32, 32, 32, 10), @@ -113,7 +114,7 @@ def test_deconvlayer_3d_bias_reg_shape(self): 'kernel_size': 3, 'stride': 1, 'with_bias': True, - 'with_bn': False, + 'feature_normalization': None, 'w_regularizer': regularizers.l2_regularizer(0.5), 'b_regularizer': regularizers.l2_regularizer(0.5)} self._test_deconv_layer_output_shape(rank=3, @@ -128,7 +129,7 @@ def test_deconvlayer_3d_bn_reg_shape(self): 'kernel_size': 3, 'stride': 1, 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'w_regularizer': regularizers.l2_regularizer(0.5)} self._test_deconv_layer_output_shape(rank=3, param_dict=input_param, @@ -144,7 +145,7 @@ def test_deconvlayer_3d_bn_reg_prelu_shape(self): 'kernel_size': [3, 5, 2], 'stride': [1, 1, 2], 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'w_regularizer': regularizers.l2_regularizer(0.5), 'acti_func': 'prelu'} self._test_deconv_layer_output_shape(rank=3, @@ -161,7 +162,7 @@ def test_deconvlayer_3d_relu_shape(self): 'kernel_size': [3, 5, 2], 'stride': [1, 1, 2], 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'acti_func': 'relu'} self._test_deconv_layer_output_shape(rank=3, param_dict=input_param, @@ -177,7 +178,7 @@ def test_deconvlayer_3d_bn_reg_dropout_shape(self): 'kernel_size': [3, 5, 2], 'stride': [1, 2, 2], 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'acti_func': 'prelu', 'w_regularizer': regularizers.l2_regularizer(0.5)} self._test_deconv_layer_output_shape(rank=3, @@ -194,7 +195,7 @@ def test_deconvlayer_3d_bn_reg_dropout_valid_shape(self): 'kernel_size': [3, 5, 2], 'stride': [1, 2, 1], 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'acti_func': 'prelu', 'w_regularizer': regularizers.l2_regularizer(0.5)} self._test_deconv_layer_output_shape(rank=3, @@ -254,7 +255,7 @@ def test_2d_deconvlayer_bias_shape(self): 'kernel_size': [3, 1], 'stride': [2, 1], 'with_bias': True, - 'with_bn': False} + 'feature_normalization': None} self._test_deconv_layer_output_shape(rank=2, param_dict=input_param, output_shape=(2, 32, 16, 10), @@ -269,7 +270,7 @@ def test_deconvlayer_2d_bias_reg_shape(self): 'kernel_size': [3, 1], 'stride': [2, 3], 'with_bias': True, - 'with_bn': False, + 'feature_normalization': None, 'w_regularizer': regularizers.l2_regularizer(0.5), 'b_regularizer': regularizers.l2_regularizer(0.5)} self._test_deconv_layer_output_shape(rank=2, @@ -286,7 +287,7 @@ def test_deconvlayer_2d_bn_reg_shape(self): 'kernel_size': [3, 1], 'stride': [1, 3], 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'w_regularizer': regularizers.l2_regularizer(0.5)} self._test_deconv_layer_output_shape(rank=2, param_dict=input_param, @@ -302,7 +303,7 @@ def test_deconvlayer_2d_bn_reg_prelu_shape(self): 'kernel_size': [4, 1], 'stride': [1, 3], 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'acti_func': 'prelu', 'w_regularizer': regularizers.l2_regularizer(0.5)} self._test_deconv_layer_output_shape(rank=2, @@ -319,7 +320,7 @@ def test_deconvlayer_2d_relu_shape(self): 'kernel_size': [4, 1], 'stride': [1, 3], 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'acti_func': 'relu', 'w_regularizer': regularizers.l2_regularizer(0.5)} self._test_deconv_layer_output_shape(rank=2, @@ -336,7 +337,7 @@ def test_deconvlayer_2d_bn_reg_dropout_prelu_shape(self): 'kernel_size': [4, 1], 'stride': [1, 3], 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'acti_func': 'prelu', 'w_regularizer': regularizers.l2_regularizer(0.5)} self._test_deconv_layer_output_shape(rank=2, @@ -355,7 +356,7 @@ def test_deconvlayer_2d_bn_reg_valid_shape(self): 'kernel_size': [4, 3], 'stride': [1, 2], 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'acti_func': 'prelu', 'padding': 'VALID', 'w_regularizer': regularizers.l2_regularizer(0.5)} @@ -375,7 +376,7 @@ def test_deconvlayer_2d_group_reg_valid_shape(self): 'kernel_size': [4, 3], 'stride': [1, 2], 'with_bias': False, - 'with_bn': False, + 'feature_normalization': 'group', 'group_size': 5, 'acti_func': 'prelu', 'padding': 'VALID', diff --git a/tests/deepmedic_test.py b/tests/deepmedic_test.py index df5da56e..07f56b57 100755 --- a/tests/deepmedic_test.py +++ b/tests/deepmedic_test.py @@ -7,10 +7,11 @@ from tensorflow.contrib.layers.python.layers import regularizers from niftynet.network.deepmedic import DeepMedic +from tests.niftynet_testcase import NiftyNetTestCase @unittest.skipIf(os.environ.get('QUICKTEST', "").lower() == "true", 'Skipping slow tests') -class DeepMedicTest(tf.test.TestCase): +class DeepMedicTest(NiftyNetTestCase): def test_3d_reg_shape(self): input_shape = (2, 57, 57, 57, 1) x = tf.ones(input_shape) @@ -22,7 +23,7 @@ def test_3d_reg_shape(self): out = deepmedic_instance(x, is_training=True) # print(tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES)) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 9, 9, 9, 160), out.shape) @@ -38,7 +39,7 @@ def test_2d_reg_shape(self): out = deepmedic_instance(x, is_training=True) # print(tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES)) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 9, 9, 160), out.shape) @@ -50,7 +51,7 @@ def test_3d_shape(self): deepmedic_instance = DeepMedic(num_classes=160) out = deepmedic_instance(x, is_training=True) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 9, 9, 9, 160), out.shape) @@ -62,7 +63,7 @@ def test_2d_shape(self): deepmedic_instance = DeepMedic(num_classes=160) out = deepmedic_instance(x, is_training=True) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 9, 9, 160), out.shape) diff --git a/tests/dense_vnet_test.py b/tests/dense_vnet_test.py index 532fd346..47a20746 100755 --- a/tests/dense_vnet_test.py +++ b/tests/dense_vnet_test.py @@ -7,9 +7,10 @@ from tensorflow.contrib.layers.python.layers import regularizers from niftynet.network.dense_vnet import DenseVNet +from tests.niftynet_testcase import NiftyNetTestCase @unittest.skipIf(os.environ.get('QUICKTEST', "").lower() == "true", 'Skipping slow tests') -class DenseVNetTest(tf.test.TestCase): +class DenseVNetTest(NiftyNetTestCase): def test_3d_shape(self): input_shape = (2, 72, 72, 72, 3) x = tf.ones(input_shape) @@ -19,7 +20,7 @@ def test_3d_shape(self): out = dense_vnet_instance(x, is_training=True) # print(tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES)) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 72, 72, 72, 2), out.shape) @@ -33,7 +34,7 @@ def test_2d_shape(self): out = dense_vnet_instance(x, is_training=True) # print(tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES)) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 72, 72, 2), out.shape) diff --git a/tests/dilatedcontext_test.py b/tests/dilatedcontext_test.py index 0050f81c..86b4b76c 100755 --- a/tests/dilatedcontext_test.py +++ b/tests/dilatedcontext_test.py @@ -2,9 +2,10 @@ import tensorflow as tf from niftynet.layer.dilatedcontext import DilatedTensor +from tests.niftynet_testcase import NiftyNetTestCase -class BNTest(tf.test.TestCase): +class BNTest(NiftyNetTestCase): def get_2d_input(self): input_shape = (2, 16, 16, 8) x = tf.ones(input_shape) @@ -21,7 +22,7 @@ def test_2d_dilating_shape(self): intermediate = dilated.tensor x = dilated.tensor - with self.test_session() as sess: + with self.cached_session() as sess: out = sess.run(x) out_dilated = sess.run(intermediate) self.assertAllClose((2, 16, 16, 8), out.shape) @@ -33,7 +34,7 @@ def test_3d_dilating_shape(self): intermediate = dilated.tensor x = dilated.tensor - with self.test_session() as sess: + with self.cached_session() as sess: out = sess.run(x) out_dilated = sess.run(intermediate) self.assertAllClose((2, 16, 16, 16, 8), out.shape) diff --git a/tests/download_test.py b/tests/download_test.py index d922bdaa..5dca36af 100755 --- a/tests/download_test.py +++ b/tests/download_test.py @@ -9,6 +9,7 @@ from niftynet.utilities.download import download from niftynet.utilities.niftynet_global_config import NiftyNetGlobalConfig +from tests.niftynet_testcase import NiftyNetTestCase MODEL_HOME = NiftyNetGlobalConfig().get_niftynet_home_folder() @@ -23,7 +24,7 @@ TEST_WRONG_ID = '42' -class NetDownloadTest(tf.test.TestCase): +class NetDownloadTest(NiftyNetTestCase): def test_download(self): self.assertTrue(download(TEST_CASE_1, False)) self.assertTrue(os.path.isdir(TEST_CASE_1_TARGET)) diff --git a/tests/downsample_res_block_test.py b/tests/downsample_res_block_test.py index 0496a384..d5cb885a 100755 --- a/tests/downsample_res_block_test.py +++ b/tests/downsample_res_block_test.py @@ -4,9 +4,10 @@ import tensorflow as tf from niftynet.layer.downsample_res_block import DownBlock +from tests.niftynet_testcase import NiftyNetTestCase -class DownsampleResBlockTest(tf.test.TestCase): +class DownsampleResBlockTest(NiftyNetTestCase): def get_3d_input(self): input_shape = (2, 16, 16, 16, 8) x = tf.ones(input_shape) @@ -29,7 +30,7 @@ def _test_nd_output_shape(self, downsample_layer = DownBlock(**param_dict) output_data = downsample_layer(input_data) print(downsample_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(output_data) self.assertAllClose(output_shape, out[0].shape) diff --git a/tests/downsample_test.py b/tests/downsample_test.py index 928dffd2..ec3ae3a0 100755 --- a/tests/downsample_test.py +++ b/tests/downsample_test.py @@ -4,9 +4,9 @@ import tensorflow as tf from niftynet.layer.downsample import DownSampleLayer +from tests.niftynet_testcase import NiftyNetTestCase - -class DownSampleTest(tf.test.TestCase): +class DownSampleTest(NiftyNetTestCase): def get_3d_input(self): input_shape = (2, 16, 16, 16, 8) x = tf.ones(input_shape) @@ -28,7 +28,7 @@ def _test_nd_downsample_output_shape(self, downsample_layer = DownSampleLayer(**param_dict) output_data = downsample_layer(input_data) print(downsample_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(output_data) self.assertAllClose(output_shape, out.shape) diff --git a/tests/driver_partitioner_test.py b/tests/driver_partitioner_test.py index 55a946c0..f0bdb15b 100755 --- a/tests/driver_partitioner_test.py +++ b/tests/driver_partitioner_test.py @@ -7,6 +7,7 @@ from niftynet.engine.application_driver import ApplicationDriver from niftynet.utilities.util_common import ParserNamespace +from tests.niftynet_testcase import NiftyNetTestCase TARGET_FILE = os.path.join('testing_data', 'test_splitting.csv') @@ -93,7 +94,7 @@ def write_target(): return -class DriverPartitionerTestExistingFile(tf.test.TestCase): +class DriverPartitionerTestExistingFile(NiftyNetTestCase): def test_training(self): write_target() user_param = generate_input_params( @@ -175,7 +176,7 @@ def test_inference_validation(self): self.assertTrue(partitioner.has_validation) -class DriverPartitionerTestNoFile(tf.test.TestCase): +class DriverPartitionerTestNoFile(NiftyNetTestCase): def test_training(self): clear_target() user_param = generate_input_params( @@ -275,7 +276,7 @@ def test_inference_no_validation(self): self.assertTrue(partitioner._partition_ids is None) -class DriverPartitionerTestNoData(tf.test.TestCase): +class DriverPartitionerTestNoData(NiftyNetTestCase): def test_no_data_param_infer(self): clear_target() user_param = generate_input_params( diff --git a/tests/elementwise_test.py b/tests/elementwise_test.py index 7115ab61..3ee18b9b 100755 --- a/tests/elementwise_test.py +++ b/tests/elementwise_test.py @@ -34,7 +34,7 @@ def test_3d_shape(self): sum_layer = ElementwiseLayer('CONCAT') out_sum_4 = sum_layer(x_1, x_2) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out_sum_1) self.assertAllClose((2, 16, 16, 16, 6), out.shape) @@ -74,7 +74,7 @@ def test_2d_shape(self): sum_layer = ElementwiseLayer('CONCAT') out_sum_4 = sum_layer(x_1, x_2) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out_sum_1) self.assertAllClose((2, 16, 16, 6), out.shape) diff --git a/tests/entry_point_test.py b/tests/entry_point_test.py index 6f1d0040..661c3e1c 100755 --- a/tests/entry_point_test.py +++ b/tests/entry_point_test.py @@ -12,8 +12,9 @@ import net_run import net_segment +from tests.niftynet_testcase import NiftyNetTestCase -class EntryPointTest(tf.test.TestCase): +class EntryPointTest(NiftyNetTestCase): def test_wrong_app(self): sys.argv = ['', 'train', '-a', 'foo', diff --git a/tests/filename_matching_test.py b/tests/filename_matching_test.py index 3a2f4160..8ad9507b 100755 --- a/tests/filename_matching_test.py +++ b/tests/filename_matching_test.py @@ -3,9 +3,9 @@ import tensorflow as tf from niftynet.utilities.filename_matching import KeywordsMatching +from tests.niftynet_testcase import NiftyNetTestCase - -class FileNameMatchingTest(tf.test.TestCase): +class FileNameMatchingTest(NiftyNetTestCase): def test_default(self): matcher = KeywordsMatching() with self.assertRaisesRegexp(ValueError, ""): diff --git a/tests/fully_connected_test.py b/tests/fully_connected_test.py index b941c090..ab55bcef 100755 --- a/tests/fully_connected_test.py +++ b/tests/fully_connected_test.py @@ -5,9 +5,9 @@ from niftynet.layer.fully_connected import FCLayer from niftynet.layer.fully_connected import FullyConnectedLayer +from tests.niftynet_testcase import NiftyNetTestCase - -class FCTest(tf.test.TestCase): +class FCTest(NiftyNetTestCase): def get_3d_input(self): input_shape = (2, 10, 10, 5, 8) x_3d = tf.ones(input_shape) @@ -30,7 +30,7 @@ def _test_fc_output_shape(self, fc_layer = FCLayer(**param_dict) output_data = fc_layer(input_data) print(fc_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) output_value = sess.run(output_data) self.assertAllClose(output_shape, output_value.shape) @@ -51,7 +51,7 @@ def _test_fc_layer_output_shape(self, is_training=is_training, keep_prob=dropout_prob) print(fc_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) output_value = sess.run(output_data) self.assertAllClose(output_shape, output_value.shape) @@ -82,14 +82,14 @@ def test_fc_3d_bias_reg_shape(self): def test_3d_fclayer_bias_shape(self): input_param = {'n_output_chns': 10, 'with_bias': True, - 'with_bn': False} + 'feature_normalization': None} self._test_fc_layer_output_shape(rank=3, param_dict=input_param, output_shape=(2, 10)) def test_fclayer_3d_bias_reg_shape(self): input_param = {'n_output_chns': 10, - 'with_bn': False, + 'feature_normalization': None, 'w_regularizer': regularizers.l2_regularizer(0.5), 'b_regularizer': regularizers.l2_regularizer(0.5)} self._test_fc_layer_output_shape(rank=3, @@ -99,7 +99,7 @@ def test_fclayer_3d_bias_reg_shape(self): def test_fclayer_3d_bn_reg_shape(self): input_param = {'n_output_chns': 10, 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'w_regularizer': regularizers.l2_regularizer(0.5), 'b_regularizer': regularizers.l2_regularizer(0.5)} self._test_fc_layer_output_shape(rank=3, @@ -110,7 +110,7 @@ def test_fclayer_3d_bn_reg_shape(self): def test_fclayer_3d_bn_reg_prelu_shape(self): input_param = {'n_output_chns': 10, 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'acti_func': 'prelu', 'w_regularizer': regularizers.l2_regularizer(0.5), 'b_regularizer': regularizers.l2_regularizer(0.5)} @@ -122,7 +122,7 @@ def test_fclayer_3d_bn_reg_prelu_shape(self): def test_fclayer_3d_relu_shape(self): input_param = {'n_output_chns': 10, 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'acti_func': 'relu', 'w_regularizer': regularizers.l2_regularizer(0.5), 'b_regularizer': regularizers.l2_regularizer(0.5)} @@ -134,7 +134,7 @@ def test_fclayer_3d_relu_shape(self): def test_fclayer_3d_bn_reg_dropout_shape(self): input_param = {'n_output_chns': 10, 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'acti_func': 'prelu'} self._test_fc_layer_output_shape(rank=3, param_dict=input_param, @@ -145,7 +145,7 @@ def test_fclayer_3d_bn_reg_dropout_shape(self): def test_fclayer_3d_bn_reg_dropout_valid_shape(self): input_param = {'n_output_chns': 10, 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'w_regularizer': regularizers.l2_regularizer(0.5), 'acti_func': 'prelu', } self._test_fc_layer_output_shape(rank=3, @@ -188,7 +188,7 @@ def test_2d_fclayer_default_shape(self): def test_2d_fclayer_bias_shape(self): input_param = {'n_output_chns': 10, 'with_bias': True, - 'with_bn': False} + 'feature_normalization': None} self._test_fc_layer_output_shape(rank=2, param_dict=input_param, output_shape=(2, 10)) @@ -196,7 +196,7 @@ def test_2d_fclayer_bias_shape(self): def test_fclayer_2d_bias_reg_shape(self): input_param = {'n_output_chns': 10, 'with_bias': True, - 'with_bn': False, + 'feature_normalization': None, 'w_regularizer': regularizers.l2_regularizer(0.5), 'b_regularizer': regularizers.l2_regularizer(0.5)} self._test_fc_layer_output_shape(rank=2, @@ -206,7 +206,7 @@ def test_fclayer_2d_bias_reg_shape(self): def test_fclayer_2d_bn_reg_shape(self): input_param = {'n_output_chns': 10, 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'w_regularizer': regularizers.l2_regularizer(0.5), 'b_regularizer': regularizers.l2_regularizer(0.5)} self._test_fc_layer_output_shape(rank=2, @@ -217,7 +217,7 @@ def test_fclayer_2d_bn_reg_shape(self): def test_fclayer_2d_bn_reg_prelu_2_shape(self): input_param = {'n_output_chns': 10, 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'acti_func': 'prelu'} self._test_fc_layer_output_shape(rank=2, param_dict=input_param, @@ -227,7 +227,7 @@ def test_fclayer_2d_bn_reg_prelu_2_shape(self): def test_fclayer_2d_relu_shape(self): input_param = {'n_output_chns': 10, 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'acti_func': 'relu'} self._test_fc_layer_output_shape(rank=2, param_dict=input_param, @@ -237,7 +237,7 @@ def test_fclayer_2d_relu_shape(self): def test_fclayer_2d_bn_reg_prelu_shape(self): input_param = {'n_output_chns': 10, 'with_bias': False, - 'with_bn': True, + 'feature_normalization': 'batch', 'acti_func': 'prelu', 'w_regularizer': regularizers.l2_regularizer(0.5)} self._test_fc_layer_output_shape(rank=2, diff --git a/tests/gn_test.py b/tests/gn_test.py index eba7eadc..fc312873 100755 --- a/tests/gn_test.py +++ b/tests/gn_test.py @@ -5,9 +5,9 @@ from tensorflow.contrib.layers.python.layers import regularizers from niftynet.layer.gn import GNLayer +from tests.niftynet_testcase import NiftyNetTestCase - -class GNTest(tf.test.TestCase): +class GNTest(NiftyNetTestCase): def get_3d_input(self): input_shape = (2, 16, 16, 16, 8) x = tf.ones(input_shape) @@ -25,7 +25,7 @@ def test_3d_gn_shape(self): out_gn = gn_layer(x) print(gn_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out_gn) @@ -40,7 +40,7 @@ def test_3d_gn_reg_shape(self): test_gn = gn_layer(x) print(gn_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out_gn) @@ -58,7 +58,7 @@ def test_2d_gn_shape(self): out_gn = gn_layer(x) print(gn_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out_gn) @@ -74,7 +74,7 @@ def test_2d_gn_reg_shape(self): print(gn_layer) reg_loss = tf.add_n(gn_layer.regularizer_loss()) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out_gn) diff --git a/tests/gradient_collector_test.py b/tests/gradient_collector_test.py index 6f0d9d60..1640830a 100755 --- a/tests/gradient_collector_test.py +++ b/tests/gradient_collector_test.py @@ -6,14 +6,14 @@ from niftynet.engine.application_variables import GradientsCollector from niftynet.engine.handler_gradient import ApplyGradients from niftynet.network.toynet import ToyNet - +from tests.niftynet_testcase import NiftyNetTestCase def get_test_network(): net = ToyNet(num_classes=4) return net -class GradientCollectorTest(tf.test.TestCase): +class GradientCollectorTest(NiftyNetTestCase): def test_nested_gradients(self): n_device = 3 grad_collector = GradientsCollector(n_devices=n_device) diff --git a/tests/grid_warper_test.py b/tests/grid_warper_test.py index 974d8736..95757f2c 100755 --- a/tests/grid_warper_test.py +++ b/tests/grid_warper_test.py @@ -6,9 +6,9 @@ from niftynet.layer.grid_warper import AffineGridWarperLayer from niftynet.layer.grid_warper import AffineWarpConstraints from niftynet.layer.grid_warper import _create_affine_features +from tests.niftynet_testcase import NiftyNetTestCase - -class GridWarperTest(tf.test.TestCase): +class GridWarperTest(NiftyNetTestCase): def test_regular_grids(self): out = _create_affine_features([3, 3], [2]) expected = [ @@ -18,7 +18,7 @@ def test_regular_grids(self): self.assertAllClose(expected, out) -class GridWarperRelativeTest(tf.test.TestCase): +class GridWarperRelativeTest(NiftyNetTestCase): def test_regular_grids(self): out = _create_affine_features([3, 3], [2], True) expected = [ @@ -28,7 +28,7 @@ def test_regular_grids(self): self.assertAllClose(expected, out) -class AffineWarpConstraintsTest(tf.test.TestCase): +class AffineWarpConstraintsTest(NiftyNetTestCase): def test_default(self): aff_c = AffineWarpConstraints() self.assertAllEqual(aff_c.constraints, @@ -109,11 +109,11 @@ def test_combine_constraints(self): aff_c_1.combine_with(aff_c_2) -class AffineGridWarperLayerTest(tf.test.TestCase): +class AffineGridWarperLayerTest(NiftyNetTestCase): def _test_correctness(self, args, aff, expected_value): grid_warper = AffineGridWarperLayer(**args) computed_grid = grid_warper(aff) - with self.test_session() as sess: + with self.cached_session() as sess: output_val = sess.run(computed_grid) self.assertAllClose(expected_value, output_val) @@ -192,7 +192,7 @@ def test_3d_3d_translation(self): self._test_correctness(params, aff, expected) -class AffineGridWarperInvLayerTest(tf.test.TestCase): +class AffineGridWarperInvLayerTest(NiftyNetTestCase): def test_simple_inverse(self): expected_grid = np.array([[ [[0.38, 0.08], @@ -209,7 +209,7 @@ def test_simple_inverse(self): output_shape=(2, 2)).inverse_op() aff = tf.constant([[1.5, 1.0, 0.2, 1.0, 1.5, 0.5]]) output = inverse_grid(aff) - with self.test_session() as sess: + with self.cached_session() as sess: out_val = sess.run(output) self.assertAllClose(out_val, expected_grid) diff --git a/tests/handler_console_test.py b/tests/handler_console_test.py index ae97a17a..11dde5a4 100755 --- a/tests/handler_console_test.py +++ b/tests/handler_console_test.py @@ -5,9 +5,10 @@ from tests.application_driver_test import get_initialised_driver from niftynet.engine.application_iteration import IterationMessage from niftynet.engine.signal import SESS_STARTED, ITER_FINISHED +from tests.niftynet_testcase import NiftyNetTestCase -class EventConsoleTest(tf.test.TestCase): +class EventConsoleTest(NiftyNetTestCase): def test_init(self): ITER_FINISHED.connect(self.iteration_listener) @@ -17,7 +18,7 @@ def test_init(self): 'niftynet.engine.handler_console.ConsoleLogger', 'niftynet.engine.handler_sampler.SamplerThreading']) graph = app_driver.create_graph(app_driver.app, 1, True) - with self.test_session(graph=graph) as sess: + with self.cached_session(graph=graph) as sess: SESS_STARTED.send(app_driver.app, iter_msg=None) msg = IterationMessage() msg.current_iter = 1 diff --git a/tests/handler_early_stopping_test.py b/tests/handler_early_stopping_test.py new file mode 100644 index 00000000..f7b468f7 --- /dev/null +++ b/tests/handler_early_stopping_test.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function + +import tensorflow as tf +import numpy as np + +from niftynet.engine.handler_early_stopping import check_should_stop +from tests.niftynet_testcase import NiftyNetTestCase + +class EarlyStopperTest(NiftyNetTestCase): + + def test_mean(self): + should_stop = check_should_stop(mode='mean', + performance_history=[1, 2, 1, 2, 1, + 2, 1, 2, 3]) + self.assertTrue(should_stop) + should_stop = check_should_stop(mode='mean', + performance_history=[1, 2, 1, 2, 1, 2, + 1, 2, 3, 0]) + self.assertFalse(should_stop) + + def test_robust_mean(self): + should_stop = check_should_stop(mode='robust_mean', + performance_history=[1, 2, 1, 2, 1, 2, + 1, 200, -10, 1.4]) + self.assertFalse(should_stop) + should_stop = check_should_stop(mode='robust_mean', + performance_history=[1, 2, 1, 2, 1, 2, + 1, 200, -10, 1.5]) + self.assertTrue(should_stop) + + def test_median(self): + should_stop = check_should_stop(mode='median', + performance_history=[1, 2, 1, 2, 1, 2, + 1, 2, 3]) + self.assertTrue(should_stop) + should_stop = check_should_stop(mode='median', + performance_history=[1, 2, 1, 2, 1, 2, + 1, 2, 3, 0]) + self.assertFalse(should_stop) + + def test_generalisation_loss(self): + should_stop = check_should_stop(mode='generalisation_loss', + performance_history=[1, 2, 1, 2, 1, + 2, 1, 2, 3]) + self.assertTrue(should_stop) + should_stop = check_should_stop(mode='generalisation_loss', + performance_history=[1, 2, 1, 2, 3, + 2, 1, 2, 1]) + self.assertFalse(should_stop) + + def test_validation_up(self): + data = [] + for i in range(10): + data.extend(np.arange(1, 9)) + data.extend(np.arange(2, 10)[::-1]) + should_stop = check_should_stop(mode='validation_up', + performance_history= + np.arange(0, 20) / 10.0) + print("1 val") + self.assertTrue(should_stop) + should_stop = check_should_stop(mode='validation_up', + performance_history=np.arange( + 0, 20)[::-1] / 10) + print("2 val") + self.assertFalse(should_stop) + + should_stop = check_should_stop(mode='validation_up', + performance_history=data, + min_delta=0.2) + print("3 val") + self.assertFalse(should_stop) + + def test_median_smoothing(self): + data = [] + for i in range(10): + data.extend(np.arange(0, 8)) + data.extend(np.arange(1, 9)[::-1]) + should_stop = \ + check_should_stop(mode='median_smoothing', + performance_history=np.arange(0, 20) / 10.0) + self.assertTrue(should_stop) + should_stop = check_should_stop(mode='median_smoothing', + performance_history=np.arange( + 0, 20)[::-1] / 10) + self.assertFalse(should_stop) + + should_stop = check_should_stop(mode='median_smoothing', + performance_history=data) + self.assertFalse(should_stop) + + def test_weird_mode(self): + with self.assertRaises(Exception): + check_should_stop(mode='adslhfjdkas', + performance_history=[1, 2, 3, 4, 5, 6, 7, 8, 9]) + + +if __name__ == "__main__": + tf.test.main() diff --git a/tests/handler_network_output_test.py b/tests/handler_network_output_test.py index 80bdc3af..24bb1f35 100755 --- a/tests/handler_network_output_test.py +++ b/tests/handler_network_output_test.py @@ -7,7 +7,7 @@ from niftynet.engine.application_variables import NETWORK_OUTPUT from tests.application_driver_test import get_initialised_driver from niftynet.engine.signal import SESS_STARTED - +from tests.niftynet_testcase import NiftyNetTestCase def set_iteration_update(msg): @@ -15,7 +15,7 @@ def set_iteration_update(msg): tf.get_default_graph().get_tensor_by_name("G/conv_bn_selu/conv_/w:0") -class EventConsoleTest(tf.test.TestCase): +class EventConsoleTest(NiftyNetTestCase): def create_interpreter(self): def mini_interpreter(np_array): self.assertEqual(np_array.shape, (10, 1, 20)) @@ -34,7 +34,7 @@ def test_init(self): ['niftynet.engine.handler_model.ModelRestorer', 'niftynet.engine.handler_network_output.OutputInterpreter', 'niftynet.engine.handler_sampler.SamplerThreading']) - with self.test_session(graph=test_graph) as sess: + with self.cached_session(graph=test_graph) as sess: SESS_STARTED.send(app_driver.app, iter_msg=None) iterator = IterationMessageGenerator(is_training_action=False) diff --git a/tests/handler_performance_test.py b/tests/handler_performance_test.py new file mode 100644 index 00000000..67d8d3ee --- /dev/null +++ b/tests/handler_performance_test.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function + +import tensorflow as tf +import numpy as np + +from tests.application_driver_test import get_initialised_driver +from niftynet.engine.application_iteration import IterationMessage +from niftynet.engine.signal import SESS_STARTED, ITER_FINISHED, VALID +from tests.niftynet_testcase import NiftyNetTestCase + + +class PerformanceLoggerTest(NiftyNetTestCase): + def test_init(self): + ITER_FINISHED.connect(self.iteration_listener) + app_driver = get_initialised_driver() + app_driver.load_event_handlers( + ['niftynet.engine.handler_model.ModelRestorer', + 'niftynet.engine.handler_console.ConsoleLogger', + 'niftynet.engine.handler_sampler.SamplerThreading', + 'niftynet.engine.handler_performance.PerformanceLogger']) + graph = app_driver.create_graph(app_driver.app, 1, True) + with self.cached_session(graph=graph) as sess: + for i in range(110): + SESS_STARTED.send(app_driver.app, iter_msg=None) + msg = IterationMessage() + msg._phase = VALID + msg.current_iter = i + app_driver.loop(app_driver.app, [msg]) + app_driver.app.stop() + ITER_FINISHED.disconnect(self.iteration_listener) + + def iteration_listener(self, sender, **msg): + msg = msg['iter_msg'] + self.assertRegexpMatches(msg.to_console_string(), '.*total_loss.*') + if msg.current_iter > 1: + self.assertTrue(isinstance(sender.performance_history, list)) + self.assertTrue(len(sender.performance_history) <= sender.patience) + self.assertTrue(all([isinstance(p, np.float32) for p in sender.performance_history])) + + +if __name__ == "__main__": + tf.test.main() diff --git a/tests/highres3dnet_test.py b/tests/highres3dnet_test.py index 86514f55..92506a1d 100755 --- a/tests/highres3dnet_test.py +++ b/tests/highres3dnet_test.py @@ -9,11 +9,12 @@ from niftynet.network.highres3dnet import HighRes3DNet from niftynet.network.highres3dnet_large import HighRes3DNetLarge from niftynet.network.highres3dnet_small import HighRes3DNetSmall +from tests.niftynet_testcase import NiftyNetTestCase @unittest.skipIf(os.environ.get('QUICKTEST', "").lower() == "true", 'Skipping slow tests') -class HighRes3DNetTest(tf.test.TestCase): +class HighRes3DNetTest(NiftyNetTestCase): def shape_test(self, input_shape, expected_shape): x = tf.ones(input_shape) @@ -25,7 +26,7 @@ def shape_test(self, input_shape, expected_shape): out_small = highres_layer_small(x, is_training=True) out_large = highres_layer_large(x, is_training=True) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out, out_large, out_small = sess.run([out, out_large, out_small]) self.assertAllClose(expected_shape, out.shape) @@ -47,7 +48,7 @@ def shape_test_reg(self, input_shape, expected_shape): out_small = highres_layer_small(x, is_training=True) out_large = highres_layer_large(x, is_training=True) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out, out_large, out_small = sess.run([out, out_large, out_small]) self.assertAllClose(expected_shape, out.shape) diff --git a/tests/highresblock_test.py b/tests/highresblock_test.py index 02c3ed92..2abf5bce 100755 --- a/tests/highresblock_test.py +++ b/tests/highresblock_test.py @@ -4,9 +4,10 @@ from tensorflow.contrib.layers.python.layers import regularizers from niftynet.network.highres3dnet import HighResBlock +from tests.niftynet_testcase import NiftyNetTestCase -class HighResBlockTest(tf.test.TestCase): +class HighResBlockTest(NiftyNetTestCase): def test_3d_increase_shape(self): input_shape = (2, 16, 16, 16, 8) x = tf.ones(input_shape) @@ -17,7 +18,7 @@ def test_3d_increase_shape(self): out = highres_layer(x, is_training=True) print(highres_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 16, 16, 16, 16), out.shape) @@ -32,7 +33,7 @@ def test_3d_same_shape(self): out = highres_layer(x, is_training=True) print(highres_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 16, 16, 16, 8), out.shape) @@ -47,7 +48,7 @@ def test_3d_reduce_shape(self): out = highres_layer(x, is_training=True) print(highres_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 16, 16, 16, 4), out.shape) @@ -64,7 +65,7 @@ def test_3d_reg_increase_shape(self): out = highres_layer(x, is_training=True) print(highres_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 16, 16, 16, 16), out.shape) @@ -82,7 +83,7 @@ def test_3d_reg_same_shape(self): out = highres_layer(x, is_training=True) print(highres_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 16, 16, 16, 8), out.shape) @@ -99,7 +100,7 @@ def test_3d_reg_reduce_shape(self): out = highres_layer(x, is_training=True) print(highres_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 16, 16, 16, 4), out.shape) @@ -114,7 +115,7 @@ def test_2d_increase_shape(self): out = highres_layer(x, is_training=True) print(highres_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 16, 16, 16), out.shape) @@ -129,7 +130,7 @@ def test_2d_same_shape(self): out = highres_layer(x, is_training=True) print(highres_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 16, 16, 8), out.shape) @@ -144,7 +145,7 @@ def test_2d_reduce_shape(self): out = highres_layer(x, is_training=True) print(highres_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 16, 16, 4), out.shape) diff --git a/tests/histogram_normalisation_test.py b/tests/histogram_normalisation_test.py index 826e30bb..31d55f01 100755 --- a/tests/histogram_normalisation_test.py +++ b/tests/histogram_normalisation_test.py @@ -14,6 +14,7 @@ from niftynet.layer.mean_variance_normalisation import \ MeanVarNormalisationLayer from niftynet.utilities.util_common import ParserNamespace +from tests.niftynet_testcase import NiftyNetTestCase DATA_PARAM = { 'T1': ParserNamespace( @@ -44,7 +45,7 @@ # @unittest.skipIf(os.environ.get('QUICKTEST', "").lower() == "true", 'Skipping slow tests') -class HistTest(tf.test.TestCase): +class HistTest(NiftyNetTestCase): def test_volume_loader(self): expected_T1 = np.array( [0.0, 8.24277910972, 21.4917343731, diff --git a/tests/holistic_net_test.py b/tests/holistic_net_test.py index 9dccb3bb..fd9e1f63 100755 --- a/tests/holistic_net_test.py +++ b/tests/holistic_net_test.py @@ -7,9 +7,10 @@ from tensorflow.contrib.layers.python.layers import regularizers from niftynet.network.holistic_net import HolisticNet +from tests.niftynet_testcase import NiftyNetTestCase -class HolisticNetTest(tf.test.TestCase): +class HolisticNetTest(NiftyNetTestCase): def test_3d_reg_shape(self): input_shape = (2, 20, 20, 20, 1) x = tf.ones(input_shape) @@ -21,7 +22,7 @@ def test_3d_reg_shape(self): out = holistic_net_instance(x, is_training=False) # print(tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES)) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 20, 20, 20, 3), out.shape) @@ -37,7 +38,7 @@ def test_2d_reg_shape(self): out = holistic_net_instance(x, is_training=False) # print(tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES)) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 20, 20, 3), out.shape) diff --git a/tests/image_loader_test.py b/tests/image_loader_test.py index 42922e9a..a9b6a38e 100755 --- a/tests/image_loader_test.py +++ b/tests/image_loader_test.py @@ -2,13 +2,15 @@ from __future__ import absolute_import, print_function import os +import numpy as np import tensorflow as tf import niftynet.io.image_loader as image_loader +from tests.niftynet_testcase import NiftyNetTestCase CASE_NIBABEL_3D = 'testing_data/FLAIR_1023.nii.gz' CASE_LOGO_2D = 'niftynet-logo.png' -class ImageLoaderTest(tf.test.TestCase): +class ImageLoaderTest(NiftyNetTestCase): def test_nibabel_3d(self): data = image_loader.load_image_obj(CASE_NIBABEL_3D).get_data() self.assertAllClose(data.shape, (256, 168, 256)) @@ -17,6 +19,10 @@ def load_2d_image(self, loader=None): data = image_loader.load_image_obj(CASE_LOGO_2D, loader=loader).get_data() self.assertAllClose(data.shape, (400, 677, 1, 1, 4)) + def test_convert_bool(self): + boolarr=np.ones((256,256,256),np.bool) + img=image_loader.image2nibabel(boolarr) + def test_2d_loaders(self): with self.assertRaisesRegexp(ValueError, ''): self.load_2d_image('test') diff --git a/tests/image_reader_test.py b/tests/image_reader_test.py index 1610c0f5..76fa163c 100755 --- a/tests/image_reader_test.py +++ b/tests/image_reader_test.py @@ -13,6 +13,7 @@ from niftynet.layer.pad import PadLayer from niftynet.utilities.util_common import ParserNamespace from tests.reader_modular_test import generate_2d_images, SEG_THRESHOLD +from tests.niftynet_testcase import NiftyNetTestCase generate_2d_images() # test multiple modalities @@ -138,7 +139,7 @@ image2d_data_list = data_partitioner.initialise(IMAGE_2D_DATA).get_file_list() -class ImageReaderTest(tf.test.TestCase): +class ImageReaderTest(NiftyNetTestCase): def test_initialisation(self): with self.assertRaisesRegexp(ValueError, ''): reader = ImageReader(['test']) diff --git a/tests/image_sets_partitioner_test.py b/tests/image_sets_partitioner_test.py index 972a5350..621b450a 100755 --- a/tests/image_sets_partitioner_test.py +++ b/tests/image_sets_partitioner_test.py @@ -9,6 +9,7 @@ from niftynet.io.image_sets_partitioner import ImageSetsPartitioner from niftynet.engine.signal import TRAIN, VALID, INFER from niftynet.utilities.util_common import ParserNamespace +from tests.niftynet_testcase import NiftyNetTestCase test_sections = { 'T1': ParserNamespace( @@ -34,7 +35,7 @@ partition_output = os.path.join('testing_data', 'partition.csv') -class ImageSetsPartitionerTest(tf.test.TestCase): +class ImageSetsPartitionerTest(NiftyNetTestCase): def test_no_partition_file(self): if os.path.isfile(partition_output): os.remove(partition_output) @@ -55,7 +56,7 @@ def test_no_partition_file(self): test_partitioner.get_file_list(INFER) -class ImageSetsPartitionerNewPartition(tf.test.TestCase): +class ImageSetsPartitionerNewPartition(NiftyNetTestCase): def test_new_partition(self): data_param = test_sections test_partitioner = ImageSetsPartitioner() @@ -93,7 +94,7 @@ def test_new_partition(self): self.assertTrue(test_partitioner.has_validation) -class ImageSetsPartitionerIllPartition(tf.test.TestCase): +class ImageSetsPartitionerIllPartition(NiftyNetTestCase): def test_incompatible_partition_file(self): self._reset_partition_file() # adding invalid line diff --git a/tests/image_type_test.py b/tests/image_type_test.py index af038d98..6a383e2d 100755 --- a/tests/image_type_test.py +++ b/tests/image_type_test.py @@ -12,6 +12,7 @@ from niftynet.io.image_type import SpatialImage4D from niftynet.io.image_type import SpatialImage5D from niftynet.io.misc_io import set_logger +from tests.niftynet_testcase import NiftyNetTestCase CASE_2D = 'testing_data/2d_3_000044.nii.gz' CASE_3D_a = 'testing_data/1040_o_T1_time_01.nii.gz' @@ -19,7 +20,7 @@ CASE_5D = 'testing_data/pat2__niftynet_out.nii.gz' -class ImageTypeTest(tf.test.TestCase): +class ImageTypeTest(NiftyNetTestCase): def test_2d(self): image = ImageFactory.create_instance( file_path=CASE_2D, diff --git a/tests/image_window_dataset_generator_test.py b/tests/image_window_dataset_generator_test.py index 6c8b45de..0e04bd1c 100755 --- a/tests/image_window_dataset_generator_test.py +++ b/tests/image_window_dataset_generator_test.py @@ -10,6 +10,7 @@ from niftynet.engine.image_window import N_SPATIAL, LOCATION_FORMAT from niftynet.engine.image_window_dataset import ImageWindowDataset from niftynet.io.image_reader import ImageReader +from tests.niftynet_testcase import NiftyNetTestCase IMAGE_PATH_2D_1 = os.path.join('.', 'example_volumes', 'gan_test_data') IMAGE_PATH_3D = os.path.join('.', 'testing_data') @@ -53,7 +54,7 @@ def layer_op(self): @unittest.skip("temp skipping window generator test") -class ImageWindowDataset_Generator_2D_Test(tf.test.TestCase): +class ImageWindowDataset_Generator_2D_Test(NiftyNetTestCase): def assert_window(self, window): if not isinstance(window, dict): window = next(window) @@ -64,7 +65,7 @@ def assert_window(self, window): self.assertEqual(window['mr_location'].dtype, np.int32) def assert_tf_window(self, sampler): - with self.test_session() as sess: + with self.cached_session() as sess: window = sess.run(sampler.pop_batch_op()) self.assert_window(window) @@ -112,7 +113,7 @@ def test_epoch(self): batch_size = 3 sampler = ImageWindowGenerator( reader=reader, batch_size=batch_size, epoch=1) - with self.test_session() as sess: + with self.cached_session() as sess: next_element = sampler.pop_batch_op() iters = 0 try: @@ -127,7 +128,7 @@ def test_epoch(self): @unittest.skip("temp skipping window generator test") -class ImageWindowDataset_Generator_3D_Test(tf.test.TestCase): +class ImageWindowDataset_Generator_3D_Test(NiftyNetTestCase): def assert_window(self, window): if not isinstance(window, dict): window = next(window) @@ -138,7 +139,7 @@ def assert_window(self, window): self.assertEqual(window['mr_location'].dtype, np.int32) def assert_tf_window(self, sampler): - with self.test_session() as sess: + with self.cached_session() as sess: window = sess.run(sampler.pop_batch_op()) self.assert_window(window) @@ -179,7 +180,7 @@ def test_epoch(self): batch_size = 3 sampler = ImageWindowGenerator( reader=reader, batch_size=batch_size, epoch=1) - with self.test_session() as sess: + with self.cached_session() as sess: next_element = sampler.pop_batch_op() iters = 0 try: @@ -194,11 +195,11 @@ def test_epoch(self): @unittest.skip("temp skipping window generator test") -class ImageDatasetParamTest(tf.test.TestCase): +class ImageDatasetParamTest(NiftyNetTestCase): def run_dataset(self, n_iters, n_threads, **kwargs): sampler = ImageWindowGenerator(**kwargs) sampler.set_num_threads(n_threads) - with self.test_session() as sess: + with self.cached_session() as sess: true_iters = 0 next_element = sampler.pop_batch_op() windows = [] diff --git a/tests/image_window_dataset_test.py b/tests/image_window_dataset_test.py index 9a53bd1f..86854d7b 100755 --- a/tests/image_window_dataset_test.py +++ b/tests/image_window_dataset_test.py @@ -8,6 +8,7 @@ from niftynet.engine.image_window_dataset import ImageWindowDataset from niftynet.io.image_reader import ImageReader +from tests.niftynet_testcase import NiftyNetTestCase IMAGE_PATH_2D_1 = os.path.join('.', 'example_volumes', 'gan_test_data') IMAGE_PATH_3D = os.path.join('.', 'testing_data') @@ -27,7 +28,7 @@ def get_3d_reader(): return reader -class ImageWindowDataset_2D_Test(tf.test.TestCase): +class ImageWindowDataset_2D_Test(NiftyNetTestCase): def assert_window(self, window): if not isinstance(window, dict): window = next(window) @@ -38,7 +39,7 @@ def assert_window(self, window): self.assertEqual(window['mr_location'].dtype, np.int32) def assert_tf_window(self, sampler): - with self.test_session() as sess: + with self.cached_session() as sess: window = sess.run(sampler.pop_batch_op()) self.assert_window(window) @@ -83,7 +84,7 @@ def test_epoch(self): batch_size = 3 sampler = ImageWindowDataset( reader=reader, batch_size=batch_size, epoch=1) - with self.test_session() as sess: + with self.cached_session() as sess: next_element = sampler.pop_batch_op() iters = 0 try: @@ -97,7 +98,7 @@ def test_epoch(self): np.ceil(reader.num_subjects / np.float(batch_size)), iters) -class ImageWindowDataset_3D_Test(tf.test.TestCase): +class ImageWindowDataset_3D_Test(NiftyNetTestCase): def assert_window(self, window): if not isinstance(window, dict): window = next(window) @@ -108,7 +109,7 @@ def assert_window(self, window): self.assertEqual(window['mr_location'].dtype, np.int32) def assert_tf_window(self, sampler): - with self.test_session() as sess: + with self.cached_session() as sess: window = sess.run(sampler.pop_batch_op()) self.assert_window(window) @@ -146,7 +147,7 @@ def test_epoch(self): batch_size = 3 sampler = ImageWindowDataset( reader=reader, batch_size=batch_size, epoch=1) - with self.test_session() as sess: + with self.cached_session() as sess: next_element = sampler.pop_batch_op() iters = 0 try: @@ -160,11 +161,11 @@ def test_epoch(self): np.ceil(reader.num_subjects / np.float(batch_size)), iters) -class ImageDatasetParamTest(tf.test.TestCase): +class ImageDatasetParamTest(NiftyNetTestCase): def run_dataset(self, n_iters, n_threads, **kwargs): sampler = ImageWindowDataset(**kwargs) sampler.set_num_threads(n_threads) - with self.test_session() as sess: + with self.cached_session() as sess: true_iters = 0 next_element = sampler.pop_batch_op() windows = [] diff --git a/tests/image_window_test.py b/tests/image_window_test.py index 79ba9053..78a185bf 100755 --- a/tests/image_window_test.py +++ b/tests/image_window_test.py @@ -5,6 +5,7 @@ from niftynet.engine.image_window import ImageWindow from niftynet.utilities.util_common import ParserNamespace +from tests.niftynet_testcase import NiftyNetTestCase def get_static_window_param(): @@ -133,7 +134,7 @@ def get_ill_image_window_2(): # ) -class ImageWindowTest(tf.test.TestCase): +class ImageWindowTest(NiftyNetTestCase): def test_init(self): window = ImageWindow.from_data_reader_properties( **get_static_window_param()) diff --git a/tests/interventional_affine_net_test.py b/tests/interventional_affine_net_test.py index d58d5e6f..2cca0af6 100755 --- a/tests/interventional_affine_net_test.py +++ b/tests/interventional_affine_net_test.py @@ -4,9 +4,10 @@ from niftynet.network.toynet import ToyNet from niftynet.network.interventional_affine_net import INetAffine +from tests.niftynet_testcase import NiftyNetTestCase -class INetAffineTest(tf.test.TestCase): +class INetAffineTest(NiftyNetTestCase): def test_3d_shape(self): input_shape = (2, 32, 32, 32, 1) x = tf.ones(input_shape) @@ -15,7 +16,7 @@ def test_3d_shape(self): out = affinenet_instance(x, x, is_training=True) print(affinenet_instance) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 32, 32, 32, 3), out.shape) @@ -28,7 +29,7 @@ def test_2d_shape(self): out = affinenet_instance(x, x, is_training=True) print(affinenet_instance) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 32, 32, 2), out.shape) diff --git a/tests/interventional_dense_net_test.py b/tests/interventional_dense_net_test.py index aab27f32..aa865c31 100755 --- a/tests/interventional_dense_net_test.py +++ b/tests/interventional_dense_net_test.py @@ -3,9 +3,9 @@ import tensorflow as tf from niftynet.network.interventional_dense_net import INetDense +from tests.niftynet_testcase import NiftyNetTestCase - -class INetDenseTest(tf.test.TestCase): +class INetDenseTest(NiftyNetTestCase): def test_3d_shape(self): input_shape = (2, 32, 32, 32, 1) x = tf.ones(input_shape) @@ -14,7 +14,7 @@ def test_3d_shape(self): out = densenet_instance(x, x, is_training=True) print(densenet_instance) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 32, 32, 32, 3), out.shape) @@ -27,7 +27,7 @@ def test_multi_scale_3d_shape(self): out = densenet_instance(x, x, is_training=True) print(densenet_instance) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 16, 16, 16, 3), out.shape) @@ -40,7 +40,7 @@ def test_multi_scale_3d_1_shape(self): out = densenet_instance(x, x, is_training=True) print(densenet_instance) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 48, 48, 48, 3), out.shape) @@ -53,7 +53,7 @@ def test_2d_shape(self): out = densenet_instance(x, x, is_training=True) print(densenet_instance) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 16, 16, 2), out.shape) diff --git a/tests/interventional_hybrid_net_test.py b/tests/interventional_hybrid_net_test.py index 6523a710..19736928 100755 --- a/tests/interventional_hybrid_net_test.py +++ b/tests/interventional_hybrid_net_test.py @@ -4,9 +4,10 @@ from niftynet.network.toynet import ToyNet from niftynet.network.interventional_hybrid_net import INetHybridPreWarp, INetHybridTwoStream +from tests.niftynet_testcase import NiftyNetTestCase -class INetHybridPreWarpTest(tf.test.TestCase): +class INetHybridPreWarpTest(NiftyNetTestCase): def test_3d_shape(self): input_shape = (2, 32, 32, 32, 1) @@ -16,7 +17,7 @@ def test_3d_shape(self): out = hybridnet_instance(x, x, is_training=True) print(hybridnet_instance) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 32, 32, 32, 3), out[0].shape) @@ -30,13 +31,13 @@ def test_2d_shape(self): out = hybridnet_instance(x, x, is_training=True) print(hybridnet_instance) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 32, 32, 2), out[0].shape) self.assertAllClose((2, 32, 32, 2), out[1].shape) -class INetHybridTwoStreamTest(tf.test.TestCase): +class INetHybridTwoStreamTest(NiftyNetTestCase): def test_3d_shape(self): @@ -47,7 +48,7 @@ def test_3d_shape(self): out = hybridnet_instance(x, x, is_training=True) print(hybridnet_instance) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 32, 32, 32, 3), out[0].shape) @@ -62,7 +63,7 @@ def test_2d_shape(self): out = hybridnet_instance(x, x, is_training=True) print(hybridnet_instance) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 32, 32, 2), out[0].shape) diff --git a/tests/linear_resize_test.py b/tests/linear_resize_test.py index ae201db0..dee7e870 100755 --- a/tests/linear_resize_test.py +++ b/tests/linear_resize_test.py @@ -4,9 +4,10 @@ import tensorflow as tf from niftynet.layer.linear_resize import LinearResizeLayer +from tests.niftynet_testcase import NiftyNetTestCase -class LinearResizeTest(tf.test.TestCase): +class LinearResizeTest(NiftyNetTestCase): def get_3d_input(self): input_shape = (2, 16, 16, 16, 4) x = tf.ones(input_shape) @@ -30,7 +31,7 @@ def run_test(self, new_size, expected_spatial_shape, is_3d=True): resize_layer = LinearResizeLayer(new_size=new_size) resized = resize_layer(x) print(resize_layer) - with self.test_session() as sess: + with self.cached_session() as sess: out = sess.run(resized) self.assertAllClose(out.shape, expected_shape) diff --git a/tests/loss_classification_multi_test.py b/tests/loss_classification_multi_test.py new file mode 100755 index 00000000..ed1876a1 --- /dev/null +++ b/tests/loss_classification_multi_test.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function + +import numpy as np +import tensorflow as tf + +from niftynet.layer.loss_classification_multi import LossFunction, variability +from tests.niftynet_testcase import NiftyNetTestCase + +class VariabilityTests(NiftyNetTestCase): + def test_variability_value(self): + # test value is -0.5 * [1 * log(e / (1+e)) + 1 * log(e^2 / (e^2 + 1))] + with self.cached_session(): + # [0,1,0] 2/3 , 1/3 4/9 + # [0,0,0] 1, 0 0 + predicted = [[0, 1,0],[1, 1,1]] + + computed_variability = variability(predicted, nrater=3) + self.assertAlmostEqual( + computed_variability[0].eval(),4.0/9.0) + + + +class LossConfusionTest(NiftyNetTestCase): + def test_confusion_matrix_loss(self): + with self.cached_session(): + predicted = tf.constant([[[1,-1],[-1,1],[1,-1]],[[1,-1],[1,-1],[1, + -1]]], + dtype=tf.float32) + predicted *= 1000 + ground_truth = [[0,0,1],[0,0,1]] + test_loss_func = LossFunction(2, 3, loss_type='ConfusionMatrix', + loss_func_params={'nrater':3}) + computed_loss = test_loss_func(ground_truth=ground_truth, + pred_multi=predicted) + self.assertAlmostEqual(computed_loss.eval(), 4.0/3.0) + +class LossVariabilityTest(NiftyNetTestCase): + def test_variability_loss(self): + with self.cached_session(): + predicted = tf.constant([[[1,-1],[-1,1],[1,-1]],[[1,-1],[1,-1],[1, + -1]]], + dtype=tf.float32) + predicted *= 1000 + ground_truth = [[0,0,1],[0,0,1]] + test_loss_func = LossFunction(2, 3, loss_type='Variability') + computed_loss = test_loss_func(ground_truth=ground_truth, + pred_multi=predicted) + self.assertAlmostEqual(computed_loss.eval(), np.sqrt(16.0/162.0)) + + +class LossConsistencyTest(NiftyNetTestCase): + def test_consistency_loss(self): + with self.cached_session(): + predicted = tf.constant([[[1,-1],[-1,1],[1,-1]],[[1,-1],[1,-1],[1, + -1]]], + dtype=tf.float32) + predicted *= 1000 + pred_ave = [[[0.66,0.33],[1,0]]] + test_loss_func = LossFunction(2, 3, loss_type='Consistency') + computed_loss = test_loss_func(pred_ave=pred_ave, + pred_multi=predicted) + self.assertAllClose(computed_loss.eval(), 0, atol=1E-2) + + + +# class LossFunctionErrorTest(NiftyNetTestCase): +# """ +# These tests check that a ValueError is called +# for non-existent loss functions. +# They also check that suggestions are returned +# if the name is close to a real one. +# """ +# +# def test_value_error_for_bad_loss_function(self): +# with self.cached_session(): +# with self.assertRaises(ValueError): +# LossFunction(0, loss_type='wrong answer') +# +# # Note: sensitive to precise wording of ValueError message. +# def test_suggestion_for_dice_typo(self): +# with self.cached_session(): +# with self.assertRaisesRegexp(ValueError, 'CrossEntropy'): +# LossFunction(0, loss_type='cross_entropy') + + +if __name__ == '__main__': + tf.test.main() diff --git a/tests/loss_classification_test.py b/tests/loss_classification_test.py index 3dcd63c9..0977efc8 100755 --- a/tests/loss_classification_test.py +++ b/tests/loss_classification_test.py @@ -5,12 +5,13 @@ import tensorflow as tf from niftynet.layer.loss_classification import LossFunction +from tests.niftynet_testcase import NiftyNetTestCase -class CrossEntropyTests(tf.test.TestCase): +class CrossEntropyTests(NiftyNetTestCase): def test_cross_entropy_value(self): # test value is -0.5 * [1 * log(e / (1+e)) + 1 * log(e^2 / (e^2 + 1))] - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[0, 1], [2, 0]], dtype=tf.float32, name='predicted') @@ -23,7 +24,7 @@ def test_cross_entropy_value(self): np.e ** 2 / (1 + np.e ** 2)))) -class LossFunctionErrorsTest(tf.test.TestCase): +class LossFunctionErrorsTest(NiftyNetTestCase): """ These tests check that a ValueError is called for non-existent loss functions. @@ -32,13 +33,13 @@ class LossFunctionErrorsTest(tf.test.TestCase): """ def test_value_error_for_bad_loss_function(self): - with self.test_session(): + with self.cached_session(): with self.assertRaises(ValueError): LossFunction(0, loss_type='wrong answer') # Note: sensitive to precise wording of ValueError message. def test_suggestion_for_dice_typo(self): - with self.test_session(): + with self.cached_session(): with self.assertRaisesRegexp(ValueError, 'CrossEntropy'): LossFunction(0, loss_type='cross_entropy') diff --git a/tests/loss_regression_test.py b/tests/loss_regression_test.py index 071c590a..b46f6d41 100755 --- a/tests/loss_regression_test.py +++ b/tests/loss_regression_test.py @@ -4,12 +4,14 @@ import tensorflow as tf from niftynet.layer.loss_regression import LossFunction -from niftynet.layer.loss_regression import l1_loss, l2_loss, huber_loss +from niftynet.layer.loss_regression import l1_loss, l2_loss, huber_loss, \ + smooth_l1_loss, cosine_loss +from tests.niftynet_testcase import NiftyNetTestCase -class L1LossTests(tf.test.TestCase): +class L1LossTests(NiftyNetTestCase): def test_l1_loss_value(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [1, 1], dtype=tf.float32, name='predicted') @@ -20,7 +22,7 @@ def test_l1_loss_value(self): computed_l1_loss.eval(), 0.5) def test_l1_loss_value_weight(self): - with self.test_session(): + with self.cached_session(): weights = tf.constant( [[1, 2]], dtype=tf.float32, name='weights') predicted = tf.constant( @@ -32,7 +34,7 @@ def test_l1_loss_value_weight(self): def test_l1_loss(self): # expected loss: mean(.2 + 2) = 1.1 - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [1.2, 1], dtype=tf.float32, name='predicted') @@ -42,9 +44,9 @@ def test_l1_loss(self): l1_loss(predicted, gold_standard).eval(), 1.1) -class L2LossTests(tf.test.TestCase): +class L2LossTests(NiftyNetTestCase): def test_l2_loss_value(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[1, 2]], dtype=tf.float32, name='predicted') labels = tf.constant( @@ -54,7 +56,7 @@ def test_l2_loss_value(self): self.assertAlmostEqual(computed_l2_loss.eval(), 2.0) def test_l2_loss_value_weight(self): - with self.test_session(): + with self.cached_session(): weights = tf.constant( [[1, 2]], dtype=tf.float32, name='weights') predicted = tf.constant( @@ -69,7 +71,7 @@ def test_l2_loss_value_weight(self): def test_l2_loss(self): # expected loss: (0.04 + 4 + 1) /2 = 2.52 # (note - not the mean, just the sum) - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [1.2, 1, 2], dtype=tf.float32, name='predicted') @@ -80,9 +82,9 @@ def test_l2_loss(self): l2_loss(predicted, gold_standard).eval(), 2.52) -class HuberLossTests(tf.test.TestCase): +class HuberLossTests(NiftyNetTestCase): def test_huber_loss(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[0, 10], [10, 0], [10, 0], [10, 0]], dtype=tf.float32, name='predicted') @@ -92,7 +94,7 @@ def test_huber_loss(self): self.assertEqual(huber_loss(predicted, gold_standard).eval(), 0.0) def test_huber_continuous(self): - with self.test_session(): + with self.cached_session(): epsilon = tf.constant( 1e-10, dtype=tf.float32) predicted = tf.constant( @@ -107,7 +109,7 @@ def test_huber_continuous(self): huber_loss_outside_delta.eval()) def test_huber_loss_hand_example(self): - with self.test_session(): + with self.cached_session(): # loss should be: mean( 0.2 ** 2/ 2 + (2-0.5) ) == 1.52/2 == 0.76 predicted = tf.constant( [1.2, 1], dtype=tf.float32, name='predicted') @@ -117,7 +119,7 @@ def test_huber_loss_hand_example(self): self.assertAlmostEqual(loss.eval(), .76) def test_huber_loss_value(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[1, 2, 0.5]], dtype=tf.float32, name='predicted') labels = tf.constant( @@ -128,7 +130,7 @@ def test_huber_loss_value(self): computed_huber_loss.eval(), 0.5417, places=4) def test_huber_loss_value_weight(self): - with self.test_session(): + with self.cached_session(): weights = tf.constant( [[1, 2, 1]], dtype=tf.float32, name='weights') predicted = tf.constant( @@ -141,9 +143,9 @@ def test_huber_loss_value_weight(self): computed_huber_loss.eval(), 3.125 / 4) -class RMSELossTests(tf.test.TestCase): +class RMSELossTests(NiftyNetTestCase): def test_rmse_loss_value(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[1.2, 1]], dtype=tf.float32, name='predicted') labels = tf.constant( @@ -154,7 +156,7 @@ def test_rmse_loss_value(self): computed_rmse_loss.eval(), 0.7211, places=4) def test_rmse_loss_value_weight(self): - with self.test_session(): + with self.cached_session(): weights = tf.constant( [[1, 2.1]], dtype=tf.float32, name='weights') predicted = tf.constant( @@ -166,9 +168,9 @@ def test_rmse_loss_value_weight(self): computed_rmse_loss.eval(), 0.8231, places=4) -class MAELossTests(tf.test.TestCase): +class MAELossTests(NiftyNetTestCase): def test_MAE_loss_value(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[1, 2]], dtype=tf.float32, name='predicted') labels = tf.constant( @@ -179,7 +181,7 @@ def test_MAE_loss_value(self): computed_MAE_loss.eval(), 1.1) def test_MAE_loss_value_weight(self): - with self.test_session(): + with self.cached_session(): weights = tf.constant( [[1, 2]], dtype=tf.float32, name='weights') predicted = tf.constant( @@ -191,5 +193,122 @@ def test_MAE_loss_value_weight(self): computed_MAE_loss.eval(), 2.0 / 3.0) +class SmoothL1LossTests(NiftyNetTestCase): + def test_smooth_l1_loss(self): + with self.cached_session(): + predicted = tf.constant( + [[0, 10], [10, 0], [10, 0], [10, 0]], + dtype=tf.float32, name='predicted') + gold_standard = tf.constant( + [[0, 10], [10, 0], [10, 0], [10, 0]], + dtype=tf.float32, name='gold_standard') + self.assertEqual(huber_loss(predicted, gold_standard).eval(), 0.0) + + def test_smooth_l1_continuous(self): + with self.cached_session(): + epsilon = tf.constant( + 1e-10, dtype=tf.float32) + predicted = tf.constant( + [0.5], dtype=tf.float32, name='predicted') + gold_standard = tf.constant( + [0], dtype=tf.float32, name='gold_standard') + huber_loss_inside_delta = smooth_l1_loss( + predicted + epsilon, gold_standard, value_thresh=0.5) + huber_loss_outside_delta = smooth_l1_loss( + predicted - epsilon, gold_standard) + self.assertAlmostEqual(huber_loss_inside_delta.eval(), + huber_loss_outside_delta.eval()) + + def test_smooth_l1_continuous_max(self): + with self.cached_session(): + epsilon = tf.constant( + 1e-10, dtype=tf.float32) + predicted = tf.constant( + [2.375], dtype=tf.float32, name='predicted') + gold_standard = tf.constant( + [0], dtype=tf.float32, name='gold_standard') + huber_loss_inside_delta = smooth_l1_loss( + predicted + epsilon, gold_standard, value_thresh=0.5) + huber_loss_outside_delta = smooth_l1_loss( + predicted - epsilon, gold_standard) + self.assertAlmostEqual(huber_loss_inside_delta.eval(), + huber_loss_outside_delta.eval()) + + def test_smooth_loss_value(self): + with self.cached_session(): + predicted = tf.constant( + [[1, 2.375, 0.5]], dtype=tf.float32, name='predicted') + labels = tf.constant( + [[1, 0, 1]], dtype=tf.float32, name='labels') + test_loss_func = LossFunction(loss_type='SmoothL1') + computed_smooth_loss = test_loss_func(predicted, labels) + self.assertAlmostEqual( + computed_smooth_loss.eval(), 2.125/3, places=4) + + def test_smooth_loss_value_weight(self): + with self.cached_session(): + weights = tf.constant( + [[1, 2, 1]], dtype=tf.float32, name='weights') + predicted = tf.constant( + [[1, 2.375, 0.5]], dtype=tf.float32, name='predicted') + labels = tf.constant([[1, 0, 1]], dtype=tf.float32, name='labels') + test_loss_func = LossFunction(loss_type='SmoothL1') + computed_smooth_l1_loss = test_loss_func( + predicted, labels, weight_map=weights) + self.assertAlmostEqual( + computed_smooth_l1_loss.eval(), 4.125 / 4) + + +class CosineLossTests(NiftyNetTestCase): + def test_cosine_loss_value(self): + + with self.cached_session(): + predicted = tf.constant( + [[[1, 0],[0.5,0.5]]], dtype=tf.float32, name='predicted') + labels = tf.constant( + [[[0, 1],[0.5,0.5]]], dtype=tf.float32, name='labels') + test_loss_func = LossFunction(loss_type='Cosine') + computed_cosine_loss = test_loss_func(predicted, labels) + self.assertAlmostEqual( + computed_cosine_loss.eval(), 0.5) + + def test_cosine_loss_value_equal(self): + + with self.cached_session(): + predicted = tf.constant( + [[[0.5,0.5]]], dtype=tf.float32, name='predicted') + labels = tf.constant( + [[[0.5,0.5]]], dtype=tf.float32, name='labels') + test_loss_func = LossFunction(loss_type='Cosine') + computed_cosine_loss = test_loss_func(predicted, labels) + self.assertAlmostEqual( + computed_cosine_loss.eval(), 0) + + def test_cosine_loss_value_equal2(self): + + with self.cached_session(): + predicted = tf.constant( + [[[1, 0],[0.5,0.5]]], dtype=tf.float32, name='predicted') + labels = tf.constant( + [[[1, 0],[0.5,0.5]]], dtype=tf.float32, name='labels') + test_loss_func = LossFunction(loss_type='Cosine') + computed_cosine_loss = test_loss_func(predicted, labels) + self.assertAlmostEqual( + computed_cosine_loss.eval(), 0) + + def test_cosine_loss_value_weight(self): + with self.cached_session(): + weights = tf.constant( + [[[2], [1]]], dtype=tf.float32, name='weights') + predicted = tf.constant( + [[[1, 0],[0.5,0.5]]], dtype=tf.float32, name='predicted') + labels = tf.constant( + [[[0, 1],[0.5,0.5]]], dtype=tf.float32, name='labels') + test_loss_func = LossFunction(loss_type='Cosine') + computed_cosine_loss = test_loss_func(predicted, labels, weights) + self.assertAlmostEqual( + computed_cosine_loss.eval(), 2.0 / 3.0) + + if __name__ == '__main__': tf.test.main() diff --git a/tests/loss_segmentation_test.py b/tests/loss_segmentation_test.py index 6d511da6..53cfff58 100755 --- a/tests/loss_segmentation_test.py +++ b/tests/loss_segmentation_test.py @@ -5,12 +5,13 @@ import tensorflow as tf from niftynet.layer.loss_segmentation import LossFunction, labels_to_one_hot +from tests.niftynet_testcase import NiftyNetTestCase -class DiceWithMissingClass(tf.test.TestCase): +class DiceWithMissingClass(NiftyNetTestCase): # all dice methods should return 0.0 for this case: def test_missing_class(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[0, 0, 1], [1, 0, 0]], dtype=tf.float32, name='predicted') @@ -30,7 +31,7 @@ def test_missing_class(self): self.assertAllClose(loss_value.eval(), 0.0, atol=1e-4) def test_missing_class_dice_plus_xent(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[0, 0, 999], [999, 0, 0]], dtype=tf.float32, name='predicted') @@ -45,9 +46,9 @@ def test_missing_class_dice_plus_xent(self): self.assertAllClose(loss_value.eval(), -1.0, atol=1e-4) -class DicePlusXEntTest(tf.test.TestCase): +class DicePlusXEntTest(NiftyNetTestCase): def test_dice_plus(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[0, 9999], [9999, 0], [9999, 0], [9999, 0]], dtype=tf.float32, name='predicted') @@ -60,7 +61,7 @@ def test_dice_plus(self): self.assertAllClose(loss_value.eval(), -1.0, atol=1e-3) def test_dice_plus_multilabel(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[0, 0, 9999], [9999, 0, 0], [0, 9999, 0], [9999, 0, 0]], dtype=tf.float32, name='predicted') @@ -73,7 +74,7 @@ def test_dice_plus_multilabel(self): self.assertAllClose(loss_value.eval(), -1.0, atol=1e-3) def test_dice_plus_non_zeros(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[0, 9999, 9999], [9999, 0, 0], [0, 9999, 9999], [9999, 0, 0]], dtype=tf.float32, name='predicted') @@ -86,7 +87,7 @@ def test_dice_plus_non_zeros(self): self.assertAllClose(loss_value.eval(), .5 * np.log(2) - 2. / 3., atol=1e-3) def test_dice_plus_wrong_softmax(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[0, 9999, 9999], [9999, 0, 0], [0, 9999, 9999], [9999, 0, 0]], dtype=tf.float32, name='predicted') @@ -99,7 +100,7 @@ def test_dice_plus_wrong_softmax(self): self.assertAllClose(loss_value.eval(), .5 * np.log(2) - 2. / 3., atol=1e-3) def test_dice_plus_weighted(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[0, 9999, 9999], [9999, 0, 0], [0, 9999, 9999], [9999, 0, 0]], dtype=tf.float32, name='predicted') @@ -111,9 +112,9 @@ def test_dice_plus_weighted(self): self.assertAllClose(loss_value.eval(), -1.) -class OneHotTester(tf.test.TestCase): +class OneHotTester(NiftyNetTestCase): def test_vs_tf_onehot(self): - with self.test_session(): + with self.cached_session(): labels = tf.constant([1, 2, 3, 0], dtype=tf.int64, name='labels') tf_one_hot = tf.one_hot(labels, depth=4) niftynet_one_hot = tf.sparse_tensor_to_dense(labels_to_one_hot(labels, 4)) @@ -125,7 +126,7 @@ def test_one_hot(self): [[0., 0., 0., 1., 0.], [0., 0., 0., 0., 1.]]], dtype=np.float32) - with self.test_session(): + with self.cached_session(): labels = tf.constant([[1, 2], [3, 4]]) # import pdb; pdb.set_trace() one_hot = tf.sparse_tensor_to_dense( @@ -133,10 +134,10 @@ def test_one_hot(self): self.assertAllEqual(one_hot, ref) -class SensitivitySpecificityTests(tf.test.TestCase): +class SensitivitySpecificityTests(NiftyNetTestCase): # test done by regression for refactoring purposes def test_sens_spec_loss_by_regression(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[0, 10], [10, 0], [10, 0], [10, 0]], dtype=tf.float32, name='predicted') @@ -148,7 +149,7 @@ def test_sens_spec_loss_by_regression(self): self.assertAlmostEqual(test_loss.eval(), 2.06106e-9) def test_multi_label_sens_spec(self): - with self.test_session(): + with self.cached_session(): # answer calculated by hand - predicted = tf.constant( [[0, 1, 0], [0, 0, 1]], @@ -162,10 +163,10 @@ def test_multi_label_sens_spec(self): self.assertAlmostEqual(test_loss.eval(), 0.14598623) -class GeneralisedDiceTest(tf.test.TestCase): +class GeneralisedDiceTest(NiftyNetTestCase): # test done by regression for refactoring purposes def test_generalised_dice_score_regression(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[0, 10], [10, 0], [10, 0], [10, 0]], dtype=tf.float32, name='predicted') @@ -179,7 +180,7 @@ def test_generalised_dice_score_regression(self): one_minus_generalised_dice_score.eval(), 0.0, atol=1e-4) def test_gdsc_incorrect_type_weight_error(self): - with self.test_session(): + with self.cached_session(): with self.assertRaises(ValueError) as cm: predicted = tf.constant( [[0, 10], [10, 0], [10, 0], [10, 0]], @@ -196,7 +197,7 @@ def test_gdsc_incorrect_type_weight_error(self): labels) def test_generalised_dice_score_uniform_regression(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant([[0, 10], [10, 0], [10, 0], [10, 0]], dtype=tf.float32, name='predicted') @@ -214,9 +215,9 @@ def test_generalised_dice_score_uniform_regression(self): 0.3333, atol=1e-4) -class DiceTest(tf.test.TestCase): +class DiceTest(NiftyNetTestCase): def test_dice_score(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[0, 10], [10, 0], [10, 0], [10, 0]], dtype=tf.float32, name='predicted') @@ -228,7 +229,7 @@ def test_dice_score(self): self.assertAllClose(one_minus_dice_score.eval(), 0.0, atol=1e-5) def test_dice_score_weights(self): - with self.test_session(): + with self.cached_session(): weights = tf.constant([[1, 1, 0, 0]], dtype=tf.float32, name='weights') predicted = tf.constant( @@ -243,7 +244,7 @@ def test_dice_score_weights(self): self.assertAllClose(one_minus_dice_score.eval(), 0.0, atol=1e-4) def test_wrong_prediction(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[0, 100]], dtype=tf.float32, name='predicted') @@ -257,7 +258,7 @@ def test_wrong_prediction(self): def test_dice_batch_size_greater_than_one(self): # test for Github issue #22: need to take mean per-image before # averaging Dice of ~1 and ~0.16, should get dice ~ 1 - 0.5816 - with self.test_session(): + with self.cached_session(): # predictions ~ [1, 0, 0]; [0, 0, 1]; [0, .5, .5]; [.333, .333, .333] predictions_numpy = np.array([[[10., 0, 0], [0, 0, 10]], [[-10, 0, 0], [0, 0, 0]]]).reshape([2, 2, 1, 1, 3]) @@ -272,10 +273,10 @@ def test_dice_batch_size_greater_than_one(self): self.assertAllClose(one_minus_dice_score.eval(), 1 - 0.5816, atol=1e-4) -class CrossEntropyTests(tf.test.TestCase): +class CrossEntropyTests(NiftyNetTestCase): def test_cross_entropy_value(self): # test value is -0.5 * [1 * log(e / (1+e)) + 1 * log(e^2 / (e^2 + 1))] - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[0, 1], [2, 0]], dtype=tf.float32, name='predicted') @@ -298,7 +299,7 @@ def test_cross_entropy_value(self): np.e ** 2 / (1 + np.e ** 2)))) def test_cross_entropy_value_weight(self): - with self.test_session(): + with self.cached_session(): weights = tf.constant([[1], [2]], dtype=tf.float32, name='weights') predicted = tf.constant( [[0, 1], [2, 0]], @@ -316,9 +317,9 @@ def test_cross_entropy_value_weight(self): np.e ** 2 / (1 + np.e ** 2)))) -class DiceTestNoSquare(tf.test.TestCase): +class DiceTestNoSquare(NiftyNetTestCase): def test_dice_score_nosquare(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[0, 10], [10, 0], [10, 0], [10, 0]], dtype=tf.float32, name='predicted') @@ -330,7 +331,7 @@ def test_dice_score_nosquare(self): self.assertAllClose(one_minus_dice_score.eval(), 0.0, atol=1e-4) def test_dice_score_nosquare_weights(self): - with self.test_session(): + with self.cached_session(): weights = tf.constant([[1, 1, 0, 0]], dtype=tf.float32, name='weights') predicted = tf.constant( @@ -346,7 +347,7 @@ def test_dice_score_nosquare_weights(self): self.assertAllClose(one_minus_dice_score.eval(), 0.0, atol=1e-4) def test_wrong_prediction(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[0, 100]], dtype=tf.float32, name='predicted') @@ -358,9 +359,9 @@ def test_wrong_prediction(self): self.assertAllClose(one_minus_dice_score.eval(), 1.0, atol=1e-4) -class TverskyTest(tf.test.TestCase): +class TverskyTest(NiftyNetTestCase): def test_tversky_index(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[0, 10], [10, 0], [10, 0], [10, 0]], dtype=tf.float32, name='predicted') @@ -373,7 +374,7 @@ def test_tversky_index(self): self.assertAllClose(one_minus_tversky_index.eval(), 0.0, atol=1e-4) def test_tversky_index_weights(self): - with self.test_session(): + with self.cached_session(): weights = tf.constant([[1, 1, 0, 0]], dtype=tf.float32, name='weights') predicted = tf.constant( @@ -390,7 +391,7 @@ def test_tversky_index_weights(self): self.assertAllClose(one_minus_tversky_index.eval(), 0.0, atol=1e-4) def test_wrong_prediction(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[0, 100]], dtype=tf.float32, name='predicted') @@ -403,9 +404,9 @@ def test_wrong_prediction(self): self.assertAlmostEqual(one_minus_tversky_index.eval(), 1.0) -class DiceDenseTest(tf.test.TestCase): +class DiceDenseTest(NiftyNetTestCase): def test_dice_dense_score(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[0, 10], [10, 0], [10, 0], [10, 0]], dtype=tf.float32, name='predicted') @@ -418,7 +419,7 @@ def test_dice_dense_score(self): self.assertAllClose(one_minus_dice_score.eval(), 1.0, atol=1e-4) def test_wrong_prediction(self): - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[0, 100]], dtype=tf.float32, name='predicted') @@ -432,7 +433,7 @@ def test_wrong_prediction(self): def test_dense_dice_vs_sparse(self): # regression test vs dense version - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[2, 3], [9, 8], [0, 0], [1, 0]], dtype=tf.float32, name='predicted') @@ -450,11 +451,11 @@ def test_dense_dice_vs_sparse(self): self.assertAllEqual(sparse_dice.eval(), dense_dice.eval()) -class DiceDenseNoSquareTest(tf.test.TestCase): +class DiceDenseNoSquareTest(NiftyNetTestCase): def test_dense_dice_nosquare_vs_sparse(self): # regression test vs dense version - with self.test_session(): + with self.cached_session(): predicted = tf.constant( [[2, 3], [9, 8], [0, 0], [1, 0]], dtype=tf.float32, name='predicted') @@ -472,7 +473,39 @@ def test_dense_dice_nosquare_vs_sparse(self): self.assertAllEqual(sparse_dice.eval(), dense_dice.eval()) -class LossFunctionErrorsTest(tf.test.TestCase): +class VolumeEnforcementTest(NiftyNetTestCase): + def test_volume_enforcement_equal(self): + with self.cached_session(): + predicted = tf.constant([[-1000, 1000], [1000, -1000], [1000, + -1000], [1000, -1000]], + dtype=tf.float32, name='predicted') + labels = tf.constant([1, 0, 0, 0], dtype=tf.int64, name='labels') + + predicted, labels = [tf.expand_dims(x, axis=0) for x in + (predicted, labels)] + venf_loss_func = LossFunction(2, loss_type='VolEnforcement') + venf_loss = venf_loss_func(predicted, labels) + self.assertAllClose(venf_loss.eval(), 0.0, atol=1e-4) + + def test_volume_enforcement_nonexist(self): + with self.cached_session(): + predicted = tf.constant([[1000, -1000], [1000, -1000], [1000, -1000], [1000, -1000]], + dtype=tf.float32, name='predicted') + labels = tf.constant([1, 0, 0, 0], dtype=tf.int64, name='labels') + + predicted, labels = [tf.expand_dims(x, axis=0) for x in + (predicted, labels)] + venf_loss_func = LossFunction(2, loss_type='VolEnforcement') + venf_loss = venf_loss_func(predicted, labels) + self.assertAllClose(venf_loss.eval(), 500.75, atol=0.1) + + + + + + + +class LossFunctionErrorsTest(NiftyNetTestCase): """ These tests check that a ValueError is called for non-existent loss functions. @@ -481,18 +514,18 @@ class LossFunctionErrorsTest(tf.test.TestCase): """ def test_value_error_for_bad_loss_function(self): - with self.test_session(): + with self.cached_session(): with self.assertRaises(ValueError): LossFunction(1, loss_type='wrong answer') # Note: sensitive to precise wording of ValueError message. def test_suggestion_for_dice_typo(self): - with self.test_session(): + with self.cached_session(): with self.assertRaisesRegexp(ValueError, 'Dice'): LossFunction(1, loss_type='dice') def test_suggestion_for_gdsc_typo(self): - with self.test_session(): + with self.cached_session(): with self.assertRaisesRegexp(ValueError, 'GDSC'): LossFunction(1, loss_type='GSDC') diff --git a/tests/mean_variance_normalisation_test.py b/tests/mean_variance_normalisation_test.py index 0933ad5d..f2593983 100755 --- a/tests/mean_variance_normalisation_test.py +++ b/tests/mean_variance_normalisation_test.py @@ -6,9 +6,10 @@ from niftynet.layer.binary_masking import BinaryMaskingLayer from niftynet.layer.mean_variance_normalisation import MeanVarNormalisationLayer +from tests.niftynet_testcase import NiftyNetTestCase -class BinaryMaskingTEst(tf.test.TestCase): +class BinaryMaskingTEst(NiftyNetTestCase): def get_3d_input(self): input_shape = (16, 16, 16) x = np.random.randint(-10, 10, size=input_shape) diff --git a/tests/niftynet_testcase.py b/tests/niftynet_testcase.py new file mode 100644 index 00000000..8102c7ff --- /dev/null +++ b/tests/niftynet_testcase.py @@ -0,0 +1,17 @@ +import tensorflow as tf + +# This UGLY solution is done to bypass the issue +# outlined in the NiftyNet issue #381 and Tensorflow +# issue #29439 +# +# https://github.com/NifTK/NiftyNet/issues/381 +# https://github.com/tensorflow/tensorflow/issues/29439 + +try: + delattr(tf.test.TestCase,'test_session') +except AttributeError: + pass + + +class NiftyNetTestCase(tf.test.TestCase): + pass \ No newline at end of file diff --git a/tests/output_collector_test.py b/tests/output_collector_test.py index b2cf4a2e..a060d03c 100755 --- a/tests/output_collector_test.py +++ b/tests/output_collector_test.py @@ -6,14 +6,14 @@ from niftynet.engine.application_variables import \ NETWORK_OUTPUT, CONSOLE, TF_SUMMARIES from niftynet.engine.application_variables import OutputsCollector - +from tests.niftynet_testcase import NiftyNetTestCase # def get_test_network(): # net = ToyNet(num_classes=4) # return net -class OutputCollectorTest(tf.test.TestCase): +class OutputCollectorTest(NiftyNetTestCase): def test_add_to_single_device(self): n_device = 1 collector = OutputsCollector(n_devices=n_device) diff --git a/tests/pad_test.py b/tests/pad_test.py index 9028ce7a..2e565af9 100755 --- a/tests/pad_test.py +++ b/tests/pad_test.py @@ -5,9 +5,10 @@ import tensorflow as tf from niftynet.layer.pad import PadLayer +from tests.niftynet_testcase import NiftyNetTestCase -class PaddingTest(tf.test.TestCase): +class PaddingTest(NiftyNetTestCase): def get_3d_input(self): input_shape = (16, 16, 16, 8) x = np.ones(input_shape) @@ -98,6 +99,46 @@ def test_3d_dict_large_pad_shape(self): self.run_test(False, input_param, (49, 18, 12, 8)) self.run_inverse_test(False, input_param, (13, 10, 8, 8)) + def test_pad_to_simple(self): + rand_image = np.random.random([10, 10, 2, 1]) + data_dict = {'image': rand_image} + tst = PadLayer(('image',), (0,), pad_to=(52, 52, 2)) + + padded = tst.layer_op(data_dict) + self.assertTrue(padded[0]['image'].shape == (52, 52, 2, 1)) + depadded = tst.inverse_op(padded[0]) + self.assertTrue(np.all(depadded[0]['image'] == rand_image)) + + def test_pad_to_smaller_window_than_input(self): + rand_image = np.random.random([10, 10, 2, 1]) + data_dict = {'image': rand_image} + tst = PadLayer(('image',), (0,), pad_to=(5, 5, 10)) + # test straightforward pad_to + padded = tst.layer_op(data_dict) + self.assertTrue(padded[0]['image'].shape == (10, 10, 10, 1)) + depadded = tst.inverse_op(padded[0]) + self.assertTrue(np.all(depadded[0]['image'] == rand_image)) + + def test_pad_to_odd_numbers(self): + rand_image = np.random.random([10, 10, 2, 1]) + data_dict = {'image': rand_image} + tst = PadLayer(('image',), (0,), pad_to=(15, 17, 10)) + # test straightforward pad_to + padded = tst.layer_op(data_dict) + self.assertTrue(padded[0]['image'].shape == (15, 17, 10, 1)) + depadded = tst.inverse_op(padded[0]) + self.assertTrue(np.all(depadded[0]['image'] == rand_image)) + + def test_pad_to_without_data_dict(self): + rand_image = np.random.random([10, 10, 2, 1]) + data_dict = {'image': rand_image} + # test without dictionary + tst = PadLayer(('image',), (0,), pad_to=(5, 5, 10)) + padded = tst.layer_op(data_dict['image'])[0] + self.assertTrue(padded.shape == (10, 10, 10, 1)) + depadded = tst.inverse_op(padded)[0] + self.assertTrue(np.all(depadded == rand_image)) + if __name__ == "__main__": tf.test.main() diff --git a/tests/post_processing_test.py b/tests/post_processing_test.py index 978e5d4b..7c5493d8 100755 --- a/tests/post_processing_test.py +++ b/tests/post_processing_test.py @@ -3,9 +3,9 @@ import tensorflow as tf from niftynet.layer.post_processing import PostProcessingLayer +from tests.niftynet_testcase import NiftyNetTestCase - -class PostProcessingTest(tf.test.TestCase): +class PostProcessingTest(NiftyNetTestCase): def get_3d_input(self): input_shape = (2, 16, 16, 16, 8) x = tf.ones(input_shape) @@ -23,7 +23,7 @@ def test_3d_shape(self): out_post = post_process_layer(x) print(post_process_layer) - with self.test_session() as sess: + with self.cached_session() as sess: out = sess.run(out_post) x_shape = tuple(x.shape.as_list()) self.assertAllClose(x_shape, out.shape) @@ -34,7 +34,7 @@ def test_2d_shape(self): out_post = post_process_layer(x) print(post_process_layer) - with self.test_session() as sess: + with self.cached_session() as sess: out = sess.run(out_post) x_shape = tuple(x.shape.as_list()) self.assertAllClose(x_shape, out.shape) @@ -45,7 +45,7 @@ def test_3d_argmax_shape(self): out_post = post_process_layer(x) print(post_process_layer) - with self.test_session() as sess: + with self.cached_session() as sess: out = sess.run(out_post) x_shape = tuple(x.shape.as_list()[:-1]) self.assertAllClose(x_shape + (1,), out.shape) diff --git a/tests/pylintrc b/tests/pylintrc index 744bfac4..d1ba1b31 100644 --- a/tests/pylintrc +++ b/tests/pylintrc @@ -65,7 +65,7 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable=long-suffix,standarderror-builtin,indexing-exception,delslice-method,unichr-builtin,dict-view-method,parameter-unpacking,unicode-builtin,cmp-builtin,intern-builtin,round-builtin,backtick,nonzero-method,xrange-builtin,coerce-method,raw_input-builtin,old-division,filter-builtin-not-iterating,old-octal-literal,input-builtin,map-builtin-not-iterating,buffer-builtin,basestring-builtin,zip-builtin-not-iterating,using-cmp-argument,unpacking-in-except,old-raise-syntax,coerce-builtin,dict-iter-method,hex-method,range-builtin-not-iterating,useless-suppression,cmp-method,print-statement,reduce-builtin,file-builtin,long-builtin,getslice-method,execfile-builtin,no-absolute-import,metaclass-assignment,oct-method,reload-builtin,import-star-module-level,suppressed-message,apply-builtin,raising-string,next-method-called,setslice-method,old-ne-operator,arguments-differ,wildcard-import,locally-disabled,not-context-manager,no-self-use +disable=long-suffix,standarderror-builtin,indexing-exception,delslice-method,unichr-builtin,dict-view-method,parameter-unpacking,unicode-builtin,cmp-builtin,intern-builtin,round-builtin,backtick,nonzero-method,xrange-builtin,coerce-method,raw_input-builtin,old-division,filter-builtin-not-iterating,old-octal-literal,input-builtin,map-builtin-not-iterating,buffer-builtin,basestring-builtin,zip-builtin-not-iterating,using-cmp-argument,unpacking-in-except,old-raise-syntax,coerce-builtin,dict-iter-method,hex-method,range-builtin-not-iterating,useless-suppression,cmp-method,print-statement,reduce-builtin,file-builtin,long-builtin,getslice-method,execfile-builtin,no-absolute-import,metaclass-assignment,oct-method,reload-builtin,import-star-module-level,suppressed-message,apply-builtin,raising-string,next-method-called,setslice-method,old-ne-operator,arguments-differ,wildcard-import,locally-disabled,not-context-manager,no-self-use,redefined-variable-type [REPORTS] diff --git a/tests/rand_bias_field_test.py b/tests/rand_bias_field_test.py index f80eae47..eef81e16 100755 --- a/tests/rand_bias_field_test.py +++ b/tests/rand_bias_field_test.py @@ -4,12 +4,13 @@ import tensorflow as tf from niftynet.layer.rand_bias_field import RandomBiasFieldLayer +from tests.niftynet_testcase import NiftyNetTestCase SHAPE_4D = (10, 16, 16, 2) SHAPE_5D = (10, 32, 32, 8, 1) -class RandDeformationTests(tf.test.TestCase): +class RandDeformationTests(NiftyNetTestCase): # def get_3d_input(self): # input_3d = {'image': np.random.randn(10, 16, 2)} # interp_order = {'image': (3,) * 2} @@ -70,7 +71,7 @@ def test_augmentation(self): # rand_bias_field_layer.randomise(x) # out = rand_bias_field_layer(x, interp_orders) - # with self.test_session(): + # with self.cached_session(): # self.assertFalse(np.array_equal(out['image'], x_old)) diff --git a/tests/rand_elastic_deformation_test.py b/tests/rand_elastic_deformation_test.py index b919ba3c..bcf2f96f 100755 --- a/tests/rand_elastic_deformation_test.py +++ b/tests/rand_elastic_deformation_test.py @@ -6,6 +6,7 @@ from niftynet.layer.rand_elastic_deform import RandomElasticDeformationLayer from niftynet.layer.rand_elastic_deform import sitk +from tests.niftynet_testcase import NiftyNetTestCase SHAPE_3D = (10, 16, 2) SHAPE_4D = (10, 16, 16, 2) @@ -13,7 +14,7 @@ @unittest.skipIf(not sitk, 'SimpleITK not found') -class RandDeformationTests(tf.test.TestCase): +class RandDeformationTests(NiftyNetTestCase): def get_3d_input(self): input_3d = {'testdata': np.random.randn(*SHAPE_3D)} interp_order = {'testdata': (3,) * 2} @@ -58,7 +59,7 @@ def test_no_deformation(self): rand_deformation_layer.randomise(x) out = rand_deformation_layer(x, interp_orders) - with self.test_session(): + with self.cached_session(): self.assertTrue(np.array_equal(out['testdata'], x_old)) def test_deformation(self): @@ -74,7 +75,7 @@ def test_deformation(self): rand_deformation_layer.randomise(x) out = rand_deformation_layer(x, interp_orders) - with self.test_session(): + with self.cached_session(): self.assertFalse(np.array_equal(out['testdata'], x_old)) def test_deformation_on_2d_imgs(self): @@ -91,7 +92,7 @@ def test_deformation_on_2d_imgs(self): rand_deformation_layer.randomise(x) out = rand_deformation_layer(x, interp_orders) - with self.test_session(): + with self.cached_session(): self.assertFalse(np.array_equal(out['testdata'], x_old)) diff --git a/tests/rand_flip_test.py b/tests/rand_flip_test.py index 2362b8ef..c3a6735d 100755 --- a/tests/rand_flip_test.py +++ b/tests/rand_flip_test.py @@ -7,15 +7,15 @@ import tensorflow as tf from niftynet.layer.rand_flip import RandomFlipLayer +from tests.niftynet_testcase import NiftyNetTestCase - -class RandFlipTest(tf.test.TestCase): +class RandFlipTest(NiftyNetTestCase): def test_1d_flip(self): a = np.array([[0, 1], [2, 3]]) flip_layer = RandomFlipLayer(flip_axes=[0], flip_probability=1) flip_layer.randomise(spatial_rank=2) transformed_a = flip_layer._apply_transformation(a) - with self.test_session() as sess: + with self.cached_session() as sess: self.assertTrue( np.array_equal(transformed_a, np.array([[2, 3], [0, 1]]))) @@ -24,7 +24,7 @@ def test_no_flip(self): flip_layer = RandomFlipLayer(flip_axes=[0], flip_probability=0) flip_layer.randomise(spatial_rank=2) transformed_a = flip_layer._apply_transformation(a) - with self.test_session() as sess: + with self.cached_session() as sess: self.assertTrue(np.array_equal(transformed_a, a)) def test_3d_flip(self): @@ -33,7 +33,7 @@ def test_3d_flip(self): flip_layer = RandomFlipLayer(flip_axes=[0, 1, 2], flip_probability=1) flip_layer.randomise(spatial_rank=3) transformed_a = flip_layer._apply_transformation(a) - with self.test_session() as sess: + with self.cached_session() as sess: # cube of zeros with opposite corner as 1 expected_a = np.zeros(24).reshape(2, 3, 4) expected_a[-1, -1, -1] = 1 @@ -44,7 +44,7 @@ def test_no_flip_layer(self): flip_layer = RandomFlipLayer(flip_axes=[0], flip_probability=0) flip_layer.randomise(spatial_rank=2) transformed_a = flip_layer(a) - with self.test_session() as sess: + with self.cached_session() as sess: self.assertTrue(np.array_equal(transformed_a, a)) def test_2d_flip_layer(self): @@ -52,7 +52,7 @@ def test_2d_flip_layer(self): flip_layer = RandomFlipLayer(flip_axes=[0], flip_probability=1) flip_layer.randomise(spatial_rank=2) transformed_a = flip_layer(a) - with self.test_session() as sess: + with self.cached_session() as sess: self.assertTrue( np.array_equal(transformed_a, np.array([[2, 3], [0, 1]]))) @@ -63,7 +63,7 @@ def test_2d_flip_layer_1(self): flip_layer = RandomFlipLayer(flip_axes=[0], flip_probability=1) flip_layer.randomise(spatial_rank=2) transformed_a = flip_layer(a, i) - with self.test_session() as sess: + with self.cached_session() as sess: self.assertTrue( np.array_equal(transformed_a['image'], np.array([[2, 3], [0, 1]]))) diff --git a/tests/rand_rotation_test.py b/tests/rand_rotation_test.py index 45947f48..e74f36f3 100755 --- a/tests/rand_rotation_test.py +++ b/tests/rand_rotation_test.py @@ -4,9 +4,10 @@ import tensorflow as tf from niftynet.layer.rand_rotation import RandomRotationLayer +from tests.niftynet_testcase import NiftyNetTestCase -class RandRotationTest(tf.test.TestCase): +class RandRotationTest(NiftyNetTestCase): def get_4d_input(self): input_4d = {'testdata': np.ones((16, 16, 16, 8))} interp_order = {'testdata': (3,) * 8} diff --git a/tests/rand_spatial_scaling_test.py b/tests/rand_spatial_scaling_test.py index 15ad110b..441360a4 100755 --- a/tests/rand_spatial_scaling_test.py +++ b/tests/rand_spatial_scaling_test.py @@ -4,9 +4,10 @@ import tensorflow as tf from niftynet.layer.rand_spatial_scaling import RandomSpatialScalingLayer +from tests.niftynet_testcase import NiftyNetTestCase -class RandRotationTest(tf.test.TestCase): +class RandRotationTest(NiftyNetTestCase): def get_4d_input(self): input_4d = {'testdata': np.ones((16, 16, 16, 8))} interp_order = {'testdata': (3,) * 8} diff --git a/tests/reader_modular_test.py b/tests/reader_modular_test.py index baf4a23b..b9d58025 100755 --- a/tests/reader_modular_test.py +++ b/tests/reader_modular_test.py @@ -9,6 +9,7 @@ import tensorflow as tf from niftynet.io.image_reader import ImageReader +from tests.niftynet_testcase import NiftyNetTestCase # from niftynet.io.image_sets_partitioner import ImageSetsPartitioner @@ -75,7 +76,7 @@ def generate_3d_1_1_d_images(): IMAGE_PATH_3D_1 = os.path.join('.', 'testing_data', 'images_x_y_z_1_1') -class Read2DTest(tf.test.TestCase): +class Read2DTest(NiftyNetTestCase): def default_property_asserts(self, reader): self.assertDictEqual(reader.spatial_ranks, {'mr': 2}) self.assertDictEqual(reader.input_sources, {'mr': ('mr',)}) @@ -195,7 +196,7 @@ def test_2D_multimodal_properties(self): self.assertEqual(data['ct'].shape, (100, 100, 1, 1, 3)) -class Read2D_1DTest(tf.test.TestCase): +class Read2D_1DTest(NiftyNetTestCase): # loading 2d images of rank 3: [x, y, 1] def test_no_2d_resampling_properties(self): data_param = {'mr': {'path_to_search': IMAGE_PATH_2D_1, @@ -237,7 +238,7 @@ def test_2D_multimodal_properties(self): self.assertEqual(data['ct'].shape, (120, 160, 1, 1, 3)) -class Read2D_1D_x1y_Test(tf.test.TestCase): +class Read2D_1D_x1y_Test(NiftyNetTestCase): # loading 2d images of rank 3: [x, 1, y] def test_no_2d_resampling_properties(self): data_param = {'mr': {'path_to_search': IMAGE_PATH_2D_2, @@ -278,7 +279,7 @@ def test_2D_multimodal_properties(self): self.assertEqual(data['ct'].shape, (100, 100, 1, 1, 3)) -class Read2D_colorTest(tf.test.TestCase): +class Read2D_colorTest(NiftyNetTestCase): # loading 2d images of rank 3: [x, y, 3] or [x, y, 4] def test_no_2d_resampling_properties(self): data_param = {'mr': {'path_to_search': IMAGE_PATH_2D, @@ -321,7 +322,7 @@ def test_2D_multimodal_properties(self): self.assertEqual(data['ct'].shape, (100, 100, 1, 1, 9)) -class Read3DTest(tf.test.TestCase): +class Read3DTest(NiftyNetTestCase): # loading 3d images of rank 3: [x, y, z] def test_3d_resampling_properties(self): data_param = { @@ -408,7 +409,7 @@ def test_3d_concat_properties(self): self.assertAllClose(data['image'].shape[3:], (1, 2)) -class Read3D_1_1_Test(tf.test.TestCase): +class Read3D_1_1_Test(NiftyNetTestCase): # loading 5d images of rank 3: [x, y, z, 1, 1] def test_3d_resampling_properties(self): data_param = { diff --git a/tests/resampler_batch_test.py b/tests/resampler_batch_test.py index 637ab1b5..166b0e9f 100755 --- a/tests/resampler_batch_test.py +++ b/tests/resampler_batch_test.py @@ -4,9 +4,10 @@ import tensorflow as tf from niftynet.layer.resampler import ResamplerLayer +from tests.niftynet_testcase import NiftyNetTestCase -class ResamplerTest(tf.test.TestCase): +class ResamplerTest(NiftyNetTestCase): def test_shape_interface(self): test_input = tf.zeros((2, 10, 10, 10, 3)) @@ -34,7 +35,7 @@ def test_linear_shape(self): test_input = tf.constant(test_input) test_coords = tf.ones((1, 5, 5, 5, 3)) * 0.1 out = ResamplerLayer("LINEAR")(test_input, test_coords) - with self.test_session() as sess: + with self.cached_session() as sess: out_value = sess.run(out) self.assertTrue( np.all(np.isclose(out_value[0, ..., 0], 0.9**3, atol=1e-5))) @@ -47,7 +48,7 @@ def test_linear_shape(self): test_input = tf.constant(test_input) test_coords = tf.ones((1, 5, 5, 2)) * 0.1 out = ResamplerLayer("LINEAR")(test_input, test_coords) - with self.test_session() as sess: + with self.cached_session() as sess: out_value = sess.run(out) self.assertTrue(np.all(out_value[1, ...]==0)) self.assertTrue( @@ -60,7 +61,7 @@ def test_linear_shape(self): test_input = tf.constant(test_input) test_coords = tf.ones((1, 5, 1)) * 0.1 out = ResamplerLayer("LINEAR")(test_input, test_coords) - with self.test_session() as sess: + with self.cached_session() as sess: out_value = sess.run(out) self.assertTrue(np.all(out_value[1, ...]==0)) self.assertTrue( @@ -75,7 +76,7 @@ def test_linear_no_broadcasting(self): test_coords = tf.concat([tf.ones((1, 5, 5, 5, 3)) * 0.1, tf.ones((1, 5, 5, 5, 3)) * 0.2], axis=0) out = ResamplerLayer("LINEAR")(test_input, test_coords) - with self.test_session() as sess: + with self.cached_session() as sess: out_value = sess.run(out) self.assertTrue( np.all(np.isclose(out_value[0, ..., 0], 0.9**3, atol=1e-5))) @@ -89,7 +90,7 @@ def test_linear_no_broadcasting(self): test_coords = tf.concat([tf.ones((1, 5, 5, 2)) * 0.1, tf.ones((1, 5, 5, 2)) * 0.2], axis=0) out = ResamplerLayer("LINEAR")(test_input, test_coords) - with self.test_session() as sess: + with self.cached_session() as sess: out_value = sess.run(out) self.assertTrue( np.all(np.isclose(out_value[0, ..., 0], 0.9**2, atol=1e-5))) @@ -103,7 +104,7 @@ def test_linear_no_broadcasting(self): test_coords = tf.concat([tf.ones((1, 5, 1)) * 0.1, tf.ones((1, 5, 1)) * 0.2], axis=0) out = ResamplerLayer("LINEAR")(test_input, test_coords) - with self.test_session() as sess: + with self.cached_session() as sess: out_value = sess.run(out) self.assertTrue( np.all(np.isclose(out_value[0, ..., 0], 0.9, atol=1e-5))) @@ -119,7 +120,7 @@ def test_nearest_shape(self): test_input = tf.constant(test_input) test_coords = tf.ones((1, 5, 5, 5, 3)) * 0.1 out = ResamplerLayer("NEAREST")(test_input, test_coords) - with self.test_session() as sess: + with self.cached_session() as sess: out_value = sess.run(out) self.assertTrue( np.all(np.isclose(out_value[0, ..., 0], 1.0, atol=1e-5))) @@ -132,7 +133,7 @@ def test_nearest_shape(self): test_input = tf.constant(test_input) test_coords = tf.ones((1, 5, 5, 2)) * 0.1 out = ResamplerLayer("NEAREST")(test_input, test_coords) - with self.test_session() as sess: + with self.cached_session() as sess: out_value = sess.run(out) self.assertTrue(np.all(out_value[1, ...]==0)) self.assertTrue( @@ -145,7 +146,7 @@ def test_nearest_shape(self): test_input = tf.constant(test_input) test_coords = tf.ones((1, 5, 1)) * 0.1 out = ResamplerLayer("NEAREST")(test_input, test_coords) - with self.test_session() as sess: + with self.cached_session() as sess: out_value = sess.run(out) self.assertTrue(np.all(out_value[1, ...]==0)) self.assertTrue( @@ -160,7 +161,7 @@ def test_nearest_no_broadcasting(self): test_coords = tf.concat([tf.ones((1, 5, 5, 5, 3)) * 0.1, tf.ones((1, 5, 5, 5, 3)) * 1.2], axis=0) out = ResamplerLayer("NEAREST")(test_input, test_coords) - with self.test_session() as sess: + with self.cached_session() as sess: out_value = sess.run(out) self.assertTrue( np.all(np.isclose(out_value[0, ..., 0], 1.0, atol=1e-5))) @@ -174,7 +175,7 @@ def test_nearest_no_broadcasting(self): test_coords = tf.concat([tf.ones((1, 5, 5, 2)) * 0.1, tf.ones((1, 5, 5, 2)) * 1.2], axis=0) out = ResamplerLayer("NEAREST")(test_input, test_coords) - with self.test_session() as sess: + with self.cached_session() as sess: out_value = sess.run(out) self.assertTrue( np.all(np.isclose(out_value[0, ..., 0], 1.0, atol=1e-5))) @@ -188,7 +189,7 @@ def test_nearest_no_broadcasting(self): test_coords = tf.concat([tf.ones((1, 5, 1)) * 0.1, tf.ones((1, 5, 1)) * 1.2], axis=0) out = ResamplerLayer("NEAREST")(test_input, test_coords) - with self.test_session() as sess: + with self.cached_session() as sess: out_value = sess.run(out) self.assertTrue( np.all(np.isclose(out_value[0, ..., 0], 1.0, atol=1e-5))) @@ -203,7 +204,7 @@ def test_idw_shape(self): test_input = tf.constant(test_input) test_coords = tf.ones((1, 5, 5, 5, 3)) * 0.1 out = ResamplerLayer("IDW")(test_input, test_coords) - with self.test_session() as sess: + with self.cached_session() as sess: out_value = sess.run(out) self.assertTrue( np.all(np.isclose(out_value[0, ..., 0], @@ -217,7 +218,7 @@ def test_idw_shape(self): test_input = tf.constant(test_input) test_coords = tf.ones((1, 5, 5, 2)) * 0.1 out = ResamplerLayer("IDW")(test_input, test_coords) - with self.test_session() as sess: + with self.cached_session() as sess: out_value = sess.run(out) self.assertTrue(np.all(out_value[1, ...]==0)) self.assertTrue( @@ -231,7 +232,7 @@ def test_idw_shape(self): test_input = tf.constant(test_input) test_coords = tf.ones((1, 5, 1)) * 0.1 out = ResamplerLayer("IDW")(test_input, test_coords) - with self.test_session() as sess: + with self.cached_session() as sess: out_value = sess.run(out) self.assertTrue(np.all(out_value[1, ...]==0)) self.assertTrue( @@ -247,7 +248,7 @@ def test_idw_no_broadcasting(self): test_coords = tf.concat([tf.ones((1, 5, 5, 5, 3)) * 0.2, tf.ones((1, 5, 5, 5, 3)) * 1.2], axis=0) out = ResamplerLayer("IDW")(test_input, test_coords) - with self.test_session() as sess: + with self.cached_session() as sess: out_value = sess.run(out) self.assertTrue( np.all(np.isclose(out_value[0, ..., 0], @@ -262,7 +263,7 @@ def test_idw_no_broadcasting(self): test_coords = tf.concat([tf.ones((1, 5, 5, 2)) * 0.2, tf.ones((1, 5, 5, 2)) * 1.2], axis=0) out = ResamplerLayer("IDW")(test_input, test_coords) - with self.test_session() as sess: + with self.cached_session() as sess: out_value = sess.run(out) self.assertTrue( np.all(np.isclose(out_value[0, ..., 0], @@ -277,7 +278,7 @@ def test_idw_no_broadcasting(self): test_coords = tf.concat([tf.ones((1, 5, 1)) * 0.2, tf.ones((1, 5, 1)) * 1.2], axis=0) out = ResamplerLayer("IDW")(test_input, test_coords) - with self.test_session() as sess: + with self.cached_session() as sess: out_value = sess.run(out) self.assertTrue( np.all(np.isclose(out_value[0, ..., 0], diff --git a/tests/resampler_grid_warper_test.py b/tests/resampler_grid_warper_test.py index fdd89bff..7a25591a 100755 --- a/tests/resampler_grid_warper_test.py +++ b/tests/resampler_grid_warper_test.py @@ -7,6 +7,7 @@ from niftynet.layer.grid_warper import AffineGridWarperLayer from niftynet.layer.resampler import ResamplerLayer +from tests.niftynet_testcase import NiftyNetTestCase test_case_2d_1 = { 'data': "+/b9/+3/377dpX+Mxp+Y/9nT/d/X6vfMuf+hX/hSY/1pvf/P9/z//+///+7z" @@ -145,13 +146,13 @@ def get_3d_input1(): return tf.expand_dims(test_case, 4) -class ResamplerGridWarperTest(tf.test.TestCase): +class ResamplerGridWarperTest(NiftyNetTestCase): def _test_correctness( self, inputs, grid, interpolation, boundary, expected_value): resampler = ResamplerLayer( interpolation=interpolation, boundary=boundary) out = resampler(inputs, grid) - with self.test_session() as sess: + with self.cached_session() as sess: out_value = sess.run(out) self.assertAllClose(expected_value, out_value) @@ -173,7 +174,7 @@ def test_combined(self): expected_value=expected) -class image_test(tf.test.TestCase): +class image_test(NiftyNetTestCase): def _test_grads_images(self, interpolation='linear', boundary='replicate', @@ -201,7 +202,7 @@ def _test_grads_images(self, optimiser = tf.train.AdagradOptimizer(0.01) grads = optimiser.compute_gradients(diff) opt = optimiser.apply_gradients(grads) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) init_val, affine_val = sess.run([diff, affine_var]) for _ in range(5): @@ -246,7 +247,7 @@ def test_3d_idw_symmetric(self): self._test_grads_images('idw', 'symmetric', ndim=3) -class image_2D_test_converge(tf.test.TestCase): +class image_2D_test_converge(NiftyNetTestCase): def _test_simple_2d_images(self, interpolation='linear', boundary='replicate'): @@ -272,12 +273,17 @@ def _test_simple_2d_images(self, diff = tf.reduce_mean(tf.squared_difference( new_image, tf.constant(test_target, dtype=tf.float32))) - optimiser = tf.train.AdagradOptimizer(0.05) + learning_rate = 0.05 + if(interpolation == 'linear') and (boundary == 'zero'): + learning_rate = 0.0003 + optimiser = tf.train.AdagradOptimizer(learning_rate) grads = optimiser.compute_gradients(diff) opt = optimiser.apply_gradients(grads) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) init_val, affine_val = sess.run([diff, affine_var]) + # compute the MAE between the initial estimated parameters and the expected parameters + init_var_diff = np.sum(np.abs(affine_val[0] - expected)) for it in range(500): _, diff_val, affine_val = sess.run([opt, diff, affine_var]) # print('{} diff: {}, {}'.format(it, diff_val, affine_val[0])) @@ -292,8 +298,9 @@ def _test_simple_2d_images(self, # plt.show() self.assertGreater(init_val, diff_val) + # compute the MAE between the final estimated parameters and the expected parameters var_diff = np.sum(np.abs(affine_val[0] - expected)) - self.assertGreater(4.72, var_diff) + self.assertGreater(init_var_diff, var_diff) print('{} {} -- diff {}'.format( interpolation, boundary, var_diff)) print('{}'.format(affine_val[0])) diff --git a/tests/resampler_optional_niftyreg_test.py b/tests/resampler_optional_niftyreg_test.py new file mode 100644 index 00000000..c6926cd3 --- /dev/null +++ b/tests/resampler_optional_niftyreg_test.py @@ -0,0 +1,444 @@ +from __future__ import absolute_import, print_function, division + +import numpy as np +import tensorflow as tf +import tensorflow.test as tft + +from niftynet.contrib.layer.resampler_optional_niftyreg import ResamplerOptionalNiftyRegLayer +from tests.niftynet_testcase import NiftyNetTestCase +import niftynet.contrib.layer.resampler_optional_niftyreg as resampler_module + +class ResamplerTest(NiftyNetTestCase): + def get_2d_input(self, as_tensor=True): + test_array = np.array( + [[[[1, 2, -1], [3, 4, -2]], [[5, 6, -3], [7, 8, -4]]], + [[[9, 10, -5], [11, 12, -6]], [[13, 14, -7], [15, 16, -8]]]]) + if as_tensor: + test_array = tf.constant(test_array, dtype=tf.float32) + return test_array + return test_array.astype(np.float32) + + def get_3d_input1(self, as_tensor=True): + test_array = np.array( + [[[[1, 2, -1], [3, 4, -2]], [[5, 6, -3], [7, 8, -4]]], + [[[9, 10, -5], [11, 12, -6]], [[13, 14, -7], [15, 16, -8]]]]) + if as_tensor: + test_array = tf.constant(test_array, dtype=tf.float32) + return tf.expand_dims(test_array, 4) + return np.expand_dims(test_array, 4).astype(np.float32) + + def get_3d_input2(self, as_tensor=True): + one_channel = self.get_3d_input1(as_tensor=as_tensor) + if as_tensor: + return tf.concat([one_channel, 100 + one_channel], 4) + return np.concatenate([one_channel, 100 + one_channel], 4) + + def _get_devs(self): + devs = [False] + if tft.is_gpu_available(cuda_only=True) and tft.is_built_with_cuda(): + devs += [True] + + return devs + + def _test_correctness( + self, input, grid, interpolation, boundary, expected_value): + resampler = ResamplerOptionalNiftyRegLayer(interpolation=interpolation, + boundary=boundary) + out = resampler(input, grid) + + for use_gpu in self._get_devs(): + with self.cached_session(use_gpu=use_gpu) as sess: + out_value = sess.run(out) + self.assertAllClose(expected_value, out_value) + + def test_resampler_2d_replicate_linear_correctness(self): + test_grid = tf.constant( + [[[.25, .25], [.25, .78]], + [[.62, .25], [.25, .28]]], + dtype=tf.float32) + expected = [[[2.5, 3.5, -1.75], + [3.56, 4.56, -2.28]], + [[11.98, 12.98, -6.49], + [10.56, 11.56, -5.78]]] + self._test_correctness(input=self.get_2d_input(), + grid=test_grid, + interpolation='LINEAR', + boundary='ZERO', + expected_value=expected) + + def test_gradient_correctness(self): + if not resampler_module.HAS_NIFTYREG_RESAMPLING: + self.skipTest('Using native NiftyNet resampler; skipping test') + return + + for inter in ('LINEAR', 'BSPLINE'): + for b in ('ZERO', 'REPLICATE', 'SYMMETRIC'): + for use_gpu in self._get_devs(): + inputs = ((self.get_3d_input1(as_tensor=False), + [[[-5.2, .25, .25], [.25, .95, .25]], + [[.75, .25, .25], [.25, .25, .75]]]), + (self.get_2d_input(as_tensor=False), + [[[.25, .25], [.25, .78]], + [[.62, .25], [.25, .28]]]),) + + for np_img, np_u in inputs: + with self.session(use_gpu=use_gpu): + np_u = np.array(np_u) + + while len(np_u.shape) < len(np_img.shape): + np_u = np.expand_dims(np_u, axis=2) + + img = tf.constant(np_img, dtype=tf.float32) + disp = tf.constant(np_u, dtype=tf.float32) + + # multimodal needs addressing + if img.shape.as_list()[-1] > 1: + img = tf.reshape(img[...,0], + img.shape.as_list()[:-1] + [1]) + + warped = ResamplerOptionalNiftyRegLayer(interpolation=inter, + boundary=b) + warped = warped(img, disp) + #warped = tf.reduce_sum(warped) + + tgrad, refgrad = tft.compute_gradient( + disp, + disp.shape, + warped, + warped.shape) + + error = np.power(tgrad - refgrad, 2).sum() + refmag = np.power(refgrad, 2).sum() + + self.assertLessEqual(error, 1e-2*refmag) + + def test_image_derivative_correctness(self): + if not resampler_module.HAS_NIFTYREG_RESAMPLING: + self.skipTest('Using native NiftyNet resampler; skipping test') + return + + for inter in ('LINEAR', 'BSPLINE'): + for b in ('ZERO', 'REPLICATE', 'SYMMETRIC'): + for use_gpu in self._get_devs(): + if inter != 'LINEAR' and use_gpu: + continue + + inputs = ((self.get_3d_input1(as_tensor=False), + [[[-5.2, .25, .25], [.25, .95, .25]], + [[.75, .25, .25], [.25, .25, .75]]]), + (self.get_2d_input(as_tensor=False), + [[[.25, .25], [.25, .78]], + [[.62, .25], [.25, .28]]]),) + + for np_img, np_u in inputs: + with self.session(use_gpu=use_gpu): + np_u = np.array(np_u) + + while len(np_u.shape) < len(np_img.shape): + np_u = np.expand_dims(np_u, axis=2) + + img = tf.constant(np_img, dtype=tf.float32) + disp = tf.constant(np_u, dtype=tf.float32) + + warped = ResamplerOptionalNiftyRegLayer(interpolation=inter, + boundary=b) + warped = warped(img, disp) + #warped = tf.reduce_sum(warped) + + tgrad, refgrad = tft.compute_gradient( + img, + img.shape, + warped, + warped.shape) + + error = np.power(tgrad - refgrad, 2).sum() + refmag = np.power(refgrad, 2).sum() + + self.assertLessEqual(error, 1e-2*refmag) + + def test_resampler_3d_zero_nearest_correctness(self): + test_grid = tf.constant( + [[[-5.2, .25, .25], [.25, .95, .25]], + [[.75, .25, .25], [.25, .25, .75]]], + dtype=tf.float32) + expected = [[[0, 0], [3, 103]], + [[13, 113], [10, 110]]] + self._test_correctness(input=self.get_3d_input2(), + grid=test_grid, + interpolation='NEAREST', + boundary='ZERO', + expected_value=expected) + + def test_resampler_3d_symmetric_nearest_correctness(self): + test_grid = tf.constant( + [[[-.25, -.25, -.25], + [.25 + 2, .75 + 2, .25 + 4]], + [[.75, .25, -.25 + 4], + [.25, .25, .75]]], + dtype=tf.float32) + expected = [[[1], [3]], [[13], [10]]] + self._test_correctness(input=self.get_3d_input1(), + grid=test_grid, + interpolation='NEAREST', + boundary='SYMMETRIC', + expected_value=expected) + + def test_resampler_3d_symmetric_linear_correctness(self): + test_grid = tf.constant( + [[[-.25, -.25, -.25], + [.25 + 2, .75 + 2, .25 + 4]], + [[.75, .25, -.25 + 4], + [.25, .25, .75]]], + dtype=tf.float32) + expected = [[[2.75], [3.75]], + [[12.75], [11.25]]] + self._test_correctness(input=self.get_3d_input1(), + grid=test_grid, + interpolation='LINEAR', + boundary='SYMMETRIC', + expected_value=expected) + + def test_resampler_3d_symmetric_cubic_correctness(self): + test_grid = tf.constant( + [[[-.25, -.25, -.25], + [.25 + 2, .75 + 2, .25 + 4]], + [[.75, .25, -.25 + 4], + [.25, .25, .75]]], + dtype=tf.float32) + expected = [[[3.683675], [4.140218]], + [[12.56551075], [10.69881153]]] + self._test_correctness(input=self.get_3d_input1(), + grid=test_grid, + interpolation='BSPLINE', + boundary='SYMMETRIC', + expected_value=expected) + + def _test_partial_shape_correctness(self, + input, + rank, + batch_size, + grid, + interpolation, + boundary, + expected_value=None): + + resampler = ResamplerOptionalNiftyRegLayer(interpolation=interpolation, + boundary=boundary) + input_default = tf.random_uniform(input.shape) + if batch_size > 0 and rank > 0: + input_placeholder = tf.placeholder_with_default( + input_default, shape=[batch_size] + [None] * (rank + 1)) + elif batch_size <= 0 and rank > 0: + input_placeholder = tf.placeholder_with_default( + input_default, shape=[None] * (rank + 2)) + elif batch_size <= 0 and rank <= 0: + input_placeholder = tf.placeholder_with_default( + input_default, shape=None) + + out = resampler(input_placeholder, grid) + with self.cached_session() as sess: + out_value = sess.run( + out, feed_dict={input_placeholder: input}) + if expected_value is not None: + self.assertAllClose(expected_value, out_value) + + def test_2d_linear_partial_shapes(self): + test_grid = tf.constant( + [[[.25, .25], [.25, .78]], + [[.62, .25], [.25, .28]]], dtype=tf.float32) + expected = [[[2.5, 3.5, -1.75], + [3.56, 4.56, -2.28]], + [[11.98, 12.98, -6.49], + [10.56, 11.56, -5.78]]] + interp = 'linear' + + for b in ('ZERO',): + self._test_partial_shape_correctness( + input=self.get_2d_input(False), + rank=2, + batch_size=2, + grid=test_grid, + interpolation=interp, + boundary=b, + expected_value=expected) + + with self.assertRaisesRegexp(TypeError, 'shape'): + self._test_partial_shape_correctness( + input=self.get_2d_input(False), + rank=2, + batch_size=-1, + grid=test_grid, + interpolation=interp, + boundary=b, + expected_value=None) + + with self.assertRaisesRegexp(TypeError, 'shape'): + self._test_partial_shape_correctness( + input=self.get_2d_input(False), + rank=-1, + batch_size=-1, + grid=test_grid, + interpolation=interp, + boundary=b, + expected_value=None) + + def test_3d_linear_partial_shapes(self): + test_grid = tf.constant( + [[[.25, .25, .25], [.25, .75, .25]], + [[.75, .25, .25], [.25, .25, .75]]], + dtype=tf.float32) + expected = [[[2.75, 102.75], [3.75, 103.75]], + [[12.75, 112.75], [11.25, 111.25]]] + interp = 'linear' + + for b in ('ZERO',): + self._test_partial_shape_correctness( + input=self.get_3d_input2(False), + rank=3, + batch_size=2, + grid=test_grid, + interpolation=interp, + boundary=b, + expected_value=expected) + + with self.assertRaisesRegexp(TypeError, 'shape'): + self._test_partial_shape_correctness( + input=self.get_3d_input2(False), + rank=3, + batch_size=-1, + grid=test_grid, + interpolation=interp, + boundary=b, + expected_value=None) + + with self.assertRaisesRegexp(TypeError, 'shape'): + self._test_partial_shape_correctness( + input=self.get_3d_input2(False), + rank=-1, + batch_size=-1, + grid=test_grid, + interpolation=interp, + boundary=b, + expected_value=None) + + def test_2d_nearest_partial_shapes(self): + test_grid = tf.constant( + [[[.25, .25], [.25, .78]], + [[.62, .25], [.25, .28]]], dtype=tf.float32) + expected = [[[1, 2, -1], + [3, 4, -2]], + [[13, 14, -7], + [9, 10, -5]]] + interp = 'nearest' + + for b in ('ZERO', 'REPLICATE'): + self._test_partial_shape_correctness( + input=self.get_2d_input(False), + rank=2, + batch_size=2, + grid=test_grid, + interpolation=interp, + boundary=b, + expected_value=expected) + + with self.assertRaisesRegexp(TypeError, 'shape'): + self._test_partial_shape_correctness( + input=self.get_2d_input(False), + rank=2, + batch_size=-1, + grid=test_grid, + interpolation=interp, + boundary=b, + expected_value=None) + + with self.assertRaisesRegexp(TypeError, 'shape'): + self._test_partial_shape_correctness( + input=self.get_2d_input(False), + rank=-1, + batch_size=-1, + grid=test_grid, + interpolation=interp, + boundary=b, + expected_value=None) + + def test_resampler_3d_multivariate_replicate_linear_correctness(self): + test_grid = tf.constant( + [[[.25, .25, .25], [.25, .75, .25]], + [[.75, .25, .25], [.25, .25, .75]]], + dtype=tf.float32) + expected = [[[2.75, 102.75], [3.75, 103.75]], + [[12.75, 112.75], [11.25, 111.25]]] + self._test_correctness(input=self.get_3d_input2(), + grid=test_grid, + interpolation='LINEAR', + boundary='REPLICATE', + expected_value=expected) + + def test_resampler_3d_replicate_nearest_correctness(self): + test_grid = tf.constant( + [[[.25, .25, .25], [.25, .75, .25]], + [[.75, .25, .25], [.25, .25, .75]]], + dtype=tf.float32) + expected = [[[1, 101], [3, 103]], + [[13, 113], [10, 110]]] + self._test_correctness(input=self.get_3d_input2(), + grid=test_grid, + interpolation='NEAREST', + boundary='REPLICATE', + expected_value=expected) + + def test_resampler_3d_replicate_linear_correctness(self): + test_grid = tf.constant( + [[[.25, .25, .25], [.25, .75, .25]], + [[.75, .25, .25], [.25, .25, .75]]], + dtype=tf.float32) + expected = [[[2.75], [3.75]], + [[12.75], [11.25]]] + self._test_correctness(input=self.get_3d_input1(), + grid=test_grid, + interpolation='LINEAR', + boundary='REPLICATE', + expected_value=expected) + + def test_3d_nearest_partial_shapes(self): + test_grid = tf.constant( + [[[0, 1, 2], [.25, .75, .25]], + [[.75, .25, .25], [.25, .25, .75]]], + dtype=tf.float32) + expected = [[[-2, 98], [3, 103]], + [[13, 113], [10, 110]]] + interp = 'nearest' + + for b in ('ZERO', 'REPLICATE'): + self._test_partial_shape_correctness( + input=self.get_3d_input2(False), + rank=3, + batch_size=2, + grid=test_grid, + interpolation=interp, + boundary=b, + expected_value=expected) + + with self.assertRaisesRegexp(TypeError, 'shape'): + self._test_partial_shape_correctness( + input=self.get_3d_input2(False), + rank=3, + batch_size=-1, + grid=test_grid, + interpolation=interp, + boundary=b, + expected_value=None) + + with self.assertRaisesRegexp(TypeError, 'shape'): + self._test_partial_shape_correctness( + input=self.get_3d_input2(False), + rank=-1, + batch_size=-1, + grid=test_grid, + interpolation=interp, + boundary=b, + expected_value=None) + + +if __name__ == "__main__": + tf.test.main() diff --git a/tests/resampler_test.py b/tests/resampler_test.py index 50638369..d892b109 100755 --- a/tests/resampler_test.py +++ b/tests/resampler_test.py @@ -4,9 +4,9 @@ import tensorflow as tf from niftynet.layer.resampler import ResamplerLayer +from tests.niftynet_testcase import NiftyNetTestCase - -class ResamplerTest(tf.test.TestCase): +class ResamplerTest(NiftyNetTestCase): def get_2d_input(self, as_tensor=True): test_array = np.array( [[[[1, 2, -1], [3, 4, -2]], [[5, 6, -3], [7, 8, -4]]], @@ -36,7 +36,7 @@ def _test_correctness( resampler = ResamplerLayer(interpolation=interpolation, boundary=boundary) out = resampler(input, grid) - with self.test_session() as sess: + with self.cached_session() as sess: out_value = sess.run(out) # print(expected_value) # print(out_value) @@ -257,7 +257,7 @@ def _test_partial_shape_correctness(self, input_default, shape=None) out = resampler(input_placeholder, grid) - with self.test_session() as sess: + with self.cached_session() as sess: out_value = sess.run( out, feed_dict={input_placeholder: input}) # print(expected_value) diff --git a/tests/residual_unit_test.py b/tests/residual_unit_test.py index bc86ccf2..32d875c7 100755 --- a/tests/residual_unit_test.py +++ b/tests/residual_unit_test.py @@ -5,9 +5,10 @@ from niftynet.layer.residual_unit import ResidualUnit as Res from niftynet.layer.residual_unit import SUPPORTED_OP as connection_types +from tests.niftynet_testcase import NiftyNetTestCase -class ResidualUnitTest(tf.test.TestCase): +class ResidualUnitTest(NiftyNetTestCase): def get_3d_input(self): input_shape = (2, 16, 16, 16, 8) x = tf.ones(input_shape) @@ -27,7 +28,7 @@ def _test_nd_output_shape(self, rank, param_dict, expected_shape): res_layer = Res(**param_dict) output_data = res_layer(input_data) print(res_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(output_data) self.assertAllClose(expected_shape, out.shape) diff --git a/tests/residual_upsample_test.py b/tests/residual_upsample_test.py index 8ccb1837..8c81e37b 100755 --- a/tests/residual_upsample_test.py +++ b/tests/residual_upsample_test.py @@ -4,6 +4,7 @@ import tensorflow as tf from niftynet.layer.additive_upsample import ResidualUpsampleLayer +from tests.niftynet_testcase import NiftyNetTestCase def get_3d_input(): input_shape = (2, 16, 16, 16, 4) @@ -15,7 +16,7 @@ def get_2d_input(): x = tf.ones(input_shape) return x -class ResidualUpsampleTest(tf.test.TestCase): +class ResidualUpsampleTest(NiftyNetTestCase): def run_test(self, param_dict, expected_shape, is_3d=True): if is_3d: x = get_3d_input() @@ -25,7 +26,7 @@ def run_test(self, param_dict, expected_shape, is_3d=True): upsample_layer = ResidualUpsampleLayer(**param_dict) resized = upsample_layer(x) print(upsample_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(resized) self.assertAllClose(out.shape, expected_shape) diff --git a/tests/restorer_test.py b/tests/restorer_test.py index 95949cc6..abcb02f4 100755 --- a/tests/restorer_test.py +++ b/tests/restorer_test.py @@ -8,9 +8,10 @@ from niftynet.engine.application_variables \ import RESTORABLE, global_vars_init_or_restore from niftynet.layer.convolution import ConvolutionalLayer +from tests.niftynet_testcase import NiftyNetTestCase -class RestorerTest(tf.test.TestCase): +class RestorerTest(NiftyNetTestCase): def make_checkpoint(self, checkpoint_name, definition): scopes = {} tf.reset_default_graph() @@ -30,24 +31,24 @@ def test_restore_block(self): 'bar/bing/boffin': [2]} checkpoint_name = self.make_checkpoint('chk1', definition) tf.reset_default_graph() - block1 = ConvolutionalLayer(3, 3, with_bn=False, name='foo') + block1 = ConvolutionalLayer(3, 3, feature_normalization=None, name='foo') b1 = block1(tf.ones([1., 5., 5., 1.])) tf.add_to_collection(RESTORABLE, ('foo', checkpoint_name, 'bar')) - block2 = ConvolutionalLayer(4, 3, name='bar', with_bn=False, + block2 = ConvolutionalLayer(4, 3, name='bar', feature_normalization=None, w_initializer=tf.constant_initializer(1.)) b2 = block2(tf.ones([1., 5., 5., 1.])) - block3 = ConvolutionalLayer(3, 3, with_bn=False, name='foo2') + block3 = ConvolutionalLayer(3, 3, feature_normalization=None, name='foo2') block3.restore_from_checkpoint(checkpoint_name, 'bar2') b3 = block3(tf.ones([1., 5., 5., 1.])) - block4 = ConvolutionalLayer(3, 3, with_bn=False, name='foo3') + block4 = ConvolutionalLayer(3, 3, feature_normalization=None, name='foo3') block4.restore_from_checkpoint(checkpoint_name) b4 = block4(tf.ones([1., 5., 5., 1.])) tf.add_to_collection(RESTORABLE, ('foo', checkpoint_name, 'bar')) init_op = global_vars_init_or_restore() all_vars = tf.global_variables() - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(init_op) def getvar(x): @@ -66,12 +67,12 @@ def getvar(x): def test_no_restores(self): tf.reset_default_graph() - block1 = ConvolutionalLayer(4, 3, name='bar', with_bn=False, + block1 = ConvolutionalLayer(4, 3, name='bar', feature_normalization=None, w_initializer=tf.constant_initializer(1.)) b2 = block1(tf.ones([1., 5., 5., 1.])) init_op = global_vars_init_or_restore() all_vars = tf.global_variables() - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(init_op) def getvar(x): diff --git a/tests/rgb_histogram_equilisation_test.py b/tests/rgb_histogram_equilisation_test.py new file mode 100644 index 00000000..a5a11b82 --- /dev/null +++ b/tests/rgb_histogram_equilisation_test.py @@ -0,0 +1,158 @@ +import numpy as np +import tensorflow as tf + +from niftynet.layer.rgb_histogram_equilisation import \ + RGBHistogramEquilisationLayer +from niftynet.utilities.util_import import require_module +from tests.niftynet_testcase import NiftyNetTestCase + +IMAGE_DATA = \ + np.array([[[0.49803922, 0.19215687, 0.3529412 ], + [0.49411765, 0.16862746, 0.31764707], + [0.5254902 , 0.21176471, 0.3647059 ], + [0.45882353, 0.21176471, 0.38039216], + [0.44705883, 0.19215687, 0.39607844], + [0.44705883, 0.18431373, 0.39607844], + [0.43137255, 0.18039216, 0.3882353 ], + [0.42352942, 0.16470589, 0.34901962], + [0.41960785, 0.14509805, 0.3647059 ], + [0.46666667, 0.18039216, 0.38431373]], + [[0.4509804 , 0.15686275, 0.34901962], + [0.43137255, 0.16862746, 0.3529412 ], + [0.4745098 , 0.21176471, 0.4 ], + [0.47058824, 0.2784314 , 0.4509804 ], + [0.46666667, 0.25490198, 0.4627451 ], + [0.4509804 , 0.24705882, 0.41960785], + [0.42745098, 0.17254902, 0.37254903], + [0.4392157 , 0.16078432, 0.36078432], + [0.48235294, 0.1882353 , 0.41960785], + [0.53333336, 0.2901961 , 0.48235294]], + [[0.45882353, 0.20784314, 0.38431373], + [0.48235294, 0.21960784, 0.40784314], + [0.4627451 , 0.24705882, 0.40392157], + [0.45490196, 0.23529412, 0.4117647 ], + [0.41568628, 0.14901961, 0.34901962], + [0.4509804 , 0.16862746, 0.34901962], + [0.45882353, 0.2 , 0.41568628], + [0.5019608 , 0.24313726, 0.41960785], + [0.5058824 , 0.23529412, 0.4392157 ], + [0.53333336, 0.29411766, 0.46666667]], + [[0.52156866, 0.21960784, 0.41568628], + [0.47058824, 0.16862746, 0.36862746], + [0.4392157 , 0.1882353 , 0.38039216], + [0.4117647 , 0.18431373, 0.38039216], + [0.40784314, 0.15686275, 0.36078432], + [0.43137255, 0.14901961, 0.3529412 ], + [0.5176471 , 0.2901961 , 0.47058824], + [0.5137255 , 0.2509804 , 0.4627451 ], + [0.45882353, 0.21176471, 0.39215687], + [0.44313726, 0.18039216, 0.38039216]], + [[0.47843137, 0.19215687, 0.36078432], + [0.44313726, 0.14901961, 0.37254903], + [0.40392157, 0.13333334, 0.32941177], + [0.41568628, 0.12941177, 0.34901962], + [0.43529412, 0.14509805, 0.38431373], + [0.49411765, 0.23529412, 0.44313726], + [0.5294118 , 0.3019608 , 0.45882353], + [0.50980395, 0.25882354, 0.4392157 ], + [0.43529412, 0.19607843, 0.3529412 ], + [0.39215687, 0.13333334, 0.3254902 ]], + [[0.44705883, 0.14117648, 0.34117648], + [0.39607844, 0.12156863, 0.3137255 ], + [0.4117647 , 0.14117648, 0.34509805], + [0.44705883, 0.15686275, 0.3764706 ], + [0.5058824 , 0.20784314, 0.40392157], + [0.5294118 , 0.25490198, 0.42745098], + [0.5137255 , 0.25882354, 0.42352942], + [0.48235294, 0.20392157, 0.41568628], + [0.39215687, 0.13725491, 0.3137255 ], + [0.36078432, 0.11372549, 0.29411766]], + [[0.41960785, 0.13333334, 0.3647059 ], + [0.43529412, 0.1882353 , 0.38431373], + [0.4509804 , 0.16862746, 0.3647059 ], + [0.50980395, 0.23529412, 0.44705883], + [0.56078434, 0.28235295, 0.45882353], + [0.5372549 , 0.27450982, 0.42745098], + [0.5176471 , 0.27450982, 0.47843137], + [0.48235294, 0.24705882, 0.4 ], + [0.39215687, 0.14901961, 0.3254902 ], + [0.38431373, 0.13725491, 0.3137255 ]], + [[0.45882353, 0.1764706 , 0.4 ], + [0.50980395, 0.21568628, 0.42352942], + [0.50980395, 0.20784314, 0.42352942], + [0.56078434, 0.29803923, 0.49411765], + [0.5294118 , 0.26666668, 0.43137255], + [0.54509807, 0.3254902 , 0.50980395], + [0.5254902 , 0.3137255 , 0.50980395], + [0.42745098, 0.16078432, 0.32156864], + [0.39607844, 0.12156863, 0.32156864], + [0.3647059 , 0.09803922, 0.30588236]], + [[0.50980395, 0.21568628, 0.4117647 ], + [0.54509807, 0.27450982, 0.43137255], + [0.5529412 , 0.23137255, 0.4392157 ], + [0.5568628 , 0.26666668, 0.4627451 ], + [0.5372549 , 0.2784314 , 0.47058824], + [0.5529412 , 0.30980393, 0.5176471 ], + [0.49411765, 0.21568628, 0.3882353 ], + [0.42352942, 0.1764706 , 0.3529412 ], + [0.38039216, 0.10980392, 0.3019608 ], + [0.38039216, 0.09411765, 0.28627452]], + [[0.49803922, 0.1882353 , 0.3764706 ], + [0.54901963, 0.23529412, 0.41568628], + [0.5568628 , 0.2901961 , 0.4862745 ], + [0.5529412 , 0.28235295, 0.4509804 ], + [0.5411765 , 0.29411766, 0.5137255 ], + [0.5176471 , 0.24705882, 0.4509804 ], + [0.41960785, 0.14901961, 0.32156864], + [0.42352942, 0.15686275, 0.38431373], + [0.38431373, 0.12156863, 0.31764707], + [0.38039216, 0.10588235, 0.32156864]]], dtype=np.float32) + + +class RGBEquilisationTest(NiftyNetTestCase): + """ + Test for RGBHistogramEquilisationLayer + """ + + def test_equilisation(self): + cv2 = require_module('cv2', mandatory=False) + + if cv2 is None: + self.skipTest('requires cv2 module') + return + + def _get_histogram(img): + inten = cv2.cvtColor(img[::-1], cv2.COLOR_BGR2YUV)[...,0]*255 + + return np.histogram(inten, 32, [0, 256])[0] + + hist_before = _get_histogram(IMAGE_DATA) + + layer = RGBHistogramEquilisationLayer(image_name='image') + orig_shape = list(IMAGE_DATA.shape) + input_shape = orig_shape[:2] + [1]*2 + [3] + img, _ = layer(IMAGE_DATA.reshape(input_shape)) + + hist_after = _get_histogram(img.reshape(orig_shape)) + + self.assertGreater(hist_before.astype(np.float32).std(), + hist_after.astype(np.float32).std()) + + img, _ = layer({'image': IMAGE_DATA.reshape(input_shape)}) + + hist_after = _get_histogram(img['image'].reshape(orig_shape)) + + self.assertGreater(hist_before.astype(np.float32).std(), + hist_after.astype(np.float32).std()) + + img = (255*IMAGE_DATA).astype(np.uint8) + img, _ = layer({'image': IMAGE_DATA.reshape(input_shape)}) + + hist_after = _get_histogram(img['image'].reshape(orig_shape)) + + self.assertGreater(hist_before.astype(np.float32).std(), + hist_after.astype(np.float32).std()) + + +if __name__ == "__main__": + tf.test.main() diff --git a/tests/run_vars_test.py b/tests/run_vars_test.py index 7deeeb84..a280f16d 100755 --- a/tests/run_vars_test.py +++ b/tests/run_vars_test.py @@ -9,10 +9,11 @@ from niftynet.engine.application_variables import CONSOLE from niftynet.engine.signal import \ TRAIN, ITER_FINISHED, GRAPH_CREATED, SESS_STARTED +from tests.niftynet_testcase import NiftyNetTestCase from tests.application_driver_test import get_initialised_driver -class DriverLoopTest(tf.test.TestCase): +class DriverLoopTest(NiftyNetTestCase): def test_interfaces(self): msg = IterationMessage() msg.current_iter = 0 @@ -64,7 +65,7 @@ def get_iter_msgs(_sender, **msg): ITER_FINISHED.connect(get_iter_msgs) - with self.test_session(graph=test_graph) as sess: + with self.cached_session(graph=test_graph) as sess: SESS_STARTED.send(app_driver.app, iter_msg=None) iterations = IterationMessageGenerator( initial_iter=0, diff --git a/tests/sampler_balanced_v2_test.py b/tests/sampler_balanced_v2_test.py index 4d4bb3d4..64ea7790 100755 --- a/tests/sampler_balanced_v2_test.py +++ b/tests/sampler_balanced_v2_test.py @@ -13,6 +13,7 @@ from niftynet.io.image_reader import ImageReader from niftynet.io.image_sets_partitioner import ImageSetsPartitioner from niftynet.utilities.util_common import ParserNamespace +from tests.niftynet_testcase import NiftyNetTestCase MULTI_MOD_DATA = { 'T1': ParserNamespace( @@ -109,14 +110,14 @@ def get_dynamic_window_reader(): @unittest.skipIf(os.environ.get('QUICKTEST', "").lower() == "true", 'Skipping slow tests') -class BalancedSamplerTest(tf.test.TestCase): +class BalancedSamplerTest(NiftyNetTestCase): def test_3d_init(self): sampler = BalancedSampler(reader=get_3d_reader(), window_sizes=MULTI_MOD_DATA, batch_size=2, windows_per_image=10, queue_length=10) - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) out = sess.run(sampler.pop_batch_op()) self.assertAllClose(out['image'].shape, (2, 7, 10, 2, 2)) @@ -128,7 +129,7 @@ def test_2d_init(self): batch_size=2, windows_per_image=10, queue_length=10) - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) out = sess.run(sampler.pop_batch_op()) self.assertAllClose(out['image'].shape, (2, 10, 9, 1)) @@ -140,7 +141,7 @@ def test_dynamic_init(self): batch_size=2, windows_per_image=10, queue_length=10) - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) #with self.assertRaisesRegexp(tf.errors.OutOfRangeError, ""): out = sess.run(sampler.pop_batch_op()) @@ -163,7 +164,7 @@ def test_close_early(self): sampler.close_all() -class BalancedCoordinatesTest(tf.test.TestCase): +class BalancedCoordinatesTest(NiftyNetTestCase): def assertCoordinatesAreValid(self, coords, sampling_map): for coord in coords: for i in range(len(coord.shape)): diff --git a/tests/sampler_csvpatch_v2_test.py b/tests/sampler_csvpatch_v2_test.py new file mode 100755 index 00000000..d4970ff0 --- /dev/null +++ b/tests/sampler_csvpatch_v2_test.py @@ -0,0 +1,378 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + +import os + +import numpy as np +import tensorflow as tf + +from niftynet.contrib.csv_reader.csv_reader import CSVReader +from niftynet.contrib.csv_reader.sampler_csvpatch import CSVPatchSampler +from niftynet.engine.image_window import N_SPATIAL +# from niftynet.engine.sampler_uniform import UniformSampler +from niftynet.engine.sampler_uniform_v2 import rand_spatial_coordinates +from niftynet.io.image_reader import ImageReader +from niftynet.io.image_sets_partitioner import ImageSetsPartitioner +from niftynet.utilities.util_common import ParserNamespace +from tests.niftynet_testcase import NiftyNetTestCase + +DYNAMIC_MOD_DATA = { + 'T1': + ParserNamespace( + csv_file='', + path_to_search='data/csv_data', + filename_contains=(), + filename_not_contains=('_', 'csv'), + interp_order=3, + csv_data_file='', + pixdim=None, + axcodes=None, + spatial_window_size=(69, 69, 69), + loader=None), + 'sampler': + ParserNamespace( + csv_file='', + path_to_search='', + filename_contains=(), + filename_not_contains=(), + interp_order=0, + pixdim=None, + axcodes=None, + spatial_window_size=(), + loader=None, + csv_data_file='data/csv_data/ICBMTest3.csv') +} + +DYNAMIC_MOD_TASK = ParserNamespace( + image=('T1', ), label=('T1', ), sampler=('sampler', )) + +LARGE_MOD_DATA = { + 'T1': + ParserNamespace( + csv_file='', + path_to_search='data/csv_data', + filename_contains=(), + filename_not_contains=('_', 'csv'), + interp_order=3, + csv_data_file='', + pixdim=None, + axcodes=None, + spatial_window_size=(75, 75, 75), + loader=None), + 'sampler': + ParserNamespace( + csv_file='', + path_to_search='', + filename_contains=(), + filename_not_contains=(), + interp_order=0, + pixdim=None, + axcodes=None, + spatial_window_size=(), + loader=None, + csv_data_file='data/csv_data/ICBMTest2.csv') +} +LARGE_MOD_DATA_2_ELEMENTS = { + 'T1': + ParserNamespace( + csv_file='', + path_to_search='data/csv_data', + filename_contains=(), + filename_not_contains=('_', 'csv'), + interp_order=3, + csv_data_file='', + pixdim=None, + axcodes=None, + spatial_window_size=(75, 75, 75), + loader=None), + 'sampler': + ParserNamespace( + csv_file='', + path_to_search='', + filename_contains=(), + filename_not_contains=(), + interp_order=0, + pixdim=None, + axcodes=None, + spatial_window_size=(), + loader=None, + csv_data_file='data/csv_data/ICBMTest4.csv') +} +LARGE_MOD_TASK = ParserNamespace( + image=('T1', ), label=('T1', ), sampler=('sampler', )) + +CSV_DATA = { + 'sampler': + ParserNamespace( + csv_file='', + path_to_search='', + filename_contains=(), + filename_not_contains=(), + interp_order=0, + pixdim=None, + axcodes=None, + spatial_window_size=(), + loader=None, + csv_data_file='data/csv_data/ICBMTest3.csv') +} + +CSV_DATA_TWO_ELEMENTS = { + 'sampler': + ParserNamespace( + csv_file='', + path_to_search='', + filename_contains=(), + filename_not_contains=(), + interp_order=0, + pixdim=None, + axcodes=None, + spatial_window_size=(), + loader=None, + csv_data_file='data/csv_data/ICBMTest4.csv') +} + +CSVBAD_DATA = { + 'sampler': + ParserNamespace( + csv_file='', + path_to_search='', + filename_contains=(), + filename_not_contains=(), + interp_order=0, + pixdim=None, + axcodes=None, + spatial_window_size=(), + loader=None, + csv_data_file='data/csv_data/ICBMTest.csv') +} + +data_partitioner = ImageSetsPartitioner() +# multi_mod_list = data_partitioner.initialise(MULTI_MOD_DATA).get_file_list() +# mod_2d_list = data_partitioner.initialise(MOD_2D_DATA).get_file_list() +dynamic_list = data_partitioner.initialise(DYNAMIC_MOD_DATA).get_file_list() + +# def get_3d_reader(): +# reader = ImageReader(['image']) +# reader.initialise(MULTI_MOD_DATA, MULTI_MOD_TASK, multi_mod_list) +# return reader + +# def get_2d_reader(): +# reader = ImageReader(['image']) +# reader.initialise(MOD_2D_DATA, MOD_2D_TASK, mod_2d_list) +# return reader + + +def get_dynamic_window_reader(): + reader = ImageReader(['image']) + reader.initialise(DYNAMIC_MOD_DATA, DYNAMIC_MOD_TASK, dynamic_list) + return reader + + +def get_large_window_reader(): + reader = ImageReader(['image']) + reader.initialise(LARGE_MOD_DATA, LARGE_MOD_TASK, dynamic_list) + return reader + + +def get_large_window_reader_two_elements(): + reader = ImageReader(['image']) + reader.initialise(LARGE_MOD_DATA_2_ELEMENTS, LARGE_MOD_TASK, dynamic_list) + return reader + + +# def get_concentric_window_reader(): +# reader = ImageReader(['image', 'label']) +# reader.initialise(MULTI_WINDOW_DATA, MULTI_WINDOW_TASK, multi_mod_list) +# return reader + + +def get_csvpatch_reader_two_elements(): + csv_reader = CSVReader(['sampler']) + csv_reader.initialise(CSV_DATA_TWO_ELEMENTS, DYNAMIC_MOD_TASK, + dynamic_list) + return csv_reader + + +def get_csvpatch_reader(): + csv_reader = CSVReader(['sampler']) + csv_reader.initialise(CSV_DATA, DYNAMIC_MOD_TASK, dynamic_list) + return csv_reader + + +def get_csvpatchbad_reader(): + csv_reader = CSVReader(['sampler']) + csv_reader.initialise(CSVBAD_DATA, DYNAMIC_MOD_TASK, dynamic_list) + return csv_reader + + +class CSVPatchSamplerTest(NiftyNetTestCase): + def test_3d_csvsampler_init(self): + sampler = CSVPatchSampler( + reader=get_dynamic_window_reader(), + csv_reader=get_csvpatch_reader(), + window_sizes=DYNAMIC_MOD_DATA, + batch_size=2, + windows_per_image=1, + queue_length=3) + with self.cached_session() as sess: + sampler.set_num_threads(2) + out = sess.run(sampler.pop_batch_op()) + img_loc = out['image_location'] + # print(img_loc) + self.assertAllClose(out['image'].shape, (2, 69, 69, 69, 1)) + sampler.close_all() + + def test_pad_init(self): + sampler = CSVPatchSampler( + reader=get_large_window_reader(), + csv_reader=get_csvpatch_reader(), + window_sizes=LARGE_MOD_DATA, + batch_size=2, + windows_per_image=1, + queue_length=3) + + with self.cached_session() as sess: + sampler.set_num_threads(2) + out = sess.run(sampler.pop_batch_op()) + img_loc = out['image_location'] + # print(img_loc) + self.assertAllClose(out['image'].shape[1:], (75, 75, 75, 1)) + sampler.close_all() + + def test_padd_volume(self): + sampler = CSVPatchSampler( + reader=get_large_window_reader(), + csv_reader=get_csvpatch_reader(), + window_sizes=LARGE_MOD_DATA, + batch_size=2, + windows_per_image=1, + queue_length=3) + with self.cached_session() as sess: + sampler.set_num_threads(2) + out = sess.run(sampler.pop_batch_op()) + img_loc = out['image_location'] + # print(img_loc) + self.assertAllClose(out['image'].shape[1:], (75, 75, 75, 1)) + sampler.close_all() + + def test_change_orientation(self): + sampler = CSVPatchSampler( + reader=get_large_window_reader(), + csv_reader=get_csvpatch_reader(), + window_sizes=LARGE_MOD_DATA, + batch_size=2, + windows_per_image=1, + queue_length=3) + with self.cached_session() as sess: + sampler.set_num_threads(2) + out = sess.run(sampler.pop_batch_op()) + img_loc = out['image_location'] + # print(img_loc) + self.assertAllClose(out['image'].shape[1:], (75, 75, 75, 1)) + sampler.close_all() + + def test_random_init(self): + sampler = CSVPatchSampler( + reader=get_large_window_reader(), + csv_reader=get_csvpatch_reader(), + window_sizes=LARGE_MOD_DATA, + batch_size=2, + windows_per_image=1, + queue_length=3, + mode_correction='random') + with self.cached_session() as sess: + sampler.set_num_threads(2) + out = sess.run(sampler.pop_batch_op()) + img_loc = out['image_location'] + # print(img_loc) + self.assertAllClose(out['image'].shape[1:], (75, 75, 75, 1)) + sampler.close_all() + + def test_remove_element_two_elements(self): + sampler = CSVPatchSampler( + reader=get_large_window_reader_two_elements(), + csv_reader=get_csvpatch_reader_two_elements(), + window_sizes=LARGE_MOD_DATA_2_ELEMENTS, + batch_size=2, + windows_per_image=1, + queue_length=3, + mode_correction='remove') + with self.cached_session() as sess: + sampler.set_num_threads(1) + try: + out = sess.run(sampler.pop_batch_op()) + passed = True + except Exception: + passed = False + self.assertTrue(passed) + + def test_remove_element_one_element(self): + sampler = CSVPatchSampler( + reader=get_large_window_reader(), + csv_reader=get_csvpatch_reader(), + window_sizes=LARGE_MOD_DATA, + batch_size=2, + windows_per_image=1, + queue_length=3, + mode_correction='remove') + with self.assertRaisesRegexp(Exception, ""): + with self.cached_session() as sess: + sampler.set_num_threads(1) + out = sess.run(sampler.pop_batch_op()) + + def test_ill_init(self): + with self.assertRaisesRegexp(Exception, ""): + sampler = \ + CSVPatchSampler(reader=get_dynamic_window_reader(), + csv_reader=get_csvpatchbad_reader(), + window_sizes=DYNAMIC_MOD_DATA, + batch_size=2, + windows_per_image=10, + queue_length=3) + + # + + # def test_close_early(self): + # sampler = UniformSampler(reader=get_dynamic_window_reader(), + # window_sizes=DYNAMIC_MOD_DATA, + # batch_size=2, + # windows_per_image=10, + # queue_length=10) + + +class RandomCoordinatesTest(NiftyNetTestCase): + def assertCoordinatesAreValid(self, coords, img_size, win_size): + for coord in coords: + for i in range(len(coord.shape)): + self.assertTrue(coord[i] >= int(win_size[i] / 2)) + + self.assertTrue(coord[i] <= img_size[i] - int(win_size[i] / 2)) + + def test_3d_coordinates(self): + img_size = [8, 9, 10] + win_size = [7, 9, 4] + coords = rand_spatial_coordinates(32, img_size, win_size, None) + self.assertAllEqual(coords.shape, (32, N_SPATIAL)) + self.assertCoordinatesAreValid(coords, img_size, win_size) + + def test_2d_coordinates(self): + + cropped_map = np.zeros((256, 512, 1)) + img_size = [8, 9, 1] + win_size = [8, 8, 1] + coords = rand_spatial_coordinates(64, img_size, win_size, None) + self.assertAllEqual(coords.shape, (64, N_SPATIAL)) + self.assertCoordinatesAreValid(coords, img_size, win_size) + + def test_1d_coordinates(self): + cropped_map = np.zeros((1, 1, 1)) + img_size = [4, 1, 1] + win_size = [2, 1, 1] + coords = rand_spatial_coordinates(20, img_size, win_size, None) + # print(coords) + self.assertAllEqual(coords.shape, (20, N_SPATIAL)) + self.assertCoordinatesAreValid(coords, img_size, win_size) + + +if __name__ == "__main__": + tf.test.main() diff --git a/tests/sampler_grid_v2_test.py b/tests/sampler_grid_v2_test.py index d679038c..ab5a63b6 100755 --- a/tests/sampler_grid_v2_test.py +++ b/tests/sampler_grid_v2_test.py @@ -11,6 +11,7 @@ from niftynet.io.image_reader import ImageReader from niftynet.io.image_sets_partitioner import ImageSetsPartitioner from niftynet.utilities.util_common import ParserNamespace +from tests.niftynet_testcase import NiftyNetTestCase MULTI_MOD_DATA = { 'T1': ParserNamespace( @@ -103,7 +104,7 @@ def get_dynamic_window_reader(): return reader -class GridSamplerTest(tf.test.TestCase): +class GridSamplerTest(NiftyNetTestCase): def test_3d_initialising(self): sampler = GridSampler(reader=get_3d_reader(), window_sizes=MULTI_MOD_DATA, @@ -111,7 +112,7 @@ def test_3d_initialising(self): spatial_window_size=None, window_border=(0, 0, 0), queue_length=10) - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) out = sess.run(sampler.pop_batch_op()) self.assertAllClose(out['image'].shape, (10, 8, 10, 2, 2)) @@ -124,7 +125,7 @@ def test_25d_initialising(self): spatial_window_size=(1, 20, 15), window_border=(0, 0, 0), queue_length=10) - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) out = sess.run(sampler.pop_batch_op()) self.assertAllClose(out['image'].shape, (10, 20, 15, 2)) @@ -137,7 +138,7 @@ def test_2d_initialising(self): spatial_window_size=None, window_border=(0, 0, 0), queue_length=10) - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(1) out = sess.run(sampler.pop_batch_op()) self.assertAllClose(out['image'].shape, (10, 10, 7, 1)) @@ -150,7 +151,7 @@ def test_dynamic_window_initialising(self): spatial_window_size=None, window_border=(0, 0, 0), queue_length=10) - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(1) out = sess.run(sampler.pop_batch_op()) self.assertAllClose(out['image'].shape, (10, 8, 2, 256, 2)) @@ -173,7 +174,7 @@ def test_name_mismatch(self): queue_length=10) -class CoordinatesTest(tf.test.TestCase): +class CoordinatesTest(NiftyNetTestCase): def test_coordinates(self): coords = grid_spatial_coordinates( subject_id=1, @@ -306,7 +307,7 @@ def test_nopadding_coordinates(self): border_size=(0, 0, 0)) -class StepPointsTest(tf.test.TestCase): +class StepPointsTest(NiftyNetTestCase): def test_steps(self): loc = _enumerate_step_points(0, 10, 4, 1) self.assertAllClose(loc, [0, 1, 2, 3, 4, 5, 6]) diff --git a/tests/sampler_linear_interpolate_v2_test.py b/tests/sampler_linear_interpolate_v2_test.py index aef6868e..5a25659f 100755 --- a/tests/sampler_linear_interpolate_v2_test.py +++ b/tests/sampler_linear_interpolate_v2_test.py @@ -9,6 +9,7 @@ from niftynet.io.image_reader import ImageReader from niftynet.io.image_sets_partitioner import ImageSetsPartitioner from niftynet.utilities.util_common import ParserNamespace +from tests.niftynet_testcase import NiftyNetTestCase MULTI_MOD_DATA = { 'T1': ParserNamespace( @@ -46,7 +47,7 @@ def get_3d_reader(): return reader -class LinearInterpolateSamplerTest(tf.test.TestCase): +class LinearInterpolateSamplerTest(NiftyNetTestCase): def test_init(self): sampler = LinearInterpolateSampler( reader=get_3d_reader(), @@ -54,7 +55,7 @@ def test_init(self): batch_size=1, n_interpolations=8, queue_length=1) - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) out = sess.run(sampler.pop_batch_op()) self.assertAllClose(out['image'].shape, [1, 256, 168, 256, 2]) diff --git a/tests/sampler_random_vector_v2_test.py b/tests/sampler_random_vector_v2_test.py index 5370ee32..1363679b 100755 --- a/tests/sampler_random_vector_v2_test.py +++ b/tests/sampler_random_vector_v2_test.py @@ -5,15 +5,15 @@ import tensorflow as tf from niftynet.engine.sampler_random_vector_v2 import RandomVectorSampler +from tests.niftynet_testcase import NiftyNetTestCase - -class RandomVectorSamplerTest(tf.test.TestCase): +class RandomVectorSamplerTest(NiftyNetTestCase): def test_random_vector(self): sampler = RandomVectorSampler(names=('test_vector',), vector_size=(100,), batch_size=20, repeat=None) - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) out = sess.run(sampler.pop_batch_op()) self.assertAllClose(out['test_vector'].shape, (20, 100)) @@ -38,7 +38,7 @@ def test_repeat(self): batch_size=batch_size, n_interpolations=n_interpolations, repeat=repeat) - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(1) n_output = 0 for _ in range(2): diff --git a/tests/sampler_resize_v2_test.py b/tests/sampler_resize_v2_test.py index 364d7ec8..73cee5df 100755 --- a/tests/sampler_resize_v2_test.py +++ b/tests/sampler_resize_v2_test.py @@ -9,6 +9,7 @@ from niftynet.io.image_reader import ImageReader from niftynet.io.image_sets_partitioner import ImageSetsPartitioner from niftynet.utilities.util_common import ParserNamespace +from tests.niftynet_testcase import NiftyNetTestCase MULTI_MOD_DATA = { 'T1': ParserNamespace( @@ -101,7 +102,7 @@ def get_dynamic_window_reader(): return reader -class ResizeSamplerTest(tf.test.TestCase): +class ResizeSamplerTest(NiftyNetTestCase): def test_3d_init(self): sampler = ResizeSampler( reader=get_3d_reader(), @@ -109,7 +110,7 @@ def test_3d_init(self): batch_size=1, shuffle=False, queue_length=1) - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) out = sess.run(sampler.pop_batch_op()) self.assertAllClose(out['image'].shape, [1, 7, 10, 2, 2]) @@ -122,7 +123,7 @@ def test_dynamic_init(self): batch_size=1, shuffle=False, queue_length=1) - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) out = sess.run(sampler.pop_batch_op()) self.assertAllClose(out['image'].shape, [1, 8, 2, 256, 2]) @@ -135,7 +136,7 @@ def test_2d_init(self): batch_size=1, shuffle=True, queue_length=1) - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) out = sess.run(sampler.pop_batch_op()) self.assertAllClose(out['image'].shape, [1, 10, 9, 1]) diff --git a/tests/sampler_uniform_v2_test.py b/tests/sampler_uniform_v2_test.py index 2c0d8509..bfd81ccd 100755 --- a/tests/sampler_uniform_v2_test.py +++ b/tests/sampler_uniform_v2_test.py @@ -13,6 +13,7 @@ from niftynet.io.image_sets_partitioner import ImageSetsPartitioner from niftynet.utilities.util_common import ParserNamespace from niftynet.engine.image_window import N_SPATIAL +from tests.niftynet_testcase import NiftyNetTestCase MULTI_MOD_DATA = { 'T1': ParserNamespace( @@ -136,14 +137,14 @@ def get_concentric_window_reader(): return reader -class UniformSamplerTest(tf.test.TestCase): +class UniformSamplerTest(NiftyNetTestCase): def test_3d_concentric_init(self): sampler = UniformSampler(reader=get_concentric_window_reader(), window_sizes=MULTI_WINDOW_DATA, batch_size=2, windows_per_image=10, queue_length=10) - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) out = sess.run(sampler.pop_batch_op()) img_loc = out['image_location'] @@ -161,7 +162,7 @@ def test_3d_init(self): batch_size=2, windows_per_image=10, queue_length=10) - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) out = sess.run(sampler.pop_batch_op()) self.assertAllClose(out['image'].shape, (2, 7, 10, 2, 2)) @@ -173,7 +174,7 @@ def test_2d_init(self): batch_size=2, windows_per_image=10, queue_length=10) - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) out = sess.run(sampler.pop_batch_op()) self.assertAllClose(out['image'].shape, (2, 10, 9, 1)) @@ -185,7 +186,7 @@ def test_dynamic_init(self): batch_size=2, windows_per_image=10, queue_length=10) - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) out = sess.run(sampler.pop_batch_op()) self.assertAllClose(out['image'].shape[1:], (8, 2, 256, 2)) @@ -208,7 +209,7 @@ def test_close_early(self): sampler.close_all() -class RandomCoordinatesTest(tf.test.TestCase): +class RandomCoordinatesTest(NiftyNetTestCase): def assertCoordinatesAreValid(self, coords, img_size, win_size): for coord in coords: for i in range(len(coord.shape)): diff --git a/tests/sampler_weighted_v2_test.py b/tests/sampler_weighted_v2_test.py index 7828a07c..02198820 100755 --- a/tests/sampler_weighted_v2_test.py +++ b/tests/sampler_weighted_v2_test.py @@ -12,6 +12,7 @@ from niftynet.io.image_reader import ImageReader from niftynet.io.image_sets_partitioner import ImageSetsPartitioner from niftynet.utilities.util_common import ParserNamespace +from tests.niftynet_testcase import NiftyNetTestCase MULTI_MOD_DATA = { 'T1': ParserNamespace( @@ -107,14 +108,14 @@ def get_dynamic_window_reader(): return reader -class WeightedSamplerTest(tf.test.TestCase): +class WeightedSamplerTest(NiftyNetTestCase): def test_3d_init(self): sampler = WeightedSampler(reader=get_3d_reader(), window_sizes=MULTI_MOD_DATA, batch_size=2, windows_per_image=10, queue_length=10) - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) out = sess.run(sampler.pop_batch_op()) self.assertAllClose(out['image'].shape, (2, 7, 10, 2, 2)) @@ -126,7 +127,7 @@ def test_2d_init(self): batch_size=2, windows_per_image=10, queue_length=10) - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) out = sess.run(sampler.pop_batch_op()) self.assertAllClose(out['image'].shape, (2, 10, 9, 1)) @@ -138,7 +139,7 @@ def test_dynamic_init(self): batch_size=2, windows_per_image=10, queue_length=10) - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) out = sess.run(sampler.pop_batch_op()) self.assertAllClose(out['image'].shape[1:], (8, 2, 256, 2)) @@ -160,7 +161,7 @@ def test_close_early(self): sampler.close_all() -class WeightedCoordinatesTest(tf.test.TestCase): +class WeightedCoordinatesTest(NiftyNetTestCase): def assertCoordinatesAreValid(self, coords, sampling_map): for coord in coords: for i in range(len(coord.shape)): diff --git a/tests/scaleblock_test.py b/tests/scaleblock_test.py index 732ef554..07424773 100755 --- a/tests/scaleblock_test.py +++ b/tests/scaleblock_test.py @@ -7,10 +7,10 @@ from tensorflow.contrib.layers.python.layers import regularizers from niftynet.network.scalenet import ScaleBlock - +from tests.niftynet_testcase import NiftyNetTestCase @unittest.skipIf(os.environ.get('QUICKTEST', "").lower() == "true", 'Skipping slow tests') -class ScaleBlockTest(tf.test.TestCase): +class ScaleBlockTest(NiftyNetTestCase): def get_2d_input(self): input_shape = (2, 32, 32, 4) x = tf.ones(input_shape) @@ -39,7 +39,7 @@ def test_2d_shape(self): out_2 = scalenet_layer(x, is_training=True) print(scalenet_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out_1 = sess.run(out_1) out_2 = sess.run(out_2) @@ -59,7 +59,7 @@ def test_2d_reg_shape(self): out_2 = scalenet_layer(x, is_training=True) print(scalenet_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out_1 = sess.run(out_1) out_2 = sess.run(out_2) @@ -76,7 +76,7 @@ def test_3d_shape(self): out_2 = scalenet_layer(x, is_training=True) print(scalenet_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out_1 = sess.run(out_1) out_2 = sess.run(out_2) @@ -96,7 +96,7 @@ def test_3d_reg_shape(self): out_2 = scalenet_layer(x, is_training=True) print(scalenet_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out_1 = sess.run(out_1) out_2 = sess.run(out_2) diff --git a/tests/scalenet_test.py b/tests/scalenet_test.py index 050347e3..ff4bd74b 100755 --- a/tests/scalenet_test.py +++ b/tests/scalenet_test.py @@ -7,10 +7,10 @@ from tensorflow.contrib.layers.python.layers import regularizers from niftynet.network.scalenet import ScaleNet - +from tests.niftynet_testcase import NiftyNetTestCase @unittest.skipIf(os.environ.get('QUICKTEST', "").lower() == "true", 'Skipping slow tests') -class ScaleNetTest(tf.test.TestCase): +class ScaleNetTest(NiftyNetTestCase): def test_3d_shape(self): input_shape = (2, 32, 32, 32, 4) x = tf.ones(input_shape) @@ -19,7 +19,7 @@ def test_3d_shape(self): out = scalenet_layer(x, is_training=True) print(scalenet_layer.num_trainable_params()) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 32, 32, 32, 5), out.shape) @@ -32,7 +32,7 @@ def test_2d_shape(self): out = scalenet_layer(x, is_training=True) print(scalenet_layer.num_trainable_params()) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 32, 32, 5), out.shape) @@ -47,7 +47,7 @@ def test_3d_reg_shape(self): out = scalenet_layer(x, is_training=True) print(scalenet_layer.num_trainable_params()) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 32, 32, 32, 5), out.shape) @@ -62,7 +62,7 @@ def test_2d_reg_shape(self): out = scalenet_layer(x, is_training=True) print(scalenet_layer.num_trainable_params()) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 32, 32, 5), out.shape) diff --git a/tests/segmentation_evaluator_test.py b/tests/segmentation_evaluator_test.py index 598f8589..54fff988 100755 --- a/tests/segmentation_evaluator_test.py +++ b/tests/segmentation_evaluator_test.py @@ -6,8 +6,9 @@ from niftynet.evaluation.segmentation_evaluator import SegmentationEvaluator from niftynet.io.misc_io import set_logger +from tests.niftynet_testcase import NiftyNetTestCase -class SegmentationEvaluatorTest(tf.test.TestCase): +class SegmentationEvaluatorTest(NiftyNetTestCase): def test_basic(self): class NS(object): def __init__(self, dict): diff --git a/tests/simple_gan_test.py b/tests/simple_gan_test.py index f3d8447a..b790aa7d 100755 --- a/tests/simple_gan_test.py +++ b/tests/simple_gan_test.py @@ -7,9 +7,9 @@ from tensorflow.contrib.layers.python.layers import regularizers from niftynet.network.simple_gan import SimpleGAN +from tests.niftynet_testcase import NiftyNetTestCase - -class SimpleGANTest(tf.test.TestCase): +class SimpleGANTest(NiftyNetTestCase): def test_3d_reg_shape(self): input_shape = (2, 32, 32, 32, 1) noise_shape = (2, 512) @@ -19,7 +19,7 @@ def test_3d_reg_shape(self): simple_gan_instance = SimpleGAN() out = simple_gan_instance(r, x, is_training=True) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose(input_shape, out[0].shape) @@ -35,7 +35,7 @@ def test_2d_reg_shape(self): simple_gan_instance = SimpleGAN() out = simple_gan_instance(r, x, is_training=True) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose(input_shape, out[0].shape) diff --git a/tests/spatial_gradient_test.py b/tests/spatial_gradient_test.py index 45a348e2..d1346220 100755 --- a/tests/spatial_gradient_test.py +++ b/tests/spatial_gradient_test.py @@ -5,9 +5,9 @@ import tensorflow as tf from niftynet.layer.spatial_gradient import SpatialGradientLayer +from tests.niftynet_testcase import NiftyNetTestCase - -class SpatialGradientTest(tf.test.TestCase): +class SpatialGradientTest(NiftyNetTestCase): def get_3d_input(self): input_shape = (2, 16, 16, 16, 8) @@ -32,7 +32,7 @@ def _test_nd_gradient_output_shape(self, gradient_layer = SpatialGradientLayer(**param_dict) output_data = gradient_layer(input_data) print(gradient_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(output_data) if expected_value is not None: diff --git a/tests/spatial_transformer_test.py b/tests/spatial_transformer_test.py index ff7d9288..c80e3e49 100755 --- a/tests/spatial_transformer_test.py +++ b/tests/spatial_transformer_test.py @@ -4,9 +4,9 @@ from niftynet.layer.resampler import ResamplerLayer from niftynet.layer.spatial_transformer import ResampledFieldGridWarperLayer +from tests.niftynet_testcase import NiftyNetTestCase - -class ResamplerTest(tf.test.TestCase): +class ResamplerTest(NiftyNetTestCase): def get_3d_input1(self): return tf.expand_dims(tf.constant( [[[[1, 2, -1], [3, 4, -2]], [[5, 6, -3], [7, 8, -4]]], @@ -21,7 +21,7 @@ def _test_correctness(self, input, grid, interpolation, boundary, resampler = ResamplerLayer(interpolation=interpolation, boundary=boundary) out = resampler(input, grid) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out_value = sess.run(out) self.assertAllClose(expected_value, out_value) diff --git a/tests/squeeze_excitation_test.py b/tests/squeeze_excitation_test.py index 02a10579..b799671d 100755 --- a/tests/squeeze_excitation_test.py +++ b/tests/squeeze_excitation_test.py @@ -5,15 +5,16 @@ from niftynet.layer.squeeze_excitation import ChannelSELayer from niftynet.layer.squeeze_excitation import SpatialSELayer from niftynet.layer.squeeze_excitation import ChannelSpatialSELayer +from tests.niftynet_testcase import NiftyNetTestCase -class SETest(tf.test.TestCase): +class SETest(NiftyNetTestCase): def test_cSE_3d_shape(self): input_shape = (2, 16, 16, 16, 32) x = tf.ones(input_shape) se_layer = ChannelSELayer() out_se = se_layer(x) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out_se) x_shape = tuple(x.shape.as_list()) @@ -25,7 +26,7 @@ def test_sSE_3d_shape(self): se_layer = SpatialSELayer() out_se = se_layer(x) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out_se) x_shape = tuple(x.shape.as_list()) @@ -37,7 +38,7 @@ def test_csSE_3d_shape(self): se_layer = ChannelSpatialSELayer() out_se = se_layer(x) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out_se) x_shape = tuple(x.shape.as_list()) @@ -49,7 +50,7 @@ def test_cSE_2d_shape(self): se_layer = ChannelSELayer() out_se = se_layer(x) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out_se) x_shape = tuple(x.shape.as_list()) @@ -61,7 +62,7 @@ def test_sSE_2d_shape(self): se_layer = SpatialSELayer() out_se = se_layer(x) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out_se) x_shape = tuple(x.shape.as_list()) @@ -73,7 +74,7 @@ def test_csSE_2d_shape(self): se_layer = ChannelSpatialSELayer() out_se = se_layer(x) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out_se) x_shape = tuple(x.shape.as_list()) @@ -106,7 +107,7 @@ def test_cSE_3d_excitation_op(self): div_0_1=out_0_1/x_0_1 div_1_1=out_1_1/x_1_1 - with self.test_session() as sess: + with self.cached_session() as sess: self.assertAlmostEqual(div_0_0, div_1_0,places=5) self.assertAlmostEqual(div_0_1, div_1_1,places=5) @@ -131,7 +132,7 @@ def test_sSE_3d_excitation_op(self): div_0_0=out_0_0/x_0_0 div_0_1=out_0_1/x_0_1 - with self.test_session() as sess: + with self.cached_session() as sess: self.assertAlmostEqual(div_0_0, div_0_1,places=5) def test_cSE_2d_excitation_op(self): @@ -161,7 +162,7 @@ def test_cSE_2d_excitation_op(self): div_0_1=out_0_1/x_0_1 div_1_1=out_1_1/x_1_1 - with self.test_session() as sess: + with self.cached_session() as sess: self.assertAlmostEqual(div_0_0, div_1_0,places=5) self.assertAlmostEqual(div_0_1, div_1_1,places=5) @@ -186,11 +187,11 @@ def test_sSE_2d_excitation_op(self): div_0_0=out_0_0/x_0_0 div_0_1=out_0_1/x_0_1 - with self.test_session() as sess: + with self.cached_session() as sess: self.assertAlmostEqual(div_0_0, div_0_1,places=5) def test_cSE_pooling_op_error(self): - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) with self.assertRaises(ValueError): diff --git a/tests/subpixel_test.py b/tests/subpixel_test.py new file mode 100644 index 00000000..55922ace --- /dev/null +++ b/tests/subpixel_test.py @@ -0,0 +1,91 @@ +from __future__ import division, absolute_import, print_function + +import functools as ft +import numpy as np +import tensorflow as tf + +from niftynet.layer.subpixel import SubPixelLayer +from tests.niftynet_testcase import NiftyNetTestCase + +class SubPixelTest(NiftyNetTestCase): + """ + Test for niftynet.layer.subpixel.SubPixelLayer. + Mostly adapted from convolution_test.py + """ + + def get_3d_input(self): + input_shape = (2, 16, 16, 16, 8) + x_3d = tf.ones(input_shape) + return x_3d + + def get_2d_input(self): + input_shape = (2, 16, 16, 8) + x_2d = tf.ones(input_shape) + return x_2d + + def _test_subpixel_output_shape(self, + input_data, + param_dict, + output_shape): + layer = SubPixelLayer(**param_dict) + output_data = layer(input_data) + print(layer) + with self.cached_session() as sess: + sess.run(tf.global_variables_initializer()) + output_value = sess.run(output_data) + self.assertAllClose(output_shape, output_value.shape) + + def _make_output_shape(self, data, upsampling): + data_shape = data.shape.as_list() + output_shape = [data_shape[0]] + output_shape += [upsampling * d for d in data_shape[1:-1]] + output_shape += [data_shape[-1]] + + return output_shape + + def test_3d_default(self): + data = self.get_3d_input() + + output_shape = self._make_output_shape(data, 3) + + self._test_subpixel_output_shape(data, + {}, + output_shape) + + def test_3d_bespoke(self): + data = self.get_3d_input() + upsampling = 4 + + output_shape = self._make_output_shape(data, upsampling) + + params = {'upsample_factor': upsampling, + 'layer_configurations': ((6, 32), + (3, 32), + (2, 16), + (4, -1)), + 'padding': 'SAME'} + + self._test_subpixel_output_shape(data, + params, + output_shape) + + def test_2d_bespoke(self): + data = self.get_2d_input() + upsampling = 6 + + output_shape = self._make_output_shape(data, upsampling) + + params = {'upsample_factor': upsampling, + 'layer_configurations': ((6, 32), + (3, 32), + (2, 16), + (4, -1)), + 'padding': 'SAME'} + + self._test_subpixel_output_shape(data, + params, + output_shape) + + +if __name__ == "__main__": + tf.test.main() diff --git a/tests/test_model_zoo.py b/tests/test_model_zoo.py index 8dfce685..5e7ea0f9 100755 --- a/tests/test_model_zoo.py +++ b/tests/test_model_zoo.py @@ -12,6 +12,7 @@ from niftynet.utilities.niftynet_global_config import NiftyNetGlobalConfig from niftynet import main as niftynet_main from niftynet.application.base_application import SingletonApplication +from tests.niftynet_testcase import NiftyNetTestCase MODEL_HOME = NiftyNetGlobalConfig().get_niftynet_home_folder() @@ -28,16 +29,16 @@ def net_run_with_sys_argv(argv): @unittest.skipIf(os.environ.get('QUICKTEST', "").lower() == "true", 'Skipping slow tests') -class DenseVNetAbdominalCTModelZooTest(tf.test.TestCase): - id = 'dense_vnet_abdominal_ct_model_zoo' +class DenseVNetAbdominalCTModelZooTest(NiftyNetTestCase): + zoo_id = 'dense_vnet_abdominal_ct_model_zoo' location = 'dense_vnet_abdominal_ct' config = os.path.join(MODEL_HOME, 'extensions', 'dense_vnet_abdominal_ct', 'config.ini') application = 'net_segment' - expected_output = os.path.join('segmentation_output','100__niftynet_out.nii.gz') + expected_output = os.path.join('segmentation_output','window_seg_100___niftynet_out.nii.gz') def setUp(self): - tf.test.TestCase.setUp(self) - download(self.id, download_if_already_existing=True, verbose=False) + NiftyNetTestCase.setUp(self) + download(self.zoo_id, download_if_already_existing=True, verbose=False) def test_train_infer(self): self._train() @@ -55,16 +56,16 @@ def _infer(self): @unittest.skipIf(os.environ.get('QUICKTEST', "").lower() == "true", 'Skipping slow tests') -class UltrasoundSimulatorGanModelZooTest(tf.test.TestCase): - id = 'ultrasound_simulator_gan_model_zoo' +class UltrasoundSimulatorGanModelZooTest(NiftyNetTestCase): + zoo_id = 'ultrasound_simulator_gan_model_zoo' location = 'ultrasound_simulator_gan' config = os.path.join(MODEL_HOME, 'extensions', 'ultrasound_simulator_gan', 'config.ini') application = 'net_gan' - expected_output = os.path.join('ultrasound_gan_simulated','0_000053__niftynet_generated.nii.gz') + expected_output = os.path.join('ultrasound_gan_simulated','5_000053__window_image_niftynet_generated.nii.gz') def setUp(self): - tf.test.TestCase.setUp(self) - download(self.id, download_if_already_existing=True, verbose=False) + NiftyNetTestCase.setUp(self) + download(self.zoo_id, download_if_already_existing=True, verbose=False) def test_inference(self): net_run_with_sys_argv(['net_run', '-a', self.application, '-c', self.config, 'inference']) @@ -73,16 +74,16 @@ def test_inference(self): @unittest.skipIf(os.environ.get('QUICKTEST', "").lower() == "true", 'Skipping slow tests') -class Highres3dnetBrainParcellationModelZooTest(tf.test.TestCase): - id = 'highres3dnet_brain_parcellation_model_zoo' +class Highres3dnetBrainParcellationModelZooTest(NiftyNetTestCase): + zoo_id = 'highres3dnet_brain_parcellation_model_zoo' location = 'highres3dnet_brain_parcellation' config = os.path.join(MODEL_HOME, 'extensions', 'highres3dnet_brain_parcellation', 'highres3dnet_config_eval.ini') application = 'net_segment' - expected_output = os.path.join('parcellation_output','OAS1_0145_MR2_mpr_n4_anon_sbj_111_niftynet_out.nii.gz') + expected_output = os.path.join('parcellation_output','window_seg_OAS1_0145_MR2_mpr_n4_anon_sbj_111__niftynet_out.nii.gz') def setUp(self): - tf.test.TestCase.setUp(self) - download(self.id, download_if_already_existing=True, verbose=False) + NiftyNetTestCase.setUp(self) + download(self.zoo_id, download_if_already_existing=True, verbose=False) def test_inference(self): net_run_with_sys_argv(['net_run', '-a', self.application, '-c', self.config, 'inference']) @@ -91,20 +92,20 @@ def test_inference(self): @unittest.skipIf(os.environ.get('QUICKTEST', "").lower() == "true", 'Skipping slow tests') -class AnisotropicNetsBratsChallengeModelZooTest(tf.test.TestCase): - id = 'anisotropic_nets_brats_challenge_model_zoo' +class AnisotropicNetsBratsChallengeModelZooTest(NiftyNetTestCase): + zoo_id = 'anisotropic_nets_brats_challenge_model_zoo' location = 'anisotropic_nets_brats_challenge' application = 'anisotropic_nets_brats_challenge.brats_seg_app.BRATSApp' - expected_outputs = [os.path.join('model_whole_tumor_axial','pred_whole_tumor_axial','LGG71__niftynet_out.nii.gz'), - os.path.join('model_whole_tumor_coronal','pred_whole_tumor_coronal','LGG71__niftynet_out.nii.gz'), - os.path.join('model_whole_tumor_sagittal','pred_whole_tumor_sagittal','LGG71__niftynet_out.nii.gz')] + expected_outputs = [os.path.join('model_whole_tumor_axial','pred_whole_tumor_axial','window_LGG71__niftynet_out.nii.gz'), + os.path.join('model_whole_tumor_coronal','pred_whole_tumor_coronal','window_LGG71__niftynet_out.nii.gz'), + os.path.join('model_whole_tumor_sagittal','pred_whole_tumor_sagittal','window_LGG71__niftynet_out.nii.gz')] configA = os.path.join(MODEL_HOME, 'extensions', 'anisotropic_nets_brats_challenge', 'whole_tumor_axial.ini') configC = os.path.join(MODEL_HOME, 'extensions', 'anisotropic_nets_brats_challenge', 'whole_tumor_coronal.ini') configS = os.path.join(MODEL_HOME, 'extensions', 'anisotropic_nets_brats_challenge', 'whole_tumor_sagittal.ini') def setUp(self): - tf.test.TestCase.setUp(self) - download(self.id, download_if_already_existing=True, verbose=False) + NiftyNetTestCase.setUp(self) + download(self.zoo_id, download_if_already_existing=True, verbose=False) def test_inference(self): net_run_with_sys_argv(['net_run', '-a', self.application, '-c', self.configA, 'inference']) @@ -116,8 +117,8 @@ def test_inference(self): @unittest.skipIf(os.environ.get('QUICKTEST', "").lower() == "true", 'Skipping slow tests') -class MRCTRegressionModelZooTest(tf.test.TestCase): - id = 'mr_ct_regression_model_zoo' +class MRCTRegressionModelZooTest(NiftyNetTestCase): + zoo_id = 'mr_ct_regression_model_zoo' location = 'mr_ct_regression' application = 'niftynet.contrib.regression_weighted_sampler.isample_regression.ISampleRegression' config = os.path.join(MODEL_HOME, 'extensions', 'mr_ct_regression','net_isampler.ini') @@ -126,8 +127,8 @@ class MRCTRegressionModelZooTest(tf.test.TestCase): ] def setUp(self): - tf.test.TestCase.setUp(self) - download(self.id, download_if_already_existing=True, verbose=False) + NiftyNetTestCase.setUp(self) + download(self.zoo_id, download_if_already_existing=True, verbose=False) def test_train(self): net_run_with_sys_argv(['net_run', '-a', self.application, '-c', self.config, 'train', '--starting_iter','0','--max_iter', '2']) @@ -144,8 +145,8 @@ def test_train(self): @unittest.skipIf(os.environ.get('QUICKTEST', "").lower() == "true", 'Skipping slow tests') -class AutoContextMRCTModelZooTest(tf.test.TestCase): - id = 'autocontext_mr_ct_model_zoo' +class AutoContextMRCTModelZooTest(NiftyNetTestCase): + zoo_id = 'autocontext_mr_ct_model_zoo' location = 'autocontext_mr_ct' application = 'net_regress' config = os.path.join(MODEL_HOME, 'extensions', 'autocontext_mr_ct','net_autocontext.ini') @@ -153,13 +154,13 @@ class AutoContextMRCTModelZooTest(tf.test.TestCase): os.path.join('error_maps','WEB.nii.gz'), ] expected_output_inference = [ - os.path.join('autocontext_output','CHA_niftynet_out.nii.gz'), + os.path.join('autocontext_output','window_reg_CHA_niftynet_out.nii.gz'), ] def setUp(self): - tf.test.TestCase.setUp(self) - download(self.id, download_if_already_existing=True, verbose=False) + NiftyNetTestCase.setUp(self) + download(self.zoo_id, download_if_already_existing=True, verbose=False) def test_train_infer(self): self._train() diff --git a/tests/toy_application.py b/tests/toy_application.py index 059d52ce..221a7319 100755 --- a/tests/toy_application.py +++ b/tests/toy_application.py @@ -47,6 +47,7 @@ def connect_data_and_network(self, outputs_collector=None, gradients_collector=None): print(vars(self.action_param)) + self.patience = self.action_param.patience with tf.name_scope('Optimiser'): optimiser_class = OptimiserFactory.create( name=self.action_param.optimiser) @@ -88,6 +89,9 @@ def collect_results(self, d_loss, fake_features, g_loss, outputs_collector): outputs_collector.add_to_collection( var=g_var, name='var', average_over_devices=True, collection=CONSOLE) + outputs_collector.add_to_collection( + var=g_loss, name='total_loss', average_over_devices=True, + collection=CONSOLE) outputs_collector.add_to_collection( var=g_mean, name='generated_mean', average_over_devices=False, collection=TF_SUMMARIES) @@ -122,6 +126,7 @@ def __init__(self, net_param, action_param, action): def connect_data_and_network(self, outputs_collector=None, gradients_collector=None): + self.patience = self.action_param.patience print(vars(self.action_param)) self.optimiser = dict() with tf.name_scope('OptimiserGen'): @@ -181,11 +186,11 @@ def __init__(self): def layer_op(self, features): batch_size = features.shape.as_list()[0] conv_1 = ConvolutionalLayer( - 20, 3, with_bn=False, with_bias=True, acti_func='relu') + 20, 3, feature_normalization=None, with_bias=True, acti_func='relu') fc_1 = FullyConnectedLayer( - 20, with_bn=False, with_bias=True, acti_func='relu') + 20, feature_normalization=None, with_bias=True, acti_func='relu') fc_2 = FullyConnectedLayer( - 2, with_bn=False, with_bias=True) + 2, feature_normalization=None, with_bias=True) hidden_feature = conv_1(features, is_training=True) hidden_feature = tf.reshape(hidden_feature, [batch_size, -1]) @@ -201,11 +206,11 @@ def __init__(self): def layer_op(self, noise): n_chns = noise.shape[-1] conv_1 = ConvolutionalLayer( - 20, 10, with_bn=True, acti_func='selu', with_bias=True) + 20, 10, feature_normalization='batch', acti_func='selu', with_bias=True) conv_2 = ConvolutionalLayer( - 20, 10, with_bn=True, acti_func='selu', with_bias=True) + 20, 10, feature_normalization='batch', acti_func='selu', with_bias=True) conv_3 = ConvolutionalLayer( - n_chns, 10, with_bn=False, with_bias=True) + n_chns, 10, feature_normalization=None, with_bias=True) hidden_feature = conv_1(noise, is_training=True) hidden_feature = conv_2(hidden_feature, is_training=True) fake_features = conv_3(hidden_feature, is_training=True) diff --git a/tests/toynet_test.py b/tests/toynet_test.py index 971d73e1..931c24bd 100755 --- a/tests/toynet_test.py +++ b/tests/toynet_test.py @@ -3,9 +3,9 @@ import tensorflow as tf from niftynet.network.toynet import ToyNet +from tests.niftynet_testcase import NiftyNetTestCase - -class ToyNetTest(tf.test.TestCase): +class ToyNetTest(NiftyNetTestCase): def test_3d_shape(self): input_shape = (2, 32, 32, 32, 1) x = tf.ones(input_shape) @@ -13,7 +13,7 @@ def test_3d_shape(self): toynet_instance = ToyNet(num_classes=160) out = toynet_instance(x, is_training=True) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 32, 32, 32, 160), out.shape) @@ -25,7 +25,7 @@ def test_2d_shape(self): toynet_instance = ToyNet(num_classes=160) out = toynet_instance(x, is_training=True) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 32, 32, 160), out.shape) diff --git a/tests/unet_2d_test.py b/tests/unet_2d_test.py index 4edb6363..6a872a82 100755 --- a/tests/unet_2d_test.py +++ b/tests/unet_2d_test.py @@ -7,11 +7,11 @@ from tensorflow.contrib.layers.python.layers import regularizers from niftynet.network.unet_2d import UNet2D - +from tests.niftynet_testcase import NiftyNetTestCase @unittest.skipIf(os.environ.get('QUICKTEST', "").lower() == "true", 'Skipping slow tests') -class UNet3DTest(tf.test.TestCase): +class UNet3DTest(NiftyNetTestCase): def test_2d_shape(self): #input_shape = (2, 572, 572, 3) input_shape = (2, 180, 180, 3) @@ -21,7 +21,7 @@ def test_2d_shape(self): out = unet_instance(x, is_training=True) print(unet_instance.num_trainable_params()) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) #self.assertAllClose((2, 388, 388, 2), out.shape) @@ -37,7 +37,7 @@ def test_2d_reg_shape(self): out = unet_instance(x, is_training=True) print(unet_instance.num_trainable_params()) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) #self.assertAllClose((2, 388, 388, 2), out.shape) diff --git a/tests/unet_test.py b/tests/unet_test.py index 23b5330b..b02bb6e1 100755 --- a/tests/unet_test.py +++ b/tests/unet_test.py @@ -6,10 +6,10 @@ from tensorflow.contrib.layers.python.layers import regularizers from niftynet.network.unet import UNet3D - +from tests.niftynet_testcase import NiftyNetTestCase @unittest.skip('Test currently disabled') -class UNet3DTest(tf.test.TestCase): +class UNet3DTest(NiftyNetTestCase): def test_3d_shape(self): input_shape = (2, 96, 96, 96, 1) x = tf.ones(input_shape) @@ -18,7 +18,7 @@ def test_3d_shape(self): out = unet_instance(x, is_training=True) print(unet_instance.num_trainable_params()) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 8, 8, 8, 160), out.shape) @@ -31,7 +31,7 @@ def test_2d_shape(self): out = unet_instance(x, is_training=True) print(unet_instance.num_trainable_params()) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 8, 8, 160), out.shape) @@ -45,7 +45,7 @@ def test_3d_reg_shape(self): out = unet_instance(x, is_training=True) print(unet_instance.num_trainable_params()) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 8, 8, 8, 160), out.shape) @@ -59,7 +59,7 @@ def test_2d_reg_shape(self): out = unet_instance(x, is_training=True) print(unet_instance.num_trainable_params()) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 8, 8, 160), out.shape) diff --git a/tests/unetblock_test.py b/tests/unetblock_test.py index ee4f60e3..511243be 100755 --- a/tests/unetblock_test.py +++ b/tests/unetblock_test.py @@ -4,9 +4,9 @@ from tensorflow.contrib.layers.python.layers import regularizers from niftynet.network.unet import UNetBlock +from tests.niftynet_testcase import NiftyNetTestCase - -class UNetBlockTest(tf.test.TestCase): +class UNetBlockTest(NiftyNetTestCase): def get_2d_input(self): input_shape = (2, 16, 16, 8) x = tf.ones(input_shape) @@ -33,7 +33,7 @@ def test_2d_shape(self): print(unet_block_op) print(out_3) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out_1 = sess.run(out_1) self.assertAllClose((2, 8, 8, 64), out_1.shape) @@ -58,7 +58,7 @@ def test_3d_shape(self): print(unet_block_op) print(out_3) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out_1 = sess.run(out_1) self.assertAllClose((2, 8, 8, 8, 64), out_1.shape) @@ -85,7 +85,7 @@ def test_2d_reg_shape(self): print(unet_block_op) print(out_3) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out_1 = sess.run(out_1) self.assertAllClose((2, 8, 8, 64), out_1.shape) @@ -112,7 +112,7 @@ def test_3d_reg_shape(self): print(unet_block_op) print(out_3) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out_1 = sess.run(out_1) self.assertAllClose((2, 8, 8, 8, 64), out_1.shape) diff --git a/tests/upsample_res_block_test.py b/tests/upsample_res_block_test.py index fe35a9a3..15597ec2 100755 --- a/tests/upsample_res_block_test.py +++ b/tests/upsample_res_block_test.py @@ -4,9 +4,9 @@ import tensorflow as tf from niftynet.layer.upsample_res_block import UpBlock +from tests.niftynet_testcase import NiftyNetTestCase - -class UpsampleResBlockTest(tf.test.TestCase): +class UpsampleResBlockTest(NiftyNetTestCase): def get_3d_input(self): input_shape = (2, 16, 16, 16, 8) x = tf.ones(input_shape) @@ -29,7 +29,7 @@ def _test_nd_output_shape(self, upsample_layer = UpBlock(**param_dict) output_data = upsample_layer(input_data) print(upsample_layer) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(output_data) self.assertAllClose(output_shape, out.shape) diff --git a/tests/upsample_test.py b/tests/upsample_test.py index 44250ec4..e4fddb26 100755 --- a/tests/upsample_test.py +++ b/tests/upsample_test.py @@ -3,9 +3,9 @@ import tensorflow as tf from niftynet.layer.upsample import UpSampleLayer +from tests.niftynet_testcase import NiftyNetTestCase - -class UpSampleTest(tf.test.TestCase): +class UpSampleTest(NiftyNetTestCase): def get_3d_input(self): input_shape = (4, 16, 16, 16, 8) x = tf.ones(input_shape) @@ -24,7 +24,7 @@ def _test_upsample_shape(self, rank, param_dict, output_shape): upsample_layer = UpSampleLayer(**param_dict) output_data = upsample_layer(input_data) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) output = sess.run(output_data) self.assertAllClose(output_shape, output.shape) diff --git a/tests/user_parameters_default_test.py b/tests/user_parameters_default_test.py index 03f5fac3..db0c71f7 100755 --- a/tests/user_parameters_default_test.py +++ b/tests/user_parameters_default_test.py @@ -6,9 +6,9 @@ import tensorflow as tf from niftynet.utilities.user_parameters_default import * +from tests.niftynet_testcase import NiftyNetTestCase - -class TestUserParameters(tf.test.TestCase): +class TestUserParameters(NiftyNetTestCase): def test_list_all(self): test_parser = argparse.ArgumentParser(conflict_handler='resolve') test_parser = add_application_args(test_parser) diff --git a/tests/user_parameters_regex_test.py b/tests/user_parameters_regex_test.py index b480c6c5..e09e05f3 100755 --- a/tests/user_parameters_regex_test.py +++ b/tests/user_parameters_regex_test.py @@ -5,9 +5,9 @@ import tensorflow as tf from niftynet.utilities.user_parameters_regex import STATEMENT +from tests.niftynet_testcase import NiftyNetTestCase - -class UserParameterRegexTest(tf.test.TestCase): +class UserParameterRegexTest(NiftyNetTestCase): def run_match(self, string_to_match, expected_output): regex = re.compile(STATEMENT) matched_str = regex.match(string_to_match) diff --git a/tests/util_import_test.py b/tests/util_import_test.py index 98f2eed5..ab17bbea 100755 --- a/tests/util_import_test.py +++ b/tests/util_import_test.py @@ -2,9 +2,9 @@ import tensorflow as tf from niftynet.utilities.util_import import require_module +from tests.niftynet_testcase import NiftyNetTestCase - -class OptionalPackageTest(tf.test.TestCase): +class OptionalPackageTest(NiftyNetTestCase): def test_installed(self): require_module('tensorflow') diff --git a/tests/versioning_test.py b/tests/versioning_test.py index 11cae38c..30bf9898 100755 --- a/tests/versioning_test.py +++ b/tests/versioning_test.py @@ -5,9 +5,10 @@ from niftynet.utilities.versioning import check_pep_440 from niftynet.utilities.versioning import get_niftynet_version_string +from tests.niftynet_testcase import NiftyNetTestCase -class VersioningTest(tf.test.TestCase): +class VersioningTest(NiftyNetTestCase): def test_version(self): version_str = get_niftynet_version_string() expected_string = "NiftyNet version " diff --git a/tests/vnet_test.py b/tests/vnet_test.py index 20b99f69..48bd2146 100755 --- a/tests/vnet_test.py +++ b/tests/vnet_test.py @@ -4,9 +4,9 @@ from tensorflow.contrib.layers.python.layers import regularizers from niftynet.network.vnet import VNet +from tests.niftynet_testcase import NiftyNetTestCase - -class VNetTest(tf.test.TestCase): +class VNetTest(NiftyNetTestCase): def test_3d_shape(self): input_shape = (2, 32, 32, 32, 1) x = tf.ones(input_shape) @@ -16,7 +16,7 @@ def test_3d_shape(self): out = vnet_instance(x, is_training=True) print(vnet_instance.num_trainable_params()) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 32, 32, 32, 160), out.shape) @@ -30,7 +30,7 @@ def test_2d_shape(self): out = vnet_instance(x, is_training=True) print(vnet_instance.num_trainable_params()) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 32, 32, 160), out.shape) @@ -48,7 +48,7 @@ def test_3d_reg_shape(self): print(vnet_instance.num_trainable_params()) # print(tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES)) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 32, 32, 32, 160), out.shape) @@ -67,7 +67,7 @@ def test_2d_reg_shape(self): # print(tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES)) # print(vnet_instance.regularizer_loss()) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out = sess.run(out) self.assertAllClose((2, 32, 32, 160), out.shape) diff --git a/tests/vnetblock_test.py b/tests/vnetblock_test.py index 332ffbbd..8f29d1f1 100755 --- a/tests/vnetblock_test.py +++ b/tests/vnetblock_test.py @@ -4,9 +4,9 @@ from tensorflow.contrib.layers.python.layers import regularizers from niftynet.network.vnet import VNetBlock +from tests.niftynet_testcase import NiftyNetTestCase - -class VNetBlockTest(tf.test.TestCase): +class VNetBlockTest(NiftyNetTestCase): def get_2d_data(self): input_shape = (2, 16, 16, 8) x = tf.ones(input_shape) @@ -31,7 +31,7 @@ def test_3d_shape(self): out_5, out_6 = vnet_block_op(x, x) print(vnet_block_op) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out_1 = sess.run(out_1) self.assertAllClose((2, 16, 16, 16, 16), out_1.shape) @@ -60,7 +60,7 @@ def test_2d_shape(self): out_5, out_6 = vnet_block_op(x, x) print(vnet_block_op) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out_1 = sess.run(out_1) self.assertAllClose((2, 16, 16, 16), out_1.shape) @@ -95,7 +95,7 @@ def test_3d_reg_shape(self): out_5, out_6 = vnet_block_op(x, x) print(vnet_block_op) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out_1 = sess.run(out_1) self.assertAllClose((2, 16, 16, 16, 16), out_1.shape) @@ -130,7 +130,7 @@ def test_2d_reg_shape(self): out_5, out_6 = vnet_block_op(x, x) print(vnet_block_op) - with self.test_session() as sess: + with self.cached_session() as sess: sess.run(tf.global_variables_initializer()) out_1 = sess.run(out_1) self.assertAllClose((2, 16, 16, 16), out_1.shape) diff --git a/tests/windows_aggregator_grid_v2_test.py b/tests/windows_aggregator_grid_v2_test.py index d495772a..42d472bb 100755 --- a/tests/windows_aggregator_grid_v2_test.py +++ b/tests/windows_aggregator_grid_v2_test.py @@ -5,7 +5,8 @@ import nibabel as nib import tensorflow as tf - +import numpy as np +import pandas as pd from niftynet.engine.sampler_grid_v2 import GridSampler from niftynet.engine.windows_aggregator_grid import GridSamplesAggregator from niftynet.io.image_reader import ImageReader @@ -14,6 +15,7 @@ DiscreteLabelNormalisationLayer from niftynet.layer.pad import PadLayer from niftynet.utilities.util_common import ParserNamespace +from tests.niftynet_testcase import NiftyNetTestCase MULTI_MOD_DATA = { 'T1': ParserNamespace( @@ -118,13 +120,19 @@ def get_label_reader(): return reader +def get_nonnormalising_label_reader(): + reader = ImageReader(['label']) + reader.initialise(MOD_LABEL_DATA, MOD_LABEl_TASK, mod_label_list) + return reader + + def get_25d_reader(): reader = ImageReader(['image']) reader.initialise(SINGLE_25D_DATA, SINGLE_25D_TASK, single_25d_list) return reader -class GridSamplesAggregatorTest(tf.test.TestCase): +class GridSamplesAggregatorTest(NiftyNetTestCase): def test_3d_init(self): reader = get_3d_reader() sampler = GridSampler(reader=reader, @@ -141,13 +149,13 @@ def test_3d_init(self): interp_order=0) more_batch = True - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) while more_batch: out = sess.run(sampler.pop_batch_op()) more_batch = aggregator.decode_batch( - out['image'], out['image_location']) - output_filename = '{}_niftynet_out.nii.gz'.format( + {'window_image':out['image']}, out['image_location']) + output_filename = 'window_image_{}_niftynet_out.nii.gz'.format( sampler.reader.get_subject_id(0)) output_file = os.path.join('testing_data', 'aggregated', @@ -171,19 +179,19 @@ def test_2d_init(self): window_border=(3, 4, 5), interp_order=0) more_batch = True - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) while more_batch: out = sess.run(sampler.pop_batch_op()) more_batch = aggregator.decode_batch( - out['image'], out['image_location']) - output_filename = '{}_niftynet_out.nii.gz'.format( + {'window_image':out['image']}, out['image_location']) + output_filename = 'window_image_{}_niftynet_out.nii.gz'.format( sampler.reader.get_subject_id(0)) output_file = os.path.join('testing_data', 'aggregated', output_filename) self.assertAllClose( - nib.load(output_file).shape, [128, 128, 1, 1, 1]) + nib.load(output_file).shape, [128, 128]) sampler.close_all() def test_25d_init(self): @@ -201,22 +209,390 @@ def test_25d_init(self): window_border=(3, 4, 5), interp_order=0) more_batch = True - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) while more_batch: out = sess.run(sampler.pop_batch_op()) more_batch = aggregator.decode_batch( - out['image'], out['image_location']) - output_filename = '{}_niftynet_out.nii.gz'.format( + {'window_image':out['image']}, out['image_location']) + output_filename = 'window_image_{}_niftynet_out.nii.gz'.format( sampler.reader.get_subject_id(0)) output_file = os.path.join('testing_data', 'aggregated', output_filename) + print(output_file) self.assertAllClose( - nib.load(output_file).shape, [256, 168, 256, 1, 1], + nib.load(output_file).shape, [256, 168, 256], rtol=1e-03, atol=1e-03) sampler.close_all() + def test_3d_init_mo(self): + reader = get_3d_reader() + sampler = GridSampler(reader=reader, + window_sizes=MULTI_MOD_DATA, + batch_size=10, + spatial_window_size=None, + window_border=(3, 4, 5), + queue_length=50) + aggregator = GridSamplesAggregator( + image_reader=reader, + name='image', + output_path=os.path.join('testing_data', 'aggregated'), + window_border=(3, 4, 5), + interp_order=0) + more_batch = True + + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + out = sess.run(sampler.pop_batch_op()) + out_flatten = np.reshape(np.asarray(out['image']), [10, -1]) + min_val = np.sum(np.reshape(np.asarray(out['image']), + [10, -1]), 1) + more_batch = aggregator.decode_batch( + {'window_image': out['image'], 'csv_sum': min_val}, + out['image_location']) + output_filename = 'window_image_{}_niftynet_out.nii.gz'.format( + sampler.reader.get_subject_id(0)) + sum_filename = os.path.join( + 'testing_data', 'aggregated', + 'csv_sum_{}_niftynet_out.csv'.format( + sampler.reader.get_subject_id(0))) + output_file = os.path.join('testing_data', + 'aggregated', + output_filename) + + self.assertAllClose( + nib.load(output_file).shape, (256, 168, 256, 1, 2)) + min_pd = pd.read_csv(sum_filename) + self.assertAllClose( + min_pd.shape, [420, 9] + ) + sampler.close_all() + + def test_3d_init_mo_2im(self): + reader = get_3d_reader() + sampler = GridSampler(reader=reader, + window_sizes=MULTI_MOD_DATA, + batch_size=10, + spatial_window_size=None, + window_border=(3, 4, 5), + queue_length=50) + aggregator = GridSamplesAggregator( + image_reader=reader, + name='image', + output_path=os.path.join('testing_data', 'aggregated'), + window_border=(3, 4, 5), + interp_order=0) + more_batch = True + + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + out = sess.run(sampler.pop_batch_op()) + + more_batch = aggregator.decode_batch( + {'window_image': out['image'], 'window_im2': out['image']}, + out['image_location']) + output_filename = 'window_image_{}_niftynet_out.nii.gz'.format( + sampler.reader.get_subject_id(0)) + outim2_filename = os.path.join( + 'testing_data', 'aggregated', + 'window_im2_{}_niftynet_out.nii.gz'.format( + sampler.reader.get_subject_id(0))) + output_file = os.path.join('testing_data', + 'aggregated', + output_filename) + self.assertAllClose( + nib.load(output_file).shape, (256, 168, 256, 1, 2)) + self.assertAllClose( + nib.load(outim2_filename).shape, (256, 168, 256, 1, 2)) + sampler.close_all() + + def test_init_3d_mo_3out(self): + reader = get_3d_reader() + sampler = GridSampler(reader=reader, + window_sizes=MULTI_MOD_DATA, + batch_size=10, + spatial_window_size=None, + window_border=(3, 4, 5), + queue_length=50) + aggregator = GridSamplesAggregator( + image_reader=reader, + name='image', + output_path=os.path.join('testing_data', 'aggregated'), + window_border=(3, 4, 5), + interp_order=0) + more_batch = True + + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + out = sess.run(sampler.pop_batch_op()) + print(out['image'].shape) + out_flatten = np.reshape(np.asarray(out['image']), [10, -1]) + min_val = np.sum(np.reshape( + np.asarray(out['image']), [10, -1]), 1) + stats_val = np.concatenate( + [np.min(out_flatten, 1, keepdims=True), + np.max(out_flatten, 1, keepdims=True), + np.sum(out_flatten, 1, keepdims=True)], 1) + more_batch = aggregator.decode_batch( + {'window_image': out['image'], 'csv_sum': min_val, + 'csv_stats': stats_val}, + out['image_location']) + output_filename = 'window_image_{}_niftynet_out.nii.gz'.format( + sampler.reader.get_subject_id(0)) + sum_filename = os.path.join( + 'testing_data', 'aggregated', + 'csv_sum_{}_niftynet_out.csv'.format( + sampler.reader.get_subject_id(0))) + stats_filename = os.path.join( + 'testing_data', 'aggregated', + 'csv_stats_{}_niftynet_out.csv'.format( + sampler.reader.get_subject_id(0))) + output_file = os.path.join('testing_data', + 'aggregated', + output_filename) + + self.assertAllClose( + nib.load(output_file).shape, (256, 168, 256, 1, 2)) + min_pd = pd.read_csv(sum_filename) + self.assertAllClose( + min_pd.shape, [420, 9] + ) + stats_pd = pd.read_csv(stats_filename) + self.assertAllClose( + stats_pd.shape, [420, 11] + ) + sampler.close_all() + + def test_init_3d_mo_bidimcsv(self): + reader = get_3d_reader() + sampler = GridSampler(reader=reader, + window_sizes=MULTI_MOD_DATA, + batch_size=10, + spatial_window_size=None, + window_border=(3, 4, 5), + queue_length=50) + aggregator = GridSamplesAggregator( + image_reader=reader, + name='image', + output_path=os.path.join('testing_data', 'aggregated'), + window_border=(3, 4, 5), + interp_order=0) + more_batch = True + + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + out = sess.run(sampler.pop_batch_op()) + out_flatten = np.reshape(np.asarray(out['image']), [10, -1]) + min_val = np.sum(np.reshape( + np.asarray(out['image']), [10, -1]), 1) + stats_val = np.concatenate( + [np.min(out_flatten, 1, keepdims=True), + np.max(out_flatten, 1, keepdims=True), + np.sum(out_flatten, 1, keepdims=True)], 1) + stats_val = np.expand_dims(stats_val, 1) + stats_val = np.concatenate([stats_val, stats_val], axis=1) + more_batch = aggregator.decode_batch( + {'window_image': out['image'], + 'csv_sum': min_val, + 'csv_stats_2d': stats_val}, + out['image_location']) + output_filename = 'window_image_{}_niftynet_out.nii.gz'.format( + sampler.reader.get_subject_id(0)) + sum_filename = os.path.join( + 'testing_data', 'aggregated', + 'csv_sum_{}_niftynet_out.csv'.format( + sampler.reader.get_subject_id(0))) + stats_filename = os.path.join( + 'testing_data', 'aggregated', + 'csv_stats_2d_{}_niftynet_out.csv'.format( + sampler.reader.get_subject_id(0))) + output_file = os.path.join('testing_data', + 'aggregated', + output_filename) + + self.assertAllClose( + nib.load(output_file).shape, (256, 168, 256, 1, 2)) + min_pd = pd.read_csv(sum_filename) + self.assertAllClose( + min_pd.shape, [420, 9] + ) + stats_pd = pd.read_csv(stats_filename) + self.assertAllClose( + stats_pd.shape, [420, 14] + ) + sampler.close_all() + + def test_init_2d_mo(self): + reader = get_2d_reader() + sampler = GridSampler(reader=reader, + window_sizes=MOD_2D_DATA, + batch_size=10, + spatial_window_size=None, + window_border=(3, 4, 5), + queue_length=50) + aggregator = GridSamplesAggregator( + image_reader=reader, + name='image', + output_path=os.path.join('testing_data', 'aggregated'), + window_border=(3, 4, 5), + interp_order=0) + more_batch = True + + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + out = sess.run(sampler.pop_batch_op()) + out_flatten = np.reshape(np.asarray(out['image']), [10, -1]) + min_val = np.sum(np.reshape( + np.asarray(out['image']), [10, -1]), 1) + stats_val = np.concatenate( + [np.min(out_flatten, 1, keepdims=True), + np.max(out_flatten, 1, keepdims=True), + np.sum(out_flatten, 1, keepdims=True)], 1) + more_batch = aggregator.decode_batch( + {'window_image': out['image'], 'csv_sum': min_val}, + out['image_location']) + output_filename = 'window_image_{}_niftynet_out.nii.gz'.format( + sampler.reader.get_subject_id(0)) + sum_filename = os.path.join( + 'testing_data', 'aggregated', + 'csv_sum_{}_niftynet_out.csv'.format( + sampler.reader.get_subject_id(0))) + output_file = os.path.join('testing_data', + 'aggregated', + output_filename) + + self.assertAllClose( + nib.load(output_file).shape, (128, 128)) + min_pd = pd.read_csv(sum_filename) + self.assertAllClose( + min_pd.shape, [10, 9] + ) + sampler.close_all() + + def test_init_2d_mo_3out(self): + reader = get_2d_reader() + sampler = GridSampler(reader=reader, + window_sizes=MOD_2D_DATA, + batch_size=10, + spatial_window_size=None, + window_border=(3, 4, 5), + queue_length=50) + aggregator = GridSamplesAggregator( + image_reader=reader, + name='image', + output_path=os.path.join('testing_data', 'aggregated'), + window_border=(3, 4, 5), + interp_order=0) + more_batch = True + + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + out = sess.run(sampler.pop_batch_op()) + out_flatten = np.reshape(np.asarray(out['image']), [10, -1]) + min_val = np.sum(np.reshape( + np.asarray(out['image']), [10, -1]), 1) + stats_val = np.concatenate( + [np.min(out_flatten, 1, keepdims=True), + np.max(out_flatten, 1, keepdims=True), + np.sum(out_flatten, 1, keepdims=True)], 1) + more_batch = aggregator.decode_batch( + {'window_image': out['image'], + 'csv_sum': min_val, + 'csv_stats': stats_val}, + out['image_location']) + output_filename = 'window_image_{}_niftynet_out.nii.gz'.format( + sampler.reader.get_subject_id(0)) + sum_filename = os.path.join( + 'testing_data', 'aggregated', + 'csv_sum_{}_niftynet_out.csv'.format( + sampler.reader.get_subject_id(0))) + stats_filename = os.path.join( + 'testing_data', 'aggregated', + 'csv_stats_{}_niftynet_out.csv'.format( + sampler.reader.get_subject_id(0))) + output_file = os.path.join('testing_data', + 'aggregated', + output_filename) + + self.assertAllClose( + nib.load(output_file).shape, (128, 128)) + min_pd = pd.read_csv(sum_filename) + self.assertAllClose( + min_pd.shape, [10, 9] + ) + stats_pd = pd.read_csv(stats_filename) + self.assertAllClose( + stats_pd.shape, [10, 11] + ) + sampler.close_all() + + def test_init_2d_mo_bidimcsv(self): + reader = get_2d_reader() + sampler = GridSampler(reader=reader, + window_sizes=MOD_2D_DATA, + batch_size=10, + spatial_window_size=None, + window_border=(3, 4, 5), + queue_length=50) + aggregator = GridSamplesAggregator( + image_reader=reader, + name='image', + output_path=os.path.join('testing_data', 'aggregated'), + window_border=(3, 4, 5), + interp_order=0) + more_batch = True + + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + out = sess.run(sampler.pop_batch_op()) + out_flatten = np.reshape(np.asarray(out['image']), [10, -1]) + min_val = np.sum(np.reshape( + np.asarray(out['image']), [10, -1]), 1) + stats_val = np.concatenate( + [np.min(out_flatten, 1, keepdims=True), + np.max(out_flatten, 1, keepdims=True), + np.sum(out_flatten, 1, keepdims=True)], 1) + stats_val = np.expand_dims(stats_val, 1) + stats_val = np.concatenate([stats_val, stats_val], axis=1) + more_batch = aggregator.decode_batch( + {'window_image': out['image'], + 'csv_sum': min_val, + 'csv_stats_2d': stats_val}, + out['image_location']) + output_filename = 'window_image_{}_niftynet_out.nii.gz'.format( + sampler.reader.get_subject_id(0)) + sum_filename = os.path.join( + 'testing_data', 'aggregated', + 'csv_sum_{}_niftynet_out.csv'.format( + sampler.reader.get_subject_id(0))) + stats_filename = os.path.join( + 'testing_data', 'aggregated', + 'csv_stats_2d_{}_niftynet_out.csv'.format( + sampler.reader.get_subject_id(0))) + output_file = os.path.join('testing_data', + 'aggregated', + output_filename) + + self.assertAllClose( + nib.load(output_file).shape, (128, 128)) + min_pd = pd.read_csv(sum_filename) + self.assertAllClose( + min_pd.shape, [10, 9] + ) + stats_pd = pd.read_csv(stats_filename) + self.assertAllClose( + stats_pd.shape, [10, 14] + ) + sampler.close_all() + def test_inverse_mapping(self): reader = get_label_reader() data_param = MOD_LABEL_DATA @@ -233,24 +609,72 @@ def test_inverse_mapping(self): window_border=(3, 4, 5), interp_order=0) more_batch = True - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) while more_batch: out = sess.run(sampler.pop_batch_op()) more_batch = aggregator.decode_batch( - out['label'], out['label_location']) - output_filename = '{}_niftynet_out.nii.gz'.format( + {'window_label': out['label']}, out['label_location']) + output_filename = 'window_label_{}_niftynet_out.nii.gz'.format( sampler.reader.get_subject_id(0)) output_file = os.path.join( 'testing_data', 'aggregated', output_filename) self.assertAllClose( - nib.load(output_file).shape, [256, 168, 256, 1, 1]) + nib.load(output_file).shape, [256, 168, 256]) sampler.close_all() - output_data = nib.load(output_file).get_data()[..., 0, 0] + output_data = nib.load(output_file).get_data() expected_data = nib.load( 'testing_data/T1_1023_NeuroMorph_Parcellation.nii.gz').get_data() self.assertAllClose(output_data, expected_data) + def test_filling(self): + reader = get_nonnormalising_label_reader() + test_constant = 0.5731 + postfix = '_niftynet_out_background' + test_border = (10, 7, 8) + data_param = MOD_LABEL_DATA + sampler = GridSampler(reader=reader, + window_sizes=data_param, + batch_size=10, + spatial_window_size=None, + window_border=test_border, + queue_length=50) + aggregator = GridSamplesAggregator( + image_reader=reader, + name='label', + output_path=os.path.join('testing_data', 'aggregated'), + window_border=test_border, + interp_order=0, + postfix=postfix, + fill_constant=test_constant) + more_batch = True + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + out = sess.run(sampler.pop_batch_op()) + more_batch = aggregator.decode_batch( + {'window_label': out['label']}, out['label_location']) + output_filename = 'window_label_{}_{}.nii.gz'.format( + sampler.reader.get_subject_id(0), postfix) + output_file = os.path.join( + 'testing_data', 'aggregated', output_filename) + output_data = nib.load(output_file).get_data() + output_shape = output_data.shape + for i in range(3): + def _test_background(idcs): + extract = output_data[idcs] + self.assertTrue((extract == test_constant).sum() + == extract.size) + + extract_idcs = [slice(None)]*3 + + extract_idcs[i] = slice(0, test_border[i]) + _test_background(tuple(extract_idcs)) + + extract_idcs[i] = slice(output_shape[i] - test_border[i], + output_shape[i]) + _test_background(tuple(extract_idcs)) + if __name__ == "__main__": tf.test.main() diff --git a/tests/windows_aggregator_identity_v2_test.py b/tests/windows_aggregator_identity_v2_test.py new file mode 100755 index 00000000..9809b482 --- /dev/null +++ b/tests/windows_aggregator_identity_v2_test.py @@ -0,0 +1,601 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + +import os + +import nibabel as nib +import tensorflow as tf +import pandas as pd +import numpy as np +from niftynet.engine.sampler_resize_v2 import ResizeSampler +from niftynet.engine.windows_aggregator_identity import WindowAsImageAggregator +from niftynet.io.image_reader import ImageReader +from niftynet.io.image_sets_partitioner import ImageSetsPartitioner +from niftynet.layer.discrete_label_normalisation import \ + DiscreteLabelNormalisationLayer +from niftynet.layer.pad import PadLayer +from niftynet.utilities.util_common import ParserNamespace +from tests.niftynet_testcase import NiftyNetTestCase + +NEW_ORDER = (0, 1, 2, 4, 3) +NEW_ORDER_2D = (0, 1, 3, 2) +MULTI_MOD_DATA = { + 'T1': ParserNamespace( + csv_file=os.path.join('testing_data', 'T1sampler.csv'), + path_to_search='testing_data', + filename_contains=('_o_T1_time', '23'), + filename_not_contains=('Parcellation',), + interp_order=0, + pixdim=(2.4, 5.0, 2.0), + axcodes='LAS', + spatial_window_size=(23, 32, 15), + loader=None + ), + 'FLAIR': ParserNamespace( + csv_file=os.path.join('testing_data', 'FLAIRsampler.csv'), + path_to_search='testing_data', + filename_contains=('FLAIR_', '23'), + filename_not_contains=('Parcellation',), + interp_order=0, + pixdim=(2.4, 5.0, 2.0), + axcodes='LAS', + spatial_window_size=(23, 32, 15), + loader=None + ) +} +MULTI_MOD_TASK = ParserNamespace(image=('T1', 'FLAIR')) + +MOD_2D_DATA = { + 'ultrasound': ParserNamespace( + csv_file=os.path.join('testing_data', 'T1sampler2d.csv'), + path_to_search='testing_data', + filename_contains=('2d_',), + filename_not_contains=('Parcellation',), + interp_order=0, + pixdim=None, + axcodes=None, + spatial_window_size=(72, 83, 1), + loader=None + ), +} +MOD_2D_TASK = ParserNamespace(image=('ultrasound',)) + +MOD_LABEL_DATA = { + 'parcellation': ParserNamespace( + csv_file=os.path.join('testing_data', 'Parcelsampler2d.csv'), + path_to_search='testing_data', + filename_contains=('23_NeuroMorph_Parcellation',), + filename_not_contains=('FLAIR',), + interp_order=0, + pixdim=None, + axcodes=None, + spatial_window_size=(150, 140, 100), + loader=None + ), +} +MOD_LABEl_TASK = ParserNamespace(label=('parcellation',)) + +SINGLE_25D_DATA = { + 'T1': ParserNamespace( + csv_file=os.path.join('testing_data', 'T1sampler.csv'), + path_to_search='testing_data', + filename_contains=('_o_T1_time', '106'), + filename_not_contains=('Parcellation',), + interp_order=0, + pixdim=(3.0, 5.0, 5.0), + axcodes='LAS', + spatial_window_size=(40, 30, 1), + loader=None + ), +} +SINGLE_25D_TASK = ParserNamespace(image=('T1',)) + +data_partitioner = ImageSetsPartitioner() +multi_mod_list = data_partitioner.initialise(MULTI_MOD_DATA).get_file_list() +mod_2d_list = data_partitioner.initialise(MOD_2D_DATA).get_file_list() +mod_label_list = data_partitioner.initialise(MOD_LABEL_DATA).get_file_list() +single_25d_list = data_partitioner.initialise(SINGLE_25D_DATA).get_file_list() + + +def get_3d_reader(): + ''' + define the 3d reader + :return: 3d reader + ''' + reader = ImageReader(['image']) + reader.initialise(MULTI_MOD_DATA, MULTI_MOD_TASK, multi_mod_list) + return reader + + +def get_2d_reader(): + ''' + define the 2d reader + :return: 2d reader + ''' + reader = ImageReader(['image']) + reader.initialise(MOD_2D_DATA, MOD_2D_TASK, mod_2d_list) + return reader + + +def get_label_reader(): + ''' + define the label reader + :return: label reader + ''' + reader = ImageReader(['label']) + reader.initialise(MOD_LABEL_DATA, MOD_LABEl_TASK, mod_label_list) + label_normaliser = DiscreteLabelNormalisationLayer( + image_name='label', + modalities=vars(SINGLE_25D_TASK).get('label'), + model_filename=os.path.join('testing_data', 'agg_test.txt')) + reader.add_preprocessing_layers(label_normaliser) + pad_layer = PadLayer(image_name=('label',), border=(5, 6, 7)) + reader.add_preprocessing_layers([pad_layer]) + return reader + + +def get_25d_reader(): + ''' + define the 2.5 d reader + :return: + ''' + reader = ImageReader(['image']) + reader.initialise(SINGLE_25D_DATA, SINGLE_25D_TASK, single_25d_list) + return reader + + +class IdentityAggregatorTest(NiftyNetTestCase): + def test_3d_init(self): + reader = get_3d_reader() + sampler = ResizeSampler(reader=reader, + window_sizes=MULTI_MOD_DATA, + batch_size=1, + shuffle=False, + queue_length=50) + aggregator = WindowAsImageAggregator( + image_reader=reader, + output_path=os.path.join('testing_data', 'aggregated_identity'), + ) + more_batch = True + out_shape = [] + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + try: + out = sess.run(sampler.pop_batch_op()) + out_shape = out['image'].shape[1:] + (1,) + except tf.errors.OutOfRangeError: + break + more_batch = aggregator.decode_batch( + {'window_image': out['image']}, out['image_location']) + output_filename = '{}_window_image_niftynet_generated.nii.gz'.format( + sampler.reader.get_subject_id(0)) + output_file = os.path.join('testing_data', + 'aggregated_identity', + output_filename) + out_shape = [out_shape[i] for i in NEW_ORDER] + self.assertAllClose( + nib.load(output_file).shape, out_shape) + sampler.close_all() + + def test_3d_init_mo(self): + reader = get_3d_reader() + sampler = ResizeSampler(reader=reader, + window_sizes=MULTI_MOD_DATA, + batch_size=1, + shuffle=False, + queue_length=50) + aggregator = WindowAsImageAggregator( + image_reader=reader, + output_path=os.path.join('testing_data', 'aggregated_identity') + ) + more_batch = True + out_shape = [] + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + try: + out = sess.run(sampler.pop_batch_op()) + out_shape = out['image'].shape[1:] + (1,) + except tf.errors.OutOfRangeError: + break + sum_val = np.sum(out['image']) + more_batch = aggregator.decode_batch( + {'window_image': out['image'], 'csv_sum': sum_val}, + out['image_location']) + output_filename = '{}_window_image_niftynet_generated.nii.gz'.format( + sampler.reader.get_subject_id(0)) + sum_filename = os.path.join( + 'testing_data', 'aggregated_identity', + '{}_csv_sum_niftynet_generated.csv'.format( + sampler.reader.get_subject_id(0))) + output_file = os.path.join('testing_data', + 'aggregated_identity', + output_filename) + out_shape = [out_shape[i] for i in NEW_ORDER] + self.assertAllClose( + nib.load(output_file).shape, out_shape) + sum_pd = pd.read_csv(sum_filename) + self.assertAllClose( + sum_pd.shape, [1, 2] + ) + sampler.close_all() + + def test_3d_init_mo_2im(self): + reader = get_3d_reader() + sampler = ResizeSampler(reader=reader, + window_sizes=MULTI_MOD_DATA, + batch_size=1, + shuffle=False, + queue_length=50) + aggregator = WindowAsImageAggregator( + image_reader=reader, + output_path=os.path.join('testing_data', 'aggregated_identity'), + ) + more_batch = True + out_shape = [] + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + try: + out = sess.run(sampler.pop_batch_op()) + out_shape = out['image'].shape[1:] + (1,) + except tf.errors.OutOfRangeError: + break + more_batch = aggregator.decode_batch( + {'window_image': out['image'], 'window_im2': out['image']}, + out['image_location']) + output_filename = '{}_window_image_niftynet_generated.nii.gz'.format( + sampler.reader.get_subject_id(0)) + outim2_filename = os.path.join( + 'testing_data', 'aggregated_identity', + '{}_window_im2_niftynet_generated.nii.gz'.format( + sampler.reader.get_subject_id(0))) + output_file = os.path.join('testing_data', + 'aggregated_identity', + output_filename) + out_shape = [out_shape[i] for i in NEW_ORDER] + self.assertAllClose( + nib.load(output_file).shape, out_shape) + self.assertAllClose( + nib.load(outim2_filename).shape, out_shape) + sampler.close_all() + + def test_3d_init_mo_3out(self): + reader = get_3d_reader() + sampler = ResizeSampler(reader=reader, + window_sizes=MULTI_MOD_DATA, + batch_size=1, + shuffle=False, + queue_length=50) + aggregator = WindowAsImageAggregator( + image_reader=reader, + output_path=os.path.join('testing_data', 'aggregated_identity') + ) + more_batch = True + out_shape = [] + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + try: + out = sess.run(sampler.pop_batch_op()) + out_shape = out['image'].shape[1:] + (1,) + except tf.errors.OutOfRangeError: + break + sum_val = np.sum(out['image']) + stats_val = [np.sum(out['image']), + np.min(out['image']), + np.max(out['image'])] + more_batch = aggregator.decode_batch( + {'window_image': out['image'], 'csv_sum': sum_val, + 'csv_stats': stats_val}, + out['image_location']) + output_filename = '{}_window_image_niftynet_generated.nii.gz'.format( + sampler.reader.get_subject_id(0)) + sum_filename = os.path.join( + 'testing_data', 'aggregated_identity', + '{}_csv_sum_niftynet_generated.csv'.format( + sampler.reader.get_subject_id(0))) + stats_filename = os.path.join( + 'testing_data', 'aggregated_identity', + '{}_csv_stats_niftynet_generated.csv'.format( + sampler.reader.get_subject_id(0))) + output_file = os.path.join('testing_data', + 'aggregated_identity', + output_filename) + out_shape = [out_shape[i] for i in NEW_ORDER] + self.assertAllClose( + nib.load(output_file).shape, out_shape) + sum_pd = pd.read_csv(sum_filename) + self.assertAllClose( + sum_pd.shape, [1, 2] + ) + stats_pd = pd.read_csv(stats_filename) + self.assertAllClose( + stats_pd.shape, [1, 4] + ) + sampler.close_all() + + def test_init_3d_mo_bidimcsv(self): + reader = get_3d_reader() + sampler = ResizeSampler(reader=reader, + window_sizes=MULTI_MOD_DATA, + batch_size=1, + shuffle=False, + queue_length=50) + aggregator = WindowAsImageAggregator( + image_reader=reader, + output_path=os.path.join('testing_data', 'aggregated_identity'), + ) + more_batch = True + out_shape = [] + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + try: + out = sess.run(sampler.pop_batch_op()) + out_shape = out['image'].shape[1:] + (1,) + except tf.errors.OutOfRangeError: + break + min_val = np.sum((np.asarray(out['image']).flatten())) + stats_val = [np.min(out['image']), np.max(out['image']), np.sum( + out['image'])] + stats_val = np.expand_dims(stats_val, 0) + stats_val = np.concatenate([stats_val, stats_val], axis=0) + more_batch = aggregator.decode_batch( + {'window_image': out['image'], + 'csv_sum': min_val, + 'csv_stats2d': stats_val}, + out['image_location']) + output_filename = '{}_window_image_niftynet_generated.nii.gz'.format( + sampler.reader.get_subject_id(0)) + sum_filename = os.path.join( + 'testing_data', 'aggregated_identity', + '{}_csv_sum_niftynet_generated.csv'.format( + sampler.reader.get_subject_id(0))) + stats_filename = os.path.join( + 'testing_data', 'aggregated_identity', + '{}_csv_stats2d_niftynet_generated.csv'.format( + sampler.reader.get_subject_id(0))) + output_file = os.path.join('testing_data', + 'aggregated_identity', + output_filename) + out_shape = [out_shape[i] for i in NEW_ORDER] + self.assertAllClose( + nib.load(output_file).shape, out_shape) + min_pd = pd.read_csv(sum_filename) + self.assertAllClose( + min_pd.shape, [1, 2] + ) + stats_pd = pd.read_csv(stats_filename) + self.assertAllClose( + stats_pd.shape, [1, 7] + ) + sampler.close_all() + + def test_2d_init(self): + reader = get_2d_reader() + sampler = ResizeSampler(reader=reader, + window_sizes=MOD_2D_DATA, + batch_size=1, + shuffle=False, + queue_length=50) + aggregator = WindowAsImageAggregator( + image_reader=reader, + output_path=os.path.join('testing_data', 'aggregated_identity'), + ) + more_batch = True + out_shape = [] + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + try: + out = sess.run(sampler.pop_batch_op()) + out_shape = out['image'].shape[1:] + (1,) + except tf.errors.OutOfRangeError: + break + more_batch = aggregator.decode_batch( + {'window_image': out['image']}, out['image_location']) + output_filename = '{}_window_image_niftynet_generated.nii.gz'.format( + sampler.reader.get_subject_id(0)) + output_file = os.path.join('testing_data', + 'aggregated_identity', + output_filename) + out_shape = [out_shape[i] for i in NEW_ORDER_2D] + [1,] + self.assertAllClose( + nib.load(output_file).shape, out_shape[:2]) + sampler.close_all() + + def test_init_2d_mo(self): + reader = get_2d_reader() + sampler = ResizeSampler(reader=reader, + window_sizes=MOD_2D_DATA, + batch_size=1, + shuffle=False, + queue_length=50) + aggregator = WindowAsImageAggregator( + image_reader=reader, + output_path=os.path.join('testing_data', 'aggregated_identity'), + ) + more_batch = True + out_shape = [] + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + try: + out = sess.run(sampler.pop_batch_op()) + out_shape = out['image'].shape[1:] + (1,) + except tf.errors.OutOfRangeError: + break + min_val = np.sum((np.asarray(out['image']).flatten())) + stats_val = [np.min(out), np.max(out), np.sum(out)] + more_batch = aggregator.decode_batch( + {'window_image': out['image'], 'csv_sum': min_val}, + out['image_location']) + output_filename = '{}_window_image_niftynet_generated.nii.gz'.format( + sampler.reader.get_subject_id(0)) + sum_filename = os.path.join( + 'testing_data', 'aggregated_identity', + '{}_csv_sum_niftynet_generated.csv'.format( + sampler.reader.get_subject_id(0))) + output_file = os.path.join('testing_data', + 'aggregated_identity', + output_filename) + out_shape = [out_shape[i] for i in NEW_ORDER_2D] + [1,] + + self.assertAllClose( + nib.load(output_file).shape, out_shape[:2]) + min_pd = pd.read_csv(sum_filename) + self.assertAllClose( + min_pd.shape, [1, 2] + ) + sampler.close_all() + + def test_init_2d_mo_3out(self): + reader = get_2d_reader() + sampler = ResizeSampler(reader=reader, + window_sizes=MOD_2D_DATA, + batch_size=1, + shuffle=False, + queue_length=50) + aggregator = WindowAsImageAggregator( + image_reader=reader, + + output_path=os.path.join('testing_data', 'aggregated_identity'), + ) + more_batch = True + out_shape = [] + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + try: + out = sess.run(sampler.pop_batch_op()) + out_shape = out['image'].shape[1:] + (1,) + except tf.errors.OutOfRangeError: + break + min_val = np.sum((np.asarray(out['image']).flatten())) + stats_val = [np.min(out['image']), np.max(out['image']), np.sum( + out['image'])] + more_batch = aggregator.decode_batch( + {'window_image': out['image'], + 'csv_sum': min_val, + 'csv_stats': stats_val}, + out['image_location']) + output_filename = '{}_window_image_niftynet_generated.nii.gz'.format( + sampler.reader.get_subject_id(0)) + sum_filename = os.path.join('testing_data', 'aggregated_identity', + '{}_csv_sum_niftynet_generated.csv'.format( + sampler.reader.get_subject_id(0))) + stats_filename = os.path.join('testing_data', 'aggregated_identity', + '{}_csv_stats_niftynet_generated.csv'.format( + sampler.reader.get_subject_id(0))) + output_file = os.path.join('testing_data', + 'aggregated_identity', + output_filename) + out_shape = [out_shape[i] for i in NEW_ORDER_2D] + [1,] + self.assertAllClose( + nib.load(output_file).shape, out_shape[:2]) + min_pd = pd.read_csv(sum_filename) + self.assertAllClose( + min_pd.shape, [1, 2] + ) + stats_pd = pd.read_csv(stats_filename) + self.assertAllClose( + stats_pd.shape, [1, 4] + ) + sampler.close_all() + + def test_init_2d_mo_bidimcsv(self): + reader = get_2d_reader() + sampler = ResizeSampler(reader=reader, + window_sizes=MOD_2D_DATA, + batch_size=1, + shuffle=False, + queue_length=50) + aggregator = WindowAsImageAggregator( + image_reader=reader, + + output_path=os.path.join('testing_data', 'aggregated_identity'), + ) + more_batch = True + out_shape = [] + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + try: + out = sess.run(sampler.pop_batch_op()) + out_shape = out['image'].shape[1:] + (1,) + except tf.errors.OutOfRangeError: + break + min_val = np.sum((np.asarray(out['image']).flatten())) + stats_val = [np.min(out['image']), np.max(out['image']), np.sum( + out['image'])] + stats_val = np.expand_dims(stats_val, 0) + stats_val = np.concatenate([stats_val, stats_val], axis=0) + more_batch = aggregator.decode_batch( + {'window_image': out['image'], + 'csv_sum': min_val, + 'csv_stats2d': stats_val}, + out['image_location']) + output_filename = '{}_window_image_niftynet_generated.nii.gz'.format( + sampler.reader.get_subject_id(0)) + sum_filename = os.path.join('testing_data', 'aggregated_identity', + '{}_csv_sum_niftynet_generated.csv'.format( + sampler.reader.get_subject_id(0))) + stats_filename = os.path.join('testing_data', 'aggregated_identity', + '{}_csv_stats2d_niftynet_generated.csv'.format( + sampler.reader.get_subject_id(0))) + output_file = os.path.join('testing_data', + 'aggregated_identity', + output_filename) + + out_shape = [out_shape[i] for i in NEW_ORDER_2D] + [1,] + self.assertAllClose( + nib.load(output_file).shape, out_shape[:2]) + min_pd = pd.read_csv(sum_filename) + self.assertAllClose( + min_pd.shape, [1, 2] + ) + stats_pd = pd.read_csv(stats_filename) + self.assertAllClose( + stats_pd.shape, [1, 7] + ) + sampler.close_all() + + def test_25d_init(self): + reader = get_25d_reader() + sampler = ResizeSampler(reader=reader, + window_sizes=SINGLE_25D_DATA, + batch_size=1, + shuffle=False, + queue_length=50) + aggregator = WindowAsImageAggregator( + image_reader=reader, + + output_path=os.path.join('testing_data', 'aggregated_identity'), + ) + more_batch = True + out_shape = [] + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + try: + out = sess.run(sampler.pop_batch_op()) + out_shape = out['image'].shape[1:] + (1,) + except tf.errors.OutOfRangeError: + break + more_batch = aggregator.decode_batch( + {'window_image': out['image']}, out['image_location']) + output_filename = '{}_window_image_niftynet_generated.nii.gz'.format( + sampler.reader.get_subject_id(0)) + output_file = os.path.join('testing_data', + 'aggregated_identity', + output_filename) + out_shape = [out_shape[i] for i in NEW_ORDER_2D] + [1,] + self.assertAllClose( + nib.load(output_file).shape, out_shape[:2]) + sampler.close_all() + + +if __name__ == "__main__": + tf.test.main() diff --git a/tests/windows_aggregator_resize_v2_test.py b/tests/windows_aggregator_resize_v2_test.py index 16f25acc..2f4d3133 100755 --- a/tests/windows_aggregator_resize_v2_test.py +++ b/tests/windows_aggregator_resize_v2_test.py @@ -4,6 +4,8 @@ import os import nibabel as nib +import numpy as np +import pandas as pd import tensorflow as tf from niftynet.engine.sampler_resize_v2 import ResizeSampler @@ -14,98 +16,119 @@ DiscreteLabelNormalisationLayer from niftynet.layer.pad import PadLayer from niftynet.utilities.util_common import ParserNamespace +from tests.niftynet_testcase import NiftyNetTestCase MULTI_MOD_DATA = { - 'T1': ParserNamespace( + 'T1': + ParserNamespace( csv_file=os.path.join('testing_data', 'T1sampler.csv'), path_to_search='testing_data', filename_contains=('_o_T1_time', '23'), - filename_not_contains=('Parcellation',), + filename_not_contains=('Parcellation', ), interp_order=3, pixdim=(2.4, 5.0, 2.0), axcodes='LAS', spatial_window_size=(23, 32, 15), - loader=None - ), - 'FLAIR': ParserNamespace( + loader=None), + 'FLAIR': + ParserNamespace( csv_file=os.path.join('testing_data', 'FLAIRsampler.csv'), path_to_search='testing_data', filename_contains=('FLAIR_', '23'), - filename_not_contains=('Parcellation',), + filename_not_contains=('Parcellation', ), interp_order=3, pixdim=(2.4, 5.0, 2.0), axcodes='LAS', spatial_window_size=(23, 32, 15), - loader=None - ) + loader=None) } MULTI_MOD_TASK = ParserNamespace(image=('T1', 'FLAIR')) MOD_2D_DATA = { - 'ultrasound': ParserNamespace( + 'ultrasound': + ParserNamespace( csv_file=os.path.join('testing_data', 'T1sampler2d.csv'), path_to_search='testing_data', - filename_contains=('2d_',), - filename_not_contains=('Parcellation',), + filename_contains=('2d_', ), + filename_not_contains=('Parcellation', ), interp_order=3, pixdim=None, axcodes=None, spatial_window_size=(72, 83, 1), - loader=None - ), + loader=None), } -MOD_2D_TASK = ParserNamespace(image=('ultrasound',)) +MOD_2D_TASK = ParserNamespace(image=('ultrasound', )) MOD_LABEL_DATA = { - 'parcellation': ParserNamespace( + 'parcellation': + ParserNamespace( csv_file=os.path.join('testing_data', 'Parcelsampler2d.csv'), path_to_search='testing_data', - filename_contains=('23_NeuroMorph_Parcellation',), - filename_not_contains=('FLAIR',), + filename_contains=('23_NeuroMorph_Parcellation', ), + filename_not_contains=('FLAIR', ), interp_order=0, pixdim=None, axcodes=None, spatial_window_size=(150, 140, 100), - loader=None - ), + loader=None), } -MOD_LABEl_TASK = ParserNamespace(label=('parcellation',)) +MOD_LABEl_TASK = ParserNamespace(label=('parcellation', )) SINGLE_25D_DATA = { - 'T1': ParserNamespace( + 'T1': + ParserNamespace( csv_file=os.path.join('testing_data', 'T1sampler.csv'), path_to_search='testing_data', filename_contains=('_o_T1_time', '106'), - filename_not_contains=('Parcellation',), + filename_not_contains=('Parcellation', ), interp_order=3, pixdim=(3.0, 5.0, 5.0), axcodes='LAS', spatial_window_size=(40, 30, 1), - loader=None - ), + loader=None), } -SINGLE_25D_TASK = ParserNamespace(image=('T1',)) +SINGLE_25D_TASK = ParserNamespace(image=('T1', )) data_partitioner = ImageSetsPartitioner() -multi_mod_list = data_partitioner.initialise(MULTI_MOD_DATA).get_file_list() -mod_2d_list = data_partitioner.initialise(MOD_2D_DATA).get_file_list() -mod_label_list = data_partitioner.initialise(MOD_LABEL_DATA).get_file_list() -single_25d_list = data_partitioner.initialise(SINGLE_25D_DATA).get_file_list() +multi_mod_list = data_partitioner.initialise( + MULTI_MOD_DATA, + data_split_file='testing_data/resize_split.csv').get_file_list() +mod_2d_list = data_partitioner.initialise( + MOD_2D_DATA, + data_split_file='testing_data/resize_split.csv').get_file_list() +mod_label_list = data_partitioner.initialise( + MOD_LABEL_DATA, + data_split_file='testing_data/resize_split.csv').get_file_list() +single_25d_list = data_partitioner.initialise( + SINGLE_25D_DATA, + data_split_file='testing_data/resize_split.csv').get_file_list() def get_3d_reader(): + ''' + define the 3d reader + :return: 3d reader + ''' reader = ImageReader(['image']) reader.initialise(MULTI_MOD_DATA, MULTI_MOD_TASK, multi_mod_list) return reader def get_2d_reader(): + ''' + define the 2d reader + :return: 2d reader + ''' reader = ImageReader(['image']) reader.initialise(MOD_2D_DATA, MOD_2D_TASK, mod_2d_list) return reader def get_label_reader(): + ''' + define the label reader + :return: label reader + ''' reader = ImageReader(['label']) reader.initialise(MOD_LABEL_DATA, MOD_LABEl_TASK, mod_label_list) label_normaliser = DiscreteLabelNormalisationLayer( @@ -113,25 +136,30 @@ def get_label_reader(): modalities=vars(SINGLE_25D_TASK).get('label'), model_filename=os.path.join('testing_data', 'agg_test.txt')) reader.add_preprocessing_layers(label_normaliser) - pad_layer = PadLayer(image_name=('label',), border=(5, 6, 7)) + pad_layer = PadLayer(image_name=('label', ), border=(5, 6, 7)) reader.add_preprocessing_layers([pad_layer]) return reader def get_25d_reader(): + ''' + define the 2.5 d reader + :return: + ''' reader = ImageReader(['image']) reader.initialise(SINGLE_25D_DATA, SINGLE_25D_TASK, single_25d_list) return reader -class ResizeSamplesAggregatorTest(tf.test.TestCase): +class ResizeSamplesAggregatorTest(NiftyNetTestCase): def test_3d_init(self): reader = get_3d_reader() - sampler = ResizeSampler(reader=reader, - window_sizes=MULTI_MOD_DATA, - batch_size=1, - shuffle=False, - queue_length=50) + sampler = ResizeSampler( + reader=reader, + window_sizes=MULTI_MOD_DATA, + batch_size=1, + shuffle=False, + queue_length=50) aggregator = ResizeSamplesAggregator( image_reader=reader, name='image', @@ -139,7 +167,7 @@ def test_3d_init(self): interp_order=3) more_batch = True - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) while more_batch: try: @@ -147,23 +175,209 @@ def test_3d_init(self): except tf.errors.OutOfRangeError: break more_batch = aggregator.decode_batch( - out['image'], out['image_location']) - output_filename = '{}_niftynet_out.nii.gz'.format( + {'window_image': out['image']}, out['image_location']) + output_filename = 'window_image_{}_niftynet_out.nii.gz'.format( sampler.reader.get_subject_id(0)) - output_file = os.path.join('testing_data', - 'aggregated', + output_file = os.path.join('testing_data', 'aggregated', output_filename) + self.assertAllClose(nib.load(output_file).shape, (256, 168, 256, 1, 2)) + sampler.close_all() + + def test_3d_init_mo(self): + reader = get_3d_reader() + sampler = ResizeSampler( + reader=reader, + window_sizes=MULTI_MOD_DATA, + batch_size=1, + shuffle=False, + queue_length=50) + aggregator = ResizeSamplesAggregator( + image_reader=reader, + name='image', + output_path=os.path.join('testing_data', 'aggregated'), + interp_order=3) + more_batch = True + + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + try: + out = sess.run(sampler.pop_batch_op()) + except tf.errors.OutOfRangeError: + break + sum_val = np.sum(out['image']) + more_batch = aggregator.decode_batch( + { + 'window_image': out['image'], + 'csv_sum': sum_val + }, out['image_location']) + output_filename = 'window_image_{}_niftynet_out.nii.gz'.format( + sampler.reader.get_subject_id(0)) + sum_filename = os.path.join( + 'testing_data', 'aggregated', 'csv_sum_{}_niftynet_out.csv'.format( + sampler.reader.get_subject_id(0))) + output_file = os.path.join('testing_data', 'aggregated', + output_filename) + self.assertAllClose(nib.load(output_file).shape, (256, 168, 256, 1, 2)) + sum_pd = pd.read_csv(sum_filename) + self.assertAllClose(sum_pd.shape, [1, 2]) + sampler.close_all() + + def test_3d_init_mo_2im(self): + reader = get_3d_reader() + sampler = ResizeSampler( + reader=reader, + window_sizes=MULTI_MOD_DATA, + batch_size=1, + shuffle=False, + queue_length=50) + aggregator = ResizeSamplesAggregator( + image_reader=reader, + name='image', + output_path=os.path.join('testing_data', 'aggregated'), + interp_order=3) + more_batch = True + + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + try: + out = sess.run(sampler.pop_batch_op()) + except tf.errors.OutOfRangeError: + break + more_batch = aggregator.decode_batch( + { + 'window_image': out['image'], + 'window_im2': out['image'] + }, out['image_location']) + output_filename = 'window_image_{}_niftynet_out.nii.gz'.format( + sampler.reader.get_subject_id(0)) + outim2_filename = os.path.join( + 'testing_data', 'aggregated', + 'window_im2_{}_niftynet_out.nii.gz'.format( + sampler.reader.get_subject_id(0))) + output_file = os.path.join('testing_data', 'aggregated', + output_filename) + self.assertAllClose(nib.load(output_file).shape, (256, 168, 256, 1, 2)) self.assertAllClose( - nib.load(output_file).shape, (256, 168, 256, 1, 2)) + nib.load(outim2_filename).shape, (256, 168, 256, 1, 2)) + sampler.close_all() + + def test_3d_init_mo_3out(self): + reader = get_3d_reader() + sampler = ResizeSampler( + reader=reader, + window_sizes=MULTI_MOD_DATA, + batch_size=1, + shuffle=False, + queue_length=50) + aggregator = ResizeSamplesAggregator( + image_reader=reader, + name='image', + output_path=os.path.join('testing_data', 'aggregated'), + interp_order=3) + more_batch = True + + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + try: + out = sess.run(sampler.pop_batch_op()) + except tf.errors.OutOfRangeError: + break + sum_val = np.sum(out['image']) + stats_val = [ + np.sum(out['image']), + np.min(out['image']), + np.max(out['image']) + ] + more_batch = aggregator.decode_batch( + { + 'window_image': out['image'], + 'csv_sum': sum_val, + 'csv_stats': stats_val + }, out['image_location']) + output_filename = 'window_image_{}_niftynet_out.nii.gz'.format( + sampler.reader.get_subject_id(0)) + sum_filename = os.path.join( + 'testing_data', 'aggregated', 'csv_sum_{}_niftynet_out.csv'.format( + sampler.reader.get_subject_id(0))) + stats_filename = os.path.join( + 'testing_data', 'aggregated', + 'csv_stats_{}_niftynet_out.csv'.format( + sampler.reader.get_subject_id(0))) + output_file = os.path.join('testing_data', 'aggregated', + output_filename) + self.assertAllClose(nib.load(output_file).shape, (256, 168, 256, 1, 2)) + sum_pd = pd.read_csv(sum_filename) + self.assertAllClose(sum_pd.shape, [1, 2]) + stats_pd = pd.read_csv(stats_filename) + self.assertAllClose(stats_pd.shape, [1, 4]) + sampler.close_all() + + def test_init_3d_mo_bidimcsv(self): + reader = get_3d_reader() + sampler = ResizeSampler( + reader=reader, + window_sizes=MULTI_MOD_DATA, + batch_size=1, + shuffle=False, + queue_length=50) + aggregator = ResizeSamplesAggregator( + image_reader=reader, + name='image', + output_path=os.path.join('testing_data', 'aggregated'), + interp_order=3) + more_batch = True + + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + try: + out = sess.run(sampler.pop_batch_op()) + except tf.errors.OutOfRangeError: + break + min_val = np.sum((np.asarray(out['image']).flatten())) + stats_val = [ + np.min(out['image']), + np.max(out['image']), + np.sum(out['image']) + ] + stats_val = np.expand_dims(stats_val, 0) + stats_val = np.concatenate([stats_val, stats_val], axis=0) + more_batch = aggregator.decode_batch( + { + 'window_image': out['image'], + 'csv_sum': min_val, + 'csv_stats_2d': stats_val + }, out['image_location']) + output_filename = 'window_image_{}_niftynet_out.nii.gz'.format( + sampler.reader.get_subject_id(0)) + sum_filename = os.path.join( + 'testing_data', 'aggregated', 'csv_sum_{}_niftynet_out.csv'.format( + sampler.reader.get_subject_id(0))) + stats_filename = os.path.join( + 'testing_data', 'aggregated', + 'csv_stats_2d_{}_niftynet_out.csv'.format( + sampler.reader.get_subject_id(0))) + output_file = os.path.join('testing_data', 'aggregated', + output_filename) + + self.assertAllClose(nib.load(output_file).shape, (256, 168, 256, 1, 2)) + min_pd = pd.read_csv(sum_filename) + self.assertAllClose(min_pd.shape, [1, 2]) + stats_pd = pd.read_csv(stats_filename) + self.assertAllClose(stats_pd.shape, [1, 7]) sampler.close_all() def test_2d_init(self): reader = get_2d_reader() - sampler = ResizeSampler(reader=reader, - window_sizes=MOD_2D_DATA, - batch_size=1, - shuffle=False, - queue_length=50) + sampler = ResizeSampler( + reader=reader, + window_sizes=MOD_2D_DATA, + batch_size=1, + shuffle=False, + queue_length=50) aggregator = ResizeSamplesAggregator( image_reader=reader, name='image', @@ -171,7 +385,7 @@ def test_2d_init(self): interp_order=3) more_batch = True - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) while more_batch: try: @@ -179,23 +393,172 @@ def test_2d_init(self): except tf.errors.OutOfRangeError: break more_batch = aggregator.decode_batch( - out['image'], out['image_location']) - output_filename = '{}_niftynet_out.nii.gz'.format( + {'window_image': out['image']}, out['image_location']) + output_filename = 'window_image_{}_niftynet_out.nii.gz'.format( sampler.reader.get_subject_id(0)) - output_file = os.path.join('testing_data', - 'aggregated', + output_file = os.path.join('testing_data', 'aggregated', output_filename) - self.assertAllClose( - nib.load(output_file).shape, [128, 128, 1, 1, 1]) + self.assertAllClose(nib.load(output_file).shape, [128, 128]) + sampler.close_all() + + def test_init_2d_mo(self): + reader = get_2d_reader() + sampler = ResizeSampler( + reader=reader, + window_sizes=MOD_2D_DATA, + batch_size=1, + shuffle=False, + queue_length=50) + aggregator = ResizeSamplesAggregator( + image_reader=reader, + name='image', + output_path=os.path.join('testing_data', 'aggregated'), + interp_order=3) + more_batch = True + + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + try: + out = sess.run(sampler.pop_batch_op()) + except tf.errors.OutOfRangeError: + break + min_val = np.sum((np.asarray(out['image']).flatten())) + stats_val = [np.min(out), np.max(out), np.sum(out)] + more_batch = aggregator.decode_batch( + { + 'window_image': out['image'], + 'csv_sum': min_val + }, out['image_location']) + output_filename = 'window_image_{}_niftynet_out.nii.gz'.format( + sampler.reader.get_subject_id(0)) + sum_filename = os.path.join( + 'testing_data', 'aggregated', 'csv_sum_{}_niftynet_out.csv'.format( + sampler.reader.get_subject_id(0))) + output_file = os.path.join('testing_data', 'aggregated', + output_filename) + + self.assertAllClose(nib.load(output_file).shape, (128, 128)) + min_pd = pd.read_csv(sum_filename) + self.assertAllClose(min_pd.shape, [1, 2]) + sampler.close_all() + + def test_init_2d_mo_3out(self): + reader = get_2d_reader() + sampler = ResizeSampler( + reader=reader, + window_sizes=MOD_2D_DATA, + batch_size=1, + shuffle=False, + queue_length=50) + aggregator = ResizeSamplesAggregator( + image_reader=reader, + name='image', + output_path=os.path.join('testing_data', 'aggregated'), + interp_order=3) + more_batch = True + + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + try: + out = sess.run(sampler.pop_batch_op()) + except tf.errors.OutOfRangeError: + break + min_val = np.sum((np.asarray(out['image']).flatten())) + stats_val = [ + np.min(out['image']), + np.max(out['image']), + np.sum(out['image']) + ] + more_batch = aggregator.decode_batch( + { + 'window_image': out['image'], + 'csv_sum': min_val, + 'csv_stats': stats_val + }, out['image_location']) + output_filename = 'window_image_{}_niftynet_out.nii.gz'.format( + sampler.reader.get_subject_id(0)) + sum_filename = os.path.join( + 'testing_data', 'aggregated', 'csv_sum_{}_niftynet_out.csv'.format( + sampler.reader.get_subject_id(0))) + stats_filename = os.path.join( + 'testing_data', 'aggregated', + 'csv_stats_{}_niftynet_out.csv'.format( + sampler.reader.get_subject_id(0))) + output_file = os.path.join('testing_data', 'aggregated', + output_filename) + + self.assertAllClose(nib.load(output_file).shape, (128, 128)) + min_pd = pd.read_csv(sum_filename) + self.assertAllClose(min_pd.shape, [1, 2]) + stats_pd = pd.read_csv(stats_filename) + self.assertAllClose(stats_pd.shape, [1, 4]) + sampler.close_all() + + def test_init_2d_mo_bidimcsv(self): + reader = get_2d_reader() + sampler = ResizeSampler( + reader=reader, + window_sizes=MOD_2D_DATA, + batch_size=1, + shuffle=False, + queue_length=50) + aggregator = ResizeSamplesAggregator( + image_reader=reader, + name='image', + output_path=os.path.join('testing_data', 'aggregated'), + interp_order=3) + more_batch = True + + with self.cached_session() as sess: + sampler.set_num_threads(2) + while more_batch: + try: + out = sess.run(sampler.pop_batch_op()) + except tf.errors.OutOfRangeError: + break + min_val = np.sum((np.asarray(out['image']).flatten())) + stats_val = [ + np.min(out['image']), + np.max(out['image']), + np.sum(out['image']) + ] + stats_val = np.expand_dims(stats_val, 0) + stats_val = np.concatenate([stats_val, stats_val], axis=0) + more_batch = aggregator.decode_batch( + { + 'window_image': out['image'], + 'csv_sum': min_val, + 'csv_stats_2d': stats_val + }, out['image_location']) + output_filename = 'window_image_{}_niftynet_out.nii.gz'.format( + sampler.reader.get_subject_id(0)) + sum_filename = os.path.join( + 'testing_data', 'aggregated', 'csv_sum_{}_niftynet_out.csv'.format( + sampler.reader.get_subject_id(0))) + stats_filename = os.path.join( + 'testing_data', 'aggregated', + 'csv_stats_2d_{}_niftynet_out.csv'.format( + sampler.reader.get_subject_id(0))) + output_file = os.path.join('testing_data', 'aggregated', + output_filename) + + self.assertAllClose(nib.load(output_file).shape, (128, 128)) + min_pd = pd.read_csv(sum_filename) + self.assertAllClose(min_pd.shape, [1, 2]) + stats_pd = pd.read_csv(stats_filename) + self.assertAllClose(stats_pd.shape, [1, 7]) sampler.close_all() def test_25d_init(self): reader = get_25d_reader() - sampler = ResizeSampler(reader=reader, - window_sizes=SINGLE_25D_DATA, - batch_size=1, - shuffle=False, - queue_length=50) + sampler = ResizeSampler( + reader=reader, + window_sizes=SINGLE_25D_DATA, + batch_size=1, + shuffle=False, + queue_length=50) aggregator = ResizeSamplesAggregator( image_reader=reader, name='image', @@ -203,7 +566,7 @@ def test_25d_init(self): interp_order=3) more_batch = True - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) while more_batch: try: @@ -211,23 +574,22 @@ def test_25d_init(self): except tf.errors.OutOfRangeError: break more_batch = aggregator.decode_batch( - out['image'], out['image_location']) - output_filename = '{}_niftynet_out.nii.gz'.format( + {'window_image': out['image']}, out['image_location']) + output_filename = 'window_image_{}_niftynet_out.nii.gz'.format( sampler.reader.get_subject_id(0)) - output_file = os.path.join('testing_data', - 'aggregated', + output_file = os.path.join('testing_data', 'aggregated', output_filename) - self.assertAllClose( - nib.load(output_file).shape, [256, 168, 256, 1, 1]) + self.assertAllClose(nib.load(output_file).shape, [256, 168, 256]) sampler.close_all() def test_inverse_mapping(self): reader = get_label_reader() - sampler = ResizeSampler(reader=reader, - window_sizes=MOD_LABEL_DATA, - batch_size=1, - shuffle=False, - queue_length=50) + sampler = ResizeSampler( + reader=reader, + window_sizes=MOD_LABEL_DATA, + batch_size=1, + shuffle=False, + queue_length=50) aggregator = ResizeSamplesAggregator( image_reader=reader, name='label', @@ -235,7 +597,7 @@ def test_inverse_mapping(self): interp_order=0) more_batch = True - with self.test_session() as sess: + with self.cached_session() as sess: sampler.set_num_threads(2) while more_batch: try: @@ -243,13 +605,12 @@ def test_inverse_mapping(self): except tf.errors.OutOfRangeError: break more_batch = aggregator.decode_batch( - out['label'], out['label_location']) - output_filename = '{}_niftynet_out.nii.gz'.format( + {'window_label': out['label']}, out['label_location']) + output_filename = 'window_label_{}_niftynet_out.nii.gz'.format( sampler.reader.get_subject_id(0)) - output_file = os.path.join( - 'testing_data', 'aggregated', output_filename) - self.assertAllClose( - nib.load(output_file).shape, [256, 168, 256, 1, 1]) + output_file = os.path.join('testing_data', 'aggregated', + output_filename) + self.assertAllClose(nib.load(output_file).shape, [256, 168, 256]) sampler.close_all() # output_data = nib.load(output_file).get_data()[..., 0, 0] # expected_data = nib.load( diff --git a/train_modality_classification.ini b/train_modality_classification.ini new file mode 100644 index 00000000..ff6575e2 --- /dev/null +++ b/train_modality_classification.ini @@ -0,0 +1,67 @@ +[image] +path_to_search = /home/tom/data/BRATS_18_SPLITS/train +filename_contains = +filename_not_contains =seg +spatial_window_size = (32, 32, 1) +axcodes=(A, R, S) +interp_order = 1 + +[label] +csv_data_file = ./modality_labels.csv +to_ohe = False + + +############################## system configuration sections +[SYSTEM] +cuda_devices = "" +num_threads = 2 +num_gpus = 1 +model_dir = ./models/model_highres3dnet + +[NETWORK] +name = resnet +activation_function = relu +batch_size = 1 +decay = 0 +reg_type = L2 + +# volume level preprocessing +volume_padding_size = 21 +# histogram normalisation +histogram_ref_file = ./example_volumes/monomodal_parcellation/standardisation_models.txt +norm_type = percentile +cutoff = (0.01, 0.99) +normalisation = True +whitening = True +normalise_foreground_only=True +foreground_type = otsu_plus +multimod_foreground_type = and + +queue_length = 1 +window_sampling = resize + +[TRAINING] +sample_per_volume = 32 +rotation_angle = (-10.0, 10.0) +scaling_percentage = (-10.0, 10.0) +lr = 0.0001 +loss_type = CrossEntropy +starting_iter = 0 +save_every_n = 5 +max_iter = 6 +max_checkpoints = 20 + +[INFERENCE] +border = (5, 5, 5) +#inference_iter = 10 +save_seg_dir = ./output/highres3dnet +output_interp_order = 0 +spatial_window_size = (0, 0, 3) + +############################ custom configuration sections +[CLASSIFICATION] +image = image +label = label +output_prob = False +num_classes = 4 +label_normalisation = False