From 6bb9003322e32204cadb8c76b861a02b1af5e4d4 Mon Sep 17 00:00:00 2001 From: Justus Schock Date: Fri, 3 Jan 2020 11:31:07 +0100 Subject: [PATCH 1/8] Add zero border option to image soize calculation --- rising/transforms/functional/affine.py | 12 +++++++-- tests/transforms/functional/test_affine.py | 29 ++++++++++++++++------ 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/rising/transforms/functional/affine.py b/rising/transforms/functional/affine.py index db98401a..46a2310c 100644 --- a/rising/transforms/functional/affine.py +++ b/rising/transforms/functional/affine.py @@ -132,7 +132,8 @@ def affine_image_transform(image_batch: torch.Tensor, align_corners=align_corners) -def _check_new_img_size(curr_img_size, matrix: torch.Tensor) -> torch.Tensor: +def _check_new_img_size(curr_img_size, matrix: torch.Tensor, + zero_border: bool = True) -> torch.Tensor: """ Calculates the image size so that the whole image content fits the image. The resulting size will be the maximum size of the batch, so that the @@ -145,6 +146,8 @@ def _check_new_img_size(curr_img_size, matrix: torch.Tensor) -> torch.Tensor: all image dimensions matrix : torch.Tensor a batch of affine matrices with shape N x NDIM x NDIM + 1 + zero_border : bool + whether or not to have a fixed image border at zero Returns ------- @@ -189,5 +192,10 @@ def _check_new_img_size(curr_img_size, matrix: torch.Tensor) -> torch.Tensor: *[-1 for _ in possible_points.shape]).clone(), matrix) + if zero_border: + substr = 0 + else: + substr = transformed_edges.min(1)[0] + return (transformed_edges.max(1)[0] - - transformed_edges.min(1)[0]).max(0)[0] + 1 + - substr).max(0)[0] + 1 diff --git a/tests/transforms/functional/test_affine.py b/tests/transforms/functional/test_affine.py index b5877678..309d959f 100644 --- a/tests/transforms/functional/test_affine.py +++ b/tests/transforms/functional/test_affine.py @@ -57,14 +57,27 @@ def test_check_image_size(self): new_edges = torch.bmm(edge_pts.unsqueeze(0), matrix_revert_coordinate_order(affine.clone()).permute(0, 2, 1)) - img_size = (new_edges.max(dim=1)[0] - new_edges.min(dim=1)[0])[0] - - fn_result = _check_new_img_size(size, - matrix_to_cartesian( - affine.expand(img.size(0), -1, -1).clone())) - - self.assertTrue(torch.allclose(img_size[:-1] + 1, - fn_result)) + img_size_zero_border = new_edges.max(dim=1)[0][0] + img_size_non_zero_border = (new_edges.max(dim=1)[0] + - new_edges.min(dim=1)[0])[0] + + fn_result_zero_border = _check_new_img_size( + size, + matrix_to_cartesian( + affine.expand(img.size(0), -1, -1).clone()), + zero_border=True + ) + fn_result_non_zero_border = _check_new_img_size( + size, + matrix_to_cartesian( + affine.expand(img.size(0), -1, -1).clone()), + zero_border=False + ) + + self.assertTrue(torch.allclose(img_size_zero_border[:-1] + 1, + fn_result_zero_border)) + self.assertTrue(torch.allclose(img_size_non_zero_border[:-1] + 1, + fn_result_non_zero_border)) with self.assertRaises(ValueError): _check_new_img_size([2, 3, 4, 5], torch.rand(11, 2, 2, 3, 4, 5)) From b620780e2173705ca1cd9053dfaa0af394e68b47 Mon Sep 17 00:00:00 2001 From: Justus Schock Date: Fri, 3 Jan 2020 11:31:41 +0100 Subject: [PATCH 2/8] Add Centered Trafos --- rising/transforms/affine.py | 201 +++++++++++++++++++++++++++++++++++- 1 file changed, 198 insertions(+), 3 deletions(-) diff --git a/rising/transforms/affine.py b/rising/transforms/affine.py index bf764e30..3222196b 100644 --- a/rising/transforms/affine.py +++ b/rising/transforms/affine.py @@ -1,7 +1,10 @@ +from __future__ import annotations from rising.transforms.abstract import BaseTransform -from rising.transforms.functional.affine import affine_image_transform +from rising.transforms.functional.affine import affine_image_transform, \ + _check_new_img_size from rising.utils.affine import AffineParamType, \ assemble_matrix_if_necessary, matrix_to_homogeneous, matrix_to_cartesian +from rising.interface import AbstractMixin import torch from typing import Sequence, Union @@ -583,5 +586,197 @@ def assemble_matrix(self, **data) -> torch.Tensor: return matrix_to_cartesian(whole_trafo) -# TODO: Add transforms around image center -# TODO: Add Resize Transform + +class CenterShiftMixin(AbstractMixin): + def include_shifts(self, matrix, **data) -> torch.Tensor: + batchsize = data[self.keys[0]].shape[0] + ndim = len(data[self.keys[0]].shape) - 2 # channel and batch dim + device = data[self.keys[0]].device + dtype = data[self.keys[0]].dtype + curr_img_size = data[self.keys[0]].shape[2:] + + shift_to_origin = assemble_matrix_if_necessary( + batchsize, ndim, scale=None, rotation=None, + translation=tuple([-tmp / 2 for tmp in reversed(curr_img_size)]), + matrix=None, degree=False, device=device, dtype=dtype + ) + + matrix = matrix_to_cartesian( + torch.bmm(matrix_to_homogeneous(matrix), + matrix_to_homogeneous(shift_to_origin))) + new_img_size = _check_new_img_size(curr_img_size, matrix, + zero_border=False) + + shift_back_to_center = assemble_matrix_if_necessary( + batchsize, ndim, + scale=None, rotation=None, + translation=tuple([tmp / 2 for tmp in reversed(new_img_size)]), + matrix=None, degree=False, device=device, dtype=dtype) + + return matrix_to_cartesian( + torch.bmm( + matrix_to_homogeneous(shift_back_to_center), + matrix_to_homogeneous(matrix) + ) + ) + + def assemble_matrix(self, **data) -> torch.Tensor: + matrix = super().assemble_matrix(**data) + whole_matrix = self.include_shifts(matrix, **data) + + return whole_matrix + + +class RotateAroundCenter(CenterShiftMixin, Rotate): + def __init__(self, + rotation: AffineParamType, + keys: Sequence = ('data',), + grad: bool = False, + degree: bool = False, + output_size: tuple = None, + adjust_size: bool = False, + interpolation_mode: str = 'bilinear', + padding_mode: str = 'zeros', + align_corners: bool = False, + **kwargs): + """ + Class Performing a Rotation-OnlyAffine Transformation on a given + sample dict. + The transformation will be applied to all the dict-entries specified + in :attr:`keys`. + + Parameters + ---------- + rotation : torch.Tensor, int, float, optional + the rotation factor(s). Supported are: + * a full transformation matrix of shape + (BATCHSIZE x NDIM x NDIM) + * a single parameter (as float or int), which will be + replicated for all dimensions and batch samples + * a single parameter per sample (as a 1d tensor), which will + be replicated for all dimensions + * a single parameter per dimension (either as 1d tensor or as + 2d transformation matrix), which will be replicated for + all batch samples + None will be treated as a rotation factor of 1 + keys: Sequence + keys which should be augmented + grad: bool + enable gradient computation inside transformation + degree : bool + whether the given rotation(s) are in degrees. + Only valid for rotation parameters, which aren't passed as full + transformation matrix. + output_size : Iterable + if given, this will be the resulting image size. + Defaults to ``None`` + adjust_size : bool + if True, the resulting image size will be calculated dynamically + to ensure that the whole image fits. + interpolation_mode : str + interpolation mode to calculate output values + 'bilinear' | 'nearest'. Default: 'bilinear' + padding_mode : + padding mode for outside grid values + 'zeros' | 'border' | 'reflection'. Default: 'zeros' + align_corners : Geometrically, we consider the pixels of the input as + squares rather than points. If set to True, the extrema (-1 and 1) + are considered as referring to the center points of the input’s + corner pixels. If set to False, they are instead considered as + referring to the corner points of the input’s corner pixels, + making the sampling more resolution agnostic. + **kwargs : + additional keyword arguments passed to the affine transform + + Notes + ----- + The offsets for shifting back and to origin are calculated on the + entry matching the first item iin :attr:`keys` for each batch + + """ + super().__init__(rotation=rotation, + keys=keys, + grad=grad, + degree=degree, + output_size=output_size, + adjust_size=adjust_size, + interpolation_mode=interpolation_mode, + padding_mode=padding_mode, + align_corners=align_corners, + **kwargs) + + +class ScaleAroundCenter(CenterShiftMixin, Scale): + def __init__(self, + scale: AffineParamType, + keys: Sequence = ('data',), + grad: bool = False, + output_size: tuple = None, + adjust_size: bool = False, + interpolation_mode: str = 'bilinear', + padding_mode: str = 'zeros', + align_corners: bool = False, + **kwargs): + """ + Class Performing a Scale-Only Affine Transformation on a given + sample dict. + The transformation will be applied to all the dict-entries specified + in :attr:`keys`. + + Parameters + ---------- + scale : torch.Tensor, int, float, optional + the scale factor(s). Supported are: + * a full transformation matrix of shape + (BATCHSIZE x NDIM x NDIM) + * a single parameter (as float or int), which will be + replicated for all dimensions and batch samples + * a single parameter per sample (as a 1d tensor), which will + be replicated for all dimensions + * a single parameter per dimension (either as 1d tensor or as + 2d transformation matrix), which will be replicated for + all batch samples + None will be treated as a scaling factor of 1 + keys: Sequence + keys which should be augmented + grad: bool + enable gradient computation inside transformation + degree : bool + whether the given rotation(s) are in degrees. + Only valid for rotation parameters, which aren't passed as full + transformation matrix. + output_size : Iterable + if given, this will be the resulting image size. + Defaults to ``None`` + adjust_size : bool + if True, the resulting image size will be calculated dynamically + to ensure that the whole image fits. + interpolation_mode : str + interpolation mode to calculate output values + 'bilinear' | 'nearest'. Default: 'bilinear' + padding_mode : + padding mode for outside grid values + 'zeros' | 'border' | 'reflection'. Default: 'zeros' + align_corners : Geometrically, we consider the pixels of the input as + squares rather than points. If set to True, the extrema (-1 and 1) + are considered as referring to the center points of the input’s + corner pixels. If set to False, they are instead considered as + referring to the corner points of the input’s corner pixels, + making the sampling more resolution agnostic. + **kwargs : + additional keyword arguments passed to the affine transform + + Notes + ----- + The offsets for shifting back and to origin are calculated on the + entry matching the first item iin :attr:`keys` for each batch + """ + super().__init__(scale=scale, + keys=keys, + grad=grad, + output_size=output_size, + adjust_size=adjust_size, + interpolation_mode=interpolation_mode, + padding_mode=padding_mode, + align_corners=align_corners, + **kwargs) From 2ec7f51ba21356c9345aafd070c9854a74976472 Mon Sep 17 00:00:00 2001 From: Justus Schock Date: Fri, 3 Jan 2020 11:45:38 +0100 Subject: [PATCH 3/8] Add Resizing Trafo --- rising/transforms/affine.py | 86 ++++++++++++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 5 deletions(-) diff --git a/rising/transforms/affine.py b/rising/transforms/affine.py index 3222196b..30bf166b 100644 --- a/rising/transforms/affine.py +++ b/rising/transforms/affine.py @@ -4,9 +4,10 @@ _check_new_img_size from rising.utils.affine import AffineParamType, \ assemble_matrix_if_necessary, matrix_to_homogeneous, matrix_to_cartesian +from rising.utils.checktype import check_scalar from rising.interface import AbstractMixin import torch -from typing import Sequence, Union +from typing import Sequence, Union, Iterable __all__ = [ 'Affine', @@ -741,10 +742,6 @@ def __init__(self, keys which should be augmented grad: bool enable gradient computation inside transformation - degree : bool - whether the given rotation(s) are in degrees. - Only valid for rotation parameters, which aren't passed as full - transformation matrix. output_size : Iterable if given, this will be the resulting image size. Defaults to ``None`` @@ -780,3 +777,82 @@ def __init__(self, padding_mode=padding_mode, align_corners=align_corners, **kwargs) + + +class Resize(ScaleAroundCenter): + def __init__(self, + size: Union[int, Iterable], + keys: Sequence = ('data',), + grad: bool = False, + interpolation_mode: str = 'bilinear', + padding_mode: str = 'zeros', + align_corners: bool = False, + **kwargs): + """ + Class Performing a Resizing Affine Transformation on a given + sample dict. + The transformation will be applied to all the dict-entries specified + in :attr:`keys`. + + Parameters + ---------- + size : int, Iterable + the target size. If int, this will be repeated for all the + dimensions + keys: Sequence + keys which should be augmented + grad: bool + enable gradient computation inside transformation + interpolation_mode : str + interpolation mode to calculate output values + 'bilinear' | 'nearest'. Default: 'bilinear' + padding_mode : + padding mode for outside grid values + 'zeros' | 'border' | 'reflection'. Default: 'zeros' + align_corners : Geometrically, we consider the pixels of the input as + squares rather than points. If set to True, the extrema (-1 and 1) + are considered as referring to the center points of the input’s + corner pixels. If set to False, they are instead considered as + referring to the corner points of the input’s corner pixels, + making the sampling more resolution agnostic. + **kwargs : + additional keyword arguments passed to the affine transform + + Note + ---- + The offsets for shifting back and to origin are calculated on the + entry matching the first item iin :attr:`keys` for each batch + + Note + ---- + The target size must be specified in x, y (,z) order and will be + converted to (D,) H, W order internally + + """ + super().__init__(output_size=size, + scale=None, + keys=keys, + grad=grad, + adjust_size=False, + interpolation_mode=interpolation_mode, + padding_mode=padding_mode, + align_corners=align_corners, + **kwargs) + + def assemble_matrix(self, **data) -> torch.Tensor: + curr_img_size = data[self.keys[0]].shape[2:] + + was_scalar = check_scalar(self.output_size) + + if was_scalar: + self.output_size = [self.output_size] * len(curr_img_size) + + self.scale = (self.output_size[i] / curr_img_size[-i] + for i in range(len(curr_img_size))) + + matrix = super().assemble_matrix(**data) + + if was_scalar: + self.output_size = self.output_size[0] + + return matrix From 42cc7568151260dbe4bb8cfb9774be4403f30564 Mon Sep 17 00:00:00 2001 From: Justus Schock Date: Sat, 1 Feb 2020 20:58:49 +0100 Subject: [PATCH 4/8] finalize the additional transforms --- rising/transforms/affine.py | 69 ++++++++++++++++++++++++++++----- tests/transforms/test_affine.py | 8 ++-- 2 files changed, 64 insertions(+), 13 deletions(-) diff --git a/rising/transforms/affine.py b/rising/transforms/affine.py index 30bf166b..965e9a72 100644 --- a/rising/transforms/affine.py +++ b/rising/transforms/affine.py @@ -12,8 +12,8 @@ __all__ = [ 'Affine', 'StackedAffine', - 'Rotate', - 'Scale', + 'RotateAroundOrigin', + 'ScaleAroundOrigin', 'Translate' ] @@ -254,7 +254,7 @@ def __radd__(self, other): **other.kwargs) -class Rotate(Affine): +class RotateAroundOrigin(Affine): def __init__(self, rotation: AffineParamType, keys: Sequence = ('data',), @@ -407,7 +407,7 @@ def __init__(self, **kwargs) -class Scale(Affine): +class ScaleAroundOrigin(Affine): def __init__(self, scale: AffineParamType, keys: Sequence = ('data',), @@ -589,7 +589,27 @@ def assemble_matrix(self, **data) -> torch.Tensor: class CenterShiftMixin(AbstractMixin): - def include_shifts(self, matrix, **data) -> torch.Tensor: + """ + Mixin to Add Center Shifts to Transforms for transformations around image + centers + """ + def include_shifts(self, matrix: torch.Tensor, **data) -> torch.Tensor: + """ + adds the actual shifts to a transformation matrix + + Parameters + ---------- + matrix : torch.Tensor + the original transformation matrix + data : + the data to calculate the center offsets from + + Returns + ------- + torch.Tensor + the matrix including the center shifts + + """ batchsize = data[self.keys[0]].shape[0] ndim = len(data[self.keys[0]].shape) - 2 # channel and batch dim device = data[self.keys[0]].device @@ -622,13 +642,28 @@ def include_shifts(self, matrix, **data) -> torch.Tensor: ) def assemble_matrix(self, **data) -> torch.Tensor: + """ + Handles the matrix assembly and stacks it with the center shifts + + Parameters + ---------- + **data : + the data to be transformed. Will be used to determine batchsize, + dimensionality, dtype and device + + Returns + ------- + torch.Tensor + the (batched) transformation matrix + + """ matrix = super().assemble_matrix(**data) whole_matrix = self.include_shifts(matrix, **data) return whole_matrix -class RotateAroundCenter(CenterShiftMixin, Rotate): +class Rotate(CenterShiftMixin, RotateAroundOrigin): def __init__(self, rotation: AffineParamType, keys: Sequence = ('data',), @@ -641,7 +676,7 @@ def __init__(self, align_corners: bool = False, **kwargs): """ - Class Performing a Rotation-OnlyAffine Transformation on a given + Class Performing a Rotation-Only Affine Transformation on a given sample dict. The transformation will be applied to all the dict-entries specified in :attr:`keys`. @@ -707,7 +742,7 @@ def __init__(self, **kwargs) -class ScaleAroundCenter(CenterShiftMixin, Scale): +class Scale(CenterShiftMixin, ScaleAroundOrigin): def __init__(self, scale: AffineParamType, keys: Sequence = ('data',), @@ -779,7 +814,7 @@ def __init__(self, **kwargs) -class Resize(ScaleAroundCenter): +class Resize(Scale): def __init__(self, size: Union[int, Iterable], keys: Sequence = ('data',), @@ -840,6 +875,22 @@ def __init__(self, **kwargs) def assemble_matrix(self, **data) -> torch.Tensor: + """ + Handles the matrix assembly and calculates the scale factors for + resizing + + Parameters + ---------- + **data : + the data to be transformed. Will be used to determine batchsize, + dimensionality, dtype and device + + Returns + ------- + torch.Tensor + the (batched) transformation matrix + + """ curr_img_size = data[self.keys[0]].shape[2:] was_scalar = check_scalar(self.output_size) diff --git a/tests/transforms/test_affine.py b/tests/transforms/test_affine.py index 79406c14..4b56b595 100644 --- a/tests/transforms/test_affine.py +++ b/tests/transforms/test_affine.py @@ -1,6 +1,6 @@ import unittest -from rising.transforms.affine import Affine, StackedAffine, Translate, Rotate, \ - Scale +from rising.transforms.affine import Affine, StackedAffine, Translate, RotateAroundOrigin, \ + ScaleAroundOrigin import torch from copy import deepcopy from rising.utils.affine import matrix_to_cartesian, matrix_to_homogeneous @@ -81,8 +81,8 @@ def test_affine_subtypes(self): sample = {'data': torch.rand(10, 3, 25, 25)} trafos = [ - Scale(5), - Rotate(45), + ScaleAroundOrigin(5), + RotateAroundOrigin(45), Translate(10) ] From d7767c7640b93d69b9829d7faff646157849c5f1 Mon Sep 17 00:00:00 2001 From: Justus Schock Date: Sat, 1 Feb 2020 21:26:34 +0100 Subject: [PATCH 5/8] test dummys --- tests/transforms/test_affine.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/transforms/test_affine.py b/tests/transforms/test_affine.py index 4b56b595..12fb6531 100644 --- a/tests/transforms/test_affine.py +++ b/tests/transforms/test_affine.py @@ -1,6 +1,6 @@ import unittest from rising.transforms.affine import Affine, StackedAffine, Translate, RotateAroundOrigin, \ - ScaleAroundOrigin + ScaleAroundOrigin, Scale, Rotate, Resize import torch from copy import deepcopy from rising.utils.affine import matrix_to_cartesian, matrix_to_homogeneous @@ -90,6 +90,27 @@ def test_affine_subtypes(self): with self.subTest(trafo=trafo): self.assertIsInstance(trafo(**sample)['data'], torch.Tensor) + def test_center_affines(self): + sample = {'data': torch.rand(10, 3, 25, 30)} + + trafos = [ + Scale([2, 3]), + Resize([50, 90]), + Rotate([90]), + ] + + expected_sizes = [ + (50, 90), + (50, 90), + (30, 25) + ] + + for trafo, expected_size in zip(trafos, expected_sizes): + with self.subTest(trafo=trafo, exp_size=expected_size): + result = trafo(**sample)['data'] + self.assertIsInstance(result, torch.Tensor) + self.assertTupleEqual(expected_size, result.shape[-2:]) + if __name__ == '__main__': unittest.main() From b1aea087d10021c0ac7f95f8c25be69cd8dc706c Mon Sep 17 00:00:00 2001 From: Michael Baumgartner Date: Sat, 1 Feb 2020 20:27:37 +0000 Subject: [PATCH 6/8] autopep8 fix --- rising/transforms/affine.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rising/transforms/affine.py b/rising/transforms/affine.py index 965e9a72..213c8f3b 100644 --- a/rising/transforms/affine.py +++ b/rising/transforms/affine.py @@ -593,6 +593,7 @@ class CenterShiftMixin(AbstractMixin): Mixin to Add Center Shifts to Transforms for transformations around image centers """ + def include_shifts(self, matrix: torch.Tensor, **data) -> torch.Tensor: """ adds the actual shifts to a transformation matrix From 94f7e68824a8a378fc8e663a4432c694851db242 Mon Sep 17 00:00:00 2001 From: mibaumgartner Date: Sat, 15 Feb 2020 01:21:42 +0100 Subject: [PATCH 7/8] fix tests and remove reverting of dimensions --- rising/transforms/affine.py | 8 ++++---- rising/transforms/functional/affine.py | 20 ++++++-------------- rising/utils/affine.py | 1 - tests/transforms/functional/test_affine.py | 16 +++++++--------- tests/transforms/test_affine.py | 15 ++++++++------- 5 files changed, 25 insertions(+), 35 deletions(-) diff --git a/rising/transforms/affine.py b/rising/transforms/affine.py index 213c8f3b..3e4568eb 100644 --- a/rising/transforms/affine.py +++ b/rising/transforms/affine.py @@ -619,7 +619,7 @@ def include_shifts(self, matrix: torch.Tensor, **data) -> torch.Tensor: shift_to_origin = assemble_matrix_if_necessary( batchsize, ndim, scale=None, rotation=None, - translation=tuple([-tmp / 2 for tmp in reversed(curr_img_size)]), + translation=tuple([-tmp / 2 for tmp in curr_img_size]), matrix=None, degree=False, device=device, dtype=dtype ) @@ -632,7 +632,7 @@ def include_shifts(self, matrix: torch.Tensor, **data) -> torch.Tensor: shift_back_to_center = assemble_matrix_if_necessary( batchsize, ndim, scale=None, rotation=None, - translation=tuple([tmp / 2 for tmp in reversed(new_img_size)]), + translation=tuple([tmp / 2 for tmp in new_img_size]), matrix=None, degree=False, device=device, dtype=dtype) return matrix_to_cartesian( @@ -899,8 +899,8 @@ def assemble_matrix(self, **data) -> torch.Tensor: if was_scalar: self.output_size = [self.output_size] * len(curr_img_size) - self.scale = (self.output_size[i] / curr_img_size[-i] - for i in range(len(curr_img_size))) + self.scale = [self.output_size[i] / curr_img_size[-i] + for i in range(len(curr_img_size))] matrix = super().assemble_matrix(**data) diff --git a/rising/transforms/functional/affine.py b/rising/transforms/functional/affine.py index 46a2310c..5d6ac6a6 100644 --- a/rising/transforms/functional/affine.py +++ b/rising/transforms/functional/affine.py @@ -31,11 +31,9 @@ def affine_point_transform(point_batch: torch.Tensor, point_batch = points_to_homogeneous(point_batch) matrix_batch = matrix_to_homogeneous(matrix_batch) - matrix_batch = matrix_revert_coordinate_order(matrix_batch) - - transformed_points = torch.bmm(point_batch, - matrix_batch.permute(0, 2, 1)) + # matrix_batch = matrix_revert_coordinate_order(matrix_batch) + transformed_points = torch.bmm(point_batch, matrix_batch.permute(0, 2, 1)) return points_to_cartesian(transformed_points) @@ -120,9 +118,7 @@ def affine_image_transform(image_batch: torch.Tensor, missing_dims = len(image_batch.shape) - len(image_size) new_size = (*image_batch.shape[:missing_dims], *new_size) - matrix_batch = matrix_batch.to(device=image_batch.device, - dtype=image_batch.dtype) - + matrix_batch = matrix_batch.to(image_batch) grid = torch.nn.functional.affine_grid(matrix_batch, size=new_size, align_corners=align_corners) @@ -153,15 +149,13 @@ def _check_new_img_size(curr_img_size, matrix: torch.Tensor, ------- torch.Tensor the new image size - """ - n_dim = matrix.size(-1) - 1 if check_scalar(curr_img_size): curr_img_size = [curr_img_size] * n_dim - curr_img_size = [tmp - 1 for tmp in curr_img_size] + # curr_img_size = [tmp - 1 for tmp in curr_img_size] if n_dim == 2: possible_points = torch.tensor([[0., 0.], [0., curr_img_size[1]], @@ -188,8 +182,7 @@ def _check_new_img_size(curr_img_size, matrix: torch.Tensor, transformed_edges = affine_point_transform( possible_points[None].expand( - matrix.size(0), - *[-1 for _ in possible_points.shape]).clone(), + matrix.size(0), *[-1 for _ in possible_points.shape]).clone(), matrix) if zero_border: @@ -197,5 +190,4 @@ def _check_new_img_size(curr_img_size, matrix: torch.Tensor, else: substr = transformed_edges.min(1)[0] - return (transformed_edges.max(1)[0] - - substr).max(0)[0] + 1 + return (transformed_edges.max(1)[0] - substr).max(0)[0] # + 1 diff --git a/rising/utils/affine.py b/rising/utils/affine.py index ee21a9ab..e035f609 100644 --- a/rising/utils/affine.py +++ b/rising/utils/affine.py @@ -197,7 +197,6 @@ def _format_scale(scale: AffineParamType, scale = 1 if check_scalar(scale): - scale = get_batched_eye(batchsize=batchsize, ndim=ndim, device=device, dtype=dtype) * scale diff --git a/tests/transforms/functional/test_affine.py b/tests/transforms/functional/test_affine.py index 309d959f..6c73895a 100644 --- a/tests/transforms/functional/test_affine.py +++ b/tests/transforms/functional/test_affine.py @@ -8,7 +8,6 @@ class AffineTestCase(unittest.TestCase): - def test_check_image_size(self): images = [torch.rand(11, 2, 3, 4, 5), torch.rand(11, 2, 3, 4), torch.rand(11, 2, 3, 3)] @@ -51,11 +50,10 @@ def test_check_image_size(self): batchsize=1, ndim=ndim, dtype=torch.float)) edge_pts = torch.tensor(edge_pts, dtype=torch.float) - edge_pts[edge_pts > 1] = edge_pts[edge_pts > 1] - 1 + # edge_pts[edge_pts > 1] = edge_pts[edge_pts > 1] - 1 img = img.to(torch.float) - new_edges = torch.bmm(edge_pts.unsqueeze(0), - matrix_revert_coordinate_order(affine.clone()).permute(0, 2, 1)) + new_edges = torch.bmm(edge_pts.unsqueeze(0), affine.clone().permute(0, 2, 1)) img_size_zero_border = new_edges.max(dim=1)[0][0] img_size_non_zero_border = (new_edges.max(dim=1)[0] @@ -74,9 +72,9 @@ def test_check_image_size(self): zero_border=False ) - self.assertTrue(torch.allclose(img_size_zero_border[:-1] + 1, + self.assertTrue(torch.allclose(img_size_zero_border[:-1], fn_result_zero_border)) - self.assertTrue(torch.allclose(img_size_non_zero_border[:-1] + 1, + self.assertTrue(torch.allclose(img_size_non_zero_border[:-1], fn_result_non_zero_border)) with self.assertRaises(ValueError): @@ -85,10 +83,10 @@ def test_check_image_size(self): def test_affine_point_transform(self): points = [ [[[0, 1], [1, 0]]], - [[[0, 0, 1]]] + [[[1, 0, 0]]] ] matrices = [ - torch.tensor([[[1., 0.], [0., 5.]]]), + torch.tensor([[[5., 0.], [0., 1.]]]), parametrize_matrix(scale=1, translation=0, rotation=[0, 0, 90], @@ -124,7 +122,7 @@ def test_affine_image_trafo(self): image_batch = torch.zeros(10, 3, 25, 25, dtype=torch.float, device='cpu') - target_sizes = [(121, 97), image_batch.shape[2:], (50, 50), (50, 50), + target_sizes = [(100, 125), image_batch.shape[2:], (50, 50), (50, 50), (45, 50), (45, 50)] for output_size in [None, 50, (45, 50)]: diff --git a/tests/transforms/test_affine.py b/tests/transforms/test_affine.py index 12fb6531..61dacf6c 100644 --- a/tests/transforms/test_affine.py +++ b/tests/transforms/test_affine.py @@ -13,7 +13,7 @@ def test_affine(self): device='cpu') matrix = matrix.expand(image_batch.size(0), -1, -1).clone() - target_sizes = [(121, 97), image_batch.shape[2:], (50, 50), (50, 50), + target_sizes = [(100, 125), image_batch.shape[2:], (50, 50), (50, 50), (45, 50), (45, 50)] for output_size in [None, 50, (45, 50)]: @@ -79,24 +79,25 @@ def test_stacked_transformation_assembly(self): def test_affine_subtypes(self): - sample = {'data': torch.rand(10, 3, 25, 25)} + sample = {'data': torch.rand(10, 3, 50, 25)} trafos = [ ScaleAroundOrigin(5), - RotateAroundOrigin(45), + RotateAroundOrigin(90), Translate(10) ] for trafo in trafos: with self.subTest(trafo=trafo): - self.assertIsInstance(trafo(**sample)['data'], torch.Tensor) + res = trafo(**sample)['data'] + self.assertIsInstance(res, torch.Tensor) def test_center_affines(self): - sample = {'data': torch.rand(10, 3, 25, 30)} + sample = {'data': torch.rand(1, 3, 25, 30)} trafos = [ - Scale([2, 3]), + ScaleAroundOrigin([2, 3], adjust_size=True), Resize([50, 90]), - Rotate([90]), + Rotate([90], adjust_size=True, degree=True), ] expected_sizes = [ From abe37570605268b1ce3578c356a34504c5ae21a0 Mon Sep 17 00:00:00 2001 From: mibaumgartner Date: Sat, 15 Feb 2020 11:41:25 +0100 Subject: [PATCH 8/8] add affines to init and transformation notebook --- notebooks/transformations.ipynb | 267 ++++++++++++++++++++++++++++++++ rising/transforms/__init__.py | 1 + rising/transforms/affine.py | 5 +- 3 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 notebooks/transformations.ipynb diff --git a/notebooks/transformations.ipynb b/notebooks/transformations.ipynb new file mode 100644 index 00000000..d92f17f4 --- /dev/null +++ b/notebooks/transformations.ipynb @@ -0,0 +1,267 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-02-15T09:57:50.791332Z", + "start_time": "2020-02-15T09:57:46.068701Z" + }, + "scrolled": true + }, + "outputs": [], + "source": [ + "!pip install napari\n", + "!pip install SimpleITK" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-02-15T10:01:52.452774Z", + "start_time": "2020-02-15T10:01:52.389042Z" + } + }, + "outputs": [], + "source": [ + "%reload_ext autoreload\n", + "%autoreload 2\n", + "%matplotlib inline\n", + "%gui qt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-02-15T09:57:14.608576Z", + "start_time": "2020-02-15T09:56:29.264911Z" + } + }, + "outputs": [], + "source": [ + "from io import BytesIO\n", + "from zipfile import ZipFile\n", + "from urllib.request import urlopen\n", + "\n", + "resp = urlopen(\"http://www.fmrib.ox.ac.uk/primers/intro_primer/ExBox3/ExBox3.zip\")\n", + "zipfile = ZipFile(BytesIO(resp.read()))\n", + "#zipfile_list = zipfile.namelist()\n", + "#print(zipfile_list)\n", + "img_file = zipfile.extract(\"ExBox3/T1_brain.nii.gz\")\n", + "mask_file = zipfile.extract(\"ExBox3/T1_brain_seg.nii.gz\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-02-15T10:04:15.140790Z", + "start_time": "2020-02-15T10:04:14.896805Z" + } + }, + "outputs": [], + "source": [ + "import SimpleITK as sitk\n", + "import numpy as np\n", + "\n", + "# load image and mask\n", + "img = sitk.GetArrayFromImage(sitk.ReadImage(img_file))\n", + "img = img.astype(np.float32)\n", + "mask = mask = sitk.GetArrayFromImage(sitk.ReadImage(mask_file))\n", + "mask = mask.astype(np.float32)\n", + "\n", + "assert mask.shape == img.shape\n", + "print(f\"Image shape {img.shape}\")\n", + "print(f\"Image shape {mask.shape}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-02-15T10:19:58.269109Z", + "start_time": "2020-02-15T10:19:58.196441Z" + } + }, + "outputs": [], + "source": [ + "import napari\n", + "def view_batch(batch):\n", + " viewer = napari.view_image(batch[\"data\"].cpu().numpy(), name=\"data\")\n", + " viewer.add_image(batch[\"mask\"].cpu().numpy(), name=\"mask\", opacity=0.2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-02-15T10:24:42.215111Z", + "start_time": "2020-02-15T10:24:42.121751Z" + } + }, + "outputs": [], + "source": [ + "import torch\n", + "from rising.transforms import *\n", + "\n", + "batch = {\n", + " \"data\": torch.from_numpy(img).float()[None],\n", + " \"mask\": torch.from_numpy(mask).long()[None],\n", + "}\n", + "\n", + "def apply_transform(trafo, batch):\n", + " transformed = trafo(**batch)\n", + " print(f\"Transformed data shape: {transformed['data'].shape}\")\n", + " print(f\"Transformed mask shape: {transformed['mask'].shape}\")\n", + " print(f\"Transformed data min: {transformed['data'].min()}\")\n", + " print(f\"Transformed data max: {transformed['data'].max()}\")\n", + " print(f\"Transformed data mean: {transformed['data'].mean()}\")\n", + " return transformed" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-02-15T10:36:11.811863Z", + "start_time": "2020-02-15T10:36:11.729206Z" + } + }, + "outputs": [], + "source": [ + "print(f\"Transformed data shape: {batch['data'].shape}\")\n", + "print(f\"Transformed mask shape: {batch['mask'].shape}\")\n", + "print(f\"Transformed data min: {batch['data'].min()}\")\n", + "print(f\"Transformed data max: {batch['data'].max()}\")\n", + "print(f\"Transformed data mean: {batch['data'].mean()}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-02-15T10:37:56.495818Z", + "start_time": "2020-02-15T10:37:55.880045Z" + } + }, + "outputs": [], + "source": [ + "trafo = ScaleAroundOrigin(0.5, adjust_size=False)\n", + "transformed = apply_transform(trafo, batch)\n", + "view_batch(transformed)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-02-15T10:37:21.018478Z", + "start_time": "2020-02-15T10:37:20.365852Z" + } + }, + "outputs": [], + "source": [ + "trafo = RotateAroundOrigin(45, degree=True, adjust_size=False)\n", + "transformed = apply_transform(trafo, batch)\n", + "view_batch(transformed)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-02-15T10:36:47.910532Z", + "start_time": "2020-02-15T10:36:47.303825Z" + } + }, + "outputs": [], + "source": [ + "trafo = Translate(20, adjust_size=False)\n", + "transformed = apply_transform(trafo, batch)\n", + "view_batch(transformed)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.5" + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/rising/transforms/__init__.py b/rising/transforms/__init__.py index 99b95a19..026fe35f 100644 --- a/rising/transforms/__init__.py +++ b/rising/transforms/__init__.py @@ -8,3 +8,4 @@ from rising.transforms.spatial import * from rising.transforms.utility import * from rising.transforms.tensor import * +from rising.transforms.affine import * diff --git a/rising/transforms/affine.py b/rising/transforms/affine.py index 3e4568eb..9d82cb2f 100644 --- a/rising/transforms/affine.py +++ b/rising/transforms/affine.py @@ -14,7 +14,10 @@ 'StackedAffine', 'RotateAroundOrigin', 'ScaleAroundOrigin', - 'Translate' + 'Translate', + 'Rotate', + 'Scale', + 'Resize', ]