From 9a15904e441d0a1da51d53c4a771734e0c9857de Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 28 Feb 2024 11:42:07 +0100 Subject: [PATCH 01/50] Add `has_image_metadata` test assertion --- .../tool_util/verify/asserts/__init__.py | 2 +- lib/galaxy/tool_util/verify/asserts/image.py | 16 ++++++++++++++ test/functional/tools/sample_tool_conf.xml | 1 + test/functional/tools/validation_image.xml | 21 +++++++++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 lib/galaxy/tool_util/verify/asserts/image.py create mode 100644 test/functional/tools/validation_image.xml diff --git a/lib/galaxy/tool_util/verify/asserts/__init__.py b/lib/galaxy/tool_util/verify/asserts/__init__.py index 3d34e5bdb18..10e0ce8909e 100644 --- a/lib/galaxy/tool_util/verify/asserts/__init__.py +++ b/lib/galaxy/tool_util/verify/asserts/__init__.py @@ -11,7 +11,7 @@ from galaxy.util.compression_utils import get_fileobj log = logging.getLogger(__name__) -assertion_module_names = ["text", "tabular", "xml", "json", "hdf5", "archive", "size"] +assertion_module_names = ["text", "tabular", "xml", "json", "hdf5", "archive", "size", "image"] # Code for loading modules containing assertion checking functions, to # create a new module of assertion functions, create the needed python diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py new file mode 100644 index 00000000000..a29432acef9 --- /dev/null +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -0,0 +1,16 @@ +from typing import ( + Optional, +) + +from PIL import Image + + +def assert_has_image_metadata( + output_bytes: bytes, + width: Optional[int] = None, +) -> None: + """Asserts the specified image output has a specific width""" + buf = io.BytesIO(output_bytes) + with Image.open(buf) as im: + assert width is None or im.size[0] == width, + "Image has wrong width: {im.size[0]} (expected {width})" \ No newline at end of file diff --git a/test/functional/tools/sample_tool_conf.xml b/test/functional/tools/sample_tool_conf.xml index e1fa711e0df..2ec2fe9ff32 100644 --- a/test/functional/tools/sample_tool_conf.xml +++ b/test/functional/tools/sample_tool_conf.xml @@ -143,6 +143,7 @@ + diff --git a/test/functional/tools/validation_image.xml b/test/functional/tools/validation_image.xml new file mode 100644 index 00000000000..41adc712b6e --- /dev/null +++ b/test/functional/tools/validation_image.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 5e4cab2f2e82d7b00b9a6ca1c27ed9d49c1dce2f Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 28 Feb 2024 11:42:49 +0100 Subject: [PATCH 02/50] Fix test --- test/functional/tools/validation_image.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/tools/validation_image.xml b/test/functional/tools/validation_image.xml index 41adc712b6e..e4b124f5c37 100644 --- a/test/functional/tools/validation_image.xml +++ b/test/functional/tools/validation_image.xml @@ -1,4 +1,4 @@ - + From aad820f59a81fb6b0864dd9dc2daad787b93e077 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 28 Feb 2024 11:46:42 +0100 Subject: [PATCH 03/50] Fix syntax error --- lib/galaxy/tool_util/verify/asserts/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index a29432acef9..3242f7b5d04 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -12,5 +12,5 @@ def assert_has_image_metadata( """Asserts the specified image output has a specific width""" buf = io.BytesIO(output_bytes) with Image.open(buf) as im: - assert width is None or im.size[0] == width, + assert width is None or im.size[0] == width, \ "Image has wrong width: {im.size[0]} (expected {width})" \ No newline at end of file From 0b04d8432c32ec431712f9ccabfceaa57d0f925e Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 28 Feb 2024 11:49:13 +0100 Subject: [PATCH 04/50] Add missing import --- lib/galaxy/tool_util/verify/asserts/image.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 3242f7b5d04..4f9351bf78f 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -1,3 +1,4 @@ +import io from typing import ( Optional, ) From f85d6bf867f4268c82ff274447411ecc89669bfd Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 28 Feb 2024 12:11:20 +0100 Subject: [PATCH 05/50] Fix test --- test/functional/tools/validation_image.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/tools/validation_image.xml b/test/functional/tools/validation_image.xml index e4b124f5c37..72b5eaead25 100644 --- a/test/functional/tools/validation_image.xml +++ b/test/functional/tools/validation_image.xml @@ -10,7 +10,7 @@ - + From c9ba5d2e74d4b099db0e695924ff94ae24714ad4 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 28 Feb 2024 12:15:16 +0100 Subject: [PATCH 06/50] Fix bug --- lib/galaxy/tool_util/verify/asserts/image.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 4f9351bf78f..3a9ecbc353c 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -14,4 +14,6 @@ def assert_has_image_metadata( buf = io.BytesIO(output_bytes) with Image.open(buf) as im: assert width is None or im.size[0] == width, \ - "Image has wrong width: {im.size[0]} (expected {width})" \ No newline at end of file + f"Image has wrong width: {im.size[0]} (expected {width})" + + # len(image.getbands()) \ No newline at end of file From 900881b0443f0cbebc9e5cc78e37e17e6007a7d3 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 28 Feb 2024 12:17:54 +0100 Subject: [PATCH 07/50] Fix bug --- lib/galaxy/tool_util/verify/asserts/image.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 3a9ecbc353c..1ac9ce1b1da 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -1,6 +1,7 @@ import io from typing import ( Optional, + Union, ) from PIL import Image @@ -8,12 +9,12 @@ from PIL import Image def assert_has_image_metadata( output_bytes: bytes, - width: Optional[int] = None, + width: Optional[Union[int,str]] = None, ) -> None: """Asserts the specified image output has a specific width""" buf = io.BytesIO(output_bytes) with Image.open(buf) as im: - assert width is None or im.size[0] == width, \ - f"Image has wrong width: {im.size[0]} (expected {width})" + assert width is None or im.size[0] == int(width), \ + f"Image has wrong width: {im.size[0]} (expected {int(width)})" # len(image.getbands()) \ No newline at end of file From 982e12889233ea994868255735106fc2fb2cfd00 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 28 Feb 2024 12:22:29 +0100 Subject: [PATCH 08/50] Add more metadata assertions --- lib/galaxy/tool_util/verify/asserts/image.py | 14 ++++++++++++-- test/functional/tools/validation_image.xml | 10 +++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 1ac9ce1b1da..0a4de1574e3 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -10,11 +10,21 @@ from PIL import Image def assert_has_image_metadata( output_bytes: bytes, width: Optional[Union[int,str]] = None, + height: Optional[Union[int,str]] = None, + channels: Optional[Union[int,str]] = None, ) -> None: - """Asserts the specified image output has a specific width""" + """ + Assert the specified image output has specific metadata. + """ buf = io.BytesIO(output_bytes) with Image.open(buf) as im: + assert width is None or im.size[0] == int(width), \ f"Image has wrong width: {im.size[0]} (expected {int(width)})" - # len(image.getbands()) \ No newline at end of file + assert height is None or im.size[1] == int(height), \ + f"Image has wrong height: {im.size[1]} (expected {int(height)})" + + actual_channels = len(im.getbands()) + assert channels is None or actual_channels == int(channels), \ + f"Image has wrong number of channels: {actual_channels} (expected {int(channels)})" \ No newline at end of file diff --git a/test/functional/tools/validation_image.xml b/test/functional/tools/validation_image.xml index 72b5eaead25..050d2f229b2 100644 --- a/test/functional/tools/validation_image.xml +++ b/test/functional/tools/validation_image.xml @@ -13,7 +13,15 @@ - + + + + + + + + + From 7af279300b7a46ca9388852b903fc313e5cca434 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 28 Feb 2024 14:18:52 +0100 Subject: [PATCH 09/50] Add tests for label images --- lib/galaxy/tool_util/verify/asserts/image.py | 45 ++++++++++++++++++-- test/functional/tools/validation_image.xml | 33 +++++++++++++- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 0a4de1574e3..91a7e4c66b4 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -2,19 +2,21 @@ import io from typing import ( Optional, Union, + List ) +import numpy from PIL import Image -def assert_has_image_metadata( +def assert_image_has_metadata( output_bytes: bytes, width: Optional[Union[int,str]] = None, height: Optional[Union[int,str]] = None, channels: Optional[Union[int,str]] = None, ) -> None: """ - Assert the specified image output has specific metadata. + Assert the image output has specific metadata. """ buf = io.BytesIO(output_bytes) with Image.open(buf) as im: @@ -27,4 +29,41 @@ def assert_has_image_metadata( actual_channels = len(im.getbands()) assert channels is None or actual_channels == int(channels), \ - f"Image has wrong number of channels: {actual_channels} (expected {int(channels)})" \ No newline at end of file + f"Image has wrong number of channels: {actual_channels} (expected {int(channels)})" + + +def assert_image_has_labels( + output_bytes: bytes, + number_of_objects: Optional[Union[int, str]] = None, + mean_object_size: Optional[Union[float, str]] = None, + exclude_labels: Optional[Union[str, List[int]]] = list(), + eps: Optional[Union[float, str]] = 1e-8, +) -> None: + """ + Assert the image output has specific label content. + """ + buf = io.BytesIO(output_bytes) + with Image.open(buf) as im: + im_arr = numpy.array(im) + + # Determine labels present in the image. + labels = numpy.unique(im_arr) + + # Apply filtering induced by `exclude_labels`. + if isinstance(exclude_labels, str): + exclude_labels = [im_arr.dtype(label) for label in exclude_labels.split(",") if len(label) > 0] + labels = [label for label in labels if label is not in exclude_labels] + + # Perform `number_of_objects` assertion. + if number_of_objects is not None: + actual = len(labels) + expected = int(number_of_objects) + assert actual == expected, \ + f"Wrong number of objects: {actual} (expected {expected})" + + # Perform `mean_object_size` assertion. + if mean_object_size is not None: + actual = numpy.mean((im_arr == label).sum() for label in labels) + expected = float(mean_object_size) + assert abs(actual - expected) <= float(eps), \ + f"Wrong mean object size: {actual} (expected {expected})" \ No newline at end of file diff --git a/test/functional/tools/validation_image.xml b/test/functional/tools/validation_image.xml index 050d2f229b2..76f4b86a309 100644 --- a/test/functional/tools/validation_image.xml +++ b/test/functional/tools/validation_image.xml @@ -9,11 +9,12 @@ + - + @@ -21,7 +22,35 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From d182c5be88c45b2d413c3ae54a76d29186e8c421 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 28 Feb 2024 14:22:17 +0100 Subject: [PATCH 10/50] Fix XML --- test/functional/tools/validation_image.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/functional/tools/validation_image.xml b/test/functional/tools/validation_image.xml index 76f4b86a309..cd3862a7799 100644 --- a/test/functional/tools/validation_image.xml +++ b/test/functional/tools/validation_image.xml @@ -9,7 +9,7 @@ - + @@ -26,7 +26,7 @@ - + From fa81d0059ef2b533cb1dd099ac4e6832cafb4671 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 28 Feb 2024 14:29:16 +0100 Subject: [PATCH 11/50] Fix XML --- test/functional/tools/validation_image.xml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/test/functional/tools/validation_image.xml b/test/functional/tools/validation_image.xml index cd3862a7799..bd9d5c1cc3d 100644 --- a/test/functional/tools/validation_image.xml +++ b/test/functional/tools/validation_image.xml @@ -32,7 +32,7 @@ - + @@ -41,7 +41,15 @@ - + + + + + + + + + @@ -50,7 +58,7 @@ - + From ca9e93614be99aebc74cf5a42adcadf0f0467e5c Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 28 Feb 2024 14:47:28 +0100 Subject: [PATCH 12/50] Fix bug --- lib/galaxy/tool_util/verify/asserts/image.py | 2 +- test/functional/tools/validation_image.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 91a7e4c66b4..d093ad6891c 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -52,7 +52,7 @@ def assert_image_has_labels( # Apply filtering induced by `exclude_labels`. if isinstance(exclude_labels, str): exclude_labels = [im_arr.dtype(label) for label in exclude_labels.split(",") if len(label) > 0] - labels = [label for label in labels if label is not in exclude_labels] + labels = [label for label in labels if label not in exclude_labels] # Perform `number_of_objects` assertion. if number_of_objects is not None: diff --git a/test/functional/tools/validation_image.xml b/test/functional/tools/validation_image.xml index bd9d5c1cc3d..6f35004a608 100644 --- a/test/functional/tools/validation_image.xml +++ b/test/functional/tools/validation_image.xml @@ -3,10 +3,10 @@ cp '$input' '$output' ]]> - + - + From e4b1ecb6b985b2257d192c56533d27855bc757c3 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 28 Feb 2024 14:54:55 +0100 Subject: [PATCH 13/50] Fix bug --- lib/galaxy/tool_util/verify/asserts/image.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index d093ad6891c..70896d626f9 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -51,7 +51,13 @@ def assert_image_has_labels( # Apply filtering induced by `exclude_labels`. if isinstance(exclude_labels, str): - exclude_labels = [im_arr.dtype(label) for label in exclude_labels.split(",") if len(label) > 0] + if numpy.issubdtype(im_arr.dtype, numpy.integer): + cast_label = lambda label: int(label) + elif numpy.issubdtype(im_arr.dtype, numpy.float): + cast_label = lambda label: float(label) + else: + raise AssertionError(f'Unsupported image label type: "{im_arr.dtype}"') + exclude_labels = [cast_label(label) for label in exclude_labels.split(",") if len(label) > 0] labels = [label for label in labels if label not in exclude_labels] # Perform `number_of_objects` assertion. @@ -63,7 +69,7 @@ def assert_image_has_labels( # Perform `mean_object_size` assertion. if mean_object_size is not None: - actual = numpy.mean((im_arr == label).sum() for label in labels) + actual = numpy.mean([(im_arr == label).sum() for label in labels]) expected = float(mean_object_size) assert abs(actual - expected) <= float(eps), \ f"Wrong mean object size: {actual} (expected {expected})" \ No newline at end of file From 9e361d45b1e64153fb05cb0d947cd32ba29bc4f6 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 28 Feb 2024 15:12:50 +0100 Subject: [PATCH 14/50] Fix bug --- lib/galaxy/tool_util/verify/asserts/image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 70896d626f9..76ff0425da9 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -49,7 +49,7 @@ def assert_image_has_labels( # Determine labels present in the image. labels = numpy.unique(im_arr) - # Apply filtering induced by `exclude_labels`. + # Apply filtering due to `exclude_labels`. if isinstance(exclude_labels, str): if numpy.issubdtype(im_arr.dtype, numpy.integer): cast_label = lambda label: int(label) @@ -69,7 +69,7 @@ def assert_image_has_labels( # Perform `mean_object_size` assertion. if mean_object_size is not None: - actual = numpy.mean([(im_arr == label).sum() for label in labels]) + actual = sum((im_arr == label).sum() for label in labels) / len(labels) expected = float(mean_object_size) assert abs(actual - expected) <= float(eps), \ f"Wrong mean object size: {actual} (expected {expected})" \ No newline at end of file From a1ddff00ee568d3be4eae397642503af5d6cbead Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 28 Feb 2024 15:19:51 +0100 Subject: [PATCH 15/50] Fix tests --- test/functional/tools/validation_image.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/functional/tools/validation_image.xml b/test/functional/tools/validation_image.xml index 6f35004a608..23cc130136b 100644 --- a/test/functional/tools/validation_image.xml +++ b/test/functional/tools/validation_image.xml @@ -41,7 +41,7 @@ - + @@ -49,7 +49,7 @@ - + @@ -58,7 +58,7 @@ - + From c4ea7274f0d70a23f3a3dfe6859dfd161ee249be Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 28 Feb 2024 15:56:29 +0100 Subject: [PATCH 16/50] Add `image_has_intensities` and tests --- lib/galaxy/tool_util/verify/asserts/image.py | 50 +++++++++++++++++++- test/functional/tools/validation_image.xml | 21 +++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 76ff0425da9..622758395e9 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -2,7 +2,8 @@ import io from typing import ( Optional, Union, - List + List, + Tuple, ) import numpy @@ -32,6 +33,53 @@ def assert_image_has_metadata( f"Image has wrong number of channels: {actual_channels} (expected {int(channels)})" +def _compute_center_of_mass(im_arr): + while im_arr.ndim > 2: + im_arr = im_arr.sum(axis=2) + im_arr = numpy.abs(im_arr) + if im_arr.sum() == 0: + return (numpy.nan, numpy.nan) + im_arr = im_arr / im_arr.sum() + yy, xx = numpy.indices(im_arr.shape) + return (im_arr * xx).sum(), (im_arr * yy).sum() + + +def assert_image_has_intensities( + output_bytes: bytes, + channel: Optional[Union[int, str]] = None, + mean_intensity: Optional[Union[float, str]] = None, + center_of_mass: Optional[Union[Tuple[float], str]] = None, + eps: Optional[Union[float, str]] = 1e-8, +) -> None: + """ + Assert the image output has specific intensity content. + """ + buf = io.BytesIO(output_bytes) + with Image.open(buf) as im: + im_arr = numpy.array(im) + + # Select the specified channel (if any). + if channel is not None: + im_arr = im_arr[:, :, channel] + + # Perform `mean_intensity` assertion. + if mean_intensity is not None: + actual = im_arr.mean() + expected = float(mean_intensity) + assert abs(actual - expected) <= float(eps), \ + f"Wrong mean intensity: {actual} (expected {expected})" + + # Perform `center_of_mass` assertion. + if center_of_mass is not None: + if isinstance(center_of_mass, str): + center_of_mass = [float(c.strip()) for c in center_of_mass.split(",")] + assert len(center_of_mass) == 2, "center_of_mass must have two components" + actual = _compute_center_of_mass(im_arr) + distance = numpy.linalg.norm(numpy.subtract(center_of_mass, expected)) + assert distance <= float(eps), \ + f"Wrong center of mass: {actual} (expected {center_of_mass})" + + def assert_image_has_labels( output_bytes: bytes, number_of_objects: Optional[Union[int, str]] = None, diff --git a/test/functional/tools/validation_image.xml b/test/functional/tools/validation_image.xml index 23cc130136b..7a0c561426a 100644 --- a/test/functional/tools/validation_image.xml +++ b/test/functional/tools/validation_image.xml @@ -14,7 +14,7 @@ - + @@ -23,6 +23,25 @@ + + + + + + + + + + + + + + + + + + + From e8bb59e34b7e0ea761a38edc34355d93f2daf894 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 28 Feb 2024 16:16:47 +0100 Subject: [PATCH 17/50] Fix bug --- lib/galaxy/tool_util/verify/asserts/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 622758395e9..b2729c9052a 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -60,7 +60,7 @@ def assert_image_has_intensities( # Select the specified channel (if any). if channel is not None: - im_arr = im_arr[:, :, channel] + im_arr = im_arr[:, :, int(channel)] # Perform `mean_intensity` assertion. if mean_intensity is not None: From a392d9eca8e0d41110c6075a9cd85d8b4c56ea73 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 28 Feb 2024 16:23:06 +0100 Subject: [PATCH 18/50] Fix tests --- test/functional/tools/validation_image.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/functional/tools/validation_image.xml b/test/functional/tools/validation_image.xml index 7a0c561426a..8c2fcf24e5e 100644 --- a/test/functional/tools/validation_image.xml +++ b/test/functional/tools/validation_image.xml @@ -19,7 +19,7 @@ - + @@ -30,7 +30,7 @@ - + @@ -38,7 +38,7 @@ - + From 6cfe3998352f5ded132b88fe9acc41a0785a9d58 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 28 Feb 2024 16:26:24 +0100 Subject: [PATCH 19/50] Fix tests --- test/functional/tools/validation_image.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/functional/tools/validation_image.xml b/test/functional/tools/validation_image.xml index 8c2fcf24e5e..ef033db56a3 100644 --- a/test/functional/tools/validation_image.xml +++ b/test/functional/tools/validation_image.xml @@ -19,7 +19,7 @@ - + @@ -30,7 +30,7 @@ - + @@ -38,7 +38,7 @@ - + From c61e306a4ff7d2886e86bff542b7d9e54ebd4ff2 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 28 Feb 2024 16:35:45 +0100 Subject: [PATCH 20/50] Add debug info --- lib/galaxy/tool_util/verify/asserts/image.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index b2729c9052a..7c01efb3927 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -67,7 +67,7 @@ def assert_image_has_intensities( actual = im_arr.mean() expected = float(mean_intensity) assert abs(actual - expected) <= float(eps), \ - f"Wrong mean intensity: {actual} (expected {expected})" + f"Wrong mean intensity: {actual} (expected {expected}, eps: {eps})" # Perform `center_of_mass` assertion. if center_of_mass is not None: @@ -77,7 +77,7 @@ def assert_image_has_intensities( actual = _compute_center_of_mass(im_arr) distance = numpy.linalg.norm(numpy.subtract(center_of_mass, expected)) assert distance <= float(eps), \ - f"Wrong center of mass: {actual} (expected {center_of_mass})" + f"Wrong center of mass: {actual} (expected {center_of_mass}, distance: {distance}, eps: {eps})" def assert_image_has_labels( @@ -120,4 +120,4 @@ def assert_image_has_labels( actual = sum((im_arr == label).sum() for label in labels) / len(labels) expected = float(mean_object_size) assert abs(actual - expected) <= float(eps), \ - f"Wrong mean object size: {actual} (expected {expected})" \ No newline at end of file + f"Wrong mean object size: {actual} (expected {expected}, eps: {eps})" From 8c567541c660eb1d5ba799ed5f973ef7c73b9732 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 28 Feb 2024 16:39:59 +0100 Subject: [PATCH 21/50] Fix bug --- lib/galaxy/tool_util/verify/asserts/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 7c01efb3927..f39a32d9808 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -75,7 +75,7 @@ def assert_image_has_intensities( center_of_mass = [float(c.strip()) for c in center_of_mass.split(",")] assert len(center_of_mass) == 2, "center_of_mass must have two components" actual = _compute_center_of_mass(im_arr) - distance = numpy.linalg.norm(numpy.subtract(center_of_mass, expected)) + distance = numpy.linalg.norm(numpy.subtract(center_of_mass, actual)) assert distance <= float(eps), \ f"Wrong center of mass: {actual} (expected {center_of_mass}, distance: {distance}, eps: {eps})" From 04ade007609dc81a0262f32c7746b12ca92ba6e6 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 28 Feb 2024 16:51:45 +0100 Subject: [PATCH 22/50] Fix tests --- test/functional/tools/validation_image.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/tools/validation_image.xml b/test/functional/tools/validation_image.xml index ef033db56a3..fe437c03e83 100644 --- a/test/functional/tools/validation_image.xml +++ b/test/functional/tools/validation_image.xml @@ -41,7 +41,7 @@ - + From 37c1cba1bf7772ec3fdeaf5a3cad46086e431982 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 28 Feb 2024 16:56:48 +0100 Subject: [PATCH 23/50] Add test --- test/functional/tools/validation_image.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/test/functional/tools/validation_image.xml b/test/functional/tools/validation_image.xml index fe437c03e83..335cb31d252 100644 --- a/test/functional/tools/validation_image.xml +++ b/test/functional/tools/validation_image.xml @@ -26,6 +26,7 @@ + From 3ed81ec0698964012735fc7d0706c863eeb3a303 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Thu, 29 Feb 2024 10:52:37 +0100 Subject: [PATCH 24/50] Add XSD for `image_has_metadata` --- lib/galaxy/tool_util/xsd/galaxy.xsd | 39 +++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/lib/galaxy/tool_util/xsd/galaxy.xsd b/lib/galaxy/tool_util/xsd/galaxy.xsd index 34361b426d8..44e315bd7b2 100644 --- a/lib/galaxy/tool_util/xsd/galaxy.xsd +++ b/lib/galaxy/tool_util/xsd/galaxy.xsd @@ -2242,6 +2242,16 @@ module. + + + + + + + + + + @@ -2739,6 +2749,35 @@ $attribute_list::5 + + + +``` + +$attribute_list::5 +]]> + + + + + The required width of the image. + + + + + The required height of the image. + + + + + The required number of channels of the image (e.g., 1 for grayscale, 3 for RGB, 4 for RGBA images). + + + Date: Thu, 29 Feb 2024 10:24:49 +0000 Subject: [PATCH 25/50] Fix bug in test --- test/functional/tools/validation_image.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/tools/validation_image.xml b/test/functional/tools/validation_image.xml index 335cb31d252..8789d9655bf 100644 --- a/test/functional/tools/validation_image.xml +++ b/test/functional/tools/validation_image.xml @@ -14,7 +14,8 @@ - + + From 88d67e38da449f6f7a91e010e5504191a5b2a599 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Thu, 29 Feb 2024 10:56:36 +0000 Subject: [PATCH 26/50] Update XSD --- lib/galaxy/tool_util/xsd/galaxy.xsd | 82 ++++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 6 deletions(-) diff --git a/lib/galaxy/tool_util/xsd/galaxy.xsd b/lib/galaxy/tool_util/xsd/galaxy.xsd index 44e315bd7b2..dc148e41302 100644 --- a/lib/galaxy/tool_util/xsd/galaxy.xsd +++ b/lib/galaxy/tool_util/xsd/galaxy.xsd @@ -2178,6 +2178,7 @@ module. + @@ -2247,9 +2248,9 @@ module. - - - + + + @@ -2500,6 +2501,7 @@ $attribute_list::5 + @@ -2762,22 +2764,90 @@ $attribute_list::5 ]]> - + The required width of the image. - + The required height of the image. - + The required number of channels of the image (e.g., 1 for grayscale, 3 for RGB, 4 for RGBA images). + + + +``` + +$attribute_list::5 +]]> + + + + + The required mean value of the image intensities. + + + + + The required center of mass of the image intensities (horizontal and vertical coordinate, separated by a comma). + + + + + The absolute tolerance to be used for the ``mean_intensity`` and ``center_of_mass`` assertions. + + + + + Restricts the ``mean_intensity`` and ``center_of_mass`` assertions to a specific channel of the image (where the value ``0`` corresponds to the first image channel). + + + + + + +``` + +$attribute_list::5 +]]> + + + + + The required number of unique labels in the image. It is assumed that each individual object corresponds to a unique label. + + + + + The required mean size of the objects in the image, where the size of an object is measured by the number of pixels. It is assumed that each individual object corresponds to a unique label. + + + + + The absolute tolerance to be used for the ``mean_object_size`` assertion. + + + + + List of labels to be excluded from consideration for the ``number_of_objects`` and ``mean_object_size`` assertions, separated by a comma. The primary usage of this attribute is to exclude the background of a label image. + + + Date: Thu, 29 Feb 2024 11:03:32 +0000 Subject: [PATCH 27/50] Update XSD --- lib/galaxy/tool_util/xsd/galaxy.xsd | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/galaxy/tool_util/xsd/galaxy.xsd b/lib/galaxy/tool_util/xsd/galaxy.xsd index dc148e41302..718892a12cf 100644 --- a/lib/galaxy/tool_util/xsd/galaxy.xsd +++ b/lib/galaxy/tool_util/xsd/galaxy.xsd @@ -2803,9 +2803,9 @@ $attribute_list::5 The required center of mass of the image intensities (horizontal and vertical coordinate, separated by a comma). - + - The absolute tolerance to be used for the ``mean_intensity`` and ``center_of_mass`` assertions. + The absolute tolerance to be used for the ``mean_intensity`` and ``center_of_mass`` assertions (defaults to ``1e-8``). @@ -2837,9 +2837,9 @@ $attribute_list::5 The required mean size of the objects in the image, where the size of an object is measured by the number of pixels. It is assumed that each individual object corresponds to a unique label. - + - The absolute tolerance to be used for the ``mean_object_size`` assertion. + The absolute tolerance to be used for the ``mean_object_size`` assertion (defaults to ``1e-8``). From 6855df53163bcddc6f0be148a74d9e38cccaef48 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sat, 2 Mar 2024 08:04:06 +0000 Subject: [PATCH 28/50] Fix assertion errors --- lib/galaxy/tool_util/verify/asserts/image.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index f39a32d9808..69da6e92900 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -49,7 +49,7 @@ def assert_image_has_intensities( channel: Optional[Union[int, str]] = None, mean_intensity: Optional[Union[float, str]] = None, center_of_mass: Optional[Union[Tuple[float], str]] = None, - eps: Optional[Union[float, str]] = 1e-8, + eps: Union[float, str] = 1e-8, ) -> None: """ Assert the image output has specific intensity content. @@ -84,8 +84,8 @@ def assert_image_has_labels( output_bytes: bytes, number_of_objects: Optional[Union[int, str]] = None, mean_object_size: Optional[Union[float, str]] = None, - exclude_labels: Optional[Union[str, List[int]]] = list(), - eps: Optional[Union[float, str]] = 1e-8, + exclude_labels: Optional[Union[str, List[int]]] = None, + eps: Union[float, str] = 1e-8, ) -> None: """ Assert the image output has specific label content. @@ -98,12 +98,14 @@ def assert_image_has_labels( labels = numpy.unique(im_arr) # Apply filtering due to `exclude_labels`. + if exclude_labels is None: + exclude_labels = list() if isinstance(exclude_labels, str): - if numpy.issubdtype(im_arr.dtype, numpy.integer): - cast_label = lambda label: int(label) - elif numpy.issubdtype(im_arr.dtype, numpy.float): - cast_label = lambda label: float(label) - else: + def cast_label(label): + if numpy.issubdtype(im_arr.dtype, numpy.integer): + return int(label) + if numpy.issubdtype(im_arr.dtype, numpy.float): + return float(label) raise AssertionError(f'Unsupported image label type: "{im_arr.dtype}"') exclude_labels = [cast_label(label) for label in exclude_labels.split(",") if len(label) > 0] labels = [label for label in labels if label not in exclude_labels] From 42e1b0744ac30e3df165d260ea526da1b435d20e Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sat, 2 Mar 2024 08:08:59 +0000 Subject: [PATCH 29/50] Fix linting issues --- lib/galaxy/tool_util/verify/asserts/image.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 69da6e92900..552a6e27937 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -12,9 +12,9 @@ from PIL import Image def assert_image_has_metadata( output_bytes: bytes, - width: Optional[Union[int,str]] = None, - height: Optional[Union[int,str]] = None, - channels: Optional[Union[int,str]] = None, + width: Optional[Union[int, str]] = None, + height: Optional[Union[int, str]] = None, + channels: Optional[Union[int, str]] = None, ) -> None: """ Assert the image output has specific metadata. @@ -57,7 +57,7 @@ def assert_image_has_intensities( buf = io.BytesIO(output_bytes) with Image.open(buf) as im: im_arr = numpy.array(im) - + # Select the specified channel (if any). if channel is not None: im_arr = im_arr[:, :, int(channel)] @@ -68,7 +68,7 @@ def assert_image_has_intensities( expected = float(mean_intensity) assert abs(actual - expected) <= float(eps), \ f"Wrong mean intensity: {actual} (expected {expected}, eps: {eps})" - + # Perform `center_of_mass` assertion. if center_of_mass is not None: if isinstance(center_of_mass, str): @@ -93,7 +93,7 @@ def assert_image_has_labels( buf = io.BytesIO(output_bytes) with Image.open(buf) as im: im_arr = numpy.array(im) - + # Determine labels present in the image. labels = numpy.unique(im_arr) From 4d52d07d1bdaf362c860851f84dc0dda74cd7347 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sat, 2 Mar 2024 08:19:18 +0000 Subject: [PATCH 30/50] Fix linting issues --- lib/galaxy/tool_util/verify/asserts/image.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 552a6e27937..1bef4ebe1fb 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -72,7 +72,7 @@ def assert_image_has_intensities( # Perform `center_of_mass` assertion. if center_of_mass is not None: if isinstance(center_of_mass, str): - center_of_mass = [float(c.strip()) for c in center_of_mass.split(",")] + center_of_mass = tuple(float(c.strip()) for c in center_of_mass.split(",")) assert len(center_of_mass) == 2, "center_of_mass must have two components" actual = _compute_center_of_mass(im_arr) distance = numpy.linalg.norm(numpy.subtract(center_of_mass, actual)) @@ -104,7 +104,7 @@ def assert_image_has_labels( def cast_label(label): if numpy.issubdtype(im_arr.dtype, numpy.integer): return int(label) - if numpy.issubdtype(im_arr.dtype, numpy.float): + if numpy.issubdtype(im_arr.dtype, float): return float(label) raise AssertionError(f'Unsupported image label type: "{im_arr.dtype}"') exclude_labels = [cast_label(label) for label in exclude_labels.split(",") if len(label) > 0] @@ -112,14 +112,14 @@ def assert_image_has_labels( # Perform `number_of_objects` assertion. if number_of_objects is not None: - actual = len(labels) - expected = int(number_of_objects) - assert actual == expected, \ - f"Wrong number of objects: {actual} (expected {expected})" + actual_number_of_objects = len(labels) + expected_number_of_objects = int(number_of_objects) + assert actual_number_of_objects == expected_number_of_objects, \ + f"Wrong number of objects: {actual_number_of_objects} (expected {expected_number_of_objects})" # Perform `mean_object_size` assertion. if mean_object_size is not None: - actual = sum((im_arr == label).sum() for label in labels) / len(labels) - expected = float(mean_object_size) - assert abs(actual - expected) <= float(eps), \ - f"Wrong mean object size: {actual} (expected {expected}, eps: {eps})" + actual_mean_object_size = sum((im_arr == label).sum() for label in labels) / len(labels) + expected_mean_object_size = float(mean_object_size) + assert abs(actual_mean_object_size - expected_mean_object_size) <= float(eps), \ + f"Wrong mean object size: {actual_mean_object_size} (expected {expected_mean_object_size}, eps: {eps})" From 9094a7f2c63140d8ca7755f74f411617304bb633 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sat, 2 Mar 2024 08:30:31 +0000 Subject: [PATCH 31/50] Test --- lib/galaxy/tool_util/verify/asserts/image.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 1bef4ebe1fb..170b74b29af 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -72,6 +72,7 @@ def assert_image_has_intensities( # Perform `center_of_mass` assertion. if center_of_mass is not None: if isinstance(center_of_mass, str): + test: Tuple[float] = tuple(float(c.strip()) for c in center_of_mass.split(",")) center_of_mass = tuple(float(c.strip()) for c in center_of_mass.split(",")) assert len(center_of_mass) == 2, "center_of_mass must have two components" actual = _compute_center_of_mass(im_arr) From af948be4bd5a5a91a10eb7d1ba1f666bac280862 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sat, 2 Mar 2024 08:32:18 +0000 Subject: [PATCH 32/50] Test --- lib/galaxy/tool_util/verify/asserts/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 170b74b29af..0147ed8da22 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -73,7 +73,7 @@ def assert_image_has_intensities( if center_of_mass is not None: if isinstance(center_of_mass, str): test: Tuple[float] = tuple(float(c.strip()) for c in center_of_mass.split(",")) - center_of_mass = tuple(float(c.strip()) for c in center_of_mass.split(",")) + center_of_mass = test assert len(center_of_mass) == 2, "center_of_mass must have two components" actual = _compute_center_of_mass(im_arr) distance = numpy.linalg.norm(numpy.subtract(center_of_mass, actual)) From f1f7d48e1be16975b63f0462074c04d1c3ff4d4d Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sat, 2 Mar 2024 08:40:50 +0000 Subject: [PATCH 33/50] Fix linting issues --- lib/galaxy/tool_util/verify/asserts/image.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 0147ed8da22..9162c5ddf95 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -48,7 +48,7 @@ def assert_image_has_intensities( output_bytes: bytes, channel: Optional[Union[int, str]] = None, mean_intensity: Optional[Union[float, str]] = None, - center_of_mass: Optional[Union[Tuple[float], str]] = None, + center_of_mass: Optional[Union[Tuple[float, float], str]] = None, eps: Union[float, str] = 1e-8, ) -> None: """ @@ -72,8 +72,9 @@ def assert_image_has_intensities( # Perform `center_of_mass` assertion. if center_of_mass is not None: if isinstance(center_of_mass, str): - test: Tuple[float] = tuple(float(c.strip()) for c in center_of_mass.split(",")) - center_of_mass = test + center_of_mass_parts = center_of_mass.split(",") + assert len(center_of_mass_parts) == 2 + center_of_mass = tuple(float(c.strip()) for c in center_of_mass_parts) assert len(center_of_mass) == 2, "center_of_mass must have two components" actual = _compute_center_of_mass(im_arr) distance = numpy.linalg.norm(numpy.subtract(center_of_mass, actual)) From 94d43f156fbe652ab6903bc9ab7dadb18f0c181d Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sat, 2 Mar 2024 08:45:25 +0000 Subject: [PATCH 34/50] Fix linting issues --- lib/galaxy/tool_util/verify/asserts/image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 9162c5ddf95..72d96b14b9f 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -72,9 +72,9 @@ def assert_image_has_intensities( # Perform `center_of_mass` assertion. if center_of_mass is not None: if isinstance(center_of_mass, str): - center_of_mass_parts = center_of_mass.split(",") + center_of_mass_parts = [c.strip() for c in center_of_mass.split(",")] assert len(center_of_mass_parts) == 2 - center_of_mass = tuple(float(c.strip()) for c in center_of_mass_parts) + center_of_mass = (float(center_of_mass_parts[0]), float(center_of_mass_parts[1])) assert len(center_of_mass) == 2, "center_of_mass must have two components" actual = _compute_center_of_mass(im_arr) distance = numpy.linalg.norm(numpy.subtract(center_of_mass, actual)) From 7d0ef9f17bde8a1548d6697372410e5b05915f77 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sat, 2 Mar 2024 08:53:15 +0000 Subject: [PATCH 35/50] Add missing type checks --- lib/galaxy/tool_util/verify/asserts/image.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 72d96b14b9f..7e0f0f24046 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -4,10 +4,20 @@ from typing import ( Union, List, Tuple, + TYPE_CHECKING, ) -import numpy -from PIL import Image +try: + import numpy +except ImportError: + pass +try: + from PIL import Image +except ImportError: + pass + +if TYPE_CHECKING: + import numpy.typing def assert_image_has_metadata( @@ -33,7 +43,7 @@ def assert_image_has_metadata( f"Image has wrong number of channels: {actual_channels} (expected {int(channels)})" -def _compute_center_of_mass(im_arr): +def _compute_center_of_mass(im_arr: "numpy.typing.NDArray") -> Tuple[float, float]: while im_arr.ndim > 2: im_arr = im_arr.sum(axis=2) im_arr = numpy.abs(im_arr) From 7bcc0ea61493c0a165bf751c208446a9bdcec88c Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sat, 2 Mar 2024 08:55:47 +0000 Subject: [PATCH 36/50] Fix linting issues --- lib/galaxy/tool_util/verify/asserts/image.py | 28 ++++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 7e0f0f24046..d52d669ddba 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -32,15 +32,15 @@ def assert_image_has_metadata( buf = io.BytesIO(output_bytes) with Image.open(buf) as im: - assert width is None or im.size[0] == int(width), \ - f"Image has wrong width: {im.size[0]} (expected {int(width)})" + error_text = f"Image has wrong width: {im.size[0]} (expected {int(width)})" + assert width is None or im.size[0] == int(width), error_text - assert height is None or im.size[1] == int(height), \ - f"Image has wrong height: {im.size[1]} (expected {int(height)})" + error_text = f"Image has wrong height: {im.size[1]} (expected {int(height)})" + assert height is None or im.size[1] == int(height), error_text actual_channels = len(im.getbands()) - assert channels is None or actual_channels == int(channels), \ - f"Image has wrong number of channels: {actual_channels} (expected {int(channels)})" + error_text = f"Image has wrong number of channels: {actual_channels} (expected {int(channels)})" + assert channels is None or actual_channels == int(channels), error_text def _compute_center_of_mass(im_arr: "numpy.typing.NDArray") -> Tuple[float, float]: @@ -76,8 +76,8 @@ def assert_image_has_intensities( if mean_intensity is not None: actual = im_arr.mean() expected = float(mean_intensity) - assert abs(actual - expected) <= float(eps), \ - f"Wrong mean intensity: {actual} (expected {expected}, eps: {eps})" + error_text = f"Wrong mean intensity: {actual} (expected {expected}, eps: {eps})" + assert abs(actual - expected) <= float(eps), error_text # Perform `center_of_mass` assertion. if center_of_mass is not None: @@ -88,8 +88,8 @@ def assert_image_has_intensities( assert len(center_of_mass) == 2, "center_of_mass must have two components" actual = _compute_center_of_mass(im_arr) distance = numpy.linalg.norm(numpy.subtract(center_of_mass, actual)) - assert distance <= float(eps), \ - f"Wrong center of mass: {actual} (expected {center_of_mass}, distance: {distance}, eps: {eps})" + error_text = f"Wrong center of mass: {actual} (expected {center_of_mass}, distance: {distance}, eps: {eps})" + assert distance <= float(eps), error_text def assert_image_has_labels( @@ -126,12 +126,12 @@ def assert_image_has_labels( if number_of_objects is not None: actual_number_of_objects = len(labels) expected_number_of_objects = int(number_of_objects) - assert actual_number_of_objects == expected_number_of_objects, \ - f"Wrong number of objects: {actual_number_of_objects} (expected {expected_number_of_objects})" + error_text = f"Wrong number of objects: {actual_number_of_objects} (expected {expected_number_of_objects})" + assert actual_number_of_objects == expected_number_of_objects, error_text # Perform `mean_object_size` assertion. if mean_object_size is not None: actual_mean_object_size = sum((im_arr == label).sum() for label in labels) / len(labels) expected_mean_object_size = float(mean_object_size) - assert abs(actual_mean_object_size - expected_mean_object_size) <= float(eps), \ - f"Wrong mean object size: {actual_mean_object_size} (expected {expected_mean_object_size}, eps: {eps})" + error_text = f"Wrong mean object size: {actual_mean_object_size} (expected {expected_mean_object_size}, eps: {eps})" + assert abs(actual_mean_object_size - expected_mean_object_size) <= float(eps), error_text \ No newline at end of file From f08d68fda0b763b1e5cff68b0d6eec6a11427f0b Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sat, 2 Mar 2024 08:57:47 +0000 Subject: [PATCH 37/50] Fix linting issues --- lib/galaxy/tool_util/verify/asserts/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index d52d669ddba..f8c67073129 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -134,4 +134,4 @@ def assert_image_has_labels( actual_mean_object_size = sum((im_arr == label).sum() for label in labels) / len(labels) expected_mean_object_size = float(mean_object_size) error_text = f"Wrong mean object size: {actual_mean_object_size} (expected {expected_mean_object_size}, eps: {eps})" - assert abs(actual_mean_object_size - expected_mean_object_size) <= float(eps), error_text \ No newline at end of file + assert abs(actual_mean_object_size - expected_mean_object_size) <= float(eps), error_text From f7845ce5a9b937d80153fcfad919fe11f9b30636 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sat, 2 Mar 2024 09:02:36 +0000 Subject: [PATCH 38/50] Fix linting issues --- lib/galaxy/tool_util/verify/asserts/image.py | 21 +++++++------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index f8c67073129..12ceef33d9a 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -32,15 +32,12 @@ def assert_image_has_metadata( buf = io.BytesIO(output_bytes) with Image.open(buf) as im: - error_text = f"Image has wrong width: {im.size[0]} (expected {int(width)})" - assert width is None or im.size[0] == int(width), error_text + assert width is None or im.size[0] == int(width), f"Image has wrong width: {im.size[0]} (expected {int(width)})" - error_text = f"Image has wrong height: {im.size[1]} (expected {int(height)})" - assert height is None or im.size[1] == int(height), error_text + assert height is None or im.size[1] == int(height), f"Image has wrong height: {im.size[1]} (expected {int(height)})" actual_channels = len(im.getbands()) - error_text = f"Image has wrong number of channels: {actual_channels} (expected {int(channels)})" - assert channels is None or actual_channels == int(channels), error_text + assert channels is None or actual_channels == int(channels), f"Image has wrong number of channels: {actual_channels} (expected {int(channels)})" def _compute_center_of_mass(im_arr: "numpy.typing.NDArray") -> Tuple[float, float]: @@ -76,8 +73,7 @@ def assert_image_has_intensities( if mean_intensity is not None: actual = im_arr.mean() expected = float(mean_intensity) - error_text = f"Wrong mean intensity: {actual} (expected {expected}, eps: {eps})" - assert abs(actual - expected) <= float(eps), error_text + assert abs(actual - expected) <= float(eps), f"Wrong mean intensity: {actual} (expected {expected}, eps: {eps})" # Perform `center_of_mass` assertion. if center_of_mass is not None: @@ -88,8 +84,7 @@ def assert_image_has_intensities( assert len(center_of_mass) == 2, "center_of_mass must have two components" actual = _compute_center_of_mass(im_arr) distance = numpy.linalg.norm(numpy.subtract(center_of_mass, actual)) - error_text = f"Wrong center of mass: {actual} (expected {center_of_mass}, distance: {distance}, eps: {eps})" - assert distance <= float(eps), error_text + assert distance <= float(eps), f"Wrong center of mass: {actual} (expected {center_of_mass}, distance: {distance}, eps: {eps})" def assert_image_has_labels( @@ -126,12 +121,10 @@ def assert_image_has_labels( if number_of_objects is not None: actual_number_of_objects = len(labels) expected_number_of_objects = int(number_of_objects) - error_text = f"Wrong number of objects: {actual_number_of_objects} (expected {expected_number_of_objects})" - assert actual_number_of_objects == expected_number_of_objects, error_text + assert actual_number_of_objects == expected_number_of_objects, f"Wrong number of objects: {actual_number_of_objects} (expected {expected_number_of_objects})" # Perform `mean_object_size` assertion. if mean_object_size is not None: actual_mean_object_size = sum((im_arr == label).sum() for label in labels) / len(labels) expected_mean_object_size = float(mean_object_size) - error_text = f"Wrong mean object size: {actual_mean_object_size} (expected {expected_mean_object_size}, eps: {eps})" - assert abs(actual_mean_object_size - expected_mean_object_size) <= float(eps), error_text + assert abs(actual_mean_object_size - expected_mean_object_size) <= float(eps), f"Wrong mean object size: {actual_mean_object_size} (expected {expected_mean_object_size}, eps: {eps})" From 84c4cbf54b14f0b79efd6139f73cf68afb610955 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sat, 2 Mar 2024 09:10:30 +0000 Subject: [PATCH 39/50] Fix linting issues --- lib/galaxy/tool_util/verify/asserts/image.py | 26 +++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 12ceef33d9a..3d64199fc56 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -32,12 +32,18 @@ def assert_image_has_metadata( buf = io.BytesIO(output_bytes) with Image.open(buf) as im: - assert width is None or im.size[0] == int(width), f"Image has wrong width: {im.size[0]} (expected {int(width)})" + assert width is None or im.size[0] == int( + width + ), f"Image has wrong width: {im.size[0]} (expected {int(width)})" - assert height is None or im.size[1] == int(height), f"Image has wrong height: {im.size[1]} (expected {int(height)})" + assert height is None or im.size[1] == int( + height + ), f"Image has wrong height: {im.size[1]} (expected {int(height)})" actual_channels = len(im.getbands()) - assert channels is None or actual_channels == int(channels), f"Image has wrong number of channels: {actual_channels} (expected {int(channels)})" + assert channels is None or actual_channels == int( + channels + ), f"Image has wrong number of channels: {actual_channels} (expected {int(channels)})" def _compute_center_of_mass(im_arr: "numpy.typing.NDArray") -> Tuple[float, float]: @@ -84,7 +90,9 @@ def assert_image_has_intensities( assert len(center_of_mass) == 2, "center_of_mass must have two components" actual = _compute_center_of_mass(im_arr) distance = numpy.linalg.norm(numpy.subtract(center_of_mass, actual)) - assert distance <= float(eps), f"Wrong center of mass: {actual} (expected {center_of_mass}, distance: {distance}, eps: {eps})" + assert distance <= float( + eps + ), f"Wrong center of mass: {actual} (expected {center_of_mass}, distance: {distance}, eps: {eps})" def assert_image_has_labels( @@ -108,12 +116,14 @@ def assert_image_has_labels( if exclude_labels is None: exclude_labels = list() if isinstance(exclude_labels, str): + def cast_label(label): if numpy.issubdtype(im_arr.dtype, numpy.integer): return int(label) if numpy.issubdtype(im_arr.dtype, float): return float(label) raise AssertionError(f'Unsupported image label type: "{im_arr.dtype}"') + exclude_labels = [cast_label(label) for label in exclude_labels.split(",") if len(label) > 0] labels = [label for label in labels if label not in exclude_labels] @@ -121,10 +131,14 @@ def assert_image_has_labels( if number_of_objects is not None: actual_number_of_objects = len(labels) expected_number_of_objects = int(number_of_objects) - assert actual_number_of_objects == expected_number_of_objects, f"Wrong number of objects: {actual_number_of_objects} (expected {expected_number_of_objects})" + assert ( + actual_number_of_objects == expected_number_of_objects + ), f"Wrong number of objects: {actual_number_of_objects} (expected {expected_number_of_objects})" # Perform `mean_object_size` assertion. if mean_object_size is not None: actual_mean_object_size = sum((im_arr == label).sum() for label in labels) / len(labels) expected_mean_object_size = float(mean_object_size) - assert abs(actual_mean_object_size - expected_mean_object_size) <= float(eps), f"Wrong mean object size: {actual_mean_object_size} (expected {expected_mean_object_size}, eps: {eps})" + assert ( + abs(actual_mean_object_size - expected_mean_object_size) <= float(eps) + ), f"Wrong mean object size: {actual_mean_object_size} (expected {expected_mean_object_size}, eps: {eps})" From ef9302ce5c989e1c846eb4279d68f81704ab8d88 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sat, 2 Mar 2024 09:15:42 +0000 Subject: [PATCH 40/50] Fix linting issues --- lib/galaxy/tool_util/verify/asserts/image.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 3d64199fc56..65d0fc80488 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -32,9 +32,7 @@ def assert_image_has_metadata( buf = io.BytesIO(output_bytes) with Image.open(buf) as im: - assert width is None or im.size[0] == int( - width - ), f"Image has wrong width: {im.size[0]} (expected {int(width)})" + assert width is None or im.size[0] == int(width), f"Image has wrong width: {im.size[0]} (expected {int(width)})" assert height is None or im.size[1] == int( height @@ -139,6 +137,6 @@ def assert_image_has_labels( if mean_object_size is not None: actual_mean_object_size = sum((im_arr == label).sum() for label in labels) / len(labels) expected_mean_object_size = float(mean_object_size) - assert ( - abs(actual_mean_object_size - expected_mean_object_size) <= float(eps) + assert abs(actual_mean_object_size - expected_mean_object_size) <= float( + eps ), f"Wrong mean object size: {actual_mean_object_size} (expected {expected_mean_object_size}, eps: {eps})" From 10b05d5ca4c7fcb1e68bac850b26f95da344a783 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sat, 2 Mar 2024 09:28:47 +0000 Subject: [PATCH 41/50] Fix linting issues --- lib/galaxy/tool_util/verify/asserts/image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 65d0fc80488..ff3c0899dcc 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -1,10 +1,10 @@ import io from typing import ( - Optional, - Union, List, + Optional, Tuple, TYPE_CHECKING, + Union, ) try: From 5ce6045865a5efb07fe9535f2352aebe0423e945 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sat, 2 Mar 2024 20:24:44 +0100 Subject: [PATCH 42/50] Add `*_min` and `*_max` for `mean_intensity` and `mean_object_size`, fix default value for `eps` - Add `mean_intensity_min` and `mean_intensity_max` attributes for `image_has_intensities` assertion tag - Add `mean_object_size_min` and `mean_object_size_max` attributes for `image_has_labels` assertion tag - Fix default value for `eps`. --- lib/galaxy/tool_util/verify/asserts/image.py | 52 ++++++++++++++++---- lib/galaxy/tool_util/xsd/galaxy.xsd | 30 +++++++++-- test/functional/tools/validation_image.xml | 50 ++++++++++++++++++- 3 files changed, 117 insertions(+), 15 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index ff3c0899dcc..9f077900714 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -59,8 +59,10 @@ def assert_image_has_intensities( output_bytes: bytes, channel: Optional[Union[int, str]] = None, mean_intensity: Optional[Union[float, str]] = None, + mean_intensity_min: Optional[Union[float, str]] = None, + mean_intensity_max: Optional[Union[float, str]] = None, center_of_mass: Optional[Union[Tuple[float, float], str]] = None, - eps: Union[float, str] = 1e-8, + eps: Union[float, str] = 0.01, ) -> None: """ Assert the image output has specific intensity content. @@ -74,10 +76,26 @@ def assert_image_has_intensities( im_arr = im_arr[:, :, int(channel)] # Perform `mean_intensity` assertion. + actual_mean_intensity = im_arr.mean() if mean_intensity is not None: - actual = im_arr.mean() - expected = float(mean_intensity) - assert abs(actual - expected) <= float(eps), f"Wrong mean intensity: {actual} (expected {expected}, eps: {eps})" + mean_intensity = float(mean_intensity) + assert abs(actual_mean_intensity - mean_intensity) <= float( + eps + ), f"Wrong mean intensity: {actual_mean_intensity} (expected {mean_intensity}, eps: {eps})" + + # Perform `mean_intensity_min` assertion. + if mean_intensity_min is not None: + mean_intensity_min = float(mean_intensity_min) + assert ( + actual_mean_intensity >= mean_intensity_min + ), f"Wrong mean intensity: {actual_mean_intensity} (mean_intensity_min: {mean_intensity_min})" + + # Perform `mean_intensity_max` assertion. + if mean_intensity_max is not None: + mean_intensity_max = float(mean_intensity_max) + assert ( + actual_mean_intensity <= mean_intensity_max + ), f"Wrong mean intensity: {actual_mean_intensity} (mean_intensity_max: {mean_intensity_max})" # Perform `center_of_mass` assertion. if center_of_mass is not None: @@ -86,19 +104,21 @@ def assert_image_has_intensities( assert len(center_of_mass_parts) == 2 center_of_mass = (float(center_of_mass_parts[0]), float(center_of_mass_parts[1])) assert len(center_of_mass) == 2, "center_of_mass must have two components" - actual = _compute_center_of_mass(im_arr) - distance = numpy.linalg.norm(numpy.subtract(center_of_mass, actual)) + actual_center_of_mass = _compute_center_of_mass(im_arr) + distance = numpy.linalg.norm(numpy.subtract(center_of_mass, actual_center_of_mass)) assert distance <= float( eps - ), f"Wrong center of mass: {actual} (expected {center_of_mass}, distance: {distance}, eps: {eps})" + ), f"Wrong center of mass: {actual_center_of_mass} (expected {center_of_mass}, distance: {distance}, eps: {eps})" def assert_image_has_labels( output_bytes: bytes, number_of_objects: Optional[Union[int, str]] = None, mean_object_size: Optional[Union[float, str]] = None, + mean_object_size_min: Optional[Union[float, str]] = None, + mean_object_size_max: Optional[Union[float, str]] = None, exclude_labels: Optional[Union[str, List[int]]] = None, - eps: Union[float, str] = 1e-8, + eps: Union[float, str] = 0.01, ) -> None: """ Assert the image output has specific label content. @@ -134,9 +154,23 @@ def assert_image_has_labels( ), f"Wrong number of objects: {actual_number_of_objects} (expected {expected_number_of_objects})" # Perform `mean_object_size` assertion. + actual_mean_object_size = sum((im_arr == label).sum() for label in labels) / len(labels) if mean_object_size is not None: - actual_mean_object_size = sum((im_arr == label).sum() for label in labels) / len(labels) expected_mean_object_size = float(mean_object_size) assert abs(actual_mean_object_size - expected_mean_object_size) <= float( eps ), f"Wrong mean object size: {actual_mean_object_size} (expected {expected_mean_object_size}, eps: {eps})" + + # Perform `mean_object_size_min` assertion. + if mean_object_size_min is not None: + mean_object_size_min = float(mean_object_size_min) + assert ( + actual_mean_object_size >= mean_object_size_min + ), f"Wrong mean object size: {actual_mean_object_size} (mean_object_size_min: {mean_object_size_min})" + + # Perform `mean_object_size_max` assertion. + if mean_object_size_max is not None: + mean_object_size_max = float(mean_object_size_max) + assert ( + actual_mean_object_size <= mean_object_size_max + ), f"Wrong mean object size: {actual_mean_object_size} (mean_object_size_max: {mean_object_size_max})" diff --git a/lib/galaxy/tool_util/xsd/galaxy.xsd b/lib/galaxy/tool_util/xsd/galaxy.xsd index 718892a12cf..73a3e7f3e1a 100644 --- a/lib/galaxy/tool_util/xsd/galaxy.xsd +++ b/lib/galaxy/tool_util/xsd/galaxy.xsd @@ -2798,19 +2798,29 @@ $attribute_list::5 The required mean value of the image intensities. + + + A lower bound of the required mean value of the image intensities. + + + + + An upper bound of the required mean value of the image intensities. + + The required center of mass of the image intensities (horizontal and vertical coordinate, separated by a comma). - + - The absolute tolerance to be used for the ``mean_intensity`` and ``center_of_mass`` assertions (defaults to ``1e-8``). + The absolute tolerance to be used for the ``mean_intensity`` and ``center_of_mass`` assertions (defaults to ``0.01``). - Restricts the ``mean_intensity`` and ``center_of_mass`` assertions to a specific channel of the image (where the value ``0`` corresponds to the first image channel). + Restricts the ``mean_intensity``, ``mean_intensity_min``, ``mean_intensity_max``, and ``center_of_mass`` assertions to a specific channel of the image (where the value ``0`` corresponds to the first image channel). @@ -2837,9 +2847,19 @@ $attribute_list::5 The required mean size of the objects in the image, where the size of an object is measured by the number of pixels. It is assumed that each individual object corresponds to a unique label. - + - The absolute tolerance to be used for the ``mean_object_size`` assertion (defaults to ``1e-8``). + A lower bound of the required mean size of the objects in the image, where the size of an object is measured by the number of pixels. It is assumed that each individual object corresponds to a unique label. + + + + + An upper bound of the required mean size of the objects in the image, where the size of an object is measured by the number of pixels. It is assumed that each individual object corresponds to a unique label. + + + + + The absolute tolerance to be used for the ``mean_object_size`` assertion (defaults to 0.01). diff --git a/test/functional/tools/validation_image.xml b/test/functional/tools/validation_image.xml index 8789d9655bf..421736f36cc 100644 --- a/test/functional/tools/validation_image.xml +++ b/test/functional/tools/validation_image.xml @@ -15,7 +15,7 @@ - + @@ -47,6 +47,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -83,5 +107,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 7aebd26db4d7b252f180a197cbc73c14da7049d8 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sat, 2 Mar 2024 21:52:01 +0100 Subject: [PATCH 43/50] Add `_assert_float` function and refactor --- lib/galaxy/tool_util/verify/asserts/image.py | 81 ++++++++++---------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 9f077900714..d635b72b8ae 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -20,6 +20,30 @@ if TYPE_CHECKING: import numpy.typing +def _assert_float( + actual: float, + label: str, + tolerance: Union[float, str], + expected: Optional[Union[float, str]] = None, + range_min: Optional[Union[float, str]] = None, + range_max: Optional[Union[float, str]] = None, +) -> None: + + # Perform `tolerance` based check. + if expected is not None: + assert abs(actual - float(expected)) <= float( + tolerance + ), f"Wrong {label}: {actual} (expected {expected} ±{tolerance})" + + # Perform `range_min` based check. + if range_min is not None: + assert actual >= float(range_min), f"Wrong {label}: {actual} (must be {range_min} or larger)" + + # Perform `range_max` based check. + if range_max is not None: + assert actual <= float(range_max), f"Wrong {label}: {actual} (must be {range_max} or smaller)" + + def assert_image_has_metadata( output_bytes: bytes, width: Optional[Union[int, str]] = None, @@ -75,27 +99,15 @@ def assert_image_has_intensities( if channel is not None: im_arr = im_arr[:, :, int(channel)] - # Perform `mean_intensity` assertion. - actual_mean_intensity = im_arr.mean() - if mean_intensity is not None: - mean_intensity = float(mean_intensity) - assert abs(actual_mean_intensity - mean_intensity) <= float( - eps - ), f"Wrong mean intensity: {actual_mean_intensity} (expected {mean_intensity}, eps: {eps})" - - # Perform `mean_intensity_min` assertion. - if mean_intensity_min is not None: - mean_intensity_min = float(mean_intensity_min) - assert ( - actual_mean_intensity >= mean_intensity_min - ), f"Wrong mean intensity: {actual_mean_intensity} (mean_intensity_min: {mean_intensity_min})" - - # Perform `mean_intensity_max` assertion. - if mean_intensity_max is not None: - mean_intensity_max = float(mean_intensity_max) - assert ( - actual_mean_intensity <= mean_intensity_max - ), f"Wrong mean intensity: {actual_mean_intensity} (mean_intensity_max: {mean_intensity_max})" + # Perform `mean_intensity` assertions. + _assert_float( + actual=im_arr.mean(), + label="mean intensity", + tolerance=eps, + expected=mean_intensity, + range_min=mean_intensity_min, + range_max=mean_intensity_max, + ) # Perform `center_of_mass` assertion. if center_of_mass is not None: @@ -155,22 +167,11 @@ def assert_image_has_labels( # Perform `mean_object_size` assertion. actual_mean_object_size = sum((im_arr == label).sum() for label in labels) / len(labels) - if mean_object_size is not None: - expected_mean_object_size = float(mean_object_size) - assert abs(actual_mean_object_size - expected_mean_object_size) <= float( - eps - ), f"Wrong mean object size: {actual_mean_object_size} (expected {expected_mean_object_size}, eps: {eps})" - - # Perform `mean_object_size_min` assertion. - if mean_object_size_min is not None: - mean_object_size_min = float(mean_object_size_min) - assert ( - actual_mean_object_size >= mean_object_size_min - ), f"Wrong mean object size: {actual_mean_object_size} (mean_object_size_min: {mean_object_size_min})" - - # Perform `mean_object_size_max` assertion. - if mean_object_size_max is not None: - mean_object_size_max = float(mean_object_size_max) - assert ( - actual_mean_object_size <= mean_object_size_max - ), f"Wrong mean object size: {actual_mean_object_size} (mean_object_size_max: {mean_object_size_max})" + _assert_float( + actual=actual_mean_object_size, + label="mean object size", + tolerance=eps, + expected=mean_object_size, + range_min=mean_object_size_min, + range_max=mean_object_size_max, + ) From 00485ccc5294815c06e1d4d51d92d487f4133c65 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Tue, 5 Mar 2024 22:04:55 +0100 Subject: [PATCH 44/50] Squashed commit of the following: commit 33b660ee19fa58e84643bba0b875c874a7d3da12 Author: Leonid Kostrykin Date: Tue Mar 5 20:58:24 2024 +0000 Fix XSD commit 4ee6112b84d3406d6ba98dd190fe07fda2d90307 Author: Leonid Kostrykin Date: Tue Mar 5 20:53:14 2024 +0000 Add `labels` attribute for `has_image_labels` and `has_image_mean_object_size` commit 4ff401c4f0731cc3aceb9c511f7c3c61abf2b19c Author: Leonid Kostrykin Date: Tue Mar 5 20:32:57 2024 +0000 Replace `image_has_labels` by `has_image_labels` and `has_image_mean_object_size` commit 09149d1394a1b81c8c3036316ab2be355db24cde Author: Leonid Kostrykin Date: Tue Mar 5 19:51:56 2024 +0000 Fix bug commit 28fedb98f19a35ab6a4747aa5b435c329277df50 Author: Leonid Kostrykin Date: Tue Mar 5 19:44:12 2024 +0000 Replace `image_has_intensities` by `has_image_mean_intensity` and `has_image_center_of_mass` commit fe96916ca28752f5c5512658db53c4162d8d7d66 Author: Leonid Kostrykin Date: Tue Mar 5 19:10:38 2024 +0000 Replace `image_has_metadata` by `has_image_width`, etc. --- lib/galaxy/tool_util/verify/asserts/image.py | 259 +++++++++++++------ lib/galaxy/tool_util/xsd/galaxy.xsd | 248 ++++++++++++++---- test/functional/tools/validation_image.xml | 89 ++++--- 3 files changed, 442 insertions(+), 154 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index d635b72b8ae..ff2ea64654a 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -1,5 +1,6 @@ import io from typing import ( + Any, List, Optional, Tuple, @@ -7,6 +8,8 @@ from typing import ( Union, ) +from ._util import _assert_number + try: import numpy except ImportError: @@ -44,28 +47,79 @@ def _assert_float( assert actual <= float(range_max), f"Wrong {label}: {actual} (must be {range_max} or smaller)" -def assert_image_has_metadata( +def assert_has_image_width( output_bytes: bytes, - width: Optional[Union[int, str]] = None, - height: Optional[Union[int, str]] = None, - channels: Optional[Union[int, str]] = None, + value: Optional[Union[int, str]] = None, + delta: Union[int, str] = 0, + min: Optional[Union[int, str]] = None, + max: Optional[Union[int, str]] = None, + negate: Union[bool, str] = False, ) -> None: """ - Assert the image output has specific metadata. + 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], + value, + delta, + min, + max, + negate, + "{expected} width {n}+-{delta}", + "{expected} width to be in [{min}:{max}]", + ) - assert width is None or im.size[0] == int(width), f"Image has wrong width: {im.size[0]} (expected {int(width)})" - assert height is None or im.size[1] == int( - height - ), f"Image has wrong height: {im.size[1]} (expected {int(height)})" +def assert_has_image_height( + output_bytes: bytes, + value: Optional[Union[int, str]] = None, + delta: Union[int, str] = 0, + min: Optional[Union[int, str]] = None, + max: Optional[Union[int, str]] = None, + negate: Union[bool, str] = False, +) -> None: + """ + 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], + value, + delta, + min, + max, + negate, + "{expected} height {n}+-{delta}", + "{expected} height to be in [{min}:{max}]", + ) - actual_channels = len(im.getbands()) - assert channels is None or actual_channels == int( - channels - ), f"Image has wrong number of channels: {actual_channels} (expected {int(channels)})" + +def assert_has_image_channels( + output_bytes: bytes, + value: Optional[Union[int, str]] = None, + delta: Union[int, str] = 0, + min: Optional[Union[int, str]] = None, + max: Optional[Union[int, str]] = None, + negate: Union[bool, str] = False, +) -> None: + """ + 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()), + value, + 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]: @@ -79,17 +133,12 @@ def _compute_center_of_mass(im_arr: "numpy.typing.NDArray") -> Tuple[float, floa return (im_arr * xx).sum(), (im_arr * yy).sum() -def assert_image_has_intensities( +def _get_image( output_bytes: bytes, channel: Optional[Union[int, str]] = None, - mean_intensity: Optional[Union[float, str]] = None, - mean_intensity_min: Optional[Union[float, str]] = None, - mean_intensity_max: Optional[Union[float, str]] = None, - center_of_mass: Optional[Union[Tuple[float, float], str]] = None, - eps: Union[float, str] = 0.01, -) -> None: +) -> "numpy.typing.NDArray": """ - Assert the image output has specific intensity content. + Returns the output image or a specific channel. """ buf = io.BytesIO(output_bytes) with Image.open(buf) as im: @@ -99,79 +148,143 @@ def assert_image_has_intensities( if channel is not None: im_arr = im_arr[:, :, int(channel)] - # Perform `mean_intensity` assertions. + # Return the image + return im_arr + + +def assert_has_image_mean_intensity( + output_bytes: bytes, + channel: Optional[Union[int, str]] = None, + value: Optional[Union[float, str]] = None, + delta: Union[float, str] = 0.01, + min: Optional[Union[float, str]] = None, + max: Optional[Union[float, str]] = None, +) -> None: + """ + Asserts the specified output is an image and has the specified mean intensity value. + """ + im_arr = _get_image(output_bytes, channel) _assert_float( actual=im_arr.mean(), label="mean intensity", - tolerance=eps, - expected=mean_intensity, - range_min=mean_intensity_min, - range_max=mean_intensity_max, + tolerance=delta, + expected=value, + range_min=min, + range_max=max, ) - # Perform `center_of_mass` assertion. - if center_of_mass is not None: - if isinstance(center_of_mass, str): - center_of_mass_parts = [c.strip() for c in center_of_mass.split(",")] - assert len(center_of_mass_parts) == 2 - center_of_mass = (float(center_of_mass_parts[0]), float(center_of_mass_parts[1])) - assert len(center_of_mass) == 2, "center_of_mass must have two components" - actual_center_of_mass = _compute_center_of_mass(im_arr) - distance = numpy.linalg.norm(numpy.subtract(center_of_mass, actual_center_of_mass)) - assert distance <= float( - eps - ), f"Wrong center of mass: {actual_center_of_mass} (expected {center_of_mass}, distance: {distance}, eps: {eps})" - -def assert_image_has_labels( +def assert_has_image_center_of_mass( output_bytes: bytes, - number_of_objects: Optional[Union[int, str]] = None, - mean_object_size: Optional[Union[float, str]] = None, - mean_object_size_min: Optional[Union[float, str]] = None, - mean_object_size_max: Optional[Union[float, str]] = None, - exclude_labels: Optional[Union[str, List[int]]] = None, - eps: Union[float, str] = 0.01, + channel: Optional[Union[int, str]] = None, + point: Optional[Union[Tuple[float, float], str]] = None, + delta: Union[float, str] = 0.01, ) -> None: """ - Assert the image output has specific label content. + Asserts the specified output is an image and has the specified center of mass. """ - buf = io.BytesIO(output_bytes) - with Image.open(buf) as im: - im_arr = numpy.array(im) + im_arr = _get_image(output_bytes, channel) + if point is not None: + if isinstance(point, str): + point_parts = [c.strip() for c in point.split(",")] + assert len(point_parts) == 2 + point = (float(point_parts[0]), float(point_parts[1])) + assert len(point) == 2, "point must have two components" + actual_center_of_mass = _compute_center_of_mass(im_arr) + distance = numpy.linalg.norm(numpy.subtract(point, actual_center_of_mass)) + assert distance <= float( + delta + ), f"Wrong center of mass: {actual_center_of_mass} (expected {point}, distance: {distance}, delta: {delta})" + + +def _get_image_labels( + output_bytes: bytes, + channel: Optional[Union[int, str]] = None, + labels: Optional[Union[str, List[int]]] = None, + exclude_labels: Optional[Union[str, List[int]]] = None, +) -> Tuple["numpy.typing.NDArray", List[Any]]: + """ + Determines the unique labels in the output image or a specific channel. + """ + assert labels is None or exclude_labels is None + im_arr = _get_image(output_bytes, channel) + + def cast_label(label): + label = label.strip() + if numpy.issubdtype(im_arr.dtype, numpy.integer): + return int(label) + if numpy.issubdtype(im_arr.dtype, float): + return float(label) + raise AssertionError(f'Unsupported image label type: "{im_arr.dtype}"') # Determine labels present in the image. - labels = numpy.unique(im_arr) + present_labels = numpy.unique(im_arr) + + # Apply filtering due to `labels` (keep only those). + if labels is None: + labels = list() + if isinstance(labels, str): + labels = [cast_label(label) for label in labels.split(",") if len(label) > 0] + if len(labels) > 0: + present_labels = [label for label in present_labels if label in labels] # Apply filtering due to `exclude_labels`. if exclude_labels is None: exclude_labels = list() if isinstance(exclude_labels, str): - - def cast_label(label): - if numpy.issubdtype(im_arr.dtype, numpy.integer): - return int(label) - if numpy.issubdtype(im_arr.dtype, float): - return float(label) - raise AssertionError(f'Unsupported image label type: "{im_arr.dtype}"') - exclude_labels = [cast_label(label) for label in exclude_labels.split(",") if len(label) > 0] - labels = [label for label in labels if label not in exclude_labels] + present_labels = [label for label in present_labels if label not in exclude_labels] - # Perform `number_of_objects` assertion. - if number_of_objects is not None: - actual_number_of_objects = len(labels) - expected_number_of_objects = int(number_of_objects) - assert ( - actual_number_of_objects == expected_number_of_objects - ), f"Wrong number of objects: {actual_number_of_objects} (expected {expected_number_of_objects})" + # Return the image data and the labels. + return im_arr, present_labels - # Perform `mean_object_size` assertion. - actual_mean_object_size = sum((im_arr == label).sum() for label in labels) / len(labels) + +def assert_has_image_labels( + output_bytes: bytes, + channel: Optional[Union[int, str]] = None, + exclude_labels: Optional[Union[str, List[int]]] = None, + value: Optional[Union[int, str]] = None, + delta: Union[int, str] = 0, + min: Optional[Union[int, str]] = None, + max: Optional[Union[int, str]] = None, + negate: Union[bool, str] = False, +) -> None: + """ + Asserts the specified output is an image and has the specified number of unique values (e.g., uniquely labeled objects). + """ + present_labels = _get_image_labels(output_bytes, channel, exclude_labels)[1] + _assert_number( + len(present_labels), + value, + delta, + min, + max, + negate, + "{expected} labels {n}+-{delta}", + "{expected} labels to be in [{min}:{max}]", + ) + + +def assert_has_image_mean_object_size( + output_bytes: bytes, + channel: Optional[Union[int, str]] = None, + labels: Optional[Union[str, List[int]]] = None, + exclude_labels: Optional[Union[str, List[int]]] = None, + value: Optional[Union[float, str]] = None, + delta: Union[float, str] = 0.01, + min: Optional[Union[float, str]] = None, + max: Optional[Union[float, str]] = None, +) -> None: + """ + Asserts the specified output is an image with labeled objects which have the specified mean size (number of pixels). + """ + im_arr, present_labels = _get_image_labels(output_bytes, channel, labels, exclude_labels) + actual_mean_object_size = sum((im_arr == label).sum() for label in present_labels) / len(present_labels) _assert_float( actual=actual_mean_object_size, label="mean object size", - tolerance=eps, - expected=mean_object_size, - range_min=mean_object_size_min, - range_max=mean_object_size_max, + tolerance=delta, + expected=value, + range_min=min, + range_max=max, ) diff --git a/lib/galaxy/tool_util/xsd/galaxy.xsd b/lib/galaxy/tool_util/xsd/galaxy.xsd index 73a3e7f3e1a..0d107df49ee 100644 --- a/lib/galaxy/tool_util/xsd/galaxy.xsd +++ b/lib/galaxy/tool_util/xsd/galaxy.xsd @@ -2248,9 +2248,13 @@ module. - - - + + + + + + + @@ -2751,120 +2755,260 @@ $attribute_list::5 - + -``` +Asserts the output is an image and has a specific width (in pixels) of ``value`` +plus/minus ``delta`` (e.g., ````). +Alternatively the range of the expected width can be specified by ``min`` and/or ``max``. $attribute_list::5 ]]> - + - The required width of the image. + Expected width of the image (in pixels).` - + - The required height of the image. + Maximum allowed difference of the image width (in pixels, default is 0). The observed width has to be in the range ``value +- delta``. - + - The required number of channels of the image (e.g., 1 for grayscale, 3 for RGB, 4 for RGBA images). + Minimum allowed width of the image (in pixels). + + + Maximum allowed width of the image (in pixels). + + + - + -``` +Asserts the output is an image and has a specific height (in pixels) of ``value`` +plus/minus ``delta`` (e.g., ````). +Alternatively the range of the expected height can be specified by ``min`` and/or ``max``. $attribute_list::5 ]]> - + + + Expected height of the image (in pixels).` + + + + + Maximum allowed difference of the image height (in pixels, default is 0). The observed height has to be in the range ``value +- delta``. + + + + + Minimum allowed height of the image (in pixels). + + + + + Maximum allowed height of the image (in pixels). + + + + + + + ``). +Alternatively the range of the expected number of channels can be specified by ``min`` and/or ``max``. + +$attribute_list::5 +]]> + + + + + Expected number of channels of the image.` + + + + + Maximum allowed difference of the number of channels (default is 0). The observed number of channels has to be in the range ``value +- delta``. + + + + + Minimum allowed number of channels. + + + + + Maximum allowed number of channels. + + + + + + + ``). +Alternatively the range of the expected mean intensity value can be specified by ``min`` and/or ``max``. + +$attribute_list::5 +]]> + + + The required mean value of the image intensities. - + + + The absolute tolerance to be used for ``value`` (defaults to ``0.01``). The observed mean value of the image intensities has to be in the range ``value +- delta``. + + + A lower bound of the required mean value of the image intensities. - + An upper bound of the required mean value of the image intensities. - - - The required center of mass of the image intensities (horizontal and vertical coordinate, separated by a comma). - - - - - The absolute tolerance to be used for the ``mean_intensity`` and ``center_of_mass`` assertions (defaults to ``0.01``). - - - Restricts the ``mean_intensity``, ``mean_intensity_min``, ``mean_intensity_max``, and ``center_of_mass`` assertions to a specific channel of the image (where the value ``0`` corresponds to the first image channel). + Restricts the assertion to a specific channel of the image (where ``0`` corresponds to the first image channel). - + -``` +Asserts the output is an image and has a specific center of mass of ``point``, +or has an Euclidean distance of ``delta`` or less to that point (e.g., +````). $attribute_list::5 ]]> - + - The required number of unique labels in the image. It is assumed that each individual object corresponds to a unique label. + The required center of mass of the image intensities (horizontal and vertical coordinate, separated by a comma). - + - The required mean size of the objects in the image, where the size of an object is measured by the number of pixels. It is assumed that each individual object corresponds to a unique label. + The maximum allowed Euclidean distance to the required center of mass (defaults to ``0.01``). - + - A lower bound of the required mean size of the objects in the image, where the size of an object is measured by the number of pixels. It is assumed that each individual object corresponds to a unique label. + Restricts the assertion to a specific channel of the image (where ``0`` corresponds to the first image channel). - + + + + ``). +The primary usage of this assertion is to verify the number of objects in images with uniquely labeled objects. + +$attribute_list::5 +]]> + + + - An upper bound of the required mean size of the objects in the image, where the size of an object is measured by the number of pixels. It is assumed that each individual object corresponds to a unique label. + Expected number of labels.` - + - The absolute tolerance to be used for the ``mean_object_size`` assertion (defaults to 0.01). + Maximum allowed difference of the number of labels (default is 0). The observed number of labels has to be in the range ``value +- delta``. + + + + + Minimum allowed number of labels. + + + + + Maximum allowed number of labels. + + + + + Restricts the assertion to a specific channel of the image (where ``0`` corresponds to the first image channel). Must be used with multi-channel imags. + + + + + List of labels, separated by a comma. Labels *not* on this list will be excluded from consideration. Cannot be used in combination with ``exclude_labels``. - List of labels to be excluded from consideration for the ``number_of_objects`` and ``mean_object_size`` assertions, separated by a comma. The primary usage of this attribute is to exclude the background of a label image. + List of labels to be excluded from consideration, separated by a comma. The primary usage of this attribute is to exclude the background of a label image. Cannot be used in combination with ``labels``. + + + + + + + ``). The labels must be unique. + +$attribute_list::5 +]]> + + + + + The required mean size of the uniquely labeled objects. + + + + + The absolute tolerance to be used for ``value`` (defaults to ``0.01``). The observed mean size of the uniquely labeled objects has to be in the range ``value +- delta``. + + + + + A lower bound of the required mean size of the uniquely labeled objects. + + + + + An upper bound of the required mean size of the uniquely labeled objects. + + + + + Restricts the assertion to a specific channel of the image (where ``0`` corresponds to the first image channel). Must be used with multi-channel imags. + + + + + List of labels, separated by a comma. Labels *not* on this list will be excluded from consideration. Cannot be used in combination with ``exclude_labels``. + + + + + List of labels to be excluded from consideration, separated by a comma. The primary usage of this attribute is to exclude the background of a label image. Cannot be used in combination with ``labels``. diff --git a/test/functional/tools/validation_image.xml b/test/functional/tools/validation_image.xml index 421736f36cc..840ffdfa771 100644 --- a/test/functional/tools/validation_image.xml +++ b/test/functional/tools/validation_image.xml @@ -14,8 +14,10 @@ - - + + + + @@ -23,11 +25,15 @@ - - - - - + + + + + + + + + @@ -35,7 +41,7 @@ - + @@ -43,7 +49,7 @@ - + @@ -51,7 +57,7 @@ - + @@ -59,7 +65,7 @@ - + @@ -67,7 +73,7 @@ - + @@ -76,8 +82,11 @@ - - + + + + + @@ -85,16 +94,11 @@ - - - - - - - - - - + + + + + @@ -102,8 +106,7 @@ - - + @@ -111,7 +114,7 @@ - + @@ -119,7 +122,27 @@ - + + + + + + + + + + + + + + + + + + + + + @@ -127,7 +150,15 @@ - + + + + + + + + + From fb7bbaaa83807153cee1474b5e5e5c638165d857 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Mon, 11 Mar 2024 11:31:23 +0100 Subject: [PATCH 45/50] Rename `delta` to `eps` for float-based assertions (#1) * Rename `delta` to `eps` for float-based assertions * Fix tests --- lib/galaxy/tool_util/verify/asserts/image.py | 14 ++++++------- lib/galaxy/tool_util/xsd/galaxy.xsd | 19 +++++++++-------- test/functional/tools/validation_image.xml | 22 +++++++++++++++++--- 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index ff2ea64654a..6f9c1609c34 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -156,7 +156,7 @@ def assert_has_image_mean_intensity( output_bytes: bytes, channel: Optional[Union[int, str]] = None, value: Optional[Union[float, str]] = None, - delta: Union[float, str] = 0.01, + eps: Union[float, str] = 0.01, min: Optional[Union[float, str]] = None, max: Optional[Union[float, str]] = None, ) -> None: @@ -167,7 +167,7 @@ def assert_has_image_mean_intensity( _assert_float( actual=im_arr.mean(), label="mean intensity", - tolerance=delta, + tolerance=eps, expected=value, range_min=min, range_max=max, @@ -178,7 +178,7 @@ def assert_has_image_center_of_mass( output_bytes: bytes, channel: Optional[Union[int, str]] = None, point: Optional[Union[Tuple[float, float], str]] = None, - delta: Union[float, str] = 0.01, + eps: Union[float, str] = 0.01, ) -> None: """ Asserts the specified output is an image and has the specified center of mass. @@ -193,8 +193,8 @@ def assert_has_image_center_of_mass( actual_center_of_mass = _compute_center_of_mass(im_arr) distance = numpy.linalg.norm(numpy.subtract(point, actual_center_of_mass)) assert distance <= float( - delta - ), f"Wrong center of mass: {actual_center_of_mass} (expected {point}, distance: {distance}, delta: {delta})" + eps + ), f"Wrong center of mass: {actual_center_of_mass} (expected {point}, distance: {distance}, eps: {eps})" def _get_image_labels( @@ -271,7 +271,7 @@ def assert_has_image_mean_object_size( labels: Optional[Union[str, List[int]]] = None, exclude_labels: Optional[Union[str, List[int]]] = None, value: Optional[Union[float, str]] = None, - delta: Union[float, str] = 0.01, + eps: Union[float, str] = 0.01, min: Optional[Union[float, str]] = None, max: Optional[Union[float, str]] = None, ) -> None: @@ -283,7 +283,7 @@ def assert_has_image_mean_object_size( _assert_float( actual=actual_mean_object_size, label="mean object size", - tolerance=delta, + tolerance=eps, expected=value, range_min=min, range_max=max, diff --git a/lib/galaxy/tool_util/xsd/galaxy.xsd b/lib/galaxy/tool_util/xsd/galaxy.xsd index 0d107df49ee..b6b6fb10279 100644 --- a/lib/galaxy/tool_util/xsd/galaxy.xsd +++ b/lib/galaxy/tool_util/xsd/galaxy.xsd @@ -2858,7 +2858,7 @@ $attribute_list::5 ``). +plus/minus ``eps`` (e.g., ````). Alternatively the range of the expected mean intensity value can be specified by ``min`` and/or ``max``. $attribute_list::5 @@ -2870,9 +2870,9 @@ $attribute_list::5 The required mean value of the image intensities. - + - The absolute tolerance to be used for ``value`` (defaults to ``0.01``). The observed mean value of the image intensities has to be in the range ``value +- delta``. + The absolute tolerance to be used for ``value`` (defaults to ``0.01``). The observed mean value of the image intensities has to be in the range ``value +- eps``. @@ -2895,7 +2895,7 @@ $attribute_list::5 ``). $attribute_list::5 @@ -2907,7 +2907,7 @@ $attribute_list::5 The required center of mass of the image intensities (horizontal and vertical coordinate, separated by a comma). - + The maximum allowed Euclidean distance to the required center of mass (defaults to ``0.01``). @@ -2969,8 +2969,9 @@ $attribute_list::5 ``). The labels must be unique. +Asserts the output is an image with labeled objects which have the specified mean size (number of pixels), +plus/minus ``eps`` (e.g., ````). +The labels must be unique. $attribute_list::5 ]]> @@ -2981,9 +2982,9 @@ $attribute_list::5 The required mean size of the uniquely labeled objects. - + - The absolute tolerance to be used for ``value`` (defaults to ``0.01``). The observed mean size of the uniquely labeled objects has to be in the range ``value +- delta``. + The absolute tolerance to be used for ``value`` (defaults to ``0.01``). The observed mean size of the uniquely labeled objects has to be in the range ``value +- eps``. diff --git a/test/functional/tools/validation_image.xml b/test/functional/tools/validation_image.xml index 840ffdfa771..c6358479686 100644 --- a/test/functional/tools/validation_image.xml +++ b/test/functional/tools/validation_image.xml @@ -21,6 +21,22 @@ + + + + + + + + + + + + + + + + @@ -49,7 +65,7 @@ - + @@ -122,7 +138,7 @@ - + @@ -134,7 +150,7 @@ - + From 5964f72dfcb3092ecb3eac1037daa6dae099b2c0 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 13 Mar 2024 07:28:48 +0000 Subject: [PATCH 46/50] Rename assertion attribute `value` --- lib/galaxy/tool_util/verify/asserts/image.py | 43 +++++++------ lib/galaxy/tool_util/xsd/galaxy.xsd | 34 +++++------ test/functional/tools/validation_image.xml | 64 ++++++++++---------- 3 files changed, 70 insertions(+), 71 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 6f9c1609c34..3ebd46095f9 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -49,7 +49,7 @@ def _assert_float( def assert_has_image_width( output_bytes: bytes, - value: Optional[Union[int, str]] = None, + width: Optional[Union[int, str]] = None, delta: Union[int, str] = 0, min: Optional[Union[int, str]] = None, max: Optional[Union[int, str]] = None, @@ -62,7 +62,7 @@ def assert_has_image_width( with Image.open(buf) as im: _assert_number( im.size[0], - value, + width, delta, min, max, @@ -74,7 +74,7 @@ def assert_has_image_width( def assert_has_image_height( output_bytes: bytes, - value: Optional[Union[int, str]] = None, + height: Optional[Union[int, str]] = None, delta: Union[int, str] = 0, min: Optional[Union[int, str]] = None, max: Optional[Union[int, str]] = None, @@ -87,7 +87,7 @@ def assert_has_image_height( with Image.open(buf) as im: _assert_number( im.size[1], - value, + height, delta, min, max, @@ -99,7 +99,7 @@ def assert_has_image_height( def assert_has_image_channels( output_bytes: bytes, - value: Optional[Union[int, str]] = None, + channels: Optional[Union[int, str]] = None, delta: Union[int, str] = 0, min: Optional[Union[int, str]] = None, max: Optional[Union[int, str]] = None, @@ -112,7 +112,7 @@ def assert_has_image_channels( with Image.open(buf) as im: _assert_number( len(im.getbands()), - value, + channels, delta, min, max, @@ -155,7 +155,7 @@ def _get_image( def assert_has_image_mean_intensity( output_bytes: bytes, channel: Optional[Union[int, str]] = None, - value: Optional[Union[float, str]] = None, + mean_intensity: Optional[Union[float, str]] = None, eps: Union[float, str] = 0.01, min: Optional[Union[float, str]] = None, max: Optional[Union[float, str]] = None, @@ -168,7 +168,7 @@ def assert_has_image_mean_intensity( actual=im_arr.mean(), label="mean intensity", tolerance=eps, - expected=value, + expected=mean_intensity, range_min=min, range_max=max, ) @@ -176,25 +176,24 @@ def assert_has_image_mean_intensity( def assert_has_image_center_of_mass( output_bytes: bytes, + center_of_mass: Union[Tuple[float, float], str], channel: Optional[Union[int, str]] = None, - point: Optional[Union[Tuple[float, float], str]] = None, eps: Union[float, str] = 0.01, ) -> None: """ Asserts the specified output is an image and has the specified center of mass. """ im_arr = _get_image(output_bytes, channel) - if point is not None: - if isinstance(point, str): - point_parts = [c.strip() for c in point.split(",")] - assert len(point_parts) == 2 - point = (float(point_parts[0]), float(point_parts[1])) - assert len(point) == 2, "point must have two components" - actual_center_of_mass = _compute_center_of_mass(im_arr) - distance = numpy.linalg.norm(numpy.subtract(point, actual_center_of_mass)) - assert distance <= float( - eps - ), f"Wrong center of mass: {actual_center_of_mass} (expected {point}, distance: {distance}, eps: {eps})" + if isinstance(center_of_mass, str): + center_of_mass_parts = [c.strip() for c in center_of_mass.split(",")] + assert len(center_of_mass_parts) == 2 + center_of_mass = (float(center_of_mass_parts[0]), float(center_of_mass_parts[1])) + assert len(center_of_mass) == 2, "center_of_mass must have two components" + actual_center_of_mass = _compute_center_of_mass(im_arr) + distance = numpy.linalg.norm(numpy.subtract(center_of_mass, actual_center_of_mass)) + assert distance <= float( + eps + ), f"Wrong center of mass: {actual_center_of_mass} (expected {center_of_mass}, distance: {distance}, eps: {eps})" def _get_image_labels( @@ -270,7 +269,7 @@ def assert_has_image_mean_object_size( channel: Optional[Union[int, str]] = None, labels: Optional[Union[str, List[int]]] = None, exclude_labels: Optional[Union[str, List[int]]] = None, - value: Optional[Union[float, str]] = None, + mean_object_size: Optional[Union[float, str]] = None, eps: Union[float, str] = 0.01, min: Optional[Union[float, str]] = None, max: Optional[Union[float, str]] = None, @@ -284,7 +283,7 @@ def assert_has_image_mean_object_size( actual=actual_mean_object_size, label="mean object size", tolerance=eps, - expected=value, + expected=mean_object_size, range_min=min, range_max=max, ) diff --git a/lib/galaxy/tool_util/xsd/galaxy.xsd b/lib/galaxy/tool_util/xsd/galaxy.xsd index b6b6fb10279..f9d13253511 100644 --- a/lib/galaxy/tool_util/xsd/galaxy.xsd +++ b/lib/galaxy/tool_util/xsd/galaxy.xsd @@ -2758,15 +2758,15 @@ $attribute_list::5 ``). +Asserts the output is an image and has a specific width (in pixels), +plus/minus ``delta`` (e.g., ````). Alternatively the range of the expected width can be specified by ``min`` and/or ``max``. $attribute_list::5 ]]> - + Expected width of the image (in pixels).` @@ -2791,15 +2791,15 @@ $attribute_list::5 ``). +Asserts the output is an image and has a specific height (in pixels), +plus/minus ``delta`` (e.g., ````). Alternatively the range of the expected height can be specified by ``min`` and/or ``max``. $attribute_list::5 ]]> - + Expected height of the image (in pixels).` @@ -2824,15 +2824,15 @@ $attribute_list::5 ``). +Asserts the output is an image and has a specific number of channels, +plus/minus ``delta`` (e.g., ````). Alternatively the range of the expected number of channels can be specified by ``min`` and/or ``max``. $attribute_list::5 ]]> - + Expected number of channels of the image.` @@ -2857,15 +2857,15 @@ $attribute_list::5 ``). +Asserts the output is an image and has a specific mean intensity value, +plus/minus ``eps`` (e.g., ````). Alternatively the range of the expected mean intensity value can be specified by ``min`` and/or ``max``. $attribute_list::5 ]]> - + The required mean value of the image intensities. @@ -2894,15 +2894,15 @@ $attribute_list::5 ``). +````). $attribute_list::5 ]]> - + The required center of mass of the image intensities (horizontal and vertical coordinate, separated by a comma). @@ -2970,14 +2970,14 @@ $attribute_list::5 ``). +plus/minus ``eps`` (e.g., ````). The labels must be unique. $attribute_list::5 ]]> - + The required mean size of the uniquely labeled objects. diff --git a/test/functional/tools/validation_image.xml b/test/functional/tools/validation_image.xml index c6358479686..dd79503a357 100644 --- a/test/functional/tools/validation_image.xml +++ b/test/functional/tools/validation_image.xml @@ -14,10 +14,10 @@ - - - - + + + + @@ -25,7 +25,7 @@ - + @@ -33,7 +33,7 @@ - + @@ -41,15 +41,15 @@ - - - - - - - - - + + + + + + + + + @@ -57,7 +57,7 @@ - + @@ -65,7 +65,7 @@ - + @@ -98,11 +98,11 @@ - - - + + + - + @@ -110,11 +110,11 @@ - - - + + + - + @@ -122,7 +122,7 @@ - + @@ -130,7 +130,7 @@ - + @@ -138,7 +138,7 @@ - + @@ -146,11 +146,11 @@ - - - + + + - + From 8a81593c5372d1e2b6031e6ae62a820f4ef6857c Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 13 Mar 2024 09:34:34 +0000 Subject: [PATCH 47/50] Rename `has_image_labels` to `has_image_n_labels` --- lib/galaxy/tool_util/verify/asserts/image.py | 2 +- lib/galaxy/tool_util/xsd/galaxy.xsd | 8 ++++---- test/functional/tools/validation_image.xml | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 3ebd46095f9..bac607cf3d1 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -238,7 +238,7 @@ def _get_image_labels( return im_arr, present_labels -def assert_has_image_labels( +def assert_has_image_n_labels( output_bytes: bytes, channel: Optional[Union[int, str]] = None, exclude_labels: Optional[Union[str, List[int]]] = None, diff --git a/lib/galaxy/tool_util/xsd/galaxy.xsd b/lib/galaxy/tool_util/xsd/galaxy.xsd index f9d13253511..771d584b030 100644 --- a/lib/galaxy/tool_util/xsd/galaxy.xsd +++ b/lib/galaxy/tool_util/xsd/galaxy.xsd @@ -2253,7 +2253,7 @@ module. - + @@ -2918,18 +2918,18 @@ $attribute_list::5 - + ``). +````). The primary usage of this assertion is to verify the number of objects in images with uniquely labeled objects. $attribute_list::5 ]]> - + Expected number of labels.` diff --git a/test/functional/tools/validation_image.xml b/test/functional/tools/validation_image.xml index dd79503a357..852e78191a1 100644 --- a/test/functional/tools/validation_image.xml +++ b/test/functional/tools/validation_image.xml @@ -101,7 +101,7 @@ - + @@ -113,7 +113,7 @@ - + @@ -149,7 +149,7 @@ - + From 762e22c97822190cf1eadecc3f773e28c6a0f4a4 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 13 Mar 2024 11:48:58 +0100 Subject: [PATCH 48/50] Update lib/galaxy/tool_util/verify/asserts/image.py Co-authored-by: M Bernt --- lib/galaxy/tool_util/verify/asserts/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index bac607cf3d1..7c21326ff10 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -242,7 +242,7 @@ def assert_has_image_n_labels( output_bytes: bytes, channel: Optional[Union[int, str]] = None, exclude_labels: Optional[Union[str, List[int]]] = None, - value: Optional[Union[int, str]] = None, + n: Optional[Union[int, str]] = None, delta: Union[int, str] = 0, min: Optional[Union[int, str]] = None, max: Optional[Union[int, str]] = None, From 0210711fffa83e20727d31610281636d7848470d Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 13 Mar 2024 11:49:05 +0100 Subject: [PATCH 49/50] Update lib/galaxy/tool_util/verify/asserts/image.py Co-authored-by: M Bernt --- lib/galaxy/tool_util/verify/asserts/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index 7c21326ff10..b5d14b0a418 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -254,7 +254,7 @@ def assert_has_image_n_labels( present_labels = _get_image_labels(output_bytes, channel, exclude_labels)[1] _assert_number( len(present_labels), - value, + n, delta, min, max, From 209d15fc506953dac975cdc06449484f6f59882d Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Wed, 13 Mar 2024 10:49:33 +0000 Subject: [PATCH 50/50] Fix linting issues --- lib/galaxy/tool_util/verify/asserts/image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py index b5d14b0a418..ec5fb01d52d 100644 --- a/lib/galaxy/tool_util/verify/asserts/image.py +++ b/lib/galaxy/tool_util/verify/asserts/image.py @@ -221,7 +221,7 @@ def _get_image_labels( # Apply filtering due to `labels` (keep only those). if labels is None: - labels = list() + labels = [] if isinstance(labels, str): labels = [cast_label(label) for label in labels.split(",") if len(label) > 0] if len(labels) > 0: @@ -229,7 +229,7 @@ def _get_image_labels( # Apply filtering due to `exclude_labels`. if exclude_labels is None: - exclude_labels = list() + exclude_labels = [] if isinstance(exclude_labels, str): exclude_labels = [cast_label(label) for label in exclude_labels.split(",") if len(label) > 0] present_labels = [label for label in present_labels if label not in exclude_labels]