diff --git a/.circleci/unittest/linux/scripts/install.sh b/.circleci/unittest/linux/scripts/install.sh index 1a3e5c6f4d2..655de2aab3a 100755 --- a/.circleci/unittest/linux/scripts/install.sh +++ b/.circleci/unittest/linux/scripts/install.sh @@ -26,5 +26,11 @@ fi printf "Installing PyTorch with %s\n" "${cudatoolkit}" conda install -y -c "pytorch-${UPLOAD_CHANNEL}" -c conda-forge "pytorch-${UPLOAD_CHANNEL}"::pytorch "${cudatoolkit}" +if [ $PYTHON_VERSION == "3.6" ]; then + printf "Installing minimal PILLOW version\n" + # Install the minimal PILLOW version. Otherwise, let setup.py install the latest + pip install pillow==4.1.1 +fi + printf "* Installing torchvision\n" python setup.py develop diff --git a/test/common_utils.py b/test/common_utils.py index 7e16864d56c..4393e077392 100644 --- a/test/common_utils.py +++ b/test/common_utils.py @@ -18,12 +18,14 @@ from _utils_internal import get_relative_path import numpy as np -from PIL import Image +from PIL import Image, __version__ as PILLOW_VERSION IS_PY39 = sys.version_info.major == 3 and sys.version_info.minor == 9 PY39_SEGFAULT_SKIP_MSG = "Segmentation fault with Python 3.9, see https://github.com/pytorch/vision/issues/3367" PY39_SKIP = unittest.skipIf(IS_PY39, PY39_SEGFAULT_SKIP_MSG) +PILLOW_VERSION = tuple(int(x) for x in PILLOW_VERSION.split('.')) + @contextlib.contextmanager def get_tmp_dir(src=None, **kwargs): diff --git a/test/test_functional_tensor.py b/test/test_functional_tensor.py index 42d44dfdbd9..2f4c898dcd7 100644 --- a/test/test_functional_tensor.py +++ b/test/test_functional_tensor.py @@ -11,7 +11,7 @@ import torchvision.transforms.functional as F from torchvision.transforms import InterpolationMode -from common_utils import TransformsTester +from common_utils import TransformsTester, PILLOW_VERSION from typing import Dict, List, Sequence, Tuple @@ -624,6 +624,7 @@ def _test_affine_all_ops(self, tensor, pil_img, scripted_affine): ) ) + @unittest.skipIf(PILLOW_VERSION < (5, 0, 0), "affine requires PIL >= 5.0.0") def test_affine(self): # Tests on square and rectangular images scripted_affine = torch.jit.script(F.affine) @@ -712,6 +713,7 @@ def _test_rotate_all_options(self, tensor, pil_img, scripted_rotate, centers): ) ) + @unittest.skipIf(PILLOW_VERSION < (5, 2, 0), "rotate requires PIL >= 5.2.0") def test_rotate(self): # Tests on square image scripted_rotate = torch.jit.script(F.rotate) @@ -788,6 +790,7 @@ def _test_perspective(self, tensor, pil_img, scripted_transform, test_configs): ) ) + @unittest.skipIf(PILLOW_VERSION < (5, 0, 0), "perspective requires PIL >= 5.0.0") def test_perspective(self): from torchvision.transforms import RandomPerspective diff --git a/test/test_transforms.py b/test/test_transforms.py index 0a01247aa87..fb11898d2d6 100644 --- a/test/test_transforms.py +++ b/test/test_transforms.py @@ -21,7 +21,7 @@ except ImportError: stats = None -from common_utils import cycle_over, int_dtypes, float_dtypes +from common_utils import cycle_over, int_dtypes, float_dtypes, PILLOW_VERSION GRACE_HOPPER = get_file_path_2( @@ -238,6 +238,7 @@ def test_randomperspective(self): self.assertGreater(torch.nn.functional.mse_loss(tr_img, F.to_tensor(img)) + 0.3, torch.nn.functional.mse_loss(tr_img2, F.to_tensor(img))) + @unittest.skipIf(PILLOW_VERSION < (5, 0, 0), "fill background requires PIL >= 5.0.0") def test_randomperspective_fill(self): # assert fill being either a Sequence or a Number @@ -506,6 +507,7 @@ def test_lambda(self): trans.__repr__() @unittest.skipIf(stats is None, 'scipy.stats not available') + @unittest.skipIf(PILLOW_VERSION < (5, 2, 0), "fill background requires PIL >= 5.2.0") def test_random_apply(self): random_state = random.getstate() random.seed(42) @@ -1264,7 +1266,7 @@ def test_adjust_contrast(self): y_ans = np.array(y_ans, dtype=np.uint8).reshape(x_shape) self.assertTrue(np.allclose(y_np, y_ans)) - @unittest.skipIf(Image.__version__ >= '7', "Temporarily disabled") + @unittest.skipIf(PILLOW_VERSION >= (7,), "Temporarily disabled") def test_adjust_saturation(self): x_shape = [2, 2, 3] x_data = [0, 5, 13, 54, 135, 226, 37, 8, 234, 90, 255, 1] @@ -1322,6 +1324,7 @@ def test_adjust_hue(self): y_ans = np.array(y_ans, dtype=np.uint8).reshape(x_shape) self.assertTrue(np.allclose(y_np, y_ans)) + @unittest.skipIf(PILLOW_VERSION < (7,), "Lower PIL versions lead to slightly different results") def test_adjust_sharpness(self): x_shape = [4, 4, 3] x_data = [75, 121, 114, 105, 97, 107, 105, 32, 66, 111, 117, 114, 99, 104, 97, 0, @@ -1489,6 +1492,7 @@ def test_rotate(self): self.assertTrue(np.all(np.array(result_a) == np.array(result_b))) + @unittest.skipIf(PILLOW_VERSION < (5, 2, 0), "fill background requires PIL >= 5.2.0") def test_rotate_fill(self): img = F.to_pil_image(np.ones((100, 100, 3), dtype=np.uint8) * 255, "RGB") @@ -1943,6 +1947,7 @@ def test_random_solarize(self): ) @unittest.skipIf(stats is None, 'scipy.stats not available') + @unittest.skipIf(PILLOW_VERSION < (7,), "Lower PIL versions lead to slightly different results") def test_random_adjust_sharpness(self): self._test_randomness( F.adjust_sharpness, @@ -1966,6 +1971,7 @@ def test_random_equalize(self): [{}] ) + @unittest.skipIf(PILLOW_VERSION < (5, 2, 0), "fill background requires PIL >= 5.2.0") def test_autoaugment(self): for policy in transforms.AutoAugmentPolicy: for fill in [None, 85, (128, 128, 128)]: diff --git a/test/test_transforms_tensor.py b/test/test_transforms_tensor.py index 1bd0099af63..d62cf36d459 100644 --- a/test/test_transforms_tensor.py +++ b/test/test_transforms_tensor.py @@ -9,7 +9,7 @@ import unittest from typing import Sequence -from common_utils import TransformsTester, get_tmp_dir, int_dtypes, float_dtypes +from common_utils import TransformsTester, get_tmp_dir, int_dtypes, float_dtypes, PILLOW_VERSION NEAREST, BILINEAR, BICUBIC = InterpolationMode.NEAREST, InterpolationMode.BILINEAR, InterpolationMode.BICUBIC @@ -105,6 +105,7 @@ def test_random_solarize(self): 'solarize', 'RandomSolarize', fn_kwargs=fn_kwargs, meth_kwargs=meth_kwargs ) + @unittest.skipIf(PILLOW_VERSION < (7,), "Lower PIL versions lead to slightly different results") def test_random_adjust_sharpness(self): fn_kwargs = meth_kwargs = {"sharpness_factor": 2.0} self._test_op( diff --git a/test/test_utils.py b/test/test_utils.py index 8c4cc620229..01ba99ca160 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -7,11 +7,10 @@ import unittest from io import BytesIO import torchvision.transforms.functional as F -from PIL import Image, __version__ as PILLOW_VERSION +from PIL import Image +from common_utils import PILLOW_VERSION -PILLOW_VERSION = tuple(int(x) for x in PILLOW_VERSION.split('.')) - boxes = torch.tensor([[0, 0, 20, 20], [0, 0, 0, 0], [10, 15, 30, 35], [23, 35, 93, 95]], dtype=torch.float) @@ -110,6 +109,7 @@ def test_save_image_single_pixel_file_object(self): self.assertTrue(torch.equal(F.to_tensor(img_orig), F.to_tensor(img_bytes)), 'Pixel Image not stored in file object') + @unittest.skipIf(PILLOW_VERSION < (5, 3, 0), "draw_bounding_box is only available for PIL >= 5.3.0") def test_draw_boxes(self): img = torch.full((3, 100, 100), 255, dtype=torch.uint8) img_cp = img.clone() @@ -132,6 +132,7 @@ def test_draw_boxes(self): self.assertTrue(torch.all(torch.eq(boxes, boxes_cp)).item()) self.assertTrue(torch.all(torch.eq(img, img_cp)).item()) + @unittest.skipIf(PILLOW_VERSION < (5, 3, 0), "draw_bounding_box is only available for PIL >= 5.3.0") def test_draw_boxes_vanilla(self): img = torch.full((3, 100, 100), 0, dtype=torch.uint8) img_cp = img.clone() diff --git a/torchvision/utils.py b/torchvision/utils.py index 39423e3b227..0a6222928be 100644 --- a/torchvision/utils.py +++ b/torchvision/utils.py @@ -4,7 +4,7 @@ import math import warnings import numpy as np -from PIL import Image, ImageDraw, ImageFont, ImageColor +from PIL import Image, ImageDraw, ImageFont, ImageColor, __version__ as PILLOW_VERSION __all__ = ["make_grid", "save_image", "draw_bounding_boxes", "draw_segmentation_masks"] @@ -187,6 +187,10 @@ def draw_bounding_boxes( elif image.dim() != 3: raise ValueError("Pass individual images, not batches") + pillow_version = tuple(int(x) for x in PILLOW_VERSION.split('.')) + if pillow_version < (5, 3, 0): + raise ValueError("draw_bounding_boxes requires Pillow >= 5.3.0") + ndarr = image.permute(1, 2, 0).numpy() img_to_draw = Image.fromarray(ndarr)