Skip to content

Commit e597994

Browse files
atbenmurraypre-commit-ci[bot]ericspodNic-Mamonai-bot
authored
Lazy functionality 1 2 (#6537)
Reduced lazy resampling functionality for MONAI 1.2 ### Description This PR is a subset of #6257 intended for MONAI 1.2. It contains the basic resampling strategy that has been approved for the 1.2 release during MONAI core dev meeting of 19th May, 2023. Draft status: * still to do * doc strings * topic page * resolve compose reference doc issue ### Types of changes <!--- Put an `x` in all the boxes that apply, and remove the not applicable items --> - [x] Non-breaking change (fix or new feature that would not break existing functionality). - [ ] Breaking change (fix or new feature that would cause existing functionality to change). - [x] New tests added to cover the changes. - [ ] Integration tests passed locally by running `./runtests.sh -f -u --net --coverage`. - [ ] Quick tests passed locally by running `./runtests.sh --quick --unittests --disttests`. - [x] In-line docstrings updated. - [x] Documentation updated, tested `make html` command in the `docs/` folder. --------- Signed-off-by: Ben Murray <[email protected]> Signed-off-by: monai-bot <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Kerfoot <[email protected]> Co-authored-by: Nic Ma <[email protected]> Co-authored-by: monai-bot <[email protected]>
1 parent 957fdf7 commit e597994

File tree

71 files changed

+2951
-961
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+2951
-961
lines changed

docs/images/lazy_resampling_apply_pending_example.svg

+1
Loading

docs/images/lazy_resampling_homogeneous_matrices.svg

+1
Loading

docs/images/lazy_resampling_lazy_example_1.svg

+1
Loading

docs/images/lazy_resampling_none_example.svg

+1
Loading

docs/images/lazy_resampling_trad_example_1.svg

+1
Loading

docs/source/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Technical documentation is available at `docs.monai.io <https://docs.monai.io>`_
6969
:caption: Specifications
7070

7171
bundle_intro
72+
lazy_resampling
7273

7374
Model Zoo
7475
---------

docs/source/lazy_resampling.rst

+273
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
.. _lazy_resampling:
2+
3+
:github_url: https://github.com/Project-MONAI/MONAI
4+
5+
Lazy Resampling
6+
===============
7+
8+
.. toctree::
9+
:maxdepth: 2
10+
11+
Introduction
12+
^^^^^^^^^^^^
13+
14+
Lazy Resampling is a new feature introduced in MONAI 1.2. This feature is still experimental at this time and it is
15+
possible that behaviour and APIs will change in upcoming releases.
16+
17+
Lazy resampling reworks the way that preprocessing is performed. It improves upon standard preprocessing pipelines and
18+
can provide significant benefits over traditional preprocessing. It can improve:
19+
* pipeline execution time
20+
* pipeline memory usage in CPU or GPU
21+
* image and segmentation quality by reducing incidental noise and artifacts caused by resampling
22+
23+
The way it does this is by adopting the methods used in computer graphics pipelines, in which transformations to objects
24+
in a scene are modified by composing together a sequence of "homogeneous matrices".
25+
26+
Rather than each transform being executed in isolation, potentially requiring the data to be resampled to make a new
27+
tensor, transforms whose operations can be described in terms of homogeneous transforms do not execute their transforms
28+
immediately. Instead, they create a "pending operation", which is added to a list of operations that will be fused
29+
together and carried out at the point that they are required.
30+
31+
32+
How Lazy Resampling changes preprocessing
33+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
34+
35+
In order to understand the difference between traditional pipelines and lazy pipelines, it is best to look at an example
36+
pipeline and the differences between their execution strategies:
37+
38+
39+
Traditional execution
40+
+++++++++++++++++++++
41+
42+
With traditional resampling, found both in MONAI and many other preprocessing libraries, you typically define a sequence
43+
of transforms and pass them to a ``Compose`` object, such as :class:`monai.transforms.compose.Compose`.
44+
45+
Example::
46+
47+
transforms = [
48+
Spacingd(keys=["img", "seg"], ...),
49+
Orientationd(keys=["img", "seg"], ...),
50+
RandSpatialCropd(keys=["img", "seg"], ...),
51+
RandRotate90d(keys=["img", "seg"], ...),
52+
RandRotated(keys=["img", "seg"], ...),
53+
RandZoomd(keys=["img", "seg"], ...),
54+
RandGaussianNoised(keys="img", ...),
55+
]
56+
pipeline = Compose(transforms)
57+
58+
# elsewhere this will be called many times (such as in a Dataset instance)
59+
outputs = pipeline(inputs)
60+
61+
62+
The following will then happen when we call ``pipeline(inputs)``:
63+
64+
1. ``Spacingd`` is called and interpolates the data samples
65+
2. ``Orientationd`` permutes the data samples so that their spatial dimensions are reorganised
66+
3. ``RandSpatialCropd`` crops a random patch of the data samples, throwing away the rest of the data in the process
67+
4. ``RandRotate90d`` has a chance of performing a tensor-based rotation of the data samples
68+
5. ``RandRotated`` has a chance of performing a full resample of the data samples
69+
6. ``RandZoomd`` has a chance of performing a interpolation of the data samples
70+
7. ``RandGaussianNoised`` has a chance of adding noise to ``img``
71+
72+
.. figure:: ../images/lazy_resampling_trad_example_1.svg
73+
74+
Figure showing traditional pipeline execution. Tensors (the boxes in the main body of the image) are passed through
75+
the pipeline, and the state of their `applied_operations` property is shown at each step. Tensors with a thick red
76+
border have undergone some kind of resample operation at that stage.
77+
78+
Overall, there are up to three occasions where the data is either interpolated or resampled through spatial transforms
79+
(``Spacingd``, ``RandRotated`` and ``RandZoomd``). Furthermore, the crop that occurs means that the output data
80+
samples might contain pixels for which there is data but that show padding values, because the data was thrown away by
81+
``RandSpatialCrop``.
82+
83+
Each of these operations takes time and memory, but, as we can see in the example above, also creates resampling
84+
artifacts and can even destroy data in the resulting data samples.
85+
86+
Lazy execution
87+
++++++++++++++
88+
89+
Lazy resampling works very differently. When you execute the same pipeline with `lazy=True`, the following happens:
90+
91+
#. ``Spacingd`` is executed lazily. It puts a description of the operation that it wants to perform onto a list of
92+
pending operations
93+
#. ``Orientationd`` is executed lazily. It adds a description of its own operation to the pending operation list so
94+
now there are 2 pending operations
95+
#. ``RandSpatialCropd`` is executed lazily. It adds a description of its own operation to the pending
96+
operation list so now there are 3 pending operations
97+
#. ``RandRotate90d`` is executed lazily. It adds a description of its own operation to the pending operation
98+
list so now there are 4 pending operations
99+
#. ``RandRotated`` is executed lazily. It adds a description of its own operation to the pending operation
100+
list so now there are 5 pending operations
101+
#. ``RandZoomd`` is executed lazily. It adds a description of its own operation to the pending operation
102+
list so now there are 6 pending operations
103+
104+
#. [Spacingd, Orientationd, RandSpatialCropd, RandRotate90d, RandRotated, RandZoomd] are all on the pending
105+
operations list but have yet to be carried out on the data
106+
#. ``RandGaussianNoised`` is not a lazy transform. It is now time for the pending operations to be evaluated. Their
107+
descriptions are mathematically composited together, to determine the operation that results from all of them being
108+
carried out. This is then applied in a single resample operation. Once that is done, RandGaussianNoised operates on
109+
the resulting data
110+
111+
.. figure:: ../images/lazy_resampling_lazy_example_1.svg
112+
113+
Figure showing lazy pipeline execution. We show the state of the `pending_operations` and `applied_operations`
114+
properties of the tensor as it is processed by the pipeline. Thick red borders indicate some kind of resampling
115+
operation has taken place at that step. Lazy resampling performs far fewer of these operations.
116+
117+
The single resampling operation has less noise induced by resampling, as it only occurs once in this pipeline rather
118+
than three times in the traditional pipeline. More importantly, although the crop describes an operation to keep only a
119+
subset of the data sample, the crop is not performed until after the spatial transforms are completed, which means that
120+
all of the data sample that is within bounds is preserved and is part of the resulting output.
121+
122+
123+
Composing homogeneous matrices
124+
++++++++++++++++++++++++++++++
125+
126+
.. image:: ../images/lazy_resampling_homogeneous_matrices.svg
127+
128+
129+
Although a full treatment of homogeneous matrices is outside the scope of this document, a brief overview of them is
130+
useful to understand the mechanics of lazy resampling. Homogeneous matrices are used in computer graphics to describe
131+
operations in cartesian space in a unified (homogeneous) fashion. Rotation, scaling, translation, and skewing are
132+
amongst the operations that can be performed. Homogeneous matrices have the interesting property that they can be
133+
composited together, thus describing the result of a sequence of operations. Note that ordering is important;
134+
`scale -> rotate -> translation` gives a very different result to `translation -> rotate -> scale`.
135+
136+
The ability to composite homogeneous matrices together allows a sequence of operations to be carried out as a single
137+
operation, which is the key mechanism by which lazy resampling functions.
138+
139+
140+
API changes
141+
^^^^^^^^^^^
142+
143+
A number of new arguments have been added to existing properties, which we'll go over in detail here. In particular,
144+
we'll focus on :class:`Compose<monai.transforms.compose.Compose`> and
145+
:class:`LazyTrait<monai.transforms.traits.LazyTrait>`/ :class:`LazyTransform<monai.transforms.transform.LazyTransform>`
146+
and the way that they interact with each other.
147+
148+
149+
Compose
150+
+++++++
151+
152+
:class:`Compose<monai.transforms.compose.Compose>` gains a number of new arguments that can be used to control
153+
resampling behaviour. Each of them is covered in its own section:
154+
155+
156+
lazy
157+
""""
158+
159+
``lazy`` controls whether execution is carried out in a lazy manner or not. It has three values that it can take:
160+
161+
* `lazy=False` forces the pipeline to be executed in the standard way with every transform applied immediately
162+
* `lazy=True` forces the pipeline to be executed lazily. Every transform that implements
163+
:class:`LazyTrait<monai.transforms.traits.LazyTrait>` (or inherits
164+
:class:`LazyTransform<monai.transforms.transform.LazyTransform>`) will be executed lazily
165+
* `lazy=None` means that the pipeline can execute lazily, but only on transforms that have their own `lazy` property
166+
set to True.
167+
168+
169+
overrides
170+
"""""""""
171+
172+
``overrides`` allows the user to specify certain parameters that transforms can be overridden with when they are
173+
executed lazily. This parameter is primarily provided to allow you to run a pipeline without having to modify fields
174+
like ``mode`` and ``padding_mode``.
175+
When executing dictionary-based transforms, you provide a dictionary containing overrides for each key, as follows. You
176+
can omit keys that don't require overrides:
177+
178+
.. code-block::
179+
180+
{
181+
"image": {"mode": "bilinear"},
182+
"label": {"padding_mode": "zeros"}
183+
}
184+
185+
186+
log_stats
187+
"""""""""
188+
189+
Logging of transform execution is provided if you wish to understand exactly how your pipelines execute. It can take a
190+
``bool`` or ``str`` value, and is False by default, which disables logging. Otherwise, you can enable it by passing it
191+
the name of a logger that you wish to use (note, you don't have to construct the logger beforehand).
192+
193+
194+
LazyTrait / LazyTransform
195+
+++++++++++++++++++++++++
196+
197+
Many transforms now implement either `LazyTrait<monai.transforms.traits.LazyTrait>` or
198+
`LazyTransform<monai.transforms.transform.Transform>`. Doing so marks the transform for lazy execution. Lazy
199+
transforms have the following in common:
200+
201+
202+
``__init__`` has a ``lazy`` argument
203+
""""""""""""""""""""""""""""""""""""
204+
205+
``lazy`` is a ``bool`` value that can be passed to the initialiser when a lazy transform is instantiated. This
206+
indicates to the transform that it should execute lazily or not lazily. Note that this value can be overridden by
207+
passing ``lazy`` to ``__init__``. ``lazy`` is ``False`` by default
208+
209+
210+
``__call__`` has a ``lazy`` argument
211+
""""""""""""""""""""""""""""""""""""
212+
213+
``lazy`` is an optional ``bool`` value that can be passed at call time to override the behaviour defined during
214+
initialisation. It has a default value of ``None``. If it is not ``None``, then this value is used instead of
215+
``self.lazy``. This allows the calling :class:`Compose<monai.transforms.compose.Compose>` instance to override
216+
default values rather than having to set it on every lazy transform (unless the user sets
217+
:class:`Compose.lazy<monai.transforms.compose.Compose>` to ``None``).
218+
219+
220+
lazy property
221+
"""""""""""""
222+
223+
The lazy property allows you to get or set the lazy status of a lazy transform after constructing it.
224+
225+
226+
requires_current_data property (get only)
227+
"""""""""""""""""""""""""""""""""""""""""
228+
229+
The ``requires_current_data`` property indicates that a transform makes use of the data in one or more of the tensors
230+
that it is passed during its execution. Such transforms require that the tensors must therefore be up to date, even if
231+
the transform itself is executing lazily. This is required for transforms such as ``CropForeground[d]``,
232+
``RandCropByPosNegLabel[d]``, and ``RandCropByLabelClasses[d]``. This property is implemented to return ``False`` on
233+
``LazyTransform`` and must be overridden to return ``True`` by transforms that check data values when executing.
234+
235+
236+
Controlling laziness
237+
^^^^^^^^^^^^^^^^^^^^
238+
239+
There are two ways that a user can provide more fine-grained control over laziness. One is to make use of lazy=None
240+
when initialising or calling ``Compose`` instances. The other is to use the ``ApplyPending[d]`` transforms. These
241+
techniques can be freely mixed and matched.
242+
243+
244+
Using ``lazy=None``
245+
+++++++++++++++++++
246+
247+
``Lazy=None`` tells ``Compose`` to honor the lazy flags set on each lazy transform. These are set to False by default
248+
so the user must set lazy=True on the transforms that they still wish to execute lazily.
249+
250+
251+
``lazy=None`` example:
252+
""""""""""""""""""""""
253+
254+
.. figure:: ../images/lazy_resampling_none_example.svg
255+
256+
Figure shwoing the effect of using ``lazy=False`` when ``Compose`` is being executed with ``lazy=None``. Note that
257+
the additional resamples that occur due to ``RandRotate90d`` being executed in a non-lazy fashion.
258+
259+
260+
Using ``ApplyPending[d]``
261+
+++++++++++++++++++++++++
262+
263+
``ApplyPending[d]`` causes all pending transforms to be executed before the following transform, regardless of whether
264+
the following transform is a lazy transform, or is configured to execute lazily.
265+
266+
267+
``ApplyPending`` Example:
268+
"""""""""""""""""""""""""
269+
270+
.. figure:: ../images/lazy_resampling_apply_pending_example.svg
271+
272+
Figure showing the use of :class:`ApplyPendingd<monai.transforms.lazy.dictionary.ApplyPendingd>` to cause
273+
resampling to occur in the midele of a chain of lazy transforms.

docs/source/transforms.rst

+22-6
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,17 @@ MRI Transforms
958958
:special-members: __call__
959959

960960

961+
Lazy
962+
^^^^
963+
964+
`ApplyPending`
965+
""""""""""""""
966+
967+
.. autoclass:: ApplyPending
968+
:members:
969+
:special-members: __call__
970+
971+
961972
Utility
962973
^^^^^^^
963974

@@ -1912,6 +1923,17 @@ Smooth Field (Dict)
19121923
:special-members: __call__
19131924

19141925

1926+
Lazy (Dict)
1927+
^^^^^^^^^^^
1928+
1929+
`ApplyPendingd`
1930+
"""""""""""""""
1931+
1932+
.. autoclass:: ApplyPendingd
1933+
:members:
1934+
:special-members: __call__
1935+
1936+
19151937
Utility (Dict)
19161938
^^^^^^^^^^^^^^
19171939

@@ -2211,9 +2233,3 @@ Utilities
22112233

22122234
.. automodule:: monai.transforms.utils_pytorch_numpy_unification
22132235
:members:
2214-
2215-
Lazy
2216-
----
2217-
.. automodule:: monai.transforms.lazy
2218-
:members:
2219-
:imported-members:

monai/apps/detection/transforms/dictionary.py

+13-3
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
from monai.data.box_utils import COMPUTE_DTYPE, BoxMode, clip_boxes_to_image
4444
from monai.data.meta_tensor import MetaTensor, get_track_meta
4545
from monai.data.utils import orientation_ras_lps
46-
from monai.transforms import Flip, RandFlip, RandRotate90d, RandZoom, Rotate90, SpatialCrop, Zoom
46+
from monai.transforms import Flip, RandFlip, RandZoom, Rotate90, SpatialCrop, Zoom
4747
from monai.transforms.inverse import InvertibleTransform
4848
from monai.transforms.transform import MapTransform, Randomizable, RandomizableTransform
4949
from monai.transforms.utils import generate_pos_neg_label_crop_centers, map_binary_to_indices
@@ -1291,7 +1291,7 @@ def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> dict[Hashable, torch
12911291
return d
12921292

12931293

1294-
class RandRotateBox90d(RandRotate90d):
1294+
class RandRotateBox90d(RandomizableTransform, MapTransform, InvertibleTransform):
12951295
"""
12961296
With probability `prob`, input boxes and images are rotated by 90 degrees
12971297
in the plane specified by `spatial_axes`.
@@ -1323,7 +1323,13 @@ def __init__(
13231323
) -> None:
13241324
self.image_keys = ensure_tuple(image_keys)
13251325
self.box_keys = ensure_tuple(box_keys)
1326-
super().__init__(self.image_keys + self.box_keys, prob, max_k, spatial_axes, allow_missing_keys)
1326+
1327+
MapTransform.__init__(self, self.image_keys + self.box_keys, allow_missing_keys)
1328+
RandomizableTransform.__init__(self, prob)
1329+
1330+
self.max_k = max_k
1331+
self.spatial_axes = spatial_axes
1332+
self._rand_k = 0
13271333
self.box_ref_image_keys = ensure_tuple_rep(box_ref_image_keys, len(self.box_keys))
13281334

13291335
def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Mapping[Hashable, torch.Tensor]:
@@ -1364,6 +1370,10 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Mapping[Hashable, t
13641370
self.push_transform(d[key], extra_info=xform)
13651371
return d
13661372

1373+
def randomize(self, data: Any | None = None) -> None:
1374+
self._rand_k = self.R.randint(self.max_k) + 1
1375+
super().randomize(None)
1376+
13671377
def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> dict[Hashable, torch.Tensor]:
13681378
d = dict(data)
13691379
if self._rand_k % 4 == 0:

monai/apps/reconstruction/transforms/dictionary.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
from monai.apps.reconstruction.transforms.array import EquispacedKspaceMask, RandomKspaceMask
2121
from monai.config import DtypeLike, KeysCollection
2222
from monai.config.type_definitions import NdarrayOrTensor
23+
from monai.transforms import InvertibleTransform
2324
from monai.transforms.croppad.array import SpatialCrop
24-
from monai.transforms.croppad.dictionary import Cropd
2525
from monai.transforms.intensity.array import NormalizeIntensity
2626
from monai.transforms.transform import MapTransform, RandomizableTransform
2727
from monai.utils import FastMRIKeys
@@ -190,7 +190,7 @@ def set_random_state(
190190
return self
191191

192192

193-
class ReferenceBasedSpatialCropd(Cropd):
193+
class ReferenceBasedSpatialCropd(MapTransform, InvertibleTransform):
194194
"""
195195
Dictionary-based wrapper of :py:class:`monai.transforms.SpatialCrop`.
196196
This is similar to :py:class:`monai.transforms.SpatialCropd` which is a
@@ -213,7 +213,7 @@ class ReferenceBasedSpatialCropd(Cropd):
213213
"""
214214

215215
def __init__(self, keys: KeysCollection, ref_key: str, allow_missing_keys: bool = False) -> None:
216-
super().__init__(keys, cropper=None, allow_missing_keys=allow_missing_keys) # type: ignore
216+
MapTransform.__init__(self, keys, allow_missing_keys)
217217
self.ref_key = ref_key
218218

219219
def __call__(self, data: Mapping[Hashable, Tensor]) -> dict[Hashable, Tensor]:

0 commit comments

Comments
 (0)