Unverified Commit 9bd25d08 authored by vfdev's avatar vfdev Committed by GitHub
Browse files

Unified inputs for `T.RandomAffine` transformation (2292) (#2478)

* [WIP] Unified input for T.RandomAffine

* Unified inputs for T.RandomAffine transformation

* Update transforms.py

* Updated docs of F.affine fillcolor

* Update transforms.py
parent 1aef87d0
...@@ -248,9 +248,8 @@ class Tester(unittest.TestCase): ...@@ -248,9 +248,8 @@ class Tester(unittest.TestCase):
def test_resized_crop(self): def test_resized_crop(self):
tensor = torch.randint(0, 255, size=(3, 44, 56), dtype=torch.uint8) tensor = torch.randint(0, 255, size=(3, 44, 56), dtype=torch.uint8)
scale = (0.7, 1.2) for scale in [(0.7, 1.2), [0.7, 1.2]]:
ratio = (0.75, 1.333) for ratio in [(0.75, 1.333), [0.75, 1.333]]:
for size in [(32, ), [32, ], [32, 32], (32, 32)]: for size in [(32, ), [32, ], [32, 32], (32, 32)]:
for interpolation in [NEAREST, BILINEAR, BICUBIC]: for interpolation in [NEAREST, BILINEAR, BICUBIC]:
transform = T.RandomResizedCrop( transform = T.RandomResizedCrop(
...@@ -264,6 +263,26 @@ class Tester(unittest.TestCase): ...@@ -264,6 +263,26 @@ class Tester(unittest.TestCase):
out2 = s_transform(tensor) out2 = s_transform(tensor)
self.assertTrue(out1.equal(out2)) self.assertTrue(out1.equal(out2))
def test_random_affine(self):
tensor = torch.randint(0, 255, size=(3, 44, 56), dtype=torch.uint8)
for shear in [15, 10.0, (5.0, 10.0), [-15, 15], [-10.0, 10.0, -11.0, 11.0]]:
for scale in [(0.7, 1.2), [0.7, 1.2]]:
for translate in [(0.1, 0.2), [0.2, 0.1]]:
for degrees in [45, 35.0, (-45, 45), [-90.0, 90.0]]:
for interpolation in [NEAREST, BILINEAR]:
transform = T.RandomAffine(
degrees=degrees, translate=translate,
scale=scale, shear=shear, resample=interpolation
)
s_transform = torch.jit.script(transform)
torch.manual_seed(12)
out1 = transform(tensor)
torch.manual_seed(12)
out2 = s_transform(tensor)
self.assertTrue(out1.equal(out2))
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
...@@ -858,7 +858,9 @@ def affine( ...@@ -858,7 +858,9 @@ def affine(
An optional resampling filter. See `filters`_ for more information. An optional resampling filter. See `filters`_ for more information.
If omitted, or if the image is PIL Image and has mode "1" or "P", it is set to ``PIL.Image.NEAREST``. If omitted, or if the image is PIL Image and has mode "1" or "P", it is set to ``PIL.Image.NEAREST``.
If input is Tensor, only ``PIL.Image.NEAREST`` and ``PIL.Image.BILINEAR`` are supported. If input is Tensor, only ``PIL.Image.NEAREST`` and ``PIL.Image.BILINEAR`` are supported.
fillcolor (int): Optional fill color for the area outside the transform in the output image. (Pillow>=5.0.0) fillcolor (int): Optional fill color for the area outside the transform in the output image (Pillow>=5.0.0).
This option is not supported for Tensor input. Fill value for the area outside the transform in the output
image is always 0.
Returns: Returns:
PIL Image or Tensor: Transformed image. PIL Image or Tensor: Transformed image.
......
...@@ -5,7 +5,6 @@ import warnings ...@@ -5,7 +5,6 @@ import warnings
from collections.abc import Sequence from collections.abc import Sequence
from typing import Tuple, List, Optional from typing import Tuple, List, Optional
import numpy as np
import torch import torch
from PIL import Image from PIL import Image
from torch import Tensor from torch import Tensor
...@@ -721,9 +720,9 @@ class RandomResizedCrop(torch.nn.Module): ...@@ -721,9 +720,9 @@ class RandomResizedCrop(torch.nn.Module):
raise ValueError("Please provide only two dimensions (h, w) for size.") raise ValueError("Please provide only two dimensions (h, w) for size.")
self.size = size self.size = size
if not isinstance(scale, (tuple, list)): if not isinstance(scale, Sequence):
raise TypeError("Scale should be a sequence") raise TypeError("Scale should be a sequence")
if not isinstance(ratio, (tuple, list)): if not isinstance(ratio, Sequence):
raise TypeError("Ratio should be a sequence") raise TypeError("Ratio should be a sequence")
if (scale[0] > scale[1]) or (ratio[0] > ratio[1]): if (scale[0] > scale[1]) or (ratio[0] > ratio[1]):
warnings.warn("Scale and ratio should be of kind (min, max)") warnings.warn("Scale and ratio should be of kind (min, max)")
...@@ -734,14 +733,14 @@ class RandomResizedCrop(torch.nn.Module): ...@@ -734,14 +733,14 @@ class RandomResizedCrop(torch.nn.Module):
@staticmethod @staticmethod
def get_params( def get_params(
img: Tensor, scale: Tuple[float, float], ratio: Tuple[float, float] img: Tensor, scale: List[float], ratio: List[float]
) -> Tuple[int, int, int, int]: ) -> Tuple[int, int, int, int]:
"""Get parameters for ``crop`` for a random sized crop. """Get parameters for ``crop`` for a random sized crop.
Args: Args:
img (PIL Image or Tensor): Input image. img (PIL Image or Tensor): Input image.
scale (tuple): range of scale of the origin size cropped scale (list): range of scale of the origin size cropped
ratio (tuple): range of aspect ratio of the origin aspect ratio cropped ratio (list): range of aspect ratio of the origin aspect ratio cropped
Returns: Returns:
tuple: params (i, j, h, w) to be passed to ``crop`` for a random tuple: params (i, j, h, w) to be passed to ``crop`` for a random
...@@ -751,7 +750,7 @@ class RandomResizedCrop(torch.nn.Module): ...@@ -751,7 +750,7 @@ class RandomResizedCrop(torch.nn.Module):
area = height * width area = height * width
for _ in range(10): for _ in range(10):
target_area = area * torch.empty(1).uniform_(*scale).item() target_area = area * torch.empty(1).uniform_(scale[0], scale[1]).item()
log_ratio = torch.log(torch.tensor(ratio)) log_ratio = torch.log(torch.tensor(ratio))
aspect_ratio = torch.exp( aspect_ratio = torch.exp(
torch.empty(1).uniform_(log_ratio[0], log_ratio[1]) torch.empty(1).uniform_(log_ratio[0], log_ratio[1])
...@@ -1173,8 +1172,10 @@ class RandomRotation(object): ...@@ -1173,8 +1172,10 @@ class RandomRotation(object):
return format_string return format_string
class RandomAffine(object): class RandomAffine(torch.nn.Module):
"""Random affine transformation of the image keeping center invariant """Random affine transformation of the image keeping center invariant.
The image can be a PIL Image or a Tensor, in which case it is expected
to have [..., H, W] shape, where ... means an arbitrary number of leading dimensions.
Args: Args:
degrees (sequence or float or int): Range of degrees to select from. degrees (sequence or float or int): Range of degrees to select from.
...@@ -1188,41 +1189,51 @@ class RandomAffine(object): ...@@ -1188,41 +1189,51 @@ class RandomAffine(object):
randomly sampled from the range a <= scale <= b. Will keep original scale by default. randomly sampled from the range a <= scale <= b. Will keep original scale by default.
shear (sequence or float or int, optional): Range of degrees to select from. shear (sequence or float or int, optional): Range of degrees to select from.
If shear is a number, a shear parallel to the x axis in the range (-shear, +shear) If shear is a number, a shear parallel to the x axis in the range (-shear, +shear)
will be apllied. Else if shear is a tuple or list of 2 values a shear parallel to the x axis in the will be applied. Else if shear is a tuple or list of 2 values a shear parallel to the x axis in the
range (shear[0], shear[1]) will be applied. Else if shear is a tuple or list of 4 values, range (shear[0], shear[1]) will be applied. Else if shear is a tuple or list of 4 values,
a x-axis shear in (shear[0], shear[1]) and y-axis shear in (shear[2], shear[3]) will be applied. a x-axis shear in (shear[0], shear[1]) and y-axis shear in (shear[2], shear[3]) will be applied.
Will not apply shear by default Will not apply shear by default.
resample ({PIL.Image.NEAREST, PIL.Image.BILINEAR, PIL.Image.BICUBIC}, optional): resample (int, optional): An optional resampling filter. See `filters`_ for more information.
An optional resampling filter. See `filters`_ for more information. If omitted, or if the image has mode "1" or "P", it is set to ``PIL.Image.NEAREST``.
If omitted, or if the image has mode "1" or "P", it is set to PIL.Image.NEAREST. If input is Tensor, only ``PIL.Image.NEAREST`` and ``PIL.Image.BILINEAR`` are supported.
fillcolor (tuple or int): Optional fill color (Tuple for RGB Image And int for grayscale) for the area fillcolor (tuple or int): Optional fill color (Tuple for RGB Image and int for grayscale) for the area
outside the transform in the output image.(Pillow>=5.0.0) outside the transform in the output image (Pillow>=5.0.0). This option is not supported for Tensor
input. Fill value for the area outside the transform in the output image is always 0.
.. _filters: https://pillow.readthedocs.io/en/latest/handbook/concepts.html#filters .. _filters: https://pillow.readthedocs.io/en/latest/handbook/concepts.html#filters
""" """
def __init__(self, degrees, translate=None, scale=None, shear=None, resample=False, fillcolor=0): def __init__(self, degrees, translate=None, scale=None, shear=None, resample=0, fillcolor=0):
super().__init__()
if isinstance(degrees, numbers.Number): if isinstance(degrees, numbers.Number):
if degrees < 0: if degrees < 0:
raise ValueError("If degrees is a single number, it must be positive.") raise ValueError("If degrees is a single number, it must be positive.")
self.degrees = (-degrees, degrees) degrees = [-degrees, degrees]
else: else:
assert isinstance(degrees, (tuple, list)) and len(degrees) == 2, \ if not isinstance(degrees, Sequence):
"degrees should be a list or tuple and it must be of length 2." raise TypeError("degrees should be a sequence of length 2.")
self.degrees = degrees if len(degrees) != 2:
raise ValueError("degrees should be sequence of length 2.")
self.degrees = [float(d) for d in degrees]
if translate is not None: if translate is not None:
assert isinstance(translate, (tuple, list)) and len(translate) == 2, \ if not isinstance(translate, Sequence):
"translate should be a list or tuple and it must be of length 2." raise TypeError("translate should be a sequence of length 2.")
if len(translate) != 2:
raise ValueError("translate should be sequence of length 2.")
for t in translate: for t in translate:
if not (0.0 <= t <= 1.0): if not (0.0 <= t <= 1.0):
raise ValueError("translation values should be between 0 and 1") raise ValueError("translation values should be between 0 and 1")
self.translate = translate self.translate = translate
if scale is not None: if scale is not None:
assert isinstance(scale, (tuple, list)) and len(scale) == 2, \ if not isinstance(scale, Sequence):
"scale should be a list or tuple and it must be of length 2." raise TypeError("scale should be a sequence of length 2.")
if len(scale) != 2:
raise ValueError("scale should be sequence of length 2.")
for s in scale: for s in scale:
if s <= 0: if s <= 0:
raise ValueError("scale values should be positive") raise ValueError("scale values should be positive")
...@@ -1232,16 +1243,14 @@ class RandomAffine(object): ...@@ -1232,16 +1243,14 @@ class RandomAffine(object):
if isinstance(shear, numbers.Number): if isinstance(shear, numbers.Number):
if shear < 0: if shear < 0:
raise ValueError("If shear is a single number, it must be positive.") raise ValueError("If shear is a single number, it must be positive.")
self.shear = (-shear, shear) shear = [-shear, shear]
else: else:
assert isinstance(shear, (tuple, list)) and \ if not isinstance(shear, Sequence):
(len(shear) == 2 or len(shear) == 4), \ raise TypeError("shear should be a sequence of length 2 or 4.")
"shear should be a list or tuple and it must be of length 2 or 4." if len(shear) not in (2, 4):
# X-Axis shear with [min, max] raise ValueError("shear should be sequence of length 2 or 4.")
if len(shear) == 2:
self.shear = [shear[0], shear[1], 0., 0.] self.shear = [float(s) for s in shear]
elif len(shear) == 4:
self.shear = [s for s in shear]
else: else:
self.shear = shear self.shear = shear
...@@ -1249,45 +1258,54 @@ class RandomAffine(object): ...@@ -1249,45 +1258,54 @@ class RandomAffine(object):
self.fillcolor = fillcolor self.fillcolor = fillcolor
@staticmethod @staticmethod
def get_params(degrees, translate, scale_ranges, shears, img_size): def get_params(
degrees: List[float],
translate: Optional[List[float]],
scale_ranges: Optional[List[float]],
shears: Optional[List[float]],
img_size: List[int]
) -> Tuple[float, Tuple[int, int], float, Tuple[float, float]]:
"""Get parameters for affine transformation """Get parameters for affine transformation
Returns: Returns:
sequence: params to be passed to the affine transformation params to be passed to the affine transformation
""" """
angle = random.uniform(degrees[0], degrees[1]) angle = float(torch.empty(1).uniform_(float(degrees[0]), float(degrees[1])).item())
if translate is not None: if translate is not None:
max_dx = translate[0] * img_size[0] max_dx = float(translate[0] * img_size[0])
max_dy = translate[1] * img_size[1] max_dy = float(translate[1] * img_size[1])
translations = (np.round(random.uniform(-max_dx, max_dx)), tx = int(round(torch.empty(1).uniform_(-max_dx, max_dx).item()))
np.round(random.uniform(-max_dy, max_dy))) ty = int(round(torch.empty(1).uniform_(-max_dy, max_dy).item()))
translations = (tx, ty)
else: else:
translations = (0, 0) translations = (0, 0)
if scale_ranges is not None: if scale_ranges is not None:
scale = random.uniform(scale_ranges[0], scale_ranges[1]) scale = float(torch.empty(1).uniform_(scale_ranges[0], scale_ranges[1]).item())
else: else:
scale = 1.0 scale = 1.0
shear_x = shear_y = 0.0
if shears is not None: if shears is not None:
if len(shears) == 2: shear_x = float(torch.empty(1).uniform_(shears[0], shears[1]).item())
shear = [random.uniform(shears[0], shears[1]), 0.] if len(shears) == 4:
elif len(shears) == 4: shear_y = float(torch.empty(1).uniform_(shears[2], shears[3]).item())
shear = [random.uniform(shears[0], shears[1]),
random.uniform(shears[2], shears[3])] shear = (shear_x, shear_y)
else:
shear = 0.0
return angle, translations, scale, shear return angle, translations, scale, shear
def __call__(self, img): def forward(self, img):
""" """
img (PIL Image): Image to be transformed. img (PIL Image or Tensor): Image to be transformed.
Returns: Returns:
PIL Image: Affine transformed image. PIL Image or Tensor: Affine transformed image.
""" """
ret = self.get_params(self.degrees, self.translate, self.scale, self.shear, img.size)
img_size = F._get_image_size(img)
ret = self.get_params(self.degrees, self.translate, self.scale, self.shear, img_size)
return F.affine(img, *ret, resample=self.resample, fillcolor=self.fillcolor) return F.affine(img, *ret, resample=self.resample, fillcolor=self.fillcolor)
def __repr__(self): def __repr__(self):
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment