From 4fffff6db619ee2644a095f310e56cdbc911b931 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Tue, 19 Mar 2024 22:03:34 +0100 Subject: [PATCH 1/3] Add float TIFF test case (fails with Pillow) --- test-data/im4_float.tif | Bin 0 -> 5272 bytes test/functional/tools/image_diff.xml | 5 +++++ 2 files changed, 5 insertions(+) create mode 100644 test-data/im4_float.tif diff --git a/test-data/im4_float.tif b/test-data/im4_float.tif new file mode 100644 index 0000000000000000000000000000000000000000..21f0e11bb096db6d4ed0bbe81fbd0b5927b989e9 GIT binary patch literal 5272 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 + + + + + From 477a45401466e3ef05fe73e790160322d29975e3 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 20 Mar 2024 08:56:13 +0100 Subject: [PATCH 2/3] Add tifffile support --- lib/galaxy/tool_util/verify/__init__.py | 22 ++++++++++++++++++---- packages/tool_util/setup.cfg | 1 + 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/galaxy/tool_util/verify/__init__.py b/lib/galaxy/tool_util/verify/__init__.py index 976c8706b684..da654645d1b9 100644 --- a/lib/galaxy/tool_util/verify/__init__.py +++ b/lib/galaxy/tool_util/verify/__init__.py @@ -32,6 +32,10 @@ from PIL import Image except ImportError: pass +try: + import tifffile +except ImportError: + pass from galaxy.tool_util.parser.util import ( DEFAULT_DELTA, @@ -496,14 +500,24 @@ def get_image_metric( raise ValueError(f'No such metric: "{metric_name}"') +def _load_image(filepath: str) -> "numpy.typing.NDArray": + try: + # Try reading with tifffile first. It fails if the file is not a TIFF. + arr = tifffile.imread(filepath) + except tifffile.TiffFileError: + + # If tifffile failed, then the file is not a tifffile. In that case, try with Pillow. + with Image.open(filepath) as im: + arr = numpy.array(im) + return arr + + def files_image_diff(file1: str, file2: str, attributes: Optional[Dict[str, Any]] = None) -> None: """Check the pixel data of 2 image files for differences.""" attributes = attributes or {} - with Image.open(file1) as im1: - arr1 = numpy.array(im1) - with Image.open(file2) as im2: - arr2 = numpy.array(im2) + arr1 = _load_image(file1) + arr2 = _load_image(file2) if arr1.dtype != arr2.dtype: raise AssertionError(f"Image data types did not match ({arr1.dtype}, {arr2.dtype}).") diff --git a/packages/tool_util/setup.cfg b/packages/tool_util/setup.cfg index 5d6b9b9a378d..848a609a6b2c 100644 --- a/packages/tool_util/setup.cfg +++ b/packages/tool_util/setup.cfg @@ -70,6 +70,7 @@ extended-assertions = numpy pysam pillow + tifffile [options.packages.find] exclude = From 45a819ebc80359b22efdf7918f9d65d79a4646aa Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 20 Mar 2024 09:35:50 +0100 Subject: [PATCH 3/3] Adds tifffile support for image assertions --- lib/galaxy/tool_util/verify/__init__.py | 11 ++- lib/galaxy/tool_util/verify/asserts/image.py | 88 +++++++++++--------- test/functional/tools/validation_image.xml | 12 +++ 3 files changed, 70 insertions(+), 41 deletions(-) diff --git a/lib/galaxy/tool_util/verify/__init__.py b/lib/galaxy/tool_util/verify/__init__.py index da654645d1b9..dd40e8cacf62 100644 --- a/lib/galaxy/tool_util/verify/__init__.py +++ b/lib/galaxy/tool_util/verify/__init__.py @@ -501,14 +501,19 @@ def get_image_metric( def _load_image(filepath: str) -> "numpy.typing.NDArray": + """ + Reads the given image, trying tifffile and Pillow for reading. + """ + # Try reading with tifffile first. It fails if the file is not a TIFF. try: - # Try reading with tifffile first. It fails if the file is not a TIFF. arr = tifffile.imread(filepath) - except tifffile.TiffFileError: - # If tifffile failed, then the file is not a tifffile. In that case, try with Pillow. + # If tifffile failed, then the file is not a tifffile. In that case, try with Pillow. + except tifffile.TiffFileError: with Image.open(filepath) as im: arr = numpy.array(im) + + # Return loaded image return arr diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index f0fbc6e8326e..7513be700f5e 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -18,6 +18,10 @@ from PIL import Image except ImportError: pass +try: + import tifffile +except ImportError: + pass if TYPE_CHECKING: import numpy.typing @@ -58,18 +62,17 @@ def assert_has_image_width( """ Asserts the specified output is an image and has a width of the specified value. """ - buf = io.BytesIO(output_bytes) - with Image.open(buf) as im: - _assert_number( - im.size[0], - width, - delta, - min, - max, - negate, - "{expected} width {n}+-{delta}", - "{expected} width to be in [{min}:{max}]", - ) + im_arr = _get_image(output_bytes) + _assert_number( + im_arr.shape[1], + width, + delta, + min, + max, + negate, + "{expected} width {n}+-{delta}", + "{expected} width to be in [{min}:{max}]", + ) def assert_has_image_height( @@ -83,18 +86,17 @@ def assert_has_image_height( """ Asserts the specified output is an image and has a height of the specified value. """ - buf = io.BytesIO(output_bytes) - with Image.open(buf) as im: - _assert_number( - im.size[1], - height, - delta, - min, - max, - negate, - "{expected} height {n}+-{delta}", - "{expected} height to be in [{min}:{max}]", - ) + im_arr = _get_image(output_bytes) + _assert_number( + im_arr.shape[0], + height, + delta, + min, + max, + negate, + "{expected} height {n}+-{delta}", + "{expected} height to be in [{min}:{max}]", + ) def assert_has_image_channels( @@ -108,18 +110,18 @@ def assert_has_image_channels( """ Asserts the specified output is an image and has the specified number of channels. """ - buf = io.BytesIO(output_bytes) - with Image.open(buf) as im: - _assert_number( - len(im.getbands()), - channels, - delta, - min, - max, - negate, - "{expected} image channels {n}+-{delta}", - "{expected} image channels to be in [{min}:{max}]", - ) + im_arr = _get_image(output_bytes) + n_channels = 1 if im_arr.ndim < 3 else im_arr.shape[2] # we assume here that the image is a 2-D image + _assert_number( + n_channels, + channels, + delta, + min, + max, + negate, + "{expected} image channels {n}+-{delta}", + "{expected} image channels to be in [{min}:{max}]", + ) def _compute_center_of_mass(im_arr: "numpy.typing.NDArray") -> Tuple[float, float]: @@ -139,10 +141,20 @@ def _get_image( ) -> "numpy.typing.NDArray": """ Returns the output image or a specific channel. + + The function tries to read the image using tifffile and Pillow. """ buf = io.BytesIO(output_bytes) - with Image.open(buf) as im: - im_arr = numpy.array(im) + + # Try reading with tifffile first. It fails if the file is not a TIFF. + try: + im_arr = tifffile.imread(buf) + + # If tifffile failed, then the file is not a tifffile. In that case, try with Pillow. + except tifffile.TiffFileError: + buf.seek(0) + with Image.open(buf) as im: + im_arr = numpy.array(im) # Select the specified channel (if any). if channel is not None: diff --git a/test/functional/tools/validation_image.xml b/test/functional/tools/validation_image.xml index a1a7791fa6ec..a3c93ea84f67 100644 --- a/test/functional/tools/validation_image.xml +++ b/test/functional/tools/validation_image.xml @@ -93,6 +93,18 @@ + + + + + + + + + + + +