Switch to unified view

a b/ants/contrib/sampling/affine2d.py
1
"""
2
Affine transforms
3
4
See http://www.cs.cornell.edu/courses/cs4620/2010fa/lectures/03transforms3D.pdf
5
"""
6
7
__all__ = [
8
    "Zoom2D",
9
    "RandomZoom2D",
10
    "Rotate2D",
11
    "RandomRotate2D",
12
    "Shear2D",
13
    "RandomShear2D",
14
    "Translate2D",
15
    "RandomTranslate2D",
16
]
17
18
import random
19
import math
20
import numpy as np
21
22
from ...core import ants_transform as tio
23
24
25
class Translate2D(object):
26
    """
27
    Create an ANTs Affine Transform with a specified translation.
28
    """
29
30
    def __init__(self, translation, reference=None, lazy=False):
31
        """
32
        Initialize a Translate2D object
33
34
        Arguments
35
        ---------
36
        translation : list or tuple
37
            translation values for each axis, in degrees.
38
            Negative values can be used for translation in the
39
            other direction
40
41
        reference : ANTsImage (optional but recommended)
42
            image providing the reference space for the transform.
43
            this will also set the transform fixed parameters.
44
45
        lazy : boolean (default = False)
46
            if True, calling the `transform` method only returns
47
            the randomly generated transform and does not actually
48
            transform the image
49
        """
50
        if (not isinstance(translation, (list, tuple))) or (len(translation) != 2):
51
            raise ValueError("translation argument must be list/tuple with two values!")
52
53
        self.translation = translation
54
        self.lazy = lazy
55
        self.reference = reference
56
57
        self.tx = tio.ANTsTransform(
58
            precision="float", dimension=2, transform_type="AffineTransform"
59
        )
60
        if self.reference is not None:
61
            self.tx.set_fixed_parameters(self.reference.get_center_of_mass())
62
63
    def transform(self, X=None, y=None):
64
        """
65
        Transform an image using an Affine transform with the given
66
        translation parameters.  Return the transform if X=None.
67
68
        Arguments
69
        ---------
70
        X : ANTsImage
71
            Image to transform
72
73
        y : ANTsImage (optional)
74
            Another image to transform
75
76
        Returns
77
        -------
78
        ANTsImage if y is None, else a tuple of ANTsImage types
79
80
        Examples
81
        --------
82
        >>> import ants
83
        >>> img = ants.image_read(ants.get_data('r16'))
84
        >>> tx = ants.contrib.Translate2D(translation=(10,0))
85
        >>> img2_x = tx.transform(img)
86
        >>> tx = ants.contrib.Translate2D(translation=(-10,0)) # other direction
87
        >>> img2_x = tx.transform(img)
88
        >>> tx = ants.contrib.Translate2D(translation=(0,10))
89
        >>> img2_z = tx.transform(img)
90
        >>> tx = ants.contrib.Translate2D(translation=(10,10))
91
        >>> img2 = tx.transform(img)
92
        """
93
        # convert to radians and unpack
94
        translation_x, translation_y = self.translation
95
96
        translation_matrix = np.array([[1, 0, translation_x], [0, 1, translation_y]])
97
        self.tx.set_parameters(translation_matrix)
98
        if self.lazy or X is None:
99
            return self.tx
100
        else:
101
            if y is None:
102
                return self.tx.apply_to_image(X, reference=self.reference)
103
            else:
104
                return (
105
                    self.tx.apply_to_image(X, reference=self.reference),
106
                    self.tx.apply_to_image(y, reference=self.reference),
107
                )
108
109
110
class RandomTranslate2D(object):
111
    """
112
    Apply a Translate2D transform to an image, but with the
113
    parameters randomly generated from a user-specified range.
114
    The range is determined by a mean (first parameter) and standard deviation
115
    (second parameter) via calls to random.gauss.
116
    """
117
118
    def __init__(self, translation_range, reference=None, lazy=False):
119
        """
120
        Initialize a RandomTranslate2D object
121
122
        Arguments
123
        ---------
124
        translation_range : list or tuple
125
            Lower and Upper bounds on rotation parameter, in degrees.
126
            e.g. translation_range = (-10,10) will result in a random
127
            draw of the rotation parameters between -10 and 10 degrees
128
129
        reference : ANTsImage (optional but recommended)
130
            image providing the reference space for the transform.
131
            this will also set the transform fixed parameters.
132
133
        lazy : boolean (default = False)
134
            if True, calling the `transform` method only returns
135
            the randomly generated transform and does not actually
136
            transform the image
137
        """
