diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml index f76fe01699..734a84ff2f 100644 --- a/.github/workflows/cron.yml +++ b/.github/workflows/cron.yml @@ -15,7 +15,7 @@ jobs: runs-on: [self-hosted, linux, x64, common] strategy: matrix: - pytorch-version: [1.6.0, 1.7.1, 1.8.1, 1.9.1, latest] + pytorch-version: [1.7.1, 1.8.1, 1.9.1, 1.10.2, latest] steps: - uses: actions/checkout@v2 - name: Install the dependencies @@ -24,15 +24,15 @@ jobs: python -m pip install --upgrade pip wheel python -m pip uninstall -y torch torchvision if [ ${{ matrix.pytorch-version }} == "latest" ]; then - python -m pip install torch torchvision - elif [ ${{ matrix.pytorch-version }} == "1.6.0" ]; then - python -m pip install torch==1.6.0 torchvision==0.7.0 + python -m pip install torch torchvision --extra-index-url https://download.pytorch.org/whl/cu113 elif [ ${{ matrix.pytorch-version }} == "1.7.1" ]; then - python -m pip install torch==1.7.1 torchvision==0.8.2 + python -m pip install torch==1.7.1 torchvision==0.8.2 --extra-index-url https://download.pytorch.org/whl/cu113 elif [ ${{ matrix.pytorch-version }} == "1.8.1" ]; then - python -m pip install torch==1.8.1 torchvision==0.9.1 + python -m pip install torch==1.8.1 torchvision==0.9.1 --extra-index-url https://download.pytorch.org/whl/cu113 elif [ ${{ matrix.pytorch-version }} == "1.9.1" ]; then - python -m pip install torch==1.9.1 torchvision==0.10.1 + python -m pip install torch==1.9.1 torchvision==0.10.1 --extra-index-url https://download.pytorch.org/whl/cu113 + elif [ ${{ matrix.pytorch-version }} == "1.10.2" ]; then + python -m pip install torch==1.10.2 torchvision==0.11.3 --extra-index-url https://download.pytorch.org/whl/cu113 fi python -m pip install -r requirements-dev.txt python -m pip list diff --git a/.github/workflows/pythonapp-gpu.yml b/.github/workflows/pythonapp-gpu.yml index d19bbe437f..90b31e99ab 100644 --- a/.github/workflows/pythonapp-gpu.yml +++ b/.github/workflows/pythonapp-gpu.yml @@ -50,7 +50,7 @@ jobs: pytorch: "-h" base: "nvcr.io/nvidia/pytorch:22.03-py3" - environment: PT110+CUDA102 - pytorch: "torch==1.10.1 torchvision==0.11.2" + pytorch: "torch==1.10.2 torchvision==0.11.3" base: "nvcr.io/nvidia/cuda:10.2-devel-ubuntu18.04" - environment: PT111+CUDA102 pytorch: "torch==1.11.0 torchvision==0.12.0" diff --git a/.github/workflows/pythonapp-min.yml b/.github/workflows/pythonapp-min.yml index c3294c2b2a..e50f74d816 100644 --- a/.github/workflows/pythonapp-min.yml +++ b/.github/workflows/pythonapp-min.yml @@ -119,7 +119,7 @@ jobs: strategy: fail-fast: false matrix: - pytorch-version: [1.6.0, 1.7.1, 1.8.1, 1.9.1, 1.10.1, latest] + pytorch-version: [1.7.1, 1.8.1, 1.9.1, 1.10.2, latest] timeout-minutes: 40 steps: - uses: actions/checkout@v2 @@ -148,16 +148,14 @@ jobs: # min. requirements if [ ${{ matrix.pytorch-version }} == "latest" ]; then python -m pip install torch - elif [ ${{ matrix.pytorch-version }} == "1.6.0" ]; then - python -m pip install torch==1.6.0 elif [ ${{ matrix.pytorch-version }} == "1.7.1" ]; then python -m pip install torch==1.7.1 elif [ ${{ matrix.pytorch-version }} == "1.8.1" ]; then python -m pip install torch==1.8.1 elif [ ${{ matrix.pytorch-version }} == "1.9.1" ]; then python -m pip install torch==1.9.1 - elif [ ${{ matrix.pytorch-version }} == "1.10.1" ]; then - python -m pip install torch==1.10.1 + elif [ ${{ matrix.pytorch-version }} == "1.10.2" ]; then + python -m pip install torch==1.10.2 fi python -m pip install -r requirements-min.txt python -m pip list diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index cf251c2293..38c96b3b0d 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -137,7 +137,7 @@ jobs: # install the latest pytorch for testing # however, "pip install monai*.tar.gz" will build cpp/cuda with an isolated # fresh torch installation according to pyproject.toml - python -m pip install torch>=1.6 torchvision + python -m pip install torch>=1.7 torchvision - name: Check packages run: | pip uninstall monai diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3fc762db46..9e8fa75854 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: - id: detect-private-key - repo: https://github.com/asottile/pyupgrade - rev: v2.31.0 + rev: v2.31.1 hooks: - id: pyupgrade args: [--py37-plus] diff --git a/CITATION.cff b/CITATION.cff index 8cbf686ce6..dcce4af377 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -6,11 +6,15 @@ title: "MONAI: Medical Open Network for AI" abstract: "AI Toolkit for Healthcare Imaging" authors: - name: "MONAI Consortium" -date-released: 2020-03-28 -version: "0.6.0" -doi: "10.5281/zenodo.4323058" +date-released: 2022-02-16 +version: "0.8.1" +identifiers: + - description: "This DOI represents all versions of MONAI, and will always resolve to the latest one." + type: doi + value: "10.5281/zenodo.4323058" license: "Apache-2.0" repository-code: "https://github.com/Project-MONAI/MONAI" -cff-version: "1.1.0" +url: "https://monai.io" +cff-version: "1.2.0" message: "If you use this software, please cite it using these metadata." ... diff --git a/docs/source/data.rst b/docs/source/data.rst index 2bdf401c7f..0910001783 100644 --- a/docs/source/data.rst +++ b/docs/source/data.rst @@ -107,16 +107,23 @@ Patch-based dataset .. autoclass:: GridPatchDataset :members: -`PatchIter` -~~~~~~~~~~~ -.. autoclass:: PatchIter - :members: - `PatchDataset` ~~~~~~~~~~~~~~ .. autoclass:: PatchDataset :members: +`PatchIter` +""""""""""" +.. autoclass:: PatchIter + :members: + :special-members: __call__ + +`PatchIterd` +"""""""""""" +.. autoclass:: PatchIterd + :members: + :special-members: __call__ + Image reader ------------ @@ -264,3 +271,14 @@ ThreadDataLoader TestTimeAugmentation ~~~~~~~~~~~~~~~~~~~~ .. autoclass:: monai.data.TestTimeAugmentation + + +Meta Object +----------- +.. automodule:: monai.data.meta_obj + :members: + +MetaTensor +---------- +.. autoclass:: monai.data.MetaTensor + :members: diff --git a/docs/source/mb_specification.rst b/docs/source/mb_specification.rst index 5bdfa148e2..1d286052a5 100644 --- a/docs/source/mb_specification.rst +++ b/docs/source/mb_specification.rst @@ -61,8 +61,9 @@ This file contains the metadata information relating to the model, including wha Tensor format specifiers are used to define input and output tensors and their meanings, and must be a dictionary containing at least these keys: -* **type**: what sort of data the tensor represents: "image", "label", etc. -* **format**: what format of information is stored: "magnitude", "hounsfield", "kspace", "segmentation", "multiclass", etc. +* **type**: what sort of data the tensor represents: "image" for any spatial regular data whether an actual image or just data with that sort of shape, "series" for (time-) sequences of values such as signals, "tuples" for a series of items defined by a known number of values such as N-sized points in ND space, "probabilities" for a set of probabilities such as classifier output, this useful for interpreting what the dimensions and shape of the data represent and allow users to guess how to plot the data +* **format**: what format of information is stored, see below for list of known formats +* **modality**: describes the modality, protocol type, sort of capturing technology, or other property of the data not described by either it's type or format, known modalities are "MR", "CT", "US", "EKG", but can include any custom types or protocol types (eg. "T1"), default value is "n/a" * **num_channels**: number of channels the tensor has, assumed channel dimension first * **spatial_shape**: shape of the spatial dimensions of the form "[H]", "[H, W]", or "[H, W, D]", see below for possible values of H, W, and D * **dtype**: data type of tensor, eg. "float32", "int32" @@ -78,6 +79,22 @@ Optional keys: * **data_type**: type of source data used for training/validation. * **references**: list of published referenced relating to the model. +The format for tensors used as inputs and outputs can be used to specify semantic meaning of these values, and later is used by software handling bundles to determine how to process and interpret this data. There are various types of image data that MONAI is uses, and other data types such as point clouds, dictionary sequences, time signals, and others. The following list is provided as a set of supported definitions of what a tensor "format" is but is not exhaustive and users can provide their own which would be left up to the model users to interpret: + +* **magnitude**: ND field of continuous magnitude values with one or more channels, eg. MR T1 image having 1 channel or natural RGB image with 3 channels +* **hounsfield**: ND field of semi-categorical values given in Hounsfield, eg. CT image +* **kspace**: 2D/3D fourier transform image associated with MR imaging +* **raw**: ND field of values considered unprocessed from an image acquisition device, eg. directly from a MR scanner without reconstruction or other processing +* **labels**: ND categorical image with N one-hot channels for N-class segmentation/labels, the "channel_def" states in plain language what the interpretation of each channel is, for each pixel/voxel the predicted label is the index of the largest channel value +* **classes**: ND categorical image with N channels for N-class classes, the "channel_def" states in plain language what the interpretation of each channel is, this permits multi-class labeling as the channels need not be one-hot encoded +* **segmentation**: ND categorical image with one channel assigning each pixel/voxel to a label described in "channel_def" +* **points**: list of points/nodes/coordinates/vertices/vectors in ND space, so having a shape of [I, N] for I points with N dimensions +* **normals**: list of vectors (possible of unit length) in ND space, so having a shape of [I, N] for I vectors with N dimensions +* **indices**: list of indices into a vertices array and/or other array representing a set of shapes, so having a shape of [I, N] for I shapes defined by N values +* **sequence**: time-related sequence of values having one or more channels, such as a signal or dictionary lookup sentence, so having a shape of [C, N] for C channels of data at N time points. +* **latent**: ND tensor of data from the latent space from some layer of a network +* **gradient**: ND tensor of gradients from some layer of a network + Spatial shape definition can be complex for models accepting inputs of varying shapes, especially if there are specific conditions on what those shapes can be. Shapes are specified as lists of either positive integers for fixed sizes or strings containing expressions defining the condition a size depends on. This can be "*" to mean any size, or use an expression with Python mathematical operators and one character variables to represent dependence on an unknown quantity. For example, "2**n" represents a size which must be a power of 2, "2**n*m" must be a multiple of a power of 2. Variables are shared between dimension expressions, so a spatial shape of `["2**n", "2**n"]` states that the dimensions must be the same powers of 2 given by `n`. A JSON schema for this file can be found at https://github.com/Project-MONAI/MONAI/blob/3049e280f2424962bb2a69261389fcc0b98e0036/monai/apps/mmars/schema/metadata.json @@ -118,6 +135,7 @@ An example JSON metadata file: "image": { "type": "image", "format": "magnitude", + "modality": "MR", "num_channels": 1, "spatial_shape": [160, 160, 160], "dtype": "float32", @@ -129,11 +147,11 @@ An example JSON metadata file: "outputs":{ "pred": { "type": "image", - "format": "segmentation", + "format": "labels", "num_channels": 2, "spatial_shape": [160, 160, 160], "dtype": "float32", - "value_range": [0, 1], + "value_range": [], "is_patch_data": false, "channel_def": {0: "background", 1: "spleen"} } diff --git a/docs/source/metrics.rst b/docs/source/metrics.rst index 332571d345..1eb8935141 100644 --- a/docs/source/metrics.rst +++ b/docs/source/metrics.rst @@ -69,6 +69,13 @@ Metrics .. autoclass:: SurfaceDistanceMetric :members: +`Surface dice` +-------------- +.. autofunction:: compute_surface_dice + +.. autoclass:: SurfaceDiceMetric + :members: + `Mean squared error` -------------------- .. autoclass:: MSEMetric diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst index 8fc832a253..676e0274fe 100644 --- a/docs/source/transforms.rst +++ b/docs/source/transforms.rst @@ -803,6 +803,12 @@ Utility :members: :special-members: __call__ +`SplitDim` +"""""""""" +.. autoclass:: SplitDim + :members: + :special-members: __call__ + `SplitChannel` """""""""""""" .. autoclass:: SplitChannel @@ -1638,6 +1644,12 @@ Utility (Dict) :members: :special-members: __call__ +`SplitDimd` +""""""""""" +.. autoclass:: SplitDimd + :members: + :special-members: __call__ + `SplitChanneld` """"""""""""""" .. autoclass:: SplitChanneld @@ -1830,6 +1842,21 @@ Utility (Dict) :members: :special-members: __call__ +MetaTensor +^^^^^^^^^^ + +`ToMetaTensord` +""""""""""""""" +.. autoclass:: ToMetaTensord + :members: + :special-members: __call__ + +`FromMetaTensord` +""""""""""""""""" +.. autoclass:: FromMetaTensord + :members: + :special-members: __call__ + Transform Adaptors ------------------ .. automodule:: monai.transforms.adaptors diff --git a/monai/_extensions/gmm/gmm.cpp b/monai/_extensions/gmm/gmm.cpp index 686fddb721..4087095340 100644 --- a/monai/_extensions/gmm/gmm.cpp +++ b/monai/_extensions/gmm/gmm.cpp @@ -15,75 +15,71 @@ limitations under the License. #include "gmm.h" -py::tuple init() -{ - torch::Tensor gmm_tensor = torch::zeros({GMM_COUNT, GMM_COMPONENT_COUNT}, torch::dtype(torch::kFloat32).device(torch::kCUDA)); - torch::Tensor scratch_tensor = torch::empty({1}, torch::dtype(torch::kFloat32).device(torch::kCUDA)); - return py::make_tuple(gmm_tensor, scratch_tensor); +py::tuple init() { + torch::Tensor gmm_tensor = + torch::zeros({GMM_COUNT, GMM_COMPONENT_COUNT}, torch::dtype(torch::kFloat32).device(torch::kCUDA)); + torch::Tensor scratch_tensor = torch::empty({1}, torch::dtype(torch::kFloat32).device(torch::kCUDA)); + return py::make_tuple(gmm_tensor, scratch_tensor); } -void learn(torch::Tensor gmm_tensor, torch::Tensor scratch_tensor, torch::Tensor input_tensor, torch::Tensor label_tensor) -{ - c10::DeviceType device_type = input_tensor.device().type(); - - unsigned int batch_count = input_tensor.size(0); - unsigned int element_count = input_tensor.stride(1); - - unsigned int scratch_size = batch_count * (element_count + GMM_COMPONENT_COUNT * GMM_COUNT * (element_count / (32 * 32))); - - if (scratch_tensor.size(0) < scratch_size) - { - scratch_tensor.resize_({scratch_size}); - } - - float* gmm = gmm_tensor.data_ptr(); - float* scratch = scratch_tensor.data_ptr(); - float* input = input_tensor.data_ptr(); - int* labels = label_tensor.data_ptr(); - - if(device_type == torch::kCUDA) - { - learn_cuda(input, labels, gmm, scratch, batch_count, element_count); - } - else - { - learn_cpu(input, labels, gmm, scratch, batch_count, element_count); - } +void learn( + torch::Tensor gmm_tensor, + torch::Tensor scratch_tensor, + torch::Tensor input_tensor, + torch::Tensor label_tensor) { + c10::DeviceType device_type = input_tensor.device().type(); + + unsigned int batch_count = input_tensor.size(0); + unsigned int element_count = input_tensor.stride(1); + + unsigned int scratch_size = + batch_count * (element_count + GMM_COMPONENT_COUNT * GMM_COUNT * (element_count / (32 * 32))); + + if (scratch_tensor.size(0) < scratch_size) { + scratch_tensor.resize_({scratch_size}); + } + + float* gmm = gmm_tensor.data_ptr(); + float* scratch = scratch_tensor.data_ptr(); + float* input = input_tensor.data_ptr(); + int* labels = label_tensor.data_ptr(); + + if (device_type == torch::kCUDA) { + learn_cuda(input, labels, gmm, scratch, batch_count, element_count); + } else { + learn_cpu(input, labels, gmm, scratch, batch_count, element_count); + } } -torch::Tensor apply(torch::Tensor gmm_tensor, torch::Tensor input_tensor) -{ - c10::DeviceType device_type = input_tensor.device().type(); - - unsigned int dim = input_tensor.dim(); - unsigned int batch_count = input_tensor.size(0); - unsigned int element_count = input_tensor.stride(1); - - long int* output_size = new long int[dim]; - memcpy(output_size, input_tensor.sizes().data(), dim * sizeof(long int)); - output_size[1] = MIXTURE_COUNT; - torch::Tensor output_tensor = torch::empty(c10::IntArrayRef(output_size, dim), torch::dtype(torch::kFloat32).device(device_type)); - delete output_size; - - const float* gmm = gmm_tensor.data_ptr(); - const float* input = input_tensor.data_ptr(); - float* output = output_tensor.data_ptr(); - - if(device_type == torch::kCUDA) - { - apply_cuda(gmm, input, output, batch_count, element_count); - } - else - { - apply_cpu(gmm, input, output, batch_count, element_count); - } - - return output_tensor; +torch::Tensor apply(torch::Tensor gmm_tensor, torch::Tensor input_tensor) { + c10::DeviceType device_type = input_tensor.device().type(); + + unsigned int dim = input_tensor.dim(); + unsigned int batch_count = input_tensor.size(0); + unsigned int element_count = input_tensor.stride(1); + + long int* output_size = new long int[dim]; + memcpy(output_size, input_tensor.sizes().data(), dim * sizeof(long int)); + output_size[1] = MIXTURE_COUNT; + torch::Tensor output_tensor = + torch::empty(c10::IntArrayRef(output_size, dim), torch::dtype(torch::kFloat32).device(device_type)); + delete output_size; + + const float* gmm = gmm_tensor.data_ptr(); + const float* input = input_tensor.data_ptr(); + float* output = output_tensor.data_ptr(); + + if (device_type == torch::kCUDA) { + apply_cuda(gmm, input, output, batch_count, element_count); + } else { + apply_cpu(gmm, input, output, batch_count, element_count); + } + + return output_tensor; } -PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) -{ - m.def("init", torch::wrap_pybind_function(init)); - m.def("learn", torch::wrap_pybind_function(learn)); - m.def("apply", torch::wrap_pybind_function(apply)); +PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { + m.def("init", torch::wrap_pybind_function(init)); + m.def("learn", torch::wrap_pybind_function(learn)); + m.def("apply", torch::wrap_pybind_function(apply)); } diff --git a/monai/_extensions/gmm/gmm.h b/monai/_extensions/gmm/gmm.h index 6317baa41a..09c0389ae6 100644 --- a/monai/_extensions/gmm/gmm.h +++ b/monai/_extensions/gmm/gmm.h @@ -24,9 +24,30 @@ limitations under the License. #define GMM_COMPONENT_COUNT (MATRIX_COMPONENT_COUNT + 1) #define GMM_COUNT (MIXTURE_COUNT * MIXTURE_SIZE) +void learn_cpu( + const float* input, + const int* labels, + float* gmm, + float* scratch_memory, + unsigned int batch_count, + unsigned int element_count); +void apply_cpu( + const float* gmm, + const float* input, + float* output, + unsigned int batch_count, + unsigned int element_count); -void learn_cpu(const float* input, const int* labels, float* gmm, float* scratch_memory, unsigned int batch_count, unsigned int element_count); -void apply_cpu(const float* gmm, const float* input, float* output, unsigned int batch_count, unsigned int element_count); - -void learn_cuda(const float* input, const int* labels, float* gmm, float* scratch_memory, unsigned int batch_count, unsigned int element_count); -void apply_cuda(const float* gmm, const float* input, float* output, unsigned int batch_count, unsigned int element_count); +void learn_cuda( + const float* input, + const int* labels, + float* gmm, + float* scratch_memory, + unsigned int batch_count, + unsigned int element_count); +void apply_cuda( + const float* gmm, + const float* input, + float* output, + unsigned int batch_count, + unsigned int element_count); diff --git a/monai/_extensions/gmm/gmm_cpu.cpp b/monai/_extensions/gmm/gmm_cpu.cpp index c9b55490eb..d7eedc07c8 100644 --- a/monai/_extensions/gmm/gmm_cpu.cpp +++ b/monai/_extensions/gmm/gmm_cpu.cpp @@ -15,12 +15,21 @@ limitations under the License. #include "gmm.h" -void learn_cpu(const float* input, const int* labels, float* gmm, float* scratch_memory, unsigned int batch_count, unsigned int element_count) -{ - throw std::invalid_argument("GMM received a cpu tensor but is not yet implemented for the cpu"); +void learn_cpu( + const float* input, + const int* labels, + float* gmm, + float* scratch_memory, + unsigned int batch_count, + unsigned int element_count) { + throw std::invalid_argument("GMM received a cpu tensor but is not yet implemented for the cpu"); } -void apply_cpu(const float* gmm, const float* input, float* output, unsigned int batch_count, unsigned int element_count) -{ - throw std::invalid_argument("GMM received a cpu tensor but is not yet implemented for the cpu"); +void apply_cpu( + const float* gmm, + const float* input, + float* output, + unsigned int batch_count, + unsigned int element_count) { + throw std::invalid_argument("GMM received a cpu tensor but is not yet implemented for the cpu"); } diff --git a/monai/_extensions/gmm/gmm_cuda.cu b/monai/_extensions/gmm/gmm_cuda.cu index 765ffe5b1c..2cf70a9920 100644 --- a/monai/_extensions/gmm/gmm_cuda.cu +++ b/monai/_extensions/gmm/gmm_cuda.cu @@ -20,434 +20,408 @@ limitations under the License. #define EPSILON 1e-5 #define BLOCK_SIZE 32 -#define TILE(SIZE, STRIDE) ((((SIZE) - 1)/(STRIDE)) + 1) +#define TILE(SIZE, STRIDE) ((((SIZE)-1) / (STRIDE)) + 1) -template -__global__ void CovarianceReductionKernel(int gaussian_index, const float* g_image, const int* g_alpha, float* g_matrices, int element_count) -{ - constexpr int block_size = warp_count * 32; +template +__global__ void CovarianceReductionKernel( + int gaussian_index, + const float* g_image, + const int* g_alpha, + float* g_matrices, + int element_count) { + constexpr int block_size = warp_count * 32; - __shared__ float s_matrix_component[warp_count]; + __shared__ float s_matrix_component[warp_count]; - int batch_index = blockIdx.z; + int batch_index = blockIdx.z; - const float* g_batch_image = g_image + batch_index * element_count * CHANNEL_COUNT; - const int* g_batch_alpha = g_alpha + batch_index * element_count; - float* g_batch_matrices = g_matrices + batch_index * GMM_COUNT * GMM_COMPONENT_COUNT * gridDim.x; + const float* g_batch_image = g_image + batch_index * element_count * CHANNEL_COUNT; + const int* g_batch_alpha = g_alpha + batch_index * element_count; + float* g_batch_matrices = g_matrices + batch_index * GMM_COUNT * GMM_COMPONENT_COUNT * gridDim.x; - int local_index = threadIdx.x; - int block_index = blockIdx.x; - int warp_index = local_index >> 5; - int lane_index = local_index & 31; - int global_index = local_index + block_index * block_size * load_count; - int matrix_offset = (gaussian_index * gridDim.x + block_index) * GMM_COMPONENT_COUNT; + int local_index = threadIdx.x; + int block_index = blockIdx.x; + int warp_index = local_index >> 5; + int lane_index = local_index & 31; + int global_index = local_index + block_index * block_size * load_count; + int matrix_offset = (gaussian_index * gridDim.x + block_index) * GMM_COMPONENT_COUNT; - float matrix[MATRIX_COMPONENT_COUNT]; + float matrix[MATRIX_COMPONENT_COUNT]; - for (int i = 0; i < MATRIX_COMPONENT_COUNT; i++) - { - matrix[i] = 0; - } + for (int i = 0; i < MATRIX_COMPONENT_COUNT; i++) { + matrix[i] = 0; + } + + for (int load = 0; load < load_count; load++) { + global_index += load * block_size; + + if (global_index < element_count) { + int my_alpha = g_batch_alpha[global_index]; + + if (my_alpha != -1) { + if (gaussian_index == (my_alpha & 15) + (my_alpha >> 4) * MIXTURE_COUNT) { + float feature[CHANNEL_COUNT + 1]; - for (int load = 0; load < load_count; load++) - { - global_index += load * block_size; - - if (global_index < element_count) - { - int my_alpha = g_batch_alpha[global_index]; - - if (my_alpha != -1) - { - if (gaussian_index == (my_alpha & 15) + (my_alpha >> 4) * MIXTURE_COUNT) - { - float feature[CHANNEL_COUNT + 1]; - - feature[0] = 1; - - for (int i = 0; i < CHANNEL_COUNT; i++) - { - feature[i + 1] = g_batch_image[global_index + i * element_count]; - } - - for (int index = 0, i = 0; i < CHANNEL_COUNT + 1; i++) - { - for (int j = i; j < CHANNEL_COUNT + 1; j++, index++) - { - matrix[index] += feature[i] * feature[j]; - } - } - } + feature[0] = 1; + + for (int i = 0; i < CHANNEL_COUNT; i++) { + feature[i + 1] = g_batch_image[global_index + i * element_count]; + } + + for (int index = 0, i = 0; i < CHANNEL_COUNT + 1; i++) { + for (int j = i; j < CHANNEL_COUNT + 1; j++, index++) { + matrix[index] += feature[i] * feature[j]; } + } } + } } + } - __syncthreads(); + __syncthreads(); - for (int i = 0; i < MATRIX_COMPONENT_COUNT; i++) - { - float matrix_component = matrix[i]; + for (int i = 0; i < MATRIX_COMPONENT_COUNT; i++) { + float matrix_component = matrix[i]; - matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 16); - matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 8); - matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 4); - matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 2); - matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 1); - - if (lane_index == 0) - { - s_matrix_component[warp_index] = matrix_component; - } - - __syncthreads(); + matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 16); + matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 8); + matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 4); + matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 2); + matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 1); - if (warp_index == 0) - { - matrix_component = s_matrix_component[lane_index]; + if (lane_index == 0) { + s_matrix_component[warp_index] = matrix_component; + } - if (warp_count >= 32) { matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 16); } - if (warp_count >= 16) { matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 8); } - if (warp_count >= 8) { matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 4); } - if (warp_count >= 4) { matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 2); } - if (warp_count >= 2) { matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 1); } + __syncthreads(); - if (lane_index == 0) - { - g_batch_matrices[matrix_offset + i] = matrix_component; - } - } + if (warp_index == 0) { + matrix_component = s_matrix_component[lane_index]; - __syncthreads(); + if (warp_count >= 32) { + matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 16); + } + if (warp_count >= 16) { + matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 8); + } + if (warp_count >= 8) { + matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 4); + } + if (warp_count >= 4) { + matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 2); + } + if (warp_count >= 2) { + matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 1); + } + + if (lane_index == 0) { + g_batch_matrices[matrix_offset + i] = matrix_component; + } } + + __syncthreads(); + } } -template -__global__ void CovarianceFinalizationKernel(const float* g_matrices, float* g_gmm, int matrix_count) -{ - constexpr int block_size = warp_count * 32; +template +__global__ void CovarianceFinalizationKernel(const float* g_matrices, float* g_gmm, int matrix_count) { + constexpr int block_size = warp_count * 32; - __shared__ float s_matrix_component[warp_count]; - __shared__ float s_gmm[GMM_COMPONENT_COUNT]; + __shared__ float s_matrix_component[warp_count]; + __shared__ float s_gmm[GMM_COMPONENT_COUNT]; - int batch_index = blockIdx.z; + int batch_index = blockIdx.z; - const float* g_batch_matrices = g_matrices + batch_index * GMM_COUNT * GMM_COMPONENT_COUNT * matrix_count; - float* g_batch_gmm = g_gmm + batch_index * GMM_COUNT * GMM_COMPONENT_COUNT; + const float* g_batch_matrices = g_matrices + batch_index * GMM_COUNT * GMM_COMPONENT_COUNT * matrix_count; + float* g_batch_gmm = g_gmm + batch_index * GMM_COUNT * GMM_COMPONENT_COUNT; - int local_index = threadIdx.x; - int warp_index = local_index >> 5; - int lane_index = local_index & 31; - int gmm_index = blockIdx.x; - int matrix_offset = gmm_index * matrix_count; + int local_index = threadIdx.x; + int warp_index = local_index >> 5; + int lane_index = local_index & 31; + int gmm_index = blockIdx.x; + int matrix_offset = gmm_index * matrix_count; - int load_count = TILE(matrix_count, block_size); + int load_count = TILE(matrix_count, block_size); - float norm_factor = 1.0f; + float norm_factor = 1.0f; - for (int index = 0, i = 0; i < CHANNEL_COUNT + 1; i++) - { - for (int j = i; j < CHANNEL_COUNT + 1; j++, index++) - { - float matrix_component = 0.0f; + for (int index = 0, i = 0; i < CHANNEL_COUNT + 1; i++) { + for (int j = i; j < CHANNEL_COUNT + 1; j++, index++) { + float matrix_component = 0.0f; - for(int load = 0; load < load_count; load++) - { - int matrix_index = local_index + load * block_size; + for (int load = 0; load < load_count; load++) { + int matrix_index = local_index + load * block_size; - if(matrix_index < matrix_count) - { - matrix_component += g_batch_matrices[(matrix_offset + matrix_index) * GMM_COMPONENT_COUNT + index]; - } - } + if (matrix_index < matrix_count) { + matrix_component += g_batch_matrices[(matrix_offset + matrix_index) * GMM_COMPONENT_COUNT + index]; + } + } - matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 16); - matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 8); - matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 4); - matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 2); - matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 1); + matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 16); + matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 8); + matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 4); + matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 2); + matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 1); - if (lane_index == 0) - { - s_matrix_component[warp_index] = matrix_component; - } + if (lane_index == 0) { + s_matrix_component[warp_index] = matrix_component; + } - __syncthreads(); + __syncthreads(); - if (warp_index == 0) - { - matrix_component = s_matrix_component[lane_index]; + if (warp_index == 0) { + matrix_component = s_matrix_component[lane_index]; - if (warp_count >= 32) { matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 16); } - if (warp_count >= 16) { matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 8); } - if (warp_count >= 8) { matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 4); } - if (warp_count >= 4) { matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 2); } - if (warp_count >= 2) { matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 1); } - - if (lane_index == 0) - { - float constant = i == 0 ? 0.0f : s_gmm[i] * s_gmm[j]; + if (warp_count >= 32) { + matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 16); + } + if (warp_count >= 16) { + matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 8); + } + if (warp_count >= 8) { + matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 4); + } + if (warp_count >= 4) { + matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 2); + } + if (warp_count >= 2) { + matrix_component += __shfl_down_sync(0xffffffff, matrix_component, 1); + } - if (i != 0 && i == j) - { - constant -= EPSILON; - } + if (lane_index == 0) { + float constant = i == 0 ? 0.0f : s_gmm[i] * s_gmm[j]; - s_gmm[index] = norm_factor * matrix_component - constant; + if (i != 0 && i == j) { + constant -= EPSILON; + } - if (index == 0 && matrix_component > 0) - { - norm_factor = 1.0f / matrix_component; - } - } - } + s_gmm[index] = norm_factor * matrix_component - constant; - __syncthreads(); + if (index == 0 && matrix_component > 0) { + norm_factor = 1.0f / matrix_component; + } } + } + + __syncthreads(); } + } - float* matrix = s_gmm + (CHANNEL_COUNT + 1); - float* det_ptr = s_gmm + MATRIX_COMPONENT_COUNT; + float* matrix = s_gmm + (CHANNEL_COUNT + 1); + float* det_ptr = s_gmm + MATRIX_COMPONENT_COUNT; - if (local_index == 0) - { - float square_mat[CHANNEL_COUNT][CHANNEL_COUNT]; - float cholesky_mat[CHANNEL_COUNT][CHANNEL_COUNT]; + if (local_index == 0) { + float square_mat[CHANNEL_COUNT][CHANNEL_COUNT]; + float cholesky_mat[CHANNEL_COUNT][CHANNEL_COUNT]; - for(int i = 0; i < CHANNEL_COUNT; i++) - { - for(int j = 0; j < CHANNEL_COUNT; j++) - { - square_mat[i][j] = 0.0f; - cholesky_mat[i][j] = 0.0f; - } - } + for (int i = 0; i < CHANNEL_COUNT; i++) { + for (int j = 0; j < CHANNEL_COUNT; j++) { + square_mat[i][j] = 0.0f; + cholesky_mat[i][j] = 0.0f; + } + } - to_square(matrix, square_mat); - cholesky(square_mat, cholesky_mat); + to_square(matrix, square_mat); + cholesky(square_mat, cholesky_mat); - *det_ptr = chol_det(cholesky_mat); + *det_ptr = chol_det(cholesky_mat); - if (invert_matrix) - { - chol_inv(cholesky_mat, square_mat); - to_triangle(square_mat, matrix); - } + if (invert_matrix) { + chol_inv(cholesky_mat, square_mat); + to_triangle(square_mat, matrix); } + } - if (local_index < GMM_COMPONENT_COUNT) - { - g_batch_gmm[gmm_index * GMM_COMPONENT_COUNT + local_index] = s_gmm[local_index]; - } + if (local_index < GMM_COMPONENT_COUNT) { + g_batch_gmm[gmm_index * GMM_COMPONENT_COUNT + local_index] = s_gmm[local_index]; + } } -struct GMMSplit_t -{ - int idx; - float threshold; - float eigenvector[CHANNEL_COUNT]; +struct GMMSplit_t { + int idx; + float threshold; + float eigenvector[CHANNEL_COUNT]; }; // 1 Block, 32xMIXTURE_COUNT -__global__ void GMMFindSplit(GMMSplit_t *gmmSplit, int gmmK, float *gmm) -{ - int batch_index = blockIdx.z; +__global__ void GMMFindSplit(GMMSplit_t* gmmSplit, int gmmK, float* gmm) { + int batch_index = blockIdx.z; - float* g_batch_gmm = gmm + batch_index * GMM_COUNT * GMM_COMPONENT_COUNT; - GMMSplit_t* g_batch_gmmSplit = gmmSplit + batch_index * MIXTURE_COUNT; + float* g_batch_gmm = gmm + batch_index * GMM_COUNT * GMM_COMPONENT_COUNT; + GMMSplit_t* g_batch_gmmSplit = gmmSplit + batch_index * MIXTURE_COUNT; - int gmm_idx = threadIdx.x * MIXTURE_COUNT + threadIdx.y; + int gmm_idx = threadIdx.x * MIXTURE_COUNT + threadIdx.y; - float eigenvalue = 0; - float eigenvector[CHANNEL_COUNT]; + float eigenvalue = 0; + float eigenvector[CHANNEL_COUNT]; - if (threadIdx.x < gmmK) - { - float* matrix = g_batch_gmm + gmm_idx * GMM_COMPONENT_COUNT + (CHANNEL_COUNT + 1); - largest_eigenpair(matrix, eigenvector, &eigenvalue); - } - - float max_value = eigenvalue; + if (threadIdx.x < gmmK) { + float* matrix = g_batch_gmm + gmm_idx * GMM_COMPONENT_COUNT + (CHANNEL_COUNT + 1); + largest_eigenpair(matrix, eigenvector, &eigenvalue); + } - max_value = max(max_value, __shfl_xor_sync(0xffffffff, max_value, 16)); - max_value = max(max_value, __shfl_xor_sync(0xffffffff, max_value, 8)); - max_value = max(max_value, __shfl_xor_sync(0xffffffff, max_value, 4)); - max_value = max(max_value, __shfl_xor_sync(0xffffffff, max_value, 2)); - max_value = max(max_value, __shfl_xor_sync(0xffffffff, max_value, 1)); + float max_value = eigenvalue; - if (max_value == eigenvalue) - { - GMMSplit_t split; + max_value = max(max_value, __shfl_xor_sync(0xffffffff, max_value, 16)); + max_value = max(max_value, __shfl_xor_sync(0xffffffff, max_value, 8)); + max_value = max(max_value, __shfl_xor_sync(0xffffffff, max_value, 4)); + max_value = max(max_value, __shfl_xor_sync(0xffffffff, max_value, 2)); + max_value = max(max_value, __shfl_xor_sync(0xffffffff, max_value, 1)); - float* average_feature = gmm + gmm_idx * GMM_COMPONENT_COUNT + 1; + if (max_value == eigenvalue) { + GMMSplit_t split; - split.idx = threadIdx.x; - split.threshold = scalar_prod(average_feature, eigenvector); + float* average_feature = gmm + gmm_idx * GMM_COMPONENT_COUNT + 1; - for (int i = 0; i < CHANNEL_COUNT; i++) - { - split.eigenvector[i] = eigenvector[i]; - } + split.idx = threadIdx.x; + split.threshold = scalar_prod(average_feature, eigenvector); - g_batch_gmmSplit[threadIdx.y] = split; + for (int i = 0; i < CHANNEL_COUNT; i++) { + split.eigenvector[i] = eigenvector[i]; } + + g_batch_gmmSplit[threadIdx.y] = split; + } } #define DO_SPLIT_DEGENERACY 4 -__global__ void GMMDoSplit(const GMMSplit_t *gmmSplit, int k, const float *image, int *alpha, int element_count) -{ - __shared__ GMMSplit_t s_gmmSplit[MIXTURE_COUNT]; +__global__ void GMMDoSplit(const GMMSplit_t* gmmSplit, int k, const float* image, int* alpha, int element_count) { + __shared__ GMMSplit_t s_gmmSplit[MIXTURE_COUNT]; - int batch_index = blockIdx.z; + int batch_index = blockIdx.z; - const GMMSplit_t* g_batch_gmmSplit = gmmSplit + batch_index * MIXTURE_COUNT; - const float* g_batch_image = image + batch_index * element_count * CHANNEL_COUNT; - int* g_batch_alpha = alpha + batch_index * element_count; + const GMMSplit_t* g_batch_gmmSplit = gmmSplit + batch_index * MIXTURE_COUNT; + const float* g_batch_image = image + batch_index * element_count * CHANNEL_COUNT; + int* g_batch_alpha = alpha + batch_index * element_count; - int *s_linear = (int *) s_gmmSplit; - int *g_linear = (int *) g_batch_gmmSplit; + int* s_linear = (int*)s_gmmSplit; + int* g_linear = (int*)g_batch_gmmSplit; - if (threadIdx.x < MIXTURE_COUNT * sizeof(GMMSplit_t)) - { - s_linear[threadIdx.x] = g_linear[threadIdx.x]; - } + if (threadIdx.x < MIXTURE_COUNT * sizeof(GMMSplit_t)) { + s_linear[threadIdx.x] = g_linear[threadIdx.x]; + } - __syncthreads(); + __syncthreads(); - int index = threadIdx.x + blockIdx.x * BLOCK_SIZE * DO_SPLIT_DEGENERACY; + int index = threadIdx.x + blockIdx.x * BLOCK_SIZE * DO_SPLIT_DEGENERACY; - for (int i = 0; i < DO_SPLIT_DEGENERACY; i++) - { - index += BLOCK_SIZE; + for (int i = 0; i < DO_SPLIT_DEGENERACY; i++) { + index += BLOCK_SIZE; - if (index < element_count) - { - int my_alpha = g_batch_alpha[index]; + if (index < element_count) { + int my_alpha = g_batch_alpha[index]; - if(my_alpha != -1) - { - int select = my_alpha & 15; - int gmm_idx = my_alpha >> 4; + if (my_alpha != -1) { + int select = my_alpha & 15; + int gmm_idx = my_alpha >> 4; - if (gmm_idx == s_gmmSplit[select].idx) - { - // in the split cluster now - float feature[CHANNEL_COUNT]; + if (gmm_idx == s_gmmSplit[select].idx) { + // in the split cluster now + float feature[CHANNEL_COUNT]; - for (int i = 0; i < CHANNEL_COUNT; i++) - { - feature[i] = g_batch_image[index + i * element_count]; - } + for (int i = 0; i < CHANNEL_COUNT; i++) { + feature[i] = g_batch_image[index + i * element_count]; + } - float value = scalar_prod(s_gmmSplit[select].eigenvector, feature); + float value = scalar_prod(s_gmmSplit[select].eigenvector, feature); - if (value > s_gmmSplit[select].threshold) - { - // assign pixel to new cluster - g_batch_alpha[index] = k + select; - } - } - } + if (value > s_gmmSplit[select].threshold) { + // assign pixel to new cluster + g_batch_alpha[index] = k + select; + } } + } } + } } // Single block, 32xMIXTURE_COUNT -__global__ void GMMcommonTerm(float *g_gmm) -{ - int batch_index = blockIdx.z; +__global__ void GMMcommonTerm(float* g_gmm) { + int batch_index = blockIdx.z; - float* g_batch_gmm = g_gmm + batch_index * GMM_COUNT * GMM_COMPONENT_COUNT; + float* g_batch_gmm = g_gmm + batch_index * GMM_COUNT * GMM_COMPONENT_COUNT; - int gmm_index = (threadIdx.x * MIXTURE_COUNT) + threadIdx.y; + int gmm_index = (threadIdx.x * MIXTURE_COUNT) + threadIdx.y; - float gmm_n = threadIdx.x < MIXTURE_SIZE ? g_batch_gmm[gmm_index * GMM_COMPONENT_COUNT] : 0.0f; + float gmm_n = threadIdx.x < MIXTURE_SIZE ? g_batch_gmm[gmm_index * GMM_COMPONENT_COUNT] : 0.0f; - float sum = gmm_n; + float sum = gmm_n; - sum += __shfl_xor_sync(0xffffffff, sum, 1); - sum += __shfl_xor_sync(0xffffffff, sum, 2); - sum += __shfl_xor_sync(0xffffffff, sum, 4); - sum += __shfl_xor_sync(0xffffffff, sum, 8); - sum += __shfl_xor_sync(0xffffffff, sum, 16); + sum += __shfl_xor_sync(0xffffffff, sum, 1); + sum += __shfl_xor_sync(0xffffffff, sum, 2); + sum += __shfl_xor_sync(0xffffffff, sum, 4); + sum += __shfl_xor_sync(0xffffffff, sum, 8); + sum += __shfl_xor_sync(0xffffffff, sum, 16); - if (threadIdx.x < MIXTURE_SIZE) - { - float det = g_batch_gmm[gmm_index * GMM_COMPONENT_COUNT + MATRIX_COMPONENT_COUNT] + EPSILON; - float commonTerm = det > 0.0f ? gmm_n / (sqrtf(det) * sum) : gmm_n / sum; + if (threadIdx.x < MIXTURE_SIZE) { + float det = g_batch_gmm[gmm_index * GMM_COMPONENT_COUNT + MATRIX_COMPONENT_COUNT] + EPSILON; + float commonTerm = det > 0.0f ? gmm_n / (sqrtf(det) * sum) : gmm_n / sum; - g_batch_gmm[gmm_index * GMM_COMPONENT_COUNT + MATRIX_COMPONENT_COUNT] = commonTerm; - } + g_batch_gmm[gmm_index * GMM_COMPONENT_COUNT + MATRIX_COMPONENT_COUNT] = commonTerm; + } } -__device__ float GMMTerm(float* feature, const float *gmm) -{ - const float* average_feature = gmm + 1; - const float* matrix = gmm + CHANNEL_COUNT + 1; +__device__ float GMMTerm(float* feature, const float* gmm) { + const float* average_feature = gmm + 1; + const float* matrix = gmm + CHANNEL_COUNT + 1; - float diff[CHANNEL_COUNT]; + float diff[CHANNEL_COUNT]; - for (int i = 0; i < CHANNEL_COUNT; i++) - { - diff[i] = feature[i] - average_feature[i]; - } + for (int i = 0; i < CHANNEL_COUNT; i++) { + diff[i] = feature[i] - average_feature[i]; + } - float value = 0.0f; + float value = 0.0f; - for (int index = 0, i = 0; i < CHANNEL_COUNT; i++) - { - for (int j = i; j < CHANNEL_COUNT; j++, index++) - { - float term = diff[i] * diff[j] * matrix[index]; + for (int index = 0, i = 0; i < CHANNEL_COUNT; i++) { + for (int j = i; j < CHANNEL_COUNT; j++, index++) { + float term = diff[i] * diff[j] * matrix[index]; - value += i == j ? term : 2 * term; - } + value += i == j ? term : 2 * term; } + } - return gmm[MATRIX_COMPONENT_COUNT] * expf(-0.5 * value); + return gmm[MATRIX_COMPONENT_COUNT] * expf(-0.5 * value); } -__global__ void GMMDataTermKernel(const float *image, const float *gmm, float* output, int element_count) -{ - int batch_index = blockIdx.z; +__global__ void GMMDataTermKernel(const float* image, const float* gmm, float* output, int element_count) { + int batch_index = blockIdx.z; - const float* g_batch_image = image + batch_index * element_count * CHANNEL_COUNT; - const float* g_batch_gmm = gmm + batch_index * GMM_COUNT * GMM_COMPONENT_COUNT; - float* g_batch_output = output + batch_index * element_count * MIXTURE_COUNT; + const float* g_batch_image = image + batch_index * element_count * CHANNEL_COUNT; + const float* g_batch_gmm = gmm + batch_index * GMM_COUNT * GMM_COMPONENT_COUNT; + float* g_batch_output = output + batch_index * element_count * MIXTURE_COUNT; - int index = blockIdx.x * blockDim.x + threadIdx.x; + int index = blockIdx.x * blockDim.x + threadIdx.x; - if (index >= element_count) return; + if (index >= element_count) + return; - float feature[CHANNEL_COUNT]; + float feature[CHANNEL_COUNT]; - for (int i = 0; i < CHANNEL_COUNT; i++) - { - feature[i] = g_batch_image[index + i * element_count]; - } - - float weights[MIXTURE_COUNT]; - float weight_total = 0.0f; + for (int i = 0; i < CHANNEL_COUNT; i++) { + feature[i] = g_batch_image[index + i * element_count]; + } - for(int i = 0; i < MIXTURE_COUNT; i++) - { - float mixture_weight = 0.0f; + float weights[MIXTURE_COUNT]; + float weight_total = 0.0f; - for(int j = 0; j < MIXTURE_SIZE; j++) - { - mixture_weight += GMMTerm(feature, &g_batch_gmm[(MIXTURE_COUNT * j + i) * GMM_COMPONENT_COUNT]); - } + for (int i = 0; i < MIXTURE_COUNT; i++) { + float mixture_weight = 0.0f; - weights[i] = mixture_weight; - weight_total += mixture_weight; + for (int j = 0; j < MIXTURE_SIZE; j++) { + mixture_weight += GMMTerm(feature, &g_batch_gmm[(MIXTURE_COUNT * j + i) * GMM_COMPONENT_COUNT]); } - for(int i = 0; i < MIXTURE_COUNT; i++) - { - // protecting against pixels with 0 in all mixtures - float final_weight = weight_total > 0.0f ? weights[i] / weight_total : 0.0f; - g_batch_output[index + i * element_count] = final_weight; - } + weights[i] = mixture_weight; + weight_total += mixture_weight; + } + + for (int i = 0; i < MIXTURE_COUNT; i++) { + // protecting against pixels with 0 in all mixtures + float final_weight = weight_total > 0.0f ? weights[i] / weight_total : 0.0f; + g_batch_output[index + i * element_count] = final_weight; + } } #define THREADS 512 @@ -455,67 +429,90 @@ __global__ void GMMDataTermKernel(const float *image, const float *gmm, float* o #define BLOCK (WARPS << 5) #define LOAD 4 -void GMMInitialize(const float *image, int *alpha, float *gmm, float *scratch_mem, unsigned int batch_count, unsigned int element_count) -{ - unsigned int block_count = TILE(element_count, BLOCK * LOAD); +void GMMInitialize( + const float* image, + int* alpha, + float* gmm, + float* scratch_mem, + unsigned int batch_count, + unsigned int element_count) { + unsigned int block_count = TILE(element_count, BLOCK * LOAD); - float* block_gmm_scratch = scratch_mem; - GMMSplit_t* gmm_split_scratch = (GMMSplit_t*) scratch_mem; + float* block_gmm_scratch = scratch_mem; + GMMSplit_t* gmm_split_scratch = (GMMSplit_t*)scratch_mem; - int gmm_N = MIXTURE_COUNT * MIXTURE_SIZE; + int gmm_N = MIXTURE_COUNT * MIXTURE_SIZE; - for (unsigned int k = MIXTURE_COUNT; k < gmm_N; k+=MIXTURE_COUNT) - { - for (unsigned int i = 0; i < k; ++i) - { - CovarianceReductionKernel<<<{block_count, 1, batch_count}, BLOCK>>>(i, image, alpha, block_gmm_scratch, element_count); - } + for (unsigned int k = MIXTURE_COUNT; k < gmm_N; k += MIXTURE_COUNT) { + for (unsigned int i = 0; i < k; ++i) { + CovarianceReductionKernel + <<<{block_count, 1, batch_count}, BLOCK>>>(i, image, alpha, block_gmm_scratch, element_count); + } - CovarianceFinalizationKernel<<<{k, 1, batch_count}, BLOCK>>>(block_gmm_scratch, gmm, block_count); + CovarianceFinalizationKernel<<<{k, 1, batch_count}, BLOCK>>>(block_gmm_scratch, gmm, block_count); - GMMFindSplit<<<{1, 1, batch_count}, dim3(BLOCK_SIZE, MIXTURE_COUNT)>>>(gmm_split_scratch, k / MIXTURE_COUNT, gmm); - GMMDoSplit<<<{TILE(element_count, BLOCK_SIZE * DO_SPLIT_DEGENERACY), 1, batch_count}, BLOCK_SIZE>>>(gmm_split_scratch, (k / MIXTURE_COUNT) << 4, image, alpha, element_count); - } + GMMFindSplit<<<{1, 1, batch_count}, dim3(BLOCK_SIZE, MIXTURE_COUNT)>>>(gmm_split_scratch, k / MIXTURE_COUNT, gmm); + GMMDoSplit<<<{TILE(element_count, BLOCK_SIZE * DO_SPLIT_DEGENERACY), 1, batch_count}, BLOCK_SIZE>>>( + gmm_split_scratch, (k / MIXTURE_COUNT) << 4, image, alpha, element_count); + } } -void GMMUpdate(const float *image, int *alpha, float *gmm, float *scratch_mem, unsigned int batch_count, unsigned int element_count) -{ - unsigned int block_count = TILE(element_count, BLOCK * LOAD); +void GMMUpdate( + const float* image, + int* alpha, + float* gmm, + float* scratch_mem, + unsigned int batch_count, + unsigned int element_count) { + unsigned int block_count = TILE(element_count, BLOCK * LOAD); - float* block_gmm_scratch = scratch_mem; + float* block_gmm_scratch = scratch_mem; - unsigned int gmm_N = MIXTURE_COUNT * MIXTURE_SIZE; + unsigned int gmm_N = MIXTURE_COUNT * MIXTURE_SIZE; - for (unsigned int i = 0; i < gmm_N; ++i) - { - CovarianceReductionKernel<<<{block_count, 1, batch_count}, BLOCK>>>(i, image, alpha, block_gmm_scratch, element_count); - } + for (unsigned int i = 0; i < gmm_N; ++i) { + CovarianceReductionKernel + <<<{block_count, 1, batch_count}, BLOCK>>>(i, image, alpha, block_gmm_scratch, element_count); + } - CovarianceFinalizationKernel<<<{gmm_N, 1, batch_count}, BLOCK>>>(block_gmm_scratch, gmm, block_count); + CovarianceFinalizationKernel<<<{gmm_N, 1, batch_count}, BLOCK>>>(block_gmm_scratch, gmm, block_count); - GMMcommonTerm<<<{1, 1, batch_count}, dim3(BLOCK_SIZE, MIXTURE_COUNT)>>>(gmm); + GMMcommonTerm<<<{1, 1, batch_count}, dim3(BLOCK_SIZE, MIXTURE_COUNT)>>>(gmm); } -void GMMDataTerm(const float *image, const float *gmm, float* output, unsigned int batch_count, unsigned int element_count) -{ - dim3 block(BLOCK_SIZE, 1); - dim3 grid(TILE(element_count, BLOCK_SIZE), 1, batch_count); +void GMMDataTerm( + const float* image, + const float* gmm, + float* output, + unsigned int batch_count, + unsigned int element_count) { + dim3 block(BLOCK_SIZE, 1); + dim3 grid(TILE(element_count, BLOCK_SIZE), 1, batch_count); - GMMDataTermKernel<<>>(image, gmm, output, element_count); + GMMDataTermKernel<<>>(image, gmm, output, element_count); } -void learn_cuda(const float* input, const int* labels, float* gmm, float* scratch_memory, unsigned int batch_count, unsigned int element_count) -{ - int* alpha = (int*)scratch_memory; - float* scratch_mem = scratch_memory + batch_count * element_count; +void learn_cuda( + const float* input, + const int* labels, + float* gmm, + float* scratch_memory, + unsigned int batch_count, + unsigned int element_count) { + int* alpha = (int*)scratch_memory; + float* scratch_mem = scratch_memory + batch_count * element_count; - cudaMemcpyAsync(alpha, labels, batch_count * element_count * sizeof(int), cudaMemcpyDeviceToDevice); + cudaMemcpyAsync(alpha, labels, batch_count * element_count * sizeof(int), cudaMemcpyDeviceToDevice); - GMMInitialize(input, alpha, gmm, scratch_mem, batch_count, element_count); - GMMUpdate(input, alpha, gmm, scratch_mem, batch_count, element_count); + GMMInitialize(input, alpha, gmm, scratch_mem, batch_count, element_count); + GMMUpdate(input, alpha, gmm, scratch_mem, batch_count, element_count); } -void apply_cuda(const float* gmm, const float* input, float* output, unsigned int batch_count, unsigned int element_count) -{ - GMMDataTerm(input, gmm, output, batch_count, element_count); +void apply_cuda( + const float* gmm, + const float* input, + float* output, + unsigned int batch_count, + unsigned int element_count) { + GMMDataTerm(input, gmm, output, batch_count, element_count); } diff --git a/monai/_extensions/gmm/gmm_cuda_linalg.cuh b/monai/_extensions/gmm/gmm_cuda_linalg.cuh index 9d54d80d3b..56c7c7ccdc 100644 --- a/monai/_extensions/gmm/gmm_cuda_linalg.cuh +++ b/monai/_extensions/gmm/gmm_cuda_linalg.cuh @@ -11,170 +11,134 @@ See the License for the specific language governing permissions and limitations under the License. */ -__device__ void to_square(float in[SUB_MATRIX_COMPONENT_COUNT], float out[CHANNEL_COUNT][CHANNEL_COUNT]) -{ - for (int index = 0, i = 0; i < CHANNEL_COUNT; i++) - { - for (int j = i; j < CHANNEL_COUNT; j++, index++) - { - out[i][j] = in[index]; - out[j][i] = in[index]; - } +__device__ void to_square(float in[SUB_MATRIX_COMPONENT_COUNT], float out[CHANNEL_COUNT][CHANNEL_COUNT]) { + for (int index = 0, i = 0; i < CHANNEL_COUNT; i++) { + for (int j = i; j < CHANNEL_COUNT; j++, index++) { + out[i][j] = in[index]; + out[j][i] = in[index]; } + } } -__device__ void to_triangle(float in[CHANNEL_COUNT][CHANNEL_COUNT], float out[SUB_MATRIX_COMPONENT_COUNT]) -{ - for (int index = 0, i = 0; i < CHANNEL_COUNT; i++) - { - for (int j = i; j < CHANNEL_COUNT; j++, index++) - { - out[index] = in[j][i]; - } +__device__ void to_triangle(float in[CHANNEL_COUNT][CHANNEL_COUNT], float out[SUB_MATRIX_COMPONENT_COUNT]) { + for (int index = 0, i = 0; i < CHANNEL_COUNT; i++) { + for (int j = i; j < CHANNEL_COUNT; j++, index++) { + out[index] = in[j][i]; } + } } -__device__ void cholesky(float in[CHANNEL_COUNT][CHANNEL_COUNT], float out[CHANNEL_COUNT][CHANNEL_COUNT]) -{ - for (int i = 0; i < CHANNEL_COUNT; i++) - { - for (int j = 0; j < i+1; j++) - { - float sum = 0.0f; - - for (int k = 0; k < j; k++) - { - sum += out[i][k] * out[j][k]; - } - - if (i == j) - { - out[i][j] = sqrtf(in[i][i] - sum); - } - else - { - out[i][j] = (in[i][j] - sum) / out[j][j]; - } - } +__device__ void cholesky(float in[CHANNEL_COUNT][CHANNEL_COUNT], float out[CHANNEL_COUNT][CHANNEL_COUNT]) { + for (int i = 0; i < CHANNEL_COUNT; i++) { + for (int j = 0; j < i + 1; j++) { + float sum = 0.0f; + + for (int k = 0; k < j; k++) { + sum += out[i][k] * out[j][k]; + } + + if (i == j) { + out[i][j] = sqrtf(in[i][i] - sum); + } else { + out[i][j] = (in[i][j] - sum) / out[j][j]; + } } + } } -__device__ float chol_det(float in[CHANNEL_COUNT][CHANNEL_COUNT]) -{ - float det = 1.0f; +__device__ float chol_det(float in[CHANNEL_COUNT][CHANNEL_COUNT]) { + float det = 1.0f; - for (int i = 0; i < CHANNEL_COUNT; i++) - { - det *= in[i][i]; - } + for (int i = 0; i < CHANNEL_COUNT; i++) { + det *= in[i][i]; + } - return det * det; + return det * det; } -__device__ void chol_inv(float in[CHANNEL_COUNT][CHANNEL_COUNT], float out[CHANNEL_COUNT][CHANNEL_COUNT]) -{ - // Invert cholesky matrix - for (int i = 0; i < CHANNEL_COUNT; i++) - { - in[i][i] = 1.0f / (in[i][i] + 0.0001f); +__device__ void chol_inv(float in[CHANNEL_COUNT][CHANNEL_COUNT], float out[CHANNEL_COUNT][CHANNEL_COUNT]) { + // Invert cholesky matrix + for (int i = 0; i < CHANNEL_COUNT; i++) { + in[i][i] = 1.0f / (in[i][i] + 0.0001f); - for (int j = 0; j < i; j++) - { - float sum = 0.0f; + for (int j = 0; j < i; j++) { + float sum = 0.0f; - for (int k = j; k < i; k++) - { - sum += in[i][k] * in[k][j]; - } + for (int k = j; k < i; k++) { + sum += in[i][k] * in[k][j]; + } - in[i][j] = -in[i][i] * sum; - } + in[i][j] = -in[i][i] * sum; } + } - // Dot with transpose of self - for (int i = 0; i < CHANNEL_COUNT; i++) - { - for (int j = 0; j < CHANNEL_COUNT; j++) - { - out[i][j] = 0.0f; - - for (int k = max(i, j); k < CHANNEL_COUNT; k++) - { - out[i][j] += in[k][i] * in[k][j]; - } - } + // Dot with transpose of self + for (int i = 0; i < CHANNEL_COUNT; i++) { + for (int j = 0; j < CHANNEL_COUNT; j++) { + out[i][j] = 0.0f; + + for (int k = max(i, j); k < CHANNEL_COUNT; k++) { + out[i][j] += in[k][i] * in[k][j]; + } } + } } -__device__ void normalize(float* v) -{ - float norm = 0.0f; +__device__ void normalize(float* v) { + float norm = 0.0f; - for (int i = 0; i < CHANNEL_COUNT; i++) - { - norm += v[i] * v[i]; - } + for (int i = 0; i < CHANNEL_COUNT; i++) { + norm += v[i] * v[i]; + } - norm = 1.0f / sqrtf(norm); + norm = 1.0f / sqrtf(norm); - for (int i = 0; i < CHANNEL_COUNT; i++) - { - v[i] *= norm; - } + for (int i = 0; i < CHANNEL_COUNT; i++) { + v[i] *= norm; + } } -__device__ float scalar_prod(float* a, float* b) -{ - float product = 0.0f; +__device__ float scalar_prod(float* a, float* b) { + float product = 0.0f; - for (int i = 0; i < CHANNEL_COUNT; i++) - { - product += a[i] * b[i]; - } + for (int i = 0; i < CHANNEL_COUNT; i++) { + product += a[i] * b[i]; + } - return product; + return product; } -__device__ void largest_eigenpair(const float *M, float* evec, float* eval) -{ - float scratch[CHANNEL_COUNT]; - - for(int i = 0; i < CHANNEL_COUNT; i++) - { - scratch[i] = i + 1; - } +__device__ void largest_eigenpair(const float* M, float* evec, float* eval) { + float scratch[CHANNEL_COUNT]; - for (int itr = 0; itr < 10; itr++) - { - *eval = 0.0f; + for (int i = 0; i < CHANNEL_COUNT; i++) { + scratch[i] = i + 1; + } - for (int i = 0; i < CHANNEL_COUNT; i++) - { - int index = i; + for (int itr = 0; itr < 10; itr++) { + *eval = 0.0f; - evec[i] = 0.0f; + for (int i = 0; i < CHANNEL_COUNT; i++) { + int index = i; - for (int j = 0; j < CHANNEL_COUNT; j++) - { - evec[i] += M[index] * scratch[j]; + evec[i] = 0.0f; - if (j < i) - { - index += CHANNEL_COUNT - (j + 1); - } - else - { - index += 1; - } - } + for (int j = 0; j < CHANNEL_COUNT; j++) { + evec[i] += M[index] * scratch[j]; - *eval = max(*eval, evec[i]); + if (j < i) { + index += CHANNEL_COUNT - (j + 1); + } else { + index += 1; } + } - for (int i = 0; i < CHANNEL_COUNT; i++) - { - evec[i] /= *eval; - scratch[i] = evec[i]; - } + *eval = max(*eval, evec[i]); + } + + for (int i = 0; i < CHANNEL_COUNT; i++) { + evec[i] /= *eval; + scratch[i] = evec[i]; } + } } diff --git a/monai/bundle/config_item.py b/monai/bundle/config_item.py index 3300fe91ff..a0e3ccca26 100644 --- a/monai/bundle/config_item.py +++ b/monai/bundle/config_item.py @@ -312,7 +312,7 @@ class ConfigExpression(ConfigItem): """ prefix = EXPR_KEY - run_eval = False if os.environ.get("MONAI_EVAL_EXPR", "1") == "0" else True + run_eval = not os.environ.get("MONAI_EVAL_EXPR", "1") == "0" def __init__(self, config: Any, id: str = "", globals: Optional[Dict] = None) -> None: super().__init__(config=config, id=id) diff --git a/monai/bundle/reference_resolver.py b/monai/bundle/reference_resolver.py index f9f73c9c71..656424a2a4 100644 --- a/monai/bundle/reference_resolver.py +++ b/monai/bundle/reference_resolver.py @@ -9,7 +9,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os import re +import warnings from typing import Any, Dict, Optional, Sequence, Set from monai.bundle.config_item import ConfigComponent, ConfigExpression, ConfigItem @@ -50,6 +52,8 @@ class ReferenceResolver: ref = ID_REF_KEY # reference prefix # match a reference string, e.g. "@id#key", "@id#key#0", "@_target_#key" id_matcher = re.compile(rf"{ref}(?:\w*)(?:{sep}\w*)*") + # if `allow_missing_reference` and can't find a reference ID, will just raise a warning and don't update the config + allow_missing_reference = not os.environ.get("MONAI_ALLOW_MISSING_REFERENCE", "0") == "0" def __init__(self, items: Optional[Sequence[ConfigItem]] = None): # save the items in a dictionary with the `ConfigItem.id` as key @@ -140,7 +144,12 @@ def _resolve_one_item(self, id: str, waiting_list: Optional[Set[str]] = None, ** try: look_up_option(d, self.items, print_all_options=False) except ValueError as err: - raise ValueError(f"the referring item `@{d}` is not defined in the config content.") from err + msg = f"the referring item `@{d}` is not defined in the config content." + if self.allow_missing_reference: + warnings.warn(msg) + continue + else: + raise ValueError(msg) from err # recursively resolve the reference first self._resolve_one_item(id=d, waiting_list=waiting_list, **kwargs) waiting_list.discard(d) @@ -210,7 +219,12 @@ def update_refs_pattern(cls, value: str, refs: Dict) -> str: for item in result: ref_id = item[len(cls.ref) :] # remove the ref prefix "@" if ref_id not in refs: - raise KeyError(f"can not find expected ID '{ref_id}' in the references.") + msg = f"can not find expected ID '{ref_id}' in the references." + if cls.allow_missing_reference: + warnings.warn(msg) + continue + else: + raise KeyError(msg) if value_is_expr: # replace with local code, will be used in the `evaluate` logic with `locals={"refs": ...}` value = value.replace(item, f"{cls._vars}['{ref_id}']") diff --git a/monai/data/__init__.py b/monai/data/__init__.py index bed194d2f4..19ca29eafa 100644 --- a/monai/data/__init__.py +++ b/monai/data/__init__.py @@ -32,7 +32,7 @@ load_decathlon_properties, ) from .folder_layout import FolderLayout -from .grid_dataset import GridPatchDataset, PatchDataset, PatchIter +from .grid_dataset import GridPatchDataset, PatchDataset, PatchIter, PatchIterd from .image_dataset import ImageDataset from .image_reader import ImageReader, ITKReader, NibabelReader, NumpyReader, PILReader, WSIReader from .image_writer import ( @@ -46,6 +46,8 @@ resolve_writer, ) from .iterable_dataset import CSVIterableDataset, IterableDataset, ShuffleBuffer +from .meta_obj import MetaObj, get_track_meta, get_track_transforms, set_track_meta, set_track_transforms +from .meta_tensor import MetaTensor from .nifti_saver import NiftiSaver from .nifti_writer import write_nifti from .png_saver import PNGSaver diff --git a/monai/data/grid_dataset.py b/monai/data/grid_dataset.py index 9eb84a58c9..33497b5a68 100644 --- a/monai/data/grid_dataset.py +++ b/monai/data/grid_dataset.py @@ -9,21 +9,26 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Callable, Dict, Iterable, Optional, Sequence, Union +from copy import deepcopy +from typing import Callable, Dict, Hashable, Iterable, Mapping, Optional, Sequence, Union +import numpy as np + +from monai.config import KeysCollection from monai.data.dataset import Dataset from monai.data.iterable_dataset import IterableDataset from monai.data.utils import iter_patch from monai.transforms import apply_transform -from monai.utils import NumpyPadMode, deprecated_arg, ensure_tuple, look_up_option +from monai.utils import NumpyPadMode, deprecated_arg, ensure_tuple, first, look_up_option -__all__ = ["PatchDataset", "GridPatchDataset", "PatchIter"] +__all__ = ["PatchDataset", "GridPatchDataset", "PatchIter", "PatchIterd"] class PatchIter: """ - A class to return a patch generator with predefined properties such as `patch_size`. + Return a patch generator with predefined properties such as `patch_size`. Typically used with :py:class:`monai.data.GridPatchDataset`. + """ def __init__( @@ -42,7 +47,8 @@ def __init__( ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} One of the listed string values or a user supplied function. Defaults to ``"wrap"``. See also: https://numpy.org/doc/1.18/reference/generated/numpy.pad.html - pad_opts: padding options, see numpy.pad + pad_opts: other arguments for the `np.pad` function. + note that `np.pad` treats channel dimension as the first dimension. Note: The `patch_size` is the size of the @@ -52,19 +58,20 @@ def __init__( specified by a `patch_size` of (10, 10, 10). """ - self.patch_size = (None,) + tuple(patch_size) + self.patch_size = (None,) + tuple(patch_size) # expand to have the channel dim self.start_pos = ensure_tuple(start_pos) self.mode: NumpyPadMode = look_up_option(mode, NumpyPadMode) self.pad_opts = pad_opts - def __call__(self, array): + def __call__(self, array: np.ndarray): """ Args: array: the image to generate patches from. + """ yield from iter_patch( array, - patch_size=self.patch_size, # expand to have the channel dim + patch_size=self.patch_size, # type: ignore start_pos=self.start_pos, copy_back=False, mode=self.mode, @@ -72,17 +79,68 @@ def __call__(self, array): ) +class PatchIterd: + """ + Dictionary-based wrapper of :py:class:`monai.data.PatchIter`. + Return a patch generator for dictionary data and the coordinate, Typically used + with :py:class:`monai.data.GridPatchDataset`. + Suppose all the expected fields specified by `keys` have same shape. + + Args: + keys: keys of the corresponding items to iterate patches. + patch_size: size of patches to generate slices for, 0/None selects whole dimension + start_pos: starting position in the array, default is 0 for each dimension + mode: {``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, ``"mean"``, + ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + One of the listed string values or a user supplied function. Defaults to ``"wrap"``. + See also: https://numpy.org/doc/1.18/reference/generated/numpy.pad.html + pad_opts: other arguments for the `np.pad` function. + note that `np.pad` treats channel dimension as the first dimension. + + """ + + coords_key = "patch_coords" + original_spatial_shape_key = "original_spatial_shape" + start_pos_key = "start_pos" + + def __init__( + self, + keys: KeysCollection, + patch_size: Sequence[int], + start_pos: Sequence[int] = (), + mode: Union[NumpyPadMode, str] = NumpyPadMode.WRAP, + **pad_opts, + ): + self.keys = ensure_tuple(keys) + self.patch_iter = PatchIter(patch_size=patch_size, start_pos=start_pos, mode=mode, **pad_opts) + + def __call__(self, data: Mapping[Hashable, np.ndarray]): + d = dict(data) + original_spatial_shape = d[first(self.keys)].shape[1:] + + for patch in zip(*[self.patch_iter(d[key]) for key in self.keys]): + coords = patch[0][1] # use the coordinate of the first item + ret = {k: v[0] for k, v in zip(self.keys, patch)} + # fill in the extra keys with unmodified data + for k in set(d.keys()).difference(set(self.keys)): + ret[k] = deepcopy(d[k]) + # also store the `coordinate`, `spatial shape of original image`, `start position` in the dictionary + ret[self.coords_key] = coords + ret[self.original_spatial_shape_key] = original_spatial_shape + ret[self.start_pos_key] = self.patch_iter.start_pos + yield ret, coords + + class GridPatchDataset(IterableDataset): """ - Yields patches from images read from an image dataset. - Typically used with `PatchIter` so that the patches are chosen in a contiguous grid sampling scheme. + Yields patches from data read from an image dataset. + Typically used with `PatchIter` or `PatchIterd` so that the patches are chosen in a contiguous grid sampling scheme. .. code-block:: python import numpy as np - from monai.data import GridPatchDataset, DataLoader, PatchIter - from monai.transforms import RandShiftIntensity + from monai.data import GridPatchDataset, DataLoader, PatchIter, RandShiftIntensity # image-level dataset images = [np.arange(16, dtype=float).reshape(1, 4, 4), @@ -109,7 +167,7 @@ class GridPatchDataset(IterableDataset): data: the data source to read image data from. patch_iter: converts an input image (item from dataset) into a iterable of image patches. `patch_iter(dataset[idx])` must yield a tuple: (patches, coordinates). - see also: :py:class:`monai.data.PatchIter`. + see also: :py:class:`monai.data.PatchIter` or :py:class:`monai.data.PatchIterd`. transform: a callable data transform operates on the patches. with_coordinates: whether to yield the coordinates of each patch, default to `True`. diff --git a/monai/data/image_reader.py b/monai/data/image_reader.py index 502c6fb93b..ca77178e0b 100644 --- a/monai/data/image_reader.py +++ b/monai/data/image_reader.py @@ -820,7 +820,10 @@ def get_data( """ # Verify inputs if level is None: - level = self._check_level(img, level) + level = self.level + max_level = self._get_max_level(img) + if level > max_level: + raise ValueError(f"The maximum level of this image is {max_level} while level={level} is requested)!") # Extract a region or the entire image region = self._extract_region(img, location=location, size=size, level=level, dtype=dtype) @@ -844,21 +847,19 @@ def get_data( return patches, metadata - def _check_level(self, img, level): - level = self.level + def _get_max_level(self, img_obj): + """ + Return the maximum number of levels in the whole slide image + Args: + img: the whole slide image object - level_count = 0 + """ if self.backend == "openslide": - level_count = img.level_count - elif self.backend == "cucim": - level_count = img.resolutions["level_count"] - elif self.backend == "tifffile": - level_count = len(img.pages) - - if level > level_count - 1: - raise ValueError(f"The maximum level of this image is {level_count - 1} while level={level} is requested)!") - - return level + return img_obj.level_count - 1 + if self.backend == "cucim": + return img_obj.resolutions["level_count"] - 1 + if self.backend == "tifffile": + return len(img_obj.pages) - 1 def _get_image_size(self, img, size, level, location): """ diff --git a/monai/data/meta_obj.py b/monai/data/meta_obj.py new file mode 100644 index 0000000000..0e213f130b --- /dev/null +++ b/monai/data/meta_obj.py @@ -0,0 +1,208 @@ +# Copyright (c) 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 __future__ import annotations + +from copy import deepcopy +from typing import Any, Callable, Sequence + +_TRACK_META = True +_TRACK_TRANSFORMS = True + +__all__ = ["get_track_meta", "get_track_transforms", "set_track_meta", "set_track_transforms", "MetaObj"] + + +def set_track_meta(val: bool) -> None: + """ + Boolean to set whether metadata is tracked. If `True`, metadata will be associated + its data by using subclasses of `MetaObj`. If `False`, then data will be returned + with empty metadata. + + If both `set_track_meta` and `set_track_transforms` are set to + `False`, then standard data objects will be returned (e.g., `torch.Tensor` and + `np.ndarray`) as opposed to our enhanced objects. + + By default, this is `True`, and most users will want to leave it this way. However, + if you are experiencing any problems regarding metadata, and aren't interested in + preserving metadata, then you can disable it. + """ + global _TRACK_META + _TRACK_META = val + + +def set_track_transforms(val: bool) -> None: + """ + Boolean to set whether transforms are tracked. If `True`, applied transforms will be + associated its data by using subclasses of `MetaObj`. If `False`, then transforms + won't be tracked. + + If both `set_track_meta` and `set_track_transforms` are set to + `False`, then standard data objects will be returned (e.g., `torch.Tensor` and + `np.ndarray`) as opposed to our enhanced objects. + + By default, this is `True`, and most users will want to leave it this way. However, + if you are experiencing any problems regarding transforms, and aren't interested in + preserving transforms, then you can disable it. + """ + global _TRACK_TRANSFORMS + _TRACK_TRANSFORMS = val + + +def get_track_meta() -> bool: + """ + Return the boolean as to whether metadata is tracked. If `True`, metadata will be + associated its data by using subclasses of `MetaObj`. If `False`, then data will be + returned with empty metadata. + + If both `set_track_meta` and `set_track_transforms` are set to + `False`, then standard data objects will be returned (e.g., `torch.Tensor` and + `np.ndarray`) as opposed to our enhanced objects. + + By default, this is `True`, and most users will want to leave it this way. However, + if you are experiencing any problems regarding metadata, and aren't interested in + preserving metadata, then you can disable it. + """ + return _TRACK_META + + +def get_track_transforms() -> bool: + """ + Return the boolean as to whether transforms are tracked. If `True`, applied + transforms will be associated its data by using subclasses of `MetaObj`. If `False`, + then transforms won't be tracked. + + If both `set_track_meta` and `set_track_transforms` are set to + `False`, then standard data objects will be returned (e.g., `torch.Tensor` and + `np.ndarray`) as opposed to our enhanced objects. + + By default, this is `True`, and most users will want to leave it this way. However, + if you are experiencing any problems regarding transforms, and aren't interested in + preserving transforms, then you can disable it. + """ + return _TRACK_TRANSFORMS + + +class MetaObj: + """ + Abstract base class that stores data as well as any extra metadata. + + This allows for subclassing `torch.Tensor` and `np.ndarray` through multiple + inheritance. + + Metadata is stored in the form of a dictionary. + + Behavior should be the same as extended class (e.g., `torch.Tensor` or `np.ndarray`) + aside from the extended meta functionality. + + Copying of information: + + * For `c = a + b`, then auxiliary data (e.g., metadata) will be copied from the + first instance of `MetaObj`. + + """ + + def __init__(self): + self._meta: dict = self.get_default_meta() + + @staticmethod + def flatten_meta_objs(args: Sequence[Any]) -> list[MetaObj]: + """ + Recursively flatten input and return all instances of `MetaObj` as a single + list. This means that for both `torch.add(a, b)`, `torch.stack([a, b])` (and + their numpy equivalents), we return `[a, b]` if both `a` and `b` are of type + `MetaObj`. + + Args: + args: Sequence of inputs to be flattened. + Returns: + list of nested `MetaObj` from input. + """ + out = [] + for a in args: + if isinstance(a, (list, tuple)): + out += MetaObj.flatten_meta_objs(a) + elif isinstance(a, MetaObj): + out.append(a) + return out + + def _copy_attr(self, attribute: str, input_objs: list[MetaObj], default_fn: Callable, deep_copy: bool) -> None: + """ + Copy an attribute from the first in a list of `MetaObj`. In the case of + `torch.add(a, b)`, both `a` and `b` could be `MetaObj` or something else, so + check them all. Copy the first to `self`. + + We also perform a deep copy of the data if desired. + + Args: + attribute: string corresponding to attribute to be copied (e.g., `meta`). + input_objs: List of `MetaObj`. We'll copy the attribute from the first one + that contains that particular attribute. + default_fn: If none of `input_objs` have the attribute that we're + interested in, then use this default function (e.g., `lambda: {}`.) + deep_copy: Should the attribute be deep copied? See `_copy_meta`. + + Returns: + Returns `None`, but `self` should be updated to have the copied attribute. + """ + attributes = [getattr(i, attribute) for i in input_objs] + if len(attributes) > 0: + val = attributes[0] + if deep_copy: + val = deepcopy(val) + setattr(self, attribute, val) + else: + setattr(self, attribute, default_fn()) + + def _copy_meta(self, input_objs: list[MetaObj]) -> None: + """ + Copy metadata from a list of `MetaObj`. For a given attribute, we copy the + adjunct data from the first element in the list containing that attribute. + + If there has been a change in `id` (e.g., `a=b+c`), then deepcopy. Else (e.g., + `a+=1`), then don't. + + Args: + input_objs: list of `MetaObj` to copy data from. + + """ + id_in = id(input_objs[0]) if len(input_objs) > 0 else None + deep_copy = id(self) != id_in + self._copy_attr("meta", input_objs, self.get_default_meta, deep_copy) + + def get_default_meta(self) -> dict: + """Get the default meta. + + Returns: + default metadata. + """ + return {} + + def __repr__(self) -> str: + """String representation of class.""" + out: str = super().__repr__() + + out += "\nMetaData\n" + if self.meta is not None: + out += "".join(f"\t{k}: {v}\n" for k, v in self.meta.items()) + else: + out += "None" + + return out + + @property + def meta(self) -> dict: + """Get the meta.""" + return self._meta + + @meta.setter + def meta(self, d: dict) -> None: + """Set the meta.""" + self._meta = d diff --git a/monai/data/meta_tensor.py b/monai/data/meta_tensor.py new file mode 100644 index 0000000000..ba80f93e74 --- /dev/null +++ b/monai/data/meta_tensor.py @@ -0,0 +1,152 @@ +# Copyright (c) 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 __future__ import annotations + +import warnings +from copy import deepcopy +from typing import Callable + +import torch + +from monai.data.meta_obj import MetaObj, get_track_meta, get_track_transforms +from monai.utils.enums import PostFix + +__all__ = ["MetaTensor"] + + +class MetaTensor(MetaObj, torch.Tensor): + """ + Class that inherits from both `torch.Tensor` and `MetaObj`, adding support for metadata. + + Metadata is stored in the form of a dictionary. Nested, an affine matrix will be + stored. This should be in the form of `torch.Tensor`. + + Behavior should be the same as `torch.Tensor` aside from the extended + meta functionality. + + Copying of information: + + * For `c = a + b`, then auxiliary data (e.g., metadata) will be copied from the + first instance of `MetaTensor`. + + Example: + .. code-block:: python + + import torch + from monai.data import MetaTensor + + t = torch.tensor([1,2,3]) + affine = torch.eye(4) * 100 + meta = {"some": "info"} + m = MetaTensor(t, affine=affine, meta=meta) + m2 = m+m + assert isinstance(m2, MetaTensor) + assert m2.meta["some"] == "info" + assert m2.affine == affine + + Notes: + - Older versions of pytorch (<=1.8), `torch.jit.trace(net, im)` may + not work if `im` is of type `MetaTensor`. This can be resolved with + `torch.jit.trace(net, im.as_tensor())`. + - A warning will be raised if in the constructor `affine` is not `None` and + `meta` already contains the key `affine`. + """ + + @staticmethod + def __new__(cls, x, affine: torch.Tensor | None = None, meta: dict | None = None, *args, **kwargs) -> MetaTensor: + return torch.as_tensor(x, *args, **kwargs).as_subclass(cls) # type: ignore + + def __init__(self, x, affine: torch.Tensor | None = None, meta: dict | None = None) -> None: + """ + If `meta` is given, use it. Else, if `meta` exists in the input tensor, use it. + Else, use the default value. Similar for the affine, except this could come from + four places. + Priority: `affine`, `meta["affine"]`, `x.affine`, `get_default_affine`. + """ + super().__init__() + # set meta + if meta is not None: + self.meta = meta + elif isinstance(x, MetaObj): + self.meta = x.meta + # set the affine + if affine is not None: + if "affine" in self.meta: + warnings.warn("Setting affine, but the applied meta contains an affine. This will be overwritten.") + self.affine = affine + elif "affine" in self.meta: + pass # nothing to do + elif isinstance(x, MetaTensor): + self.affine = x.affine + else: + self.affine = self.get_default_affine() + + # if we are creating a new MetaTensor, then deep copy attributes + if isinstance(x, torch.Tensor) and not isinstance(x, MetaTensor): + self.meta = deepcopy(self.meta) + self.affine = self.affine.to(self.device) + + def _copy_attr(self, attribute: str, input_objs: list[MetaObj], default_fn: Callable, deep_copy: bool) -> None: + super()._copy_attr(attribute, input_objs, default_fn, deep_copy) + val = getattr(self, attribute) + if isinstance(val, torch.Tensor): + setattr(self, attribute, val.to(self.device)) + + @classmethod + def __torch_function__(cls, func, types, args=(), kwargs=None) -> torch.Tensor: + """Wraps all torch functions.""" + if kwargs is None: + kwargs = {} + ret: MetaTensor = super().__torch_function__(func, types, args, kwargs) + # e.g., __repr__ returns a string + if not isinstance(ret, torch.Tensor): + return ret + if not (get_track_meta() or get_track_transforms()): + return ret.as_tensor() + meta_args = MetaObj.flatten_meta_objs(list(args) + list(kwargs.values())) + ret._copy_meta(meta_args) + ret.affine = ret.affine.to(ret.device) + return ret + + def get_default_affine(self, dtype=torch.float64) -> torch.Tensor: + return torch.eye(4, device=self.device, dtype=dtype) + + def as_tensor(self) -> torch.Tensor: + """ + Return the `MetaTensor` as a `torch.Tensor`. + It is OS dependent as to whether this will be a deep copy or not. + """ + return self.as_subclass(torch.Tensor) # type: ignore + + def as_dict(self, key: str) -> dict: + """ + Get the object as a dictionary for backwards compatibility. + + Args: + key: Base key to store main data. The key for the metadata will be + determined using `PostFix.meta`. + + Return: + A dictionary consisting of two keys, the main data (stored under `key`) and + the metadata. + """ + return {key: self.as_tensor(), PostFix.meta(key): self.meta} + + @property + def affine(self) -> torch.Tensor: + """Get the affine.""" + return self.meta["affine"] # type: ignore + + @affine.setter + def affine(self, d: torch.Tensor) -> None: + """Set the affine.""" + self.meta["affine"] = d diff --git a/monai/data/torchscript_utils.py b/monai/data/torchscript_utils.py index 61477e8ca9..ca46dd9dc4 100644 --- a/monai/data/torchscript_utils.py +++ b/monai/data/torchscript_utils.py @@ -18,7 +18,6 @@ from monai.config import get_config_values from monai.utils import JITMetadataKeys -from monai.utils.module import pytorch_after METADATA_FILENAME = "metadata.json" @@ -80,19 +79,10 @@ def save_net_with_metadata( json_data = json.dumps(metadict) - # Pytorch>1.6 can use dictionaries directly, otherwise need to use special map object - if pytorch_after(1, 7): - extra_files = {METADATA_FILENAME: json_data.encode()} + extra_files = {METADATA_FILENAME: json_data.encode()} - if more_extra_files is not None: - extra_files.update(more_extra_files) - else: - extra_files = torch._C.ExtraFilesMap() # type:ignore[attr-defined] - extra_files[METADATA_FILENAME] = json_data.encode() - - if more_extra_files is not None: - for k, v in more_extra_files.items(): - extra_files[k] = v + if more_extra_files is not None: + extra_files.update(more_extra_files) if isinstance(filename_prefix_or_stream, str): filename_no_ext, ext = os.path.splitext(filename_prefix_or_stream) @@ -123,16 +113,8 @@ def load_net_with_metadata( Returns: Triple containing loaded object, metadata dict, and extra files dict containing other file data if present """ - # Pytorch>1.6 can use dictionaries directly, otherwise need to use special map object - if pytorch_after(1, 7): - extra_files = {f: "" for f in more_extra_files} - extra_files[METADATA_FILENAME] = "" - else: - extra_files = torch._C.ExtraFilesMap() # type:ignore[attr-defined] - extra_files[METADATA_FILENAME] = "" - - for f in more_extra_files: - extra_files[f] = "" + extra_files = {f: "" for f in more_extra_files} + extra_files[METADATA_FILENAME] = "" jit_obj = torch.jit.load(filename_prefix_or_stream, map_location, extra_files) diff --git a/monai/engines/evaluator.py b/monai/engines/evaluator.py index c3e8c456b7..f9dab35450 100644 --- a/monai/engines/evaluator.py +++ b/monai/engines/evaluator.py @@ -74,6 +74,8 @@ class Evaluator(Workflow): decollate: whether to decollate the batch-first data to a list of data after model computation, recommend `decollate=True` when `postprocessing` uses components from `monai.transforms`. default to `True`. + to_kwargs: dict of other args for `prepare_batch` API when converting the input data, except for + `device`, `non_blocking`. """ @@ -95,6 +97,7 @@ def __init__( event_names: Optional[List[Union[str, EventEnum]]] = None, event_to_attr: Optional[dict] = None, decollate: bool = True, + to_kwargs: Optional[Dict] = None, ) -> None: super().__init__( device=device, @@ -113,6 +116,7 @@ def __init__( event_names=event_names, event_to_attr=event_to_attr, decollate=decollate, + to_kwargs=to_kwargs, ) mode = look_up_option(mode, ForwardMode) if mode == ForwardMode.EVAL: @@ -181,6 +185,8 @@ class SupervisedEvaluator(Evaluator): decollate: whether to decollate the batch-first data to a list of data after model computation, recommend `decollate=True` when `postprocessing` uses components from `monai.transforms`. default to `True`. + to_kwargs: dict of other args for `prepare_batch` API when converting the input data, except for + `device`, `non_blocking`. """ @@ -204,6 +210,7 @@ def __init__( event_names: Optional[List[Union[str, EventEnum]]] = None, event_to_attr: Optional[dict] = None, decollate: bool = True, + to_kwargs: Optional[Dict] = None, ) -> None: super().__init__( device=device, @@ -222,6 +229,7 @@ def __init__( event_names=event_names, event_to_attr=event_to_attr, decollate=decollate, + to_kwargs=to_kwargs, ) self.network = network @@ -245,7 +253,9 @@ def _iteration(self, engine: Engine, batchdata: Dict[str, torch.Tensor]): """ if batchdata is None: raise ValueError("Must provide batch data for current iteration.") - batch = self.prepare_batch(batchdata, engine.state.device, engine.non_blocking) # type: ignore + batch = self.prepare_batch( + batchdata, engine.state.device, engine.non_blocking, **engine.to_kwargs # type: ignore + ) if len(batch) == 2: inputs, targets = batch args: Tuple = () @@ -314,6 +324,8 @@ class EnsembleEvaluator(Evaluator): decollate: whether to decollate the batch-first data to a list of data after model computation, recommend `decollate=True` when `postprocessing` uses components from `monai.transforms`. default to `True`. + to_kwargs: dict of other args for `prepare_batch` API when converting the input data, except for + `device`, `non_blocking`. """ @@ -338,6 +350,7 @@ def __init__( event_names: Optional[List[Union[str, EventEnum]]] = None, event_to_attr: Optional[dict] = None, decollate: bool = True, + to_kwargs: Optional[Dict] = None, ) -> None: super().__init__( device=device, @@ -356,6 +369,7 @@ def __init__( event_names=event_names, event_to_attr=event_to_attr, decollate=decollate, + to_kwargs=to_kwargs, ) self.networks = ensure_tuple(networks) @@ -387,7 +401,9 @@ def _iteration(self, engine: Engine, batchdata: Dict[str, torch.Tensor]): """ if batchdata is None: raise ValueError("Must provide batch data for current iteration.") - batch = self.prepare_batch(batchdata, engine.state.device, engine.non_blocking) # type: ignore + batch = self.prepare_batch( + batchdata, engine.state.device, engine.non_blocking, **engine.to_kwargs # type: ignore + ) if len(batch) == 2: inputs, targets = batch args: Tuple = () diff --git a/monai/engines/trainer.py b/monai/engines/trainer.py index 774e535e7f..16c50d4fa2 100644 --- a/monai/engines/trainer.py +++ b/monai/engines/trainer.py @@ -26,7 +26,7 @@ from monai.engines.workflow import Workflow from monai.inferers import Inferer, SimpleInferer from monai.transforms import Transform -from monai.utils import min_version, optional_import, pytorch_after +from monai.utils import min_version, optional_import from monai.utils.enums import CommonKeys as Keys if TYPE_CHECKING: @@ -105,6 +105,8 @@ class SupervisedTrainer(Trainer): default to `True`. optim_set_to_none: when calling `optimizer.zero_grad()`, instead of setting to zero, set the grads to None. more details: https://pytorch.org/docs/stable/generated/torch.optim.Optimizer.zero_grad.html. + to_kwargs: dict of other args for `prepare_batch` API when converting the input data, except for + `device`, `non_blocking`. """ @@ -131,6 +133,7 @@ def __init__( event_to_attr: Optional[dict] = None, decollate: bool = True, optim_set_to_none: bool = False, + to_kwargs: Optional[Dict] = None, ) -> None: super().__init__( device=device, @@ -149,6 +152,7 @@ def __init__( event_names=event_names, event_to_attr=event_to_attr, decollate=decollate, + to_kwargs=to_kwargs, ) self.network = network @@ -176,7 +180,9 @@ def _iteration(self, engine: Engine, batchdata: Dict[str, torch.Tensor]): """ if batchdata is None: raise ValueError("Must provide batch data for current iteration.") - batch = self.prepare_batch(batchdata, engine.state.device, engine.non_blocking) # type: ignore + batch = self.prepare_batch( + batchdata, engine.state.device, engine.non_blocking, **engine.to_kwargs # type: ignore + ) if len(batch) == 2: inputs, targets = batch args: Tuple = () @@ -193,11 +199,7 @@ def _compute_pred_loss(): engine.fire_event(IterationEvents.LOSS_COMPLETED) self.network.train() - # `set_to_none` only work from PyTorch 1.7.0 - if not pytorch_after(1, 7): - self.optimizer.zero_grad() - else: - self.optimizer.zero_grad(set_to_none=self.optim_set_to_none) + self.optimizer.zero_grad(set_to_none=self.optim_set_to_none) if self.amp and self.scaler is not None: with torch.cuda.amp.autocast(): @@ -271,6 +273,8 @@ class GanTrainer(Trainer): default to `True`. optim_set_to_none: when calling `optimizer.zero_grad()`, instead of setting to zero, set the grads to None. more details: https://pytorch.org/docs/stable/generated/torch.optim.Optimizer.zero_grad.html. + to_kwargs: dict of other args for `prepare_batch` API when converting the input data, except for + `device`, `non_blocking`. """ @@ -302,6 +306,7 @@ def __init__( train_handlers: Optional[Sequence] = None, decollate: bool = True, optim_set_to_none: bool = False, + to_kwargs: Optional[Dict] = None, ): if not isinstance(train_data_loader, DataLoader): raise ValueError("train_data_loader must be PyTorch DataLoader.") @@ -321,6 +326,7 @@ def __init__( handlers=train_handlers, postprocessing=postprocessing, decollate=decollate, + to_kwargs=to_kwargs, ) self.g_network = g_network self.g_optimizer = g_optimizer @@ -353,24 +359,23 @@ def _iteration( if batchdata is None: raise ValueError("must provide batch data for current iteration.") - d_input = self.prepare_batch(batchdata, engine.state.device, engine.non_blocking) # type: ignore + d_input = self.prepare_batch( + batchdata, engine.state.device, engine.non_blocking, **engine.to_kwargs # type: ignore + ) batch_size = self.data_loader.batch_size # type: ignore g_input = self.g_prepare_batch( num_latents=batch_size, latent_size=self.latent_shape, device=engine.state.device, # type: ignore non_blocking=engine.non_blocking, # type: ignore + **engine.to_kwargs, # type: ignore ) g_output = self.g_inferer(g_input, self.g_network) # Train Discriminator d_total_loss = torch.zeros(1) for _ in range(self.d_train_steps): - # `set_to_none` only work from PyTorch 1.7.0 - if not pytorch_after(1, 7): - self.d_optimizer.zero_grad() - else: - self.d_optimizer.zero_grad(set_to_none=self.optim_set_to_none) + self.d_optimizer.zero_grad(set_to_none=self.optim_set_to_none) dloss = self.d_loss_function(g_output, d_input) dloss.backward() self.d_optimizer.step() @@ -383,12 +388,10 @@ def _iteration( latent_size=self.latent_shape, device=engine.state.device, # type: ignore non_blocking=engine.non_blocking, # type: ignore + **engine.to_kwargs, # type: ignore ) g_output = self.g_inferer(g_input, self.g_network) - if not pytorch_after(1, 7): - self.g_optimizer.zero_grad() - else: - self.g_optimizer.zero_grad(set_to_none=self.optim_set_to_none) + self.g_optimizer.zero_grad(set_to_none=self.optim_set_to_none) g_loss = self.g_loss_function(g_output) g_loss.backward() self.g_optimizer.step() diff --git a/monai/engines/utils.py b/monai/engines/utils.py index 726dfc8e98..8f3a57beda 100644 --- a/monai/engines/utils.py +++ b/monai/engines/utils.py @@ -104,12 +104,16 @@ def get_devices_spec(devices: Optional[Sequence[torch.device]] = None) -> List[t def default_prepare_batch( - batchdata: Dict[str, torch.Tensor], device: Optional[Union[str, torch.device]] = None, non_blocking: bool = False + batchdata: Dict[str, torch.Tensor], + device: Optional[Union[str, torch.device]] = None, + non_blocking: bool = False, + **kwargs, ) -> Union[Tuple[torch.Tensor, Optional[torch.Tensor]], torch.Tensor]: """ Default function to prepare the data for current iteration. - Refer to ignite: https://pytorch.org/ignite/v0.4.5/generated/ignite.engine.create_supervised_trainer.html - #ignite.engine.create_supervised_trainer. + Args `batchdata`, `device`, `non_blocking` refer to the ignite API: + https://pytorch.org/ignite/v0.4.8/generated/ignite.engine.create_supervised_trainer.html. + `kwargs` supports other args for `Tensor.to()` API. Returns: image, label(optional). @@ -119,18 +123,21 @@ def default_prepare_batch( raise AssertionError("default prepare_batch expects dictionary input data.") if isinstance(batchdata.get(CommonKeys.LABEL), torch.Tensor): return ( - batchdata[CommonKeys.IMAGE].to(device=device, non_blocking=non_blocking), - batchdata[CommonKeys.LABEL].to(device=device, non_blocking=non_blocking), + batchdata[CommonKeys.IMAGE].to(device=device, non_blocking=non_blocking, **kwargs), + batchdata[CommonKeys.LABEL].to(device=device, non_blocking=non_blocking, **kwargs), ) if GanKeys.REALS in batchdata: - return batchdata[GanKeys.REALS].to(device=device, non_blocking=non_blocking) - return batchdata[CommonKeys.IMAGE].to(device=device, non_blocking=non_blocking), None + return batchdata[GanKeys.REALS].to(device=device, non_blocking=non_blocking, **kwargs) + return batchdata[CommonKeys.IMAGE].to(device=device, non_blocking=non_blocking, **kwargs), None class PrepareBatch(ABC): """ Interface of customized prepare_batch in the trainer or evaluator workflows. It takes the data of current batch, target device and non_blocking flag as input. + Args `batchdata`, `device`, `non_blocking` refer to the ignite API: + https://pytorch.org/ignite/v0.4.8/generated/ignite.engine.create_supervised_trainer.html. + `kwargs` supports other args for `Tensor.to()` API. """ @@ -140,6 +147,7 @@ def __call__( batchdata: Dict[str, torch.Tensor], device: Optional[Union[str, torch.device]] = None, non_blocking: bool = False, + **kwargs, ): raise NotImplementedError(f"Subclass {self.__class__.__name__} must implement this method.") @@ -155,8 +163,15 @@ def __call__( batchdata: Dict[str, torch.Tensor], device: Optional[Union[str, torch.device]] = None, non_blocking: bool = False, + **kwargs, ): - return default_prepare_batch(batchdata, device, non_blocking) + """ + Args `batchdata`, `device`, `non_blocking` refer to the ignite API: + https://pytorch.org/ignite/v0.4.8/generated/ignite.engine.create_supervised_trainer.html. + `kwargs` supports other args for `Tensor.to()` API. + + """ + return default_prepare_batch(batchdata, device, non_blocking, **kwargs) class PrepareBatchExtraInput(PrepareBatch): @@ -181,29 +196,42 @@ def __call__( batchdata: Dict[str, torch.Tensor], device: Optional[Union[str, torch.device]] = None, non_blocking: bool = False, + **kwargs, ): - image, label = default_prepare_batch(batchdata, device, non_blocking) - args = list() - kwargs = dict() + """ + Args `batchdata`, `device`, `non_blocking` refer to the ignite API: + https://pytorch.org/ignite/v0.4.8/generated/ignite.engine.create_supervised_trainer.html. + `kwargs` supports other args for `Tensor.to()` API. + + """ + image, label = default_prepare_batch(batchdata, device, non_blocking, **kwargs) + args_ = list() + kwargs_ = dict() def _get_data(key: str): data = batchdata[key] - return data.to(device=device, non_blocking=non_blocking) if isinstance(data, torch.Tensor) else data + return ( + data.to(device=device, non_blocking=non_blocking, **kwargs) if isinstance(data, torch.Tensor) else data + ) if isinstance(self.extra_keys, (str, list, tuple)): for k in ensure_tuple(self.extra_keys): - args.append(_get_data(k)) + args_.append(_get_data(k)) elif isinstance(self.extra_keys, dict): for k, v in self.extra_keys.items(): - kwargs.update({k: _get_data(v)}) + kwargs_.update({k: _get_data(v)}) - return image, label, tuple(args), kwargs + return image, label, tuple(args_), kwargs_ def default_make_latent( - num_latents: int, latent_size: int, device: Optional[Union[str, torch.device]] = None, non_blocking: bool = False + num_latents: int, + latent_size: int, + device: Optional[Union[str, torch.device]] = None, + non_blocking: bool = False, + **kwargs, ) -> torch.Tensor: - return torch.randn(num_latents, latent_size).to(device=device, non_blocking=non_blocking) + return torch.randn(num_latents, latent_size).to(device=device, non_blocking=non_blocking, **kwargs) def engine_apply_transform(batch: Any, output: Any, transform: Callable[..., Dict]): diff --git a/monai/engines/workflow.py b/monai/engines/workflow.py index 65bb313e53..4ea0a69d55 100644 --- a/monai/engines/workflow.py +++ b/monai/engines/workflow.py @@ -94,6 +94,8 @@ class Workflow(IgniteEngine): # type: ignore[valid-type, misc] # due to optiona decollate: whether to decollate the batch-first data to a list of data after model computation, recommend `decollate=True` when `postprocessing` uses components from `monai.transforms`. default to `True`. + to_kwargs: dict of other args for `prepare_batch` API when converting the input data, except for + `device`, `non_blocking`. Raises: TypeError: When ``device`` is not a ``torch.Device``. @@ -121,6 +123,7 @@ def __init__( event_names: Optional[List[Union[str, EventEnum]]] = None, event_to_attr: Optional[dict] = None, decollate: bool = True, + to_kwargs: Optional[Dict] = None, ) -> None: if iteration_update is not None: super().__init__(iteration_update) @@ -166,6 +169,7 @@ def set_sampler_epoch(engine: Engine): self.prepare_batch = prepare_batch self.metric_cmp_fn = metric_cmp_fn self.amp = amp + self.to_kwargs = {} if to_kwargs is None else to_kwargs self.scaler: Optional[torch.cuda.amp.GradScaler] = None if event_names is None: diff --git a/monai/metrics/__init__.py b/monai/metrics/__init__.py index d18c20f7b2..53d11893ed 100644 --- a/monai/metrics/__init__.py +++ b/monai/metrics/__init__.py @@ -17,5 +17,6 @@ from .metric import Cumulative, CumulativeIterationMetric, IterationMetric, Metric from .regression import MAEMetric, MSEMetric, PSNRMetric, RMSEMetric from .rocauc import ROCAUCMetric, compute_roc_auc +from .surface_dice import SurfaceDiceMetric, compute_surface_dice from .surface_distance import SurfaceDistanceMetric, compute_average_surface_distance from .utils import do_metric_reduction, get_mask_edges, get_surface_distance, ignore_background diff --git a/monai/metrics/surface_dice.py b/monai/metrics/surface_dice.py new file mode 100644 index 0000000000..5630af178d --- /dev/null +++ b/monai/metrics/surface_dice.py @@ -0,0 +1,236 @@ +# Copyright (c) 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 warnings +from typing import List, Union + +import numpy as np +import torch + +from monai.metrics.utils import do_metric_reduction, get_mask_edges, get_surface_distance, ignore_background +from monai.utils import MetricReduction, convert_data_type + +from .metric import CumulativeIterationMetric + + +class SurfaceDiceMetric(CumulativeIterationMetric): + """ + Computes the Normalized Surface Distance (NSD) for each batch sample and class of + predicted segmentations `y_pred` and corresponding reference segmentations `y` according to equation :eq:`nsd`. + This implementation supports 2D images. For 3D images, please refer to DeepMind's implementation + https://github.com/deepmind/surface-distance. + + The class- and batch sample-wise NSD values can be aggregated with the function `aggregate`. + + Args: + class_thresholds: List of class-specific thresholds. + The thresholds relate to the acceptable amount of deviation in the segmentation boundary in pixels. + Each threshold needs to be a finite, non-negative number. + include_background: Whether to skip NSD computation on the first channel of the predicted output. + Defaults to ``False``. + distance_metric: The metric used to compute surface distances. + One of [``"euclidean"``, ``"chessboard"``, ``"taxicab"``]. + Defaults to ``"euclidean"``. + reduction: The mode to aggregate metrics. + One of [``"mean"``, ``"sum"``, ``"mean_batch"``, ``"sum_batch"``, ``"mean_channel"``, ``"sum_channel"``, + ``"none"``]. + Defaults to ``"mean"``. + If ``"none"`` is chosen, no aggregation will be performed. + The aggregation will ignore nan values. + get_not_nans: whether to return the `not_nans` count. + Defaults to ``False``. + `not_nans` is the number of batch samples for which not all class-specific NSD values were nan values. + If set to ``True``, the function `aggregate` will return both the aggregated NSD and the `not_nans` count. + If set to ``False``, `aggregate` will only return the aggregated NSD. + """ + + def __init__( + self, + class_thresholds: List[float], + include_background: bool = False, + distance_metric: str = "euclidean", + reduction: Union[MetricReduction, str] = MetricReduction.MEAN, + get_not_nans: bool = False, + ) -> None: + super().__init__() + self.class_thresholds = class_thresholds + self.include_background = include_background + self.distance_metric = distance_metric + self.reduction = reduction + self.get_not_nans = get_not_nans + + def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor): # type: ignore + r""" + Args: + y_pred: Predicted segmentation, typically segmentation model output. + It must be a one-hot encoded, batch-first tensor [B,C,H,W]. + y: Reference segmentation. + It must be a one-hot encoded, batch-first tensor [B,C,H,W]. + + Returns: + Pytorch Tensor of shape [B,C], containing the NSD values :math:`\operatorname {NSD}_{b,c}` for each batch + index :math:`b` and class :math:`c`. + """ + return compute_surface_dice( + y_pred=y_pred, + y=y, + class_thresholds=self.class_thresholds, + include_background=self.include_background, + distance_metric=self.distance_metric, + ) + + def aggregate(self): + r""" + Aggregates the output of `_compute_tensor`. + + Returns: + If `get_not_nans` is set to ``True``, this function returns the aggregated NSD and the `not_nans` count. + If `get_not_nans` is set to ``False``, this function returns only the aggregated NSD. + """ + data = self.get_buffer() + if not isinstance(data, torch.Tensor): + raise ValueError("the data to aggregate must be PyTorch Tensor.") + + # do metric reduction + f, not_nans = do_metric_reduction(data, self.reduction) + return (f, not_nans) if self.get_not_nans else f + + +def compute_surface_dice( + y_pred: torch.Tensor, + y: torch.Tensor, + class_thresholds: List[float], + include_background: bool = False, + distance_metric: str = "euclidean", +): + r""" + This function computes the (Normalized) Surface Dice (NSD) between the two tensors `y_pred` (referred to as + :math:`\hat{Y}`) and `y` (referred to as :math:`Y`). This metric determines which fraction of a segmentation + boundary is correctly predicted. A boundary element is considered correctly predicted if the closest distance to the + reference boundary is smaller than or equal to the specified threshold related to the acceptable amount of deviation in + pixels. The NSD is bounded between 0 and 1. + + This implementation supports multi-class tasks with an individual threshold :math:`\tau_c` for each class :math:`c`. + The class-specific NSD for batch index :math:`b`, :math:`\operatorname {NSD}_{b,c}`, is computed using the function: + + .. math:: + \operatorname {NSD}_{b,c} \left(Y_{b,c}, \hat{Y}_{b,c}\right) = \frac{\left|\mathcal{D}_{Y_{b,c}}^{'}\right| + + \left| \mathcal{D}_{\hat{Y}_{b,c}}^{'} \right|}{\left|\mathcal{D}_{Y_{b,c}}\right| + + \left|\mathcal{D}_{\hat{Y}_{b,c}}\right|} + :label: nsd + + with :math:`\mathcal{D}_{Y_{b,c}}` and :math:`\mathcal{D}_{\hat{Y}_{b,c}}` being two sets of nearest-neighbor + distances. :math:`\mathcal{D}_{Y_{b,c}}` is computed from the predicted segmentation boundary towards the reference segmentation + boundary and vice-versa for :math:`\mathcal{D}_{\hat{Y}_{b,c}}`. :math:`\mathcal{D}_{Y_{b,c}}^{'}` and + :math:`\mathcal{D}_{\hat{Y}_{b,c}}^{'}` refer to the subsets of distances that are smaller or equal to the + acceptable distance :math:`\tau_c`: + + .. math:: + \mathcal{D}_{Y_{b,c}}^{'} = \{ d \in \mathcal{D}_{Y_{b,c}} \, | \, d \leq \tau_c \}. + + + In the case of a class neither being present in the predicted segmentation, nor in the reference segmentation, a nan value + will be returned for this class. In the case of a class being present in only one of predicted segmentation or + reference segmentation, the class NSD will be 0. + + This implementation is based on https://arxiv.org/abs/2111.05408 and supports 2D images. + Be aware that the computation of boundaries is different from DeepMind's implementation + https://github.com/deepmind/surface-distance. In this implementation, the length of a segmentation boundary is + interpreted as the number of its edge pixels. In DeepMind's implementation, the length of a segmentation boundary + depends on the local neighborhood (cf. https://arxiv.org/abs/1809.04430). + + Args: + y_pred: Predicted segmentation, typically segmentation model output. + It must be a one-hot encoded, batch-first tensor [B,C,H,W]. + y: Reference segmentation. + It must be a one-hot encoded, batch-first tensor [B,C,H,W]. + class_thresholds: List of class-specific thresholds. + The thresholds relate to the acceptable amount of deviation in the segmentation boundary in pixels. + Each threshold needs to be a finite, non-negative number. + include_background: Whether to skip the surface dice computation on the first channel of + the predicted output. Defaults to ``False``. + distance_metric: The metric used to compute surface distances. + One of [``"euclidean"``, ``"chessboard"``, ``"taxicab"``]. + Defaults to ``"euclidean"``. + + Raises: + ValueError: If `y_pred` and/or `y` are not PyTorch tensors. + ValueError: If `y_pred` and/or `y` do not have four dimensions. + ValueError: If `y_pred` and/or `y` have different shapes. + ValueError: If `y_pred` and/or `y` are not one-hot encoded + ValueError: If the number of channels of `y_pred` and/or `y` is different from the number of class thresholds. + ValueError: If any class threshold is not finite. + ValueError: If any class threshold is negative. + + Returns: + Pytorch Tensor of shape [B,C], containing the NSD values :math:`\operatorname {NSD}_{b,c}` for each batch index + :math:`b` and class :math:`c`. + """ + + if not include_background: + y_pred, y = ignore_background(y_pred=y_pred, y=y) + + if not isinstance(y_pred, torch.Tensor) or not isinstance(y, torch.Tensor): + raise ValueError("y_pred and y must be PyTorch Tensor.") + + if y_pred.ndimension() != 4 or y.ndimension() != 4: + raise ValueError("y_pred and y should have four dimensions: [B,C,H,W].") + + if y_pred.shape != y.shape: + raise ValueError( + f"y_pred and y should have same shape, but instead, shapes are {y_pred.shape} (y_pred) and {y.shape} (y)." + ) + + if not torch.all(y_pred.byte() == y_pred) or not torch.all(y.byte() == y): + raise ValueError("y_pred and y should be binarized tensors (e.g. torch.int64).") + if torch.any(y_pred > 1) or torch.any(y > 1): + raise ValueError("y_pred and y should be one-hot encoded.") + + y = y.float() + y_pred = y_pred.float() + + batch_size, n_class = y_pred.shape[:2] + + if n_class != len(class_thresholds): + raise ValueError( + f"number of classes ({n_class}) does not match number of class thresholds ({len(class_thresholds)})." + ) + + if any(~np.isfinite(class_thresholds)): + raise ValueError("All class thresholds need to be finite.") + + if any(np.array(class_thresholds) < 0): + raise ValueError("All class thresholds need to be >= 0.") + + nsd = np.empty((batch_size, n_class)) + + for b, c in np.ndindex(batch_size, n_class): + (edges_pred, edges_gt) = get_mask_edges(y_pred[b, c], y[b, c], crop=False) + if not np.any(edges_gt): + warnings.warn(f"the ground truth of class {c} is all 0, this may result in nan/inf distance.") + if not np.any(edges_pred): + warnings.warn(f"the prediction of class {c} is all 0, this may result in nan/inf distance.") + + distances_pred_gt = get_surface_distance(edges_pred, edges_gt, distance_metric=distance_metric) + distances_gt_pred = get_surface_distance(edges_gt, edges_pred, distance_metric=distance_metric) + + boundary_complete = len(distances_pred_gt) + len(distances_gt_pred) + boundary_correct = np.sum(distances_pred_gt <= class_thresholds[c]) + np.sum( + distances_gt_pred <= class_thresholds[c] + ) + + if boundary_complete == 0: + # the class is neither present in the prediction, nor in the reference segmentation + nsd[b, c] = np.nan + else: + nsd[b, c] = boundary_correct / boundary_complete + + return convert_data_type(nsd, torch.Tensor)[0] diff --git a/monai/networks/blocks/mlp.py b/monai/networks/blocks/mlp.py index a1728365cf..0feeb044f3 100644 --- a/monai/networks/blocks/mlp.py +++ b/monai/networks/blocks/mlp.py @@ -9,8 +9,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Tuple, Union + import torch.nn as nn +from monai.networks.layers import get_act_layer +from monai.utils import look_up_option + +SUPPORTED_DROPOUT_MODE = {"vit", "swin"} + class MLPBlock(nn.Module): """ @@ -18,12 +25,26 @@ class MLPBlock(nn.Module): An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale " """ - def __init__(self, hidden_size: int, mlp_dim: int, dropout_rate: float = 0.0) -> None: + def __init__( + self, + hidden_size: int, + mlp_dim: int, + dropout_rate: float = 0.0, + act: Union[Tuple, str] = "GELU", + dropout_mode="vit", + ) -> None: """ Args: hidden_size: dimension of hidden layer. - mlp_dim: dimension of feedforward layer. + mlp_dim: dimension of feedforward layer. If 0, `hidden_size` will be used. dropout_rate: faction of the input units to drop. + act: activation type and arguments. Defaults to GELU. + dropout_mode: dropout mode, can be "vit" or "swin". + "vit" mode uses two dropout instances as implemented in + https://github.com/google-research/vision_transformer/blob/main/vit_jax/models.py#L87 + "swin" corresponds to one instance as implemented in + https://github.com/microsoft/Swin-Transformer/blob/main/models/swin_mlp.py#L23 + """ @@ -31,12 +52,18 @@ def __init__(self, hidden_size: int, mlp_dim: int, dropout_rate: float = 0.0) -> if not (0 <= dropout_rate <= 1): raise ValueError("dropout_rate should be between 0 and 1.") - + mlp_dim = mlp_dim or hidden_size self.linear1 = nn.Linear(hidden_size, mlp_dim) self.linear2 = nn.Linear(mlp_dim, hidden_size) - self.fn = nn.GELU() + self.fn = get_act_layer(act) self.drop1 = nn.Dropout(dropout_rate) - self.drop2 = nn.Dropout(dropout_rate) + dropout_opt = look_up_option(dropout_mode, SUPPORTED_DROPOUT_MODE) + if dropout_opt == "vit": + self.drop2 = nn.Dropout(dropout_rate) + elif dropout_opt == "swin": + self.drop2 = self.drop1 + else: + raise ValueError(f"dropout_mode should be one of {SUPPORTED_DROPOUT_MODE}") def forward(self, x): x = self.fn(self.linear1(x)) diff --git a/monai/networks/layers/simplelayers.py b/monai/networks/layers/simplelayers.py index 7a0a45cb64..3de4e75766 100644 --- a/monai/networks/layers/simplelayers.py +++ b/monai/networks/layers/simplelayers.py @@ -20,19 +20,11 @@ from monai.networks.layers.convutils import gaussian_1d from monai.networks.layers.factories import Conv -from monai.utils import ( - ChannelMatching, - InvalidPyTorchVersionError, - SkipMode, - look_up_option, - optional_import, - pytorch_after, -) +from monai.utils import ChannelMatching, SkipMode, look_up_option, optional_import, pytorch_after from monai.utils.misc import issequenceiterable _C, _ = optional_import("monai._C") -if pytorch_after(1, 7): - fft, _ = optional_import("torch.fft") +fft, _ = optional_import("torch.fft") __all__ = [ "ChannelPad", @@ -377,7 +369,6 @@ def _make_coeffs(window_length, order): class HilbertTransform(nn.Module): """ Determine the analytical signal of a Tensor along a particular axis. - Requires PyTorch 1.7.0+ and the PyTorch FFT module (which is not included in NVIDIA PyTorch Release 20.10). Args: axis: Axis along which to apply Hilbert transform. Default 2 (first spatial dimension). @@ -386,9 +377,6 @@ class HilbertTransform(nn.Module): def __init__(self, axis: int = 2, n: Union[int, None] = None) -> None: - if not pytorch_after(1, 7): - raise InvalidPyTorchVersionError("1.7.0", self.__class__.__name__) - super().__init__() self.axis = axis self.n = n diff --git a/monai/networks/nets/dynunet.py b/monai/networks/nets/dynunet.py index 337a99acd8..e858dcbb9b 100644 --- a/monai/networks/nets/dynunet.py +++ b/monai/networks/nets/dynunet.py @@ -74,10 +74,14 @@ class DynUNet(nn.Module): is no less than 3 in order to have at least one downsample and upsample blocks. To meet the requirements of the structure, the input size for each spatial dimension should be divisible - by `2 * the product of all strides in the corresponding dimension`. The output size for each spatial dimension - equals to the input size of the corresponding dimension divided by the stride in strides[0]. - For example, if `strides=((1, 2, 4), 2, 1, 1)`, the minimal spatial size of the input is `(8, 16, 32)`, and - the spatial size of the output is `(8, 8, 8)`. + by the product of all strides in the corresponding dimension. In addition, the minimal spatial size should have + at least one dimension that has twice the size of the product of all strides. + For example, if `strides=((1, 2, 4), 2, 2, 1)`, the spatial size should be divisible by `(4, 8, 16)`, + and the minimal spatial size is `(8, 8, 16)` or `(4, 16, 16)` or `(4, 8, 32)`. + + The output size for each spatial dimension equals to the input size of the corresponding dimension divided by the + stride in strides[0]. + For example, if `strides=((1, 2, 4), 2, 2, 1)` and the input size is `(64, 32, 32)`, the output size is `(64, 16, 8)`. For backwards compatibility with old weights, please set `strict=False` when calling `load_state_dict`. diff --git a/monai/networks/utils.py b/monai/networks/utils.py index a6b0699107..f22be31524 100644 --- a/monai/networks/utils.py +++ b/monai/networks/utils.py @@ -281,14 +281,16 @@ def pixelshuffle( f"divisible by scale_factor ** dimensions ({factor}**{dim}={scale_divisor})." ) - org_channels = channels // scale_divisor + org_channels = int(channels // scale_divisor) output_size = [batch_size, org_channels] + [d * factor for d in input_size[2:]] - indices = tuple(range(2, 2 + 2 * dim)) - indices_factor, indices_dim = indices[:dim], indices[dim:] - permute_indices = (0, 1) + sum(zip(indices_dim, indices_factor), ()) + indices = list(range(2, 2 + 2 * dim)) + indices = indices[dim:] + indices[:dim] + permute_indices = [0, 1] + for idx in range(dim): + permute_indices.extend(indices[idx::dim]) - x = x.reshape(batch_size, org_channels, *([factor] * dim + input_size[2:])) + x = x.reshape([batch_size, org_channels] + [factor] * dim + input_size[2:]) x = x.permute(permute_indices).reshape(output_size) return x @@ -502,7 +504,6 @@ def convert_to_torchscript( filename_or_obj: if not None, specify a file-like object (has to implement write and flush) or a string containing a file path name to save the TorchScript model. extra_files: map from filename to contents which will be stored as part of the save model file. - works for PyTorch 1.7 or later. for more details: https://pytorch.org/docs/stable/generated/torch.jit.save.html. verify: whether to verify the input and output of TorchScript model. if `filename_or_obj` is not None, load the saved TorchScript model and verify. @@ -519,10 +520,7 @@ def convert_to_torchscript( with torch.no_grad(): script_module = torch.jit.script(model, **kwargs) if filename_or_obj is not None: - if not pytorch_after(1, 7): - torch.jit.save(m=script_module, f=filename_or_obj) - else: - torch.jit.save(m=script_module, f=filename_or_obj, _extra_files=extra_files) + torch.jit.save(m=script_module, f=filename_or_obj, _extra_files=extra_files) if verify: if device is None: diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index a3dc439a51..581e368ba0 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -206,6 +206,14 @@ from .inverse_batch_transform import BatchInverseTransform, Decollated, DecollateD, DecollateDict from .io.array import SUPPORTED_READERS, LoadImage, SaveImage from .io.dictionary import LoadImaged, LoadImageD, LoadImageDict, SaveImaged, SaveImageD, SaveImageDict +from .meta_utility.dictionary import ( + FromMetaTensord, + FromMetaTensorD, + FromMetaTensorDict, + ToMetaTensord, + ToMetaTensorD, + ToMetaTensorDict, +) from .nvtx import ( Mark, Markd, @@ -412,6 +420,7 @@ RepeatChannel, SimulateDelay, SplitChannel, + SplitDim, SqueezeDim, ToCupy, ToDevice, @@ -509,6 +518,9 @@ SplitChanneld, SplitChannelD, SplitChannelDict, + SplitDimd, + SplitDimD, + SplitDimDict, SqueezeDimd, SqueezeDimD, SqueezeDimDict, diff --git a/monai/transforms/croppad/dictionary.py b/monai/transforms/croppad/dictionary.py index 19ebe40b46..79f4040018 100644 --- a/monai/transforms/croppad/dictionary.py +++ b/monai/transforms/croppad/dictionary.py @@ -70,6 +70,7 @@ "RandCropByPosNegLabeld", "ResizeWithPadOrCropd", "BoundingRectd", + "RandCropByLabelClassesd", "SpatialPadD", "SpatialPadDict", "BorderPadD", @@ -98,7 +99,6 @@ "ResizeWithPadOrCropDict", "BoundingRectD", "BoundingRectDict", - "RandCropByLabelClassesd", "RandCropByLabelClassesD", "RandCropByLabelClassesDict", ] diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index da46b105e1..06b8cfa108 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -30,14 +30,12 @@ from monai.transforms.utils import Fourier, equalize_hist, is_positive, rescale_array from monai.transforms.utils_pytorch_numpy_unification import clip, percentile, where from monai.utils import ( - InvalidPyTorchVersionError, convert_data_type, convert_to_dst_type, ensure_tuple, ensure_tuple_rep, ensure_tuple_size, fall_back_tuple, - pytorch_after, ) from monai.utils.deprecate_utils import deprecated_arg from monai.utils.enums import TransformBackends @@ -1085,7 +1083,6 @@ def __call__(self, img: NdarrayOrTensor) -> NdarrayOrTensor: class DetectEnvelope(Transform): """ Find the envelope of the input data along the requested axis using a Hilbert transform. - Requires PyTorch 1.7.0+ and the PyTorch FFT module (which is not included in NVIDIA PyTorch Release 20.10). Args: axis: Axis along which to detect the envelope. Default 1, i.e. the first spatial dimension. @@ -1098,9 +1095,6 @@ class DetectEnvelope(Transform): def __init__(self, axis: int = 1, n: Union[int, None] = None) -> None: - if not pytorch_after(1, 7): - raise InvalidPyTorchVersionError("1.7.0", self.__class__.__name__) - if axis < 0: raise ValueError("axis must be zero or positive.") diff --git a/monai/transforms/inverse.py b/monai/transforms/inverse.py index c8bfeeca05..d2e5b2c6ba 100644 --- a/monai/transforms/inverse.py +++ b/monai/transforms/inverse.py @@ -38,7 +38,7 @@ class TraceableTransform(Transform): `MONAI_TRACE_TRANSFORM` when initializing the class. """ - tracing = False if os.environ.get("MONAI_TRACE_TRANSFORM", "1") == "0" else True + tracing = not os.environ.get("MONAI_TRACE_TRANSFORM", "1") == "0" def set_tracing(self, tracing: bool) -> None: """Set whether to trace transforms.""" diff --git a/monai/transforms/meta_utility/__init__.py b/monai/transforms/meta_utility/__init__.py new file mode 100644 index 0000000000..1e97f89407 --- /dev/null +++ b/monai/transforms/meta_utility/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) 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/monai/transforms/meta_utility/dictionary.py b/monai/transforms/meta_utility/dictionary.py new file mode 100644 index 0000000000..1a9cf4c631 --- /dev/null +++ b/monai/transforms/meta_utility/dictionary.py @@ -0,0 +1,102 @@ +# Copyright (c) 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. +""" +A collection of dictionary-based wrappers for moving between MetaTensor types and dictionaries of data. +These can be used to make backwards compatible code. + +Class names are ended with 'd' to denote dictionary-based transforms. +""" + +from copy import deepcopy +from typing import Dict, Hashable, Mapping + +from monai.config.type_definitions import NdarrayOrTensor +from monai.data.meta_tensor import MetaTensor +from monai.transforms.inverse import InvertibleTransform +from monai.transforms.transform import MapTransform +from monai.utils.enums import PostFix, TransformBackends + +__all__ = [ + "FromMetaTensord", + "FromMetaTensorD", + "FromMetaTensorDict", + "ToMetaTensord", + "ToMetaTensorD", + "ToMetaTensorDict", +] + + +class FromMetaTensord(MapTransform, InvertibleTransform): + """ + Dictionary-based transform to convert MetaTensor to a dictionary. + + If input is `{"a": MetaTensor, "b": MetaTensor}`, then output will + have the form `{"a": torch.Tensor, "a_meta_dict": dict, "b": ...}`. + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = dict(data) + for key in self.key_iterator(d): + self.push_transform(d, key) + im: MetaTensor = d[key] # type: ignore + d.update(im.as_dict(key)) + return d + + def inverse(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = deepcopy(dict(data)) + for key in self.key_iterator(d): + # check transform + _ = self.get_most_recent_transform(d, key) + # do the inverse + im, meta = d[key], d.pop(PostFix.meta(key), None) + im = MetaTensor(im, meta=meta) # type: ignore + d[key] = im + # Remove the applied transform + self.pop_transform(d, key) + return d + + +class ToMetaTensord(MapTransform, InvertibleTransform): + """ + Dictionary-based transform to convert a dictionary to MetaTensor. + + If input is `{"a": torch.Tensor, "a_meta_dict": dict, "b": ...}`, then output will + have the form `{"a": MetaTensor, "b": MetaTensor}`. + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = dict(data) + for key in self.key_iterator(d): + self.push_transform(d, key) + im, meta = d[key], d.pop(PostFix.meta(key), None) + im = MetaTensor(im, meta=meta) # type: ignore + d[key] = im + return d + + def inverse(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = deepcopy(dict(data)) + for key in self.key_iterator(d): + # check transform + _ = self.get_most_recent_transform(d, key) + # do the inverse + im: MetaTensor = d[key] # type: ignore + d.update(im.as_dict(key)) + # Remove the applied transform + self.pop_transform(d, key) + return d + + +FromMetaTensorD = FromMetaTensorDict = FromMetaTensord +ToMetaTensorD = ToMetaTensorDict = ToMetaTensord diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index 72f50f6894..bc0c09e949 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -37,6 +37,7 @@ convert_to_cupy, convert_to_numpy, convert_to_tensor, + deprecated, deprecated_arg, ensure_tuple, look_up_option, @@ -62,6 +63,7 @@ "EnsureType", "RepeatChannel", "RemoveRepeatedChannel", + "SplitDim", "SplitChannel", "CastToType", "ToTensor", @@ -281,33 +283,57 @@ def __call__(self, img: NdarrayOrTensor) -> NdarrayOrTensor: return img[:: self.repeats, :] -class SplitChannel(Transform): +class SplitDim(Transform): """ - Split Numpy array or PyTorch Tensor data according to the channel dim. - It can help applying different following transforms to different channels. + Given an image of size X along a certain dimension, return a list of length X containing + images. Useful for converting 3D images into a stack of 2D images, splitting multichannel inputs into + single channels, for example. - Args: - channel_dim: which dimension of input image is the channel, default to 0. + Note: `torch.split`/`np.split` is used, so the outputs are views of the input (shallow copy). + Args: + dim: dimension on which to split + keepdim: if `True`, output will have singleton in the split dimension. If `False`, this + dimension will be squeezed. """ backend = [TransformBackends.TORCH, TransformBackends.NUMPY] - def __init__(self, channel_dim: int = 0) -> None: - self.channel_dim = channel_dim + def __init__(self, dim: int = -1, keepdim: bool = True) -> None: + self.dim = dim + self.keepdim = keepdim def __call__(self, img: NdarrayOrTensor) -> List[NdarrayOrTensor]: - num_classes = img.shape[self.channel_dim] - if num_classes <= 1: - raise RuntimeError("input image does not contain multiple channels.") + """ + Apply the transform to `img`. + """ + n_out = img.shape[self.dim] + if n_out <= 1: + raise RuntimeError("Input image is singleton along dimension to be split.") + if isinstance(img, torch.Tensor): + outputs = list(torch.split(img, 1, self.dim)) + else: + outputs = np.split(img, n_out, self.dim) + if not self.keepdim: + outputs = [o.squeeze(self.dim) for o in outputs] + return outputs - outputs = [] - slices = [slice(None)] * len(img.shape) - for i in range(num_classes): - slices[self.channel_dim] = slice(i, i + 1) - outputs.append(img[tuple(slices)]) - return outputs +@deprecated(since="0.8", msg_suffix="please use `SplitDim` instead.") +class SplitChannel(SplitDim): + """ + Split Numpy array or PyTorch Tensor data according to the channel dim. + It can help applying different following transforms to different channels. + + Note: `torch.split`/`np.split` is used, so the outputs are views of the input (shallow copy). + + Args: + channel_dim: which dimension of input image is the channel, default to 0. + + """ + + def __init__(self, channel_dim: int = 0) -> None: + super().__init__(channel_dim) class CastToType(Transform): diff --git a/monai/transforms/utility/dictionary.py b/monai/transforms/utility/dictionary.py index ecf9aaffa4..564b2993e7 100644 --- a/monai/transforms/utility/dictionary.py +++ b/monai/transforms/utility/dictionary.py @@ -49,7 +49,7 @@ RemoveRepeatedChannel, RepeatChannel, SimulateDelay, - SplitChannel, + SplitDim, SqueezeDim, ToCupy, ToDevice, @@ -61,7 +61,7 @@ ) from monai.transforms.utils import extreme_points_to_image, get_extreme_points from monai.transforms.utils_pytorch_numpy_unification import concatenate -from monai.utils import convert_to_numpy, deprecated_arg, ensure_tuple, ensure_tuple_rep +from monai.utils import convert_to_numpy, deprecated, deprecated_arg, ensure_tuple, ensure_tuple_rep from monai.utils.enums import PostFix, TraceKeys, TransformBackends from monai.utils.type_conversion import convert_to_dst_type @@ -150,6 +150,9 @@ "SplitChannelD", "SplitChannelDict", "SplitChanneld", + "SplitDimD", + "SplitDimDict", + "SplitDimd", "SqueezeDimD", "SqueezeDimDict", "SqueezeDimd", @@ -372,19 +375,14 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, N return d -class SplitChanneld(MapTransform): - """ - Dictionary-based wrapper of :py:class:`monai.transforms.SplitChannel`. - All the input specified by `keys` should be split into same count of data. - """ - - backend = SplitChannel.backend - +class SplitDimd(MapTransform): def __init__( self, keys: KeysCollection, output_postfixes: Optional[Sequence[str]] = None, - channel_dim: int = 0, + dim: int = 0, + keepdim: bool = True, + update_meta: bool = True, allow_missing_keys: bool = False, ) -> None: """ @@ -395,13 +393,17 @@ def __init__( for example: if the key of input data is `pred` and split 2 classes, the output data keys will be: pred_(output_postfixes[0]), pred_(output_postfixes[1]) if None, using the index number: `pred_0`, `pred_1`, ... `pred_N`. - channel_dim: which dimension of input image is the channel, default to 0. + dim: which dimension of input image is the channel, default to 0. + keepdim: if `True`, output will have singleton in the split dimension. If `False`, this + dimension will be squeezed. + update_meta: if `True`, copy `[key]_meta_dict` for each output and update affine to + reflect the cropped image allow_missing_keys: don't raise exception if key is missing. - """ super().__init__(keys, allow_missing_keys) self.output_postfixes = output_postfixes - self.splitter = SplitChannel(channel_dim=channel_dim) + self.splitter = SplitDim(dim, keepdim) + self.update_meta = update_meta def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: d = dict(data) @@ -415,9 +417,44 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, N if split_key in d: raise RuntimeError(f"input data already contains key {split_key}.") d[split_key] = r + + if self.update_meta: + orig_meta = d.get(PostFix.meta(key), None) + if orig_meta is not None: + split_meta_key = PostFix.meta(split_key) + d[split_meta_key] = deepcopy(orig_meta) + dim = self.splitter.dim + if dim > 0: # don't update affine if channel dim + shift = np.eye(len(d[split_meta_key]["affine"])) # type: ignore + shift[dim - 1, -1] = i # type: ignore + d[split_meta_key]["affine"] = d[split_meta_key]["affine"] @ shift # type: ignore + return d +@deprecated(since="0.8", msg_suffix="please use `SplitDimd` instead.") +class SplitChanneld(SplitDimd): + """ + Dictionary-based wrapper of :py:class:`monai.transforms.SplitChannel`. + All the input specified by `keys` should be split into same count of data. + """ + + def __init__( + self, + keys: KeysCollection, + output_postfixes: Optional[Sequence[str]] = None, + channel_dim: int = 0, + allow_missing_keys: bool = False, + ) -> None: + super().__init__( + keys, + output_postfixes=output_postfixes, + dim=channel_dim, + update_meta=False, # for backwards compatibility + allow_missing_keys=allow_missing_keys, + ) + + class CastToTyped(MapTransform): """ Dictionary-based wrapper of :py:class:`monai.transforms.CastToType`. @@ -1637,6 +1674,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, N RemoveRepeatedChannelD = RemoveRepeatedChannelDict = RemoveRepeatedChanneld RepeatChannelD = RepeatChannelDict = RepeatChanneld SplitChannelD = SplitChannelDict = SplitChanneld +SplitDimD = SplitDimDict = SplitDimd CastToTypeD = CastToTypeDict = CastToTyped ToTensorD = ToTensorDict = ToTensord EnsureTypeD = EnsureTypeDict = EnsureTyped diff --git a/monai/transforms/utils_pytorch_numpy_unification.py b/monai/transforms/utils_pytorch_numpy_unification.py index 2103ccff58..2aedc77dd7 100644 --- a/monai/transforms/utils_pytorch_numpy_unification.py +++ b/monai/transforms/utils_pytorch_numpy_unification.py @@ -15,7 +15,7 @@ import torch from monai.config.type_definitions import NdarrayOrTensor, NdarrayTensor -from monai.utils.misc import ensure_tuple, is_module_ver_at_least +from monai.utils.misc import is_module_ver_at_least from monai.utils.type_conversion import convert_data_type, convert_to_dst_type __all__ = [ @@ -54,31 +54,12 @@ def allclose(a: NdarrayTensor, b: NdarrayOrTensor, rtol=1e-5, atol=1e-8, equal_n def moveaxis(x: NdarrayOrTensor, src: Union[int, Sequence[int]], dst: Union[int, Sequence[int]]) -> NdarrayOrTensor: - """`moveaxis` for pytorch and numpy, using `permute` for pytorch version < 1.7""" + """`moveaxis` for pytorch and numpy""" if isinstance(x, torch.Tensor): - if hasattr(torch, "movedim"): # `movedim` is new in torch 1.7.0 - # torch.moveaxis is a recent alias since torch 1.8.0 - return torch.movedim(x, src, dst) # type: ignore - return _moveaxis_with_permute(x, src, dst) + return torch.movedim(x, src, dst) # type: ignore return np.moveaxis(x, src, dst) -def _moveaxis_with_permute( - x: torch.Tensor, src: Union[int, Sequence[int]], dst: Union[int, Sequence[int]] -) -> torch.Tensor: - # get original indices - indices = list(range(x.ndim)) - len_indices = len(indices) - for s, d in zip(ensure_tuple(src), ensure_tuple(dst)): - # make src and dst positive - # remove desired index and insert it in new position - pos_s = len_indices + s if s < 0 else s - pos_d = len_indices + d if d < 0 else d - indices.pop(pos_s) - indices.insert(pos_d, pos_s) - return x.permute(indices) - - def in1d(x, y): """`np.in1d` with equivalent implementation for torch.""" if isinstance(x, np.ndarray): @@ -101,10 +82,7 @@ def percentile( ) -> Union[NdarrayOrTensor, float, int]: """`np.percentile` with equivalent implementation for torch. - Pytorch uses `quantile`, but this functionality is only available from v1.7. - For earlier methods, we calculate it ourselves. This doesn't do interpolation, - so is the equivalent of ``numpy.percentile(..., interpolation="nearest")``. - For more details, please refer to: + Pytorch uses `quantile`. For more details please refer to: https://pytorch.org/docs/stable/generated/torch.quantile.html. https://numpy.org/doc/stable/reference/generated/numpy.percentile.html. @@ -112,7 +90,7 @@ def percentile( x: input data q: percentile to compute (should in range 0 <= q <= 100) dim: the dim along which the percentiles are computed. default is to compute the percentile - along a flattened version of the array. only work for numpy array or Tensor with PyTorch >= 1.7.0. + along a flattened version of the array. keepdim: whether the output data has dim retained or not. kwargs: if `x` is numpy array, additional args for `np.percentile`, more details: https://numpy.org/doc/stable/reference/generated/numpy.percentile.html. @@ -130,18 +108,7 @@ def percentile( result = np.percentile(x, q, axis=dim, keepdims=keepdim, **kwargs) else: q = torch.tensor(q, device=x.device) - if hasattr(torch, "quantile"): # `quantile` is new in torch 1.7.0 - result = torch.quantile(x, q / 100.0, dim=dim, keepdim=keepdim) - else: - # Note that ``kthvalue()`` works one-based, i.e., the first sorted value - # corresponds to k=1, not k=0. Thus, we need the `1 +`. - k = 1 + (0.01 * q * (x.numel() - 1)).round().int() - if k.numel() > 1: - r = [x.view(-1).kthvalue(int(_k)).values.item() for _k in k] - result = torch.tensor(r, device=x.device) - else: - result = x.view(-1).kthvalue(int(k)).values.item() - + result = torch.quantile(x, q / 100.0, dim=dim, keepdim=keepdim) return result @@ -277,8 +244,6 @@ def any_np_pt(x: NdarrayOrTensor, axis: Union[int, Sequence[int]]) -> NdarrayOrT def maximum(a: NdarrayOrTensor, b: NdarrayOrTensor) -> NdarrayOrTensor: """`np.maximum` with equivalent implementation for torch. - `torch.maximum` only available from pt>1.6, else use `torch.stack` and `torch.max`. - Args: a: first array/tensor b: second array/tensor @@ -287,10 +252,7 @@ def maximum(a: NdarrayOrTensor, b: NdarrayOrTensor) -> NdarrayOrTensor: Element-wise maximum between two arrays/tensors. """ if isinstance(a, torch.Tensor) and isinstance(b, torch.Tensor): - # is torch and has torch.maximum (pt>1.6) - if hasattr(torch, "maximum"): # `maximum` is new in torch 1.7.0 - return torch.maximum(a, b) - return torch.stack((a, b)).max(dim=0)[0] + return torch.maximum(a, b) return np.maximum(a, b) diff --git a/monai/utils/enums.py b/monai/utils/enums.py index 4bc3d6ee84..1bfbdf824b 100644 --- a/monai/utils/enums.py +++ b/monai/utils/enums.py @@ -227,13 +227,13 @@ class ForwardMode(Enum): class TraceKeys: """Extra meta data keys used for traceable transforms.""" - CLASS_NAME = "class" - ID = "id" - ORIG_SIZE = "orig_size" - EXTRA_INFO = "extra_info" - DO_TRANSFORM = "do_transforms" - KEY_SUFFIX = "_transforms" - NONE = "none" + CLASS_NAME: str = "class" + ID: str = "id" + ORIG_SIZE: str = "orig_size" + EXTRA_INFO: str = "extra_info" + DO_TRANSFORM: str = "do_transforms" + KEY_SUFFIX: str = "_transforms" + NONE: str = "none" @deprecated(since="0.8.0", msg_suffix="use monai.utils.enums.TraceKeys instead.") @@ -287,6 +287,10 @@ def meta(key: Optional[str] = None): def orig_meta(key: Optional[str] = None): return PostFix._get_str(key, "orig_meta_dict") + @staticmethod + def transforms(key: Optional[str] = None): + return PostFix._get_str(key, TraceKeys.KEY_SUFFIX[1:]) + class TransformBackends(Enum): """ diff --git a/pyproject.toml b/pyproject.toml index 03e9f49ab5..eea4ebf9b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ requires = [ "wheel", "setuptools", - "torch>=1.6", + "torch>=1.7", "ninja", ] diff --git a/requirements.txt b/requirements.txt index e4ea34b5d4..14eb2b30e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -torch>=1.6 +torch>=1.7 numpy>=1.17 diff --git a/runtests.sh b/runtests.sh index 5464f3d020..6d2da4a6e7 100755 --- a/runtests.sh +++ b/runtests.sh @@ -146,6 +146,8 @@ function clang_format { exit 1 fi find monai/csrc -type f | while read i; do $clang_format_tool -style=file -i $i; done + find monai/_extensions -type f -name "*.cpp" -o -name "*.h" -o -name "*.cuh" -o -name "*.cu" |\ + while read i; do $clang_format_tool -style=file -i $i; done } function clean_py { @@ -405,7 +407,7 @@ then then install_deps fi - ${cmdPrefix}isort --version + ${cmdPrefix}${PY_EXE} -m isort --version if [ $doIsortFix = true ] then diff --git a/setup.cfg b/setup.cfg index a7d597d6bd..12f974ca6d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,7 +24,7 @@ setup_requires = torch ninja install_requires = - torch>=1.6 + torch>=1.7 numpy>=1.17 [options.extras_require] diff --git a/tests/min_tests.py b/tests/min_tests.py index 9bf95f3f49..66b6c9ff3d 100644 --- a/tests/min_tests.py +++ b/tests/min_tests.py @@ -143,7 +143,9 @@ def run_testsuit(): "test_smartcachedataset", "test_spacing", "test_spacingd", + "test_splitdimd", "test_surface_distance", + "test_surface_dice", "test_testtimeaugmentation", "test_torchvision", "test_torchvisiond", diff --git a/tests/ngc_mmar_loading.py b/tests/ngc_mmar_loading.py index 5261dfd612..4bb89dde0e 100644 --- a/tests/ngc_mmar_loading.py +++ b/tests/ngc_mmar_loading.py @@ -10,6 +10,7 @@ # limitations under the License. import os +import sys import unittest import torch @@ -17,6 +18,7 @@ from monai.apps.mmars import MODEL_DESC, load_from_mmar from monai.config import print_debug_info +from monai.networks.utils import copy_model_state class TestAllDownloadingMMAR(unittest.TestCase): @@ -26,10 +28,33 @@ def setUp(self): @parameterized.expand((item,) for item in MODEL_DESC) def test_loading_mmar(self, item): + if item["name"] == "clara_pt_self_supervised_learning_segmentation": # test the byow model + default_model_file = os.path.join("ssl_models_2gpu", "best_metric_model.pt") + pretrained_weights = load_from_mmar( + item=item["name"], + mmar_dir="./", + map_location="cpu", + api=True, + model_file=default_model_file, + weights_only=True, + ) + pretrained_weights = {k.split(".", 1)[1]: v for k, v in pretrained_weights["state_dict"].items()} + sys.path.append(os.path.join(f"{item['name']}", "custom")) # custom model folder + from vit_network import ViTAutoEnc # pylint: disable=E0401 + + model = ViTAutoEnc( + in_channels=1, + img_size=(96, 96, 96), + patch_size=(16, 16, 16), + pos_embed="conv", + hidden_size=768, + mlp_dim=3072, + ) + _, loaded, not_loaded = copy_model_state(model, pretrained_weights) + self.assertTrue(len(loaded) > 0 and len(not_loaded) == 0) + return if item["name"] == "clara_pt_fed_learning_brain_tumor_mri_segmentation": default_model_file = os.path.join("models", "server", "best_FL_global_model.pt") - elif item["name"] == "clara_pt_self_supervised_learning_segmentation": - default_model_file = os.path.join("models_2gpu", "best_metric_model.pt") else: default_model_file = None pretrained_model = load_from_mmar( diff --git a/tests/test_cachedataset.py b/tests/test_cachedataset.py index 7227f53e04..4b77d4a55a 100644 --- a/tests/test_cachedataset.py +++ b/tests/test_cachedataset.py @@ -80,7 +80,7 @@ def test_set_data(self): cache_rate=1.0, num_workers=4, progress=True, - copy_cache=False if sys.platform == "linux" else True, + copy_cache=not sys.platform == "linux", ) num_workers = 2 if sys.platform == "linux" else 0 diff --git a/tests/test_cachedataset_persistent_workers.py b/tests/test_cachedataset_persistent_workers.py index 4bea0486bc..8cef298be7 100644 --- a/tests/test_cachedataset_persistent_workers.py +++ b/tests/test_cachedataset_persistent_workers.py @@ -13,10 +13,8 @@ from monai.data import CacheDataset, DataLoader, create_test_image_2d from monai.transforms import Compose, RandAffined, Spacingd -from tests.utils import SkipIfBeforePyTorchVersion -@SkipIfBeforePyTorchVersion((1, 7)) class TestTransformsWCacheDatasetAndPersistentWorkers(unittest.TestCase): def test_duplicate_transforms(self): data = [{"img": create_test_image_2d(128, 128, num_seg_classes=1, channel_dim=0)[0]} for _ in range(2)] diff --git a/tests/test_config_parser.py b/tests/test_config_parser.py index 9c727c29ac..9ab002f7af 100644 --- a/tests/test_config_parser.py +++ b/tests/test_config_parser.py @@ -16,7 +16,7 @@ from parameterized import parameterized -from monai.bundle.config_parser import ConfigParser +from monai.bundle import ConfigParser, ReferenceResolver from monai.data import DataLoader, Dataset from monai.transforms import Compose, LoadImaged, RandTorchVisiond from monai.utils import min_version, optional_import @@ -86,6 +86,8 @@ def __call__(self, a, b): } ] +TEST_CASE_4 = [{"A": 1, "B": "@A", "C": "@D", "E": "$'test' + '@F'"}] + class TestConfigParser(unittest.TestCase): def test_config_content(self): @@ -154,6 +156,27 @@ def test_macro_replace(self): parser.resolve_macro_and_relative_ids() self.assertEqual(str(parser.get()), str({"A": {"B": 1, "C": 2}, "D": [3, 1, 3, 4]})) + @parameterized.expand([TEST_CASE_4]) + def test_allow_missing_reference(self, config): + default = ReferenceResolver.allow_missing_reference + ReferenceResolver.allow_missing_reference = True + parser = ConfigParser(config=config) + + for id in config: + item = parser.get_parsed_content(id=id) + if id in ("A", "B"): + self.assertEqual(item, 1) + elif id == "C": + self.assertEqual(item, "@D") + elif id == "E": + self.assertEqual(item, "test@F") + + # restore the default value + ReferenceResolver.allow_missing_reference = default + with self.assertRaises(ValueError): + parser.parse() + parser.get_parsed_content(id="E") + if __name__ == "__main__": unittest.main() diff --git a/tests/test_detect_envelope.py b/tests/test_detect_envelope.py index f1b9c7ad1a..5ea82a463d 100644 --- a/tests/test_detect_envelope.py +++ b/tests/test_detect_envelope.py @@ -16,8 +16,8 @@ from parameterized import parameterized from monai.transforms import DetectEnvelope -from monai.utils import InvalidPyTorchVersionError, OptionalImportError -from tests.utils import SkipIfAtLeastPyTorchVersion, SkipIfBeforePyTorchVersion, SkipIfModule, SkipIfNoModule +from monai.utils import OptionalImportError +from tests.utils import SkipIfModule, SkipIfNoModule n_samples = 500 hann_windowed_sine = np.sin(2 * np.pi * 10 * np.linspace(0, 1, n_samples)) * np.hanning(n_samples) @@ -112,7 +112,6 @@ TEST_CASE_INVALID_OBJ = [{}, "a string", "__call__"] # method expected to raise exception -@SkipIfBeforePyTorchVersion((1, 7)) @SkipIfNoModule("torch.fft") class TestDetectEnvelope(unittest.TestCase): @parameterized.expand( @@ -147,19 +146,11 @@ def test_value_error(self, arguments, image, method): raise ValueError("Expected raising method invalid. Should be __init__ or __call__.") -@SkipIfBeforePyTorchVersion((1, 7)) @SkipIfModule("torch.fft") class TestHilbertTransformNoFFTMod(unittest.TestCase): def test_no_fft_module_error(self): self.assertRaises(OptionalImportError, DetectEnvelope(), np.random.rand(1, 10)) -@SkipIfAtLeastPyTorchVersion((1, 7)) -class TestDetectEnvelopeInvalidPyTorch(unittest.TestCase): - def test_invalid_pytorch_error(self): - with self.assertRaisesRegex(InvalidPyTorchVersionError, "version"): - DetectEnvelope() - - if __name__ == "__main__": unittest.main() diff --git a/tests/test_dice_ce_loss.py b/tests/test_dice_ce_loss.py index ff2cd00b02..83ad5b8d9a 100644 --- a/tests/test_dice_ce_loss.py +++ b/tests/test_dice_ce_loss.py @@ -16,7 +16,7 @@ from parameterized import parameterized from monai.losses import DiceCELoss -from tests.utils import SkipIfBeforePyTorchVersion, test_script_save +from tests.utils import test_script_save TEST_CASES = [ [ # shape: (2, 2, 3), (2, 1, 3) @@ -85,7 +85,6 @@ def test_ill_reduction(self): loss = DiceCELoss(reduction="none") loss(torch.ones((1, 2, 3)), torch.ones((1, 1, 2, 3))) - @SkipIfBeforePyTorchVersion((1, 7, 0)) def test_script(self): loss = DiceCELoss() test_input = torch.ones(2, 1, 8, 8) diff --git a/tests/test_dice_focal_loss.py b/tests/test_dice_focal_loss.py index c611fe4160..b77a36e720 100644 --- a/tests/test_dice_focal_loss.py +++ b/tests/test_dice_focal_loss.py @@ -15,7 +15,7 @@ import torch from monai.losses import DiceFocalLoss, DiceLoss, FocalLoss -from tests.utils import SkipIfBeforePyTorchVersion, test_script_save +from tests.utils import test_script_save class TestDiceFocalLoss(unittest.TestCase): @@ -61,7 +61,6 @@ def test_ill_lambda(self): with self.assertRaisesRegex(ValueError, ""): DiceFocalLoss(lambda_dice=-1.0) - @SkipIfBeforePyTorchVersion((1, 7, 0)) def test_script(self): loss = DiceFocalLoss() test_input = torch.ones(2, 1, 8, 8) diff --git a/tests/test_dice_loss.py b/tests/test_dice_loss.py index 4e45393de6..223b09e624 100644 --- a/tests/test_dice_loss.py +++ b/tests/test_dice_loss.py @@ -16,7 +16,7 @@ from parameterized import parameterized from monai.losses import DiceLoss -from tests.utils import SkipIfBeforePyTorchVersion, test_script_save +from tests.utils import test_script_save TEST_CASES = [ [ # shape: (1, 1, 2, 2), (1, 1, 2, 2) @@ -184,7 +184,6 @@ def test_input_warnings(self): loss = DiceLoss(to_onehot_y=True) loss.forward(chn_input, chn_target) - @SkipIfBeforePyTorchVersion((1, 7, 0)) def test_script(self): loss = DiceLoss() test_input = torch.ones(2, 1, 8, 8) diff --git a/tests/test_efficientnet.py b/tests/test_efficientnet.py index a2a5e30750..d56f901af7 100644 --- a/tests/test_efficientnet.py +++ b/tests/test_efficientnet.py @@ -367,7 +367,8 @@ def test_func_get_efficientnet_input_shape(self): self.assertEqual(result_shape, expected_shape) def test_script(self): - net = EfficientNetBN(model_name="efficientnet-b0", spatial_dims=2, in_channels=3, num_classes=1000) + with skip_if_downloading_fails(): + net = EfficientNetBN(model_name="efficientnet-b0", spatial_dims=2, in_channels=3, num_classes=1000) net.set_swish(memory_efficient=False) # at the moment custom memory efficient swish is not exportable with jit test_data = torch.randn(1, 3, 224, 224) test_script_save(net, test_data) diff --git a/tests/test_focal_loss.py b/tests/test_focal_loss.py index d8a9c8ab5b..5a063ba6c8 100644 --- a/tests/test_focal_loss.py +++ b/tests/test_focal_loss.py @@ -17,7 +17,7 @@ from monai.losses import FocalLoss from monai.networks import one_hot -from tests.utils import SkipIfBeforePyTorchVersion, test_script_save +from tests.utils import test_script_save class TestFocalLoss(unittest.TestCase): @@ -261,7 +261,6 @@ def test_ill_class_weight(self): with self.assertRaisesRegex(ValueError, ""): FocalLoss(include_background=False, weight=(1.0, 1.0, -1.0))(chn_input, chn_target) - @SkipIfBeforePyTorchVersion((1, 7, 0)) def test_script(self): loss = FocalLoss() test_input = torch.ones(2, 2, 8, 8) diff --git a/tests/test_gaussian_filter.py b/tests/test_gaussian_filter.py index 9d76e44cec..c4ffe56896 100644 --- a/tests/test_gaussian_filter.py +++ b/tests/test_gaussian_filter.py @@ -16,7 +16,7 @@ from parameterized import parameterized from monai.networks.layers import GaussianFilter -from tests.utils import SkipIfBeforePyTorchVersion, skip_if_quick +from tests.utils import skip_if_quick TEST_CASES = [[{"type": "erf", "gt": 2.0}], [{"type": "scalespace", "gt": 3.0}], [{"type": "sampled", "gt": 5.0}]] TEST_CASES_GPU = [[{"type": "erf", "gt": 0.8, "device": "cuda"}], [{"type": "sampled", "gt": 5.0, "device": "cuda"}]] @@ -82,7 +82,6 @@ def code_to_run(self, input_args): ) @parameterized.expand(TEST_CASES + TEST_CASES_GPU + TEST_CASES_3d) - @SkipIfBeforePyTorchVersion((1, 7)) def test_train_quick(self, input_args): self.code_to_run(input_args) diff --git a/tests/test_generalized_dice_loss.py b/tests/test_generalized_dice_loss.py index fa301201e4..81f8f4c0b0 100644 --- a/tests/test_generalized_dice_loss.py +++ b/tests/test_generalized_dice_loss.py @@ -16,7 +16,7 @@ from parameterized import parameterized from monai.losses import GeneralizedDiceLoss -from tests.utils import SkipIfBeforePyTorchVersion, test_script_save +from tests.utils import test_script_save TEST_CASES = [ [ # shape: (1, 1, 2, 2), (1, 1, 2, 2) @@ -193,7 +193,6 @@ def test_batch(self): loss = generalized_dice_loss(prediction, target) self.assertNotEqual(loss.grad_fn, None) - @SkipIfBeforePyTorchVersion((1, 7, 0)) def test_script(self): loss = GeneralizedDiceLoss() test_input = torch.ones(2, 1, 8, 8) diff --git a/tests/test_generalized_wasserstein_dice_loss.py b/tests/test_generalized_wasserstein_dice_loss.py index 2c33d365f4..49a5aa0556 100644 --- a/tests/test_generalized_wasserstein_dice_loss.py +++ b/tests/test_generalized_wasserstein_dice_loss.py @@ -18,7 +18,7 @@ import torch.optim as optim from monai.losses import GeneralizedWassersteinDiceLoss -from tests.utils import SkipIfBeforePyTorchVersion, test_script_save +from tests.utils import test_script_save class TestGeneralizedWassersteinDiceLoss(unittest.TestCase): @@ -216,7 +216,6 @@ def forward(self, x): # check that the predicted segmentation has improved self.assertGreater(diff_start, diff_end) - @SkipIfBeforePyTorchVersion((1, 7, 0)) def test_script(self): target = torch.tensor([[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]]) diff --git a/tests/test_grid_dataset.py b/tests/test_grid_dataset.py index 9361d82cdf..4d2d0d6948 100644 --- a/tests/test_grid_dataset.py +++ b/tests/test_grid_dataset.py @@ -14,8 +14,8 @@ import numpy as np -from monai.data import DataLoader, GridPatchDataset, PatchIter -from monai.transforms import RandShiftIntensity +from monai.data import DataLoader, GridPatchDataset, PatchIter, PatchIterd +from monai.transforms import RandShiftIntensity, RandShiftIntensityd from monai.utils import set_determinism @@ -76,6 +76,48 @@ def test_loading_array(self): item[1], np.array([[[0, 1], [0, 2], [2, 4]], [[0, 1], [2, 4], [2, 4]]]), rtol=1e-5 ) + def test_loading_dict(self): + set_determinism(seed=1234) + # test sequence input data with dict + data = [ + { + "image": np.arange(16, dtype=float).reshape(1, 4, 4), + "label": np.arange(16, dtype=float).reshape(1, 4, 4), + "metadata": "test string", + }, + { + "image": np.arange(16, dtype=float).reshape(1, 4, 4), + "label": np.arange(16, dtype=float).reshape(1, 4, 4), + "metadata": "test string", + }, + ] + # image level + patch_intensity = RandShiftIntensityd(keys="image", offsets=1.0, prob=1.0) + patch_iter = PatchIterd(keys=["image", "label"], patch_size=(2, 2), start_pos=(0, 0)) + ds = GridPatchDataset(data=data, patch_iter=patch_iter, transform=patch_intensity, with_coordinates=True) + # use the grid patch dataset + for item in DataLoader(ds, batch_size=2, shuffle=False, num_workers=0): + np.testing.assert_equal(item[0]["image"].shape, (2, 1, 2, 2)) + np.testing.assert_equal(item[0]["label"].shape, (2, 1, 2, 2)) + self.assertListEqual(item[0]["metadata"], ["test string", "test string"]) + np.testing.assert_allclose( + item[0]["image"], + np.array([[[[1.4965, 2.4965], [5.4965, 6.4965]]], [[[11.3584, 12.3584], [15.3584, 16.3584]]]]), + rtol=1e-4, + ) + np.testing.assert_allclose(item[1], np.array([[[0, 1], [0, 2], [2, 4]], [[0, 1], [2, 4], [2, 4]]]), rtol=1e-5) + if sys.platform != "win32": + for item in DataLoader(ds, batch_size=2, shuffle=False, num_workers=2): + np.testing.assert_equal(item[0]["image"].shape, (2, 1, 2, 2)) + np.testing.assert_allclose( + item[0]["image"], + np.array([[[[1.2548, 2.2548], [5.2548, 6.2548]]], [[[9.1106, 10.1106], [13.1106, 14.1106]]]]), + rtol=1e-3, + ) + np.testing.assert_allclose( + item[1], np.array([[[0, 1], [0, 2], [2, 4]], [[0, 1], [2, 4], [2, 4]]]), rtol=1e-5 + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_handler_smartcache.py b/tests/test_handler_smartcache.py index ec96d47e3d..7bc9011c2d 100644 --- a/tests/test_handler_smartcache.py +++ b/tests/test_handler_smartcache.py @@ -17,10 +17,8 @@ from monai.data import SmartCacheDataset from monai.handlers import SmartCacheHandler -from tests.utils import SkipIfBeforePyTorchVersion -@SkipIfBeforePyTorchVersion((1, 7)) class TestHandlerSmartCache(unittest.TestCase): def test_content(self): data = [0, 1, 2, 3, 4, 5, 6, 7, 8] diff --git a/tests/test_hilbert_transform.py b/tests/test_hilbert_transform.py index 10aa83293f..f7954d6b24 100644 --- a/tests/test_hilbert_transform.py +++ b/tests/test_hilbert_transform.py @@ -16,14 +16,8 @@ from parameterized import parameterized from monai.networks.layers import HilbertTransform -from monai.utils import InvalidPyTorchVersionError, OptionalImportError -from tests.utils import ( - SkipIfAtLeastPyTorchVersion, - SkipIfBeforePyTorchVersion, - SkipIfModule, - SkipIfNoModule, - skip_if_no_cuda, -) +from monai.utils import OptionalImportError +from tests.utils import SkipIfModule, SkipIfNoModule, skip_if_no_cuda def create_expected_numpy_output(input_datum, **kwargs): @@ -164,7 +158,6 @@ def create_expected_numpy_output(input_datum, **kwargs): # TESTS CHECKING PADDING, AXIS SELECTION ETC ARE COVERED BY test_detect_envelope.py -@SkipIfBeforePyTorchVersion((1, 7)) @SkipIfNoModule("torch.fft") class TestHilbertTransformCPU(unittest.TestCase): @parameterized.expand( @@ -183,7 +176,6 @@ def test_value(self, arguments, image, expected_data, atol): @skip_if_no_cuda -@SkipIfBeforePyTorchVersion((1, 7)) @SkipIfNoModule("torch.fft") class TestHilbertTransformGPU(unittest.TestCase): @parameterized.expand( @@ -204,19 +196,11 @@ def test_value(self, arguments, image, expected_data, atol): np.testing.assert_allclose(result, expected_data.squeeze(), atol=atol) -@SkipIfBeforePyTorchVersion((1, 7)) @SkipIfModule("torch.fft") class TestHilbertTransformNoFFTMod(unittest.TestCase): def test_no_fft_module_error(self): self.assertRaises(OptionalImportError, HilbertTransform(), torch.randn(1, 1, 10)) -@SkipIfAtLeastPyTorchVersion((1, 7)) -class TestHilbertTransformInvalidPyTorch(unittest.TestCase): - def test_invalid_pytorch_error(self): - with self.assertRaisesRegex(InvalidPyTorchVersionError, "version"): - HilbertTransform() - - if __name__ == "__main__": unittest.main() diff --git a/tests/test_integration_fast_train.py b/tests/test_integration_fast_train.py index 51b2ac1d3f..4dbb70b102 100644 --- a/tests/test_integration_fast_train.py +++ b/tests/test_integration_fast_train.py @@ -52,12 +52,11 @@ ToDeviced, ) from monai.utils import set_determinism -from tests.utils import DistTestCase, SkipIfBeforePyTorchVersion, TimedCall, skip_if_no_cuda, skip_if_quick +from tests.utils import DistTestCase, TimedCall, skip_if_no_cuda, skip_if_quick @skip_if_no_cuda @skip_if_quick -@SkipIfBeforePyTorchVersion((1, 7)) class IntegrationFastTrain(DistTestCase): def setUp(self): set_determinism(seed=0) diff --git a/tests/test_integration_workflows.py b/tests/test_integration_workflows.py index 432e5e90a0..fafdf43522 100644 --- a/tests/test_integration_workflows.py +++ b/tests/test_integration_workflows.py @@ -147,7 +147,8 @@ def _forward_completed(self, engine): additional_metrics={"val_acc": Accuracy(output_transform=from_engine(["pred", "label"]))}, metric_cmp_fn=lambda cur, prev: cur >= prev, # if greater or equal, treat as new best metric val_handlers=val_handlers, - amp=True if amp else False, + amp=bool(amp), + to_kwargs={"memory_format": torch.preserve_format}, ) train_postprocessing = Compose( @@ -200,8 +201,9 @@ def _model_completed(self, engine): postprocessing=train_postprocessing, key_train_metric={"train_acc": Accuracy(output_transform=from_engine(["pred", "label"]))}, train_handlers=train_handlers, - amp=True if amp else False, + amp=bool(amp), optim_set_to_none=True, + to_kwargs={"memory_format": torch.preserve_format}, ) trainer.run() @@ -271,7 +273,7 @@ def run_inference_test(root_dir, model_file, device="cuda:0", amp=False, num_wor }, additional_metrics={"val_acc": Accuracy(output_transform=from_engine(["pred", "label"]))}, val_handlers=val_handlers, - amp=True if amp else False, + amp=bool(amp), ) evaluator.run() diff --git a/tests/test_integration_workflows_gan.py b/tests/test_integration_workflows_gan.py index c9306b349f..f65a30450a 100644 --- a/tests/test_integration_workflows_gan.py +++ b/tests/test_integration_workflows_gan.py @@ -117,6 +117,7 @@ def generator_loss(gen_images): latent_shape=latent_size, key_train_metric=key_train_metric, train_handlers=train_handlers, + to_kwargs={"memory_format": torch.preserve_format, "dtype": torch.float32}, ) trainer.run() diff --git a/tests/test_keep_largest_connected_component.py b/tests/test_keep_largest_connected_component.py index 5c96b62131..6419914be6 100644 --- a/tests/test_keep_largest_connected_component.py +++ b/tests/test_keep_largest_connected_component.py @@ -19,7 +19,7 @@ from monai.transforms import KeepLargestConnectedComponent from monai.transforms.utils_pytorch_numpy_unification import moveaxis from monai.utils.type_conversion import convert_to_dst_type -from tests.utils import TEST_NDARRAYS, SkipIfBeforePyTorchVersion, assert_allclose +from tests.utils import TEST_NDARRAYS, assert_allclose def to_onehot(x): @@ -353,7 +353,6 @@ def test_correct_results(self, _, args, input_image, expected): assert_allclose(result, expected, type_test=False) @parameterized.expand(TESTS) - @SkipIfBeforePyTorchVersion((1, 7)) def test_correct_results_before_after_onehot(self, _, args, input_image, expected): """ From torch==1.7, torch.argmax changes its mechanism that if there are multiple maximal values then the diff --git a/tests/test_masked_loss.py b/tests/test_masked_loss.py index 9f28d51aa4..a00b3ae7e7 100644 --- a/tests/test_masked_loss.py +++ b/tests/test_masked_loss.py @@ -17,7 +17,7 @@ from monai.losses.dice import DiceFocalLoss, DiceLoss from monai.losses.spatial_mask import MaskedLoss from monai.utils import set_determinism -from tests.utils import SkipIfBeforePyTorchVersion, test_script_save +from tests.utils import test_script_save device = "cuda" if torch.cuda.is_available() else "cpu" @@ -73,7 +73,6 @@ def test_ill_opts(self): masked = MaskedLoss(loss=dice_loss) masked(input=torch.zeros((3, 3, 2, 2)), target=torch.zeros((3, 2, 2, 2)), mask=torch.zeros((3, 3, 2, 2))) - @SkipIfBeforePyTorchVersion((1, 7, 0)) def test_script(self): input_param, expected_val = TEST_CASES[0] size = [3, 3, 5, 5] diff --git a/tests/test_meta_tensor.py b/tests/test_meta_tensor.py new file mode 100644 index 0000000000..c18ef08b85 --- /dev/null +++ b/tests/test_meta_tensor.py @@ -0,0 +1,273 @@ +# Copyright (c) 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 random +import string +import tempfile +import unittest +import warnings +from copy import deepcopy +from typing import Optional, Union + +import torch +from parameterized import parameterized + +from monai.data.meta_obj import get_track_meta, get_track_transforms, set_track_meta, set_track_transforms +from monai.data.meta_tensor import MetaTensor +from monai.utils.enums import PostFix +from monai.utils.module import pytorch_after +from tests.utils import TEST_DEVICES, assert_allclose, skip_if_no_cuda + +DTYPES = [[torch.float32], [torch.float64], [torch.float16], [torch.int64], [torch.int32]] +TESTS = [] +for _device in TEST_DEVICES: + for _dtype in DTYPES: + TESTS.append((*_device, *_dtype)) + + +def rand_string(min_len=5, max_len=10): + str_size = random.randint(min_len, max_len) + chars = string.ascii_letters + string.punctuation + return "".join(random.choice(chars) for _ in range(str_size)) + + +class TestMetaTensor(unittest.TestCase): + @staticmethod + def get_im(shape=None, dtype=None, device=None): + if shape is None: + shape = (1, 10, 8) + affine = torch.randint(0, 10, (4, 4)) + meta = {"fname": rand_string()} + t = torch.rand(shape) + if dtype is not None: + t = t.to(dtype) + if device is not None: + t = t.to(device) + m = MetaTensor(t.clone(), affine, meta) + return m, t + + def check_ids(self, a, b, should_match): + comp = self.assertEqual if should_match else self.assertNotEqual + comp(id(a), id(b)) + + def check( + self, + out: torch.Tensor, + orig: torch.Tensor, + *, + shape: bool = True, + vals: bool = True, + ids: bool = True, + device: Optional[Union[str, torch.device]] = None, + meta: bool = True, + check_ids: bool = True, + **kwargs, + ): + if device is None: + device = orig.device + + # check the image + self.assertIsInstance(out, type(orig)) + if shape: + assert_allclose(torch.as_tensor(out.shape), torch.as_tensor(orig.shape)) + if vals: + assert_allclose(out, orig, **kwargs) + if check_ids: + self.check_ids(out, orig, ids) + self.assertTrue(str(device) in str(out.device)) + + # check meta and affine are equal and affine is on correct device + if isinstance(orig, MetaTensor) and isinstance(out, MetaTensor) and meta: + orig_meta_no_affine = deepcopy(orig.meta) + del orig_meta_no_affine["affine"] + out_meta_no_affine = deepcopy(out.meta) + del out_meta_no_affine["affine"] + self.assertEqual(orig_meta_no_affine, out_meta_no_affine) + assert_allclose(out.affine, orig.affine) + self.assertTrue(str(device) in str(out.affine.device)) + if check_ids: + self.check_ids(out.affine, orig.affine, ids) + self.check_ids(out.meta, orig.meta, ids) + + @parameterized.expand(TESTS) + def test_as_tensor(self, device, dtype): + m, t = self.get_im(device=device, dtype=dtype) + t2 = m.as_tensor() + self.assertIsInstance(t2, torch.Tensor) + self.assertNotIsInstance(t2, MetaTensor) + self.assertIsInstance(m, MetaTensor) + self.check(t, t2, ids=False) + + def test_as_dict(self): + m, _ = self.get_im() + m_dict = m.as_dict("im") + im, meta = m_dict["im"], m_dict[PostFix.meta("im")] + affine = meta.pop("affine") + m2 = MetaTensor(im, affine, meta) + self.check(m2, m, check_ids=False) + + @parameterized.expand(TESTS) + def test_constructor(self, device, dtype): + m, t = self.get_im(device=device, dtype=dtype) + # construct from pre-existing + m1 = MetaTensor(m.clone()) + self.check(m, m1, ids=False, meta=False) + # meta already has affine + m2 = MetaTensor(t.clone(), meta=m.meta) + self.check(m, m2, ids=False, meta=False) + # meta dosen't have affine + affine = m.meta.pop("affine") + m3 = MetaTensor(t.clone(), affine=affine, meta=m.meta) + self.check(m, m3, ids=False, meta=False) + + @parameterized.expand(TESTS) + @skip_if_no_cuda + def test_to_cuda(self, device, dtype): + """Test `to`, `cpu` and `cuda`. For `to`, check args and kwargs.""" + orig, _ = self.get_im(device=device, dtype=dtype) + m = orig.clone() + m = m.to("cuda") + self.check(m, orig, ids=False, device="cuda") + m = m.cpu() + self.check(m, orig, ids=False, device="cpu") + m = m.cuda() + self.check(m, orig, ids=False, device="cuda") + m = m.to("cpu") + self.check(m, orig, ids=False, device="cpu") + m = m.to(device="cuda") + self.check(m, orig, ids=False, device="cuda") + m = m.to(device="cpu") + self.check(m, orig, ids=False, device="cpu") + + @skip_if_no_cuda + def test_affine_device(self): + m, _ = self.get_im() # device="cuda") + m.affine = torch.eye(4) + self.assertEqual(m.device, m.affine.device) + + @parameterized.expand(TESTS) + def test_copy(self, device, dtype): + m, _ = self.get_im(device=device, dtype=dtype) + # shallow copy + a = m + self.check(a, m, ids=True) + # deepcopy + a = deepcopy(m) + self.check(a, m, ids=False) + # clone + a = m.clone() + self.check(a, m, ids=False) + + @parameterized.expand(TESTS) + def test_add(self, device, dtype): + m1, t1 = self.get_im(device=device, dtype=dtype) + m2, t2 = self.get_im(device=device, dtype=dtype) + self.check(m1 + m2, t1 + t2, ids=False) + self.check(torch.add(m1, m2), t1 + t2, ids=False) + self.check(torch.add(input=m1, other=m2), t1 + t2, ids=False) + self.check(torch.add(m1, other=m2), t1 + t2, ids=False) + m3 = deepcopy(m2) + t3 = deepcopy(t2) + m3 += 3 + t3 += 3 + self.check(m3, t3, ids=False) + # check torch.Tensor+MetaTensor and MetaTensor+torch.Tensor + self.check(torch.add(m1, t2), t1 + t2, ids=False) + self.check(torch.add(t2, m1), t1 + t2, ids=False) + + @parameterized.expand(TEST_DEVICES) + def test_conv(self, device): + im, _ = self.get_im((1, 3, 10, 8, 12), device=device) + conv = torch.nn.Conv3d(im.shape[1], 5, 3) + conv.to(device) + out = conv(im) + self.check(out, im, shape=False, vals=False, ids=False) + + @parameterized.expand(TESTS) + def test_stack(self, device, dtype): + numel = 3 + ims = [self.get_im(device=device, dtype=dtype)[0] for _ in range(numel)] + stacked = torch.stack(ims) + self.assertIsInstance(stacked, MetaTensor) + orig_affine = ims[0].meta.pop("affine") + stacked_affine = stacked.meta.pop("affine") + assert_allclose(orig_affine, stacked_affine) + self.assertEqual(stacked.meta, ims[0].meta) + + def test_get_set_meta_fns(self): + set_track_meta(False) + self.assertEqual(get_track_meta(), False) + set_track_meta(True) + self.assertEqual(get_track_meta(), True) + set_track_transforms(False) + self.assertEqual(get_track_transforms(), False) + set_track_transforms(True) + self.assertEqual(get_track_transforms(), True) + + @parameterized.expand(TEST_DEVICES) + def test_torchscript(self, device): + shape = (1, 3, 10, 8) + im, _ = self.get_im(shape, device=device) + conv = torch.nn.Conv2d(im.shape[1], 5, 3) + conv.to(device) + im_conv = conv(im) + traced_fn = torch.jit.trace(conv, im.as_tensor()) + # save it, load it, use it + with tempfile.TemporaryDirectory() as tmp_dir: + fname = os.path.join(tmp_dir, "im.pt") + torch.jit.save(traced_fn, f=fname) + traced_fn = torch.jit.load(fname) + out = traced_fn(im) + self.assertIsInstance(out, torch.Tensor) + if not isinstance(out, MetaTensor) and not pytorch_after(1, 9, 1): + warnings.warn( + "When calling `nn.Module(MetaTensor) on a module traced with " + "`torch.jit.trace`, your version of pytorch returns a " + "`torch.Tensor` instead of a `MetaTensor`. Consider upgrading " + "your pytorch version if this is important to you." + ) + im_conv = im_conv.as_tensor() + self.check(out, im_conv, ids=False) + + def test_pickling(self): + m, _ = self.get_im() + with tempfile.TemporaryDirectory() as tmp_dir: + fname = os.path.join(tmp_dir, "im.pt") + torch.save(m, fname) + m2 = torch.load(fname) + if not isinstance(m2, MetaTensor) and not pytorch_after(1, 8, 1): + warnings.warn("Old version of pytorch. pickling converts `MetaTensor` to `torch.Tensor`.") + m = m.as_tensor() + self.check(m2, m, ids=False) + + @skip_if_no_cuda + def test_amp(self): + shape = (1, 3, 10, 8) + device = "cuda" + im, _ = self.get_im(shape, device=device) + conv = torch.nn.Conv2d(im.shape[1], 5, 3) + conv.to(device) + im_conv = conv(im) + with torch.cuda.amp.autocast(): + im_conv2 = conv(im) + self.check(im_conv2, im_conv, ids=False, rtol=1e-4, atol=1e-3) + + # TODO + # collate + # decollate + # dataset + # dataloader + # matplotlib + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_mlp.py b/tests/test_mlp.py index 6fec5b6854..737762cfb1 100644 --- a/tests/test_mlp.py +++ b/tests/test_mlp.py @@ -21,7 +21,7 @@ TEST_CASE_MLP = [] for dropout_rate in np.linspace(0, 1, 4): for hidden_size in [128, 256, 512, 768]: - for mlp_dim in [512, 1028, 2048, 3072]: + for mlp_dim in [0, 1028, 2048, 3072]: test_case = [ {"hidden_size": hidden_size, "mlp_dim": mlp_dim, "dropout_rate": dropout_rate}, diff --git a/tests/test_module_list.py b/tests/test_module_list.py index 83c6979f30..d81d067c58 100644 --- a/tests/test_module_list.py +++ b/tests/test_module_list.py @@ -40,6 +40,7 @@ def test_transform_api(self): to_exclude = {"MapTransform"} # except for these transforms to_exclude_docs = {"Decollate", "Ensemble", "Invert", "SaveClassification", "RandTorchVision"} to_exclude_docs.update({"DeleteItems", "SelectItems", "CopyItems", "ConcatItems"}) + to_exclude_docs.update({"ToMetaTensor", "FromMetaTensor"}) xforms = { name: obj for name, obj in monai.transforms.__dict__.items() diff --git a/tests/test_multi_scale.py b/tests/test_multi_scale.py index 963824f25e..f348c09512 100644 --- a/tests/test_multi_scale.py +++ b/tests/test_multi_scale.py @@ -16,7 +16,7 @@ from monai.losses import DiceLoss from monai.losses.multi_scale import MultiScaleLoss -from tests.utils import SkipIfBeforePyTorchVersion, test_script_save +from tests.utils import test_script_save dice_loss = DiceLoss(include_background=True, sigmoid=True, smooth_nr=1e-5, smooth_dr=1e-5) device = "cuda" if torch.cuda.is_available() else "cpu" @@ -67,7 +67,6 @@ def test_ill_opts(self): torch.ones((1, 1, 3), device=device), torch.ones((1, 1, 3), device=device) ) - @SkipIfBeforePyTorchVersion((1, 7, 0)) def test_script(self): input_param, input_data, expected_val = TEST_CASES[0] loss = MultiScaleLoss(**input_param) diff --git a/tests/test_randtorchvisiond.py b/tests/test_randtorchvisiond.py index 2e96d723ee..4fc1a1e630 100644 --- a/tests/test_randtorchvisiond.py +++ b/tests/test_randtorchvisiond.py @@ -16,7 +16,6 @@ from monai.transforms import Randomizable, RandTorchVisiond from monai.utils import set_determinism -from tests.utils import SkipIfBeforePyTorchVersion TEST_CASE_1 = [ {"keys": "img", "name": "ColorJitter"}, @@ -49,7 +48,6 @@ ] -@SkipIfBeforePyTorchVersion((1, 7)) class TestRandTorchVisiond(unittest.TestCase): @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3]) def test_value(self, input_param, input_data, expected_value): diff --git a/tests/test_splitdim.py b/tests/test_splitdim.py new file mode 100644 index 0000000000..623396a8fe --- /dev/null +++ b/tests/test_splitdim.py @@ -0,0 +1,50 @@ +# Copyright (c) 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 unittest + +import numpy as np +from parameterized import parameterized + +from monai.transforms.utility.array import SplitDim +from tests.utils import TEST_NDARRAYS + +TESTS = [] +for p in TEST_NDARRAYS: + for keepdim in (True, False): + TESTS.append(((2, 10, 8, 7), keepdim, p)) + + +class TestSplitDim(unittest.TestCase): + @parameterized.expand(TESTS) + def test_correct_shape(self, shape, keepdim, im_type): + arr = im_type(np.random.rand(*shape)) + for dim in range(arr.ndim): + out = SplitDim(dim, keepdim)(arr) + self.assertIsInstance(out, (list, tuple)) + self.assertEqual(len(out), arr.shape[dim]) + expected_ndim = arr.ndim if keepdim else arr.ndim - 1 + self.assertEqual(out[0].ndim, expected_ndim) + # assert is a shallow copy + arr[0, 0, 0, 0] *= 2 + self.assertEqual(arr.flatten()[0], out[0].flatten()[0]) + + def test_error(self): + """Should fail because splitting along singleton dimension""" + shape = (2, 1, 8, 7) + for p in TEST_NDARRAYS: + arr = p(np.random.rand(*shape)) + with self.assertRaises(RuntimeError): + _ = SplitDim(dim=1)(arr) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_splitdimd.py b/tests/test_splitdimd.py new file mode 100644 index 0000000000..6b164a3cb8 --- /dev/null +++ b/tests/test_splitdimd.py @@ -0,0 +1,78 @@ +# Copyright (c) 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 unittest +from copy import deepcopy + +import numpy as np +from parameterized import parameterized + +from monai.transforms import LoadImaged +from monai.transforms.utility.dictionary import SplitDimd +from tests.utils import TEST_NDARRAYS, assert_allclose, make_nifti_image, make_rand_affine + +TESTS = [] +for p in TEST_NDARRAYS: + for keepdim in (True, False): + for update_meta in (True, False): + TESTS.append((keepdim, p, update_meta)) + + +class TestSplitDimd(unittest.TestCase): + @classmethod + def setUpClass(cls): + arr = np.random.rand(2, 10, 8, 7) + affine = make_rand_affine() + data = {"i": make_nifti_image(arr, affine)} + + cls.data = LoadImaged("i")(data) + + @parameterized.expand(TESTS) + def test_correct(self, keepdim, im_type, update_meta): + data = deepcopy(self.data) + data["i"] = im_type(data["i"]) + arr = data["i"] + for dim in range(arr.ndim): + out = SplitDimd("i", dim=dim, keepdim=keepdim, update_meta=update_meta)(data) + self.assertIsInstance(out, dict) + num_new_keys = 2 if update_meta else 1 + self.assertEqual(len(out.keys()), len(data.keys()) + num_new_keys * arr.shape[dim]) + # if updating meta data, pick some random points and + # check same world coordinates between input and output + if update_meta: + for _ in range(10): + idx = [np.random.choice(i) for i in arr.shape] + split_im_idx = idx[dim] + split_idx = deepcopy(idx) + split_idx[dim] = 0 + # idx[1:] to remove channel and then add 1 for 4th element + real_world = data["i_meta_dict"]["affine"] @ (idx[1:] + [1]) + real_world2 = out[f"i_{split_im_idx}_meta_dict"]["affine"] @ (split_idx[1:] + [1]) + assert_allclose(real_world, real_world2) + + out = out["i_0"] + expected_ndim = arr.ndim if keepdim else arr.ndim - 1 + self.assertEqual(out.ndim, expected_ndim) + # assert is a shallow copy + arr[0, 0, 0, 0] *= 2 + self.assertEqual(arr.flatten()[0], out.flatten()[0]) + + def test_error(self): + """Should fail because splitting along singleton dimension""" + shape = (2, 1, 8, 7) + for p in TEST_NDARRAYS: + arr = p(np.random.rand(*shape)) + with self.assertRaises(RuntimeError): + _ = SplitDimd("i", dim=1)({"i": arr}) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_subpixel_upsample.py b/tests/test_subpixel_upsample.py index 0216f164c3..3e5370473c 100644 --- a/tests/test_subpixel_upsample.py +++ b/tests/test_subpixel_upsample.py @@ -18,6 +18,7 @@ from monai.networks import eval_mode from monai.networks.blocks import SubpixelUpsample from monai.networks.layers.factories import Conv +from tests.utils import SkipIfBeforePyTorchVersion, test_script_save TEST_CASE_SUBPIXEL = [] for inch in range(1, 5): @@ -73,6 +74,13 @@ def test_subpixel_shape(self, input_param, input_shape, expected_shape): result = net.forward(torch.randn(input_shape)) self.assertEqual(result.shape, expected_shape) + @SkipIfBeforePyTorchVersion((1, 8, 1)) + def test_script(self): + input_param, input_shape, _ = TEST_CASE_SUBPIXEL[0] + net = SubpixelUpsample(**input_param) + test_data = torch.randn(input_shape) + test_script_save(net, test_data) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_surface_dice.py b/tests/test_surface_dice.py new file mode 100644 index 0000000000..5252adafce --- /dev/null +++ b/tests/test_surface_dice.py @@ -0,0 +1,292 @@ +# Copyright (c) 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 unittest + +import numpy as np +import torch +import torch.nn.functional as F + +from monai.metrics.surface_dice import SurfaceDiceMetric + + +class TestAllSurfaceDiceMetrics(unittest.TestCase): + def test_tolerance_euclidean_distance(self): + batch_size = 2 + n_class = 2 + predictions = torch.zeros((batch_size, 480, 640), dtype=torch.int64) + labels = torch.zeros((batch_size, 480, 640), dtype=torch.int64) + predictions[0, :, 50:] = 1 + labels[0, :, 60:] = 1 # 10 px shift + predictions_hot = F.one_hot(predictions, num_classes=n_class).permute(0, 3, 1, 2) + labels_hot = F.one_hot(labels, num_classes=n_class).permute(0, 3, 1, 2) + + sd0 = SurfaceDiceMetric(class_thresholds=[0, 0], include_background=True) + res0 = sd0(predictions_hot, labels_hot) + agg0 = sd0.aggregate() # aggregation: nanmean across image then nanmean across batch + sd0_nans = SurfaceDiceMetric(class_thresholds=[0, 0], include_background=True, get_not_nans=True) + res0_nans = sd0_nans(predictions_hot, labels_hot) + agg0_nans, not_nans = sd0_nans.aggregate() + + np.testing.assert_array_equal(res0, res0_nans) + np.testing.assert_array_equal(agg0, agg0_nans) + + res1 = SurfaceDiceMetric(class_thresholds=[1, 1], include_background=True)(predictions_hot, labels_hot) + res9 = SurfaceDiceMetric(class_thresholds=[9, 9], include_background=True)(predictions_hot, labels_hot) + res10 = SurfaceDiceMetric(class_thresholds=[10, 10], include_background=True)(predictions_hot, labels_hot) + res11 = SurfaceDiceMetric(class_thresholds=[11, 11], include_background=True)(predictions_hot, labels_hot) + + for res in [res0, res9, res10, res11]: + assert res.shape == torch.Size([2, 2]) + + assert res0[0, 0] < res1[0, 0] < res9[0, 0] < res10[0, 0] + assert res0[0, 1] < res1[0, 1] < res9[0, 1] < res10[0, 1] + np.testing.assert_array_equal(res10, res11) + + expected_res0 = np.zeros((batch_size, n_class)) + expected_res0[0, 1] = 1 - (478 + 480 + 9 * 2) / (480 * 4 + 588 * 2 + 578 * 2) + expected_res0[0, 0] = 1 - (478 + 480 + 9 * 2) / (480 * 4 + 48 * 2 + 58 * 2) + expected_res0[1, 0] = 1 + expected_res0[1, 1] = np.nan + for b, c in np.ndindex(batch_size, n_class): + np.testing.assert_allclose(expected_res0[b, c], res0[b, c]) + np.testing.assert_array_equal(agg0, np.nanmean(np.nanmean(expected_res0, axis=1), axis=0)) + np.testing.assert_equal(not_nans, torch.tensor(2)) + + def test_tolerance_all_distances(self): + batch_size = 1 + n_class = 2 + predictions = torch.zeros((batch_size, 10, 10), dtype=torch.int64) + labels = torch.zeros((batch_size, 10, 10), dtype=torch.int64) + predictions[0, 1:4, 1] = 1 + """ + [[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]] + """ + labels[0, 5:8, 6] = 1 + """ + [[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]] + """ + predictions_hot = F.one_hot(predictions, num_classes=n_class).permute(0, 3, 1, 2) + labels_hot = F.one_hot(labels, num_classes=n_class).permute(0, 3, 1, 2) + + # Euclidean distance: + # background: + # 36 boundary pixels have 0 distances; non-zero distances: + # distances gt_pred: [3, np.sqrt(9+4), 2, 3, 2, 2, 2, 1] + # distances pred_gt: [1, 2, 2, 1] + # class 1: + # distances gt_pred: [sqrt(25+4), sqrt(25+9), sqrt(25+16)] = [5.38516481, 5.83095189, 6.40312424] + # distances pred_gt: [sqrt(25+16), sqrt(25+9), sqrt(25+4)] = [6.40312424, 5.83095189, 5.38516481] + + res = SurfaceDiceMetric(class_thresholds=[0, 0], include_background=True)(predictions_hot, labels_hot) + expected_res = [[1 - (8 + 4) / (36 * 2 + 8 + 4), 0]] + np.testing.assert_array_almost_equal(res, expected_res) + + res = SurfaceDiceMetric(class_thresholds=[2.8, 5.5], include_background=True)(predictions_hot, labels_hot) + expected_res = [[1 - 3 / (36 * 2 + 8 + 4), 1 - (2 + 2) / (3 + 3)]] + np.testing.assert_array_almost_equal(res, expected_res) + + res = SurfaceDiceMetric(class_thresholds=[3, 6], include_background=True)(predictions_hot, labels_hot) + expected_res = [[1 - 1 / (36 * 2 + 8 + 4), 1 - 2 / (3 + 3)]] + np.testing.assert_array_almost_equal(res, expected_res) + + # Chessboard distance: + # background: + # 36 boundary pixels have 0 distances; non-zero distances: + # distances gt_pred: [max(3,0), max(3,2), max(2,0), max(3,3), max(2,0), max(0,2), max(2,0), max(0,1)] = + # [3, 3, 2, 3, 2, 2, 2, 1] + # distances pred_gt: [max(0,1), max(2,0), max(2,0), max(1,0)] = [1, 2, 2, 1] + # class 1: + # distances gt_pred: [max(5,2), max(5,3), max(5,4)] = [5, 5, 5] + # distances pred_gt: [max(5,4), max(5,3), max(5,2)] = [5, 5, 5] + + res = SurfaceDiceMetric(class_thresholds=[0, 0], include_background=True, distance_metric="chessboard")( + predictions_hot, labels_hot + ) + expected_res = [[1 - (8 + 4) / (36 * 2 + 8 + 4), 0]] + np.testing.assert_array_almost_equal(res, expected_res) + + res = SurfaceDiceMetric(class_thresholds=[1, 4.999], include_background=True, distance_metric="chessboard")( + predictions_hot, labels_hot + ) + expected_res = [[1 - (7 + 2) / (36 * 2 + 8 + 4), 0]] + np.testing.assert_array_almost_equal(res, expected_res) + + res = SurfaceDiceMetric(class_thresholds=[2, 5], include_background=True, distance_metric="chessboard")( + predictions_hot, labels_hot + ) + expected_res = [[1 - 3 / (36 * 2 + 8 + 4), 1]] + np.testing.assert_array_almost_equal(res, expected_res) + + # Taxicab distance (= Manhattan distance): + # background: + # 36 boundary pixels have 0 distances; non-zero distances: + # distances gt_pred: [3+0, 4+0, 2+0, 0+3, 2+0, 0+2, 2+0, 0+1] = [3, 4, 2, 3, 2, 2, 2, 1] + # distances pred_gt: [0+1, 2+0, 2+0, 1+0] = [1, 2, 2, 1] + # class 1: + # distances gt_pred: [5+2, 5+3, 5+4] = [7, 8, 9] + # distances pred_gt: [5+4, 5+3, 5+2] = [9, 8, 7] + + res = SurfaceDiceMetric(class_thresholds=[0, 0], include_background=True, distance_metric="taxicab")( + predictions_hot, labels_hot + ) + expected_res = [[1 - (8 + 4) / (36 * 2 + 8 + 4), 0]] + np.testing.assert_array_almost_equal(res, expected_res) + + res = SurfaceDiceMetric(class_thresholds=[1, 7], include_background=True, distance_metric="taxicab")( + predictions_hot, labels_hot + ) + expected_res = [[1 - (7 + 2) / (36 * 2 + 8 + 4), 1 - (2 + 2) / (3 + 3)]] + np.testing.assert_array_almost_equal(res, expected_res) + + res = SurfaceDiceMetric(class_thresholds=[3, 9], include_background=True, distance_metric="taxicab")( + predictions_hot, labels_hot + ) + expected_res = [[1 - 1 / (36 * 2 + 8 + 4), 1]] + np.testing.assert_array_almost_equal(res, expected_res) + + def test_asserts(self): + batch_size = 1 + n_class = 2 + predictions = torch.zeros((batch_size, 80, 80), dtype=torch.int64) + labels = torch.zeros((batch_size, 80, 80), dtype=torch.int64) + predictions[0, 10:20, 10:20] = 1 + labels[0, 20:30, 20:30] = 1 + predictions_hot = F.one_hot(predictions, num_classes=n_class).permute(0, 3, 1, 2) + labels_hot = F.one_hot(labels, num_classes=n_class).permute(0, 3, 1, 2) + + # no torch tensor + with self.assertRaises(ValueError) as context: + SurfaceDiceMetric(class_thresholds=[1, 1], include_background=True)(predictions_hot.numpy(), labels_hot) + self.assertEqual( + "y_pred or y must be a list/tuple of `channel-first` Tensors or a `batch-first` Tensor.", + str(context.exception), + ) + with self.assertRaises(ValueError) as context: + SurfaceDiceMetric(class_thresholds=[1, 1], include_background=True)(predictions_hot, labels_hot.numpy()) + self.assertEqual("y_pred and y must be PyTorch Tensor.", str(context.exception)) + + # wrong dimensions + with self.assertRaises(ValueError) as context: + SurfaceDiceMetric(class_thresholds=[1, 1], include_background=True)(predictions, labels_hot) + self.assertEqual("y_pred and y should have four dimensions: [B,C,H,W].", str(context.exception)) + with self.assertRaises(ValueError) as context: + SurfaceDiceMetric(class_thresholds=[1, 1], include_background=True)(predictions_hot, labels) + self.assertEqual("y_pred and y should have four dimensions: [B,C,H,W].", str(context.exception)) + + # mismatch of shape of input tensors + input_bad_shape = torch.clone(predictions_hot) + input_bad_shape = input_bad_shape[:, :, :, :50] + + with self.assertRaises(ValueError) as context: + SurfaceDiceMetric(class_thresholds=[1, 1], include_background=True)(predictions_hot, input_bad_shape) + self.assertEqual( + "y_pred and y should have same shape, but instead, shapes are torch.Size([1, 2, 80, 80]) (y_pred) and " + "torch.Size([1, 2, 80, 50]) (y).", + str(context.exception), + ) + + # input tensors not one-hot encoded + predictions_no_hot = torch.clone(predictions_hot) + predictions_no_hot[0, :, 0, 0] = torch.tensor([2, 0]) + + with self.assertRaises(ValueError) as context: + SurfaceDiceMetric(class_thresholds=[1, 1], include_background=True)(predictions_no_hot, predictions_hot) + self.assertEqual("y_pred and y should be one-hot encoded.", str(context.exception)) + with self.assertRaises(ValueError) as context: + SurfaceDiceMetric(class_thresholds=[1, 1], include_background=True)(predictions_hot, predictions_no_hot) + self.assertEqual("y_pred and y should be one-hot encoded.", str(context.exception)) + + predictions_no_hot = predictions_no_hot.float() + predictions_no_hot[0, :, 0, 0] = torch.tensor([0.5, 0]) + with self.assertRaises(ValueError) as context: + SurfaceDiceMetric(class_thresholds=[1, 1], include_background=True)(predictions_no_hot, predictions_hot) + self.assertEqual("y_pred and y should be binarized tensors (e.g. torch.int64).", str(context.exception)) + with self.assertRaises(ValueError) as context: + SurfaceDiceMetric(class_thresholds=[1, 1], include_background=True)(predictions_hot, predictions_no_hot) + self.assertEqual("y_pred and y should be binarized tensors (e.g. torch.int64).", str(context.exception)) + + # wrong number of class thresholds + with self.assertRaises(ValueError) as context: + SurfaceDiceMetric(class_thresholds=[1, 1, 1], include_background=True)(predictions_hot, labels_hot) + self.assertEqual("number of classes (2) does not match number of class thresholds (3).", str(context.exception)) + + # inf and nan values in class thresholds + with self.assertRaises(ValueError) as context: + SurfaceDiceMetric(class_thresholds=[np.inf, 1], include_background=True)(predictions_hot, labels_hot) + self.assertEqual("All class thresholds need to be finite.", str(context.exception)) + + with self.assertRaises(ValueError) as context: + SurfaceDiceMetric(class_thresholds=[np.nan, 1], include_background=True)(predictions_hot, labels_hot) + self.assertEqual("All class thresholds need to be finite.", str(context.exception)) + + # negative values in class thresholds: + with self.assertRaises(ValueError) as context: + SurfaceDiceMetric(class_thresholds=[-0.22, 1], include_background=True)(predictions_hot, labels_hot) + self.assertEqual("All class thresholds need to be >= 0.", str(context.exception)) + + def test_not_predicted_not_present(self): + # class is present in labels, but not in prediction -> nsd of 0 should be yielded for that class; class is + # neither present on labels, nor prediction -> nan should be yielded + batch_size = 1 + n_class = 4 + predictions = torch.zeros((batch_size, 80, 80), dtype=torch.int64) + labels = torch.zeros((batch_size, 80, 80), dtype=torch.int64) + predictions[0, 10:20, 10:20] = 1 + labels[0, 10:20, 10:20] = 2 + predictions_hot = F.one_hot(predictions, num_classes=n_class).permute(0, 3, 1, 2) + labels_hot = F.one_hot(labels, num_classes=n_class).permute(0, 3, 1, 2) + + # with and without background class + sur_metric_bgr = SurfaceDiceMetric(class_thresholds=[1, 1, 1, 1], include_background=True) + sur_metric = SurfaceDiceMetric(class_thresholds=[1, 1, 1], include_background=False) + + # test per-class results + res_bgr_classes = sur_metric_bgr(predictions_hot, labels_hot) + np.testing.assert_array_equal(res_bgr_classes, [[1, 0, 0, np.nan]]) + res_classes = sur_metric(predictions_hot, labels_hot) + np.testing.assert_array_equal(res_classes, [[0, 0, np.nan]]) + + # test aggregation + res_bgr = sur_metric_bgr.aggregate() + np.testing.assert_equal(res_bgr, torch.tensor([1 / 3], dtype=torch.float64)) + res = sur_metric.aggregate() + np.testing.assert_equal(res, torch.tensor([0], dtype=torch.float64)) + + predictions_empty = torch.zeros((2, 3, 1, 1)) + sur_metric_nans = SurfaceDiceMetric(class_thresholds=[1, 1, 1], include_background=True, get_not_nans=True) + res_classes = sur_metric_nans(predictions_empty, predictions_empty) + res, not_nans = sur_metric_nans.aggregate() + np.testing.assert_array_equal(res_classes, [[np.nan, np.nan, np.nan], [np.nan, np.nan, np.nan]]) + np.testing.assert_equal(res, torch.tensor([0], dtype=torch.float64)) + np.testing.assert_equal(not_nans, torch.tensor([0], dtype=torch.float64)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_to_from_meta_tensord.py b/tests/test_to_from_meta_tensord.py new file mode 100644 index 0000000000..9bbf4592ab --- /dev/null +++ b/tests/test_to_from_meta_tensord.py @@ -0,0 +1,182 @@ +# Copyright (c) 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 random +import string +import unittest +from copy import deepcopy +from typing import Optional, Union + +import torch +from parameterized import parameterized + +from monai.data.meta_tensor import MetaTensor +from monai.transforms import FromMetaTensord, ToMetaTensord +from monai.utils.enums import PostFix +from monai.utils.module import get_torch_version_tuple +from tests.utils import TEST_DEVICES, assert_allclose + +PT_VER_MAJ, PT_VER_MIN = get_torch_version_tuple() + +DTYPES = [[torch.float32], [torch.float64], [torch.float16], [torch.int64], [torch.int32]] +TESTS = [] +for _device in TEST_DEVICES: + for _dtype in DTYPES: + TESTS.append((*_device, *_dtype)) + + +def rand_string(min_len=5, max_len=10): + str_size = random.randint(min_len, max_len) + chars = string.ascii_letters + string.punctuation + return "".join(random.choice(chars) for _ in range(str_size)) + + +class TestToFromMetaTensord(unittest.TestCase): + @staticmethod + def get_im(shape=None, dtype=None, device=None): + if shape is None: + shape = shape = (1, 10, 8) + affine = torch.randint(0, 10, (4, 4)) + meta = {"fname": rand_string()} + t = torch.rand(shape) + if dtype is not None: + t = t.to(dtype) + if device is not None: + t = t.to(device) + m = MetaTensor(t.clone(), affine, meta) + return m + + def check_ids(self, a, b, should_match): + comp = self.assertEqual if should_match else self.assertNotEqual + comp(id(a), id(b)) + + def check( + self, + out: torch.Tensor, + orig: torch.Tensor, + *, + shape: bool = True, + vals: bool = True, + ids: bool = True, + device: Optional[Union[str, torch.device]] = None, + meta: bool = True, + check_ids: bool = True, + **kwargs, + ): + if device is None: + device = orig.device + + # check the image + self.assertIsInstance(out, type(orig)) + if shape: + assert_allclose(torch.as_tensor(out.shape), torch.as_tensor(orig.shape)) + if vals: + assert_allclose(out, orig, **kwargs) + if check_ids: + self.check_ids(out, orig, ids) + self.assertTrue(str(device) in str(out.device)) + + # check meta and affine are equal and affine is on correct device + if isinstance(orig, MetaTensor) and isinstance(out, MetaTensor) and meta: + orig_meta_no_affine = deepcopy(orig.meta) + del orig_meta_no_affine["affine"] + out_meta_no_affine = deepcopy(out.meta) + del out_meta_no_affine["affine"] + self.assertEqual(orig_meta_no_affine, out_meta_no_affine) + assert_allclose(out.affine, orig.affine) + self.assertTrue(str(device) in str(out.affine.device)) + if check_ids: + self.check_ids(out.affine, orig.affine, ids) + self.check_ids(out.meta, orig.meta, ids) + + @parameterized.expand(TESTS) + def test_from_to_meta_tensord(self, device, dtype): + m1 = self.get_im(device=device, dtype=dtype) + m2 = self.get_im(device=device, dtype=dtype) + m3 = self.get_im(device=device, dtype=dtype) + d_metas = {"m1": m1, "m2": m2, "m3": m3} + m1_meta = {k: v for k, v in m1.meta.items() if k != "affine"} + m1_aff = m1.affine + + # FROM -> forward + t_from_meta = FromMetaTensord(["m1", "m2"]) + d_dict = t_from_meta(d_metas) + + self.assertEqual( + sorted(d_dict.keys()), + [ + "m1", + PostFix.meta("m1"), + PostFix.transforms("m1"), + "m2", + PostFix.meta("m2"), + PostFix.transforms("m2"), + "m3", + ], + ) + self.check(d_dict["m3"], m3, ids=True) # unchanged + self.check(d_dict["m1"], m1.as_tensor(), ids=False) + meta_out = {k: v for k, v in d_dict["m1_meta_dict"].items() if k != "affine"} + aff_out = d_dict["m1_meta_dict"]["affine"] + self.check(aff_out, m1_aff, ids=True) + self.assertEqual(meta_out, m1_meta) + + # FROM -> inverse + d_meta_dict_meta = t_from_meta.inverse(d_dict) + self.assertEqual( + sorted(d_meta_dict_meta.keys()), ["m1", PostFix.transforms("m1"), "m2", PostFix.transforms("m2"), "m3"] + ) + self.check(d_meta_dict_meta["m3"], m3, ids=False) # unchanged (except deep copy in inverse) + self.check(d_meta_dict_meta["m1"], m1, ids=False) + meta_out = {k: v for k, v in d_meta_dict_meta["m1"].meta.items() if k != "affine"} + aff_out = d_meta_dict_meta["m1"].affine + self.check(aff_out, m1_aff, ids=False) + self.assertEqual(meta_out, m1_meta) + + # TO -> Forward + t_to_meta = ToMetaTensord(["m1", "m2"]) + del d_dict["m1_transforms"] + del d_dict["m2_transforms"] + d_dict_meta = t_to_meta(d_dict) + self.assertEqual( + sorted(d_dict_meta.keys()), ["m1", PostFix.transforms("m1"), "m2", PostFix.transforms("m2"), "m3"] + ) + self.check(d_dict_meta["m3"], m3, ids=True) # unchanged (except deep copy in inverse) + self.check(d_dict_meta["m1"], m1, ids=False) + meta_out = {k: v for k, v in d_dict_meta["m1"].meta.items() if k != "affine"} + aff_out = d_dict_meta["m1"].meta["affine"] + self.check(aff_out, m1_aff, ids=False) + self.assertEqual(meta_out, m1_meta) + + # TO -> Inverse + d_dict_meta_dict = t_to_meta.inverse(d_dict_meta) + self.assertEqual( + sorted(d_dict_meta_dict.keys()), + [ + "m1", + PostFix.meta("m1"), + PostFix.transforms("m1"), + "m2", + PostFix.meta("m2"), + PostFix.transforms("m2"), + "m3", + ], + ) + self.check(d_dict_meta_dict["m3"], m3.as_tensor(), ids=False) # unchanged (except deep copy in inverse) + self.check(d_dict_meta_dict["m1"], m1.as_tensor(), ids=False) + meta_out = {k: v for k, v in d_dict_meta_dict["m1_meta_dict"].items() if k != "affine"} + aff_out = d_dict_meta_dict["m1_meta_dict"]["affine"] + self.check(aff_out, m1_aff, ids=False) + self.assertEqual(meta_out, m1_meta) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_torchscript_utils.py b/tests/test_torchscript_utils.py index d6bea09ed6..cdf2f19eb3 100644 --- a/tests/test_torchscript_utils.py +++ b/tests/test_torchscript_utils.py @@ -18,7 +18,6 @@ from monai.config import get_config_values from monai.data import load_net_with_metadata, save_net_with_metadata from monai.utils import JITMetadataKeys -from monai.utils.module import pytorch_after class TestModule(torch.nn.Module): @@ -102,10 +101,7 @@ def test_save_load_more_extra_files(self): _, _, loaded_extra_files = load_net_with_metadata(f"{tempdir}/test.ts", more_extra_files=("test.txt",)) - if pytorch_after(1, 7): - self.assertEqual(more_extra_files["test.txt"], loaded_extra_files["test.txt"]) - else: - self.assertEqual(more_extra_files["test.txt"].decode(), loaded_extra_files["test.txt"]) + self.assertEqual(more_extra_files["test.txt"], loaded_extra_files["test.txt"]) if __name__ == "__main__": diff --git a/tests/test_torchvision.py b/tests/test_torchvision.py index e0844eb4b9..68b9413e65 100644 --- a/tests/test_torchvision.py +++ b/tests/test_torchvision.py @@ -15,7 +15,7 @@ from monai.transforms import TorchVision from monai.utils import set_determinism -from tests.utils import TEST_NDARRAYS, SkipIfBeforePyTorchVersion, assert_allclose +from tests.utils import TEST_NDARRAYS, assert_allclose TESTS = [] for p in TEST_NDARRAYS: @@ -52,7 +52,6 @@ ) -@SkipIfBeforePyTorchVersion((1, 7)) class TestTorchVision(unittest.TestCase): @parameterized.expand(TESTS) def test_value(self, input_param, input_data, expected_value): diff --git a/tests/test_torchvisiond.py b/tests/test_torchvisiond.py index 4c62c6e41a..def26fa26b 100644 --- a/tests/test_torchvisiond.py +++ b/tests/test_torchvisiond.py @@ -16,7 +16,6 @@ from monai.transforms import TorchVisiond from monai.utils import set_determinism -from tests.utils import SkipIfBeforePyTorchVersion TEST_CASE_1 = [ {"keys": "img", "name": "ColorJitter"}, @@ -49,7 +48,6 @@ ] -@SkipIfBeforePyTorchVersion((1, 7)) class TestTorchVisiond(unittest.TestCase): @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3]) def test_value(self, input_param, input_data, expected_value): diff --git a/tests/test_tversky_loss.py b/tests/test_tversky_loss.py index 2bb2409360..22f57cc8c6 100644 --- a/tests/test_tversky_loss.py +++ b/tests/test_tversky_loss.py @@ -16,7 +16,7 @@ from parameterized import parameterized from monai.losses import TverskyLoss -from tests.utils import SkipIfBeforePyTorchVersion, test_script_save +from tests.utils import test_script_save TEST_CASES = [ [ # shape: (1, 1, 2, 2), (1, 1, 2, 2) @@ -175,7 +175,6 @@ def test_input_warnings(self): loss = TverskyLoss(to_onehot_y=True) loss.forward(chn_input, chn_target) - @SkipIfBeforePyTorchVersion((1, 7, 0)) def test_script(self): loss = TverskyLoss() test_input = torch.ones(2, 1, 8, 8) diff --git a/tests/test_utils_pytorch_numpy_unification.py b/tests/test_utils_pytorch_numpy_unification.py index b13378debe..1b08a5bd2f 100644 --- a/tests/test_utils_pytorch_numpy_unification.py +++ b/tests/test_utils_pytorch_numpy_unification.py @@ -12,12 +12,11 @@ import unittest import numpy as np -import torch from parameterized import parameterized from monai.transforms.utils_pytorch_numpy_unification import mode, percentile from monai.utils import set_determinism -from tests.utils import TEST_NDARRAYS, SkipIfBeforePyTorchVersion, assert_allclose +from tests.utils import TEST_NDARRAYS, assert_allclose TEST_MODE = [] for p in TEST_NDARRAYS: @@ -37,10 +36,7 @@ def test_percentile(self): for p in TEST_NDARRAYS: arr = p(np.arange(100 * 101).reshape(1, 100, 101).astype(np.float32)) results.append(percentile(arr, q)) - # pre torch 1.7, no `quantile`. Our own method doesn't interpolate, - # so we can only be accurate to 0.5 - atol = 0.5 if not hasattr(torch, "quantile") else 1e-4 - assert_allclose(results[0], results[-1], type_test=False, atol=atol) + assert_allclose(results[0], results[-1], type_test=False, atol=1e-4) def test_fails(self): for p in TEST_NDARRAYS: @@ -49,17 +45,13 @@ def test_fails(self): with self.assertRaises(ValueError): percentile(arr, q) - @SkipIfBeforePyTorchVersion((1, 7)) def test_dim(self): q = np.random.randint(0, 100, size=50) results = [] for p in TEST_NDARRAYS: arr = p(np.arange(6).reshape(1, 2, 3).astype(np.float32)) results.append(percentile(arr, q, dim=1)) - # pre torch 1.7, no `quantile`. Our own method doesn't interpolate, - # so we can only be accurate to 0.5 - atol = 0.5 if not hasattr(torch, "quantile") else 1e-4 - assert_allclose(results[0], results[-1], type_test=False, atol=atol) + assert_allclose(results[0], results[-1], type_test=False, atol=1e-4) @parameterized.expand(TEST_MODE) def test_mode(self, array, expected, to_long): diff --git a/tests/utils.py b/tests/utils.py index 3065f9b3df..c4f9bd1b70 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -102,8 +102,8 @@ def assert_allclose( if isinstance(desired, torch.Tensor) or isinstance(actual, torch.Tensor): if device_test: np.testing.assert_equal(str(actual.device), str(desired.device), "torch device check") # type: ignore - actual = actual.cpu().numpy() if isinstance(actual, torch.Tensor) else actual - desired = desired.cpu().numpy() if isinstance(desired, torch.Tensor) else desired + actual = actual.detach().cpu().numpy() if isinstance(actual, torch.Tensor) else actual + desired = desired.detach().cpu().numpy() if isinstance(desired, torch.Tensor) else desired np.testing.assert_allclose(actual, desired, *args, **kwargs) @@ -715,5 +715,10 @@ def query_memory(n=2): TEST_NDARRAYS = TEST_NDARRAYS + (gpu_tensor,) # type: ignore +TEST_DEVICES = [[torch.device("cpu")]] +if torch.cuda.is_available(): + TEST_DEVICES.append([torch.device("cuda")]) + + if __name__ == "__main__": print(query_memory())