diff --git a/lib/galaxy/tool_util/verify/__init__.py b/lib/galaxy/tool_util/verify/__init__.py index 976c8706b684..dd40e8cacf62 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,29 @@ def get_image_metric( raise ValueError(f'No such metric: "{metric_name}"') +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: + arr = tifffile.imread(filepath) + + # 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 + + 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/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/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 = diff --git a/test-data/im4_float.tif b/test-data/im4_float.tif new file mode 100644 index 000000000000..21f0e11bb096 Binary files /dev/null and b/test-data/im4_float.tif differ diff --git a/test/functional/tools/image_diff.xml b/test/functional/tools/image_diff.xml index a079992e5fe5..adb17639c603 100644 --- a/test/functional/tools/image_diff.xml +++ b/test/functional/tools/image_diff.xml @@ -22,6 +22,11 @@ + + + + + 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 @@ + + + + + + + + + + + +