diff --git a/MANIFEST.in b/MANIFEST.in index dc8f75ecc362021410f6686954b25c1d98966bc8..cf1d827b4de456cfd9faa016ac18001948a7caf3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ include README.rst buildout.cfg COPYING version.txt requirements.txt recursive-include doc *.rst *.png *.ico *.txt -recursive-include bob *.json +recursive-include bob *.json *.png diff --git a/bob/ip/binseg/data/transforms.py b/bob/ip/binseg/data/transforms.py index a347fcbb3af00052ea79dc7394a2945a006986e1..bea81bcd9dbf7ba998795f96c35ccae3b7c1b417 100644 --- a/bob/ip/binseg/data/transforms.py +++ b/bob/ip/binseg/data/transforms.py @@ -18,8 +18,6 @@ import PIL.Image import torchvision.transforms import torchvision.transforms.functional -import bob.core - class TupleMixin: """Adds support to work with tuples of objects to torchvision transforms""" @@ -104,12 +102,17 @@ class SingleAutoLevel16to8: To auto-level, we calculate the maximum and the minimum of the image, and consider such a range should be mapped to the [0,255] range of the destination image. + """ def __call__(self, img): + imin, imax = img.getextrema() + irange = imax - imin return PIL.Image.fromarray( - bob.core.convert(img, "uint8", (0, 255), img.getextrema()) - ) + numpy.round( + 255.0 * (numpy.array(img).astype(float) - imin) / irange + ).astype("uint8"), + ).convert("L") class AutoLevel16to8(TupleMixin, SingleAutoLevel16to8): @@ -121,6 +124,7 @@ class AutoLevel16to8(TupleMixin, SingleAutoLevel16to8): consider such a range should be mapped to the [0,255] range of the destination image. """ + pass @@ -132,6 +136,7 @@ class SingleToRGB: defaults. This may be aggressive if applied to 16-bit images without further considerations. """ + def __call__(self, img): return img.convert(mode="RGB") @@ -195,8 +200,8 @@ class RandomRotation(torchvision.transforms.RandomRotation): """ def __init__(self, p=0.5, **kwargs): - kwargs.setdefault('degrees', 15) - kwargs.setdefault('resample', PIL.Image.BILINEAR) + kwargs.setdefault("degrees", 15) + kwargs.setdefault("resample", PIL.Image.BILINEAR) super(RandomRotation, self).__init__(**kwargs) self.p = p @@ -205,16 +210,17 @@ class RandomRotation(torchvision.transforms.RandomRotation): if random.random() < self.p: angle = self.get_params(self.degrees) return [ - torchvision.transforms.functional.rotate(img, angle, - self.resample, self.expand, self.center) + torchvision.transforms.functional.rotate( + img, angle, self.resample, self.expand, self.center + ) for img in args - ] + ] else: return args def __repr__(self): retval = super(RandomRotation, self).__repr__() - return retval.replace('(', f'(p={self.p},', 1) + return retval.replace("(", f"(p={self.p},", 1) class ColorJitter(torchvision.transforms.ColorJitter): @@ -243,10 +249,10 @@ class ColorJitter(torchvision.transforms.ColorJitter): """ def __init__(self, p=0.5, **kwargs): - kwargs.setdefault('brightness', 0.3) - kwargs.setdefault('contrast', 0.3) - kwargs.setdefault('saturation', 0.02) - kwargs.setdefault('hue', 0.02) + kwargs.setdefault("brightness", 0.3) + kwargs.setdefault("contrast", 0.3) + kwargs.setdefault("saturation", 0.02) + kwargs.setdefault("hue", 0.02) super(ColorJitter, self).__init__(**kwargs) self.p = p @@ -259,4 +265,4 @@ class ColorJitter(torchvision.transforms.ColorJitter): def __repr__(self): retval = super(ColorJitter, self).__repr__() - return retval.replace('(', f'(p={self.p},', 1) + return retval.replace("(", f"(p={self.p},", 1) diff --git a/bob/ip/binseg/test/test_transforms.py b/bob/ip/binseg/test/test_transforms.py index 826343f632d9c4e7b62fbecea68df45bcf71728b..a1967fe54894eccd1fe818c8564c08d92c98ad88 100644 --- a/bob/ip/binseg/test/test_transforms.py +++ b/bob/ip/binseg/test/test_transforms.py @@ -4,7 +4,10 @@ import random import nose.tools +import pkg_resources + import numpy +import PIL.Image import torch import torchvision.transforms.functional @@ -93,7 +96,7 @@ def test_pad_default(): # checks that the border introduced with padding is all about "fill" img_t = numpy.array(img_t) img_t[idx] = 0 - border_size_plane = (img_t[:,:,0].size - numpy.array(img)[:,:,0].size) + border_size_plane = img_t[:, :, 0].size - numpy.array(img)[:, :, 0].size nose.tools.eq_(img_t.sum(), 0) gt_t = numpy.array(gt_t) @@ -131,8 +134,8 @@ def test_pad_2tuple(): # checks that the border introduced with padding is all about "fill" img_t = numpy.array(img_t) img_t[idx] = 0 - border_size_plane = (img_t[:,:,0].size - numpy.array(img)[:,:,0].size) - expected_sum = sum((fill[k]*border_size_plane) for k in range(3)) + border_size_plane = img_t[:, :, 0].size - numpy.array(img)[:, :, 0].size + expected_sum = sum((fill[k] * border_size_plane) for k in range(3)) nose.tools.eq_(img_t.sum(), expected_sum) gt_t = numpy.array(gt_t) @@ -170,8 +173,8 @@ def test_pad_4tuple(): # checks that the border introduced with padding is all about "fill" img_t = numpy.array(img_t) img_t[idx] = 0 - border_size_plane = (img_t[:,:,0].size - numpy.array(img)[:,:,0].size) - expected_sum = sum((fill[k]*border_size_plane) for k in range(3)) + border_size_plane = img_t[:, :, 0].size - numpy.array(img)[:, :, 0].size + expected_sum = sum((fill[k] * border_size_plane) for k in range(3)) nose.tools.eq_(img_t.sum(), expected_sum) gt_t = numpy.array(gt_t) @@ -194,7 +197,7 @@ def test_resize_downscale_w(): img, gt, mask = [_create_img(im_size) for i in range(3)] nose.tools.eq_(img.size, (im_size[2], im_size[1])) # confirms the above img_t, gt_t, mask_t = transforms(img, gt, mask) - new_size = (new_size, (new_size*im_size[1])/im_size[2]) + new_size = (new_size, (new_size * im_size[1]) / im_size[2]) nose.tools.eq_(img_t.size, new_size) nose.tools.eq_(gt_t.size, new_size) nose.tools.eq_(mask_t.size, new_size) @@ -224,8 +227,8 @@ def test_crop(): # test idx = ( - slice(crop_size[0], crop_size[0]+crop_size[2]), - slice(crop_size[1], crop_size[1]+crop_size[3]), + slice(crop_size[0], crop_size[0] + crop_size[2]), + slice(crop_size[1], crop_size[1] + crop_size[3]), slice(0, im_size[0]), ) transforms = Crop(*crop_size) @@ -297,7 +300,7 @@ def test_rotation(): assert numpy.any(numpy.array(img1_t) != numpy.array(img)) # asserts two random transforms are not the same - img_t2, = transforms(img) + (img_t2,) = transforms(img) assert numpy.any(numpy.array(img_t2) != numpy.array(img1_t)) @@ -327,15 +330,40 @@ def test_color_jitter(): def test_compose(): - transforms = Compose([ - RandomVerticalFlip(p=1), - RandomHorizontalFlip(p=1), - RandomVerticalFlip(p=1), - RandomHorizontalFlip(p=1), - ]) + transforms = Compose( + [ + RandomVerticalFlip(p=1), + RandomHorizontalFlip(p=1), + RandomVerticalFlip(p=1), + RandomHorizontalFlip(p=1), + ] + ) img, gt, mask = [_create_img((3, 24, 42)) for i in range(3)] img_t, gt_t, mask_t = transforms(img, gt, mask) assert numpy.all(numpy.array(img_t) == numpy.array(img)) assert numpy.all(numpy.array(gt_t) == numpy.array(gt)) assert numpy.all(numpy.array(mask_t) == numpy.array(mask)) + + +def test_16bit_autolevel(): + + test_image_path = pkg_resources.resource_filename( + __name__, "testimg-16bit.png" + ) + # the way to load a 16-bit PNG image correctly, according to: + # https://stackoverflow.com/questions/32622658/read-16-bit-png-image-file-using-python + # https://github.com/python-pillow/Pillow/issues/3011 + img = PIL.Image.fromarray( + numpy.array( + PIL.Image.open("bob/ip/binseg/test/testimg-16bit.png") + ).astype("uint16") + ) + nose.tools.eq_(img.mode, "I;16") + nose.tools.eq_(img.getextrema(), (0, 65281)) + + timg = SingleAutoLevel16to8()(img) + nose.tools.eq_(timg.mode, "L") + nose.tools.eq_(timg.getextrema(), (0, 255)) + #timg.show() + #import ipdb; ipdb.set_trace() diff --git a/bob/ip/binseg/test/testimg-16bit.png b/bob/ip/binseg/test/testimg-16bit.png new file mode 100644 index 0000000000000000000000000000000000000000..1bd5c6ad984e9200e0611d492149126b628fd74d Binary files /dev/null and b/bob/ip/binseg/test/testimg-16bit.png differ diff --git a/conda/meta.yaml b/conda/meta.yaml index a6e591aa6c590bb61dc4358b47d830c65b03cc2b..b0b78dc569dc0490873f3a81547e3def158661f3 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -25,21 +25,21 @@ requirements: host: - python {{ python }} - setuptools {{ setuptools }} - - torchvision {{ torchvision }} # [linux] - - pytorch {{ pytorch }} # [linux] - numpy {{ numpy }} + - h5py {{ h5py }} + - pytorch {{ pytorch }} # [linux] + - torchvision {{ torchvision }} # [linux] - bob.extension - - bob.core - - bob.io.base run: - python - setuptools + - {{ pin_compatible('numpy') }} + - {{ pin_compatible('pillow') }} + - {{ pin_compatible('pandas') }} + - {{ pin_compatible('matplotlib') }} - {{ pin_compatible('pytorch') }} # [linux] - {{ pin_compatible('torchvision') }} # [linux] - - {{ pin_compatible('numpy') }} - - pandas - - pillow - - matplotlib + - {{ pin_compatible('h5py') }} - tqdm - tabulate diff --git a/requirements.txt b/requirements.txt index 8eff46db1eacdeae2082181c185a43425a537d36..e394a12df53ee4280702ad258089f3704c007bd8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,8 @@ -bob.core bob.extension -bob.io.base matplotlib numpy pandas +h5py pillow setuptools tabulate