diff --git a/.gitattributes b/.gitattributes index 6ae8fe7383ab9af1e51554c790ba025c73049923..ba514c652617311193743b3a49e151c09fc55626 100644 --- a/.gitattributes +++ b/.gitattributes @@ -331,3 +331,8 @@ my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/opencv_pyth my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/pyzmq.libs/libsodium-bcf9f097.so.23.3.0 filter=lfs diff=lfs merge=lfs -text my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/yarl/_quoting_c.cpython-38-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/h5py.libs/libaec-9c9e97eb.so.0.0.10 filter=lfs diff=lfs merge=lfs -text +my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/opencv_python.libs/libQt5Test-c38a5234.so.5.15.0 filter=lfs diff=lfs merge=lfs -text +my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/scipy.libs/libgfortran-040039e1.so.5.0.0 filter=lfs diff=lfs merge=lfs -text +my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/brotli/_brotli.abi3.so filter=lfs diff=lfs merge=lfs -text +my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/opencv_python.libs/libxcb-xkb-9ba31ab3.so.1.0.0 filter=lfs diff=lfs merge=lfs -text +my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/opencv_python.libs/libssl-28bef1ac.so.1.1 filter=lfs diff=lfs merge=lfs -text diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/brotli/_brotli.abi3.so b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/brotli/_brotli.abi3.so new file mode 100644 index 0000000000000000000000000000000000000000..a351b3b99ddcea0186c984e449fb180c8cadb35e --- /dev/null +++ b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/brotli/_brotli.abi3.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c9ea7e74f258c0527249f553bdd1a136e016017a222f38186df92e85361a4f0 +size 746208 diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/_extensions/__pycache__/__init__.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/_extensions/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..35137c48ded9bc0df4c516829f4008151bdf76a8 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/_extensions/__pycache__/__init__.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/_extensions/__pycache__/loader.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/_extensions/__pycache__/loader.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..87cd1dc93533d76b86a81331689d2acc1a0323b8 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/_extensions/__pycache__/loader.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/_extensions/gmm/gmm.cpp b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/_extensions/gmm/gmm.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4087095340a5fad91903fdbb8d1c91e8ae24c70a --- /dev/null +++ b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/_extensions/gmm/gmm.cpp @@ -0,0 +1,85 @@ +/* +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. +*/ + +#include + +#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); +} + +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; +} + +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/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/_extensions/gmm/gmm.h b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/_extensions/gmm/gmm.h new file mode 100644 index 0000000000000000000000000000000000000000..09c0389ae66f0161e3ca4d997f0ce0a95e66e5df --- /dev/null +++ b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/_extensions/gmm/gmm.h @@ -0,0 +1,53 @@ +/* +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. +*/ + +#if !defined(CHANNEL_COUNT) || !defined(MIXTURE_COUNT) || !defined(MIXTURE_SIZE) +#error Definition of CHANNEL_COUNT, MIXTURE_COUNT, and MIXTURE_SIZE required +#endif + +#if CHANNEL_COUNT < 1 || MIXTURE_COUNT < 1 || MIXTURE_SIZE < 1 +#error CHANNEL_COUNT, MIXTURE_COUNT, and MIXTURE_SIZE must be positive +#endif + +#define MATRIX_COMPONENT_COUNT ((CHANNEL_COUNT + 1) * (CHANNEL_COUNT + 2) / 2) +#define SUB_MATRIX_COMPONENT_COUNT (CHANNEL_COUNT * (CHANNEL_COUNT + 1) / 2) +#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_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/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/_extensions/gmm/gmm_cpu.cpp b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/_extensions/gmm/gmm_cpu.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d7eedc07c8602b9c08e09d2be1c4431eb6045d7e --- /dev/null +++ b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/_extensions/gmm/gmm_cpu.cpp @@ -0,0 +1,35 @@ +/* +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. +*/ + +#include + +#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 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/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/_extensions/gmm/gmm_cuda.cu b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/_extensions/gmm/gmm_cuda.cu new file mode 100644 index 0000000000000000000000000000000000000000..2cf70a9920efe99e47adbf86ae00529cfb282c23 --- /dev/null +++ b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/_extensions/gmm/gmm_cuda.cu @@ -0,0 +1,518 @@ +/* +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. +*/ + +#include +#include + +#include "gmm.h" + +#include "gmm_cuda_linalg.cuh" + +#define EPSILON 1e-5 +#define BLOCK_SIZE 32 +#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; + + __shared__ float s_matrix_component[warp_count]; + + 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; + + 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]; + + 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]; + + 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(); + + 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(); + + 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) { + 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; + + __shared__ float s_matrix_component[warp_count]; + __shared__ float s_gmm[GMM_COMPONENT_COUNT]; + + 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; + + 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); + + 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 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]; + } + } + + 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(); + + 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 (i != 0 && i == j) { + constant -= EPSILON; + } + + s_gmm[index] = norm_factor * matrix_component - constant; + + 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; + + 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; + } + } + + to_square(matrix, square_mat); + cholesky(square_mat, cholesky_mat); + + *det_ptr = chol_det(cholesky_mat); + + 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]; + } +} + +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; + + 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; + + 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; + + 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)); + + if (max_value == eigenvalue) { + GMMSplit_t split; + + float* average_feature = gmm + gmm_idx * GMM_COMPONENT_COUNT + 1; + + split.idx = threadIdx.x; + split.threshold = scalar_prod(average_feature, eigenvector); + + 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]; + + 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; + + 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]; + } + + __syncthreads(); + + int index = threadIdx.x + blockIdx.x * BLOCK_SIZE * DO_SPLIT_DEGENERACY; + + for (int i = 0; i < DO_SPLIT_DEGENERACY; i++) { + index += BLOCK_SIZE; + + 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 (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]; + } + + 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; + } + } + } + } + } +} + +// Single block, 32xMIXTURE_COUNT +__global__ void GMMcommonTerm(float* g_gmm) { + int batch_index = blockIdx.z; + + float* g_batch_gmm = g_gmm + batch_index * GMM_COUNT * GMM_COMPONENT_COUNT; + + 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 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); + + 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; + } +} + +__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]; + + for (int i = 0; i < CHANNEL_COUNT; i++) { + diff[i] = feature[i] - average_feature[i]; + } + + 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]; + + value += i == j ? term : 2 * term; + } + } + + 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; + + 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; + + if (index >= element_count) + return; + + 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 < MIXTURE_COUNT; i++) { + float mixture_weight = 0.0f; + + for (int j = 0; j < MIXTURE_SIZE; j++) { + mixture_weight += GMMTerm(feature, &g_batch_gmm[(MIXTURE_COUNT * j + i) * GMM_COMPONENT_COUNT]); + } + + 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 +#define WARPS 16 +#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); + + float* block_gmm_scratch = scratch_mem; + GMMSplit_t* gmm_split_scratch = (GMMSplit_t*)scratch_mem; + + 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); + } + + 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); + } +} + +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; + + 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); + } + + CovarianceFinalizationKernel<<<{gmm_N, 1, batch_count}, BLOCK>>>(block_gmm_scratch, gmm, block_count); + + 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); + + 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; + + 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); +} + +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/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/_extensions/gmm/gmm_cuda_linalg.cuh b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/_extensions/gmm/gmm_cuda_linalg.cuh new file mode 100644 index 0000000000000000000000000000000000000000..56c7c7ccdcd53b7bb5c24dcba660af35571caa76 --- /dev/null +++ b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/_extensions/gmm/gmm_cuda_linalg.cuh @@ -0,0 +1,144 @@ +/* +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. +*/ + +__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 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; + + for (int i = 0; i < CHANNEL_COUNT; i++) { + det *= in[i][i]; + } + + 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); + + 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]; + } + + 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]; + } + } + } +} + +__device__ void normalize(float* v) { + float norm = 0.0f; + + for (int i = 0; i < CHANNEL_COUNT; i++) { + norm += v[i] * v[i]; + } + + norm = 1.0f / sqrtf(norm); + + for (int i = 0; i < CHANNEL_COUNT; i++) { + v[i] *= norm; + } +} + +__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]; + } + + 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; + } + + for (int itr = 0; itr < 10; itr++) { + *eval = 0.0f; + + for (int i = 0; i < CHANNEL_COUNT; i++) { + int index = i; + + evec[i] = 0.0f; + + for (int j = 0; j < CHANNEL_COUNT; j++) { + evec[i] += M[index] * scratch[j]; + + if (j < i) { + index += CHANNEL_COUNT - (j + 1); + } else { + index += 1; + } + } + + *eval = max(*eval, evec[i]); + } + + for (int i = 0; i < CHANNEL_COUNT; i++) { + evec[i] /= *eval; + scratch[i] = evec[i]; + } + } +} diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/engines/__pycache__/__init__.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/engines/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..782b8c5216ad9cf6be90943a6a465ecc81bdcb66 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/engines/__pycache__/__init__.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/engines/__pycache__/evaluator.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/engines/__pycache__/evaluator.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1064b8a1fcc1a5db6c1a8446bb2d559fae266a19 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/engines/__pycache__/evaluator.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/engines/__pycache__/multi_gpu_supervised_trainer.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/engines/__pycache__/multi_gpu_supervised_trainer.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..acd7eb5129b0c187ca963fdb0f4465b1c6146aa2 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/engines/__pycache__/multi_gpu_supervised_trainer.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/engines/__pycache__/trainer.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/engines/__pycache__/trainer.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..14814e6e32ebc715a03547f0bceaf3369a43121c Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/engines/__pycache__/trainer.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/engines/__pycache__/utils.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/engines/__pycache__/utils.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..270d428ba5a9e8dcd14a9b86036466bf514b1fef Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/engines/__pycache__/utils.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/engines/__pycache__/workflow.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/engines/__pycache__/workflow.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3e48484d5ae301e5d5267d1078a5c849434b7148 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/engines/__pycache__/workflow.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/__init__.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b3c31c495ea3262f51e7e6898fdd2eda95fb25c8 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/__init__.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/checkpoint_saver.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/checkpoint_saver.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..628a8c266a67f2c31d422cf3ce2f9e1b4efd1822 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/checkpoint_saver.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/classification_saver.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/classification_saver.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d4524de9a2951f82555992d85b08ed97ea529ef4 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/classification_saver.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/confusion_matrix.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/confusion_matrix.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6f4af04ff37772d6a47e0bfacd4f1b3d79dbb1bc Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/confusion_matrix.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/decollate_batch.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/decollate_batch.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..81775df7713b9a8e358fb960e612a8e61b58c76f Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/decollate_batch.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/earlystop_handler.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/earlystop_handler.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..997df17b36f01a2c71a506a46de78312c925b077 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/earlystop_handler.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/garbage_collector.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/garbage_collector.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..13d678e6e4180e30e11f1e9c1f1a60ac2651c439 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/garbage_collector.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/hausdorff_distance.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/hausdorff_distance.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cb8c0dde27c6d6cf40ff09c31ec9ba68d48d3776 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/hausdorff_distance.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/ignite_metric.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/ignite_metric.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7866fbf003e38d8768c82402a9dfaa3450d1929d Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/ignite_metric.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/lr_schedule_handler.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/lr_schedule_handler.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f61c2321202d1d05a44e472acb96a848863cd1f4 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/lr_schedule_handler.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/mean_dice.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/mean_dice.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..403b46279a9b882763d403f329fef8e964d9059c Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/mean_dice.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/metric_logger.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/metric_logger.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f4ad4029c7b339d00ac2183c639c5ec3b65f47c6 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/metric_logger.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/metrics_saver.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/metrics_saver.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9a22d3f181eb06165b57ee30dc12d955c3f1e7ad Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/metrics_saver.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/mlflow_handler.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/mlflow_handler.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..18374933331de391dc158aaa3a3fabadf6727bad Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/mlflow_handler.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/nvtx_handlers.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/nvtx_handlers.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1deadbeb5bf1e4116f81118eeba4f90bbe7033d5 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/nvtx_handlers.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/postprocessing.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/postprocessing.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4b29ec070e02df0aeff3d4fab0b1e063aca3fc9d Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/postprocessing.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/probability_maps.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/probability_maps.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..405e285ce4ef86ca27d1f7314f96860b4af1fcb6 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/probability_maps.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/regression_metrics.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/regression_metrics.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e0a6cced51064986577488f2d7439077f1d7603d Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/regression_metrics.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/roc_auc.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/roc_auc.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8cc45a0ecb5b7d29fbd8e4e42b949561a1f8c2b7 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/roc_auc.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/smartcache_handler.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/smartcache_handler.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1d3796b4e645e2ece60366289649a2e465262cc1 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/smartcache_handler.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/stats_handler.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/stats_handler.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f3b91cec8424e30dfceb87dfd8be4d1e4e10d0cd Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/stats_handler.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/surface_distance.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/surface_distance.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..492e168e33e656a72f9810a398ccfe87aee8e278 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/surface_distance.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/tensorboard_handlers.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/tensorboard_handlers.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5ccdf7a038665f92856830aec4f2f15e7be17061 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/tensorboard_handlers.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/validation_handler.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/validation_handler.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0cb13bd61d37a899a147b33c906c5614262b6163 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/handlers/__pycache__/validation_handler.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/inferers/__pycache__/__init__.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/inferers/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc8942aa2d5373df41cca4bd0972c13ae973d651 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/inferers/__pycache__/__init__.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/inferers/__pycache__/inferer.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/inferers/__pycache__/inferer.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f8d83929dccd98f9781d55d414e6a82f83d78f90 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/inferers/__pycache__/inferer.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/inferers/__pycache__/utils.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/inferers/__pycache__/utils.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a1c7b7d1360dd8e5995126e80f26841abf435da9 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/inferers/__pycache__/utils.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/__init__.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ee77da906eb950dd9aaedc023b6a69f3dbc0143 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/__init__.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/confusion_matrix.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/confusion_matrix.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ce121cff01dab60944a563b59d0fa054a0e64d87 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/confusion_matrix.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/cumulative_average.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/cumulative_average.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cf484dfea6572bc43cd9be1d53b699c5084c78dc Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/cumulative_average.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/froc.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/froc.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..00a7382e479f983229cabdd8419966fa1a2a1d16 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/froc.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/generalized_dice.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/generalized_dice.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa6e8145f33dcf1b05dd70c059dbbb744fb50618 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/generalized_dice.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/hausdorff_distance.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/hausdorff_distance.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cb244f84ea9872914154f30d8dde53c28c1146ac Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/hausdorff_distance.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/meandice.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/meandice.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..31e4f8e696e10ecd70d27358daa2e10ac260c8c4 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/meandice.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/metric.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/metric.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..789092421767ebf8cf3c95c9cab3358563035d91 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/metric.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/regression.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/regression.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..24227b1ab9ba74e877840cc407b3188c1bf154b9 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/regression.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/rocauc.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/rocauc.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b39c46b1c5483101d520184bab53559e4b58be6f Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/rocauc.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/surface_dice.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/surface_dice.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1b8571d26ad0d37fa5aa376c5570e4cad3a852fc Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/surface_dice.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/surface_distance.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/surface_distance.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..01ceb6f0bab2352c0d15f6bb66b4aaf509be4fb2 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/surface_distance.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/utils.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/utils.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ba8aa8ed5c1693480e3f4b4a83de75fcd622b5d0 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/metrics/__pycache__/utils.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/croppad/__init__.py b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/croppad/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1e97f8940782e96a77c1c08483fc41da9a48ae22 --- /dev/null +++ b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/croppad/__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/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/croppad/__pycache__/dictionary.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/croppad/__pycache__/dictionary.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bbfd4c050b58e7ab71dc2d5ebd43976d9c636d88 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/croppad/__pycache__/dictionary.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/croppad/array.py b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/croppad/array.py new file mode 100644 index 0000000000000000000000000000000000000000..6537cf3e216b161c4a878865d4084d0834e714b7 --- /dev/null +++ b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/croppad/array.py @@ -0,0 +1,1223 @@ +# 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 "vanilla" transforms for crop and pad operations +https://github.com/Project-MONAI/MONAI/wiki/MONAI_Design +""" + +from itertools import chain +from math import ceil +from typing import Any, Callable, List, Optional, Sequence, Tuple, Union + +import numpy as np +import torch +from torch.nn.functional import pad as pad_pt + +from monai.config import IndexSelection +from monai.config.type_definitions import NdarrayOrTensor +from monai.data.utils import get_random_patch, get_valid_patch_size +from monai.transforms.transform import Randomizable, Transform +from monai.transforms.utils import ( + compute_divisible_spatial_size, + convert_pad_mode, + generate_label_classes_crop_centers, + generate_pos_neg_label_crop_centers, + generate_spatial_bounding_box, + is_positive, + map_binary_to_indices, + map_classes_to_indices, + weighted_patch_samples, +) +from monai.transforms.utils_pytorch_numpy_unification import floor_divide, maximum +from monai.utils import ( + Method, + NumpyPadMode, + PytorchPadMode, + ensure_tuple, + ensure_tuple_rep, + fall_back_tuple, + look_up_option, +) +from monai.utils.enums import TransformBackends +from monai.utils.type_conversion import convert_data_type, convert_to_dst_type + +__all__ = [ + "Pad", + "SpatialPad", + "BorderPad", + "DivisiblePad", + "SpatialCrop", + "CenterSpatialCrop", + "CenterScaleCrop", + "RandSpatialCrop", + "RandScaleCrop", + "RandSpatialCropSamples", + "CropForeground", + "RandWeightedCrop", + "RandCropByPosNegLabel", + "RandCropByLabelClasses", + "ResizeWithPadOrCrop", + "BoundingRect", +] + + +class Pad(Transform): + """ + Perform padding for a given an amount of padding in each dimension. + If input is `torch.Tensor`, `torch.nn.functional.pad` will be used, otherwise, `np.pad` will be used. + + Args: + to_pad: the amount to be padded in each dimension [(low_H, high_H), (low_W, high_W), ...]. + mode: available modes for numpy array:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, + ``"mean"``, ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + available modes for PyTorch Tensor: {``"constant"``, ``"reflect"``, ``"replicate"``, ``"circular"``}. + One of the listed string values or a user supplied function. Defaults to ``"constant"``. + See also: https://numpy.org/doc/1.18/reference/generated/numpy.pad.html + https://pytorch.org/docs/stable/generated/torch.nn.functional.pad.html + kwargs: other arguments for the `np.pad` or `torch.pad` function. + note that `np.pad` treats channel dimension as the first dimension. + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + def __init__( + self, + to_pad: List[Tuple[int, int]], + mode: Union[NumpyPadMode, PytorchPadMode, str] = NumpyPadMode.CONSTANT, + **kwargs, + ) -> None: + self.to_pad = to_pad + self.mode = mode + self.kwargs = kwargs + + @staticmethod + def _np_pad(img: np.ndarray, all_pad_width, mode, **kwargs) -> np.ndarray: + return np.pad(img, all_pad_width, mode=mode, **kwargs) # type: ignore + + @staticmethod + def _pt_pad(img: torch.Tensor, all_pad_width, mode, **kwargs) -> torch.Tensor: + pt_pad_width = [val for sublist in all_pad_width[1:] for val in sublist[::-1]][::-1] + # torch.pad expects `[B, C, H, W, [D]]` shape + return pad_pt(img.unsqueeze(0), pt_pad_width, mode=mode, **kwargs).squeeze(0) + + def __call__( + self, img: NdarrayOrTensor, mode: Optional[Union[NumpyPadMode, PytorchPadMode, str]] = None + ) -> NdarrayOrTensor: + """ + Args: + img: data to be transformed, assuming `img` is channel-first and + padding doesn't apply to the channel dim. + mode: available modes for numpy array:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, + ``"mean"``, ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + available modes for PyTorch Tensor: {``"constant"``, ``"reflect"``, ``"replicate"`` or ``"circular"``}. + One of the listed string values or a user supplied function. Defaults to `self.mode`. + See also: https://numpy.org/doc/1.18/reference/generated/numpy.pad.html + https://pytorch.org/docs/stable/generated/torch.nn.functional.pad.html + + """ + if not np.asarray(self.to_pad).any(): + # all zeros, skip padding + return img + mode = convert_pad_mode(dst=img, mode=mode or self.mode).value + pad = self._pt_pad if isinstance(img, torch.Tensor) else self._np_pad + return pad(img, self.to_pad, mode, **self.kwargs) # type: ignore + + +class SpatialPad(Transform): + """ + Performs padding to the data, symmetric for all sides or all on one side for each dimension. + + If input is `torch.Tensor` and mode is `constant`, `torch.nn.functional.pad` will be used. + Otherwise, `np.pad` will be used (input converted to `np.ndarray` if necessary). + + Uses np.pad so in practice, a mode needs to be provided. See numpy.lib.arraypad.pad + for additional details. + + Args: + spatial_size: the spatial size of output data after padding, if a dimension of the input + data size is bigger than the pad size, will not pad that dimension. + If its components have non-positive values, the corresponding size of input image will be used + (no padding). for example: if the spatial size of input data is [30, 30, 30] and + `spatial_size=[32, 25, -1]`, the spatial size of output data will be [32, 30, 30]. + method: {``"symmetric"``, ``"end"``} + Pad image symmetrically on every side or only pad at the end sides. Defaults to ``"symmetric"``. + mode: available modes for numpy array:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, + ``"mean"``, ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + available modes for PyTorch Tensor: {``"constant"``, ``"reflect"``, ``"replicate"``, ``"circular"``}. + One of the listed string values or a user supplied function. Defaults to ``"constant"``. + See also: https://numpy.org/doc/1.18/reference/generated/numpy.pad.html + https://pytorch.org/docs/stable/generated/torch.nn.functional.pad.html + kwargs: other arguments for the `np.pad` or `torch.pad` function. + note that `np.pad` treats channel dimension as the first dimension. + + """ + + backend = Pad.backend + + def __init__( + self, + spatial_size: Union[Sequence[int], int], + method: Union[Method, str] = Method.SYMMETRIC, + mode: Union[NumpyPadMode, PytorchPadMode, str] = NumpyPadMode.CONSTANT, + **kwargs, + ) -> None: + self.spatial_size = spatial_size + self.method: Method = look_up_option(method, Method) + self.mode = mode + self.kwargs = kwargs + + def _determine_data_pad_width(self, data_shape: Sequence[int]) -> List[Tuple[int, int]]: + spatial_size = fall_back_tuple(self.spatial_size, data_shape) + if self.method == Method.SYMMETRIC: + pad_width = [] + for i, sp_i in enumerate(spatial_size): + width = max(sp_i - data_shape[i], 0) + pad_width.append((width // 2, width - (width // 2))) + return pad_width + return [(0, max(sp_i - data_shape[i], 0)) for i, sp_i in enumerate(spatial_size)] + + def __call__( + self, img: NdarrayOrTensor, mode: Optional[Union[NumpyPadMode, PytorchPadMode, str]] = None + ) -> NdarrayOrTensor: + """ + Args: + img: data to be transformed, assuming `img` is channel-first and + padding doesn't apply to the channel dim. + mode: available modes for numpy array:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, + ``"mean"``, ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + available modes for PyTorch Tensor: {``"constant"``, ``"reflect"``, ``"replicate"``, ``"circular"``}. + One of the listed string values or a user supplied function. Defaults to `self.mode`. + See also: https://numpy.org/doc/1.18/reference/generated/numpy.pad.html + https://pytorch.org/docs/stable/generated/torch.nn.functional.pad.html + + """ + data_pad_width = self._determine_data_pad_width(img.shape[1:]) + all_pad_width = [(0, 0)] + data_pad_width + if not np.asarray(all_pad_width).any(): + # all zeros, skip padding + return img + + padder = Pad(to_pad=all_pad_width, mode=mode or self.mode, **self.kwargs) + return padder(img) + + +class BorderPad(Transform): + """ + Pad the input data by adding specified borders to every dimension. + + Args: + spatial_border: specified size for every spatial border. Any -ve values will be set to 0. It can be 3 shapes: + + - single int number, pad all the borders with the same size. + - length equals the length of image shape, pad every spatial dimension separately. + for example, image shape(CHW) is [1, 4, 4], spatial_border is [2, 1], + pad every border of H dim with 2, pad every border of W dim with 1, result shape is [1, 8, 6]. + - length equals 2 x (length of image shape), pad every border of every dimension separately. + for example, image shape(CHW) is [1, 4, 4], spatial_border is [1, 2, 3, 4], pad top of H dim with 1, + pad bottom of H dim with 2, pad left of W dim with 3, pad right of W dim with 4. + the result shape is [1, 7, 11]. + mode: available modes for numpy array:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, + ``"mean"``, ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + available modes for PyTorch Tensor: {``"constant"``, ``"reflect"``, ``"replicate"``, ``"circular"``}. + One of the listed string values or a user supplied function. Defaults to ``"constant"``. + See also: https://numpy.org/doc/1.18/reference/generated/numpy.pad.html + https://pytorch.org/docs/stable/generated/torch.nn.functional.pad.html + kwargs: other arguments for the `np.pad` or `torch.pad` function. + note that `np.pad` treats channel dimension as the first dimension. + + """ + + backend = Pad.backend + + def __init__( + self, + spatial_border: Union[Sequence[int], int], + mode: Union[NumpyPadMode, PytorchPadMode, str] = NumpyPadMode.CONSTANT, + **kwargs, + ) -> None: + self.spatial_border = spatial_border + self.mode = mode + self.kwargs = kwargs + + def __call__( + self, img: NdarrayOrTensor, mode: Optional[Union[NumpyPadMode, PytorchPadMode, str]] = None + ) -> NdarrayOrTensor: + """ + Args: + img: data to be transformed, assuming `img` is channel-first and + padding doesn't apply to the channel dim. + mode: available modes for numpy array:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, + ``"mean"``, ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + available modes for PyTorch Tensor: {``"constant"``, ``"reflect"``, ``"replicate"``, ``"circular"``}. + One of the listed string values or a user supplied function. Defaults to `self.mode`. + See also: https://numpy.org/doc/1.18/reference/generated/numpy.pad.html + https://pytorch.org/docs/stable/generated/torch.nn.functional.pad.html + + Raises: + ValueError: When ``self.spatial_border`` does not contain ints. + ValueError: When ``self.spatial_border`` length is not one of + [1, len(spatial_shape), 2*len(spatial_shape)]. + + """ + spatial_shape = img.shape[1:] + spatial_border = ensure_tuple(self.spatial_border) + if not all(isinstance(b, int) for b in spatial_border): + raise ValueError(f"self.spatial_border must contain only ints, got {spatial_border}.") + spatial_border = tuple(max(0, b) for b in spatial_border) + + if len(spatial_border) == 1: + data_pad_width = [(spatial_border[0], spatial_border[0]) for _ in spatial_shape] + elif len(spatial_border) == len(spatial_shape): + data_pad_width = [(sp, sp) for sp in spatial_border[: len(spatial_shape)]] + elif len(spatial_border) == len(spatial_shape) * 2: + data_pad_width = [(spatial_border[2 * i], spatial_border[2 * i + 1]) for i in range(len(spatial_shape))] + else: + raise ValueError( + f"Unsupported spatial_border length: {len(spatial_border)}, available options are " + f"[1, len(spatial_shape)={len(spatial_shape)}, 2*len(spatial_shape)={2*len(spatial_shape)}]." + ) + + all_pad_width = [(0, 0)] + data_pad_width + padder = Pad(all_pad_width, mode or self.mode, **self.kwargs) + return padder(img) + + +class DivisiblePad(Transform): + """ + Pad the input data, so that the spatial sizes are divisible by `k`. + """ + + backend = SpatialPad.backend + + def __init__( + self, + k: Union[Sequence[int], int], + mode: Union[NumpyPadMode, PytorchPadMode, str] = NumpyPadMode.CONSTANT, + method: Union[Method, str] = Method.SYMMETRIC, + **kwargs, + ) -> None: + """ + Args: + k: the target k for each spatial dimension. + if `k` is negative or 0, the original size is preserved. + if `k` is an int, the same `k` be applied to all the input spatial dimensions. + mode: available modes for numpy array:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, + ``"mean"``, ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + available modes for PyTorch Tensor: {``"constant"``, ``"reflect"``, ``"replicate"``, ``"circular"``}. + One of the listed string values or a user supplied function. Defaults to ``"constant"``. + See also: https://numpy.org/doc/1.18/reference/generated/numpy.pad.html + https://pytorch.org/docs/stable/generated/torch.nn.functional.pad.html + method: {``"symmetric"``, ``"end"``} + Pad image symmetrically on every side or only pad at the end sides. Defaults to ``"symmetric"``. + kwargs: other arguments for the `np.pad` or `torch.pad` function. + note that `np.pad` treats channel dimension as the first dimension. + + See also :py:class:`monai.transforms.SpatialPad` + """ + self.k = k + self.mode: NumpyPadMode = NumpyPadMode(mode) + self.method: Method = Method(method) + self.kwargs = kwargs + + def __call__( + self, img: NdarrayOrTensor, mode: Optional[Union[NumpyPadMode, PytorchPadMode, str]] = None + ) -> NdarrayOrTensor: + """ + Args: + img: data to be transformed, assuming `img` is channel-first + and padding doesn't apply to the channel dim. + mode: available modes for numpy array:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, + ``"mean"``, ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + available modes for PyTorch Tensor: {``"constant"``, ``"reflect"``, ``"replicate"``, ``"circular"``}. + One of the listed string values or a user supplied function. Defaults to `self.mode`. + See also: https://numpy.org/doc/1.18/reference/generated/numpy.pad.html + https://pytorch.org/docs/stable/generated/torch.nn.functional.pad.html + + """ + new_size = compute_divisible_spatial_size(spatial_shape=img.shape[1:], k=self.k) + spatial_pad = SpatialPad(spatial_size=new_size, method=self.method, mode=mode or self.mode, **self.kwargs) + + return spatial_pad(img) + + +class SpatialCrop(Transform): + """ + General purpose cropper to produce sub-volume region of interest (ROI). + If a dimension of the expected ROI size is bigger than the input image size, will not crop that dimension. + So the cropped result may be smaller than the expected ROI, and the cropped results of several images may + not have exactly the same shape. + It can support to crop ND spatial (channel-first) data. + + The cropped region can be parameterised in various ways: + - a list of slices for each spatial dimension (allows for use of -ve indexing and `None`) + - a spatial center and size + - the start and end coordinates of the ROI + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + def __init__( + self, + roi_center: Union[Sequence[int], NdarrayOrTensor, None] = None, + roi_size: Union[Sequence[int], NdarrayOrTensor, None] = None, + roi_start: Union[Sequence[int], NdarrayOrTensor, None] = None, + roi_end: Union[Sequence[int], NdarrayOrTensor, None] = None, + roi_slices: Optional[Sequence[slice]] = None, + ) -> None: + """ + Args: + roi_center: voxel coordinates for center of the crop ROI. + roi_size: size of the crop ROI, if a dimension of ROI size is bigger than image size, + will not crop that dimension of the image. + roi_start: voxel coordinates for start of the crop ROI. + roi_end: voxel coordinates for end of the crop ROI, if a coordinate is out of image, + use the end coordinate of image. + roi_slices: list of slices for each of the spatial dimensions. + """ + roi_start_torch: torch.Tensor + + if roi_slices: + if not all(s.step is None or s.step == 1 for s in roi_slices): + raise ValueError("Only slice steps of 1/None are currently supported") + self.slices = list(roi_slices) + else: + if roi_center is not None and roi_size is not None: + roi_center, *_ = convert_data_type( + data=roi_center, output_type=torch.Tensor, dtype=torch.int16, wrap_sequence=True + ) + roi_size, *_ = convert_to_dst_type(src=roi_size, dst=roi_center, wrap_sequence=True) + _zeros = torch.zeros_like(roi_center) + roi_start_torch = maximum(roi_center - floor_divide(roi_size, 2), _zeros) # type: ignore + roi_end_torch = maximum(roi_start_torch + roi_size, roi_start_torch) + else: + if roi_start is None or roi_end is None: + raise ValueError("Please specify either roi_center, roi_size or roi_start, roi_end.") + roi_start_torch, *_ = convert_data_type( + data=roi_start, output_type=torch.Tensor, dtype=torch.int16, wrap_sequence=True + ) + roi_start_torch = maximum(roi_start_torch, torch.zeros_like(roi_start_torch)) # type: ignore + roi_end_torch, *_ = convert_to_dst_type(src=roi_end, dst=roi_start_torch, wrap_sequence=True) + roi_end_torch = maximum(roi_end_torch, roi_start_torch) + # convert to slices (accounting for 1d) + if roi_start_torch.numel() == 1: + self.slices = [slice(int(roi_start_torch.item()), int(roi_end_torch.item()))] + else: + self.slices = [slice(int(s), int(e)) for s, e in zip(roi_start_torch.tolist(), roi_end_torch.tolist())] + + def __call__(self, img: NdarrayOrTensor) -> NdarrayOrTensor: + """ + Apply the transform to `img`, assuming `img` is channel-first and + slicing doesn't apply to the channel dim. + """ + sd = min(len(self.slices), len(img.shape[1:])) # spatial dims + slices = [slice(None)] + self.slices[:sd] + return img[tuple(slices)] + + +class CenterSpatialCrop(Transform): + """ + Crop at the center of image with specified ROI size. + If a dimension of the expected ROI size is bigger than the input image size, will not crop that dimension. + So the cropped result may be smaller than the expected ROI, and the cropped results of several images may + not have exactly the same shape. + + Args: + roi_size: the spatial size of the crop region e.g. [224,224,128] + if a dimension of ROI size is bigger than image size, will not crop that dimension of the image. + If its components have non-positive values, the corresponding size of input image will be used. + for example: if the spatial size of input data is [40, 40, 40] and `roi_size=[32, 64, -1]`, + the spatial size of output data will be [32, 40, 40]. + """ + + backend = SpatialCrop.backend + + def __init__(self, roi_size: Union[Sequence[int], int]) -> None: + self.roi_size = roi_size + + def __call__(self, img: NdarrayOrTensor) -> NdarrayOrTensor: + """ + Apply the transform to `img`, assuming `img` is channel-first and + slicing doesn't apply to the channel dim. + """ + roi_size = fall_back_tuple(self.roi_size, img.shape[1:]) + center = [i // 2 for i in img.shape[1:]] + cropper = SpatialCrop(roi_center=center, roi_size=roi_size) + return cropper(img) + + +class CenterScaleCrop(Transform): + """ + Crop at the center of image with specified scale of ROI size. + + Args: + roi_scale: specifies the expected scale of image size to crop. e.g. [0.3, 0.4, 0.5] or a number for all dims. + If its components have non-positive values, will use `1.0` instead, which means the input image size. + + """ + + backend = CenterSpatialCrop.backend + + def __init__(self, roi_scale: Union[Sequence[float], float]): + self.roi_scale = roi_scale + + def __call__(self, img: NdarrayOrTensor) -> NdarrayOrTensor: + img_size = img.shape[1:] + ndim = len(img_size) + roi_size = [ceil(r * s) for r, s in zip(ensure_tuple_rep(self.roi_scale, ndim), img_size)] + sp_crop = CenterSpatialCrop(roi_size=roi_size) + return sp_crop(img=img) + + +class RandSpatialCrop(Randomizable, Transform): + """ + Crop image with random size or specific size ROI. It can crop at a random position as center + or at the image center. And allows to set the minimum and maximum size to limit the randomly generated ROI. + + Note: even `random_size=False`, if a dimension of the expected ROI size is bigger than the input image size, + will not crop that dimension. So the cropped result may be smaller than the expected ROI, and the cropped results + of several images may not have exactly the same shape. + + Args: + roi_size: if `random_size` is True, it specifies the minimum crop region. + if `random_size` is False, it specifies the expected ROI size to crop. e.g. [224, 224, 128] + if a dimension of ROI size is bigger than image size, will not crop that dimension of the image. + If its components have non-positive values, the corresponding size of input image will be used. + for example: if the spatial size of input data is [40, 40, 40] and `roi_size=[32, 64, -1]`, + the spatial size of output data will be [32, 40, 40]. + max_roi_size: if `random_size` is True and `roi_size` specifies the min crop region size, `max_roi_size` + can specify the max crop region size. if None, defaults to the input image size. + if its components have non-positive values, the corresponding size of input image will be used. + random_center: crop at random position as center or the image center. + random_size: crop with random size or specific size ROI. + if True, the actual size is sampled from `randint(roi_size, max_roi_size + 1)`. + """ + + backend = CenterSpatialCrop.backend + + def __init__( + self, + roi_size: Union[Sequence[int], int], + max_roi_size: Optional[Union[Sequence[int], int]] = None, + random_center: bool = True, + random_size: bool = True, + ) -> None: + self.roi_size = roi_size + self.max_roi_size = max_roi_size + self.random_center = random_center + self.random_size = random_size + self._size: Optional[Sequence[int]] = None + self._slices: Optional[Tuple[slice, ...]] = None + + def randomize(self, img_size: Sequence[int]) -> None: + self._size = fall_back_tuple(self.roi_size, img_size) + if self.random_size: + max_size = img_size if self.max_roi_size is None else fall_back_tuple(self.max_roi_size, img_size) + if any(i > j for i, j in zip(self._size, max_size)): + raise ValueError(f"min ROI size: {self._size} is bigger than max ROI size: {max_size}.") + self._size = tuple(self.R.randint(low=self._size[i], high=max_size[i] + 1) for i in range(len(img_size))) + if self.random_center: + valid_size = get_valid_patch_size(img_size, self._size) + self._slices = (slice(None),) + get_random_patch(img_size, valid_size, self.R) + + def __call__(self, img: NdarrayOrTensor) -> NdarrayOrTensor: + """ + Apply the transform to `img`, assuming `img` is channel-first and + slicing doesn't apply to the channel dim. + """ + self.randomize(img.shape[1:]) + if self._size is None: + raise RuntimeError("self._size not specified.") + if self.random_center: + return img[self._slices] + cropper = CenterSpatialCrop(self._size) + return cropper(img) + + +class RandScaleCrop(RandSpatialCrop): + """ + Subclass of :py:class:`monai.transforms.RandSpatialCrop`. Crop image with + random size or specific size ROI. It can crop at a random position as + center or at the image center. And allows to set the minimum and maximum + scale of image size to limit the randomly generated ROI. + + Args: + roi_scale: if `random_size` is True, it specifies the minimum crop size: `roi_scale * image spatial size`. + if `random_size` is False, it specifies the expected scale of image size to crop. e.g. [0.3, 0.4, 0.5]. + If its components have non-positive values, will use `1.0` instead, which means the input image size. + max_roi_scale: if `random_size` is True and `roi_scale` specifies the min crop region size, `max_roi_scale` + can specify the max crop region size: `max_roi_scale * image spatial size`. + if None, defaults to the input image size. if its components have non-positive values, + will use `1.0` instead, which means the input image size. + random_center: crop at random position as center or the image center. + random_size: crop with random size or specified size ROI by `roi_scale * image spatial size`. + if True, the actual size is sampled from + `randint(roi_scale * image spatial size, max_roi_scale * image spatial size + 1)`. + """ + + def __init__( + self, + roi_scale: Union[Sequence[float], float], + max_roi_scale: Optional[Union[Sequence[float], float]] = None, + random_center: bool = True, + random_size: bool = True, + ) -> None: + super().__init__(roi_size=-1, max_roi_size=None, random_center=random_center, random_size=random_size) + self.roi_scale = roi_scale + self.max_roi_scale = max_roi_scale + + def __call__(self, img: NdarrayOrTensor) -> NdarrayOrTensor: + """ + Apply the transform to `img`, assuming `img` is channel-first and + slicing doesn't apply to the channel dim. + """ + img_size = img.shape[1:] + ndim = len(img_size) + self.roi_size = [ceil(r * s) for r, s in zip(ensure_tuple_rep(self.roi_scale, ndim), img_size)] + if self.max_roi_scale is not None: + self.max_roi_size = [ceil(r * s) for r, s in zip(ensure_tuple_rep(self.max_roi_scale, ndim), img_size)] + else: + self.max_roi_size = None + return super().__call__(img=img) + + +class RandSpatialCropSamples(Randomizable, Transform): + """ + Crop image with random size or specific size ROI to generate a list of N samples. + It can crop at a random position as center or at the image center. And allows to set + the minimum size to limit the randomly generated ROI. + It will return a list of cropped images. + + Note: even `random_size=False`, if a dimension of the expected ROI size is bigger than the input image size, + will not crop that dimension. So the cropped result may be smaller than the expected ROI, and the cropped + results of several images may not have exactly the same shape. + + Args: + roi_size: if `random_size` is True, it specifies the minimum crop region. + if `random_size` is False, it specifies the expected ROI size to crop. e.g. [224, 224, 128] + if a dimension of ROI size is bigger than image size, will not crop that dimension of the image. + If its components have non-positive values, the corresponding size of input image will be used. + for example: if the spatial size of input data is [40, 40, 40] and `roi_size=[32, 64, -1]`, + the spatial size of output data will be [32, 40, 40]. + num_samples: number of samples (crop regions) to take in the returned list. + max_roi_size: if `random_size` is True and `roi_size` specifies the min crop region size, `max_roi_size` + can specify the max crop region size. if None, defaults to the input image size. + if its components have non-positive values, the corresponding size of input image will be used. + random_center: crop at random position as center or the image center. + random_size: crop with random size or specific size ROI. + The actual size is sampled from `randint(roi_size, img_size)`. + + Raises: + ValueError: When ``num_samples`` is nonpositive. + + """ + + backend = RandSpatialCrop.backend + + def __init__( + self, + roi_size: Union[Sequence[int], int], + num_samples: int, + max_roi_size: Optional[Union[Sequence[int], int]] = None, + random_center: bool = True, + random_size: bool = True, + ) -> None: + if num_samples < 1: + raise ValueError(f"num_samples must be positive, got {num_samples}.") + self.num_samples = num_samples + self.cropper = RandSpatialCrop(roi_size, max_roi_size, random_center, random_size) + + def set_random_state( + self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None + ) -> "RandSpatialCropSamples": + super().set_random_state(seed, state) + self.cropper.set_random_state(seed, state) + return self + + def randomize(self, data: Optional[Any] = None) -> None: + pass + + def __call__(self, img: NdarrayOrTensor) -> List[NdarrayOrTensor]: + """ + Apply the transform to `img`, assuming `img` is channel-first and + cropping doesn't change the channel dim. + """ + return [self.cropper(img) for _ in range(self.num_samples)] + + +class CropForeground(Transform): + """ + Crop an image using a bounding box. The bounding box is generated by selecting foreground using select_fn + at channels channel_indices. margin is added in each spatial dimension of the bounding box. + The typical usage is to help training and evaluation if the valid part is small in the whole medical image. + Users can define arbitrary function to select expected foreground from the whole image or specified channels. + And it can also add margin to every dim of the bounding box of foreground object. + For example: + + .. code-block:: python + + image = np.array( + [[[0, 0, 0, 0, 0], + [0, 1, 2, 1, 0], + [0, 1, 3, 2, 0], + [0, 1, 2, 1, 0], + [0, 0, 0, 0, 0]]]) # 1x5x5, single channel 5x5 image + + + def threshold_at_one(x): + # threshold at 1 + return x > 1 + + + cropper = CropForeground(select_fn=threshold_at_one, margin=0) + print(cropper(image)) + [[[2, 1], + [3, 2], + [2, 1]]] + + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + def __init__( + self, + select_fn: Callable = is_positive, + channel_indices: Optional[IndexSelection] = None, + margin: Union[Sequence[int], int] = 0, + allow_smaller: bool = True, + return_coords: bool = False, + k_divisible: Union[Sequence[int], int] = 1, + mode: Optional[Union[NumpyPadMode, PytorchPadMode, str]] = NumpyPadMode.CONSTANT, + **pad_kwargs, + ) -> None: + """ + Args: + select_fn: function to select expected foreground, default is to select values > 0. + channel_indices: if defined, select foreground only on the specified channels + of image. if None, select foreground on the whole image. + margin: add margin value to spatial dims of the bounding box, if only 1 value provided, use it for all dims. + allow_smaller: when computing box size with `margin`, whether allow the image size to be smaller + than box size, default to `True`. if the margined size is bigger than image size, will pad with + specified `mode`. + return_coords: whether return the coordinates of spatial bounding box for foreground. + k_divisible: make each spatial dimension to be divisible by k, default to 1. + if `k_divisible` is an int, the same `k` be applied to all the input spatial dimensions. + mode: available modes for numpy array:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, + ``"mean"``, ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + available modes for PyTorch Tensor: {``"constant"``, ``"reflect"``, ``"replicate"``, ``"circular"``}. + One of the listed string values or a user supplied function. Defaults to ``"constant"``. + See also: https://numpy.org/doc/1.18/reference/generated/numpy.pad.html + https://pytorch.org/docs/stable/generated/torch.nn.functional.pad.html + pad_kwargs: other arguments for the `np.pad` or `torch.pad` function. + note that `np.pad` treats channel dimension as the first dimension. + + """ + self.select_fn = select_fn + self.channel_indices = ensure_tuple(channel_indices) if channel_indices is not None else None + self.margin = margin + self.allow_smaller = allow_smaller + self.return_coords = return_coords + self.k_divisible = k_divisible + self.mode: NumpyPadMode = look_up_option(mode, NumpyPadMode) + self.pad_kwargs = pad_kwargs + + def compute_bounding_box(self, img: NdarrayOrTensor): + """ + Compute the start points and end points of bounding box to crop. + And adjust bounding box coords to be divisible by `k`. + + """ + box_start, box_end = generate_spatial_bounding_box( + img, self.select_fn, self.channel_indices, self.margin, self.allow_smaller + ) + box_start_, *_ = convert_data_type(box_start, output_type=np.ndarray, dtype=np.int16, wrap_sequence=True) + box_end_, *_ = convert_data_type(box_end, output_type=np.ndarray, dtype=np.int16, wrap_sequence=True) + orig_spatial_size = box_end_ - box_start_ + # make the spatial size divisible by `k` + spatial_size = np.asarray(compute_divisible_spatial_size(orig_spatial_size.tolist(), k=self.k_divisible)) + # update box_start and box_end + box_start_ = box_start_ - np.floor_divide(np.asarray(spatial_size) - orig_spatial_size, 2) + box_end_ = box_start_ + spatial_size + return box_start_, box_end_ + + def crop_pad( + self, + img: NdarrayOrTensor, + box_start: np.ndarray, + box_end: np.ndarray, + mode: Optional[Union[NumpyPadMode, PytorchPadMode, str]] = None, + ): + """ + Crop and pad based on the bounding box. + + """ + cropped = SpatialCrop(roi_start=box_start, roi_end=box_end)(img) + pad_to_start = np.maximum(-box_start, 0) + pad_to_end = np.maximum(box_end - np.asarray(img.shape[1:]), 0) + pad = list(chain(*zip(pad_to_start.tolist(), pad_to_end.tolist()))) + return BorderPad(spatial_border=pad, mode=mode or self.mode, **self.pad_kwargs)(cropped) + + def __call__(self, img: NdarrayOrTensor, mode: Optional[Union[NumpyPadMode, PytorchPadMode, str]] = None): + """ + Apply the transform to `img`, assuming `img` is channel-first and + slicing doesn't change the channel dim. + """ + box_start, box_end = self.compute_bounding_box(img) + cropped = self.crop_pad(img, box_start, box_end, mode) + + if self.return_coords: + return cropped, box_start, box_end + return cropped + + +class RandWeightedCrop(Randomizable, Transform): + """ + Samples a list of `num_samples` image patches according to the provided `weight_map`. + + Args: + spatial_size: the spatial size of the image patch e.g. [224, 224, 128]. + If its components have non-positive values, the corresponding size of `img` will be used. + num_samples: number of samples (image patches) to take in the returned list. + weight_map: weight map used to generate patch samples. The weights must be non-negative. + Each element denotes a sampling weight of the spatial location. 0 indicates no sampling. + It should be a single-channel array in shape, for example, `(1, spatial_dim_0, spatial_dim_1, ...)`. + """ + + backend = SpatialCrop.backend + + def __init__( + self, + spatial_size: Union[Sequence[int], int], + num_samples: int = 1, + weight_map: Optional[NdarrayOrTensor] = None, + ): + self.spatial_size = ensure_tuple(spatial_size) + self.num_samples = int(num_samples) + self.weight_map = weight_map + self.centers: List[np.ndarray] = [] + + def randomize(self, weight_map: NdarrayOrTensor) -> None: + self.centers = weighted_patch_samples( + spatial_size=self.spatial_size, w=weight_map[0], n_samples=self.num_samples, r_state=self.R + ) # using only the first channel as weight map + + def __call__(self, img: NdarrayOrTensor, weight_map: Optional[NdarrayOrTensor] = None) -> List[NdarrayOrTensor]: + """ + Args: + img: input image to sample patches from. assuming `img` is a channel-first array. + weight_map: weight map used to generate patch samples. The weights must be non-negative. + Each element denotes a sampling weight of the spatial location. 0 indicates no sampling. + It should be a single-channel array in shape, for example, `(1, spatial_dim_0, spatial_dim_1, ...)` + + Returns: + A list of image patches + """ + if weight_map is None: + weight_map = self.weight_map + if weight_map is None: + raise ValueError("weight map must be provided for weighted patch sampling.") + if img.shape[1:] != weight_map.shape[1:]: + raise ValueError(f"image and weight map spatial shape mismatch: {img.shape[1:]} vs {weight_map.shape[1:]}.") + + self.randomize(weight_map) + _spatial_size = fall_back_tuple(self.spatial_size, weight_map.shape[1:]) + results: List[NdarrayOrTensor] = [] + for center in self.centers: + cropper = SpatialCrop(roi_center=center, roi_size=_spatial_size) + results.append(cropper(img)) + return results + + +class RandCropByPosNegLabel(Randomizable, Transform): + """ + Crop random fixed sized regions with the center being a foreground or background voxel + based on the Pos Neg Ratio. + And will return a list of arrays for all the cropped images. + For example, crop two (3 x 3) arrays from (5 x 5) array with pos/neg=1:: + + [[[0, 0, 0, 0, 0], + [0, 1, 2, 1, 0], [[0, 1, 2], [[2, 1, 0], + [0, 1, 3, 0, 0], --> [0, 1, 3], [3, 0, 0], + [0, 0, 0, 0, 0], [0, 0, 0]] [0, 0, 0]] + [0, 0, 0, 0, 0]]] + + If a dimension of the expected spatial size is bigger than the input image size, + will not crop that dimension. So the cropped result may be smaller than expected size, and the cropped + results of several images may not have exactly same shape. + And if the crop ROI is partly out of the image, will automatically adjust the crop center to ensure the + valid crop ROI. + + Args: + spatial_size: the spatial size of the crop region e.g. [224, 224, 128]. + if a dimension of ROI size is bigger than image size, will not crop that dimension of the image. + if its components have non-positive values, the corresponding size of `label` will be used. + for example: if the spatial size of input data is [40, 40, 40] and `spatial_size=[32, 64, -1]`, + the spatial size of output data will be [32, 40, 40]. + label: the label image that is used for finding foreground/background, if None, must set at + `self.__call__`. Non-zero indicates foreground, zero indicates background. + pos: used with `neg` together to calculate the ratio ``pos / (pos + neg)`` for the probability + to pick a foreground voxel as a center rather than a background voxel. + neg: used with `pos` together to calculate the ratio ``pos / (pos + neg)`` for the probability + to pick a foreground voxel as a center rather than a background voxel. + num_samples: number of samples (crop regions) to take in each list. + image: optional image data to help select valid area, can be same as `img` or another image array. + if not None, use ``label == 0 & image > image_threshold`` to select the negative + sample (background) center. So the crop center will only come from the valid image areas. + image_threshold: if enabled `image`, use ``image > image_threshold`` to determine + the valid image content areas. + fg_indices: if provided pre-computed foreground indices of `label`, will ignore above `image` and + `image_threshold`, and randomly select crop centers based on them, need to provide `fg_indices` + and `bg_indices` together, expect to be 1 dim array of spatial indices after flattening. + a typical usage is to call `FgBgToIndices` transform first and cache the results. + bg_indices: if provided pre-computed background indices of `label`, will ignore above `image` and + `image_threshold`, and randomly select crop centers based on them, need to provide `fg_indices` + and `bg_indices` together, expect to be 1 dim array of spatial indices after flattening. + a typical usage is to call `FgBgToIndices` transform first and cache the results. + allow_smaller: if `False`, an exception will be raised if the image is smaller than + the requested ROI in any dimension. If `True`, any smaller dimensions will be set to + match the cropped size (i.e., no cropping in that dimension). + + Raises: + ValueError: When ``pos`` or ``neg`` are negative. + ValueError: When ``pos=0`` and ``neg=0``. Incompatible values. + + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + def __init__( + self, + spatial_size: Union[Sequence[int], int], + label: Optional[NdarrayOrTensor] = None, + pos: float = 1.0, + neg: float = 1.0, + num_samples: int = 1, + image: Optional[NdarrayOrTensor] = None, + image_threshold: float = 0.0, + fg_indices: Optional[NdarrayOrTensor] = None, + bg_indices: Optional[NdarrayOrTensor] = None, + allow_smaller: bool = False, + ) -> None: + self.spatial_size = spatial_size + self.label = label + if pos < 0 or neg < 0: + raise ValueError(f"pos and neg must be nonnegative, got pos={pos} neg={neg}.") + if pos + neg == 0: + raise ValueError("Incompatible values: pos=0 and neg=0.") + self.pos_ratio = pos / (pos + neg) + self.num_samples = num_samples + self.image = image + self.image_threshold = image_threshold + self.centers: Optional[List[List[int]]] = None + self.fg_indices = fg_indices + self.bg_indices = bg_indices + self.allow_smaller = allow_smaller + + def randomize( + self, + label: NdarrayOrTensor, + fg_indices: Optional[NdarrayOrTensor] = None, + bg_indices: Optional[NdarrayOrTensor] = None, + image: Optional[NdarrayOrTensor] = None, + ) -> None: + if fg_indices is None or bg_indices is None: + if self.fg_indices is not None and self.bg_indices is not None: + fg_indices_ = self.fg_indices + bg_indices_ = self.bg_indices + else: + fg_indices_, bg_indices_ = map_binary_to_indices(label, image, self.image_threshold) + else: + fg_indices_ = fg_indices + bg_indices_ = bg_indices + self.centers = generate_pos_neg_label_crop_centers( + self.spatial_size, + self.num_samples, + self.pos_ratio, + label.shape[1:], + fg_indices_, + bg_indices_, + self.R, + self.allow_smaller, + ) + + def __call__( + self, + img: NdarrayOrTensor, + label: Optional[NdarrayOrTensor] = None, + image: Optional[NdarrayOrTensor] = None, + fg_indices: Optional[NdarrayOrTensor] = None, + bg_indices: Optional[NdarrayOrTensor] = None, + ) -> List[NdarrayOrTensor]: + """ + Args: + img: input data to crop samples from based on the pos/neg ratio of `label` and `image`. + Assumes `img` is a channel-first array. + label: the label image that is used for finding foreground/background, if None, use `self.label`. + image: optional image data to help select valid area, can be same as `img` or another image array. + use ``label == 0 & image > image_threshold`` to select the negative sample(background) center. + so the crop center will only exist on valid image area. if None, use `self.image`. + fg_indices: foreground indices to randomly select crop centers, + need to provide `fg_indices` and `bg_indices` together. + bg_indices: background indices to randomly select crop centers, + need to provide `fg_indices` and `bg_indices` together. + + """ + if label is None: + label = self.label + if label is None: + raise ValueError("label should be provided.") + if image is None: + image = self.image + + self.randomize(label, fg_indices, bg_indices, image) + results: List[NdarrayOrTensor] = [] + if self.centers is not None: + for center in self.centers: + roi_size = fall_back_tuple(self.spatial_size, default=label.shape[1:]) + cropper = SpatialCrop(roi_center=center, roi_size=roi_size) + results.append(cropper(img)) + + return results + + +class RandCropByLabelClasses(Randomizable, Transform): + """ + Crop random fixed sized regions with the center being a class based on the specified ratios of every class. + The label data can be One-Hot format array or Argmax data. And will return a list of arrays for all the + cropped images. For example, crop two (3 x 3) arrays from (5 x 5) array with `ratios=[1, 2, 3, 1]`:: + + image = np.array([ + [[0.0, 0.3, 0.4, 0.2, 0.0], + [0.0, 0.1, 0.2, 0.1, 0.4], + [0.0, 0.3, 0.5, 0.2, 0.0], + [0.1, 0.2, 0.1, 0.1, 0.0], + [0.0, 0.1, 0.2, 0.1, 0.0]] + ]) + label = np.array([ + [[0, 0, 0, 0, 0], + [0, 1, 2, 1, 0], + [0, 1, 3, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]] + ]) + cropper = RandCropByLabelClasses( + spatial_size=[3, 3], + ratios=[1, 2, 3, 1], + num_classes=4, + num_samples=2, + ) + label_samples = cropper(img=label, label=label, image=image) + + The 2 randomly cropped samples of `label` can be: + [[0, 1, 2], [[0, 0, 0], + [0, 1, 3], [1, 2, 1], + [0, 0, 0]] [1, 3, 0]] + + If a dimension of the expected spatial size is bigger than the input image size, + will not crop that dimension. So the cropped result may be smaller than expected size, and the cropped + results of several images may not have exactly same shape. + And if the crop ROI is partly out of the image, will automatically adjust the crop center to ensure the + valid crop ROI. + + Args: + spatial_size: the spatial size of the crop region e.g. [224, 224, 128]. + if a dimension of ROI size is bigger than image size, will not crop that dimension of the image. + if its components have non-positive values, the corresponding size of `label` will be used. + for example: if the spatial size of input data is [40, 40, 40] and `spatial_size=[32, 64, -1]`, + the spatial size of output data will be [32, 40, 40]. + ratios: specified ratios of every class in the label to generate crop centers, including background class. + if None, every class will have the same ratio to generate crop centers. + label: the label image that is used for finding every classes, if None, must set at `self.__call__`. + num_classes: number of classes for argmax label, not necessary for One-Hot label. + num_samples: number of samples (crop regions) to take in each list. + image: if image is not None, only return the indices of every class that are within the valid + region of the image (``image > image_threshold``). + image_threshold: if enabled `image`, use ``image > image_threshold`` to + determine the valid image content area and select class indices only in this area. + indices: if provided pre-computed indices of every class, will ignore above `image` and + `image_threshold`, and randomly select crop centers based on them, expect to be 1 dim array + of spatial indices after flattening. a typical usage is to call `ClassesToIndices` transform first + and cache the results for better performance. + allow_smaller: if `False`, an exception will be raised if the image is smaller than + the requested ROI in any dimension. If `True`, any smaller dimensions will remain + unchanged. + + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + def __init__( + self, + spatial_size: Union[Sequence[int], int], + ratios: Optional[List[Union[float, int]]] = None, + label: Optional[NdarrayOrTensor] = None, + num_classes: Optional[int] = None, + num_samples: int = 1, + image: Optional[NdarrayOrTensor] = None, + image_threshold: float = 0.0, + indices: Optional[List[NdarrayOrTensor]] = None, + allow_smaller: bool = False, + ) -> None: + self.spatial_size = spatial_size + self.ratios = ratios + self.label = label + self.num_classes = num_classes + self.num_samples = num_samples + self.image = image + self.image_threshold = image_threshold + self.centers: Optional[List[List[int]]] = None + self.indices = indices + self.allow_smaller = allow_smaller + + def randomize( + self, + label: NdarrayOrTensor, + indices: Optional[List[NdarrayOrTensor]] = None, + image: Optional[NdarrayOrTensor] = None, + ) -> None: + indices_: Sequence[NdarrayOrTensor] + if indices is None: + if self.indices is not None: + indices_ = self.indices + else: + indices_ = map_classes_to_indices(label, self.num_classes, image, self.image_threshold) + else: + indices_ = indices + self.centers = generate_label_classes_crop_centers( + self.spatial_size, self.num_samples, label.shape[1:], indices_, self.ratios, self.R, self.allow_smaller + ) + + def __call__( + self, + img: NdarrayOrTensor, + label: Optional[NdarrayOrTensor] = None, + image: Optional[NdarrayOrTensor] = None, + indices: Optional[List[NdarrayOrTensor]] = None, + ) -> List[NdarrayOrTensor]: + """ + Args: + img: input data to crop samples from based on the ratios of every class, assumes `img` is a + channel-first array. + label: the label image that is used for finding indices of every class, if None, use `self.label`. + image: optional image data to help select valid area, can be same as `img` or another image array. + use ``image > image_threshold`` to select the centers only in valid region. if None, use `self.image`. + indices: list of indices for every class in the image, used to randomly select crop centers. + + """ + if label is None: + label = self.label + if label is None: + raise ValueError("label should be provided.") + if image is None: + image = self.image + + self.randomize(label, indices, image) + results: List[NdarrayOrTensor] = [] + if self.centers is not None: + for center in self.centers: + roi_size = fall_back_tuple(self.spatial_size, default=label.shape[1:]) + cropper = SpatialCrop(roi_center=tuple(center), roi_size=roi_size) + results.append(cropper(img)) + + return results + + +class ResizeWithPadOrCrop(Transform): + """ + Resize an image to a target spatial size by either centrally cropping the image or + padding it evenly with a user-specified mode. + When the dimension is smaller than the target size, do symmetric padding along that dim. + When the dimension is larger than the target size, do central cropping along that dim. + + Args: + spatial_size: the spatial size of output data after padding or crop. + If has non-positive values, the corresponding size of input image will be used (no padding). + mode: available modes for numpy array:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, + ``"mean"``, ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + available modes for PyTorch Tensor: {``"constant"``, ``"reflect"``, ``"replicate"``, ``"circular"``}. + One of the listed string values or a user supplied function. Defaults to ``"constant"``. + See also: https://numpy.org/doc/1.18/reference/generated/numpy.pad.html + https://pytorch.org/docs/stable/generated/torch.nn.functional.pad.html + method: {``"symmetric"``, ``"end"``} + Pad image symmetrically on every side or only pad at the end sides. Defaults to ``"symmetric"``. + pad_kwargs: other arguments for the `np.pad` or `torch.pad` function. + note that `np.pad` treats channel dimension as the first dimension. + + """ + + backend = list(set(SpatialPad.backend) & set(CenterSpatialCrop.backend)) + + def __init__( + self, + spatial_size: Union[Sequence[int], int], + mode: Union[NumpyPadMode, PytorchPadMode, str] = NumpyPadMode.CONSTANT, + method: Union[Method, str] = Method.SYMMETRIC, + **pad_kwargs, + ): + self.padder = SpatialPad(spatial_size=spatial_size, method=method, mode=mode, **pad_kwargs) + self.cropper = CenterSpatialCrop(roi_size=spatial_size) + + def __call__( + self, img: NdarrayOrTensor, mode: Optional[Union[NumpyPadMode, PytorchPadMode, str]] = None + ) -> NdarrayOrTensor: + """ + Args: + img: data to pad or crop, assuming `img` is channel-first and + padding or cropping doesn't apply to the channel dim. + mode: available modes for numpy array:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, + ``"mean"``, ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + available modes for PyTorch Tensor: {``"constant"``, ``"reflect"``, ``"replicate"``, ``"circular"``}. + One of the listed string values or a user supplied function. Defaults to ``"constant"``. + See also: https://numpy.org/doc/1.18/reference/generated/numpy.pad.html + https://pytorch.org/docs/stable/generated/torch.nn.functional.pad.html + """ + return self.padder(self.cropper(img), mode=mode) + + +class BoundingRect(Transform): + """ + Compute coordinates of axis-aligned bounding rectangles from input image `img`. + The output format of the coordinates is (shape is [channel, 2 * spatial dims]): + + [[1st_spatial_dim_start, 1st_spatial_dim_end, + 2nd_spatial_dim_start, 2nd_spatial_dim_end, + ..., + Nth_spatial_dim_start, Nth_spatial_dim_end], + + ... + + [1st_spatial_dim_start, 1st_spatial_dim_end, + 2nd_spatial_dim_start, 2nd_spatial_dim_end, + ..., + Nth_spatial_dim_start, Nth_spatial_dim_end]] + + The bounding boxes edges are aligned with the input image edges. + This function returns [0, 0, ...] if there's no positive intensity. + + Args: + select_fn: function to select expected foreground, default is to select values > 0. + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + def __init__(self, select_fn: Callable = is_positive) -> None: + self.select_fn = select_fn + + def __call__(self, img: NdarrayOrTensor) -> np.ndarray: + """ + See also: :py:class:`monai.transforms.utils.generate_spatial_bounding_box`. + """ + bbox = [] + + for channel in range(img.shape[0]): + start_, end_ = generate_spatial_bounding_box(img, select_fn=self.select_fn, channel_indices=channel) + bbox.append([i for k in zip(start_, end_) for i in k]) + + return np.stack(bbox, axis=0) diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/croppad/batch.py b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/croppad/batch.py new file mode 100644 index 0000000000000000000000000000000000000000..ab16633fdebb2ae1c0777dbd7149af031cc6b7f8 --- /dev/null +++ b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/croppad/batch.py @@ -0,0 +1,128 @@ +# 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 "vanilla" transforms for crop and pad operations acting on batches of data +https://github.com/Project-MONAI/MONAI/wiki/MONAI_Design +""" + +from copy import deepcopy +from typing import Any, Dict, Hashable, Union + +import numpy as np +import torch + +from monai.data.utils import list_data_collate +from monai.transforms.croppad.array import CenterSpatialCrop, SpatialPad +from monai.transforms.inverse import InvertibleTransform +from monai.utils.enums import Method, NumpyPadMode, PytorchPadMode, TraceKeys + +__all__ = ["PadListDataCollate"] + + +def replace_element(to_replace, batch, idx, key_or_idx): + # since tuple is immutable we'll have to recreate + if isinstance(batch[idx], tuple): + batch_idx_list = list(batch[idx]) + batch_idx_list[key_or_idx] = to_replace + batch[idx] = tuple(batch_idx_list) + # else, replace + else: + batch[idx][key_or_idx] = to_replace + return batch + + +class PadListDataCollate(InvertibleTransform): + """ + Same as MONAI's ``list_data_collate``, except any tensors are centrally padded to match the shape of the biggest + tensor in each dimension. This transform is useful if some of the applied transforms generate batch data of + different sizes. + + This can be used on both list and dictionary data. + Note that in the case of the dictionary data, it may add the transform information to the list of invertible transforms + if input batch have different spatial shape, so need to call static method: `inverse` before inverting other transforms. + + Note that normally, a user won't explicitly use the `__call__` method. Rather this would be passed to the `DataLoader`. + This means that `__call__` handles data as it comes out of a `DataLoader`, containing batch dimension. However, the + `inverse` operates on dictionaries containing images of shape `C,H,W,[D]`. This asymmetry is necessary so that we can + pass the inverse through multiprocessing. + + Args: + method: padding method (see :py:class:`monai.transforms.SpatialPad`) + mode: padding mode (see :py:class:`monai.transforms.SpatialPad`) + kwargs: other arguments for the `np.pad` or `torch.pad` function. + note that `np.pad` treats channel dimension as the first dimension. + + """ + + def __init__( + self, + method: Union[Method, str] = Method.SYMMETRIC, + mode: Union[NumpyPadMode, PytorchPadMode, str] = NumpyPadMode.CONSTANT, + **kwargs, + ) -> None: + self.method = method + self.mode = mode + self.kwargs = kwargs + + def __call__(self, batch: Any): + """ + Args: + batch: batch of data to pad-collate + """ + # data is either list of dicts or list of lists + is_list_of_dicts = isinstance(batch[0], dict) + # loop over items inside of each element in a batch + for key_or_idx in batch[0].keys() if is_list_of_dicts else range(len(batch[0])): + # calculate max size of each dimension + max_shapes = [] + for elem in batch: + if not isinstance(elem[key_or_idx], (torch.Tensor, np.ndarray)): + break + max_shapes.append(elem[key_or_idx].shape[1:]) + # len > 0 if objects were arrays, else skip as no padding to be done + if not max_shapes: + continue + max_shape = np.array(max_shapes).max(axis=0) + # If all same size, skip + if np.all(np.array(max_shapes).min(axis=0) == max_shape): + continue + + # Use `SpatialPad` to match sizes, Default params are central padding, padding with 0's + padder = SpatialPad(spatial_size=max_shape, method=self.method, mode=self.mode, **self.kwargs) + for idx, batch_i in enumerate(batch): + orig_size = batch_i[key_or_idx].shape[1:] + padded = padder(batch_i[key_or_idx]) + batch = replace_element(padded, batch, idx, key_or_idx) + + # If we have a dictionary of data, append to list + if is_list_of_dicts: + self.push_transform(batch[idx], key_or_idx, orig_size=orig_size) + + # After padding, use default list collator + return list_data_collate(batch) + + @staticmethod + def inverse(data: dict) -> Dict[Hashable, np.ndarray]: + if not isinstance(data, dict): + raise RuntimeError("Inverse can only currently be applied on dictionaries.") + + d = deepcopy(data) + for key in d: + transform_key = InvertibleTransform.trace_key(key) + if transform_key in d: + transform = d[transform_key][-1] + if not isinstance(transform, Dict): + continue + if transform.get(TraceKeys.CLASS_NAME) == PadListDataCollate.__name__: + d[key] = CenterSpatialCrop(transform.get("orig_size", -1))(d[key]) # fallback to image size + # remove transform + d[transform_key].pop() + return d diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/croppad/dictionary.py b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/croppad/dictionary.py new file mode 100644 index 0000000000000000000000000000000000000000..50cc767caba127996eb3eb2f6761c2d4d90705a3 --- /dev/null +++ b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/croppad/dictionary.py @@ -0,0 +1,1545 @@ +# 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 around the "vanilla" transforms for crop and pad operations +defined in :py:class:`monai.transforms.croppad.array`. + +Class names are ended with 'd' to denote dictionary-based transforms. +""" + +import contextlib +from copy import deepcopy +from enum import Enum +from itertools import chain +from math import ceil, floor +from typing import Any, Callable, Dict, Hashable, List, Mapping, Optional, Sequence, Tuple, Union + +import numpy as np + +from monai.config import IndexSelection, KeysCollection +from monai.config.type_definitions import NdarrayOrTensor +from monai.data.utils import get_random_patch, get_valid_patch_size +from monai.transforms.croppad.array import ( + BorderPad, + BoundingRect, + CenterSpatialCrop, + CropForeground, + DivisiblePad, + RandCropByLabelClasses, + RandCropByPosNegLabel, + ResizeWithPadOrCrop, + SpatialCrop, + SpatialPad, +) +from monai.transforms.inverse import InvertibleTransform +from monai.transforms.transform import MapTransform, Randomizable +from monai.transforms.utils import ( + allow_missing_keys_mode, + generate_label_classes_crop_centers, + generate_pos_neg_label_crop_centers, + is_positive, + map_binary_to_indices, + map_classes_to_indices, + weighted_patch_samples, +) +from monai.utils import ImageMetaKey as Key +from monai.utils import Method, NumpyPadMode, PytorchPadMode, ensure_tuple, ensure_tuple_rep, fall_back_tuple +from monai.utils.enums import PostFix, TraceKeys + +__all__ = [ + "PadModeSequence", + "SpatialPadd", + "BorderPadd", + "DivisiblePadd", + "SpatialCropd", + "CenterSpatialCropd", + "CenterScaleCropd", + "RandScaleCropd", + "RandSpatialCropd", + "RandSpatialCropSamplesd", + "CropForegroundd", + "RandWeightedCropd", + "RandCropByPosNegLabeld", + "ResizeWithPadOrCropd", + "BoundingRectd", + "RandCropByLabelClassesd", + "SpatialPadD", + "SpatialPadDict", + "BorderPadD", + "BorderPadDict", + "DivisiblePadD", + "DivisiblePadDict", + "SpatialCropD", + "SpatialCropDict", + "CenterSpatialCropD", + "CenterSpatialCropDict", + "CenterScaleCropD", + "CenterScaleCropDict", + "RandScaleCropD", + "RandScaleCropDict", + "RandSpatialCropD", + "RandSpatialCropDict", + "RandSpatialCropSamplesD", + "RandSpatialCropSamplesDict", + "CropForegroundD", + "CropForegroundDict", + "RandWeightedCropD", + "RandWeightedCropDict", + "RandCropByPosNegLabelD", + "RandCropByPosNegLabelDict", + "ResizeWithPadOrCropD", + "ResizeWithPadOrCropDict", + "BoundingRectD", + "BoundingRectDict", + "RandCropByLabelClassesD", + "RandCropByLabelClassesDict", +] + +PadModeSequence = Union[Sequence[Union[NumpyPadMode, PytorchPadMode, str]], NumpyPadMode, PytorchPadMode, str] +DEFAULT_POST_FIX = PostFix.meta() + + +class SpatialPadd(MapTransform, InvertibleTransform): + """ + Dictionary-based wrapper of :py:class:`monai.transforms.SpatialPad`. + Performs padding to the data, symmetric for all sides or all on one side for each dimension. + """ + + backend = SpatialPad.backend + + def __init__( + self, + keys: KeysCollection, + spatial_size: Union[Sequence[int], int], + method: Union[Method, str] = Method.SYMMETRIC, + mode: PadModeSequence = NumpyPadMode.CONSTANT, + allow_missing_keys: bool = False, + **kwargs, + ) -> None: + """ + Args: + keys: keys of the corresponding items to be transformed. + See also: :py:class:`monai.transforms.compose.MapTransform` + spatial_size: the spatial size of output data after padding, if a dimension of the input + data size is bigger than the pad size, will not pad that dimension. + If its components have non-positive values, the corresponding size of input image will be used. + for example: if the spatial size of input data is [30, 30, 30] and `spatial_size=[32, 25, -1]`, + the spatial size of output data will be [32, 30, 30]. + method: {``"symmetric"``, ``"end"``} + Pad image symmetrically on every side or only pad at the end sides. Defaults to ``"symmetric"``. + mode: available modes for numpy array:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, + ``"mean"``, ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + available modes for PyTorch Tensor: {``"constant"``, ``"reflect"``, ``"replicate"``, ``"circular"``}. + One of the listed string values or a user supplied function. Defaults to ``"constant"``. + See also: https://numpy.org/doc/1.18/reference/generated/numpy.pad.html + https://pytorch.org/docs/stable/generated/torch.nn.functional.pad.html + It also can be a sequence of string, each element corresponds to a key in ``keys``. + allow_missing_keys: don't raise exception if key is missing. + kwargs: other arguments for the `np.pad` or `torch.pad` function. + note that `np.pad` treats channel dimension as the first dimension. + + """ + super().__init__(keys, allow_missing_keys) + self.mode = ensure_tuple_rep(mode, len(self.keys)) + self.padder = SpatialPad(spatial_size, method, **kwargs) + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = dict(data) + for key, m in self.key_iterator(d, self.mode): + self.push_transform(d, key, extra_info={"mode": m.value if isinstance(m, Enum) else m}) + d[key] = self.padder(d[key], mode=m) + return d + + def inverse(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = deepcopy(dict(data)) + for key in self.key_iterator(d): + transform = self.get_most_recent_transform(d, key) + # Create inverse transform + orig_size = transform[TraceKeys.ORIG_SIZE] + if self.padder.method == Method.SYMMETRIC: + current_size = d[key].shape[1:] + roi_center = [floor(i / 2) if r % 2 == 0 else (i - 1) // 2 for r, i in zip(orig_size, current_size)] + else: + roi_center = [floor(r / 2) if r % 2 == 0 else (r - 1) // 2 for r in orig_size] + + inverse_transform = SpatialCrop(roi_center, orig_size) + # Apply inverse transform + d[key] = inverse_transform(d[key]) + # Remove the applied transform + self.pop_transform(d, key) + + return d + + +class BorderPadd(MapTransform, InvertibleTransform): + """ + Pad the input data by adding specified borders to every dimension. + Dictionary-based wrapper of :py:class:`monai.transforms.BorderPad`. + """ + + backend = BorderPad.backend + + def __init__( + self, + keys: KeysCollection, + spatial_border: Union[Sequence[int], int], + mode: PadModeSequence = NumpyPadMode.CONSTANT, + allow_missing_keys: bool = False, + **kwargs, + ) -> None: + """ + Args: + keys: keys of the corresponding items to be transformed. + See also: :py:class:`monai.transforms.compose.MapTransform` + spatial_border: specified size for every spatial border. it can be 3 shapes: + + - single int number, pad all the borders with the same size. + - length equals the length of image shape, pad every spatial dimension separately. + for example, image shape(CHW) is [1, 4, 4], spatial_border is [2, 1], + pad every border of H dim with 2, pad every border of W dim with 1, result shape is [1, 8, 6]. + - length equals 2 x (length of image shape), pad every border of every dimension separately. + for example, image shape(CHW) is [1, 4, 4], spatial_border is [1, 2, 3, 4], pad top of H dim with 1, + pad bottom of H dim with 2, pad left of W dim with 3, pad right of W dim with 4. + the result shape is [1, 7, 11]. + + mode: available modes for numpy array:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, + ``"mean"``, ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + available modes for PyTorch Tensor: {``"constant"``, ``"reflect"``, ``"replicate"``, ``"circular"``}. + One of the listed string values or a user supplied function. Defaults to ``"constant"``. + See also: https://numpy.org/doc/1.18/reference/generated/numpy.pad.html + https://pytorch.org/docs/stable/generated/torch.nn.functional.pad.html + It also can be a sequence of string, each element corresponds to a key in ``keys``. + allow_missing_keys: don't raise exception if key is missing. + kwargs: other arguments for the `np.pad` or `torch.pad` function. + note that `np.pad` treats channel dimension as the first dimension. + + """ + super().__init__(keys, allow_missing_keys) + self.mode = ensure_tuple_rep(mode, len(self.keys)) + self.padder = BorderPad(spatial_border=spatial_border, **kwargs) + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = dict(data) + for key, m in self.key_iterator(d, self.mode): + self.push_transform(d, key, extra_info={"mode": m.value if isinstance(m, Enum) else m}) + d[key] = self.padder(d[key], mode=m) + return d + + def inverse(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = deepcopy(dict(data)) + + for key in self.key_iterator(d): + transform = self.get_most_recent_transform(d, key) + # Create inverse transform + orig_size = np.array(transform[TraceKeys.ORIG_SIZE]) + roi_start = np.array(self.padder.spatial_border) + # Need to convert single value to [min1,min2,...] + if roi_start.size == 1: + roi_start = np.full((len(orig_size)), roi_start) + # need to convert [min1,max1,min2,...] to [min1,min2,...] + elif roi_start.size == 2 * orig_size.size: + roi_start = roi_start[::2] + roi_end = np.array(transform[TraceKeys.ORIG_SIZE]) + roi_start + + inverse_transform = SpatialCrop(roi_start=roi_start, roi_end=roi_end) + # Apply inverse transform + d[key] = inverse_transform(d[key]) + # Remove the applied transform + self.pop_transform(d, key) + + return d + + +class DivisiblePadd(MapTransform, InvertibleTransform): + """ + Pad the input data, so that the spatial sizes are divisible by `k`. + Dictionary-based wrapper of :py:class:`monai.transforms.DivisiblePad`. + """ + + backend = DivisiblePad.backend + + def __init__( + self, + keys: KeysCollection, + k: Union[Sequence[int], int], + mode: PadModeSequence = NumpyPadMode.CONSTANT, + method: Union[Method, str] = Method.SYMMETRIC, + allow_missing_keys: bool = False, + **kwargs, + ) -> None: + """ + Args: + keys: keys of the corresponding items to be transformed. + See also: :py:class:`monai.transforms.compose.MapTransform` + k: the target k for each spatial dimension. + if `k` is negative or 0, the original size is preserved. + if `k` is an int, the same `k` be applied to all the input spatial dimensions. + mode: available modes for numpy array:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, + ``"mean"``, ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + available modes for PyTorch Tensor: {``"constant"``, ``"reflect"``, ``"replicate"``, ``"circular"``}. + One of the listed string values or a user supplied function. Defaults to ``"constant"``. + See also: https://numpy.org/doc/1.18/reference/generated/numpy.pad.html + https://pytorch.org/docs/stable/generated/torch.nn.functional.pad.html + It also can be a sequence of string, each element corresponds to a key in ``keys``. + method: {``"symmetric"``, ``"end"``} + Pad image symmetrically on every side or only pad at the end sides. Defaults to ``"symmetric"``. + allow_missing_keys: don't raise exception if key is missing. + kwargs: other arguments for the `np.pad` or `torch.pad` function. + note that `np.pad` treats channel dimension as the first dimension. + + See also :py:class:`monai.transforms.SpatialPad` + + """ + super().__init__(keys, allow_missing_keys) + self.mode = ensure_tuple_rep(mode, len(self.keys)) + self.padder = DivisiblePad(k=k, method=method, **kwargs) + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = dict(data) + for key, m in self.key_iterator(d, self.mode): + self.push_transform(d, key, extra_info={"mode": m.value if isinstance(m, Enum) else m}) + d[key] = self.padder(d[key], mode=m) + return d + + def inverse(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = deepcopy(dict(data)) + + for key in self.key_iterator(d): + transform = self.get_most_recent_transform(d, key) + # Create inverse transform + orig_size = np.array(transform[TraceKeys.ORIG_SIZE]) + current_size = np.array(d[key].shape[1:]) + roi_start = np.floor((current_size - orig_size) / 2) + roi_end = orig_size + roi_start + inverse_transform = SpatialCrop(roi_start=roi_start, roi_end=roi_end) + # Apply inverse transform + d[key] = inverse_transform(d[key]) + # Remove the applied transform + self.pop_transform(d, key) + + return d + + +class SpatialCropd(MapTransform, InvertibleTransform): + """ + Dictionary-based wrapper of :py:class:`monai.transforms.SpatialCrop`. + General purpose cropper to produce sub-volume region of interest (ROI). + If a dimension of the expected ROI size is bigger than the input image size, will not crop that dimension. + So the cropped result may be smaller than the expected ROI, and the cropped results of several images may + not have exactly the same shape. + It can support to crop ND spatial (channel-first) data. + + The cropped region can be parameterised in various ways: + - a list of slices for each spatial dimension (allows for use of -ve indexing and `None`) + - a spatial center and size + - the start and end coordinates of the ROI + """ + + backend = SpatialCrop.backend + + def __init__( + self, + keys: KeysCollection, + roi_center: Optional[Sequence[int]] = None, + roi_size: Optional[Sequence[int]] = None, + roi_start: Optional[Sequence[int]] = None, + roi_end: Optional[Sequence[int]] = None, + roi_slices: Optional[Sequence[slice]] = None, + allow_missing_keys: bool = False, + ) -> None: + """ + Args: + keys: keys of the corresponding items to be transformed. + See also: :py:class:`monai.transforms.compose.MapTransform` + roi_center: voxel coordinates for center of the crop ROI. + roi_size: size of the crop ROI, if a dimension of ROI size is bigger than image size, + will not crop that dimension of the image. + roi_start: voxel coordinates for start of the crop ROI. + roi_end: voxel coordinates for end of the crop ROI, if a coordinate is out of image, + use the end coordinate of image. + roi_slices: list of slices for each of the spatial dimensions. + allow_missing_keys: don't raise exception if key is missing. + """ + super().__init__(keys, allow_missing_keys) + self.cropper = SpatialCrop(roi_center, roi_size, roi_start, roi_end, roi_slices) + + 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) + d[key] = self.cropper(d[key]) + return d + + def inverse(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = deepcopy(dict(data)) + + for key in self.key_iterator(d): + transform = self.get_most_recent_transform(d, key) + # Create inverse transform + orig_size = np.array(transform[TraceKeys.ORIG_SIZE]) + current_size = np.array(d[key].shape[1:]) + # get required pad to start and end + pad_to_start = np.array([s.indices(o)[0] for s, o in zip(self.cropper.slices, orig_size)]) + pad_to_end = orig_size - current_size - pad_to_start + # interleave mins and maxes + pad = list(chain(*zip(pad_to_start.tolist(), pad_to_end.tolist()))) + inverse_transform = BorderPad(pad) + # Apply inverse transform + d[key] = inverse_transform(d[key]) + # Remove the applied transform + self.pop_transform(d, key) + + return d + + +class CenterSpatialCropd(MapTransform, InvertibleTransform): + """ + Dictionary-based wrapper of :py:class:`monai.transforms.CenterSpatialCrop`. + If a dimension of the expected ROI size is bigger than the input image size, will not crop that dimension. + So the cropped result may be smaller than the expected ROI, and the cropped results of several images may + not have exactly the same shape. + + Args: + keys: keys of the corresponding items to be transformed. + See also: monai.transforms.MapTransform + roi_size: the size of the crop region e.g. [224,224,128] + if a dimension of ROI size is bigger than image size, will not crop that dimension of the image. + If its components have non-positive values, the corresponding size of input image will be used. + for example: if the spatial size of input data is [40, 40, 40] and `roi_size=[32, 64, -1]`, + the spatial size of output data will be [32, 40, 40]. + allow_missing_keys: don't raise exception if key is missing. + """ + + backend = CenterSpatialCrop.backend + + def __init__( + self, keys: KeysCollection, roi_size: Union[Sequence[int], int], allow_missing_keys: bool = False + ) -> None: + super().__init__(keys, allow_missing_keys) + self.cropper = CenterSpatialCrop(roi_size) + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = dict(data) + for key in self.key_iterator(d): + orig_size = d[key].shape[1:] + d[key] = self.cropper(d[key]) + self.push_transform(d, key, orig_size=orig_size) + return d + + def inverse(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = deepcopy(dict(data)) + + for key in self.key_iterator(d): + transform = self.get_most_recent_transform(d, key) + # Create inverse transform + orig_size = np.array(transform[TraceKeys.ORIG_SIZE]) + current_size = np.array(d[key].shape[1:]) + pad_to_start = np.floor((orig_size - current_size) / 2).astype(int) + # in each direction, if original size is even and current size is odd, += 1 + pad_to_start[np.logical_and(orig_size % 2 == 0, current_size % 2 == 1)] += 1 + pad_to_end = orig_size - current_size - pad_to_start + pad = list(chain(*zip(pad_to_start.tolist(), pad_to_end.tolist()))) + inverse_transform = BorderPad(pad) + # Apply inverse transform + d[key] = inverse_transform(d[key]) + # Remove the applied transform + self.pop_transform(d, key) + + return d + + +class CenterScaleCropd(MapTransform, InvertibleTransform): + """ + Dictionary-based wrapper of :py:class:`monai.transforms.CenterScaleCrop`. + Note: as using the same scaled ROI to crop, all the input data specified by `keys` should have + the same spatial shape. + + Args: + keys: keys of the corresponding items to be transformed. + See also: monai.transforms.MapTransform + roi_scale: specifies the expected scale of image size to crop. e.g. [0.3, 0.4, 0.5] or a number for all dims. + If its components have non-positive values, will use `1.0` instead, which means the input image size. + allow_missing_keys: don't raise exception if key is missing. + """ + + backend = CenterSpatialCrop.backend + + def __init__( + self, keys: KeysCollection, roi_scale: Union[Sequence[float], float], allow_missing_keys: bool = False + ) -> None: + super().__init__(keys, allow_missing_keys=allow_missing_keys) + self.roi_scale = roi_scale + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = dict(data) + first_key: Union[Hashable, List] = self.first_key(d) + if first_key == []: + return d + + # use the spatial size of first image to scale, expect all images have the same spatial size + img_size = d[first_key].shape[1:] # type: ignore + ndim = len(img_size) + roi_size = [ceil(r * s) for r, s in zip(ensure_tuple_rep(self.roi_scale, ndim), img_size)] + cropper = CenterSpatialCrop(roi_size) + for key in self.key_iterator(d): + self.push_transform(d, key, orig_size=img_size) + d[key] = cropper(d[key]) + + return d + + def inverse(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = deepcopy(dict(data)) + + for key in self.key_iterator(d): + transform = self.get_most_recent_transform(d, key) + # Create inverse transform + orig_size = np.array(transform[TraceKeys.ORIG_SIZE]) + current_size = np.array(d[key].shape[1:]) + pad_to_start = np.floor((orig_size - current_size) / 2).astype(int) + # in each direction, if original size is even and current size is odd, += 1 + pad_to_start[np.logical_and(orig_size % 2 == 0, current_size % 2 == 1)] += 1 + pad_to_end = orig_size - current_size - pad_to_start + pad = list(chain(*zip(pad_to_start.tolist(), pad_to_end.tolist()))) + inverse_transform = BorderPad(pad) + # Apply inverse transform + d[key] = inverse_transform(d[key]) + # Remove the applied transform + self.pop_transform(d, key) + + return d + + +class RandSpatialCropd(Randomizable, MapTransform, InvertibleTransform): + """ + Dictionary-based version :py:class:`monai.transforms.RandSpatialCrop`. + Crop image with random size or specific size ROI. It can crop at a random position as + center or at the image center. And allows to set the minimum and maximum size to limit the randomly + generated ROI. Suppose all the expected fields specified by `keys` have same shape. + + Note: even `random_size=False`, if a dimension of the expected ROI size is bigger than the input image size, + will not crop that dimension. So the cropped result may be smaller than the expected ROI, and the cropped + results of several images may not have exactly the same shape. + + Args: + keys: keys of the corresponding items to be transformed. + See also: monai.transforms.MapTransform + roi_size: if `random_size` is True, it specifies the minimum crop region. + if `random_size` is False, it specifies the expected ROI size to crop. e.g. [224, 224, 128] + if a dimension of ROI size is bigger than image size, will not crop that dimension of the image. + If its components have non-positive values, the corresponding size of input image will be used. + for example: if the spatial size of input data is [40, 40, 40] and `roi_size=[32, 64, -1]`, + the spatial size of output data will be [32, 40, 40]. + max_roi_size: if `random_size` is True and `roi_size` specifies the min crop region size, `max_roi_size` + can specify the max crop region size. if None, defaults to the input image size. + if its components have non-positive values, the corresponding size of input image will be used. + random_center: crop at random position as center or the image center. + random_size: crop with random size or specific size ROI. + if True, the actual size is sampled from: + `randint(roi_scale * image spatial size, max_roi_scale * image spatial size + 1)`. + allow_missing_keys: don't raise exception if key is missing. + """ + + backend = CenterSpatialCrop.backend + + def __init__( + self, + keys: KeysCollection, + roi_size: Union[Sequence[int], int], + max_roi_size: Optional[Union[Sequence[int], int]] = None, + random_center: bool = True, + random_size: bool = True, + allow_missing_keys: bool = False, + ) -> None: + MapTransform.__init__(self, keys, allow_missing_keys) + self.roi_size = roi_size + self.max_roi_size = max_roi_size + self.random_center = random_center + self.random_size = random_size + self._slices: Optional[Tuple[slice, ...]] = None + self._size: Optional[Sequence[int]] = None + + def randomize(self, img_size: Sequence[int]) -> None: + self._size = fall_back_tuple(self.roi_size, img_size) + if self.random_size: + max_size = img_size if self.max_roi_size is None else fall_back_tuple(self.max_roi_size, img_size) + if any(i > j for i, j in zip(self._size, max_size)): + raise ValueError(f"min ROI size: {self._size} is bigger than max ROI size: {max_size}.") + self._size = [self.R.randint(low=self._size[i], high=max_size[i] + 1) for i in range(len(img_size))] + if self.random_center: + valid_size = get_valid_patch_size(img_size, self._size) + self._slices = (slice(None),) + get_random_patch(img_size, valid_size, self.R) + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = dict(data) + first_key: Union[Hashable, List] = self.first_key(d) + if first_key == []: + return d + + self.randomize(d[first_key].shape[1:]) # type: ignore + if self._size is None: + raise RuntimeError("self._size not specified.") + for key in self.key_iterator(d): + if self.random_center: + self.push_transform(d, key, {"slices": [(i.start, i.stop) for i in self._slices[1:]]}) # type: ignore + d[key] = d[key][self._slices] + else: + self.push_transform(d, key) + cropper = CenterSpatialCrop(self._size) + d[key] = cropper(d[key]) + return d + + def inverse(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = deepcopy(dict(data)) + + for key in self.key_iterator(d): + transform = self.get_most_recent_transform(d, key) + # Create inverse transform + orig_size = transform[TraceKeys.ORIG_SIZE] + random_center = self.random_center + pad_to_start = np.empty((len(orig_size)), dtype=np.int32) + pad_to_end = np.empty((len(orig_size)), dtype=np.int32) + if random_center: + for i, _slice in enumerate(transform[TraceKeys.EXTRA_INFO]["slices"]): + pad_to_start[i] = _slice[0] + pad_to_end[i] = orig_size[i] - _slice[1] + else: + current_size = d[key].shape[1:] + for i, (o_s, c_s) in enumerate(zip(orig_size, current_size)): + pad_to_start[i] = pad_to_end[i] = (o_s - c_s) / 2 + if o_s % 2 == 0 and c_s % 2 == 1: + pad_to_start[i] += 1 + elif o_s % 2 == 1 and c_s % 2 == 0: + pad_to_end[i] += 1 + # interleave mins and maxes + pad = list(chain(*zip(pad_to_start.tolist(), pad_to_end.tolist()))) + inverse_transform = BorderPad(pad) + # Apply inverse transform + d[key] = inverse_transform(d[key]) + # Remove the applied transform + self.pop_transform(d, key) + + return d + + +class RandScaleCropd(RandSpatialCropd): + """ + Dictionary-based version :py:class:`monai.transforms.RandScaleCrop`. + Crop image with random size or specific size ROI. + It can crop at a random position as center or at the image center. + And allows to set the minimum and maximum scale of image size to limit the randomly generated ROI. + Suppose all the expected fields specified by `keys` have same shape. + + Args: + keys: keys of the corresponding items to be transformed. + See also: monai.transforms.MapTransform + roi_scale: if `random_size` is True, it specifies the minimum crop size: `roi_scale * image spatial size`. + if `random_size` is False, it specifies the expected scale of image size to crop. e.g. [0.3, 0.4, 0.5]. + If its components have non-positive values, will use `1.0` instead, which means the input image size. + max_roi_scale: if `random_size` is True and `roi_scale` specifies the min crop region size, `max_roi_scale` + can specify the max crop region size: `max_roi_scale * image spatial size`. + if None, defaults to the input image size. if its components have non-positive values, + will use `1.0` instead, which means the input image size. + random_center: crop at random position as center or the image center. + random_size: crop with random size or specified size ROI by `roi_scale * image spatial size`. + if True, the actual size is sampled from: + `randint(roi_scale * image spatial size, max_roi_scale * image spatial size + 1)`. + allow_missing_keys: don't raise exception if key is missing. + """ + + backend = RandSpatialCropd.backend + + def __init__( + self, + keys: KeysCollection, + roi_scale: Union[Sequence[float], float], + max_roi_scale: Optional[Union[Sequence[float], float]] = None, + random_center: bool = True, + random_size: bool = True, + allow_missing_keys: bool = False, + ) -> None: + super().__init__( + keys=keys, + roi_size=-1, + max_roi_size=None, + random_center=random_center, + random_size=random_size, + allow_missing_keys=allow_missing_keys, + ) + self.roi_scale = roi_scale + self.max_roi_scale = max_roi_scale + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + first_key: Union[Hashable, List] = self.first_key(data) # type: ignore + if first_key == []: + return data # type: ignore + + img_size = data[first_key].shape[1:] # type: ignore + ndim = len(img_size) + self.roi_size = [ceil(r * s) for r, s in zip(ensure_tuple_rep(self.roi_scale, ndim), img_size)] + if self.max_roi_scale is not None: + self.max_roi_size = [ceil(r * s) for r, s in zip(ensure_tuple_rep(self.max_roi_scale, ndim), img_size)] + else: + self.max_roi_size = None + return super().__call__(data=data) + + +@contextlib.contextmanager +def _nullcontext(x): + """ + This is just like contextlib.nullcontext but also works in Python 3.6. + """ + yield x + + +class RandSpatialCropSamplesd(Randomizable, MapTransform, InvertibleTransform): + """ + Dictionary-based version :py:class:`monai.transforms.RandSpatialCropSamples`. + Crop image with random size or specific size ROI to generate a list of N samples. + It can crop at a random position as center or at the image center. And allows to set + the minimum size to limit the randomly generated ROI. Suppose all the expected fields + specified by `keys` have same shape, and add `patch_index` to the corresponding metadata. + It will return a list of dictionaries for all the cropped images. + + Note: even `random_size=False`, if a dimension of the expected ROI size is bigger than the input image size, + will not crop that dimension. So the cropped result may be smaller than the expected ROI, and the cropped + results of several images may not have exactly the same shape. + + Args: + keys: keys of the corresponding items to be transformed. + See also: monai.transforms.MapTransform + roi_size: if `random_size` is True, it specifies the minimum crop region. + if `random_size` is False, it specifies the expected ROI size to crop. e.g. [224, 224, 128] + if a dimension of ROI size is bigger than image size, will not crop that dimension of the image. + If its components have non-positive values, the corresponding size of input image will be used. + for example: if the spatial size of input data is [40, 40, 40] and `roi_size=[32, 64, -1]`, + the spatial size of output data will be [32, 40, 40]. + num_samples: number of samples (crop regions) to take in the returned list. + max_roi_size: if `random_size` is True and `roi_size` specifies the min crop region size, `max_roi_size` + can specify the max crop region size. if None, defaults to the input image size. + if its components have non-positive values, the corresponding size of input image will be used. + random_center: crop at random position as center or the image center. + random_size: crop with random size or specific size ROI. + The actual size is sampled from `randint(roi_size, img_size)`. + meta_keys: explicitly indicate the key of the corresponding metadata dictionary. + used to add `patch_index` to the meta dict. + for example, for data with key `image`, the metadata by default is in `image_meta_dict`. + the metadata is a dictionary object which contains: filename, original_shape, etc. + it can be a sequence of string, map to the `keys`. + if None, will try to construct meta_keys by `key_{meta_key_postfix}`. + meta_key_postfix: if meta_keys is None, use `key_{postfix}` to fetch the metadata according + to the key data, default is `meta_dict`, the metadata is a dictionary object. + used to add `patch_index` to the meta dict. + allow_missing_keys: don't raise exception if key is missing. + + Raises: + ValueError: When ``num_samples`` is nonpositive. + + """ + + backend = RandSpatialCropd.backend + + def __init__( + self, + keys: KeysCollection, + roi_size: Union[Sequence[int], int], + num_samples: int, + max_roi_size: Optional[Union[Sequence[int], int]] = None, + random_center: bool = True, + random_size: bool = True, + meta_keys: Optional[KeysCollection] = None, + meta_key_postfix: str = DEFAULT_POST_FIX, + allow_missing_keys: bool = False, + ) -> None: + MapTransform.__init__(self, keys, allow_missing_keys) + if num_samples < 1: + raise ValueError(f"num_samples must be positive, got {num_samples}.") + self.num_samples = num_samples + self.cropper = RandSpatialCropd(keys, roi_size, max_roi_size, random_center, random_size, allow_missing_keys) + self.meta_keys = ensure_tuple_rep(None, len(self.keys)) if meta_keys is None else ensure_tuple(meta_keys) + if len(self.keys) != len(self.meta_keys): + raise ValueError("meta_keys should have the same length as keys.") + self.meta_key_postfix = ensure_tuple_rep(meta_key_postfix, len(self.keys)) + + def set_random_state( + self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None + ) -> "RandSpatialCropSamplesd": + super().set_random_state(seed, state) + self.cropper.set_random_state(seed, state) + return self + + def randomize(self, data: Optional[Any] = None) -> None: + pass + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict[Hashable, NdarrayOrTensor]]: + ret = [] + for i in range(self.num_samples): + d = dict(data) + # deep copy all the unmodified data + for key in set(data.keys()).difference(set(self.keys)): + d[key] = deepcopy(data[key]) + cropped = self.cropper(d) + # self.cropper will have added RandSpatialCropd to the list. Change to RandSpatialCropSamplesd + for key in self.key_iterator(cropped): + cropped[self.trace_key(key)][-1][TraceKeys.CLASS_NAME] = self.__class__.__name__ # type: ignore + cropped[self.trace_key(key)][-1][TraceKeys.ID] = id(self) # type: ignore + # add `patch_index` to the metadata + for key, meta_key, meta_key_postfix in self.key_iterator(d, self.meta_keys, self.meta_key_postfix): + meta_key = meta_key or f"{key}_{meta_key_postfix}" + if meta_key not in cropped: + cropped[meta_key] = {} # type: ignore + cropped[meta_key][Key.PATCH_INDEX] = i # type: ignore + ret.append(cropped) + return ret + + def inverse(self, data: Mapping[Hashable, Any]) -> Dict[Hashable, Any]: + d = deepcopy(dict(data)) + # We changed the transform name from RandSpatialCropd to RandSpatialCropSamplesd + # Need to revert that since we're calling RandSpatialCropd's inverse + for key in self.key_iterator(d): + d[self.trace_key(key)][-1][TraceKeys.CLASS_NAME] = self.cropper.__class__.__name__ + d[self.trace_key(key)][-1][TraceKeys.ID] = id(self.cropper) + context_manager = allow_missing_keys_mode if self.allow_missing_keys else _nullcontext + with context_manager(self.cropper): + return self.cropper.inverse(d) + + +class CropForegroundd(MapTransform, InvertibleTransform): + """ + Dictionary-based version :py:class:`monai.transforms.CropForeground`. + Crop only the foreground object of the expected images. + The typical usage is to help training and evaluation if the valid part is small in the whole medical image. + The valid part can be determined by any field in the data with `source_key`, for example: + - Select values > 0 in image field as the foreground and crop on all fields specified by `keys`. + - Select label = 3 in label field as the foreground to crop on all fields specified by `keys`. + - Select label > 0 in the third channel of a One-Hot label field as the foreground to crop all `keys` fields. + Users can define arbitrary function to select expected foreground from the whole source image or specified + channels. And it can also add margin to every dim of the bounding box of foreground object. + """ + + backend = CropForeground.backend + + def __init__( + self, + keys: KeysCollection, + source_key: str, + select_fn: Callable = is_positive, + channel_indices: Optional[IndexSelection] = None, + margin: Union[Sequence[int], int] = 0, + allow_smaller: bool = True, + k_divisible: Union[Sequence[int], int] = 1, + mode: PadModeSequence = NumpyPadMode.CONSTANT, + start_coord_key: str = "foreground_start_coord", + end_coord_key: str = "foreground_end_coord", + allow_missing_keys: bool = False, + **pad_kwargs, + ) -> None: + """ + Args: + keys: keys of the corresponding items to be transformed. + See also: :py:class:`monai.transforms.compose.MapTransform` + source_key: data source to generate the bounding box of foreground, can be image or label, etc. + select_fn: function to select expected foreground, default is to select values > 0. + channel_indices: if defined, select foreground only on the specified channels + of image. if None, select foreground on the whole image. + margin: add margin value to spatial dims of the bounding box, if only 1 value provided, use it for all dims. + allow_smaller: when computing box size with `margin`, whether allow the image size to be smaller + than box size, default to `True`. if the margined size is bigger than image size, will pad with + specified `mode`. + k_divisible: make each spatial dimension to be divisible by k, default to 1. + if `k_divisible` is an int, the same `k` be applied to all the input spatial dimensions. + mode: available modes for numpy array:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, + ``"mean"``, ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + available modes for PyTorch Tensor: {``"constant"``, ``"reflect"``, ``"replicate"``, ``"circular"``}. + One of the listed string values or a user supplied function. Defaults to ``"constant"``. + See also: https://numpy.org/doc/1.18/reference/generated/numpy.pad.html + https://pytorch.org/docs/stable/generated/torch.nn.functional.pad.html + it also can be a sequence of string, each element corresponds to a key in ``keys``. + start_coord_key: key to record the start coordinate of spatial bounding box for foreground. + end_coord_key: key to record the end coordinate of spatial bounding box for foreground. + allow_missing_keys: don't raise exception if key is missing. + pad_kwargs: other arguments for the `np.pad` or `torch.pad` function. + note that `np.pad` treats channel dimension as the first dimension. + + """ + super().__init__(keys, allow_missing_keys) + self.source_key = source_key + self.start_coord_key = start_coord_key + self.end_coord_key = end_coord_key + self.cropper = CropForeground( + select_fn=select_fn, + channel_indices=channel_indices, + margin=margin, + allow_smaller=allow_smaller, + k_divisible=k_divisible, + **pad_kwargs, + ) + self.mode = ensure_tuple_rep(mode, len(self.keys)) + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = dict(data) + box_start, box_end = self.cropper.compute_bounding_box(img=d[self.source_key]) + d[self.start_coord_key] = box_start + d[self.end_coord_key] = box_end + for key, m in self.key_iterator(d, self.mode): + self.push_transform(d, key, extra_info={"box_start": box_start, "box_end": box_end}) + d[key] = self.cropper.crop_pad(img=d[key], box_start=box_start, box_end=box_end, mode=m) + return d + + def inverse(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = deepcopy(dict(data)) + for key in self.key_iterator(d): + transform = self.get_most_recent_transform(d, key) + # Create inverse transform + orig_size = np.asarray(transform[TraceKeys.ORIG_SIZE]) + cur_size = np.asarray(d[key].shape[1:]) + extra_info = transform[TraceKeys.EXTRA_INFO] + box_start = np.asarray(extra_info["box_start"]) + box_end = np.asarray(extra_info["box_end"]) + # first crop the padding part + roi_start = np.maximum(-box_start, 0) + roi_end = cur_size - np.maximum(box_end - orig_size, 0) + + d[key] = SpatialCrop(roi_start=roi_start, roi_end=roi_end)(d[key]) + + # update bounding box to pad + pad_to_start = np.maximum(box_start, 0) + pad_to_end = orig_size - np.minimum(box_end, orig_size) + # interleave mins and maxes + pad = list(chain(*zip(pad_to_start.tolist(), pad_to_end.tolist()))) + # second pad back the original size + d[key] = BorderPad(pad)(d[key]) + # Remove the applied transform + self.pop_transform(d, key) + + return d + + +class RandWeightedCropd(Randomizable, MapTransform, InvertibleTransform): + """ + Samples a list of `num_samples` image patches according to the provided `weight_map`. + + Args: + keys: keys of the corresponding items to be transformed. + See also: :py:class:`monai.transforms.compose.MapTransform` + w_key: key for the weight map. The corresponding value will be used as the sampling weights, + it should be a single-channel array in size, for example, `(1, spatial_dim_0, spatial_dim_1, ...)` + spatial_size: the spatial size of the image patch e.g. [224, 224, 128]. + If its components have non-positive values, the corresponding size of `img` will be used. + num_samples: number of samples (image patches) to take in the returned list. + center_coord_key: if specified, the actual sampling location will be stored with the corresponding key. + meta_keys: explicitly indicate the key of the corresponding metadata dictionary. + used to add `patch_index` to the meta dict. + for example, for data with key `image`, the metadata by default is in `image_meta_dict`. + the metadata is a dictionary object which contains: filename, original_shape, etc. + it can be a sequence of string, map to the `keys`. + if None, will try to construct meta_keys by `key_{meta_key_postfix}`. + meta_key_postfix: if meta_keys is None, use `key_{postfix}` to fetch the metadata according + to the key data, default is `meta_dict`, the metadata is a dictionary object. + used to add `patch_index` to the meta dict. + allow_missing_keys: don't raise exception if key is missing. + + See Also: + :py:class:`monai.transforms.RandWeightedCrop` + """ + + backend = SpatialCrop.backend + + def __init__( + self, + keys: KeysCollection, + w_key: str, + spatial_size: Union[Sequence[int], int], + num_samples: int = 1, + center_coord_key: Optional[str] = None, + meta_keys: Optional[KeysCollection] = None, + meta_key_postfix: str = DEFAULT_POST_FIX, + allow_missing_keys: bool = False, + ): + MapTransform.__init__(self, keys, allow_missing_keys) + self.spatial_size = ensure_tuple(spatial_size) + self.w_key = w_key + self.num_samples = int(num_samples) + self.center_coord_key = center_coord_key + self.meta_keys = ensure_tuple_rep(None, len(self.keys)) if meta_keys is None else ensure_tuple(meta_keys) + if len(self.keys) != len(self.meta_keys): + raise ValueError("meta_keys should have the same length as keys.") + self.meta_key_postfix = ensure_tuple_rep(meta_key_postfix, len(self.keys)) + self.centers: List[np.ndarray] = [] + + def randomize(self, weight_map: NdarrayOrTensor) -> None: + self.centers = weighted_patch_samples( + spatial_size=self.spatial_size, w=weight_map[0], n_samples=self.num_samples, r_state=self.R + ) + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict[Hashable, NdarrayOrTensor]]: + d = dict(data) + self.randomize(d[self.w_key]) + _spatial_size = fall_back_tuple(self.spatial_size, d[self.w_key].shape[1:]) + + # initialize returned list with shallow copy to preserve key ordering + results: List[Dict[Hashable, NdarrayOrTensor]] = [dict(data) for _ in range(self.num_samples)] + # fill in the extra keys with unmodified data + for i in range(self.num_samples): + for key in set(data.keys()).difference(set(self.keys)): + results[i][key] = deepcopy(data[key]) + for key in self.key_iterator(d): + img = d[key] + if img.shape[1:] != d[self.w_key].shape[1:]: + raise ValueError( + f"data {key} and weight map {self.w_key} spatial shape mismatch: " + f"{img.shape[1:]} vs {d[self.w_key].shape[1:]}." + ) + for i, center in enumerate(self.centers): + cropper = SpatialCrop(roi_center=center, roi_size=_spatial_size) + orig_size = img.shape[1:] + results[i][key] = cropper(img) + self.push_transform(results[i], key, extra_info={"center": center}, orig_size=orig_size) + if self.center_coord_key: + results[i][self.center_coord_key] = center + # fill in the extra keys with unmodified data + for i in range(self.num_samples): + # add `patch_index` to the metadata + for key, meta_key, meta_key_postfix in self.key_iterator(d, self.meta_keys, self.meta_key_postfix): + meta_key = meta_key or f"{key}_{meta_key_postfix}" + if meta_key not in results[i]: + results[i][meta_key] = {} # type: ignore + results[i][meta_key][Key.PATCH_INDEX] = i # type: ignore + + return results + + def inverse(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = deepcopy(dict(data)) + for key in self.key_iterator(d): + transform = self.get_most_recent_transform(d, key) + # Create inverse transform + orig_size = np.asarray(transform[TraceKeys.ORIG_SIZE]) + current_size = np.asarray(d[key].shape[1:]) + center = transform[TraceKeys.EXTRA_INFO]["center"] + cropper = SpatialCrop(roi_center=center, roi_size=self.spatial_size) + # get required pad to start and end + pad_to_start = np.array([s.indices(o)[0] for s, o in zip(cropper.slices, orig_size)]) + pad_to_end = orig_size - current_size - pad_to_start + # interleave mins and maxes + pad = list(chain(*zip(pad_to_start.tolist(), pad_to_end.tolist()))) + inverse_transform = BorderPad(pad) + # Apply inverse transform + d[key] = inverse_transform(d[key]) + # Remove the applied transform + self.pop_transform(d, key) + + return d + + +class RandCropByPosNegLabeld(Randomizable, MapTransform, InvertibleTransform): + """ + Dictionary-based version :py:class:`monai.transforms.RandCropByPosNegLabel`. + Crop random fixed sized regions with the center being a foreground or background voxel + based on the Pos Neg Ratio. + Suppose all the expected fields specified by `keys` have same shape, + and add `patch_index` to the corresponding metadata. + And will return a list of dictionaries for all the cropped images. + + If a dimension of the expected spatial size is bigger than the input image size, + will not crop that dimension. So the cropped result may be smaller than the expected size, + and the cropped results of several images may not have exactly the same shape. + And if the crop ROI is partly out of the image, will automatically adjust the crop center + to ensure the valid crop ROI. + + Args: + keys: keys of the corresponding items to be transformed. + See also: :py:class:`monai.transforms.compose.MapTransform` + label_key: name of key for label image, this will be used for finding foreground/background. + spatial_size: the spatial size of the crop region e.g. [224, 224, 128]. + if a dimension of ROI size is bigger than image size, will not crop that dimension of the image. + if its components have non-positive values, the corresponding size of `data[label_key]` will be used. + for example: if the spatial size of input data is [40, 40, 40] and `spatial_size=[32, 64, -1]`, + the spatial size of output data will be [32, 40, 40]. + pos: used with `neg` together to calculate the ratio ``pos / (pos + neg)`` for the probability + to pick a foreground voxel as a center rather than a background voxel. + neg: used with `pos` together to calculate the ratio ``pos / (pos + neg)`` for the probability + to pick a foreground voxel as a center rather than a background voxel. + num_samples: number of samples (crop regions) to take in each list. + image_key: if image_key is not None, use ``label == 0 & image > image_threshold`` to select + the negative sample(background) center. so the crop center will only exist on valid image area. + image_threshold: if enabled image_key, use ``image > image_threshold`` to determine + the valid image content area. + fg_indices_key: if provided pre-computed foreground indices of `label`, will ignore above `image_key` and + `image_threshold`, and randomly select crop centers based on them, need to provide `fg_indices_key` + and `bg_indices_key` together, expect to be 1 dim array of spatial indices after flattening. + a typical usage is to call `FgBgToIndicesd` transform first and cache the results. + bg_indices_key: if provided pre-computed background indices of `label`, will ignore above `image_key` and + `image_threshold`, and randomly select crop centers based on them, need to provide `fg_indices_key` + and `bg_indices_key` together, expect to be 1 dim array of spatial indices after flattening. + a typical usage is to call `FgBgToIndicesd` transform first and cache the results. + meta_keys: explicitly indicate the key of the corresponding metadata dictionary. + used to add `patch_index` to the meta dict. + for example, for data with key `image`, the metadata by default is in `image_meta_dict`. + the metadata is a dictionary object which contains: filename, original_shape, etc. + it can be a sequence of string, map to the `keys`. + if None, will try to construct meta_keys by `key_{meta_key_postfix}`. + meta_key_postfix: if meta_keys is None, use `key_{postfix}` to fetch the metadata according + to the key data, default is `meta_dict`, the metadata is a dictionary object. + used to add `patch_index` to the meta dict. + allow_smaller: if `False`, an exception will be raised if the image is smaller than + the requested ROI in any dimension. If `True`, any smaller dimensions will be set to + match the cropped size (i.e., no cropping in that dimension). + allow_missing_keys: don't raise exception if key is missing. + + Raises: + ValueError: When ``pos`` or ``neg`` are negative. + ValueError: When ``pos=0`` and ``neg=0``. Incompatible values. + + """ + + backend = RandCropByPosNegLabel.backend + + def __init__( + self, + keys: KeysCollection, + label_key: str, + spatial_size: Union[Sequence[int], int], + pos: float = 1.0, + neg: float = 1.0, + num_samples: int = 1, + image_key: Optional[str] = None, + image_threshold: float = 0.0, + fg_indices_key: Optional[str] = None, + bg_indices_key: Optional[str] = None, + meta_keys: Optional[KeysCollection] = None, + meta_key_postfix: str = DEFAULT_POST_FIX, + allow_smaller: bool = False, + allow_missing_keys: bool = False, + ) -> None: + MapTransform.__init__(self, keys, allow_missing_keys) + self.label_key = label_key + self.spatial_size: Union[Tuple[int, ...], Sequence[int], int] = spatial_size + if pos < 0 or neg < 0: + raise ValueError(f"pos and neg must be nonnegative, got pos={pos} neg={neg}.") + if pos + neg == 0: + raise ValueError("Incompatible values: pos=0 and neg=0.") + self.pos_ratio = pos / (pos + neg) + self.num_samples = num_samples + self.image_key = image_key + self.image_threshold = image_threshold + self.fg_indices_key = fg_indices_key + self.bg_indices_key = bg_indices_key + self.meta_keys = ensure_tuple_rep(None, len(self.keys)) if meta_keys is None else ensure_tuple(meta_keys) + if len(self.keys) != len(self.meta_keys): + raise ValueError("meta_keys should have the same length as keys.") + self.meta_key_postfix = ensure_tuple_rep(meta_key_postfix, len(self.keys)) + self.centers: Optional[List[List[int]]] = None + self.allow_smaller = allow_smaller + + def randomize( + self, + label: NdarrayOrTensor, + fg_indices: Optional[NdarrayOrTensor] = None, + bg_indices: Optional[NdarrayOrTensor] = None, + image: Optional[NdarrayOrTensor] = None, + ) -> None: + if fg_indices is None or bg_indices is None: + fg_indices_, bg_indices_ = map_binary_to_indices(label, image, self.image_threshold) + else: + fg_indices_ = fg_indices + bg_indices_ = bg_indices + self.centers = generate_pos_neg_label_crop_centers( + self.spatial_size, + self.num_samples, + self.pos_ratio, + label.shape[1:], + fg_indices_, + bg_indices_, + self.R, + self.allow_smaller, + ) + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict[Hashable, NdarrayOrTensor]]: + d = dict(data) + label = d[self.label_key] + image = d[self.image_key] if self.image_key else None + fg_indices = d.pop(self.fg_indices_key, None) if self.fg_indices_key is not None else None + bg_indices = d.pop(self.bg_indices_key, None) if self.bg_indices_key is not None else None + + self.randomize(label, fg_indices, bg_indices, image) + if self.centers is None: + raise ValueError("no available ROI centers to crop.") + + # initialize returned list with shallow copy to preserve key ordering + results: List[Dict[Hashable, NdarrayOrTensor]] = [dict(d) for _ in range(self.num_samples)] + + for i, center in enumerate(self.centers): + # fill in the extra keys with unmodified data + for key in set(d.keys()).difference(set(self.keys)): + results[i][key] = deepcopy(d[key]) + for key in self.key_iterator(d): + img = d[key] + orig_size = img.shape[1:] + roi_size = fall_back_tuple(self.spatial_size, default=orig_size) + cropper = SpatialCrop(roi_center=tuple(center), roi_size=roi_size) + results[i][key] = cropper(img) + self.push_transform(results[i], key, extra_info={"center": center}, orig_size=orig_size) + # add `patch_index` to the metadata + for key, meta_key, meta_key_postfix in self.key_iterator(d, self.meta_keys, self.meta_key_postfix): + meta_key = meta_key or f"{key}_{meta_key_postfix}" + if meta_key not in results[i]: + results[i][meta_key] = {} # type: ignore + results[i][meta_key][Key.PATCH_INDEX] = i # type: ignore + + return results + + def inverse(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = deepcopy(dict(data)) + for key in self.key_iterator(d): + transform = self.get_most_recent_transform(d, key) + # Create inverse transform + orig_size = np.asarray(transform[TraceKeys.ORIG_SIZE]) + current_size = np.asarray(d[key].shape[1:]) + center = transform[TraceKeys.EXTRA_INFO]["center"] + roi_size = fall_back_tuple(self.spatial_size, default=orig_size) + cropper = SpatialCrop(roi_center=tuple(center), roi_size=roi_size) # type: ignore + # get required pad to start and end + pad_to_start = np.array([s.indices(o)[0] for s, o in zip(cropper.slices, orig_size)]) + pad_to_end = orig_size - current_size - pad_to_start + # interleave mins and maxes + pad = list(chain(*zip(pad_to_start.tolist(), pad_to_end.tolist()))) + inverse_transform = BorderPad(pad) + # Apply inverse transform + d[key] = inverse_transform(d[key]) + # Remove the applied transform + self.pop_transform(d, key) + + return d + + +class RandCropByLabelClassesd(Randomizable, MapTransform, InvertibleTransform): + """ + Dictionary-based version :py:class:`monai.transforms.RandCropByLabelClasses`. + Crop random fixed sized regions with the center being a class based on the specified ratios of every class. + The label data can be One-Hot format array or Argmax data. And will return a list of arrays for all the + cropped images. For example, crop two (3 x 3) arrays from (5 x 5) array with `ratios=[1, 2, 3, 1]`:: + + cropper = RandCropByLabelClassesd( + keys=["image", "label"], + label_key="label", + spatial_size=[3, 3], + ratios=[1, 2, 3, 1], + num_classes=4, + num_samples=2, + ) + data = { + "image": np.array([ + [[0.0, 0.3, 0.4, 0.2, 0.0], + [0.0, 0.1, 0.2, 0.1, 0.4], + [0.0, 0.3, 0.5, 0.2, 0.0], + [0.1, 0.2, 0.1, 0.1, 0.0], + [0.0, 0.1, 0.2, 0.1, 0.0]] + ]), + "label": np.array([ + [[0, 0, 0, 0, 0], + [0, 1, 2, 1, 0], + [0, 1, 3, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]] + ]), + } + result = cropper(data) + + The 2 randomly cropped samples of `label` can be: + [[0, 1, 2], [[0, 0, 0], + [0, 1, 3], [1, 2, 1], + [0, 0, 0]] [1, 3, 0]] + + If a dimension of the expected spatial size is bigger than the input image size, + will not crop that dimension. So the cropped result may be smaller than expected size, and the cropped + results of several images may not have exactly same shape. + And if the crop ROI is partly out of the image, will automatically adjust the crop center to ensure the + valid crop ROI. + + Args: + keys: keys of the corresponding items to be transformed. + See also: :py:class:`monai.transforms.compose.MapTransform` + label_key: name of key for label image, this will be used for finding indices of every class. + spatial_size: the spatial size of the crop region e.g. [224, 224, 128]. + if a dimension of ROI size is bigger than image size, will not crop that dimension of the image. + if its components have non-positive values, the corresponding size of `label` will be used. + for example: if the spatial size of input data is [40, 40, 40] and `spatial_size=[32, 64, -1]`, + the spatial size of output data will be [32, 40, 40]. + ratios: specified ratios of every class in the label to generate crop centers, including background class. + if None, every class will have the same ratio to generate crop centers. + num_classes: number of classes for argmax label, not necessary for One-Hot label. + num_samples: number of samples (crop regions) to take in each list. + image_key: if image_key is not None, only return the indices of every class that are within the valid + region of the image (``image > image_threshold``). + image_threshold: if enabled `image_key`, use ``image > image_threshold`` to + determine the valid image content area and select class indices only in this area. + indices_key: if provided pre-computed indices of every class, will ignore above `image` and + `image_threshold`, and randomly select crop centers based on them, expect to be 1 dim array + of spatial indices after flattening. a typical usage is to call `ClassesToIndices` transform first + and cache the results for better performance. + meta_keys: explicitly indicate the key of the corresponding metadata dictionary. + used to add `patch_index` to the meta dict. + for example, for data with key `image`, the metadata by default is in `image_meta_dict`. + the metadata is a dictionary object which contains: filename, original_shape, etc. + it can be a sequence of string, map to the `keys`. + if None, will try to construct meta_keys by `key_{meta_key_postfix}`. + meta_key_postfix: if meta_keys is None, use `key_{postfix}` to fetch the metadata according + to the key data, default is `meta_dict`, the metadata is a dictionary object. + used to add `patch_index` to the meta dict. + allow_smaller: if `False`, an exception will be raised if the image is smaller than + the requested ROI in any dimension. If `True`, any smaller dimensions will remain + unchanged. + allow_missing_keys: don't raise exception if key is missing. + + """ + + backend = RandCropByLabelClasses.backend + + def __init__( + self, + keys: KeysCollection, + label_key: str, + spatial_size: Union[Sequence[int], int], + ratios: Optional[List[Union[float, int]]] = None, + num_classes: Optional[int] = None, + num_samples: int = 1, + image_key: Optional[str] = None, + image_threshold: float = 0.0, + indices_key: Optional[str] = None, + meta_keys: Optional[KeysCollection] = None, + meta_key_postfix: str = DEFAULT_POST_FIX, + allow_smaller: bool = False, + allow_missing_keys: bool = False, + ) -> None: + MapTransform.__init__(self, keys, allow_missing_keys) + self.label_key = label_key + self.spatial_size = spatial_size + self.ratios = ratios + self.num_classes = num_classes + self.num_samples = num_samples + self.image_key = image_key + self.image_threshold = image_threshold + self.indices_key = indices_key + self.meta_keys = ensure_tuple_rep(None, len(self.keys)) if meta_keys is None else ensure_tuple(meta_keys) + if len(self.keys) != len(self.meta_keys): + raise ValueError("meta_keys should have the same length as keys.") + self.meta_key_postfix = ensure_tuple_rep(meta_key_postfix, len(self.keys)) + self.centers: Optional[List[List[int]]] = None + self.allow_smaller = allow_smaller + + def randomize( + self, + label: NdarrayOrTensor, + indices: Optional[List[NdarrayOrTensor]] = None, + image: Optional[NdarrayOrTensor] = None, + ) -> None: + if indices is None: + indices_ = map_classes_to_indices(label, self.num_classes, image, self.image_threshold) + else: + indices_ = indices + self.centers = generate_label_classes_crop_centers( + self.spatial_size, self.num_samples, label.shape[1:], indices_, self.ratios, self.R, self.allow_smaller + ) + + def __call__(self, data: Mapping[Hashable, Any]) -> List[Dict[Hashable, NdarrayOrTensor]]: + d = dict(data) + label = d[self.label_key] + image = d[self.image_key] if self.image_key else None + indices = d.pop(self.indices_key, None) if self.indices_key is not None else None + + self.randomize(label, indices, image) + if self.centers is None: + raise ValueError("no available ROI centers to crop.") + + # initialize returned list with shallow copy to preserve key ordering + results: List[Dict[Hashable, NdarrayOrTensor]] = [dict(d) for _ in range(self.num_samples)] + + for i, center in enumerate(self.centers): + # fill in the extra keys with unmodified data + for key in set(d.keys()).difference(set(self.keys)): + results[i][key] = deepcopy(d[key]) + for key in self.key_iterator(d): + img = d[key] + orig_size = img.shape[1:] + roi_size = fall_back_tuple(self.spatial_size, default=orig_size) + cropper = SpatialCrop(roi_center=tuple(center), roi_size=roi_size) + results[i][key] = cropper(img) + self.push_transform(results[i], key, extra_info={"center": center}, orig_size=orig_size) + # add `patch_index` to the metadata + for key, meta_key, meta_key_postfix in self.key_iterator(d, self.meta_keys, self.meta_key_postfix): + meta_key = meta_key or f"{key}_{meta_key_postfix}" + if meta_key not in results[i]: + results[i][meta_key] = {} # type: ignore + results[i][meta_key][Key.PATCH_INDEX] = i # type: ignore + + return results + + def inverse(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = deepcopy(dict(data)) + for key in self.key_iterator(d): + transform = self.get_most_recent_transform(d, key) + # Create inverse transform + orig_size = np.asarray(transform[TraceKeys.ORIG_SIZE]) + current_size = np.asarray(d[key].shape[1:]) + center = transform[TraceKeys.EXTRA_INFO]["center"] + roi_size = fall_back_tuple(self.spatial_size, default=orig_size) + cropper = SpatialCrop(roi_center=tuple(center), roi_size=roi_size) # type: ignore + # get required pad to start and end + pad_to_start = np.array([s.indices(o)[0] for s, o in zip(cropper.slices, orig_size)]) + pad_to_end = orig_size - current_size - pad_to_start + # interleave mins and maxes + pad = list(chain(*zip(pad_to_start.tolist(), pad_to_end.tolist()))) + inverse_transform = BorderPad(pad) + # Apply inverse transform + d[key] = inverse_transform(d[key]) + # Remove the applied transform + self.pop_transform(d, key) + + return d + + +class ResizeWithPadOrCropd(MapTransform, InvertibleTransform): + """ + Dictionary-based wrapper of :py:class:`monai.transforms.ResizeWithPadOrCrop`. + + Args: + keys: keys of the corresponding items to be transformed. + See also: monai.transforms.MapTransform + spatial_size: the spatial size of output data after padding or crop. + If has non-positive values, the corresponding size of input image will be used (no padding). + mode: available modes for numpy array:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, + ``"mean"``, ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + available modes for PyTorch Tensor: {``"constant"``, ``"reflect"``, ``"replicate"``, ``"circular"``}. + One of the listed string values or a user supplied function. Defaults to ``"constant"``. + See also: https://numpy.org/doc/1.18/reference/generated/numpy.pad.html + https://pytorch.org/docs/stable/generated/torch.nn.functional.pad.html + It also can be a sequence of string, each element corresponds to a key in ``keys``. + allow_missing_keys: don't raise exception if key is missing. + method: {``"symmetric"``, ``"end"``} + Pad image symmetrically on every side or only pad at the end sides. Defaults to ``"symmetric"``. + pad_kwargs: other arguments for the `np.pad` or `torch.pad` function. + note that `np.pad` treats channel dimension as the first dimension. + + """ + + backend = ResizeWithPadOrCrop.backend + + def __init__( + self, + keys: KeysCollection, + spatial_size: Union[Sequence[int], int], + mode: PadModeSequence = NumpyPadMode.CONSTANT, + allow_missing_keys: bool = False, + method: Union[Method, str] = Method.SYMMETRIC, + **pad_kwargs, + ) -> None: + super().__init__(keys, allow_missing_keys) + self.mode = ensure_tuple_rep(mode, len(self.keys)) + self.padcropper = ResizeWithPadOrCrop(spatial_size=spatial_size, method=method, **pad_kwargs) + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = dict(data) + for key, m in self.key_iterator(d, self.mode): + orig_size = d[key].shape[1:] + d[key] = self.padcropper(d[key], mode=m) + self.push_transform(d, key, orig_size=orig_size, extra_info={"mode": m.value if isinstance(m, Enum) else m}) + return d + + def inverse(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = deepcopy(dict(data)) + for key in self.key_iterator(d): + transform = self.get_most_recent_transform(d, key) + # Create inverse transform + orig_size = np.array(transform[TraceKeys.ORIG_SIZE]) + current_size = np.array(d[key].shape[1:]) + # Unfortunately, we can't just use ResizeWithPadOrCrop with original size because of odd/even rounding. + # Instead, we first pad any smaller dimensions, and then we crop any larger dimensions. + + # First, do pad + if np.any((orig_size - current_size) > 0): + pad_to_start = np.floor((orig_size - current_size) / 2).astype(int) + # in each direction, if original size is even and current size is odd, += 1 + pad_to_start[np.logical_and(orig_size % 2 == 0, current_size % 2 == 1)] += 1 + pad_to_start[pad_to_start < 0] = 0 + pad_to_end = orig_size - current_size - pad_to_start + pad_to_end[pad_to_end < 0] = 0 + pad = list(chain(*zip(pad_to_start.tolist(), pad_to_end.tolist()))) + d[key] = BorderPad(pad)(d[key]) + + # Next crop + if np.any((orig_size - current_size) < 0): + if self.padcropper.padder.method == Method.SYMMETRIC: + roi_center = [floor(i / 2) if r % 2 == 0 else (i - 1) // 2 for r, i in zip(orig_size, current_size)] + else: + roi_center = [floor(r / 2) if r % 2 == 0 else (r - 1) // 2 for r in orig_size] + + d[key] = SpatialCrop(roi_center, orig_size)(d[key]) + + # Remove the applied transform + self.pop_transform(d, key) + + return d + + +class BoundingRectd(MapTransform): + """ + Dictionary-based wrapper of :py:class:`monai.transforms.BoundingRect`. + + Args: + keys: keys of the corresponding items to be transformed. + See also: monai.transforms.MapTransform + bbox_key_postfix: the output bounding box coordinates will be + written to the value of `{key}_{bbox_key_postfix}`. + select_fn: function to select expected foreground, default is to select values > 0. + allow_missing_keys: don't raise exception if key is missing. + """ + + backend = BoundingRect.backend + + def __init__( + self, + keys: KeysCollection, + bbox_key_postfix: str = "bbox", + select_fn: Callable = is_positive, + allow_missing_keys: bool = False, + ): + super().__init__(keys, allow_missing_keys) + self.bbox = BoundingRect(select_fn=select_fn) + self.bbox_key_postfix = bbox_key_postfix + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + """ + See also: :py:class:`monai.transforms.utils.generate_spatial_bounding_box`. + """ + d = dict(data) + for key in self.key_iterator(d): + bbox = self.bbox(d[key]) + key_to_add = f"{key}_{self.bbox_key_postfix}" + if key_to_add in d: + raise KeyError(f"Bounding box data with key {key_to_add} already exists.") + d[key_to_add] = bbox + return d + + +SpatialPadD = SpatialPadDict = SpatialPadd +BorderPadD = BorderPadDict = BorderPadd +DivisiblePadD = DivisiblePadDict = DivisiblePadd +SpatialCropD = SpatialCropDict = SpatialCropd +CenterSpatialCropD = CenterSpatialCropDict = CenterSpatialCropd +CenterScaleCropD = CenterScaleCropDict = CenterScaleCropd +RandSpatialCropD = RandSpatialCropDict = RandSpatialCropd +RandScaleCropD = RandScaleCropDict = RandScaleCropd +RandSpatialCropSamplesD = RandSpatialCropSamplesDict = RandSpatialCropSamplesd +CropForegroundD = CropForegroundDict = CropForegroundd +RandWeightedCropD = RandWeightedCropDict = RandWeightedCropd +RandCropByPosNegLabelD = RandCropByPosNegLabelDict = RandCropByPosNegLabeld +RandCropByLabelClassesD = RandCropByLabelClassesDict = RandCropByLabelClassesd +ResizeWithPadOrCropD = ResizeWithPadOrCropDict = ResizeWithPadOrCropd +BoundingRectD = BoundingRectDict = BoundingRectd diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/io/__pycache__/__init__.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/io/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e130e3bd4b07235d49e47d1ca5ee676e27c85e4a Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/io/__pycache__/__init__.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/io/__pycache__/array.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/io/__pycache__/array.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bb642fa431650163952d471c2c79f772af286fb3 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/io/__pycache__/array.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/io/__pycache__/dictionary.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/io/__pycache__/dictionary.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d3565a7a011479e146923caf3f928d5d6c9cf4b5 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/io/__pycache__/dictionary.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/io/dictionary.py b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/io/dictionary.py new file mode 100644 index 0000000000000000000000000000000000000000..46dcda469e96e18a538f0d703a8e8b9a93561246 --- /dev/null +++ b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/io/dictionary.py @@ -0,0 +1,277 @@ +# 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 around the "vanilla" transforms for IO functions +defined in :py:class:`monai.transforms.io.array`. + +Class names are ended with 'd' to denote dictionary-based transforms. +""" + +from pathlib import Path +from typing import Optional, Union + +import numpy as np + +from monai.config import DtypeLike, KeysCollection +from monai.data import image_writer +from monai.data.image_reader import ImageReader +from monai.transforms.io.array import LoadImage, SaveImage +from monai.transforms.transform import MapTransform +from monai.utils import GridSampleMode, GridSamplePadMode, InterpolateMode, ensure_tuple, ensure_tuple_rep +from monai.utils.enums import PostFix + +__all__ = ["LoadImaged", "LoadImageD", "LoadImageDict", "SaveImaged", "SaveImageD", "SaveImageDict"] + +DEFAULT_POST_FIX = PostFix.meta() + + +class LoadImaged(MapTransform): + """ + Dictionary-based wrapper of :py:class:`monai.transforms.LoadImage`, + It can load both image data and metadata. When loading a list of files in one key, + the arrays will be stacked and a new dimension will be added as the first dimension + In this case, the metadata of the first image will be used to represent the stacked result. + The affine transform of all the stacked images should be same. + The output metadata field will be created as ``meta_keys`` or ``key_{meta_key_postfix}``. + + If reader is not specified, this class automatically chooses readers + based on the supported suffixes and in the following order: + + - User-specified reader at runtime when calling this loader. + - User-specified reader in the constructor of `LoadImage`. + - Readers from the last to the first in the registered list. + - Current default readers: (nii, nii.gz -> NibabelReader), (png, jpg, bmp -> PILReader), + (npz, npy -> NumpyReader), (dcm, DICOM series and others -> ITKReader). + + Note: + + - If `reader` is specified, the loader will attempt to use the specified readers and the default supported + readers. This might introduce overheads when handling the exceptions of trying the incompatible loaders. + In this case, it is therefore recommended setting the most appropriate reader as + the last item of the `reader` parameter. + + See also: + + - tutorial: https://github.com/Project-MONAI/tutorials/blob/master/modules/load_medical_images.ipynb + + """ + + def __init__( + self, + keys: KeysCollection, + reader: Optional[Union[ImageReader, str]] = None, + dtype: DtypeLike = np.float32, + meta_keys: Optional[KeysCollection] = None, + meta_key_postfix: str = DEFAULT_POST_FIX, + overwriting: bool = False, + image_only: bool = False, + ensure_channel_first: bool = False, + allow_missing_keys: bool = False, + *args, + **kwargs, + ) -> None: + """ + Args: + keys: keys of the corresponding items to be transformed. + See also: :py:class:`monai.transforms.compose.MapTransform` + reader: reader to load image file and metadata + - if `reader` is None, a default set of `SUPPORTED_READERS` will be used. + - if `reader` is a string, it's treated as a class name or dotted path + (such as ``"monai.data.ITKReader"``), the supported built-in reader classes are + ``"ITKReader"``, ``"NibabelReader"``, ``"NumpyReader"``. + a reader instance will be constructed with the `*args` and `**kwargs` parameters. + - if `reader` is a reader class/instance, it will be registered to this loader accordingly. + dtype: if not None, convert the loaded image data to this data type. + meta_keys: explicitly indicate the key to store the corresponding metadata dictionary. + the metadata is a dictionary object which contains: filename, original_shape, etc. + it can be a sequence of string, map to the `keys`. + if None, will try to construct meta_keys by `key_{meta_key_postfix}`. + meta_key_postfix: if meta_keys is None, use `key_{postfix}` to store the metadata of the nifti image, + default is `meta_dict`. The metadata is a dictionary object. + For example, load nifti file for `image`, store the metadata into `image_meta_dict`. + overwriting: whether allow overwriting existing metadata of same key. + default is False, which will raise exception if encountering existing key. + image_only: if True return dictionary containing just only the image volumes, otherwise return + dictionary containing image data array and header dict per input key. + ensure_channel_first: if `True` and loaded both image array and metadata, automatically convert + the image array shape to `channel first`. default to `False`. + allow_missing_keys: don't raise exception if key is missing. + args: additional parameters for reader if providing a reader name. + kwargs: additional parameters for reader if providing a reader name. + """ + super().__init__(keys, allow_missing_keys) + self._loader = LoadImage(reader, image_only, dtype, ensure_channel_first, *args, **kwargs) + if not isinstance(meta_key_postfix, str): + raise TypeError(f"meta_key_postfix must be a str but is {type(meta_key_postfix).__name__}.") + self.meta_keys = ensure_tuple_rep(None, len(self.keys)) if meta_keys is None else ensure_tuple(meta_keys) + if len(self.keys) != len(self.meta_keys): + raise ValueError("meta_keys should have the same length as keys.") + self.meta_key_postfix = ensure_tuple_rep(meta_key_postfix, len(self.keys)) + self.overwriting = overwriting + + def register(self, reader: ImageReader): + self._loader.register(reader) + + def __call__(self, data, reader: Optional[ImageReader] = None): + """ + Raises: + KeyError: When not ``self.overwriting`` and key already exists in ``data``. + + """ + d = dict(data) + for key, meta_key, meta_key_postfix in self.key_iterator(d, self.meta_keys, self.meta_key_postfix): + data = self._loader(d[key], reader) + if self._loader.image_only: + if not isinstance(data, np.ndarray): + raise ValueError("loader must return a numpy array (because image_only=True was used).") + d[key] = data + else: + if not isinstance(data, (tuple, list)): + raise ValueError("loader must return a tuple or list (because image_only=False was used).") + d[key] = data[0] + if not isinstance(data[1], dict): + raise ValueError("metadata must be a dict.") + meta_key = meta_key or f"{key}_{meta_key_postfix}" + if meta_key in d and not self.overwriting: + raise KeyError(f"Metadata with key {meta_key} already exists and overwriting=False.") + d[meta_key] = data[1] + return d + + +class SaveImaged(MapTransform): + """ + Dictionary-based wrapper of :py:class:`monai.transforms.SaveImage`. + + Note: + Image should be channel-first shape: [C,H,W,[D]]. + If the data is a patch of big image, will append the patch index to filename. + + Args: + keys: keys of the corresponding items to be transformed. + See also: :py:class:`monai.transforms.compose.MapTransform` + meta_keys: explicitly indicate the key of the corresponding metadata dictionary. + For example, for data with key `image`, the metadata by default is in `image_meta_dict`. + The metadata is a dictionary contains values such as filename, original_shape. + This argument can be a sequence of string, map to the `keys`. + If `None`, will try to construct meta_keys by `key_{meta_key_postfix}`. + meta_key_postfix: if `meta_keys` is `None`, use `key_{meta_key_postfix}` to retrieve the metadict. + output_dir: output image directory. + output_postfix: a string appended to all output file names, default to `trans`. + output_ext: output file extension name, available extensions: `.nii.gz`, `.nii`, `.png`. + output_dtype: data type for saving data. Defaults to ``np.float32``. + resample: whether to resample image (if needed) before saving the data array, + based on the `spatial_shape` (and `original_affine`) from metadata. + mode: This option is used when ``resample=True``. Defaults to ``"nearest"``. + Depending on the writers, the possible options are: + + - {``"bilinear"``, ``"nearest"``, ``"bicubic"``}. + See also: https://pytorch.org/docs/stable/nn.functional.html#grid-sample + - {``"nearest"``, ``"linear"``, ``"bilinear"``, ``"bicubic"``, ``"trilinear"``, ``"area"``}. + See also: https://pytorch.org/docs/stable/nn.functional.html#interpolate + + padding_mode: This option is used when ``resample = True``. Defaults to ``"border"``. + Possible options are {``"zeros"``, ``"border"``, ``"reflection"``} + See also: https://pytorch.org/docs/stable/nn.functional.html#grid-sample + scale: {``255``, ``65535``} postprocess data by clipping to [0, 1] and scaling + [0, 255] (uint8) or [0, 65535] (uint16). Default is `None` (no scaling). + dtype: data type during resampling computation. Defaults to ``np.float64`` for best precision. + if None, use the data type of input data. To be compatible with other modules, + output_dtype: data type for saving data. Defaults to ``np.float32``. + it's used for NIfTI format only. + allow_missing_keys: don't raise exception if key is missing. + squeeze_end_dims: if True, any trailing singleton dimensions will be removed (after the channel + has been moved to the end). So if input is (C,H,W,D), this will be altered to (H,W,D,C), and + then if C==1, it will be saved as (H,W,D). If D is also 1, it will be saved as (H,W). If `false`, + image will always be saved as (H,W,D,C). + data_root_dir: if not empty, it specifies the beginning parts of the input file's + absolute path. It's used to compute `input_file_rel_path`, the relative path to the file from + `data_root_dir` to preserve folder structure when saving in case there are files in different + folders with the same file names. For example, with the following inputs: + + - input_file_name: `/foo/bar/test1/image.nii` + - output_postfix: `seg` + - output_ext: `.nii.gz` + - output_dir: `/output` + - data_root_dir: `/foo/bar` + + The output will be: /output/test1/image/image_seg.nii.gz + + separate_folder: whether to save every file in a separate folder. For example: for the input filename + `image.nii`, postfix `seg` and folder_path `output`, if `separate_folder=True`, it will be saved as: + `output/image/image_seg.nii`, if `False`, saving as `output/image_seg.nii`. Default to `True`. + print_log: whether to print logs when saving. Default to `True`. + output_format: an optional string to specify the output image writer. + see also: `monai.data.image_writer.SUPPORTED_WRITERS`. + writer: a customised `monai.data.ImageWriter` subclass to save data arrays. + if `None`, use the default writer from `monai.data.image_writer` according to `output_ext`. + if it's a string, it's treated as a class name or dotted path; + the supported built-in writer classes are ``"NibabelWriter"``, ``"ITKWriter"``, ``"PILWriter"``. + + """ + + def __init__( + self, + keys: KeysCollection, + meta_keys: Optional[KeysCollection] = None, + meta_key_postfix: str = DEFAULT_POST_FIX, + output_dir: Union[Path, str] = "./", + output_postfix: str = "trans", + output_ext: str = ".nii.gz", + resample: bool = True, + mode: Union[GridSampleMode, InterpolateMode, str] = "nearest", + padding_mode: Union[GridSamplePadMode, str] = GridSamplePadMode.BORDER, + scale: Optional[int] = None, + dtype: DtypeLike = np.float64, + output_dtype: DtypeLike = np.float32, + allow_missing_keys: bool = False, + squeeze_end_dims: bool = True, + data_root_dir: str = "", + separate_folder: bool = True, + print_log: bool = True, + output_format: str = "", + writer: Union[image_writer.ImageWriter, str, None] = None, + ) -> None: + super().__init__(keys, allow_missing_keys) + self.meta_keys = ensure_tuple_rep(meta_keys, len(self.keys)) + self.meta_key_postfix = ensure_tuple_rep(meta_key_postfix, len(self.keys)) + self.saver = SaveImage( + output_dir=output_dir, + output_postfix=output_postfix, + output_ext=output_ext, + resample=resample, + mode=mode, + padding_mode=padding_mode, + scale=scale, + dtype=dtype, + output_dtype=output_dtype, + squeeze_end_dims=squeeze_end_dims, + data_root_dir=data_root_dir, + separate_folder=separate_folder, + print_log=print_log, + output_format=output_format, + writer=writer, + ) + + def set_options(self, init_kwargs=None, data_kwargs=None, meta_kwargs=None, write_kwargs=None): + self.saver.set_options(init_kwargs, data_kwargs, meta_kwargs, write_kwargs) + + def __call__(self, data): + d = dict(data) + for key, meta_key, meta_key_postfix in self.key_iterator(d, self.meta_keys, self.meta_key_postfix): + if meta_key is None and meta_key_postfix is not None: + meta_key = f"{key}_{meta_key_postfix}" + meta_data = d[meta_key] if meta_key is not None else None + self.saver(img=d[key], meta_data=meta_data) + return d + + +LoadImageD = LoadImageDict = LoadImaged +SaveImageD = SaveImageDict = SaveImaged diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/post/__pycache__/__init__.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/post/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..63861e6c08884d9dc0ae0c4d3ef57057c6fb7ec8 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/post/__pycache__/__init__.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/post/__pycache__/array.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/post/__pycache__/array.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e0444b0621048151e8065b74111393e8bf74dc35 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/post/__pycache__/array.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/post/__pycache__/dictionary.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/post/__pycache__/dictionary.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..034643feae410cd1f5583262293c97a83f701932 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/post/__pycache__/dictionary.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/smooth_field/__init__.py b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/smooth_field/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1e97f8940782e96a77c1c08483fc41da9a48ae22 --- /dev/null +++ b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/smooth_field/__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/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/smooth_field/__pycache__/__init__.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/smooth_field/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e528409adcd4e8f9f71d035b3c87c8bc0d4684d Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/smooth_field/__pycache__/__init__.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/smooth_field/__pycache__/array.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/smooth_field/__pycache__/array.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c56d649f81a81a3a8a0ec33cd558c6a59ec5839 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/smooth_field/__pycache__/array.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/smooth_field/__pycache__/dictionary.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/smooth_field/__pycache__/dictionary.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..439e131749daeb4991caa93755e3b4b727d1dfca Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/smooth_field/__pycache__/dictionary.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/smooth_field/array.py b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/smooth_field/array.py new file mode 100644 index 0000000000000000000000000000000000000000..f581687ea5f4b5eb308100ecd9af1ebe7a87352f --- /dev/null +++ b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/smooth_field/array.py @@ -0,0 +1,459 @@ +# 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. + +"""Transforms using a smooth spatial field generated by interpolating from smaller randomized fields.""" + +from typing import Any, Optional, Sequence, Union + +import numpy as np +import torch +from torch.nn.functional import grid_sample, interpolate + +import monai +from monai.config.type_definitions import NdarrayOrTensor +from monai.networks.utils import meshgrid_ij +from monai.transforms.transform import Randomizable, RandomizableTransform +from monai.transforms.utils_pytorch_numpy_unification import moveaxis +from monai.utils import GridSampleMode, GridSamplePadMode, InterpolateMode +from monai.utils.enums import TransformBackends +from monai.utils.module import look_up_option +from monai.utils.type_conversion import convert_to_dst_type, convert_to_tensor + +__all__ = ["SmoothField", "RandSmoothFieldAdjustContrast", "RandSmoothFieldAdjustIntensity", "RandSmoothDeform"] + + +class SmoothField(Randomizable): + """ + Generate a smooth field array by defining a smaller randomized field and then reinterpolating to the desired size. + + This exploits interpolation to create a smoothly varying field used for other applications. An initial randomized + field is defined with `rand_size` dimensions with `pad` number of values padding it along each dimension using + `pad_val` as the value. If `spatial_size` is given this is interpolated to that size, otherwise if None the random + array is produced uninterpolated. The output is always a Pytorch tensor allocated on the specified device. + + Args: + rand_size: size of the randomized field to start from + pad: number of pixels/voxels along the edges of the field to pad with `pad_val` + pad_val: value with which to pad field edges + low: low value for randomized field + high: high value for randomized field + channels: number of channels of final output + spatial_size: final output size of the array, None to produce original uninterpolated field + mode: interpolation mode for resizing the field + align_corners: if True align the corners when upsampling field + device: Pytorch device to define field on + """ + + def __init__( + self, + rand_size: Sequence[int], + pad: int = 0, + pad_val: float = 0, + low: float = -1.0, + high: float = 1.0, + channels: int = 1, + spatial_size: Optional[Sequence[int]] = None, + mode: Union[InterpolateMode, str] = InterpolateMode.AREA, + align_corners: Optional[bool] = None, + device: Optional[torch.device] = None, + ): + self.rand_size = tuple(rand_size) + self.pad = pad + self.low = low + self.high = high + self.channels = channels + self.mode = mode + self.align_corners = align_corners + self.device = device + + self.spatial_size: Optional[Sequence[int]] = None + self.spatial_zoom: Optional[Sequence[float]] = None + + if low >= high: + raise ValueError("Value for `low` must be less than `high` otherwise field will be zeros") + + self.total_rand_size = tuple(rs + self.pad * 2 for rs in self.rand_size) + + self.field = torch.ones((1, self.channels) + self.total_rand_size, device=self.device) * pad_val + + self.crand_size = (self.channels,) + self.rand_size + + pad_slice = slice(None) if self.pad == 0 else slice(self.pad, -self.pad) + self.rand_slices = (0, slice(None)) + (pad_slice,) * len(self.rand_size) + + self.set_spatial_size(spatial_size) + + def randomize(self, data: Optional[Any] = None) -> None: + self.field[self.rand_slices] = torch.from_numpy(self.R.uniform(self.low, self.high, self.crand_size)) + + def set_spatial_size(self, spatial_size: Optional[Sequence[int]]) -> None: + """ + Set the `spatial_size` and `spatial_zoom` attributes used for interpolating the field to the given + dimension, or not interpolate at all if None. + + Args: + spatial_size: new size to interpolate to, or None to not interpolate + """ + if spatial_size is None: + self.spatial_size = None + self.spatial_zoom = None + else: + self.spatial_size = tuple(spatial_size) + self.spatial_zoom = tuple(s / f for s, f in zip(self.spatial_size, self.total_rand_size)) + + def set_mode(self, mode: Union[monai.utils.InterpolateMode, str]) -> None: + self.mode = mode + + def __call__(self, randomize=False) -> torch.Tensor: + if randomize: + self.randomize() + + field = self.field.clone() + + if self.spatial_zoom is not None: + resized_field = interpolate( # type: ignore + input=field, + scale_factor=self.spatial_zoom, + mode=look_up_option(self.mode, InterpolateMode).value, + align_corners=self.align_corners, + recompute_scale_factor=False, + ) + + mina = resized_field.min() + maxa = resized_field.max() + minv = self.field.min() + maxv = self.field.max() + + # faster than rescale_array, this uses in-place operations and doesn't perform unneeded range checks + norm_field = (resized_field.squeeze(0) - mina).div_(maxa - mina) + field = norm_field.mul_(maxv - minv).add_(minv) + + return field + + +class RandSmoothFieldAdjustContrast(RandomizableTransform): + """ + Randomly adjust the contrast of input images by calculating a randomized smooth field for each invocation. + + This uses SmoothField internally to define the adjustment over the image. If `pad` is greater than 0 the + edges of the input volume of that width will be mostly unchanged. Contrast is changed by raising input + values by the power of the smooth field so the range of values given by `gamma` should be chosen with this + in mind. For example, a minimum value of 0 in `gamma` will produce white areas so this should be avoided. + Afte the contrast is adjusted the values of the result are rescaled to the range of the original input. + + Args: + spatial_size: size of input array's spatial dimensions + rand_size: size of the randomized field to start from + pad: number of pixels/voxels along the edges of the field to pad with 1 + mode: interpolation mode to use when upsampling + align_corners: if True align the corners when upsampling field + prob: probability transform is applied + gamma: (min, max) range for exponential field + device: Pytorch device to define field on + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + def __init__( + self, + spatial_size: Sequence[int], + rand_size: Sequence[int], + pad: int = 0, + mode: Union[InterpolateMode, str] = InterpolateMode.AREA, + align_corners: Optional[bool] = None, + prob: float = 0.1, + gamma: Union[Sequence[float], float] = (0.5, 4.5), + device: Optional[torch.device] = None, + ): + super().__init__(prob) + + if isinstance(gamma, (int, float)): + self.gamma = (0.5, gamma) + else: + if len(gamma) != 2: + raise ValueError("Argument `gamma` should be a number or pair of numbers.") + + self.gamma = (min(gamma), max(gamma)) + + self.sfield = SmoothField( + rand_size=rand_size, + pad=pad, + pad_val=1, + low=self.gamma[0], + high=self.gamma[1], + channels=1, + spatial_size=spatial_size, + mode=mode, + align_corners=align_corners, + device=device, + ) + + def set_random_state( + self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None + ) -> "RandSmoothFieldAdjustContrast": + super().set_random_state(seed, state) + self.sfield.set_random_state(seed, state) + return self + + def randomize(self, data: Optional[Any] = None) -> None: + super().randomize(None) + + if self._do_transform: + self.sfield.randomize() + + def set_mode(self, mode: Union[monai.utils.InterpolateMode, str]) -> None: + self.sfield.set_mode(mode) + + def __call__(self, img: NdarrayOrTensor, randomize: bool = True) -> NdarrayOrTensor: + """ + Apply the transform to `img`, if `randomize` randomizing the smooth field otherwise reusing the previous. + """ + if randomize: + self.randomize() + + if not self._do_transform: + return img + + img_min = img.min() + img_max = img.max() + img_rng = img_max - img_min + + field = self.sfield() + rfield, *_ = convert_to_dst_type(field, img) + + # everything below here is to be computed using the destination type (numpy, tensor, etc.) + + img = (img - img_min) / (img_rng + 1e-10) # rescale to unit values + img = img**rfield # contrast is changed by raising image data to a power, in this case the field + + out = (img * img_rng) + img_min # rescale back to the original image value range + + return out + + +class RandSmoothFieldAdjustIntensity(RandomizableTransform): + """ + Randomly adjust the intensity of input images by calculating a randomized smooth field for each invocation. + + This uses SmoothField internally to define the adjustment over the image. If `pad` is greater than 0 the + edges of the input volume of that width will be mostly unchanged. Intensity is changed by multiplying the + inputs by the smooth field, so the values of `gamma` should be chosen with this in mind. The default values + of `(0.1, 1.0)` are sensible in that values will not be zeroed out by the field nor multiplied greater than + the original value range. + + Args: + spatial_size: size of input array + rand_size: size of the randomized field to start from + pad: number of pixels/voxels along the edges of the field to pad with 1 + mode: interpolation mode to use when upsampling + align_corners: if True align the corners when upsampling field + prob: probability transform is applied + gamma: (min, max) range of intensity multipliers + device: Pytorch device to define field on + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + def __init__( + self, + spatial_size: Sequence[int], + rand_size: Sequence[int], + pad: int = 0, + mode: Union[InterpolateMode, str] = InterpolateMode.AREA, + align_corners: Optional[bool] = None, + prob: float = 0.1, + gamma: Union[Sequence[float], float] = (0.1, 1.0), + device: Optional[torch.device] = None, + ): + super().__init__(prob) + + if isinstance(gamma, (int, float)): + self.gamma = (0.5, gamma) + else: + if len(gamma) != 2: + raise ValueError("Argument `gamma` should be a number or pair of numbers.") + + self.gamma = (min(gamma), max(gamma)) + + self.sfield = SmoothField( + rand_size=rand_size, + pad=pad, + pad_val=1, + low=self.gamma[0], + high=self.gamma[1], + channels=1, + spatial_size=spatial_size, + mode=mode, + align_corners=align_corners, + device=device, + ) + + def set_random_state( + self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None + ) -> "RandSmoothFieldAdjustIntensity": + super().set_random_state(seed, state) + self.sfield.set_random_state(seed, state) + return self + + def randomize(self, data: Optional[Any] = None) -> None: + super().randomize(None) + + if self._do_transform: + self.sfield.randomize() + + def set_mode(self, mode: Union[InterpolateMode, str]) -> None: + self.sfield.set_mode(mode) + + def __call__(self, img: NdarrayOrTensor, randomize: bool = True) -> NdarrayOrTensor: + """ + Apply the transform to `img`, if `randomize` randomizing the smooth field otherwise reusing the previous. + """ + + if randomize: + self.randomize() + + if not self._do_transform: + return img + + field = self.sfield() + rfield, *_ = convert_to_dst_type(field, img) + + # everything below here is to be computed using the destination type (numpy, tensor, etc.) + + out = img * rfield + + return out + + +class RandSmoothDeform(RandomizableTransform): + """ + Deform an image using a random smooth field and Pytorch's grid_sample. + + The amount of deformation is given by `def_range` in fractions of the size of the image. The size of each dimension + of the input image is always defined as 2 regardless of actual image voxel dimensions, that is the coordinates in + every dimension range from -1 to 1. A value of 0.1 means pixels/voxels can be moved by up to 5% of the image's size. + + Args: + spatial_size: input array size to which deformation grid is interpolated + rand_size: size of the randomized field to start from + pad: number of pixels/voxels along the edges of the field to pad with 0 + field_mode: interpolation mode to use when upsampling the deformation field + align_corners: if True align the corners when upsampling field + prob: probability transform is applied + def_range: value of the deformation range in image size fractions, single min/max value or min/max pair + grid_dtype: type for the deformation grid calculated from the field + grid_mode: interpolation mode used for sampling input using deformation grid + grid_padding_mode: padding mode used for sampling input using deformation grid + grid_align_corners: if True align the corners when sampling the deformation grid + device: Pytorch device to define field on + """ + + backend = [TransformBackends.TORCH] + + def __init__( + self, + spatial_size: Sequence[int], + rand_size: Sequence[int], + pad: int = 0, + field_mode: Union[InterpolateMode, str] = InterpolateMode.AREA, + align_corners: Optional[bool] = None, + prob: float = 0.1, + def_range: Union[Sequence[float], float] = 1.0, + grid_dtype=torch.float32, + grid_mode: Union[GridSampleMode, str] = GridSampleMode.NEAREST, + grid_padding_mode: Union[GridSamplePadMode, str] = GridSamplePadMode.BORDER, + grid_align_corners: Optional[bool] = False, + device: Optional[torch.device] = None, + ): + super().__init__(prob) + + self.grid_dtype = grid_dtype + self.grid_mode = grid_mode + self.def_range = def_range + self.device = device + self.grid_align_corners = grid_align_corners + self.grid_padding_mode = grid_padding_mode + + if isinstance(def_range, (int, float)): + self.def_range = (-def_range, def_range) + else: + if len(def_range) != 2: + raise ValueError("Argument `def_range` should be a number or pair of numbers.") + + self.def_range = (min(def_range), max(def_range)) + + self.sfield = SmoothField( + spatial_size=spatial_size, + rand_size=rand_size, + pad=pad, + low=self.def_range[0], + high=self.def_range[1], + channels=len(rand_size), + mode=field_mode, + align_corners=align_corners, + device=device, + ) + + grid_space = spatial_size if spatial_size is not None else self.sfield.field.shape[2:] + grid_ranges = [torch.linspace(-1, 1, d) for d in grid_space] + + grid = meshgrid_ij(*grid_ranges) + + self.grid = torch.stack(grid).unsqueeze(0).to(self.device, self.grid_dtype) + + def set_random_state( + self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None + ) -> "Randomizable": + super().set_random_state(seed, state) + self.sfield.set_random_state(seed, state) + return self + + def randomize(self, data: Optional[Any] = None) -> None: + super().randomize(None) + + if self._do_transform: + self.sfield.randomize() + + def set_field_mode(self, mode: Union[monai.utils.InterpolateMode, str]) -> None: + self.sfield.set_mode(mode) + + def set_grid_mode(self, mode: Union[monai.utils.GridSampleMode, str]) -> None: + self.grid_mode = mode + + def __call__( + self, img: NdarrayOrTensor, randomize: bool = True, device: Optional[torch.device] = None + ) -> NdarrayOrTensor: + if randomize: + self.randomize() + + if not self._do_transform: + return img + + device = device if device is not None else self.device + + field = self.sfield() + + dgrid = self.grid + field.to(self.grid_dtype) + dgrid = moveaxis(dgrid, 1, -1) # type: ignore + + img_t = convert_to_tensor(img[None], torch.float32, device) + + out = grid_sample( + input=img_t, + grid=dgrid, + mode=look_up_option(self.grid_mode, GridSampleMode).value, + align_corners=self.grid_align_corners, + padding_mode=look_up_option(self.grid_padding_mode, GridSamplePadMode).value, + ) + + out_t, *_ = convert_to_dst_type(out.squeeze(0), img) + + return out_t diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/smooth_field/dictionary.py b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/smooth_field/dictionary.py new file mode 100644 index 0000000000000000000000000000000000000000..24890140cc8e45b30aa33f30e690adc41986049a --- /dev/null +++ b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/smooth_field/dictionary.py @@ -0,0 +1,293 @@ +# 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 typing import Any, Hashable, Mapping, Optional, Sequence, Union + +import numpy as np +import torch + +from monai.config import KeysCollection +from monai.config.type_definitions import NdarrayOrTensor +from monai.transforms.smooth_field.array import ( + RandSmoothDeform, + RandSmoothFieldAdjustContrast, + RandSmoothFieldAdjustIntensity, +) +from monai.transforms.transform import MapTransform, RandomizableTransform +from monai.utils import GridSampleMode, GridSamplePadMode, InterpolateMode, ensure_tuple_rep +from monai.utils.enums import TransformBackends + +__all__ = [ + "RandSmoothFieldAdjustContrastd", + "RandSmoothFieldAdjustIntensityd", + "RandSmoothDeformd", + "RandSmoothFieldAdjustContrastD", + "RandSmoothFieldAdjustIntensityD", + "RandSmoothDeformD", + "RandSmoothFieldAdjustContrastDict", + "RandSmoothFieldAdjustIntensityDict", + "RandSmoothDeformDict", +] + + +InterpolateModeType = Union[InterpolateMode, str] +GridSampleModeType = Union[GridSampleMode, str] + + +class RandSmoothFieldAdjustContrastd(RandomizableTransform, MapTransform): + """ + Dictionary version of RandSmoothFieldAdjustContrast. + + The field is randomized once per invocation by default so the same field is applied to every selected key. The + `mode` parameter specifying interpolation mode for the field can be a single value or a sequence of values with + one for each key in `keys`. + + Args: + keys: key names to apply the augment to + spatial_size: size of input arrays, all arrays stated in `keys` must have same dimensions + rand_size: size of the randomized field to start from + pad: number of pixels/voxels along the edges of the field to pad with 0 + mode: interpolation mode to use when upsampling + align_corners: if True align the corners when upsampling field + prob: probability transform is applied + gamma: (min, max) range for exponential field + device: Pytorch device to define field on + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + def __init__( + self, + keys: KeysCollection, + spatial_size: Sequence[int], + rand_size: Sequence[int], + pad: int = 0, + mode: Union[InterpolateModeType, Sequence[InterpolateModeType]] = InterpolateMode.AREA, + align_corners: Optional[bool] = None, + prob: float = 0.1, + gamma: Union[Sequence[float], float] = (0.5, 4.5), + device: Optional[torch.device] = None, + ): + RandomizableTransform.__init__(self, prob) + MapTransform.__init__(self, keys) + + self.mode = ensure_tuple_rep(mode, len(self.keys)) + + self.trans = RandSmoothFieldAdjustContrast( + spatial_size=spatial_size, + rand_size=rand_size, + pad=pad, + mode=self.mode[0], + align_corners=align_corners, + prob=1.0, + gamma=gamma, + device=device, + ) + + def set_random_state( + self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None + ) -> "RandSmoothFieldAdjustContrastd": + super().set_random_state(seed, state) + self.trans.set_random_state(seed, state) + return self + + def randomize(self, data: Optional[Any] = None) -> None: + super().randomize(None) + + if self._do_transform: + self.trans.randomize() + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Mapping[Hashable, NdarrayOrTensor]: + self.randomize() + + if not self._do_transform: + return data + + d = dict(data) + + for idx, key in enumerate(self.key_iterator(d)): + self.trans.set_mode(self.mode[idx % len(self.mode)]) + d[key] = self.trans(d[key], False) + + return d + + +class RandSmoothFieldAdjustIntensityd(RandomizableTransform, MapTransform): + """ + Dictionary version of RandSmoothFieldAdjustIntensity. + + The field is randomized once per invocation by default so the same field is applied to every selected key. The + `mode` parameter specifying interpolation mode for the field can be a single value or a sequence of values with + one for each key in `keys`. + + Args: + keys: key names to apply the augment to + spatial_size: size of input arrays, all arrays stated in `keys` must have same dimensions + rand_size: size of the randomized field to start from + pad: number of pixels/voxels along the edges of the field to pad with 0 + mode: interpolation mode to use when upsampling + align_corners: if True align the corners when upsampling field + prob: probability transform is applied + gamma: (min, max) range of intensity multipliers + device: Pytorch device to define field on + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + def __init__( + self, + keys: KeysCollection, + spatial_size: Sequence[int], + rand_size: Sequence[int], + pad: int = 0, + mode: Union[InterpolateModeType, Sequence[InterpolateModeType]] = InterpolateMode.AREA, + align_corners: Optional[bool] = None, + prob: float = 0.1, + gamma: Union[Sequence[float], float] = (0.1, 1.0), + device: Optional[torch.device] = None, + ): + RandomizableTransform.__init__(self, prob) + MapTransform.__init__(self, keys) + + self.mode = ensure_tuple_rep(mode, len(self.keys)) + + self.trans = RandSmoothFieldAdjustIntensity( + spatial_size=spatial_size, + rand_size=rand_size, + pad=pad, + mode=self.mode[0], + align_corners=align_corners, + prob=1.0, + gamma=gamma, + device=device, + ) + + def set_random_state( + self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None + ) -> "RandSmoothFieldAdjustIntensityd": + super().set_random_state(seed, state) + self.trans.set_random_state(seed, state) + return self + + def randomize(self, data: Optional[Any] = None) -> None: + super().randomize(None) + self.trans.randomize() + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Mapping[Hashable, NdarrayOrTensor]: + self.randomize() + + if not self._do_transform: + return data + + d = dict(data) + + for idx, key in enumerate(self.key_iterator(d)): + self.trans.set_mode(self.mode[idx % len(self.mode)]) + d[key] = self.trans(d[key], False) + + return d + + +class RandSmoothDeformd(RandomizableTransform, MapTransform): + """ + Dictionary version of RandSmoothDeform. + + The field is randomized once per invocation by default so the same field is applied to every selected key. The + `field_mode` parameter specifying interpolation mode for the field can be a single value or a sequence of values + with one for each key in `keys`. Similarly the `grid_mode` parameter can be one value or one per key. + + Args: + keys: key names to apply the augment to + spatial_size: input array size to which deformation grid is interpolated + rand_size: size of the randomized field to start from + pad: number of pixels/voxels along the edges of the field to pad with 0 + field_mode: interpolation mode to use when upsampling the deformation field + align_corners: if True align the corners when upsampling field + prob: probability transform is applied + def_range: value of the deformation range in image size fractions + grid_dtype: type for the deformation grid calculated from the field + grid_mode: interpolation mode used for sampling input using deformation grid + grid_padding_mode: padding mode used for sampling input using deformation grid + grid_align_corners: if True align the corners when sampling the deformation grid + device: Pytorch device to define field on + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + def __init__( + self, + keys: KeysCollection, + spatial_size: Sequence[int], + rand_size: Sequence[int], + pad: int = 0, + field_mode: Union[InterpolateModeType, Sequence[InterpolateModeType]] = InterpolateMode.AREA, + align_corners: Optional[bool] = None, + prob: float = 0.1, + def_range: Union[Sequence[float], float] = 1.0, + grid_dtype=torch.float32, + grid_mode: Union[GridSampleModeType, Sequence[GridSampleModeType]] = GridSampleMode.NEAREST, + grid_padding_mode: Union[GridSamplePadMode, str] = GridSamplePadMode.BORDER, + grid_align_corners: Optional[bool] = False, + device: Optional[torch.device] = None, + ): + RandomizableTransform.__init__(self, prob) + MapTransform.__init__(self, keys) + + self.field_mode = ensure_tuple_rep(field_mode, len(self.keys)) + self.grid_mode = ensure_tuple_rep(grid_mode, len(self.keys)) + + self.trans = RandSmoothDeform( + rand_size=rand_size, + spatial_size=spatial_size, + pad=pad, + field_mode=self.field_mode[0], + align_corners=align_corners, + prob=1.0, + def_range=def_range, + grid_dtype=grid_dtype, + grid_mode=self.grid_mode[0], + grid_padding_mode=grid_padding_mode, + grid_align_corners=grid_align_corners, + device=device, + ) + + def set_random_state( + self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None + ) -> "RandSmoothDeformd": + super().set_random_state(seed, state) + self.trans.set_random_state(seed, state) + return self + + def randomize(self, data: Optional[Any] = None) -> None: + super().randomize(None) + self.trans.randomize() + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Mapping[Hashable, NdarrayOrTensor]: + self.randomize() + + if not self._do_transform: + return data + + d = dict(data) + + for idx, key in enumerate(self.key_iterator(d)): + self.trans.set_field_mode(self.field_mode[idx % len(self.field_mode)]) + self.trans.set_grid_mode(self.grid_mode[idx % len(self.grid_mode)]) + + d[key] = self.trans(d[key], False, self.trans.device) + + return d + + +RandSmoothDeformD = RandSmoothDeformDict = RandSmoothDeformd +RandSmoothFieldAdjustIntensityD = RandSmoothFieldAdjustIntensityDict = RandSmoothFieldAdjustIntensityd +RandSmoothFieldAdjustContrastD = RandSmoothFieldAdjustContrastDict = RandSmoothFieldAdjustContrastd diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/spatial/array.py b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/spatial/array.py new file mode 100644 index 0000000000000000000000000000000000000000..c8e43df7e31da0ab1fb3fbbd3e6e0dd967974e30 --- /dev/null +++ b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/spatial/array.py @@ -0,0 +1,2807 @@ +# 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 "vanilla" transforms for spatial operations +https://github.com/Project-MONAI/MONAI/wiki/MONAI_Design +""" +import warnings +from copy import deepcopy +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union + +import numpy as np +import torch +from numpy.lib.stride_tricks import as_strided + +from monai.config import USE_COMPILED, DtypeLike +from monai.config.type_definitions import NdarrayOrTensor +from monai.data.utils import ( + AFFINE_TOL, + compute_shape_offset, + iter_patch, + reorient_spatial_axes, + to_affine_nd, + zoom_affine, +) +from monai.networks.layers import AffineTransform, GaussianFilter, grid_pull +from monai.networks.utils import meshgrid_ij, normalize_transform +from monai.transforms.croppad.array import CenterSpatialCrop, Pad +from monai.transforms.intensity.array import GaussianSmooth +from monai.transforms.transform import Randomizable, RandomizableTransform, ThreadUnsafe, Transform +from monai.transforms.utils import ( + convert_pad_mode, + create_control_grid, + create_grid, + create_rotate, + create_scale, + create_shear, + create_translate, + map_spatial_axes, +) +from monai.transforms.utils_pytorch_numpy_unification import allclose, moveaxis +from monai.utils import ( + GridSampleMode, + GridSamplePadMode, + InterpolateMode, + NumpyPadMode, + PytorchPadMode, + convert_to_dst_type, + ensure_tuple, + ensure_tuple_rep, + ensure_tuple_size, + fall_back_tuple, + issequenceiterable, + optional_import, + pytorch_after, +) +from monai.utils.deprecate_utils import deprecated_arg +from monai.utils.enums import GridPatchSort, TransformBackends +from monai.utils.misc import ImageMetaKey as Key +from monai.utils.module import look_up_option +from monai.utils.type_conversion import convert_data_type + +nib, has_nib = optional_import("nibabel") + +__all__ = [ + "SpatialResample", + "ResampleToMatch", + "Spacing", + "Orientation", + "Flip", + "GridDistortion", + "GridSplit", + "GridPatch", + "RandGridPatch", + "Resize", + "Rotate", + "Zoom", + "Rotate90", + "RandRotate90", + "RandRotate", + "RandFlip", + "RandGridDistortion", + "RandAxisFlip", + "RandZoom", + "AffineGrid", + "RandAffineGrid", + "RandDeformGrid", + "Resample", + "Affine", + "RandAffine", + "Rand2DElastic", + "Rand3DElastic", +] + +RandRange = Optional[Union[Sequence[Union[Tuple[float, float], float]], float]] + + +class SpatialResample(Transform): + """ + Resample input image from the orientation/spacing defined by ``src_affine`` affine matrix into + the ones specified by ``dst_affine`` affine matrix. + + Internally this transform computes the affine transform matrix from ``src_affine`` to ``dst_affine``, + by ``xform = linalg.solve(src_affine, dst_affine)``, and call ``monai.transforms.Affine`` with ``xform``. + """ + + backend = [TransformBackends.TORCH] + + def __init__( + self, + mode: Union[GridSampleMode, str] = GridSampleMode.BILINEAR, + padding_mode: Union[GridSamplePadMode, str] = GridSamplePadMode.BORDER, + align_corners: bool = False, + dtype: DtypeLike = np.float64, + ): + """ + Args: + mode: {``"bilinear"``, ``"nearest"``} + Interpolation mode to calculate output values. Defaults to ``"bilinear"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + When `USE_COMPILED` is `True`, this argument uses + ``"nearest"``, ``"bilinear"``, ``"bicubic"`` to indicate 0, 1, 3 order interpolations. + See also: https://docs.monai.io/en/stable/networks.html#grid-pull + padding_mode: {``"zeros"``, ``"border"``, ``"reflection"``} + Padding mode for outside grid values. Defaults to ``"border"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + align_corners: Geometrically, we consider the pixels of the input as squares rather than points. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + dtype: data type for resampling computation. Defaults to ``np.float64`` for best precision. + If ``None``, use the data type of input data. To be compatible with other modules, + the output data type is always ``np.float32``. + """ + self.mode = mode + self.padding_mode = padding_mode + self.align_corners = align_corners + self.dtype = dtype + + def __call__( + self, + img: NdarrayOrTensor, + src_affine: Optional[NdarrayOrTensor] = None, + dst_affine: Optional[NdarrayOrTensor] = None, + spatial_size: Optional[Union[Sequence[int], np.ndarray, int]] = None, + mode: Union[GridSampleMode, str, None] = None, + padding_mode: Union[GridSamplePadMode, str, None] = None, + align_corners: Optional[bool] = False, + dtype: DtypeLike = None, + ) -> Tuple[NdarrayOrTensor, NdarrayOrTensor]: + """ + Args: + img: input image to be resampled. It currently supports channel-first arrays with + at most three spatial dimensions. + src_affine: source affine matrix. Defaults to ``None``, which means the identity matrix. + the shape should be `(r+1, r+1)` where `r` is the spatial rank of ``img``. + dst_affine: destination affine matrix. Defaults to ``None``, which means the same as `src_affine`. + the shape should be `(r+1, r+1)` where `r` is the spatial rank of ``img``. + when `dst_affine` and `spatial_size` are None, the input will be returned without resampling, + but the data type will be `float32`. + spatial_size: output image spatial size. + if `spatial_size` and `self.spatial_size` are not defined, + the transform will compute a spatial size automatically containing the previous field of view. + if `spatial_size` is ``-1`` are the transform will use the corresponding input img size. + mode: {``"bilinear"``, ``"nearest"``} + Interpolation mode to calculate output values. Defaults to ``"bilinear"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + When `USE_COMPILED` is `True`, this argument uses + ``"nearest"``, ``"bilinear"``, ``"bicubic"`` to indicate 0, 1, 3 order interpolations. + See also: https://docs.monai.io/en/stable/networks.html#grid-pull + padding_mode: {``"zeros"``, ``"border"``, ``"reflection"``} + Padding mode for outside grid values. Defaults to ``"border"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + align_corners: Geometrically, we consider the pixels of the input as squares rather than points. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + dtype: data type for resampling computation. Defaults to ``self.dtype`` or + ``np.float64`` (for best precision). If ``None``, use the data type of input data. + To be compatible with other modules, the output data type is always `float32`. + + The spatial rank is determined by the smallest among ``img.ndim -1``, ``len(src_affine) - 1``, and ``3``. + + When both ``monai.config.USE_COMPILED`` and ``align_corners`` are set to ``True``, + MONAI's resampling implementation will be used. + Set `dst_affine` and `spatial_size` to `None` to turn off the resampling step. + """ + if src_affine is None: + src_affine = np.eye(4, dtype=np.float64) + spatial_rank = min(len(img.shape) - 1, src_affine.shape[0] - 1, 3) + if (not isinstance(spatial_size, int) or spatial_size != -1) and spatial_size is not None: + spatial_rank = min(len(ensure_tuple(spatial_size)), 3) # infer spatial rank based on spatial_size + src_affine = to_affine_nd(spatial_rank, src_affine) + dst_affine = to_affine_nd(spatial_rank, dst_affine) if dst_affine is not None else src_affine + dst_affine, *_ = convert_to_dst_type(dst_affine, dst_affine, dtype=torch.float32) + + in_spatial_size = np.asarray(img.shape[1 : spatial_rank + 1]) + if isinstance(spatial_size, int) and (spatial_size == -1): # using the input spatial size + spatial_size = in_spatial_size + elif spatial_size is None and spatial_rank > 1: # auto spatial size + spatial_size, _ = compute_shape_offset(in_spatial_size, src_affine, dst_affine) # type: ignore + spatial_size = np.asarray(fall_back_tuple(ensure_tuple(spatial_size)[:spatial_rank], in_spatial_size)) + + if ( + allclose(src_affine, dst_affine, atol=AFFINE_TOL) + and allclose(spatial_size, in_spatial_size) + or spatial_rank == 1 + ): + # no significant change, return original image + output_data, *_ = convert_to_dst_type(img, img, dtype=torch.float32) + return output_data, dst_affine + + if has_nib and isinstance(img, np.ndarray): + spatial_ornt, dst_r = reorient_spatial_axes(img.shape[1 : spatial_rank + 1], src_affine, dst_affine) + if allclose(dst_r, dst_affine, atol=AFFINE_TOL) and allclose(spatial_size, in_spatial_size): + # simple reorientation achieves the desired affine + spatial_ornt[:, 0] += 1 + spatial_ornt = np.concatenate([np.array([[0, 1]]), spatial_ornt]) + img_ = nib.orientations.apply_orientation(img, spatial_ornt) + output_data, *_ = convert_to_dst_type(img_, img, dtype=torch.float32) + return output_data, dst_affine + + try: + src_affine, *_ = convert_to_dst_type(src_affine, dst_affine) + if isinstance(src_affine, np.ndarray): + xform = np.linalg.solve(src_affine, dst_affine) + else: + xform = ( + torch.linalg.solve(src_affine, dst_affine) + if pytorch_after(1, 8, 0) + else torch.solve(dst_affine, src_affine).solution # type: ignore + ) + except (np.linalg.LinAlgError, RuntimeError) as e: + raise ValueError("src affine is not invertible.") from e + xform = to_affine_nd(spatial_rank, xform) + # no resampling if it's identity transform + if allclose(xform, np.diag(np.ones(len(xform))), atol=AFFINE_TOL) and allclose(spatial_size, in_spatial_size): + output_data, *_ = convert_to_dst_type(img, img, dtype=torch.float32) + return output_data, dst_affine + + _dtype = dtype or self.dtype or img.dtype + in_spatial_size = in_spatial_size.tolist() + chns, additional_dims = img.shape[0], img.shape[spatial_rank + 1 :] # beyond three spatial dims + # resample + img_ = convert_data_type(img, torch.Tensor, dtype=_dtype)[0] + xform = convert_to_dst_type(xform, img_)[0] + align_corners = self.align_corners if align_corners is None else align_corners + mode = mode or self.mode + padding_mode = padding_mode or self.padding_mode + if additional_dims: + xform_shape = [-1] + in_spatial_size + img_ = img_.reshape(xform_shape) + if align_corners: + _t_r = torch.diag(torch.ones(len(xform), dtype=xform.dtype, device=xform.device)) # type: ignore + for idx, d_dst in enumerate(spatial_size[:spatial_rank]): + _t_r[idx, -1] = (max(d_dst, 2) - 1.0) / 2.0 + xform = xform @ _t_r + if not USE_COMPILED: + _t_l = normalize_transform( + in_spatial_size, xform.device, xform.dtype, align_corners=True # type: ignore + ) + xform = _t_l @ xform # type: ignore + affine_xform = Affine( + affine=xform, spatial_size=spatial_size, normalized=True, image_only=True, dtype=_dtype + ) + output_data = affine_xform(img_, mode=mode, padding_mode=padding_mode) + else: + affine_xform = AffineTransform( + normalized=False, + mode=mode, + padding_mode=padding_mode, + align_corners=align_corners, + reverse_indexing=True, + ) + output_data = affine_xform(img_.unsqueeze(0), theta=xform, spatial_size=spatial_size).squeeze(0) + if additional_dims: + full_shape = (chns, *spatial_size, *additional_dims) + output_data = output_data.reshape(full_shape) + # output dtype float + output_data, *_ = convert_to_dst_type(output_data, img, dtype=torch.float32) + return output_data, dst_affine + + +class ResampleToMatch(SpatialResample): + """Resample an image to match given metadata. The affine matrix will be aligned, + and the size of the output image will match.""" + + def __call__( # type: ignore + self, + img: NdarrayOrTensor, + src_meta: Optional[Dict] = None, + dst_meta: Optional[Dict] = None, + mode: Union[GridSampleMode, str, None] = None, + padding_mode: Union[GridSamplePadMode, str, None] = None, + align_corners: Optional[bool] = False, + dtype: DtypeLike = None, + ): + if src_meta is None: + raise RuntimeError("`in_meta` is missing") + if dst_meta is None: + raise RuntimeError("`out_meta` is missing") + mode = mode or self.mode + padding_mode = padding_mode or self.padding_mode + align_corners = self.align_corners if align_corners is None else align_corners + dtype = dtype or self.dtype + src_affine = src_meta.get("affine") + dst_affine = dst_meta.get("affine") + img, updated_affine = super().__call__( + img=img, + src_affine=src_affine, + dst_affine=dst_affine, + spatial_size=dst_meta.get("spatial_shape"), + mode=mode, + padding_mode=padding_mode, + align_corners=align_corners, + dtype=dtype, + ) + dst_meta = deepcopy(dst_meta) + dst_meta["affine"] = updated_affine + dst_meta[Key.FILENAME_OR_OBJ] = src_meta.get(Key.FILENAME_OR_OBJ) + return img, dst_meta + + +class Spacing(Transform): + """ + Resample input image into the specified `pixdim`. + """ + + backend = SpatialResample.backend + + def __init__( + self, + pixdim: Union[Sequence[float], float, np.ndarray], + diagonal: bool = False, + mode: Union[GridSampleMode, str] = GridSampleMode.BILINEAR, + padding_mode: Union[GridSamplePadMode, str] = GridSamplePadMode.BORDER, + align_corners: bool = False, + dtype: DtypeLike = np.float64, + image_only: bool = False, + ) -> None: + """ + Args: + pixdim: output voxel spacing. if providing a single number, will use it for the first dimension. + items of the pixdim sequence map to the spatial dimensions of input image, if length + of pixdim sequence is longer than image spatial dimensions, will ignore the longer part, + if shorter, will pad with `1.0`. + if the components of the `pixdim` are non-positive values, the transform will use the + corresponding components of the original pixdim, which is computed from the `affine` + matrix of input image. + diagonal: whether to resample the input to have a diagonal affine matrix. + If True, the input data is resampled to the following affine:: + + np.diag((pixdim_0, pixdim_1, ..., pixdim_n, 1)) + + This effectively resets the volume to the world coordinate system (RAS+ in nibabel). + The original orientation, rotation, shearing are not preserved. + + If False, this transform preserves the axes orientation, orthogonal rotation and + translation components from the original affine. This option will not flip/swap axes + of the original data. + mode: {``"bilinear"``, ``"nearest"``} + Interpolation mode to calculate output values. Defaults to ``"bilinear"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + When `USE_COMPILED` is `True`, this argument uses + ``"nearest"``, ``"bilinear"``, ``"bicubic"`` to indicate 0, 1, 3 order interpolations. + See also: https://docs.monai.io/en/stable/networks.html#grid-pull + padding_mode: {``"zeros"``, ``"border"``, ``"reflection"``} + Padding mode for outside grid values. Defaults to ``"border"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + align_corners: Geometrically, we consider the pixels of the input as squares rather than points. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + dtype: data type for resampling computation. Defaults to ``np.float64`` for best precision. + If None, use the data type of input data. To be compatible with other modules, + the output data type is always ``np.float32``. + image_only: return just the image or the image, the old affine and new affine. Default is `False`. + + """ + self.pixdim = np.array(ensure_tuple(pixdim), dtype=np.float64) + self.diagonal = diagonal + self.image_only = image_only + + self.sp_resample = SpatialResample( + mode=look_up_option(mode, GridSampleMode), + padding_mode=look_up_option(padding_mode, GridSamplePadMode), + align_corners=align_corners, + dtype=dtype, + ) + + def __call__( + self, + data_array: NdarrayOrTensor, + affine: Optional[NdarrayOrTensor] = None, + mode: Optional[Union[GridSampleMode, str]] = None, + padding_mode: Optional[Union[GridSamplePadMode, str]] = None, + align_corners: Optional[bool] = None, + dtype: DtypeLike = None, + output_spatial_shape: Optional[Union[Sequence[int], np.ndarray, int]] = None, + ) -> Union[NdarrayOrTensor, Tuple[NdarrayOrTensor, NdarrayOrTensor, NdarrayOrTensor]]: + """ + Args: + data_array: in shape (num_channels, H[, W, ...]). + affine (matrix): (N+1)x(N+1) original affine matrix for spatially ND `data_array`. Defaults to identity. + mode: {``"bilinear"``, ``"nearest"``} + Interpolation mode to calculate output values. Defaults to ``self.mode``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + When `USE_COMPILED` is `True`, this argument uses + ``"nearest"``, ``"bilinear"``, ``"bicubic"`` to indicate 0, 1, 3 order interpolations. + See also: https://docs.monai.io/en/stable/networks.html#grid-pull + padding_mode: {``"zeros"``, ``"border"``, ``"reflection"``} + Padding mode for outside grid values. Defaults to ``self.padding_mode``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + align_corners: Geometrically, we consider the pixels of the input as squares rather than points. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + dtype: data type for resampling computation. Defaults to ``self.dtype``. + If None, use the data type of input data. To be compatible with other modules, + the output data type is always ``np.float32``. + output_spatial_shape: specify the shape of the output data_array. This is typically useful for + the inverse of `Spacingd` where sometimes we could not compute the exact shape due to the quantization + error with the affine. + + Raises: + ValueError: When ``data_array`` has no spatial dimensions. + ValueError: When ``pixdim`` is nonpositive. + + Returns: + data_array (resampled into `self.pixdim`), original affine, current affine. + + """ + sr = int(data_array.ndim - 1) + if sr <= 0: + raise ValueError("data_array must have at least one spatial dimension.") + if affine is None: + # default to identity + affine_np = affine = np.eye(sr + 1, dtype=np.float64) + affine_ = np.eye(sr + 1, dtype=np.float64) + else: + affine_np, *_ = convert_data_type(affine, np.ndarray) + affine_ = to_affine_nd(sr, affine_np) + + out_d = self.pixdim[:sr] + if out_d.size < sr: + out_d = np.append(out_d, [1.0] * (sr - out_d.size)) + + # compute output affine, shape and offset + new_affine = zoom_affine(affine_, out_d, diagonal=self.diagonal) + output_shape, offset = compute_shape_offset(data_array.shape[1:], affine_, new_affine) + new_affine[:sr, -1] = offset[:sr] + output_data, new_affine = self.sp_resample( + data_array, + src_affine=affine, + dst_affine=new_affine, + spatial_size=list(output_shape) if output_spatial_shape is None else output_spatial_shape, + mode=mode, + padding_mode=padding_mode, + align_corners=align_corners, + dtype=dtype, + ) + new_affine = to_affine_nd(affine_np, new_affine) + new_affine, *_ = convert_to_dst_type(src=new_affine, dst=affine, dtype=torch.float32) + + if self.image_only: + return output_data + return output_data, affine, new_affine + + +class Orientation(Transform): + """ + Change the input image's orientation into the specified based on `axcodes`. + """ + + backend = [TransformBackends.NUMPY, TransformBackends.TORCH] + + def __init__( + self, + axcodes: Optional[str] = None, + as_closest_canonical: bool = False, + labels: Optional[Sequence[Tuple[str, str]]] = (("L", "R"), ("P", "A"), ("I", "S")), + image_only: bool = False, + ) -> None: + """ + Args: + axcodes: N elements sequence for spatial ND input's orientation. + e.g. axcodes='RAS' represents 3D orientation: + (Left, Right), (Posterior, Anterior), (Inferior, Superior). + default orientation labels options are: 'L' and 'R' for the first dimension, + 'P' and 'A' for the second, 'I' and 'S' for the third. + as_closest_canonical: if True, load the image as closest to canonical axis format. + labels: optional, None or sequence of (2,) sequences + (2,) sequences are labels for (beginning, end) of output axis. + Defaults to ``(('L', 'R'), ('P', 'A'), ('I', 'S'))``. + image_only: if True return only the image volume, otherwise return (image, affine, new_affine). + + Raises: + ValueError: When ``axcodes=None`` and ``as_closest_canonical=True``. Incompatible values. + + See Also: `nibabel.orientations.ornt2axcodes`. + + """ + if axcodes is None and not as_closest_canonical: + raise ValueError("Incompatible values: axcodes=None and as_closest_canonical=True.") + if axcodes is not None and as_closest_canonical: + warnings.warn("using as_closest_canonical=True, axcodes ignored.") + self.axcodes = axcodes + self.as_closest_canonical = as_closest_canonical + self.labels = labels + self.image_only = image_only + + def __call__( + self, data_array: NdarrayOrTensor, affine: Optional[NdarrayOrTensor] = None + ) -> Union[NdarrayOrTensor, Tuple[NdarrayOrTensor, NdarrayOrTensor, NdarrayOrTensor]]: + """ + original orientation of `data_array` is defined by `affine`. + + Args: + data_array: in shape (num_channels, H[, W, ...]). + affine (matrix): (N+1)x(N+1) original affine matrix for spatially ND `data_array`. Defaults to identity. + + Raises: + ValueError: When ``data_array`` has no spatial dimensions. + ValueError: When ``axcodes`` spatiality differs from ``data_array``. + + Returns: + data_array [reoriented in `self.axcodes`] if `self.image_only`, else + (data_array [reoriented in `self.axcodes`], original axcodes, current axcodes). + + """ + spatial_shape = data_array.shape[1:] + sr = len(spatial_shape) + if sr <= 0: + raise ValueError("data_array must have at least one spatial dimension.") + affine_: np.ndarray + if affine is None: + # default to identity + affine_np = affine = np.eye(sr + 1, dtype=np.float64) + affine_ = np.eye(sr + 1, dtype=np.float64) + else: + affine_np, *_ = convert_data_type(affine, np.ndarray) + affine_ = to_affine_nd(sr, affine_np) + + src = nib.io_orientation(affine_) + if self.as_closest_canonical: + spatial_ornt = src + else: + if self.axcodes is None: + raise ValueError("Incompatible values: axcodes=None and as_closest_canonical=True.") + if sr < len(self.axcodes): + warnings.warn( + f"axcodes ('{self.axcodes}') length is smaller than the number of input spatial dimensions D={sr}.\n" + f"{self.__class__.__name__}: input spatial shape is {spatial_shape}, num. channels is {data_array.shape[0]}," + "please make sure the input is in the channel-first format." + ) + dst = nib.orientations.axcodes2ornt(self.axcodes[:sr], labels=self.labels) + if len(dst) < sr: + raise ValueError( + f"axcodes must match data_array spatially, got axcodes={len(self.axcodes)}D data_array={sr}D" + ) + spatial_ornt = nib.orientations.ornt_transform(src, dst) + new_affine = affine_ @ nib.orientations.inv_ornt_aff(spatial_ornt, spatial_shape) + _is_tensor = isinstance(data_array, torch.Tensor) + spatial_ornt[:, 0] += 1 # skip channel dim + spatial_ornt = np.concatenate([np.array([[0, 1]]), spatial_ornt]) + axes = [ax for ax, flip in enumerate(spatial_ornt[:, 1]) if flip == -1] + if axes: + data_array = ( + torch.flip(data_array, dims=axes) if _is_tensor else np.flip(data_array, axis=axes) # type: ignore + ) + full_transpose = np.arange(len(data_array.shape)) + full_transpose[: len(spatial_ornt)] = np.argsort(spatial_ornt[:, 0]) + if not np.all(full_transpose == np.arange(len(data_array.shape))): + if _is_tensor: + data_array = data_array.permute(full_transpose.tolist()) # type: ignore + else: + data_array = data_array.transpose(full_transpose) # type: ignore + out, *_ = convert_to_dst_type(src=data_array, dst=data_array) + new_affine = to_affine_nd(affine_np, new_affine) + new_affine, *_ = convert_to_dst_type(src=new_affine, dst=affine, dtype=torch.float32) + + if self.image_only: + return out + return out, affine, new_affine + + +class Flip(Transform): + """ + Reverses the order of elements along the given spatial axis. Preserves shape. + Uses ``np.flip`` in practice. See numpy.flip for additional details: + https://docs.scipy.org/doc/numpy/reference/generated/numpy.flip.html. + + Args: + spatial_axis: spatial axes along which to flip over. Default is None. + The default `axis=None` will flip over all of the axes of the input array. + If axis is negative it counts from the last to the first axis. + If axis is a tuple of ints, flipping is performed on all of the axes + specified in the tuple. + + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + def __init__(self, spatial_axis: Optional[Union[Sequence[int], int]] = None) -> None: + self.spatial_axis = spatial_axis + + def __call__(self, img: NdarrayOrTensor) -> NdarrayOrTensor: + """ + Args: + img: channel first array, must have shape: (num_channels, H[, W, ..., ]), + """ + if isinstance(img, np.ndarray): + return np.ascontiguousarray(np.flip(img, map_spatial_axes(img.ndim, self.spatial_axis))) + return torch.flip(img, map_spatial_axes(img.ndim, self.spatial_axis)) + + +class Resize(Transform): + """ + Resize the input image to given spatial size (with scaling, not cropping/padding). + Implemented using :py:class:`torch.nn.functional.interpolate`. + + Args: + spatial_size: expected shape of spatial dimensions after resize operation. + if some components of the `spatial_size` are non-positive values, the transform will use the + corresponding components of img size. For example, `spatial_size=(32, -1)` will be adapted + to `(32, 64)` if the second spatial dimension size of img is `64`. + size_mode: should be "all" or "longest", if "all", will use `spatial_size` for all the spatial dims, + if "longest", rescale the image so that only the longest side is equal to specified `spatial_size`, + which must be an int number in this case, keeping the aspect ratio of the initial image, refer to: + https://albumentations.ai/docs/api_reference/augmentations/geometric/resize/ + #albumentations.augmentations.geometric.resize.LongestMaxSize. + mode: {``"nearest"``, ``"nearest-exact"``, ``"linear"``, ``"bilinear"``, ``"bicubic"``, ``"trilinear"``, ``"area"``} + The interpolation mode. Defaults to ``"area"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.interpolate.html + align_corners: This only has an effect when mode is + 'linear', 'bilinear', 'bicubic' or 'trilinear'. Default: None. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.interpolate.html + anti_aliasing: bool + Whether to apply a Gaussian filter to smooth the image prior + to downsampling. It is crucial to filter when downsampling + the image to avoid aliasing artifacts. See also ``skimage.transform.resize`` + anti_aliasing_sigma: {float, tuple of floats}, optional + Standard deviation for Gaussian filtering used when anti-aliasing. + By default, this value is chosen as (s - 1) / 2 where s is the + downsampling factor, where s > 1. For the up-size case, s < 1, no + anti-aliasing is performed prior to rescaling. + """ + + backend = [TransformBackends.TORCH] + + def __init__( + self, + spatial_size: Union[Sequence[int], int], + size_mode: str = "all", + mode: Union[InterpolateMode, str] = InterpolateMode.AREA, + align_corners: Optional[bool] = None, + anti_aliasing: bool = False, + anti_aliasing_sigma: Union[Sequence[float], float, None] = None, + ) -> None: + self.size_mode = look_up_option(size_mode, ["all", "longest"]) + self.spatial_size = spatial_size + self.mode: InterpolateMode = look_up_option(mode, InterpolateMode) + self.align_corners = align_corners + self.anti_aliasing = anti_aliasing + self.anti_aliasing_sigma = anti_aliasing_sigma + + def __call__( + self, + img: NdarrayOrTensor, + mode: Optional[Union[InterpolateMode, str]] = None, + align_corners: Optional[bool] = None, + anti_aliasing: Optional[bool] = None, + anti_aliasing_sigma: Union[Sequence[float], float, None] = None, + ) -> NdarrayOrTensor: + """ + Args: + img: channel first array, must have shape: (num_channels, H[, W, ..., ]). + mode: {``"nearest"``, ``"nearest-exact"``, ``"linear"``, + ``"bilinear"``, ``"bicubic"``, ``"trilinear"``, ``"area"``} + The interpolation mode. Defaults to ``self.mode``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.interpolate.html + align_corners: This only has an effect when mode is + 'linear', 'bilinear', 'bicubic' or 'trilinear'. Defaults to ``self.align_corners``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.interpolate.html + anti_aliasing: bool, optional + Whether to apply a Gaussian filter to smooth the image prior + to downsampling. It is crucial to filter when downsampling + the image to avoid aliasing artifacts. See also ``skimage.transform.resize`` + anti_aliasing_sigma: {float, tuple of floats}, optional + Standard deviation for Gaussian filtering used when anti-aliasing. + By default, this value is chosen as (s - 1) / 2 where s is the + downsampling factor, where s > 1. For the up-size case, s < 1, no + anti-aliasing is performed prior to rescaling. + + Raises: + ValueError: When ``self.spatial_size`` length is less than ``img`` spatial dimensions. + + """ + anti_aliasing = self.anti_aliasing if anti_aliasing is None else anti_aliasing + anti_aliasing_sigma = self.anti_aliasing_sigma if anti_aliasing_sigma is None else anti_aliasing_sigma + + if self.size_mode == "all": + input_ndim = img.ndim - 1 # spatial ndim + output_ndim = len(ensure_tuple(self.spatial_size)) + if output_ndim > input_ndim: + input_shape = ensure_tuple_size(img.shape, output_ndim + 1, 1) + img = img.reshape(input_shape) + elif output_ndim < input_ndim: + raise ValueError( + "len(spatial_size) must be greater or equal to img spatial dimensions, " + f"got spatial_size={output_ndim} img={input_ndim}." + ) + spatial_size_ = fall_back_tuple(self.spatial_size, img.shape[1:]) + else: # for the "longest" mode + img_size = img.shape[1:] + if not isinstance(self.spatial_size, int): + raise ValueError("spatial_size must be an int number if size_mode is 'longest'.") + scale = self.spatial_size / max(img_size) + spatial_size_ = tuple(int(round(s * scale)) for s in img_size) + + if tuple(img.shape[1:]) == spatial_size_: # spatial shape is already the desired + return img + img_, *_ = convert_data_type(img, torch.Tensor, dtype=torch.float) + if anti_aliasing and any(x < y for x, y in zip(spatial_size_, img_.shape[1:])): + factors = torch.div(torch.Tensor(list(img_.shape[1:])), torch.Tensor(spatial_size_)) + if anti_aliasing_sigma is None: + # if sigma is not given, use the default sigma in skimage.transform.resize + anti_aliasing_sigma = torch.maximum(torch.zeros(factors.shape), (factors - 1) / 2).tolist() + else: + # if sigma is given, use the given value for downsampling axis + anti_aliasing_sigma = list(ensure_tuple_rep(anti_aliasing_sigma, len(spatial_size_))) + for axis in range(len(spatial_size_)): + anti_aliasing_sigma[axis] = anti_aliasing_sigma[axis] * int(factors[axis] > 1) + anti_aliasing_filter = GaussianSmooth(sigma=anti_aliasing_sigma) + img_ = anti_aliasing_filter(img_) + + resized = torch.nn.functional.interpolate( + input=img_.unsqueeze(0), + size=spatial_size_, + mode=look_up_option(self.mode if mode is None else mode, InterpolateMode).value, + align_corners=self.align_corners if align_corners is None else align_corners, + ) + out, *_ = convert_to_dst_type(resized.squeeze(0), img) + return out + + +class Rotate(Transform, ThreadUnsafe): + """ + Rotates an input image by given angle using :py:class:`monai.networks.layers.AffineTransform`. + + Args: + angle: Rotation angle(s) in radians. should a float for 2D, three floats for 3D. + keep_size: If it is True, the output shape is kept the same as the input. + If it is False, the output shape is adapted so that the + input array is contained completely in the output. Default is True. + mode: {``"bilinear"``, ``"nearest"``} + Interpolation mode to calculate output values. Defaults to ``"bilinear"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + padding_mode: {``"zeros"``, ``"border"``, ``"reflection"``} + Padding mode for outside grid values. Defaults to ``"border"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + align_corners: Defaults to False. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + dtype: data type for resampling computation. Defaults to ``np.float32``. + If None, use the data type of input data. To be compatible with other modules, + the output data type is always ``np.float32``. + """ + + backend = [TransformBackends.TORCH] + + def __init__( + self, + angle: Union[Sequence[float], float], + keep_size: bool = True, + mode: Union[GridSampleMode, str] = GridSampleMode.BILINEAR, + padding_mode: Union[GridSamplePadMode, str] = GridSamplePadMode.BORDER, + align_corners: bool = False, + dtype: Union[DtypeLike, torch.dtype] = np.float32, + ) -> None: + self.angle = angle + self.keep_size = keep_size + self.mode: GridSampleMode = look_up_option(mode, GridSampleMode) + self.padding_mode: GridSamplePadMode = look_up_option(padding_mode, GridSamplePadMode) + self.align_corners = align_corners + self.dtype = dtype + self._rotation_matrix: Optional[NdarrayOrTensor] = None + + def __call__( + self, + img: NdarrayOrTensor, + mode: Optional[Union[GridSampleMode, str]] = None, + padding_mode: Optional[Union[GridSamplePadMode, str]] = None, + align_corners: Optional[bool] = None, + dtype: Union[DtypeLike, torch.dtype] = None, + ) -> NdarrayOrTensor: + """ + Args: + img: channel first array, must have shape: [chns, H, W] or [chns, H, W, D]. + mode: {``"bilinear"``, ``"nearest"``} + Interpolation mode to calculate output values. Defaults to ``self.mode``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + padding_mode: {``"zeros"``, ``"border"``, ``"reflection"``} + Padding mode for outside grid values. Defaults to ``self.padding_mode``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + align_corners: Defaults to ``self.align_corners``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + align_corners: Defaults to ``self.align_corners``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + dtype: data type for resampling computation. Defaults to ``self.dtype``. + If None, use the data type of input data. To be compatible with other modules, + the output data type is always ``np.float32``. + + Raises: + ValueError: When ``img`` spatially is not one of [2D, 3D]. + + """ + _dtype = dtype or self.dtype or img.dtype + + img_t, *_ = convert_data_type(img, torch.Tensor, dtype=_dtype) + + im_shape = np.asarray(img_t.shape[1:]) # spatial dimensions + input_ndim = len(im_shape) + if input_ndim not in (2, 3): + raise ValueError(f"Unsupported img dimension: {input_ndim}, available options are [2, 3].") + _angle = ensure_tuple_rep(self.angle, 1 if input_ndim == 2 else 3) + transform = create_rotate(input_ndim, _angle) + shift = create_translate(input_ndim, ((im_shape - 1) / 2).tolist()) + if self.keep_size: + output_shape = im_shape + else: + corners = np.asarray(np.meshgrid(*[(0, dim) for dim in im_shape], indexing="ij")).reshape( + (len(im_shape), -1) + ) + corners = transform[:-1, :-1] @ corners # type: ignore + output_shape = np.asarray(corners.ptp(axis=1) + 0.5, dtype=int) + shift_1 = create_translate(input_ndim, (-(output_shape - 1) / 2).tolist()) + transform = shift @ transform @ shift_1 + + transform_t, *_ = convert_to_dst_type(transform, img_t) + + xform = AffineTransform( + normalized=False, + mode=look_up_option(mode or self.mode, GridSampleMode), + padding_mode=look_up_option(padding_mode or self.padding_mode, GridSamplePadMode), + align_corners=self.align_corners if align_corners is None else align_corners, + reverse_indexing=True, + ) + output: torch.Tensor = xform(img_t.unsqueeze(0), transform_t, spatial_size=output_shape).float().squeeze(0) + self._rotation_matrix = transform + out: NdarrayOrTensor + out, *_ = convert_to_dst_type(output, dst=img, dtype=output.dtype) + return out + + def get_rotation_matrix(self) -> Optional[NdarrayOrTensor]: + """ + Get the most recently applied rotation matrix + This is not thread-safe. + """ + return self._rotation_matrix + + +class Zoom(Transform): + """ + Zooms an ND image using :py:class:`torch.nn.functional.interpolate`. + For details, please see https://pytorch.org/docs/stable/generated/torch.nn.functional.interpolate.html. + + Different from :py:class:`monai.transforms.resize`, this transform takes scaling factors + as input, and provides an option of preserving the input spatial size. + + Args: + zoom: The zoom factor along the spatial axes. + If a float, zoom is the same for each spatial axis. + If a sequence, zoom should contain one value for each spatial axis. + mode: {``"nearest"``, ``"nearest-exact"``, ``"linear"``, ``"bilinear"``, ``"bicubic"``, ``"trilinear"``, ``"area"``} + The interpolation mode. Defaults to ``"area"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.interpolate.html + padding_mode: available modes for numpy array:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, + ``"mean"``, ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + available modes for PyTorch Tensor: {``"constant"``, ``"reflect"``, ``"replicate"``, ``"circular"``}. + One of the listed string values or a user supplied function. Defaults to ``"constant"``. + The mode to pad data after zooming. + See also: https://numpy.org/doc/1.18/reference/generated/numpy.pad.html + https://pytorch.org/docs/stable/generated/torch.nn.functional.pad.html + align_corners: This only has an effect when mode is + 'linear', 'bilinear', 'bicubic' or 'trilinear'. Default: None. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.interpolate.html + keep_size: Should keep original size (padding/slicing if needed), default is True. + kwargs: other arguments for the `np.pad` or `torch.pad` function. + note that `np.pad` treats channel dimension as the first dimension. + + """ + + backend = [TransformBackends.TORCH] + + def __init__( + self, + zoom: Union[Sequence[float], float], + mode: Union[InterpolateMode, str] = InterpolateMode.AREA, + padding_mode: Union[NumpyPadMode, PytorchPadMode, str] = NumpyPadMode.EDGE, + align_corners: Optional[bool] = None, + keep_size: bool = True, + **kwargs, + ) -> None: + self.zoom = zoom + self.mode: InterpolateMode = InterpolateMode(mode) + self.padding_mode = padding_mode + self.align_corners = align_corners + self.keep_size = keep_size + self.kwargs = kwargs + + def __call__( + self, + img: NdarrayOrTensor, + mode: Optional[Union[InterpolateMode, str]] = None, + padding_mode: Optional[Union[NumpyPadMode, PytorchPadMode, str]] = None, + align_corners: Optional[bool] = None, + ) -> NdarrayOrTensor: + """ + Args: + img: channel first array, must have shape: (num_channels, H[, W, ..., ]). + mode: {``"nearest"``, ``"nearest-exact"``, ``"linear"``, + ``"bilinear"``, ``"bicubic"``, ``"trilinear"``, ``"area"``} + The interpolation mode. Defaults to ``self.mode``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.interpolate.html + padding_mode: available modes for numpy array:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, + ``"mean"``, ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + available modes for PyTorch Tensor: {``"constant"``, ``"reflect"``, ``"replicate"``, ``"circular"``}. + One of the listed string values or a user supplied function. Defaults to ``"constant"``. + The mode to pad data after zooming. + See also: https://numpy.org/doc/1.18/reference/generated/numpy.pad.html + https://pytorch.org/docs/stable/generated/torch.nn.functional.pad.html + align_corners: This only has an effect when mode is + 'linear', 'bilinear', 'bicubic' or 'trilinear'. Defaults to ``self.align_corners``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.interpolate.html + + """ + img_t, *_ = convert_data_type(img, torch.Tensor, dtype=torch.float32) + + _zoom = ensure_tuple_rep(self.zoom, img.ndim - 1) # match the spatial image dim + zoomed: NdarrayOrTensor = torch.nn.functional.interpolate( # type: ignore + recompute_scale_factor=True, + input=img_t.unsqueeze(0), + scale_factor=list(_zoom), + mode=look_up_option(self.mode if mode is None else mode, InterpolateMode).value, + align_corners=self.align_corners if align_corners is None else align_corners, + ) + zoomed = zoomed.squeeze(0) + + if self.keep_size and not np.allclose(img_t.shape, zoomed.shape): + + pad_vec = [(0, 0)] * len(img_t.shape) + slice_vec = [slice(None)] * len(img_t.shape) + for idx, (od, zd) in enumerate(zip(img_t.shape, zoomed.shape)): + diff = od - zd + half = abs(diff) // 2 + if diff > 0: # need padding + pad_vec[idx] = (half, diff - half) + elif diff < 0: # need slicing + slice_vec[idx] = slice(half, half + od) + + padder = Pad(pad_vec, padding_mode or self.padding_mode) + zoomed = padder(zoomed) + zoomed = zoomed[tuple(slice_vec)] + + out, *_ = convert_to_dst_type(zoomed, dst=img) + return out + + +class Rotate90(Transform): + """ + Rotate an array by 90 degrees in the plane specified by `axes`. + See np.rot90 for additional details: + https://numpy.org/doc/stable/reference/generated/numpy.rot90.html. + + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + def __init__(self, k: int = 1, spatial_axes: Tuple[int, int] = (0, 1)) -> None: + """ + Args: + k: number of times to rotate by 90 degrees. + spatial_axes: 2 int numbers, defines the plane to rotate with 2 spatial axes. + Default: (0, 1), this is the first two axis in spatial dimensions. + If axis is negative it counts from the last to the first axis. + """ + self.k = k + spatial_axes_: Tuple[int, int] = ensure_tuple(spatial_axes) # type: ignore + if len(spatial_axes_) != 2: + raise ValueError("spatial_axes must be 2 int numbers to indicate the axes to rotate 90 degrees.") + self.spatial_axes = spatial_axes_ + + def __call__(self, img: NdarrayOrTensor) -> NdarrayOrTensor: + """ + Args: + img: channel first array, must have shape: (num_channels, H[, W, ..., ]), + """ + rot90: Callable = torch.rot90 if isinstance(img, torch.Tensor) else np.rot90 # type: ignore + out: NdarrayOrTensor = rot90(img, self.k, map_spatial_axes(img.ndim, self.spatial_axes)) + out, *_ = convert_data_type(out, dtype=img.dtype) + return out + + +class RandRotate90(RandomizableTransform): + """ + With probability `prob`, input arrays are rotated by 90 degrees + in the plane specified by `spatial_axes`. + """ + + backend = Rotate90.backend + + def __init__(self, prob: float = 0.1, max_k: int = 3, spatial_axes: Tuple[int, int] = (0, 1)) -> None: + """ + Args: + prob: probability of rotating. + (Default 0.1, with 10% probability it returns a rotated array) + max_k: number of rotations will be sampled from `np.random.randint(max_k) + 1`, (Default 3). + spatial_axes: 2 int numbers, defines the plane to rotate with 2 spatial axes. + Default: (0, 1), this is the first two axis in spatial dimensions. + """ + RandomizableTransform.__init__(self, prob) + self.max_k = max_k + self.spatial_axes = spatial_axes + + self._rand_k = 0 + + def randomize(self, data: Optional[Any] = None) -> None: + super().randomize(None) + if not self._do_transform: + return None + self._rand_k = self.R.randint(self.max_k) + 1 + + def __call__(self, img: NdarrayOrTensor, randomize: bool = True) -> NdarrayOrTensor: + """ + Args: + img: channel first array, must have shape: (num_channels, H[, W, ..., ]), + randomize: whether to execute `randomize()` function first, default to True. + """ + if randomize: + self.randomize() + + if not self._do_transform: + return img + + return Rotate90(self._rand_k, self.spatial_axes)(img) + + +class RandRotate(RandomizableTransform): + """ + Randomly rotate the input arrays. + + Args: + range_x: Range of rotation angle in radians in the plane defined by the first and second axes. + If single number, angle is uniformly sampled from (-range_x, range_x). + range_y: Range of rotation angle in radians in the plane defined by the first and third axes. + If single number, angle is uniformly sampled from (-range_y, range_y). + range_z: Range of rotation angle in radians in the plane defined by the second and third axes. + If single number, angle is uniformly sampled from (-range_z, range_z). + prob: Probability of rotation. + keep_size: If it is False, the output shape is adapted so that the + input array is contained completely in the output. + If it is True, the output shape is the same as the input. Default is True. + mode: {``"bilinear"``, ``"nearest"``} + Interpolation mode to calculate output values. Defaults to ``"bilinear"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + padding_mode: {``"zeros"``, ``"border"``, ``"reflection"``} + Padding mode for outside grid values. Defaults to ``"border"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + align_corners: Defaults to False. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + dtype: data type for resampling computation. Defaults to ``np.float32``. + If None, use the data type of input data. To be compatible with other modules, + the output data type is always ``np.float32``. + """ + + backend = Rotate.backend + + def __init__( + self, + range_x: Union[Tuple[float, float], float] = 0.0, + range_y: Union[Tuple[float, float], float] = 0.0, + range_z: Union[Tuple[float, float], float] = 0.0, + prob: float = 0.1, + keep_size: bool = True, + mode: Union[GridSampleMode, str] = GridSampleMode.BILINEAR, + padding_mode: Union[GridSamplePadMode, str] = GridSamplePadMode.BORDER, + align_corners: bool = False, + dtype: Union[DtypeLike, torch.dtype] = np.float32, + ) -> None: + RandomizableTransform.__init__(self, prob) + self.range_x = ensure_tuple(range_x) + if len(self.range_x) == 1: + self.range_x = tuple(sorted([-self.range_x[0], self.range_x[0]])) + self.range_y = ensure_tuple(range_y) + if len(self.range_y) == 1: + self.range_y = tuple(sorted([-self.range_y[0], self.range_y[0]])) + self.range_z = ensure_tuple(range_z) + if len(self.range_z) == 1: + self.range_z = tuple(sorted([-self.range_z[0], self.range_z[0]])) + + self.keep_size = keep_size + self.mode: GridSampleMode = look_up_option(mode, GridSampleMode) + self.padding_mode: GridSamplePadMode = look_up_option(padding_mode, GridSamplePadMode) + self.align_corners = align_corners + self.dtype = dtype + + self.x = 0.0 + self.y = 0.0 + self.z = 0.0 + + def randomize(self, data: Optional[Any] = None) -> None: + super().randomize(None) + if not self._do_transform: + return None + self.x = self.R.uniform(low=self.range_x[0], high=self.range_x[1]) + self.y = self.R.uniform(low=self.range_y[0], high=self.range_y[1]) + self.z = self.R.uniform(low=self.range_z[0], high=self.range_z[1]) + + def __call__( + self, + img: NdarrayOrTensor, + mode: Optional[Union[GridSampleMode, str]] = None, + padding_mode: Optional[Union[GridSamplePadMode, str]] = None, + align_corners: Optional[bool] = None, + dtype: Union[DtypeLike, torch.dtype] = None, + randomize: bool = True, + get_matrix: bool = False, + ): + """ + Args: + img: channel first array, must have shape 2D: (nchannels, H, W), or 3D: (nchannels, H, W, D). + mode: {``"bilinear"``, ``"nearest"``} + Interpolation mode to calculate output values. Defaults to ``self.mode``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + padding_mode: {``"zeros"``, ``"border"``, ``"reflection"``} + Padding mode for outside grid values. Defaults to ``self.padding_mode``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + align_corners: Defaults to ``self.align_corners``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + dtype: data type for resampling computation. Defaults to ``self.dtype``. + If None, use the data type of input data. To be compatible with other modules, + the output data type is always ``np.float32``. + randomize: whether to execute `randomize()` function first, default to True. + get_matrix: whether to return the rotated image and rotate matrix together, default to False. + """ + if randomize: + self.randomize() + + if not self._do_transform: + return img + + rotator = Rotate( + angle=self.x if img.ndim == 3 else (self.x, self.y, self.z), + keep_size=self.keep_size, + mode=look_up_option(mode or self.mode, GridSampleMode), + padding_mode=look_up_option(padding_mode or self.padding_mode, GridSamplePadMode), + align_corners=self.align_corners if align_corners is None else align_corners, + dtype=dtype or self.dtype or img.dtype, + ) + img = rotator(img) + return (img, rotator.get_rotation_matrix()) if get_matrix else img + + +class RandFlip(RandomizableTransform): + """ + Randomly flips the image along axes. Preserves shape. + See numpy.flip for additional details. + https://docs.scipy.org/doc/numpy/reference/generated/numpy.flip.html + + Args: + prob: Probability of flipping. + spatial_axis: Spatial axes along which to flip over. Default is None. + """ + + backend = Flip.backend + + def __init__(self, prob: float = 0.1, spatial_axis: Optional[Union[Sequence[int], int]] = None) -> None: + RandomizableTransform.__init__(self, prob) + self.flipper = Flip(spatial_axis=spatial_axis) + + def __call__(self, img: NdarrayOrTensor, randomize: bool = True) -> NdarrayOrTensor: + """ + Args: + img: channel first array, must have shape: (num_channels, H[, W, ..., ]), + randomize: whether to execute `randomize()` function first, default to True. + """ + if randomize: + self.randomize(None) + + if not self._do_transform: + return img + + return self.flipper(img) + + +class RandAxisFlip(RandomizableTransform): + """ + Randomly select a spatial axis and flip along it. + See numpy.flip for additional details. + https://docs.scipy.org/doc/numpy/reference/generated/numpy.flip.html + + Args: + prob: Probability of flipping. + + """ + + backend = Flip.backend + + def __init__(self, prob: float = 0.1) -> None: + RandomizableTransform.__init__(self, prob) + self._axis: Optional[int] = None + + def randomize(self, data: NdarrayOrTensor) -> None: + super().randomize(None) + if not self._do_transform: + return None + self._axis = self.R.randint(data.ndim - 1) + + def __call__(self, img: NdarrayOrTensor, randomize: bool = True) -> NdarrayOrTensor: + """ + Args: + img: channel first array, must have shape: (num_channels, H[, W, ..., ]), + randomize: whether to execute `randomize()` function first, default to True. + """ + if randomize: + self.randomize(data=img) + + if not self._do_transform: + return img + + return Flip(spatial_axis=self._axis)(img) + + +class RandZoom(RandomizableTransform): + """ + Randomly zooms input arrays with given probability within given zoom range. + + Args: + prob: Probability of zooming. + min_zoom: Min zoom factor. Can be float or sequence same size as image. + If a float, select a random factor from `[min_zoom, max_zoom]` then apply to all spatial dims + to keep the original spatial shape ratio. + If a sequence, min_zoom should contain one value for each spatial axis. + If 2 values provided for 3D data, use the first value for both H & W dims to keep the same zoom ratio. + max_zoom: Max zoom factor. Can be float or sequence same size as image. + If a float, select a random factor from `[min_zoom, max_zoom]` then apply to all spatial dims + to keep the original spatial shape ratio. + If a sequence, max_zoom should contain one value for each spatial axis. + If 2 values provided for 3D data, use the first value for both H & W dims to keep the same zoom ratio. + mode: {``"nearest"``, ``"nearest-exact"``, ``"linear"``, ``"bilinear"``, ``"bicubic"``, ``"trilinear"``, ``"area"``} + The interpolation mode. Defaults to ``"area"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.interpolate.html + padding_mode: available modes for numpy array:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, + ``"mean"``, ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + available modes for PyTorch Tensor: {``"constant"``, ``"reflect"``, ``"replicate"``, ``"circular"``}. + One of the listed string values or a user supplied function. Defaults to ``"constant"``. + The mode to pad data after zooming. + See also: https://numpy.org/doc/1.18/reference/generated/numpy.pad.html + https://pytorch.org/docs/stable/generated/torch.nn.functional.pad.html + align_corners: This only has an effect when mode is + 'linear', 'bilinear', 'bicubic' or 'trilinear'. Default: None. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.interpolate.html + keep_size: Should keep original size (pad if needed), default is True. + kwargs: other arguments for the `np.pad` or `torch.pad` function. + note that `np.pad` treats channel dimension as the first dimension. + + """ + + backend = Zoom.backend + + def __init__( + self, + prob: float = 0.1, + min_zoom: Union[Sequence[float], float] = 0.9, + max_zoom: Union[Sequence[float], float] = 1.1, + mode: Union[InterpolateMode, str] = InterpolateMode.AREA, + padding_mode: Union[NumpyPadMode, PytorchPadMode, str] = NumpyPadMode.EDGE, + align_corners: Optional[bool] = None, + keep_size: bool = True, + **kwargs, + ) -> None: + RandomizableTransform.__init__(self, prob) + self.min_zoom = ensure_tuple(min_zoom) + self.max_zoom = ensure_tuple(max_zoom) + if len(self.min_zoom) != len(self.max_zoom): + raise AssertionError("min_zoom and max_zoom must have same length.") + self.mode: InterpolateMode = look_up_option(mode, InterpolateMode) + self.padding_mode = padding_mode + self.align_corners = align_corners + self.keep_size = keep_size + self.kwargs = kwargs + + self._zoom: Sequence[float] = [1.0] + + def randomize(self, img: NdarrayOrTensor) -> None: + super().randomize(None) + if not self._do_transform: + return None + self._zoom = [self.R.uniform(l, h) for l, h in zip(self.min_zoom, self.max_zoom)] + if len(self._zoom) == 1: + # to keep the spatial shape ratio, use same random zoom factor for all dims + self._zoom = ensure_tuple_rep(self._zoom[0], img.ndim - 1) + elif len(self._zoom) == 2 and img.ndim > 3: + # if 2 zoom factors provided for 3D data, use the first factor for H and W dims, second factor for D dim + self._zoom = ensure_tuple_rep(self._zoom[0], img.ndim - 2) + ensure_tuple(self._zoom[-1]) + + def __call__( + self, + img: NdarrayOrTensor, + mode: Optional[Union[InterpolateMode, str]] = None, + padding_mode: Optional[Union[NumpyPadMode, PytorchPadMode, str]] = None, + align_corners: Optional[bool] = None, + randomize: bool = True, + ) -> NdarrayOrTensor: + """ + Args: + img: channel first array, must have shape 2D: (nchannels, H, W), or 3D: (nchannels, H, W, D). + mode: {``"nearest"``, ``"nearest-exact"``, ``"linear"``, ``"bilinear"``, ``"bicubic"``, ``"trilinear"``, ``"area"``} + The interpolation mode. Defaults to ``self.mode``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.interpolate.html + padding_mode: available modes for numpy array:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, + ``"mean"``, ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + available modes for PyTorch Tensor: {``"constant"``, ``"reflect"``, ``"replicate"``, ``"circular"``}. + One of the listed string values or a user supplied function. Defaults to ``"constant"``. + The mode to pad data after zooming. + See also: https://numpy.org/doc/1.18/reference/generated/numpy.pad.html + https://pytorch.org/docs/stable/generated/torch.nn.functional.pad.html + align_corners: This only has an effect when mode is + 'linear', 'bilinear', 'bicubic' or 'trilinear'. Defaults to ``self.align_corners``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.interpolate.html + randomize: whether to execute `randomize()` function first, default to True. + + """ + # match the spatial image dim + if randomize: + self.randomize(img=img) + + if not self._do_transform: + return img + + return Zoom( + self._zoom, + keep_size=self.keep_size, + mode=look_up_option(mode or self.mode, InterpolateMode), + padding_mode=padding_mode or self.padding_mode, + align_corners=self.align_corners if align_corners is None else align_corners, + **self.kwargs, + )(img) + + +class AffineGrid(Transform): + """ + Affine transforms on the coordinates. + + Args: + rotate_params: a rotation angle in radians, a scalar for 2D image, a tuple of 3 floats for 3D. + Defaults to no rotation. + shear_params: shearing factors for affine matrix, take a 3D affine as example:: + + [ + [1.0, params[0], params[1], 0.0], + [params[2], 1.0, params[3], 0.0], + [params[4], params[5], 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ] + + a tuple of 2 floats for 2D, a tuple of 6 floats for 3D. Defaults to no shearing. + translate_params: a tuple of 2 floats for 2D, a tuple of 3 floats for 3D. Translation is in + pixel/voxel relative to the center of the input image. Defaults to no translation. + scale_params: scale factor for every spatial dims. a tuple of 2 floats for 2D, + a tuple of 3 floats for 3D. Defaults to `1.0`. + dtype: data type for the grid computation. Defaults to ``np.float32``. + If ``None``, use the data type of input data (if `grid` is provided). + device: device on which the tensor will be allocated, if a new grid is generated. + affine: If applied, ignore the params (`rotate_params`, etc.) and use the + supplied matrix. Should be square with each side = num of image spatial + dimensions + 1. + + .. deprecated:: 0.6.0 + ``as_tensor_output`` is deprecated. + + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + @deprecated_arg(name="as_tensor_output", since="0.6") + def __init__( + self, + rotate_params: Optional[Union[Sequence[float], float]] = None, + shear_params: Optional[Union[Sequence[float], float]] = None, + translate_params: Optional[Union[Sequence[float], float]] = None, + scale_params: Optional[Union[Sequence[float], float]] = None, + as_tensor_output: bool = True, + device: Optional[torch.device] = None, + dtype: DtypeLike = np.float32, + affine: Optional[NdarrayOrTensor] = None, + ) -> None: + self.rotate_params = rotate_params + self.shear_params = shear_params + self.translate_params = translate_params + self.scale_params = scale_params + self.device = device + self.dtype = dtype + self.affine = affine + + def __call__( + self, spatial_size: Optional[Sequence[int]] = None, grid: Optional[NdarrayOrTensor] = None + ) -> Tuple[NdarrayOrTensor, NdarrayOrTensor]: + """ + The grid can be initialized with a `spatial_size` parameter, or provided directly as `grid`. + Therefore, either `spatial_size` or `grid` must be provided. + When initialising from `spatial_size`, the backend "torch" will be used. + + Args: + spatial_size: output grid size. + grid: grid to be transformed. Shape must be (3, H, W) for 2D or (4, H, W, D) for 3D. + + Raises: + ValueError: When ``grid=None`` and ``spatial_size=None``. Incompatible values. + + """ + if grid is None: # create grid from spatial_size + if spatial_size is None: + raise ValueError("Incompatible values: grid=None and spatial_size=None.") + grid = create_grid(spatial_size, device=self.device, backend="torch", dtype=self.dtype) + _b = TransformBackends.TORCH if isinstance(grid, torch.Tensor) else TransformBackends.NUMPY + _device = grid.device if isinstance(grid, torch.Tensor) else self.device + affine: NdarrayOrTensor + if self.affine is None: + spatial_dims = len(grid.shape) - 1 + affine = ( + torch.eye(spatial_dims + 1, device=_device) + if _b == TransformBackends.TORCH + else np.eye(spatial_dims + 1) + ) + if self.rotate_params: + affine = affine @ create_rotate(spatial_dims, self.rotate_params, device=_device, backend=_b) + if self.shear_params: + affine = affine @ create_shear(spatial_dims, self.shear_params, device=_device, backend=_b) + if self.translate_params: + affine = affine @ create_translate(spatial_dims, self.translate_params, device=_device, backend=_b) + if self.scale_params: + affine = affine @ create_scale(spatial_dims, self.scale_params, device=_device, backend=_b) + else: + affine = self.affine + + grid, *_ = convert_data_type(grid, torch.Tensor, device=_device, dtype=self.dtype or grid.dtype) + affine, *_ = convert_to_dst_type(affine, grid) + + grid = (affine @ grid.reshape((grid.shape[0], -1))).reshape([-1] + list(grid.shape[1:])) + return grid, affine + + +class RandAffineGrid(Randomizable, Transform): + """ + Generate randomised affine grid. + + """ + + backend = AffineGrid.backend + + @deprecated_arg(name="as_tensor_output", since="0.6") + def __init__( + self, + rotate_range: RandRange = None, + shear_range: RandRange = None, + translate_range: RandRange = None, + scale_range: RandRange = None, + as_tensor_output: bool = True, + device: Optional[torch.device] = None, + ) -> None: + """ + Args: + rotate_range: angle range in radians. If element `i` is a pair of (min, max) values, then + `uniform[-rotate_range[i][0], rotate_range[i][1])` will be used to generate the rotation parameter + for the `i`th spatial dimension. If not, `uniform[-rotate_range[i], rotate_range[i])` will be used. + This can be altered on a per-dimension basis. E.g., `((0,3), 1, ...)`: for dim0, rotation will be + in range `[0, 3]`, and for dim1 `[-1, 1]` will be used. Setting a single value will use `[-x, x]` + for dim0 and nothing for the remaining dimensions. + shear_range: shear range with format matching `rotate_range`, it defines the range to randomly select + shearing factors(a tuple of 2 floats for 2D, a tuple of 6 floats for 3D) for affine matrix, + take a 3D affine as example:: + + [ + [1.0, params[0], params[1], 0.0], + [params[2], 1.0, params[3], 0.0], + [params[4], params[5], 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ] + + translate_range: translate range with format matching `rotate_range`, it defines the range to randomly + select voxels to translate for every spatial dims. + scale_range: scaling range with format matching `rotate_range`. it defines the range to randomly select + the scale factor to translate for every spatial dims. A value of 1.0 is added to the result. + This allows 0 to correspond to no change (i.e., a scaling of 1.0). + device: device to store the output grid data. + + See also: + - :py:meth:`monai.transforms.utils.create_rotate` + - :py:meth:`monai.transforms.utils.create_shear` + - :py:meth:`monai.transforms.utils.create_translate` + - :py:meth:`monai.transforms.utils.create_scale` + + .. deprecated:: 0.6.0 + ``as_tensor_output`` is deprecated. + + """ + self.rotate_range = ensure_tuple(rotate_range) + self.shear_range = ensure_tuple(shear_range) + self.translate_range = ensure_tuple(translate_range) + self.scale_range = ensure_tuple(scale_range) + + self.rotate_params: Optional[List[float]] = None + self.shear_params: Optional[List[float]] = None + self.translate_params: Optional[List[float]] = None + self.scale_params: Optional[List[float]] = None + + self.device = device + self.affine: Optional[NdarrayOrTensor] = None + + def _get_rand_param(self, param_range, add_scalar: float = 0.0): + out_param = [] + for f in param_range: + if issequenceiterable(f): + if len(f) != 2: + raise ValueError("If giving range as [min,max], should only have two elements per dim.") + out_param.append(self.R.uniform(f[0], f[1]) + add_scalar) + elif f is not None: + out_param.append(self.R.uniform(-f, f) + add_scalar) + return out_param + + def randomize(self, data: Optional[Any] = None) -> None: + self.rotate_params = self._get_rand_param(self.rotate_range) + self.shear_params = self._get_rand_param(self.shear_range) + self.translate_params = self._get_rand_param(self.translate_range) + self.scale_params = self._get_rand_param(self.scale_range, 1.0) + + def __call__( + self, + spatial_size: Optional[Sequence[int]] = None, + grid: Optional[NdarrayOrTensor] = None, + randomize: bool = True, + ) -> NdarrayOrTensor: + """ + Args: + spatial_size: output grid size. + grid: grid to be transformed. Shape must be (3, H, W) for 2D or (4, H, W, D) for 3D. + randomize: boolean as to whether the grid parameters governing the grid + should be randomized. + + Returns: + a 2D (3xHxW) or 3D (4xHxWxD) grid. + """ + if randomize: + self.randomize() + affine_grid = AffineGrid( + rotate_params=self.rotate_params, + shear_params=self.shear_params, + translate_params=self.translate_params, + scale_params=self.scale_params, + device=self.device, + ) + _grid: NdarrayOrTensor + _grid, self.affine = affine_grid(spatial_size, grid) + return _grid + + def get_transformation_matrix(self) -> Optional[NdarrayOrTensor]: + """Get the most recently applied transformation matrix""" + return self.affine + + +class RandDeformGrid(Randomizable, Transform): + """ + Generate random deformation grid. + """ + + backend = [TransformBackends.TORCH] + + def __init__( + self, + spacing: Union[Sequence[float], float], + magnitude_range: Tuple[float, float], + as_tensor_output: bool = True, + device: Optional[torch.device] = None, + ) -> None: + """ + Args: + spacing: spacing of the grid in 2D or 3D. + e.g., spacing=(1, 1) indicates pixel-wise deformation in 2D, + spacing=(1, 1, 1) indicates voxel-wise deformation in 3D, + spacing=(2, 2) indicates deformation field defined on every other pixel in 2D. + magnitude_range: the random offsets will be generated from + `uniform[magnitude[0], magnitude[1])`. + as_tensor_output: whether to output tensor instead of numpy array. + defaults to True. + device: device to store the output grid data. + """ + self.spacing = spacing + self.magnitude = magnitude_range + + self.rand_mag = 1.0 + self.as_tensor_output = as_tensor_output + self.random_offset: np.ndarray + self.device = device + + def randomize(self, grid_size: Sequence[int]) -> None: + self.random_offset = self.R.normal(size=([len(grid_size)] + list(grid_size))).astype(np.float32, copy=False) + self.rand_mag = self.R.uniform(self.magnitude[0], self.magnitude[1]) + + def __call__(self, spatial_size: Sequence[int]): + """ + Args: + spatial_size: spatial size of the grid. + """ + self.spacing = fall_back_tuple(self.spacing, (1.0,) * len(spatial_size)) + control_grid = create_control_grid(spatial_size, self.spacing, device=self.device, backend="torch") + self.randomize(control_grid.shape[1:]) + _offset, *_ = convert_to_dst_type(self.rand_mag * self.random_offset, control_grid) + control_grid[: len(spatial_size)] += _offset + if not self.as_tensor_output: + control_grid, *_ = convert_data_type(control_grid, output_type=np.ndarray, dtype=np.float32) + return control_grid + + +class Resample(Transform): + + backend = [TransformBackends.TORCH] + + @deprecated_arg(name="as_tensor_output", since="0.6") + def __init__( + self, + mode: Union[GridSampleMode, str] = GridSampleMode.BILINEAR, + padding_mode: Union[GridSamplePadMode, str] = GridSamplePadMode.BORDER, + as_tensor_output: bool = True, + norm_coords: bool = True, + device: Optional[torch.device] = None, + dtype: DtypeLike = np.float64, + ) -> None: + """ + computes output image using values from `img`, locations from `grid` using pytorch. + supports spatially 2D or 3D (num_channels, H, W[, D]). + + Args: + mode: {``"bilinear"``, ``"nearest"``} + Interpolation mode to calculate output values. Defaults to ``"bilinear"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + padding_mode: {``"zeros"``, ``"border"``, ``"reflection"``} + Padding mode for outside grid values. Defaults to ``"border"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + When `USE_COMPILED` is `True`, this argument uses + ``"nearest"``, ``"bilinear"``, ``"bicubic"`` to indicate 0, 1, 3 order interpolations. + See also: https://docs.monai.io/en/stable/networks.html#grid-pull + norm_coords: whether to normalize the coordinates from `[-(size-1)/2, (size-1)/2]` to + `[0, size - 1]` (for ``monai/csrc`` implementation) or + `[-1, 1]` (for torch ``grid_sample`` implementation) to be compatible with the underlying + resampling API. + device: device on which the tensor will be allocated. + dtype: data type for resampling computation. Defaults to ``np.float64`` for best precision. + If ``None``, use the data type of input data. To be compatible with other modules, + the output data type is always `float32`. + + .. deprecated:: 0.6.0 + ``as_tensor_output`` is deprecated. + + """ + self.mode: GridSampleMode = look_up_option(mode, GridSampleMode) + self.padding_mode: GridSamplePadMode = look_up_option(padding_mode, GridSamplePadMode) + self.norm_coords = norm_coords + self.device = device + self.dtype = dtype + + def __call__( + self, + img: NdarrayOrTensor, + grid: Optional[NdarrayOrTensor] = None, + mode: Optional[Union[GridSampleMode, str]] = None, + padding_mode: Optional[Union[GridSamplePadMode, str]] = None, + dtype: DtypeLike = None, + ) -> NdarrayOrTensor: + """ + Args: + img: shape must be (num_channels, H, W[, D]). + grid: shape must be (3, H, W) for 2D or (4, H, W, D) for 3D. + if ``norm_coords`` is True, the grid values must be in `[-(size-1)/2, (size-1)/2]`. + if ``USE_COMPILED=True`` and ``norm_coords=False``, grid values must be in `[0, size-1]`. + if ``USE_COMPILED=False`` and ``norm_coords=False``, grid values must be in `[-1, 1]`. + mode: {``"bilinear"``, ``"nearest"``} + Interpolation mode to calculate output values. Defaults to ``self.mode``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + When `USE_COMPILED` is `True`, this argument uses + ``"nearest"``, ``"bilinear"``, ``"bicubic"`` to indicate 0, 1, 3 order interpolations. + See also: https://docs.monai.io/en/stable/networks.html#grid-pull + padding_mode: {``"zeros"``, ``"border"``, ``"reflection"``} + Padding mode for outside grid values. Defaults to ``self.padding_mode``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + dtype: data type for resampling computation. Defaults to ``self.dtype``. + To be compatible with other modules, the output data type is always `float32`. + + See also: + :py:const:`monai.config.USE_COMPILED` + """ + if grid is None: + raise ValueError("Unknown grid.") + _device = img.device if isinstance(img, torch.Tensor) else self.device + _dtype = dtype or self.dtype or img.dtype + img_t, *_ = convert_data_type(img, torch.Tensor, device=_device, dtype=_dtype) + grid_t = convert_to_dst_type(grid, img_t)[0] + if grid_t is grid: # copy if needed (convert_data_type converts to contiguous) + grid_t = grid_t.clone(memory_format=torch.contiguous_format) + sr = min(len(img_t.shape[1:]), 3) + + if USE_COMPILED: + if self.norm_coords: + for i, dim in enumerate(img_t.shape[1 : 1 + sr]): + grid_t[i] = (max(dim, 2) / 2.0 - 0.5 + grid_t[i]) / grid_t[-1:] + grid_t = moveaxis(grid_t[:sr], 0, -1) # type: ignore + _padding_mode = self.padding_mode if padding_mode is None else padding_mode + _padding_mode = _padding_mode.value if isinstance(_padding_mode, GridSamplePadMode) else _padding_mode + bound = 1 if _padding_mode == "reflection" else _padding_mode + _interp_mode = self.mode if mode is None else mode + _interp_mode = _interp_mode.value if isinstance(_interp_mode, GridSampleMode) else _interp_mode + if _interp_mode == "bicubic": + interp = 3 + elif _interp_mode == "bilinear": + interp = 1 + else: + interp = _interp_mode # type: ignore + out = grid_pull( + img_t.unsqueeze(0), grid_t.unsqueeze(0), bound=bound, extrapolate=True, interpolation=interp + )[0] + else: + if self.norm_coords: + for i, dim in enumerate(img_t.shape[1 : 1 + sr]): + grid_t[i] = 2.0 / (max(2, dim) - 1.0) * grid_t[i] / grid_t[-1:] + index_ordering: List[int] = list(range(sr - 1, -1, -1)) + grid_t = moveaxis(grid_t[index_ordering], 0, -1) # type: ignore + out = torch.nn.functional.grid_sample( + img_t.unsqueeze(0), + grid_t.unsqueeze(0), + mode=self.mode.value if mode is None else GridSampleMode(mode).value, + padding_mode=self.padding_mode.value if padding_mode is None else GridSamplePadMode(padding_mode).value, + align_corners=True, + )[0] + out_val, *_ = convert_to_dst_type(out, dst=img, dtype=np.float32) + return out_val + + +class Affine(Transform): + """ + Transform ``img`` given the affine parameters. + A tutorial is available: https://github.com/Project-MONAI/tutorials/blob/0.6.0/modules/transforms_demo_2d.ipynb. + + """ + + backend = list(set(AffineGrid.backend) & set(Resample.backend)) + + @deprecated_arg(name="as_tensor_output", since="0.6") + @deprecated_arg(name="norm_coords", since="0.8") + def __init__( + self, + rotate_params: Optional[Union[Sequence[float], float]] = None, + shear_params: Optional[Union[Sequence[float], float]] = None, + translate_params: Optional[Union[Sequence[float], float]] = None, + scale_params: Optional[Union[Sequence[float], float]] = None, + affine: Optional[NdarrayOrTensor] = None, + spatial_size: Optional[Union[Sequence[int], int]] = None, + mode: Union[GridSampleMode, str] = GridSampleMode.BILINEAR, + padding_mode: Union[GridSamplePadMode, str] = GridSamplePadMode.REFLECTION, + normalized: bool = False, + norm_coords: bool = True, + as_tensor_output: bool = True, + device: Optional[torch.device] = None, + dtype: DtypeLike = np.float32, + image_only: bool = False, + ) -> None: + """ + The affine transformations are applied in rotate, shear, translate, scale order. + + Args: + rotate_params: a rotation angle in radians, a scalar for 2D image, a tuple of 3 floats for 3D. + Defaults to no rotation. + shear_params: shearing factors for affine matrix, take a 3D affine as example:: + + [ + [1.0, params[0], params[1], 0.0], + [params[2], 1.0, params[3], 0.0], + [params[4], params[5], 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ] + + a tuple of 2 floats for 2D, a tuple of 6 floats for 3D. Defaults to no shearing. + translate_params: a tuple of 2 floats for 2D, a tuple of 3 floats for 3D. Translation is in + pixel/voxel relative to the center of the input image. Defaults to no translation. + scale_params: scale factor for every spatial dims. a tuple of 2 floats for 2D, + a tuple of 3 floats for 3D. Defaults to `1.0`. + affine: If applied, ignore the params (`rotate_params`, etc.) and use the + supplied matrix. Should be square with each side = num of image spatial + dimensions + 1. + spatial_size: output image spatial size. + if `spatial_size` and `self.spatial_size` are not defined, or smaller than 1, + the transform will use the spatial size of `img`. + if some components of the `spatial_size` are non-positive values, the transform will use the + corresponding components of img size. For example, `spatial_size=(32, -1)` will be adapted + to `(32, 64)` if the second spatial dimension size of img is `64`. + mode: {``"bilinear"``, ``"nearest"``} + Interpolation mode to calculate output values. Defaults to ``"bilinear"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + When `USE_COMPILED` is `True`, this argument uses + ``"nearest"``, ``"bilinear"``, ``"bicubic"`` to indicate 0, 1, 3 order interpolations. + See also: https://docs.monai.io/en/stable/networks.html#grid-pull + padding_mode: {``"zeros"``, ``"border"``, ``"reflection"``} + Padding mode for outside grid values. Defaults to ``"reflection"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + normalized: indicating whether the provided `affine` is defined to include a normalization + transform converting the coordinates from `[-(size-1)/2, (size-1)/2]` (defined in ``create_grid``) to + `[0, size - 1]` or `[-1, 1]` in order to be compatible with the underlying resampling API. + If `normalized=False`, additional coordinate normalization will be applied before resampling. + See also: :py:func:`monai.networks.utils.normalize_transform`. + device: device on which the tensor will be allocated. + dtype: data type for resampling computation. Defaults to ``np.float32``. + If ``None``, use the data type of input data. To be compatible with other modules, + the output data type is always `float32`. + image_only: if True return only the image volume, otherwise return (image, affine). + + .. deprecated:: 0.6.0 + ``as_tensor_output`` is deprecated. + .. deprecated:: 0.8.1 + ``norm_coords`` is deprecated, please use ``normalized`` instead + (the new flag is a negation, i.e., ``norm_coords == not normalized``). + + """ + self.affine_grid = AffineGrid( + rotate_params=rotate_params, + shear_params=shear_params, + translate_params=translate_params, + scale_params=scale_params, + affine=affine, + dtype=dtype, + device=device, + ) + self.image_only = image_only + self.resampler = Resample(norm_coords=not normalized, device=device, dtype=dtype) + self.spatial_size = spatial_size + self.mode: GridSampleMode = look_up_option(mode, GridSampleMode) + self.padding_mode: GridSamplePadMode = look_up_option(padding_mode, GridSamplePadMode) + + def __call__( + self, + img: NdarrayOrTensor, + spatial_size: Optional[Union[Sequence[int], int]] = None, + mode: Optional[Union[GridSampleMode, str]] = None, + padding_mode: Optional[Union[GridSamplePadMode, str]] = None, + ) -> Union[NdarrayOrTensor, Tuple[NdarrayOrTensor, NdarrayOrTensor]]: + """ + Args: + img: shape must be (num_channels, H, W[, D]), + spatial_size: output image spatial size. + if `spatial_size` and `self.spatial_size` are not defined, or smaller than 1, + the transform will use the spatial size of `img`. + if `img` has two spatial dimensions, `spatial_size` should have 2 elements [h, w]. + if `img` has three spatial dimensions, `spatial_size` should have 3 elements [h, w, d]. + mode: {``"bilinear"``, ``"nearest"``} + Interpolation mode to calculate output values. Defaults to ``self.mode``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + When `USE_COMPILED` is `True`, this argument uses + ``"nearest"``, ``"bilinear"``, ``"bicubic"`` to indicate 0, 1, 3 order interpolations. + See also: https://docs.monai.io/en/stable/networks.html#grid-pull + padding_mode: {``"zeros"``, ``"border"``, ``"reflection"``} + Padding mode for outside grid values. Defaults to ``self.padding_mode``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + """ + sp_size = fall_back_tuple(self.spatial_size if spatial_size is None else spatial_size, img.shape[1:]) + grid, affine = self.affine_grid(spatial_size=sp_size) + ret = self.resampler(img, grid=grid, mode=mode or self.mode, padding_mode=padding_mode or self.padding_mode) + + return ret if self.image_only else (ret, affine) + + +class RandAffine(RandomizableTransform): + """ + Random affine transform. + A tutorial is available: https://github.com/Project-MONAI/tutorials/blob/0.6.0/modules/transforms_demo_2d.ipynb. + + """ + + backend = Affine.backend + + @deprecated_arg(name="as_tensor_output", since="0.6") + def __init__( + self, + prob: float = 0.1, + rotate_range: RandRange = None, + shear_range: RandRange = None, + translate_range: RandRange = None, + scale_range: RandRange = None, + spatial_size: Optional[Union[Sequence[int], int]] = None, + mode: Union[GridSampleMode, str] = GridSampleMode.BILINEAR, + padding_mode: Union[GridSamplePadMode, str] = GridSamplePadMode.REFLECTION, + cache_grid: bool = False, + as_tensor_output: bool = True, + device: Optional[torch.device] = None, + ) -> None: + """ + Args: + prob: probability of returning a randomized affine grid. + defaults to 0.1, with 10% chance returns a randomized grid. + rotate_range: angle range in radians. If element `i` is a pair of (min, max) values, then + `uniform[-rotate_range[i][0], rotate_range[i][1])` will be used to generate the rotation parameter + for the `i`th spatial dimension. If not, `uniform[-rotate_range[i], rotate_range[i])` will be used. + This can be altered on a per-dimension basis. E.g., `((0,3), 1, ...)`: for dim0, rotation will be + in range `[0, 3]`, and for dim1 `[-1, 1]` will be used. Setting a single value will use `[-x, x]` + for dim0 and nothing for the remaining dimensions. + shear_range: shear range with format matching `rotate_range`, it defines the range to randomly select + shearing factors(a tuple of 2 floats for 2D, a tuple of 6 floats for 3D) for affine matrix, + take a 3D affine as example:: + + [ + [1.0, params[0], params[1], 0.0], + [params[2], 1.0, params[3], 0.0], + [params[4], params[5], 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ] + + translate_range: translate range with format matching `rotate_range`, it defines the range to randomly + select pixel/voxel to translate for every spatial dims. + scale_range: scaling range with format matching `rotate_range`. it defines the range to randomly select + the scale factor to translate for every spatial dims. A value of 1.0 is added to the result. + This allows 0 to correspond to no change (i.e., a scaling of 1.0). + spatial_size: output image spatial size. + if `spatial_size` and `self.spatial_size` are not defined, or smaller than 1, + the transform will use the spatial size of `img`. + if some components of the `spatial_size` are non-positive values, the transform will use the + corresponding components of img size. For example, `spatial_size=(32, -1)` will be adapted + to `(32, 64)` if the second spatial dimension size of img is `64`. + mode: {``"bilinear"``, ``"nearest"``} + Interpolation mode to calculate output values. Defaults to ``"bilinear"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + padding_mode: {``"zeros"``, ``"border"``, ``"reflection"``} + Padding mode for outside grid values. Defaults to ``"reflection"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + cache_grid: whether to cache the identity sampling grid. + If the spatial size is not dynamically defined by input image, enabling this option could + accelerate the transform. + device: device on which the tensor will be allocated. + + See also: + - :py:class:`RandAffineGrid` for the random affine parameters configurations. + - :py:class:`Affine` for the affine transformation parameters configurations. + + .. deprecated:: 0.6.0 + ``as_tensor_output`` is deprecated. + + """ + RandomizableTransform.__init__(self, prob) + + self.rand_affine_grid = RandAffineGrid( + rotate_range=rotate_range, + shear_range=shear_range, + translate_range=translate_range, + scale_range=scale_range, + device=device, + ) + self.resampler = Resample(device=device) + + self.spatial_size = spatial_size + self.cache_grid = cache_grid + self._cached_grid = self._init_identity_cache() + self.mode: GridSampleMode = GridSampleMode(mode) + self.padding_mode: GridSamplePadMode = GridSamplePadMode(padding_mode) + + def _init_identity_cache(self): + """ + Create cache of the identity grid if cache_grid=True and spatial_size is known. + """ + if self.spatial_size is None: + if self.cache_grid: + warnings.warn( + "cache_grid=True is not compatible with the dynamic spatial_size, please specify 'spatial_size'." + ) + return None + _sp_size = ensure_tuple(self.spatial_size) + _ndim = len(_sp_size) + if _sp_size != fall_back_tuple(_sp_size, [1] * _ndim) or _sp_size != fall_back_tuple(_sp_size, [2] * _ndim): + # dynamic shape because it falls back to different outcomes + if self.cache_grid: + warnings.warn( + "cache_grid=True is not compatible with the dynamic spatial_size " + f"'spatial_size={self.spatial_size}', please specify 'spatial_size'." + ) + return None + return create_grid(spatial_size=_sp_size, device=self.rand_affine_grid.device, backend="torch") + + def get_identity_grid(self, spatial_size: Sequence[int]): + """ + Return a cached or new identity grid depends on the availability. + + Args: + spatial_size: non-dynamic spatial size + """ + ndim = len(spatial_size) + if spatial_size != fall_back_tuple(spatial_size, [1] * ndim) or spatial_size != fall_back_tuple( + spatial_size, [2] * ndim + ): + raise RuntimeError(f"spatial_size should not be dynamic, got {spatial_size}.") + return ( + create_grid(spatial_size=spatial_size, device=self.rand_affine_grid.device, backend="torch") + if self._cached_grid is None + else self._cached_grid + ) + + def set_random_state( + self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None + ) -> "RandAffine": + self.rand_affine_grid.set_random_state(seed, state) + super().set_random_state(seed, state) + return self + + def randomize(self, data: Optional[Any] = None) -> None: + super().randomize(None) + if not self._do_transform: + return None + self.rand_affine_grid.randomize() + + def __call__( + self, + img: NdarrayOrTensor, + spatial_size: Optional[Union[Sequence[int], int]] = None, + mode: Optional[Union[GridSampleMode, str]] = None, + padding_mode: Optional[Union[GridSamplePadMode, str]] = None, + randomize: bool = True, + ) -> NdarrayOrTensor: + """ + Args: + img: shape must be (num_channels, H, W[, D]), + spatial_size: output image spatial size. + if `spatial_size` and `self.spatial_size` are not defined, or smaller than 1, + the transform will use the spatial size of `img`. + if `img` has two spatial dimensions, `spatial_size` should have 2 elements [h, w]. + if `img` has three spatial dimensions, `spatial_size` should have 3 elements [h, w, d]. + mode: {``"bilinear"``, ``"nearest"``} + Interpolation mode to calculate output values. Defaults to ``self.mode``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + padding_mode: {``"zeros"``, ``"border"``, ``"reflection"``} + Padding mode for outside grid values. Defaults to ``self.padding_mode``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + randomize: whether to execute `randomize()` function first, default to True. + + """ + if randomize: + self.randomize() + + # if not doing transform and spatial size doesn't change, nothing to do + # except convert to float and device + sp_size = fall_back_tuple(self.spatial_size if spatial_size is None else spatial_size, img.shape[1:]) + do_resampling = self._do_transform or (sp_size != ensure_tuple(img.shape[1:])) + if not do_resampling: + img, *_ = convert_data_type(img, dtype=torch.float32, device=self.resampler.device) + return img + grid = self.get_identity_grid(sp_size) + if self._do_transform: + grid = self.rand_affine_grid(grid=grid, randomize=randomize) + out: NdarrayOrTensor = self.resampler( + img=img, grid=grid, mode=mode or self.mode, padding_mode=padding_mode or self.padding_mode + ) + return out + + +class Rand2DElastic(RandomizableTransform): + """ + Random elastic deformation and affine in 2D. + A tutorial is available: https://github.com/Project-MONAI/tutorials/blob/0.6.0/modules/transforms_demo_2d.ipynb. + + """ + + backend = Resample.backend + + @deprecated_arg(name="as_tensor_output", since="0.6") + def __init__( + self, + spacing: Union[Tuple[float, float], float], + magnitude_range: Tuple[float, float], + prob: float = 0.1, + rotate_range: RandRange = None, + shear_range: RandRange = None, + translate_range: RandRange = None, + scale_range: RandRange = None, + spatial_size: Optional[Union[Tuple[int, int], int]] = None, + mode: Union[GridSampleMode, str] = GridSampleMode.BILINEAR, + padding_mode: Union[GridSamplePadMode, str] = GridSamplePadMode.REFLECTION, + as_tensor_output: bool = False, + device: Optional[torch.device] = None, + ) -> None: + """ + Args: + spacing : distance in between the control points. + magnitude_range: the random offsets will be generated from ``uniform[magnitude[0], magnitude[1])``. + prob: probability of returning a randomized elastic transform. + defaults to 0.1, with 10% chance returns a randomized elastic transform, + otherwise returns a ``spatial_size`` centered area extracted from the input image. + rotate_range: angle range in radians. If element `i` is a pair of (min, max) values, then + `uniform[-rotate_range[i][0], rotate_range[i][1])` will be used to generate the rotation parameter + for the `i`th spatial dimension. If not, `uniform[-rotate_range[i], rotate_range[i])` will be used. + This can be altered on a per-dimension basis. E.g., `((0,3), 1, ...)`: for dim0, rotation will be + in range `[0, 3]`, and for dim1 `[-1, 1]` will be used. Setting a single value will use `[-x, x]` + for dim0 and nothing for the remaining dimensions. + shear_range: shear range with format matching `rotate_range`, it defines the range to randomly select + shearing factors(a tuple of 2 floats for 2D) for affine matrix, take a 2D affine as example:: + + [ + [1.0, params[0], 0.0], + [params[1], 1.0, 0.0], + [0.0, 0.0, 1.0], + ] + + translate_range: translate range with format matching `rotate_range`, it defines the range to randomly + select pixel to translate for every spatial dims. + scale_range: scaling range with format matching `rotate_range`. it defines the range to randomly select + the scale factor to translate for every spatial dims. A value of 1.0 is added to the result. + This allows 0 to correspond to no change (i.e., a scaling of 1.0). + spatial_size: specifying output image spatial size [h, w]. + if `spatial_size` and `self.spatial_size` are not defined, or smaller than 1, + the transform will use the spatial size of `img`. + if some components of the `spatial_size` are non-positive values, the transform will use the + corresponding components of img size. For example, `spatial_size=(32, -1)` will be adapted + to `(32, 64)` if the second spatial dimension size of img is `64`. + mode: {``"bilinear"``, ``"nearest"``} + Interpolation mode to calculate output values. Defaults to ``"bilinear"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + padding_mode: {``"zeros"``, ``"border"``, ``"reflection"``} + Padding mode for outside grid values. Defaults to ``"reflection"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + device: device on which the tensor will be allocated. + + See also: + - :py:class:`RandAffineGrid` for the random affine parameters configurations. + - :py:class:`Affine` for the affine transformation parameters configurations. + + .. deprecated:: 0.6.0 + ``as_tensor_output`` is deprecated. + + """ + RandomizableTransform.__init__(self, prob) + self.deform_grid = RandDeformGrid( + spacing=spacing, magnitude_range=magnitude_range, as_tensor_output=True, device=device + ) + self.rand_affine_grid = RandAffineGrid( + rotate_range=rotate_range, + shear_range=shear_range, + translate_range=translate_range, + scale_range=scale_range, + device=device, + ) + self.resampler = Resample(device=device) + + self.device = device + self.spatial_size = spatial_size + self.mode: GridSampleMode = look_up_option(mode, GridSampleMode) + self.padding_mode: GridSamplePadMode = look_up_option(padding_mode, GridSamplePadMode) + + def set_random_state( + self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None + ) -> "Rand2DElastic": + self.deform_grid.set_random_state(seed, state) + self.rand_affine_grid.set_random_state(seed, state) + super().set_random_state(seed, state) + return self + + def randomize(self, spatial_size: Sequence[int]) -> None: + super().randomize(None) + if not self._do_transform: + return None + self.deform_grid.randomize(spatial_size) + self.rand_affine_grid.randomize() + + def __call__( + self, + img: NdarrayOrTensor, + spatial_size: Optional[Union[Tuple[int, int], int]] = None, + mode: Optional[Union[GridSampleMode, str]] = None, + padding_mode: Optional[Union[GridSamplePadMode, str]] = None, + randomize: bool = True, + ) -> NdarrayOrTensor: + """ + Args: + img: shape must be (num_channels, H, W), + spatial_size: specifying output image spatial size [h, w]. + if `spatial_size` and `self.spatial_size` are not defined, or smaller than 1, + the transform will use the spatial size of `img`. + mode: {``"bilinear"``, ``"nearest"``} + Interpolation mode to calculate output values. Defaults to ``self.mode``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + padding_mode: {``"zeros"``, ``"border"``, ``"reflection"``} + Padding mode for outside grid values. Defaults to ``self.padding_mode``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + randomize: whether to execute `randomize()` function first, default to True. + """ + sp_size = fall_back_tuple(self.spatial_size if spatial_size is None else spatial_size, img.shape[1:]) + if randomize: + self.randomize(spatial_size=sp_size) + + if self._do_transform: + grid = self.deform_grid(spatial_size=sp_size) + grid = self.rand_affine_grid(grid=grid) + grid = torch.nn.functional.interpolate( # type: ignore + recompute_scale_factor=True, + input=grid.unsqueeze(0), + scale_factor=list(ensure_tuple(self.deform_grid.spacing)), + mode=InterpolateMode.BICUBIC.value, + align_corners=False, + ) + grid = CenterSpatialCrop(roi_size=sp_size)(grid[0]) + else: + _device = img.device if isinstance(img, torch.Tensor) else self.device + grid = create_grid(spatial_size=sp_size, device=_device, backend="torch") + out: NdarrayOrTensor = self.resampler( + img, grid, mode=mode or self.mode, padding_mode=padding_mode or self.padding_mode + ) + return out + + +class Rand3DElastic(RandomizableTransform): + """ + Random elastic deformation and affine in 3D. + A tutorial is available: https://github.com/Project-MONAI/tutorials/blob/0.6.0/modules/transforms_demo_2d.ipynb. + + """ + + backend = Resample.backend + + @deprecated_arg(name="as_tensor_output", since="0.6") + def __init__( + self, + sigma_range: Tuple[float, float], + magnitude_range: Tuple[float, float], + prob: float = 0.1, + rotate_range: RandRange = None, + shear_range: RandRange = None, + translate_range: RandRange = None, + scale_range: RandRange = None, + spatial_size: Optional[Union[Tuple[int, int, int], int]] = None, + mode: Union[GridSampleMode, str] = GridSampleMode.BILINEAR, + padding_mode: Union[GridSamplePadMode, str] = GridSamplePadMode.REFLECTION, + as_tensor_output: bool = False, + device: Optional[torch.device] = None, + ) -> None: + """ + Args: + sigma_range: a Gaussian kernel with standard deviation sampled from + ``uniform[sigma_range[0], sigma_range[1])`` will be used to smooth the random offset grid. + magnitude_range: the random offsets on the grid will be generated from + ``uniform[magnitude[0], magnitude[1])``. + prob: probability of returning a randomized elastic transform. + defaults to 0.1, with 10% chance returns a randomized elastic transform, + otherwise returns a ``spatial_size`` centered area extracted from the input image. + rotate_range: angle range in radians. If element `i` is a pair of (min, max) values, then + `uniform[-rotate_range[i][0], rotate_range[i][1])` will be used to generate the rotation parameter + for the `i`th spatial dimension. If not, `uniform[-rotate_range[i], rotate_range[i])` will be used. + This can be altered on a per-dimension basis. E.g., `((0,3), 1, ...)`: for dim0, rotation will be + in range `[0, 3]`, and for dim1 `[-1, 1]` will be used. Setting a single value will use `[-x, x]` + for dim0 and nothing for the remaining dimensions. + shear_range: shear range with format matching `rotate_range`, it defines the range to randomly select + shearing factors(a tuple of 6 floats for 3D) for affine matrix, take a 3D affine as example:: + + [ + [1.0, params[0], params[1], 0.0], + [params[2], 1.0, params[3], 0.0], + [params[4], params[5], 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ] + + translate_range: translate range with format matching `rotate_range`, it defines the range to randomly + select voxel to translate for every spatial dims. + scale_range: scaling range with format matching `rotate_range`. it defines the range to randomly select + the scale factor to translate for every spatial dims. A value of 1.0 is added to the result. + This allows 0 to correspond to no change (i.e., a scaling of 1.0). + spatial_size: specifying output image spatial size [h, w, d]. + if `spatial_size` and `self.spatial_size` are not defined, or smaller than 1, + the transform will use the spatial size of `img`. + if some components of the `spatial_size` are non-positive values, the transform will use the + corresponding components of img size. For example, `spatial_size=(32, 32, -1)` will be adapted + to `(32, 32, 64)` if the third spatial dimension size of img is `64`. + mode: {``"bilinear"``, ``"nearest"``} + Interpolation mode to calculate output values. Defaults to ``"bilinear"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + padding_mode: {``"zeros"``, ``"border"``, ``"reflection"``} + Padding mode for outside grid values. Defaults to ``"reflection"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + device: device on which the tensor will be allocated. + + See also: + - :py:class:`RandAffineGrid` for the random affine parameters configurations. + - :py:class:`Affine` for the affine transformation parameters configurations. + + .. deprecated:: 0.6.0 + ``as_tensor_output`` is deprecated. + + """ + RandomizableTransform.__init__(self, prob) + self.rand_affine_grid = RandAffineGrid( + rotate_range=rotate_range, + shear_range=shear_range, + translate_range=translate_range, + scale_range=scale_range, + device=device, + ) + self.resampler = Resample(device=device) + + self.sigma_range = sigma_range + self.magnitude_range = magnitude_range + self.spatial_size = spatial_size + self.mode: GridSampleMode = look_up_option(mode, GridSampleMode) + self.padding_mode: GridSamplePadMode = look_up_option(padding_mode, GridSamplePadMode) + self.device = device + + self.rand_offset: np.ndarray + self.magnitude = 1.0 + self.sigma = 1.0 + + def set_random_state( + self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None + ) -> "Rand3DElastic": + self.rand_affine_grid.set_random_state(seed, state) + super().set_random_state(seed, state) + return self + + def randomize(self, grid_size: Sequence[int]) -> None: + super().randomize(None) + if not self._do_transform: + return None + self.rand_offset = self.R.uniform(-1.0, 1.0, [3] + list(grid_size)).astype(np.float32, copy=False) + self.magnitude = self.R.uniform(self.magnitude_range[0], self.magnitude_range[1]) + self.sigma = self.R.uniform(self.sigma_range[0], self.sigma_range[1]) + self.rand_affine_grid.randomize() + + def __call__( + self, + img: NdarrayOrTensor, + spatial_size: Optional[Union[Tuple[int, int, int], int]] = None, + mode: Optional[Union[GridSampleMode, str]] = None, + padding_mode: Optional[Union[GridSamplePadMode, str]] = None, + randomize: bool = True, + ) -> NdarrayOrTensor: + """ + Args: + img: shape must be (num_channels, H, W, D), + spatial_size: specifying spatial 3D output image spatial size [h, w, d]. + if `spatial_size` and `self.spatial_size` are not defined, or smaller than 1, + the transform will use the spatial size of `img`. + mode: {``"bilinear"``, ``"nearest"``} + Interpolation mode to calculate output values. Defaults to ``self.mode``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + padding_mode: {``"zeros"``, ``"border"``, ``"reflection"``} + Padding mode for outside grid values. Defaults to ``self.padding_mode``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + randomize: whether to execute `randomize()` function first, default to True. + """ + sp_size = fall_back_tuple(self.spatial_size if spatial_size is None else spatial_size, img.shape[1:]) + if randomize: + self.randomize(grid_size=sp_size) + + _device = img.device if isinstance(img, torch.Tensor) else self.device + grid = create_grid(spatial_size=sp_size, device=_device, backend="torch") + if self._do_transform: + if self.rand_offset is None: + raise RuntimeError("rand_offset is not initialized.") + gaussian = GaussianFilter(3, self.sigma, 3.0).to(device=_device) + offset = torch.as_tensor(self.rand_offset, device=_device).unsqueeze(0) + grid[:3] += gaussian(offset)[0] * self.magnitude + grid = self.rand_affine_grid(grid=grid) + out: NdarrayOrTensor = self.resampler( + img, grid, mode=mode or self.mode, padding_mode=padding_mode or self.padding_mode + ) + return out + + +class GridDistortion(Transform): + + backend = [TransformBackends.TORCH] + + def __init__( + self, + num_cells: Union[Tuple[int], int], + distort_steps: Sequence[Sequence[float]], + mode: Union[GridSampleMode, str] = GridSampleMode.BILINEAR, + padding_mode: Union[GridSamplePadMode, str] = GridSamplePadMode.BORDER, + device: Optional[torch.device] = None, + ) -> None: + """ + Grid distortion transform. Refer to: + https://github.com/albumentations-team/albumentations/blob/master/albumentations/augmentations/transforms.py + + Args: + num_cells: number of grid cells on each dimension. + distort_steps: This argument is a list of tuples, where each tuple contains the distort steps of the + corresponding dimensions (in the order of H, W[, D]). The length of each tuple equals to `num_cells + 1`. + Each value in the tuple represents the distort step of the related cell. + mode: {``"bilinear"``, ``"nearest"``} + Interpolation mode to calculate output values. Defaults to ``"bilinear"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + padding_mode: {``"zeros"``, ``"border"``, ``"reflection"``} + Padding mode for outside grid values. Defaults to ``"border"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + device: device on which the tensor will be allocated. + + """ + self.resampler = Resample(mode=mode, padding_mode=padding_mode, device=device) + self.num_cells = num_cells + self.distort_steps = distort_steps + self.device = device + + def __call__( + self, + img: NdarrayOrTensor, + distort_steps: Optional[Sequence[Sequence]] = None, + mode: Optional[Union[GridSampleMode, str]] = None, + padding_mode: Optional[Union[GridSamplePadMode, str]] = None, + ) -> NdarrayOrTensor: + """ + Args: + img: shape must be (num_channels, H, W[, D]). + distort_steps: This argument is a list of tuples, where each tuple contains the distort steps of the + corresponding dimensions (in the order of H, W[, D]). The length of each tuple equals to `num_cells + 1`. + Each value in the tuple represents the distort step of the related cell. + mode: {``"bilinear"``, ``"nearest"``} + Interpolation mode to calculate output values. Defaults to ``"bilinear"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + padding_mode: {``"zeros"``, ``"border"``, ``"reflection"``} + Padding mode for outside grid values. Defaults to ``"border"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + + """ + distort_steps = self.distort_steps if distort_steps is None else distort_steps + if len(img.shape) != len(distort_steps) + 1: + raise ValueError("the spatial size of `img` does not match with the length of `distort_steps`") + + all_ranges = [] + num_cells = ensure_tuple_rep(self.num_cells, len(img.shape) - 1) + for dim_idx, dim_size in enumerate(img.shape[1:]): + dim_distort_steps = distort_steps[dim_idx] + ranges = torch.zeros(dim_size, dtype=torch.float32) + cell_size = dim_size // num_cells[dim_idx] + prev = 0 + for idx in range(num_cells[dim_idx] + 1): + start = int(idx * cell_size) + end = start + cell_size + if end > dim_size: + end = dim_size + cur = dim_size + else: + cur = prev + cell_size * dim_distort_steps[idx] + ranges[start:end] = torch.linspace(prev, cur, end - start) + prev = cur + ranges = ranges - (dim_size - 1.0) / 2.0 + all_ranges.append(ranges) + + coords = meshgrid_ij(*all_ranges) + grid = torch.stack([*coords, torch.ones_like(coords[0])]) + + return self.resampler(img, grid=grid, mode=mode, padding_mode=padding_mode) # type: ignore + + +class RandGridDistortion(RandomizableTransform): + + backend = [TransformBackends.TORCH] + + def __init__( + self, + num_cells: Union[Tuple[int], int] = 5, + prob: float = 0.1, + distort_limit: Union[Tuple[float, float], float] = (-0.03, 0.03), + mode: Union[GridSampleMode, str] = GridSampleMode.BILINEAR, + padding_mode: Union[GridSamplePadMode, str] = GridSamplePadMode.BORDER, + device: Optional[torch.device] = None, + ) -> None: + """ + Random grid distortion transform. Refer to: + https://github.com/albumentations-team/albumentations/blob/master/albumentations/augmentations/transforms.py + + Args: + num_cells: number of grid cells on each dimension. + prob: probability of returning a randomized grid distortion transform. Defaults to 0.1. + distort_limit: range to randomly distort. + If single number, distort_limit is picked from (-distort_limit, distort_limit). + Defaults to (-0.03, 0.03). + mode: {``"bilinear"``, ``"nearest"``} + Interpolation mode to calculate output values. Defaults to ``"bilinear"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + padding_mode: {``"zeros"``, ``"border"``, ``"reflection"``} + Padding mode for outside grid values. Defaults to ``"border"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + device: device on which the tensor will be allocated. + + """ + RandomizableTransform.__init__(self, prob) + self.num_cells = num_cells + if isinstance(distort_limit, (int, float)): + self.distort_limit = (min(-distort_limit, distort_limit), max(-distort_limit, distort_limit)) + else: + self.distort_limit = (min(distort_limit), max(distort_limit)) + self.distort_steps: Sequence[Sequence[float]] = ((1.0,),) + self.grid_distortion = GridDistortion( + num_cells=num_cells, distort_steps=self.distort_steps, mode=mode, padding_mode=padding_mode, device=device + ) + + def randomize(self, spatial_shape: Sequence[int]) -> None: + super().randomize(None) + if not self._do_transform: + return + self.distort_steps = tuple( + tuple(1.0 + self.R.uniform(low=self.distort_limit[0], high=self.distort_limit[1], size=n_cells + 1)) + for n_cells in ensure_tuple_rep(self.num_cells, len(spatial_shape)) + ) + + def __call__( + self, + img: NdarrayOrTensor, + mode: Optional[Union[GridSampleMode, str]] = None, + padding_mode: Optional[Union[GridSamplePadMode, str]] = None, + randomize: bool = True, + ) -> NdarrayOrTensor: + """ + Args: + img: shape must be (num_channels, H, W[, D]). + mode: {``"bilinear"``, ``"nearest"``} + Interpolation mode to calculate output values. Defaults to ``"bilinear"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + padding_mode: {``"zeros"``, ``"border"``, ``"reflection"``} + Padding mode for outside grid values. Defaults to ``"border"``. + See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html + randomize: whether to shuffle the random factors using `randomize()`, default to True. + """ + if randomize: + self.randomize(img.shape[1:]) + if not self._do_transform: + return img + return self.grid_distortion(img, distort_steps=self.distort_steps, mode=mode, padding_mode=padding_mode) + + +class GridSplit(Transform): + """ + Split the image into patches based on the provided grid in 2D. + + Args: + grid: a tuple define the shape of the grid upon which the image is split. Defaults to (2, 2) + size: a tuple or an integer that defines the output patch sizes. + If it's an integer, the value will be repeated for each dimension. + The default is None, where the patch size will be inferred from the grid shape. + + Example: + Given an image (torch.Tensor or numpy.ndarray) with size of (3, 10, 10) and a grid of (2, 2), + it will return a Tensor or array with the size of (4, 3, 5, 5). + Here, if the `size` is provided, the returned shape will be (4, 3, size, size) + + Note: This transform currently support only image with two spatial dimensions. + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + def __init__(self, grid: Tuple[int, int] = (2, 2), size: Optional[Union[int, Tuple[int, int]]] = None): + # Grid size + self.grid = grid + + # Patch size + self.size = None if size is None else ensure_tuple_rep(size, len(self.grid)) + + def __call__( + self, image: NdarrayOrTensor, size: Optional[Union[int, Tuple[int, int], np.ndarray]] = None + ) -> List[NdarrayOrTensor]: + input_size = self.size if size is None else ensure_tuple_rep(size, len(self.grid)) + + if self.grid == (1, 1) and input_size is None: + return [image] + + split_size, steps = self._get_params(image.shape[1:], input_size) + patches: List[NdarrayOrTensor] + if isinstance(image, torch.Tensor): + unfolded_image = ( + image.unfold(1, split_size[0], steps[0]) + .unfold(2, split_size[1], steps[1]) + .flatten(1, 2) + .transpose(0, 1) + ) + # Make a list of contiguous patches + patches = [p.contiguous() for p in unfolded_image] + elif isinstance(image, np.ndarray): + x_step, y_step = steps + c_stride, x_stride, y_stride = image.strides + n_channels = image.shape[0] + strided_image = as_strided( + image, + shape=(*self.grid, n_channels, split_size[0], split_size[1]), + strides=(x_stride * x_step, y_stride * y_step, c_stride, x_stride, y_stride), + ) + # Flatten the first two dimensions + strided_image = strided_image.reshape(-1, *strided_image.shape[2:]) + # Make a list of contiguous patches + patches = [np.ascontiguousarray(p) for p in strided_image] + else: + raise ValueError(f"Input type [{type(image)}] is not supported.") + + return patches + + def _get_params( + self, image_size: Union[Sequence[int], np.ndarray], size: Optional[Union[Sequence[int], np.ndarray]] = None + ): + """ + Calculate the size and step required for splitting the image + Args: + The size of the input image + """ + if size is None: + # infer each sub-image size from the image size and the grid + size = tuple(image_size[i] // self.grid[i] for i in range(len(self.grid))) + + if any(size[i] > image_size[i] for i in range(len(self.grid))): + raise ValueError(f"The image size ({image_size})is smaller than the requested split size ({size})") + + steps = tuple( + (image_size[i] - size[i]) // (self.grid[i] - 1) if self.grid[i] > 1 else image_size[i] + for i in range(len(self.grid)) + ) + + return size, steps + + +class GridPatch(Transform): + """ + Extract all the patches sweeping the entire image in a row-major sliding-window manner with possible overlaps. + It can sort the patches and return all or a subset of them. + + Args: + patch_size: size of patches to generate slices for, 0 or None selects whole dimension + offset: offset of starting position in the array, default is 0 for each dimension. + num_patches: number of patches to return. Defaults to None, which returns all the available patches. + overlap: the amount of overlap of neighboring patches in each dimension (a value between 0.0 and 1.0). + If only one float number is given, it will be applied to all dimensions. Defaults to 0.0. + sort_fn: when `num_patches` is provided, it determines if keep patches with highest values (`"max"`), + lowest values (`"min"`), or in their default order (`None`). Default to None. + threshold: a value to keep only the patches whose sum of intensities are less than the threshold. + Defaults to no filtering. + pad_mode: refer to NumpyPadMode and PytorchPadMode. If None, no padding will be applied. Defaults to ``"constant"``. + pad_kwargs: other arguments for the `np.pad` or `torch.pad` function. + + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + def __init__( + self, + patch_size: Sequence[int], + offset: Optional[Sequence[int]] = None, + num_patches: Optional[int] = None, + overlap: Union[Sequence[float], float] = 0.0, + sort_fn: Optional[str] = None, + threshold: Optional[float] = None, + pad_mode: Union[NumpyPadMode, PytorchPadMode, str] = NumpyPadMode.CONSTANT, + **pad_kwargs, + ): + self.patch_size = ensure_tuple(patch_size) + self.offset = ensure_tuple(offset) if offset else (0,) * len(self.patch_size) + self.pad_mode: Optional[NumpyPadMode] = convert_pad_mode(dst=np.zeros(1), mode=pad_mode) if pad_mode else None + self.pad_kwargs = pad_kwargs + self.overlap = overlap + self.num_patches = num_patches + self.sort_fn = sort_fn.lower() if sort_fn else None + self.threshold = threshold + + def filter_threshold(self, image_np: np.ndarray, locations: np.ndarray): + """ + Filter the patches and their locations according to a threshold + Args: + image: a numpy.ndarray representing a stack of patches + location: a numpy.ndarray representing the stack of location of each patch + """ + if self.threshold is not None: + n_dims = len(image_np.shape) + idx = np.argwhere(image_np.sum(axis=tuple(range(1, n_dims))) < self.threshold).reshape(-1) + image_np = image_np[idx] + locations = locations[idx] + return image_np, locations + + def filter_count(self, image_np: np.ndarray, locations: np.ndarray): + """ + Sort the patches based on the sum of their intensity, and just keep `self.num_patches` of them. + Args: + image: a numpy.ndarray representing a stack of patches + location: a numpy.ndarray representing the stack of location of each patch + """ + if self.sort_fn is None: + image_np = image_np[: self.num_patches] + locations = locations[: self.num_patches] + elif self.num_patches is not None: + n_dims = len(image_np.shape) + if self.sort_fn == GridPatchSort.MIN: + idx = np.argsort(image_np.sum(axis=tuple(range(1, n_dims)))) + elif self.sort_fn == GridPatchSort.MAX: + idx = np.argsort(-image_np.sum(axis=tuple(range(1, n_dims)))) + else: + raise ValueError(f'`sort_fn` should be either "min", "max" or None! {self.sort_fn} provided!') + idx = idx[: self.num_patches] + image_np = image_np[idx] + locations = locations[idx] + return image_np, locations + + def __call__(self, array: NdarrayOrTensor): + # create the patch iterator which sweeps the image row-by-row + array_np, *_ = convert_data_type(array, np.ndarray) + patch_iterator = iter_patch( + array_np, + patch_size=(None,) + self.patch_size, # expand to have the channel dim + start_pos=(0,) + self.offset, # expand to have the channel dim + overlap=self.overlap, + copy_back=False, + mode=self.pad_mode, + **self.pad_kwargs, + ) + patches = list(zip(*patch_iterator)) + patched_image = np.array(patches[0]) + locations = np.array(patches[1])[:, 1:, 0] # only keep the starting location + + # Filter patches + if self.num_patches: + patched_image, locations = self.filter_count(patched_image, locations) + elif self.threshold: + patched_image, locations = self.filter_threshold(patched_image, locations) + + # Convert to original data type + output = list( + zip(convert_to_dst_type(src=patched_image, dst=array)[0], convert_to_dst_type(src=locations, dst=array)[0]) + ) + + # Pad the patch list to have the requested number of patches + if self.num_patches and len(output) < self.num_patches: + patch = convert_to_dst_type( + src=np.full((array.shape[0], *self.patch_size), self.pad_kwargs.get("constant_values", 0)), dst=array + )[0] + start_location = convert_to_dst_type(src=np.zeros((len(self.patch_size), 1)), dst=array)[0] + output += [(patch, start_location)] * (self.num_patches - len(output)) + + return output + + +class RandGridPatch(GridPatch, RandomizableTransform): + """ + Extract all the patches sweeping the entire image in a row-major sliding-window manner with possible overlaps, + and with random offset for the minimal corner of the image, (0,0) for 2D and (0,0,0) for 3D. + It can sort the patches and return all or a subset of them. + + Args: + patch_size: size of patches to generate slices for, 0 or None selects whole dimension + min_offset: the minimum range of offset to be selected randomly. Defaults to 0. + max_offset: the maximum range of offset to be selected randomly. + Defaults to image size modulo patch size. + num_patches: number of patches to return. Defaults to None, which returns all the available patches. + overlap: the amount of overlap of neighboring patches in each dimension (a value between 0.0 and 1.0). + If only one float number is given, it will be applied to all dimensions. Defaults to 0.0. + sort_fn: when `num_patches` is provided, it determines if keep patches with highest values (`"max"`), + lowest values (`"min"`), or in their default order (`None`). Default to None. + threshold: a value to keep only the patches whose sum of intensities are less than the threshold. + Defaults to no filtering. + pad_mode: refer to NumpyPadMode and PytorchPadMode. If None, no padding will be applied. Defaults to ``"constant"``. + pad_kwargs: other arguments for the `np.pad` or `torch.pad` function. + + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + def __init__( + self, + patch_size: Sequence[int], + min_offset: Optional[Union[Sequence[int], int]] = None, + max_offset: Optional[Union[Sequence[int], int]] = None, + num_patches: Optional[int] = None, + overlap: Union[Sequence[float], float] = 0.0, + sort_fn: Optional[str] = None, + threshold: Optional[float] = None, + pad_mode: Union[NumpyPadMode, PytorchPadMode, str] = NumpyPadMode.CONSTANT, + **pad_kwargs, + ): + super().__init__( + patch_size=patch_size, + offset=(), + num_patches=num_patches, + overlap=overlap, + sort_fn=sort_fn, + threshold=threshold, + pad_mode=pad_mode, + **pad_kwargs, + ) + self.min_offset = min_offset + self.max_offset = max_offset + + def randomize(self, array): + if self.min_offset is None: + min_offset = (0,) * len(self.patch_size) + else: + min_offset = ensure_tuple_rep(self.min_offset, len(self.patch_size)) + if self.max_offset is None: + max_offset = tuple(s % p for s, p in zip(array.shape[1:], self.patch_size)) + else: + max_offset = ensure_tuple_rep(self.max_offset, len(self.patch_size)) + + self.offset = tuple(self.R.randint(low=low, high=high + 1) for low, high in zip(min_offset, max_offset)) + + def __call__(self, array: NdarrayOrTensor, randomize: bool = True): + if randomize: + self.randomize(array) + return super().__call__(array) diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/utility/__pycache__/array.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/utility/__pycache__/array.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef3a91d33692a1a85a32c5912cdb7f304170af8a Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/transforms/utility/__pycache__/array.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/utils/__pycache__/aliases.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/utils/__pycache__/aliases.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6cb48c22dfe49966d728c0c376ee08a0ecaabc47 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/utils/__pycache__/aliases.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/utils/__pycache__/nvtx.cpython-38.pyc b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/utils/__pycache__/nvtx.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..31c8e7e80c3c3c18dc7242416369c72106278ca0 Binary files /dev/null and b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/monai/utils/__pycache__/nvtx.cpython-38.pyc differ diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/opencv_python.libs/libQt5Test-c38a5234.so.5.15.0 b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/opencv_python.libs/libQt5Test-c38a5234.so.5.15.0 new file mode 100644 index 0000000000000000000000000000000000000000..cc0db645579a974e45f6d3d22f89fb49b6af26cc --- /dev/null +++ b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/opencv_python.libs/libQt5Test-c38a5234.so.5.15.0 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba5de79c331697e46a7f7c2d808971bdcc83691f6de4c8f19584acb07dab6691 +size 419025 diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/opencv_python.libs/libssl-28bef1ac.so.1.1 b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/opencv_python.libs/libssl-28bef1ac.so.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..d7fad0127329c4e079881bab9118082b619ec007 --- /dev/null +++ b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/opencv_python.libs/libssl-28bef1ac.so.1.1 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dcca03d43a032febbe420671813ae5649a43be53c17e52c418ee91cd572d6e11 +size 736177 diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/opencv_python.libs/libxcb-xkb-9ba31ab3.so.1.0.0 b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/opencv_python.libs/libxcb-xkb-9ba31ab3.so.1.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..6e36d09ae82dc38af844a7b618f2e45c65e38262 --- /dev/null +++ b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/opencv_python.libs/libxcb-xkb-9ba31ab3.so.1.0.0 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2da004caf83ef69cde058c3bfb6425e390ca54d468aba23e61af679b5653a39 +size 157921 diff --git a/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/scipy.libs/libgfortran-040039e1.so.5.0.0 b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/scipy.libs/libgfortran-040039e1.so.5.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..1080d23be17a0c5bdbbf69e78ad99971eb73e9ea --- /dev/null +++ b/my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/scipy.libs/libgfortran-040039e1.so.5.0.0 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47ab3b68295b0a3ce8990a448de7fab11abddbc160f8895972ca9aa712cf86d0 +size 2686064