From d56d3f2856c4d4ea067dfe53fa377b8494f41aa6 Mon Sep 17 00:00:00 2001 From: Michael Fujarski Date: Wed, 11 Jan 2023 20:42:00 +0100 Subject: [PATCH] Loading 16-bit tiff images (#5005) Pull Request regarding Issue #2987 PIL.Image conversion from I;16 to L or RGB are unsuccessful as for now. See the corresponding Issue in the Pillow GitHub (Opened 2018, so no changes to be expected) https://github.com/python-pillow/Pillow/issues/3011 The proposed changes at least fix this issue for the mode 'I;16' and delivers a possible solution for other modes (eg. I;16B/L/N). This results in a correct calculation of the preview thumbnail and the actual image, the annotation will be performed on. We have used this solution on our own dataset and created annotations accordingly. --- CHANGELOG.md | 1 + cvat/apps/engine/media_extractors.py | 37 ++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f3dd9db3659..06b498d46ea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,7 @@ from online detectors & interactors) ( non-ascii paths while adding files from "Connected file share" (issue #4428) - Removed unnecessary volumes defined in docker-compose.serverless.yml () +- Added support for Image files that use the PIL.Image.mode 'I;16' - Project import/export with skeletons (, ) - Shape color is not changed on canvas after changing a label () diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index ce81389b42f5..02a4b5fd3676 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -17,7 +17,7 @@ import numpy as np from natsort import os_sorted from pyunpack import Archive -from PIL import Image, ImageFile +from PIL import Image, ImageFile, ImageOps from random import shuffle from cvat.apps.engine.utils import rotate_image from cvat.apps.engine.models import DimensionType, SortingMethod @@ -127,9 +127,25 @@ def _get_preview(obj): else: preview = obj preview = rotate_within_exif(preview) + # TODO - Check if the other formats work. I'm only interested in I;16 for now. Sorry @:-| + # Summary: + # Images in the Format I;16 definitely don't work. Most likely I;16B/L/N won't work as well. + # Simple Conversion from I;16 to I/RGB/L doesn't work as well. + # Including any Intermediate Conversions doesn't work either. (eg. I;16 to I to L) + # Seems like an internal Bug of PIL + # See Issue for further details: https://github.com/python-pillow/Pillow/issues/3011 + # Issue was opened 2018, so don't expect any changes soon and work with manual conversions. + mode: str = preview.mode + if mode == "I;16": + preview = np.array(preview, dtype=np.uint16) # 'I;16' := Unsigned Integer 16, Grayscale + image = image - image.min() # In case the used range lies in [a, 2^16] with a > 0 + preview = preview / preview.max() * 255 # Downscale into real numbers of range [0, 255] + preview = preview.astype(np.uint8) # Floor to integers of range [0, 255] + preview = Image.fromarray(preview, mode="L") # 'L' := Unsigned Integer 8, Grayscale + preview = ImageOps.equalize(preview) # The Images need equalization. High resolution with 16-bit but only small range that actually contains information preview.thumbnail(PREVIEW_SIZE) - return preview.convert('RGB') + return preview @abstractmethod def get_image_size(self, i): @@ -589,6 +605,23 @@ def _compress_image(image_path, quality): im_data = np.array(image) im_data = im_data * (2**8 / im_data.max()) image = Image.fromarray(im_data.astype(np.int32)) + + # TODO - Check if the other formats work. I'm only interested in I;16 for now. Sorry @:-| + # Summary: + # Images in the Format I;16 definitely don't work. Most likely I;16B/L/N won't work as well. + # Simple Conversion from I;16 to I/RGB/L doesn't work as well. + # Including any Intermediate Conversions doesn't work either. (eg. I;16 to I to L) + # Seems like an internal Bug of PIL + # See Issue for further details: https://github.com/python-pillow/Pillow/issues/3011 + # Issue was opened 2018, so don't expect any changes soon and work with manual conversions. + if image.mode == "I;16": + image = np.array(image, dtype=np.uint16) # 'I;16' := Unsigned Integer 16, Grayscale + image = image - image.min() # In case the used range lies in [a, 2^16] with a > 0 + image = image / image.max() * 255 # Downscale into real numbers of range [0, 255] + image = image.astype(np.uint8) # Floor to integers of range [0, 255] + image = Image.fromarray(image, mode="L") # 'L' := Unsigned Integer 8, Grayscale + image = ImageOps.equalize(image) # The Images need equalization. High resolution with 16-bit but only small range that actually contains information + converted_image = image.convert('RGB') image.close() buf = io.BytesIO()