138
        if (not isinstance(translation_range, (list, tuple))) or (
139
            len(translation_range) != 2
140
        ):
141
            raise ValueError("shear_range argument must be list/tuple with two values!")
142
143
        self.translation_range = translation_range
144
        self.reference = reference
145
        self.lazy = lazy
146
147
    def transform(self, X=None, y=None):
148
        """
149
        Transform an image using an Affine transform with
150
        translation parameters randomly generated from the user-specified
151
        range.   Return the transform if X=None.
152
153
        Arguments
154
        ---------
155
        X : ANTsImage
156
            Image to transform
157
158
        y : ANTsImage (optional)
159
            Another image to transform
160
161
        Returns
162
        -------
163
        ANTsImage if y is None, else a tuple of ANTsImage types
164
165
        Examples
166
        --------
167
        >>> import ants
168
        >>> img = ants.image_read(ants.get_data('r16'))
169
        >>> tx = ants.contrib.RandomShear2D(translation_range=(-10,10))
170
        >>> img2 = tx.transform(img)
171
        """
172
        # random draw in translation range
173
        translation_x = random.gauss(
174
            self.translation_range[0], self.translation_range[1]
175
        )
176
        translation_y = random.gauss(
177
            self.translation_range[0], self.translation_range[1]
178
        )
179
        self.params = (translation_x, translation_y)
180
181
        tx = Translate2D(
182
            (translation_x, translation_y), reference=self.reference, lazy=self.lazy
183
        )
184
185
        return tx.transform(X, y)
186
187
188
class Shear2D(object):
189
    """
190
    Create an ANTs Affine Transform with a specified shear.
191
    """
192
193
    def __init__(self, shear, reference=None, lazy=False):
194
        """
195
        Initialize a Shear2D object
196
197
        Arguments
198
        ---------
199
        shear : list or tuple
200
            shear values for each axis, in degrees.
201
            Negative values can be used for shear in the
202
            other direction
203
204
        reference : ANTsImage (optional but recommended)
205
            image providing the reference space for the transform.
206
            this will also set the transform fixed parameters.
207
208
        lazy : boolean (default = False)
209
            if True, calling the `transform` method only returns
210
            the randomly generated transform and does not actually
211
            transform the image
212
        """
213
        if (not isinstance(shear, (list, tuple))) or (len(shear) != 2):
214
            raise ValueError("shear argument must be list/tuple with two values!")
215
216
        self.shear = shear
217
        self.lazy = lazy
218
        self.reference = reference
219
220
        self.tx = tio.ANTsTransform(
221
            precision="float", dimension=2, transform_type="AffineTransform"
222
        )
223
        if self.reference is not None:
224
            self.tx.set_fixed_parameters(self.reference.get_center_of_mass())
225
226
    def transform(self, X=None, y=None):
227
        """
228
        Transform an image using an Affine transform with the given
229
        shear parameters.   Return the transform if X=None.
230
231
        Arguments
232
        ---------
233
        X : ANTsImage
234
            Image to transform
235
236
        y : ANTsImage (optional)
237
            Another image to transform
238
239
        Returns
240
        -------
241
        ANTsImage if y is None, else a tuple of ANTsImage types
242
243
        Examples
244
        --------
245
        >>> import ants
246
        >>> img = ants.image_read(ants.get_data('r16'))
247
        >>> tx = ants.contrib.Shear2D(shear=(10,0,0))
248
        >>> img2_x = tx.transform(img)# x axis stays same
249
        >>> tx = ants.contrib.Shear2D(shear=(-10,0,0)) # other direction
250
        >>> img2_x = tx.transform(img)# x axis stays same
251
        >>> tx = ants.contrib.Shear2D(shear=(0,10,0))
252
        >>> img2_y = tx.transform(img) # y axis stays same
253
        >>> tx = ants.contrib.Shear2D(shear=(0,0,10))
254
        >>> img2_z = tx.transform(img) # z axis stays same
255
        >>> tx = ants.contrib.Shear2D(shear=(10,10,10))
256
        >>> img2 = tx.transform(img)
257
        """
258
        # convert to radians and unpack
259
        shear = [math.pi / 180 * s for s in self.shear]
260
        shear_x, shear_y = shear
261
262
        shear_matrix = np.array([[1, shear_x, 0], [shear_y, 1, 0]])
