Commit 55088157 authored by ptrblck's avatar ptrblck Committed by Francisco Massa
Browse files

Add shear parallel to y-axis (#1070)

* initial commit

* add more checks, fix lint, fix doc
parent 8350645b
...@@ -1087,15 +1087,15 @@ class Tester(unittest.TestCase): ...@@ -1087,15 +1087,15 @@ class Tester(unittest.TestCase):
def _test_transformation(a, t, s, sh): def _test_transformation(a, t, s, sh):
a_rad = math.radians(a) a_rad = math.radians(a)
s_rad = math.radians(sh) s_rad = [math.radians(sh_) for sh_ in sh]
# 1) Check transformation matrix: # 1) Check transformation matrix:
c_matrix = np.array([[1.0, 0.0, cnt[0]], [0.0, 1.0, cnt[1]], [0.0, 0.0, 1.0]]) c_matrix = np.array([[1.0, 0.0, cnt[0]], [0.0, 1.0, cnt[1]], [0.0, 0.0, 1.0]])
c_inv_matrix = np.linalg.inv(c_matrix) c_inv_matrix = np.linalg.inv(c_matrix)
t_matrix = np.array([[1.0, 0.0, t[0]], t_matrix = np.array([[1.0, 0.0, t[0]],
[0.0, 1.0, t[1]], [0.0, 1.0, t[1]],
[0.0, 0.0, 1.0]]) [0.0, 0.0, 1.0]])
r_matrix = np.array([[s * math.cos(a_rad), -s * math.sin(a_rad + s_rad), 0.0], r_matrix = np.array([[s * math.cos(a_rad + s_rad[1]), -s * math.sin(a_rad + s_rad[0]), 0.0],
[s * math.sin(a_rad), s * math.cos(a_rad + s_rad), 0.0], [s * math.sin(a_rad + s_rad[1]), s * math.cos(a_rad + s_rad[0]), 0.0],
[0.0, 0.0, 1.0]]) [0.0, 0.0, 1.0]])
true_matrix = np.dot(t_matrix, np.dot(c_matrix, np.dot(r_matrix, c_inv_matrix))) true_matrix = np.dot(t_matrix, np.dot(c_matrix, np.dot(r_matrix, c_inv_matrix)))
result_matrix = _to_3x3_inv(F._get_inverse_affine_matrix(center=cnt, angle=a, result_matrix = _to_3x3_inv(F._get_inverse_affine_matrix(center=cnt, angle=a,
...@@ -1124,18 +1124,18 @@ class Tester(unittest.TestCase): ...@@ -1124,18 +1124,18 @@ class Tester(unittest.TestCase):
# Test rotation # Test rotation
a = 45 a = 45
_test_transformation(a=a, t=(0, 0), s=1.0, sh=0.0) _test_transformation(a=a, t=(0, 0), s=1.0, sh=(0.0, 0.0))
# Test translation # Test translation
t = [10, 15] t = [10, 15]
_test_transformation(a=0.0, t=t, s=1.0, sh=0.0) _test_transformation(a=0.0, t=t, s=1.0, sh=(0.0, 0.0))
# Test scale # Test scale
s = 1.2 s = 1.2
_test_transformation(a=0.0, t=(0.0, 0.0), s=s, sh=0.0) _test_transformation(a=0.0, t=(0.0, 0.0), s=s, sh=(0.0, 0.0))
# Test shear # Test shear
sh = 45.0 sh = [45.0, 25.0]
_test_transformation(a=0.0, t=(0.0, 0.0), s=1.0, sh=sh) _test_transformation(a=0.0, t=(0.0, 0.0), s=1.0, sh=sh)
# Test rotation, scale, translation, shear # Test rotation, scale, translation, shear
...@@ -1143,7 +1143,7 @@ class Tester(unittest.TestCase): ...@@ -1143,7 +1143,7 @@ class Tester(unittest.TestCase):
for t1 in range(-10, 10, 5): for t1 in range(-10, 10, 5):
for s in [0.75, 0.98, 1.0, 1.1, 1.2]: for s in [0.75, 0.98, 1.0, 1.1, 1.2]:
for sh in range(-15, 15, 5): for sh in range(-15, 15, 5):
_test_transformation(a=a, t=(t1, t1), s=s, sh=sh) _test_transformation(a=a, t=(t1, t1), s=s, sh=(sh, sh))
def test_random_rotation(self): def test_random_rotation(self):
...@@ -1182,11 +1182,12 @@ class Tester(unittest.TestCase): ...@@ -1182,11 +1182,12 @@ class Tester(unittest.TestCase):
transforms.RandomAffine([-90, 90], translate=[0.2, 0.2], scale=[0.5, 0.5], shear=-7) transforms.RandomAffine([-90, 90], translate=[0.2, 0.2], scale=[0.5, 0.5], shear=-7)
transforms.RandomAffine([-90, 90], translate=[0.2, 0.2], scale=[0.5, 0.5], shear=[-10]) transforms.RandomAffine([-90, 90], translate=[0.2, 0.2], scale=[0.5, 0.5], shear=[-10])
transforms.RandomAffine([-90, 90], translate=[0.2, 0.2], scale=[0.5, 0.5], shear=[-10, 0, 10]) transforms.RandomAffine([-90, 90], translate=[0.2, 0.2], scale=[0.5, 0.5], shear=[-10, 0, 10])
transforms.RandomAffine([-90, 90], translate=[0.2, 0.2], scale=[0.5, 0.5], shear=[-10, 0, 10, 0, 10])
x = np.zeros((100, 100, 3), dtype=np.uint8) x = np.zeros((100, 100, 3), dtype=np.uint8)
img = F.to_pil_image(x) img = F.to_pil_image(x)
t = transforms.RandomAffine(10, translate=[0.5, 0.3], scale=[0.7, 1.3], shear=[-10, 10]) t = transforms.RandomAffine(10, translate=[0.5, 0.3], scale=[0.7, 1.3], shear=[-10, 10, 20, 40])
for _ in range(100): for _ in range(100):
angle, translations, scale, shear = t.get_params(t.degrees, t.translate, t.scale, t.shear, angle, translations, scale, shear = t.get_params(t.degrees, t.translate, t.scale, t.shear,
img_size=img.size) img_size=img.size)
...@@ -1196,7 +1197,8 @@ class Tester(unittest.TestCase): ...@@ -1196,7 +1197,8 @@ class Tester(unittest.TestCase):
assert -img.size[1] * 0.5 <= translations[1] <= img.size[1] * 0.5, \ assert -img.size[1] * 0.5 <= translations[1] <= img.size[1] * 0.5, \
"{} vs {}".format(translations[1], img.size[1] * 0.5) "{} vs {}".format(translations[1], img.size[1] * 0.5)
assert 0.7 < scale < 1.3 assert 0.7 < scale < 1.3
assert -10 < shear < 10 assert -10 < shear[0] < 10
assert -20 < shear[1] < 40
# Checking if RandomAffine can be printed as string # Checking if RandomAffine can be printed as string
t.__repr__() t.__repr__()
......
...@@ -722,20 +722,29 @@ def _get_inverse_affine_matrix(center, angle, translate, scale, shear): ...@@ -722,20 +722,29 @@ def _get_inverse_affine_matrix(center, angle, translate, scale, shear):
# where T is translation matrix: [1, 0, tx | 0, 1, ty | 0, 0, 1] # where T is translation matrix: [1, 0, tx | 0, 1, ty | 0, 0, 1]
# C is translation matrix to keep center: [1, 0, cx | 0, 1, cy | 0, 0, 1] # C is translation matrix to keep center: [1, 0, cx | 0, 1, cy | 0, 0, 1]
# RSS is rotation with scale and shear matrix # RSS is rotation with scale and shear matrix
# RSS(a, scale, shear) = [ cos(a)*scale -sin(a + shear)*scale 0] # RSS(a, scale, shear) = [ cos(a + shear_y)*scale -sin(a + shear_x)*scale 0]
# [ sin(a)*scale cos(a + shear)*scale 0] # [ sin(a + shear_y)*scale cos(a + shear_x)*scale 0]
# [ 0 0 1] # [ 0 0 1]
# Thus, the inverse is M^-1 = C * RSS^-1 * C^-1 * T^-1 # Thus, the inverse is M^-1 = C * RSS^-1 * C^-1 * T^-1
angle = math.radians(angle) angle = math.radians(angle)
shear = math.radians(shear) if isinstance(shear, (tuple, list)) and len(shear) == 2:
shear = [math.radians(s) for s in shear]
elif isinstance(shear, numbers.Number):
shear = math.radians(shear)
shear = [shear, 0]
else:
raise ValueError(
"Shear should be a single value or a tuple/list containing " +
"two values. Got {}".format(shear))
scale = 1.0 / scale scale = 1.0 / scale
# Inverted rotation matrix with scale and shear # Inverted rotation matrix with scale and shear
d = math.cos(angle + shear) * math.cos(angle) + math.sin(angle + shear) * math.sin(angle) d = math.cos(angle + shear[0]) * math.cos(angle + shear[1]) + \
math.sin(angle + shear[0]) * math.sin(angle + shear[1])
matrix = [ matrix = [
math.cos(angle + shear), math.sin(angle + shear), 0, math.cos(angle + shear[0]), math.sin(angle + shear[0]), 0,
-math.sin(angle), math.cos(angle), 0 -math.sin(angle + shear[1]), math.cos(angle + shear[1]), 0
] ]
matrix = [scale / d * m for m in matrix] matrix = [scale / d * m for m in matrix]
...@@ -757,7 +766,9 @@ def affine(img, angle, translate, scale, shear, resample=0, fillcolor=None): ...@@ -757,7 +766,9 @@ def affine(img, angle, translate, scale, shear, resample=0, fillcolor=None):
angle (float or int): rotation angle in degrees between -180 and 180, clockwise direction. angle (float or int): rotation angle in degrees between -180 and 180, clockwise direction.
translate (list or tuple of integers): horizontal and vertical translations (post-rotation translation) translate (list or tuple of integers): horizontal and vertical translations (post-rotation translation)
scale (float): overall scale scale (float): overall scale
shear (float): shear angle value in degrees between -180 to 180, clockwise direction. shear (float or tuple or list): shear angle value in degrees between -180 to 180, clockwise direction.
If a tuple of list is specified, the first value corresponds to a shear parallel to the x axis, while
the second value corresponds to a shear parallel to the y axis.
resample (``PIL.Image.NEAREST`` or ``PIL.Image.BILINEAR`` or ``PIL.Image.BICUBIC``, optional): resample (``PIL.Image.NEAREST`` or ``PIL.Image.BILINEAR`` or ``PIL.Image.BICUBIC``, optional):
An optional resampling filter. An optional resampling filter.
See `filters`_ for more information. See `filters`_ for more information.
......
...@@ -1013,8 +1013,11 @@ class RandomAffine(object): ...@@ -1013,8 +1013,11 @@ class RandomAffine(object):
scale (tuple, optional): scaling factor interval, e.g (a, b), then scale is scale (tuple, optional): scaling factor interval, e.g (a, b), then scale is
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 degrees is a number instead of sequence like (min, max), the range of degrees If shear is a number, a shear parallel to the x axis in the range (-shear, +shear)
will be (-degrees, +degrees). Will not apply shear by default will be apllied. 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,
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
resample ({PIL.Image.NEAREST, PIL.Image.BILINEAR, PIL.Image.BICUBIC}, optional): resample ({PIL.Image.NEAREST, PIL.Image.BILINEAR, PIL.Image.BICUBIC}, 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.
...@@ -1057,9 +1060,14 @@ class RandomAffine(object): ...@@ -1057,9 +1060,14 @@ class RandomAffine(object):
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) self.shear = (-shear, shear)
else: else:
assert isinstance(shear, (tuple, list)) and len(shear) == 2, \ assert isinstance(shear, (tuple, list)) and \
"shear should be a list or tuple and it must be of length 2." (len(shear) == 2 or len(shear) == 4), \
self.shear = shear "shear should be a list or tuple and it must be of length 2 or 4."
# X-Axis shear with [min, max]
if len(shear) == 2:
self.shear = [shear[0], shear[1], 0., 0.]
elif len(shear) == 4:
self.shear = [s for s in shear]
else: else:
self.shear = shear self.shear = shear
...@@ -1088,7 +1096,11 @@ class RandomAffine(object): ...@@ -1088,7 +1096,11 @@ class RandomAffine(object):
scale = 1.0 scale = 1.0
if shears is not None: if shears is not None:
shear = random.uniform(shears[0], shears[1]) if len(shears) == 2:
shear = [random.uniform(shears[0], shears[1]), 0.]
elif len(shears) == 4:
shear = [random.uniform(shears[0], shears[1]),
random.uniform(shears[2], shears[3])]
else: else:
shear = 0.0 shear = 0.0
......
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