diff --git a/.gitignore b/.gitignore index 9949bc981c..c30f242fd2 100644 --- a/.gitignore +++ b/.gitignore @@ -103,3 +103,4 @@ venv.bak/ # mypy .mypy_cache/ examples/scd_lvsegs.npz +.idea/ diff --git a/monai/__init__.py b/monai/__init__.py index 9dc1300c7b..e86508dd19 100644 --- a/monai/__init__.py +++ b/monai/__init__.py @@ -12,7 +12,7 @@ import os import sys -from .utils.moduleutils import load_submodules, loadSubmodules +from .utils.moduleutils import load_submodules __copyright__ = "(c) 2020 MONAI Consortium" __version__tuple__ = (0, 0, 1) diff --git a/runtests.sh b/runtests.sh new file mode 100755 index 0000000000..102e63c68c --- /dev/null +++ b/runtests.sh @@ -0,0 +1,102 @@ +#! /bin/bash +# Test script for running all tests + + +homedir="$( cd -P "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd $homedir + +#export PYTHONPATH="$homedir:$PYTHONPATH" + +# configuration values +doCoverage=false +doQuickTests=false +doNetTests=false +doDryRun=false +doZooTests=false + +# testing command to run +cmd="python" +cmdprefix="" + + +# parse arguments +for i in "$@" +do + case $i in + --coverage) + doCoverage=true + ;; + --quick) + doQuickTests=true + doCoverage=true + export QUICKTEST=True + ;; + --net) + doNetTests=true + ;; + --dryrun) + doDryRun=true + ;; + --zoo) + doZooTests=true + ;; + *) + echo "runtests.sh [--coverage] [--quick] [--net] [--dryrun] [--zoo]" + exit 1 + ;; + esac +done + + +# commands are echoed instead of run in this case +if [ "$doDryRun" = 'true' ] +then + echo "Dry run commands:" + cmdprefix="dryrun " + + # create a dry run function which prints the command prepended with spaces for neatness + function dryrun { echo " " $* ; } +fi + + +# set command and clear previous coverage data +if [ "$doCoverage" = 'true' ] +then + cmd="coverage run -a --source ." + ${cmdprefix} coverage erase +fi + + +# # download test data if needed +# if [ ! -d testing_data ] && [ "$doDryRun" != 'true' ] +# then +# fi + + +# unit tests +${cmdprefix}${cmd} -m unittest + + +# network training/inference/eval tests +if [ "$doNetTests" = 'true' ] +then + for i in examples/*.py + do + echo $i + ${cmdprefix}${cmd} $i + done +fi + + +# # run model zoo tests +# if [ "$doZooTests" = 'true' ] +# then +# fi + + +# report on coverage +if [ "$doCoverage" = 'true' ] +then + ${cmdprefix}coverage report --omit='*/test/*' --skip-covered -m +fi + diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000..d0044e3563 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,10 @@ +# Copyright 2020 MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/testconvolutions.py b/tests/testconvolutions.py new file mode 100644 index 0000000000..14b189ccdd --- /dev/null +++ b/tests/testconvolutions.py @@ -0,0 +1,85 @@ +# Copyright 2020 MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from .utils import ImageTestCase + +from monai.networks.layers.convolutions import Convolution, ResidualUnit + + +class TestConvolution2D(ImageTestCase): + def test_conv1(self): + conv = Convolution(2, self.input_channels, self.output_channels) + out = conv(self.imt) + expected_shape = (1, self.output_channels, self.im_shape[0], self.im_shape[1]) + self.assertEqual(out.shape, expected_shape) + + def test_conv_only1(self): + conv = Convolution(2, self.input_channels, self.output_channels, conv_only=True) + out = conv(self.imt) + expected_shape = (1, self.output_channels, self.im_shape[0], self.im_shape[1]) + self.assertEqual(out.shape, expected_shape) + + def test_stride1(self): + conv = Convolution(2, self.input_channels, self.output_channels, strides=2) + out = conv(self.imt) + expected_shape = (1, self.output_channels, self.im_shape[0] // 2, self.im_shape[1] // 2) + self.assertEqual(out.shape, expected_shape) + + def test_dilation1(self): + conv = Convolution(2, self.input_channels, self.output_channels, dilation=3) + out = conv(self.imt) + expected_shape = (1, self.output_channels, self.im_shape[0], self.im_shape[1]) + self.assertEqual(out.shape, expected_shape) + + def test_dropout1(self): + conv = Convolution(2, self.input_channels, self.output_channels, dropout=0.15) + out = conv(self.imt) + expected_shape = (1, self.output_channels, self.im_shape[0], self.im_shape[1]) + self.assertEqual(out.shape, expected_shape) + + def test_transpose1(self): + conv = Convolution(2, self.input_channels, self.output_channels, is_transposed=True) + out = conv(self.imt) + expected_shape = (1, self.output_channels, self.im_shape[0], self.im_shape[1]) + self.assertEqual(out.shape, expected_shape) + + def test_transpose2(self): + conv = Convolution(2, self.input_channels, self.output_channels, strides=2, is_transposed=True) + out = conv(self.imt) + expected_shape = (1, self.output_channels, self.im_shape[0] * 2, self.im_shape[1] * 2) + self.assertEqual(out.shape, expected_shape) + + +class TestResidualUnit2D(ImageTestCase): + def test_conv_only1(self): + conv = ResidualUnit(2, 1, self.output_channels) + out = conv(self.imt) + expected_shape = (1, self.output_channels, self.im_shape[0], self.im_shape[1]) + self.assertEqual(out.shape, expected_shape) + + def test_stride1(self): + conv = ResidualUnit(2, 1, self.output_channels, strides=2) + out = conv(self.imt) + expected_shape = (1, self.output_channels, self.im_shape[0] // 2, self.im_shape[1] // 2) + self.assertEqual(out.shape, expected_shape) + + def test_dilation1(self): + conv = ResidualUnit(2, 1, self.output_channels, dilation=3) + out = conv(self.imt) + expected_shape = (1, self.output_channels, self.im_shape[0], self.im_shape[1]) + self.assertEqual(out.shape, expected_shape) + + def test_dropout1(self): + conv = ResidualUnit(2, 1, self.output_channels, dropout=0.15) + out = conv(self.imt) + expected_shape = (1, self.output_channels, self.im_shape[0], self.im_shape[1]) + self.assertEqual(out.shape, expected_shape) diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000000..f780220b77 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,70 @@ +# Copyright 2020 MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import os +import unittest +import torch +import numpy as np + +from monai.utils.arrayutils import rescale_array + +quick_test_var = "QUICKTEST" + + +def skip_if_quick(obj): + is_quick = os.environ.get(quick_test_var, "").lower() == "true" + + return unittest.skipIf(is_quick, "Skipping slow tests")(obj) + + +def create_test_image(width, height, num_objs=12, rad_max=30, noise_max=0.0, num_seg_classes=5): + """ + Return a noisy 2D image with `numObj' circles and a 2D mask image. The maximum radius of the circles is given as + `radMax'. The mask will have `numSegClasses' number of classes for segmentations labeled sequentially from 1, plus a + background class represented as 0. If `noiseMax' is greater than 0 then noise will be added to the image taken from + the uniform distribution on range [0,noiseMax). + """ + image = np.zeros((width, height)) + + for i in range(num_objs): + x = np.random.randint(rad_max, width - rad_max) + y = np.random.randint(rad_max, height - rad_max) + rad = np.random.randint(5, rad_max) + spy, spx = np.ogrid[-x : width - x, -y : height - y] + circle = (spx * spx + spy * spy) <= rad * rad + + if num_seg_classes > 1: + image[circle] = np.ceil(np.random.random() * num_seg_classes) + else: + image[circle] = np.random.random() * 0.5 + 0.5 + + labels = np.ceil(image).astype(np.int32) + + norm = np.random.uniform(0, num_seg_classes * noise_max, size=image.shape) + noisyimage = rescale_array(np.maximum(image, norm)) + + return noisyimage, labels + + +class ImageTestCase(unittest.TestCase): + im_shape = (128, 128) + input_channels = 1 + output_channels = 4 + num_classes = 3 + + def setUp(self): + im, msk = create_test_image(self.im_shape[0], self.im_shape[1], 4, 20, 0, self.num_classes) + + self.imt = torch.tensor(im[None, None]) + + self.seg1 = torch.tensor((msk[None, None] > 0).astype(np.float32)) + self.segn = torch.tensor(msk[None, None])