263
        self.tx.set_parameters(shear_matrix)
264
        if self.lazy or X is None:
265
            return self.tx
266
        else:
267
            if y is None:
268
                return self.tx.apply_to_image(X, reference=self.reference)
269
            else:
270
                return (
271
                    self.tx.apply_to_image(X, reference=self.reference),
272
                    self.tx.apply_to_image(y, reference=self.reference),
273
                )
274
275
276
class RandomShear2D(object):
277
    """
278
    Apply a Shear2D transform to an image, but with the shear
279
    parameters randomly generated from a user-specified range.
280
    The range is determined by a mean (first parameter) and standard deviation
281
    (second parameter) via calls to random.gauss.
282
    """
283
284
    def __init__(self, shear_range, reference=None, lazy=False):
285
        """
286
        Initialize a RandomShear2D object
287
288
        Arguments
289
        ---------
290
        shear_range : list or tuple
291
            Lower and Upper bounds on rotation parameter, in degrees.
292
            e.g. shear_range = (-10,10) will result in a random
293
            draw of the rotation parameters between -10 and 10 degrees
294
295
        reference : ANTsImage (optional but recommended)
296
            image providing the reference space for the transform.
297
            this will also set the transform fixed parameters.
298
299
        lazy : boolean (default = False)
300
            if True, calling the `transform` method only returns
301
            the randomly generated transform and does not actually
302
            transform the image
303
        """
304
        if (not isinstance(shear_range, (list, tuple))) or (len(shear_range) != 2):
305
            raise ValueError("shear_range argument must be list/tuple with two values!")
306
307
        self.shear_range = shear_range
308
        self.reference = reference
309
        self.lazy = lazy
310
311
    def transform(self, X=None, y=None):
312
        """
313
        Transform an image using an Affine transform with
314
        shear parameters randomly generated from the user-specified
315
        range.   Return the transform if X=None.
316
317
        Arguments
318
        ---------
319
        X : ANTsImage
320
            Image to transform
321
322
        y : ANTsImage (optional)
323
            Another image to transform
324
325
        Returns
326
        -------
327
        ANTsImage if y is None, else a tuple of ANTsImage types
328
329
        Examples
330
        --------
331
        >>> import ants
332
        >>> img = ants.image_read(ants.get_data('r16'))
333
        >>> tx = ants.contrib.RandomShear2D(shear_range=(-10,10))
334
        >>> img2 = tx.transform(img)
335
        """
336
        # random draw in shear range
337
        shear_x = random.gauss(self.shear_range[0], self.shear_range[1])
338
        shear_y = random.gauss(self.shear_range[0], self.shear_range[1])
339
        self.params = (shear_x, shear_y)
340
341
        tx = Shear2D((shear_x, shear_y), reference=self.reference, lazy=self.lazy)
342
343
        return tx.transform(X, y)
344
345
346
class Rotate2D(object):
347
    """
348
    Create an ANTs Affine Transform with a specified level
349
    of rotation.
350
    """
351
352
    def __init__(self, rotation, reference=None, lazy=False):
353
        """
354
        Initialize a Rotate2D object
355
356
        Arguments
357
        ---------
358
        rotation : scalar
359
            rotation value in degrees.
360
            Negative values can be used for rotation in the
361
            other direction
362
363
        reference : ANTsImage (optional but recommended)
364
            image providing the reference space for the transform.
365
            this will also set the transform fixed parameters.
366
367
        lazy : boolean (default = False)
368
            if True, calling the `transform` method only returns
369
            the randomly generated transform and does not actually
370
            transform the image
371
        """
372
        self.rotation = rotation
373
        self.lazy = lazy
374
        self.reference = reference
375
376
        self.tx = tio.ANTsTransform(
377
            precision="float", dimension=2, transform_type="AffineTransform"
378
        )
379
        if self.reference is not None:
380
            self.tx.set_fixed_parameters(self.reference.get_center_of_mass())
381
382
    def transform(self, X=None, y=None):
383
        """
384
        Transform an image using an Affine transform with the given
385
        rotation parameters.   Return the transform if X=None.
386
387
        Arguments
388
        ---------
389
        X : ANTsImage
390
            Image to transform
391
392
        y : ANTsImage (optional)
393
            Another image to transform
394
395
        Returns
396
        -------
397
        ANTsImage if y is None, else a tuple of ANTsImage types
398
399
        Examples
400
        --------
401
        >>> import ants
402
        >>> img = ants.image_read(ants.get_data('r16'))
403
        >>> tx = ants.contrib.Rotate2D(rotation=(10,-5,12))
404
        >>> img2 = tx.transform(img)
405
        """
