Source code for monai.transforms.adaptors

# Copyright 2020 - 2021 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
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# See the License for the specific language governing permissions and
# limitations under the License.

How to use the adaptor function

The key to using 'adaptor' lies in understanding the function that want to
adapt. The 'inputs' and 'outputs' parameters take either strings, lists/tuples
of strings or a dictionary mapping strings, depending on call signature of the
function being called.

The adaptor function is written to minimise the cognitive load on the caller.
There should be a minimal number of cases where the caller has to set anything
on the input parameter, and for functions that return a single value, it is
only necessary to name the dictionary keyword to which that value is assigned.

Use of `outputs`

`outputs` can take either a string, a list/tuple of string or a dict of string
to string, depending on what the transform being adapted returns:

    - If the transform returns a single argument, then outputs can be supplied a
      string that indicates what key to assign the return value to in the
    - If the transform returns a list/tuple of values, then outputs can be supplied
      a list/tuple of the same length. The strings in outputs map the return value
      at the corresponding position to a key in the dictionary
    - If the transform returns a dictionary of values, then outputs must be supplied
      a dictionary that maps keys in the function's return dictionary to the
      dictionary being passed between functions

Note, the caller is free to use a more complex way of specifying the outputs
parameter than is required. The following are synonymous and will be treated

.. code-block:: python

   # single argument
   adaptor(MyTransform(), 'image')
   adaptor(MyTransform(), ['image'])
   adaptor(MyTransform(), {'image': 'image'})

   # multiple arguments
   adaptor(MyTransform(), ['image', 'label'])
   adaptor(MyTransform(), {'image': 'image', 'label': 'label'})

Use of `inputs`

`inputs` can usually be omitted when using `adaptor`. It is only required when a
the function's parameter names do not match the names in the dictionary that is
used to chain transform calls.

.. code-block:: python

    class MyTransform1:
        def __call__(self, image):
            # do stuff to image
            return image + 1

    class MyTransform2:
        def __call__(self, img_dict):
            # do stuff to image
            img_dict["image"] += 1
            return img_dict

    xform = Compose([adaptor(MyTransform1(), "image"), MyTransform2()])
    d = {"image": 1}

    >>> {'image': 3}

.. code-block:: python

    class MyTransform3:
        def __call__(self, img_dict):
            # do stuff to image
            img_dict["image"] -= 1
            img_dict["segment"] = img_dict["image"]
            return img_dict

    class MyTransform4:
        def __call__(self, img, seg):
            # do stuff to image
            img -= 1
            seg -= 1
            return img, seg

    xform = Compose([MyTransform3(), adaptor(MyTransform4(), ["img", "seg"], {"image": "img", "segment": "seg"})])
    d = {"image": 1}

    >>> {'image': 0, 'segment': 0, 'img': -1, 'seg': -1}


- dictionary in: None | Name maps
- params in (match): None | Name list | Name maps
- params in (mismatch): Name maps
- params & `**kwargs` (match) : None | Name maps
- params & `**kwargs` (mismatch) : Name maps


- dictionary out: None | Name maps
- list/tuple out: list/tuple
- variable out: string


from typing import Callable

from monai.utils import export as _monai_export

__all__ = ["adaptor", "apply_alias", "to_kwargs", "FunctionSignature"]

[docs]@_monai_export("monai.transforms") def adaptor(function, outputs, inputs=None): def must_be_types_or_none(variable_name, variable, types): if variable is not None: if not isinstance(variable, types): raise TypeError(f"'{variable_name}' must be None or one of {types} but is {type(variable)}") def must_be_types(variable_name, variable, types): if not isinstance(variable, types): raise TypeError(f"'{variable_name}' must be one of {types} but is {type(variable)}") def map_names(ditems, input_map): return {input_map(k, k): v for k, v in ditems.items()} def map_only_names(ditems, input_map): return {v: ditems[k] for k, v in input_map.items()} def _inner(ditems): sig = FunctionSignature(function) if sig.found_kwargs: must_be_types_or_none("inputs", inputs, (dict,)) # we just forward all arguments unless we have been provided an input map if inputs is None: dinputs = dict(ditems) else: # dict dinputs = map_names(ditems, inputs) else: # no **kwargs # select only items from the method signature dinputs = {k: v for k, v in ditems.items() if k in sig.non_var_parameters} must_be_types_or_none("inputs", inputs, (str, list, tuple, dict)) if inputs is None: pass elif isinstance(inputs, str): if len(sig.non_var_parameters) != 1: raise ValueError("if 'inputs' is a string, function may only have a single non-variadic parameter") dinputs = {inputs: ditems[inputs]} elif isinstance(inputs, (list, tuple)): dinputs = {k: dinputs[k] for k in inputs} else: # dict dinputs = map_only_names(ditems, inputs) ret = function(**dinputs) # now the mapping back to the output dictionary depends on outputs and what was returned from the function op = outputs if isinstance(ret, dict): must_be_types_or_none("outputs", op, (dict,)) if op is not None: ret = {v: ret[k] for k, v in op.items()} elif isinstance(ret, (list, tuple)): if len(ret) == 1: must_be_types("outputs", op, (str, list, tuple)) else: must_be_types("outputs", op, (list, tuple)) if isinstance(op, str): op = [op] if len(ret) != len(outputs): raise ValueError("'outputs' must have the same length as the number of elements that were returned") ret = dict(zip(op, ret)) else: must_be_types("outputs", op, (str, list, tuple)) if isinstance(op, (list, tuple)): if len(op) != 1: raise ValueError("'outputs' must be of length one if it is a list or tuple") op = op[0] ret = {op: ret} ditems = dict(ditems) for k, v in ret.items(): ditems[k] = v return ditems return _inner
[docs]@_monai_export("monai.transforms") def apply_alias(fn, name_map): def _inner(data): # map names pre_call = dict(data) for _from, _to in name_map.items(): pre_call[_to] = pre_call.pop(_from) # execute post_call = fn(pre_call) # map names back for _from, _to in name_map.items(): post_call[_from] = post_call.pop(_to) return post_call return _inner
[docs]@_monai_export("monai.transforms") def to_kwargs(fn): def _inner(data): return fn(**data) return _inner
class FunctionSignature: def __init__(self, function: Callable) -> None: import inspect sfn = inspect.signature(function) self.found_args = False self.found_kwargs = False self.defaults = {} self.non_var_parameters = set() for p in sfn.parameters.values(): if p.kind is inspect.Parameter.VAR_POSITIONAL: self.found_args = True if p.kind is inspect.Parameter.VAR_KEYWORD: self.found_kwargs = True else: self.non_var_parameters.add( self.defaults[] = p.default is not p.empty def __repr__(self) -> str: s = "<class 'FunctionSignature': found_args={}, found_kwargs={}, defaults={}" return s.format(self.found_args, self.found_kwargs, self.defaults) def __str__(self) -> str: return self.__repr__()