Source code for InnerEye.ML.utils.transforms
# ------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
# ------------------------------------------------------------------------------------------
from __future__ import annotations
import abc
from functools import reduce
from typing import Any, Generic, List, Optional, Tuple, Union
import numpy as np
import param
import torch
from InnerEye.Common.common_util import is_gpu_tensor
from InnerEye.Common.type_annotations import T, TupleFloat2
[docs]class Transform3D(param.Parameterized, Generic[T]):
"""
Class that allows defining a transform function with the possibility of operating on the GPU.
"""
use_gpu: bool = param.Boolean(False, doc="The use_gpu flag will be "
"set based upon the available GPU devices.")
[docs] def get_gpu_tensor_if_possible(self, data: T) -> Any:
""""
Get a cuda tensor if this transform was CUDA enabled and a GPU is available, otherwise
return the input.
"""
import torch
if isinstance(data, torch.Tensor):
if self.use_gpu and not is_gpu_tensor(data):
return data.cuda()
else:
return data
else:
return data
@abc.abstractmethod
def __call__(self, sample: T) -> T:
raise Exception("__call__ function must be implemented by subclasses")
[docs]class Compose3D(Generic[T]):
"""
Class that allows chaining multiple transform functions together, and applying them to a sample
"""
def __init__(self, transforms: List[Transform3D[T]]):
self._transforms = transforms
def __call__(self, sample: T) -> T:
# pythonic implementation of the foldl function
# foldl (-) 0 [1,2,3] => (((0 - 1) - 2) - 3) => -6
return reduce(lambda x, f: f(x), self._transforms, sample)
[docs] @staticmethod
def apply(compose: Optional[Compose3D[T]], sample: T) -> T:
"""
Apply a composition of transfer functions to the provided sample
:param compose: A composition of transfer functions
:param sample: The sample to apply the composition on
:return:
"""
if compose:
return compose(sample)
else:
return sample
[docs]class CTRange(Transform3D[Union[torch.Tensor, np.ndarray]]):
output_range: TupleFloat2 = param.NumericTuple(default=(0.0, 255.0), length=2,
doc="Desired output range of intensities")
window: float = param.Number(None, doc="Width of window")
level: float = param.Number(None, doc="Mid-point of window")
def __call__(self, data: Union[torch.Tensor, np.ndarray]) -> Union[torch.Tensor, np.ndarray]:
return LinearTransform.transform(
data=self.get_gpu_tensor_if_possible(data),
input_range=get_range_for_window_level(self.level, self.window),
output_range=self.output_range,
use_gpu=self.use_gpu
)
[docs] @staticmethod
def transform(data: Union[torch.Tensor, np.ndarray],
output_range: TupleFloat2,
window: float, level: float,
use_gpu: bool = False) -> Union[torch.Tensor, np.ndarray]:
# find upper and lower values of input range to linearly map to output range. Values outside range are
# floored and capped at min or max of range.
transform = CTRange(output_range=output_range, window=window, level=level, use_gpu=use_gpu)
return transform(data)
[docs]class LinearTransform(Transform3D[Union[torch.Tensor, np.ndarray]]):
input_range: TupleFloat2 = param.NumericTuple(None, length=2, doc="Expected input range of intensities")
output_range: TupleFloat2 = param.NumericTuple(None, length=2, doc="Desired output range of intensities")
def __call__(self, data: Union[torch.Tensor, np.ndarray]) -> Union[torch.Tensor, np.ndarray]:
data = self.get_gpu_tensor_if_possible(data)
gradient = (self.output_range[1] - self.output_range[0]) / (self.input_range[1] - self.input_range[0])
c = self.output_range[1] - gradient * self.input_range[1]
_apply_transform = lambda: data * gradient + c
if torch.is_tensor(data):
gradient = self.get_gpu_tensor_if_possible(torch.tensor(gradient))
c = self.get_gpu_tensor_if_possible(torch.tensor(c))
return _apply_transform().clamp(min=self.output_range[0], max=self.output_range[1])
else:
return np.clip(_apply_transform(), a_min=self.output_range[0], a_max=self.output_range[1])
[docs] @staticmethod
def transform(data: Union[torch.Tensor, np.ndarray],
input_range: TupleFloat2, output_range: TupleFloat2,
use_gpu: bool = False) -> Union[torch.Tensor, np.ndarray]:
transform = LinearTransform(use_gpu=use_gpu, input_range=input_range, output_range=output_range)
return transform(data)
[docs]def get_range_for_window_level(level: float, window: float) -> Tuple[float, float]:
upper = level + window / 2
lower = level - window / 2
return lower, upper