406
        # unpack zoom range
407
        rotation = self.rotation
408
409
        # Rotation about X axis
410
        theta = math.pi / 180 * rotation
411
        rotation_matrix = np.array(
412
            [[np.cos(theta), -np.sin(theta), 0], [np.sin(theta), np.cos(theta), 0]]
413
        )
414
415
        self.tx.set_parameters(rotation_matrix)
416
        if self.lazy or X is None:
417
            return self.tx
418
        else:
419
            if y is None:
420
                return self.tx.apply_to_image(X, reference=self.reference)
421
            else:
422
                return (
423
                    self.tx.apply_to_image(X, reference=self.reference),
424
                    self.tx.apply_to_image(y, reference=self.reference),
425
                )
426
427
428
class RandomRotate2D(object):
429
    """
430
    Apply a Rotated2D transform to an image, but with the zoom
431
    parameters randomly generated from a user-specified range.
432
    The range is determined by a mean (first parameter) and standard deviation
433
    (second parameter) via calls to random.gauss.
434
    """
435
436
    def __init__(self, rotation_range, reference=None, lazy=False):
437
        """
438
        Initialize a RandomRotate2D object
439
440
        Arguments
441
        ---------
442
        rotation_range : list or tuple
443
            Lower and Upper bounds on rotation parameter, in degrees.
444
            e.g. rotation_range = (-10,10) will result in a random
445
            draw of the rotation parameters between -10 and 10 degrees
446
447
        reference : ANTsImage (optional but recommended)
448
            image providing the reference space for the transform.
449
            this will also set the transform fixed parameters.
450
451
        lazy : boolean (default = False)
452
            if True, calling the `transform` method only returns
453
            the randomly generated transform and does not actually
454
            transform the image
455
        """
456
        if (not isinstance(rotation_range, (list, tuple))) or (
457
            len(rotation_range) != 2
458
        ):
459
            raise ValueError(
460
                "rotation_range argument must be list/tuple with two values!"
461
            )
462
463
        self.rotation_range = rotation_range
464
        self.reference = reference
465
        self.lazy = lazy
466
467
    def transform(self, X=None, y=None):
468
        """
469
        Transform an image using an Affine transform with
470
        rotation parameters randomly generated from the user-specified
471
        range.   Return the transform if X=None.
472
473
        Arguments
474
        ---------
475
        X : ANTsImage
476
            Image to transform
477
478
        y : ANTsImage (optional)
479
            Another image to transform
480
481
        Returns
482
        -------
483
        ANTsImage if y is None, else a tuple of ANTsImage types
484
485
        Examples
486
        --------
487
        >>> import ants
488
        >>> img = ants.image_read(ants.get_data('r16'))
489
        >>> tx = ants.contrib.RandomRotate2D(rotation_range=(-10,10))
490
        >>> img2 = tx.transform(img)
491
        """
492
        # random draw in rotation range
493
        rotation = random.gauss(self.rotation_range[0], self.rotation_range[1])
494
        self.params = rotation
495
496
        tx = Rotate2D(rotation, reference=self.reference, lazy=self.lazy)
497
498
        return tx.transform(X, y)
499
500
501
class Zoom2D(object):
502
    """
503
    Create an ANTs Affine Transform with a specified level
504
    of zoom. Any value greater than 1 implies a "zoom-out" and anything
505
    less than 1 implies a "zoom-in".
506
    """
507
508
    def __init__(self, zoom, reference=None, lazy=False):
509
        """
510
        Initialize a Zoom2D object
511
512
        Arguments
513
        ---------
514
        zoom_range : list or tuple
515
            Lower and Upper bounds on zoom parameter.
516
            e.g. zoom_range = (0.7,0.9) will result in a random
517
            draw of the zoom parameters between 0.7 and 0.9
518
519
        reference : ANTsImage (optional but recommended)
520
            image providing the reference space for the transform.
521
            this will also set the transform fixed parameters.
522
523
        lazy : boolean (default = False)
524
            if True, calling the `transform` method only returns
525
            the randomly generated transform and does not actually
526
            transform the image
527
        """
528
        if (not isinstance(zoom, (list, tuple))) or (len(zoom) != 2):
