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):
def _test_transformation(a, t, s, sh):
a_rad = math.radians(a)
s_rad = math.radians(sh)
s_rad = [math.radians(sh_) for sh_ in sh]
# 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_inv_matrix = np.linalg.inv(c_matrix)
t_matrix = np.array([[1.0, 0.0, t[0]],
[0.0, 1.0, t[1]],
[0.0, 0.0, 1.0]])
r_matrix = np.array([[s * math.cos(a_rad), -s * math.sin(a_rad + s_rad), 0.0],
[s * math.sin(a_rad), s * math.cos(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_rad[1]), s * math.cos(a_rad + s_rad[0]), 0.0],
[0.0, 0.0, 1.0]])
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,
......@@ -1124,18 +1124,18 @@ class Tester(unittest.TestCase):
# Test rotation
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
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
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
sh = 45.0
sh = [45.0, 25.0]
_test_transformation(a=0.0, t=(0.0, 0.0), s=1.0, sh=sh)
# Test rotation, scale, translation, shear
......@@ -1143,7 +1143,7 @@ class Tester(unittest.TestCase):
for t1 in range(-10, 10, 5):
for s in [0.75, 0.98, 1.0, 1.1, 1.2]:
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):
......@@ -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=[-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)
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):
angle, translations, scale, shear = t.get_params(t.degrees, t.translate, t.scale, t.shear,
img_size=img.size)
......@@ -1196,7 +1197,8 @@ class Tester(unittest.TestCase):
assert -img.size[1] * 0.5 <= translations[1] <= img.size[1] * 0.5, \
"{} vs {}".format(translations[1], img.size[1] * 0.5)
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
t.__repr__()
......
......@@ -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]
# 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(a, scale, shear) = [ cos(a)*scale -sin(a + shear)*scale 0]
# [ sin(a)*scale cos(a + shear)*scale 0]
# RSS(a, scale, shear) = [ cos(a + shear_y)*scale -sin(a + shear_x)*scale 0]
# [ sin(a + shear_y)*scale cos(a + shear_x)*scale 0]
# [ 0 0 1]
# Thus, the inverse is M^-1 = C * RSS^-1 * C^-1 * T^-1
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
# 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 = [
math.cos(angle + shear), math.sin(angle + shear), 0,
-math.sin(angle), math.cos(angle), 0
math.cos(angle + shear[0]), math.sin(angle + shear[0]), 0,
-math.sin(angle + shear[1]), math.cos(angle + shear[1]), 0
]
matrix = [scale / d * m for m in matrix]
......@@ -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.
translate (list or tuple of integers): horizontal and vertical translations (post-rotation translation)
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):
An optional resampling filter.
See `filters`_ for more information.
......
......@@ -1013,8 +1013,11 @@ class RandomAffine(object):
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.
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
will be (-degrees, +degrees). Will not apply shear by default
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
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):
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.
......@@ -1057,9 +1060,14 @@ class RandomAffine(object):
raise ValueError("If shear is a single number, it must be positive.")
self.shear = (-shear, shear)
else:
assert isinstance(shear, (tuple, list)) and len(shear) == 2, \
"shear should be a list or tuple and it must be of length 2."
self.shear = shear
assert isinstance(shear, (tuple, list)) and \
(len(shear) == 2 or len(shear) == 4), \
"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:
self.shear = shear
......@@ -1088,7 +1096,11 @@ class RandomAffine(object):
scale = 1.0
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:
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