Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pil_to_tensor to functionals #2092

Merged
merged 23 commits into from
May 18, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions test/test_transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,50 @@ def test_accimage_to_tensor(self):
self.assertEqual(expected_output.size(), output.size())
self.assertTrue(np.allclose(output.numpy(), expected_output.numpy()))

def test_as_tensor(self):
test_channels = [1, 3, 4]
height, width = 4, 4
trans = transforms.AsTensor()

with self.assertRaises(TypeError):
trans(np.random.rand(1, height, width).tolist())

with self.assertRaises(ValueError):
trans(np.random.rand(height))
trans(np.random.rand(1, 1, height, width))

for channels in test_channels:
input_data = torch.ByteTensor(channels, height, width).random_(0, 255)
img = transforms.ToPILImage()(input_data)
output = trans(img)
self.assertTrue(np.allclose(input_data.numpy(), output.numpy()))

ndarray = np.random.randint(low=0, high=255, size=(height, width, channels)).astype(np.uint8)
output = trans(ndarray)
expected_output = ndarray.transpose((2, 0, 1))
self.assertTrue(np.allclose(output.numpy(), expected_output))

ndarray = np.random.rand(height, width, channels).astype(np.float32)
output = trans(ndarray)
expected_output = ndarray.transpose((2, 0, 1))
self.assertTrue(np.allclose(output.numpy(), expected_output))

# separate test for mode '1' PIL images
input_data = torch.ByteTensor(1, height, width).bernoulli_()
img = transforms.ToPILImage()(input_data.mul(255)).convert('1')
output = trans(img)
self.assertTrue(np.allclose(input_data.numpy(), output.numpy()))

@unittest.skipIf(accimage is None, 'accimage not available')
def test_accimage_as_tensor(self):
trans = transforms.AsTensor()

expected_output = trans(Image.open(GRACE_HOPPER).convert('RGB'))
output = trans(accimage.Image(GRACE_HOPPER))

self.assertEqual(expected_output.size(), output.size())
self.assertTrue(np.allclose(output.numpy(), expected_output.numpy()))

@unittest.skipIf(accimage is None, 'accimage not available')
def test_accimage_resize(self):
trans = transforms.Compose([
Expand Down
39 changes: 39 additions & 0 deletions torchvision/transforms/functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,45 @@ def to_tensor(pic):
return img


def as_tensor(pic):
"""Convert a ``PIL Image`` or ``numpy.ndarray`` to tensor of same type.

See ``AsTensor`` for more details.

Args:
pic (PIL Image or numpy.ndarray): Image to be converted to tensor.

Returns:
Tensor: Converted image.
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we indeed only consider that this function only supports the PIL -> tensor conversion, then maybe a better name would be pil_to_tensor or something like that? Open to suggestions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you take a second pass at your earliest convenience?

One of the tests is a little awkward in that ToPILImage converts FloatTensors to bytes.
The other thing was I'm unsure of the parameter name "swap_to_channelsfirst".

Let me know.

if not(_is_pil_image(pic) or _is_numpy(pic)):
raise TypeError('pic should be PIL Image or ndarray. Got {}'.format(type(pic)))

if _is_numpy(pic) and not _is_numpy_image(pic):
raise ValueError('pic should be 2/3 dimensional. Got {} dimensions.'.format(pic.ndim))

if isinstance(pic, np.ndarray):
# handle numpy array
if pic.ndim == 2:
pic = pic[:, :, None]

img = torch.as_tensor(pic.transpose((2, 0, 1)))
return img

if accimage is not None and isinstance(pic, accimage.Image):
xksteven marked this conversation as resolved.
Show resolved Hide resolved
nppic = np.zeros([pic.channels, pic.height, pic.width], dtype=np.float32)
pic.copyto(nppic)
return torch.as_tensor(nppic)

# handle PIL Image
img = torch.as_tensor(np.asarray(pic))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line will still produce the same bug mentioned in #2194. Converting to numpy with np.asarray(pic) will keep the PIL image non-writeable. If instead we use np.array(pic), the bug #2194 would not appear. But I believe this is no real fix because np.array(pic) copies the data which might be unintended behavior here.

Copy link
Member

@fmassa fmassa May 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we will wait until PyTorch fixes this behavior in master, as making a copy would be fairly expensive.

If the warnings are too annoying, an alternative would be to only use asarray if the array is writeable, and use array if it is non-writeable


img = img.view(pic.size[1], pic.size[0], len(pic.getbands()))
# put it from HWC to CHW format
img = img.permute((2, 0, 1))
return img


def to_pil_image(pic, mode=None):
"""Convert a tensor or an ndarray to PIL Image.

Expand Down
24 changes: 23 additions & 1 deletion torchvision/transforms/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from . import functional as F


__all__ = ["Compose", "ToTensor", "ToPILImage", "Normalize", "Resize", "Scale", "CenterCrop", "Pad",
__all__ = ["Compose", "ToTensor", "AsTensor", "ToPILImage", "Normalize", "Resize", "Scale", "CenterCrop", "Pad",
"Lambda", "RandomApply", "RandomChoice", "RandomOrder", "RandomCrop", "RandomHorizontalFlip",
"RandomVerticalFlip", "RandomResizedCrop", "RandomSizedCrop", "FiveCrop", "TenCrop", "LinearTransformation",
"ColorJitter", "RandomRotation", "RandomAffine", "Grayscale", "RandomGrayscale",
Expand Down Expand Up @@ -95,6 +95,28 @@ def __repr__(self):
return self.__class__.__name__ + '()'


class AsTensor(object):
"""Convert a ``PIL Image`` or ``numpy.ndarray`` to tensor of the same type.

Converts a PIL Image or numpy.ndarray (H x W x C) to a torch.Tensor of shape (C x H x W)
if the PIL Image belongs to one of the modes (L, LA, P, I, F, RGB, YCbCr, RGBA, CMYK, 1)
or if the numpy.ndarray has dtype = np.uint8
"""

def __call__(self, pic):
"""
Args:
pic (PIL Image or numpy.ndarray): Image to be converted to tensor.

Returns:
Tensor: Converted image.
"""
return F.as_tensor(pic)

def __repr__(self):
return self.__class__.__name__ + '()'


class ToPILImage(object):
"""Convert a tensor or an ndarray to PIL Image.

Expand Down