529
            raise ValueError("zoom_range argument must be list/tuple with two values!")
530
531
        self.zoom = zoom
532
        self.lazy = lazy
533
        self.reference = reference
534
535
        self.tx = tio.ANTsTransform(
536
            precision="float", dimension=2, transform_type="AffineTransform"
537
        )
538
        if self.reference is not None:
539
            self.tx.set_fixed_parameters(self.reference.get_center_of_mass())
540
541
    def transform(self, X=None, y=None):
542
        """
543
        Transform an image using an Affine transform with the given
544
        zoom parameters.   Return the transform if X=None.
545
546
        Arguments
547
        ---------
548
        X : ANTsImage
549
            Image to transform
550
551
        y : ANTsImage (optional)
552
            Another image to transform
553
554
        Returns
555
        -------
556
        ANTsImage if y is None, else a tuple of ANTsImage types
557
558
        Examples
559
        --------
560
        >>> import ants
561
        >>> img = ants.image_read(ants.get_data('r16'))
562
        >>> tx = ants.contrib.Zoom2D(zoom=(0.8,0.8,0.8))
563
        >>> img2 = tx.transform(img)
564
        """
565
        # unpack zoom range
566
        zoom_x, zoom_y = self.zoom
567
568
        self.params = (zoom_x, zoom_y)
569
        zoom_matrix = np.array([[zoom_x, 0, 0], [0, zoom_y, 0]])
570
        self.tx.set_parameters(zoom_matrix)
571
        if self.lazy or X is None:
572
            return self.tx
573
        else:
574
            if y is None:
575
                return self.tx.apply_to_image(X, reference=self.reference)
576
            else:
577
                return (
578
                    self.tx.apply_to_image(X, reference=self.reference),
579
                    self.tx.apply_to_image(y, reference=self.reference),
580
                )
581
582
583
class RandomZoom2D(object):
584
    """
585
    Apply a Zoom2D transform to an image, but with the zoom
586
    parameters randomly generated from a user-specified range.
587
    The range is determined by a mean (first parameter) and standard deviation
588
    (second parameter) via calls to random.gauss.
589
    """
590
591
    def __init__(self, zoom_range, reference=None, lazy=False):
592
        """
593
        Initialize a RandomZoom2D object
594
595
        Arguments
596
        ---------
597
        zoom_range : list or tuple
598
            Lower and Upper bounds on zoom parameter.
599
            e.g. zoom_range = (0.7,0.9) will result in a random
600
            draw of the zoom parameters between 0.7 and 0.9
601
602
        reference : ANTsImage (optional but recommended)
603
            image providing the reference space for the transform.
604
            this will also set the transform fixed parameters.
605
606
        lazy : boolean (default = False)
607
            if True, calling the `transform` method only returns
608
            the randomly generated transform and does not actually
609
            transform the image
610
        """
611
        if (not isinstance(zoom_range, (list, tuple))) or (len(zoom_range) != 2):
612
            raise ValueError("zoom_range argument must be list/tuple with two values!")
613
614
        self.zoom_range = zoom_range
615
        self.reference = reference
616
        self.lazy = lazy
617
618
    def transform(self, X=None, y=None):
619
        """
620
        Transform an image using an Affine transform with
621
        zoom parameters randomly generated from the user-specified
622
        range.  Return the transform if X=None.
623
624
        Arguments
625
        ---------
626
        X : ANTsImage
627
            Image to transform
628
629
        y : ANTsImage (optional)
630
            Another image to transform
631
632
        Returns
633
        -------
634
        ANTsImage if y is None, else a tuple of ANTsImage types
635
636
        Examples
637
        --------
638
        >>> import ants
639
        >>> img = ants.image_read(ants.get_data('r16'))
640
        >>> tx = ants.contrib.RandomZoom2D(zoom_range=(0.8,0.9))
641
        >>> img2 = tx.transform(img)
642
        """
643
        # random draw in zoom range
644
        zoom_x = np.exp(
645
            random.gauss(np.log(self.zoom_range[0]), np.log(self.zoom_range[1]))
646
        )
647
        zoom_y = np.exp(
648
            random.gauss(np.log(self.zoom_range[0]), np.log(self.zoom_range[1]))
649
        )
650
        self.params = (zoom_x, zoom_y)
651
652
        tx = Zoom2D((zoom_x, zoom_y), reference=self.reference, lazy=self.lazy)
653
654
        return tx.transform(X, y)