Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add content assertion XML tags for test output verification using images #17581

Merged
merged 50 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
9a15904
Add `has_image_metadata` test assertion
kostrykin Feb 28, 2024
5e4cab2
Fix test
kostrykin Feb 28, 2024
aad820f
Fix syntax error
kostrykin Feb 28, 2024
0b04d84
Add missing import
kostrykin Feb 28, 2024
f85d6bf
Fix test
kostrykin Feb 28, 2024
c9ba5d2
Fix bug
kostrykin Feb 28, 2024
900881b
Fix bug
kostrykin Feb 28, 2024
982e128
Add more metadata assertions
kostrykin Feb 28, 2024
7af2793
Add tests for label images
kostrykin Feb 28, 2024
d182c5b
Fix XML
kostrykin Feb 28, 2024
fa81d00
Fix XML
kostrykin Feb 28, 2024
ca9e936
Fix bug
kostrykin Feb 28, 2024
e4b1ecb
Fix bug
kostrykin Feb 28, 2024
9e361d4
Fix bug
kostrykin Feb 28, 2024
a1ddff0
Fix tests
kostrykin Feb 28, 2024
c4ea727
Add `image_has_intensities` and tests
kostrykin Feb 28, 2024
e8bb59e
Fix bug
kostrykin Feb 28, 2024
a392d9e
Fix tests
kostrykin Feb 28, 2024
6cfe399
Fix tests
kostrykin Feb 28, 2024
c61e306
Add debug info
kostrykin Feb 28, 2024
8c56754
Fix bug
kostrykin Feb 28, 2024
04ade00
Fix tests
kostrykin Feb 28, 2024
37c1cba
Add test
kostrykin Feb 28, 2024
3ed81ec
Add XSD for `image_has_metadata`
kostrykin Feb 29, 2024
d8e3e3e
Fix bug in test
kostrykin Feb 29, 2024
88d67e3
Update XSD
kostrykin Feb 29, 2024
8036ae8
Update XSD
kostrykin Feb 29, 2024
6855df5
Fix assertion errors
kostrykin Mar 2, 2024
42e1b07
Fix linting issues
kostrykin Mar 2, 2024
4d52d07
Fix linting issues
kostrykin Mar 2, 2024
9094a7f
Test
kostrykin Mar 2, 2024
af948be
Test
kostrykin Mar 2, 2024
f1f7d48
Fix linting issues
kostrykin Mar 2, 2024
94d43f1
Fix linting issues
kostrykin Mar 2, 2024
7d0ef9f
Add missing type checks
kostrykin Mar 2, 2024
7bcc0ea
Fix linting issues
kostrykin Mar 2, 2024
f08d68f
Fix linting issues
kostrykin Mar 2, 2024
f7845ce
Fix linting issues
kostrykin Mar 2, 2024
84c4cbf
Fix linting issues
kostrykin Mar 2, 2024
ef9302c
Fix linting issues
kostrykin Mar 2, 2024
10b05d5
Fix linting issues
kostrykin Mar 2, 2024
5ce6045
Add `*_min` and `*_max` for `mean_intensity` and `mean_object_size`, …
kostrykin Mar 2, 2024
7aebd26
Add `_assert_float` function and refactor
kostrykin Mar 2, 2024
00485cc
Squashed commit of the following:
kostrykin Mar 5, 2024
fb7bbaa
Rename `delta` to `eps` for float-based assertions (#1)
kostrykin Mar 11, 2024
5964f72
Rename assertion attribute `value`
kostrykin Mar 13, 2024
8a81593
Rename `has_image_labels` to `has_image_n_labels`
kostrykin Mar 13, 2024
762e22c
Update lib/galaxy/tool_util/verify/asserts/image.py
kostrykin Mar 13, 2024
0210711
Update lib/galaxy/tool_util/verify/asserts/image.py
kostrykin Mar 13, 2024
209d15f
Fix linting issues
kostrykin Mar 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/galaxy/tool_util/verify/asserts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

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
Expand Down
177 changes: 177 additions & 0 deletions lib/galaxy/tool_util/verify/asserts/image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import io
from typing import (
List,
Optional,
Tuple,
TYPE_CHECKING,
Union,
)

try:
import numpy
except ImportError:
pass
try:
from PIL import Image
except ImportError:
pass

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,
height: Optional[Union[int, str]] = None,
channels: Optional[Union[int, str]] = None,
) -> None:
"""
Assert the 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)})"

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)})"


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)
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,
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:
"""
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[:, :, int(channel)]

# 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:
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(
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,
) -> 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 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]

# 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})"

# Perform `mean_object_size` assertion.
actual_mean_object_size = sum((im_arr == label).sum() for label in labels) / len(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,
)
129 changes: 129 additions & 0 deletions lib/galaxy/tool_util/xsd/galaxy.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -2178,6 +2178,7 @@ module.
<xs:group ref="TestAssertionsXml" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="TestAssertionsJson" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="TestAssertionsH5" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="TestAssertionsImage" minOccurs="0" maxOccurs="unbounded"/>
</xs:choice>
</xs:complexType>
<xs:group name="TestAssertionsGeneral">
Expand Down Expand Up @@ -2242,6 +2243,16 @@ module.
<xs:element name="has_json_property_with_text" type="AssertHasJsonPropertyWithText"/>
</xs:choice>
</xs:group>
<xs:group name="TestAssertionsImage">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[Assertions for image data]]></xs:documentation>
</xs:annotation>
<xs:choice>
<xs:element name="image_has_metadata" type="AssertImageHasMetadata" />
<xs:element name="image_has_intensities" type="AssertImageHasIntensities" />
<xs:element name="image_has_labels" type="AssertImageHasLabels" />
</xs:choice>
</xs:group>
<xs:group name="TestAssertionsH5">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[Assertions for H5 data]]></xs:documentation>
Expand Down Expand Up @@ -2490,6 +2501,7 @@ $attribute_list::5
<xs:group ref="TestAssertionsJson" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="TestAssertionsXml" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="TestAssertionsH5" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="TestAssertionsImage" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="path" type="xs:string">
<xs:annotation>
Expand Down Expand Up @@ -2739,6 +2751,123 @@ $attribute_list::5
<xs:attributeGroup ref="AssertAttributePath"/>
<xs:attributeGroup ref="AssertAttributeNegate"/>
</xs:complexType>
<xs:complexType name="AssertImageHasMetadata">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts the image has the specified metadata.

```xml
<image_has_metadata width="512" height="512" channels="3" />
bernt-matthias marked this conversation as resolved.
Show resolved Hide resolved
```

$attribute_list::5
]]>
</xs:documentation>
</xs:annotation>
<xs:attribute name="width" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">The required width of the image.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="height" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">The required height of the image.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="channels" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">The required number of channels of the image (e.g., 1 for grayscale, 3 for RGB, 4 for RGBA images).</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="AssertImageHasIntensities">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts the image intensities comply with the specified constraints.

```xml
<image_has_intensities mean_intensity="0.83" center_of_mass="511.07, 223.34" eps="0.01" />
```

$attribute_list::5
]]>
</xs:documentation>
</xs:annotation>
<xs:attribute name="mean_intensity" type="xs:float" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">The required mean value of the image intensities.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="mean_intensity_min" type="xs:float" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">A lower bound of the required mean value of the image intensities.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="mean_intensity_max" type="xs:float" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">An upper bound of the required mean value of the image intensities.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="center_of_mass" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">The required center of mass of the image intensities (horizontal and vertical coordinate, separated by a comma).</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="eps" type="xs:float" use="optional" default="0.01">
<xs:annotation>
<xs:documentation xml:lang="en">The absolute tolerance to be used for the ``mean_intensity`` and ``center_of_mass`` assertions (defaults to ``0.01``).</xs:documentation>
bernt-matthias marked this conversation as resolved.
Show resolved Hide resolved
</xs:annotation>
</xs:attribute>
<xs:attribute name="channel" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">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).</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="AssertImageHasLabels">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Asserts the labels in a label image comply with the specified constraints.

```xml
<number_of_objects number_of_objects="187" mean_object_size="111.87" eps="0.01" exclude_labels="0" />
bernt-matthias marked this conversation as resolved.
Show resolved Hide resolved
```

$attribute_list::5
]]>
</xs:documentation>
</xs:annotation>
<xs:attribute name="number_of_objects" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">The required number of unique labels in the image. It is assumed that each individual object corresponds to a unique label.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="mean_object_size" type="xs:float" use="optional">
bernt-matthias marked this conversation as resolved.
Show resolved Hide resolved
<xs:annotation>
<xs:documentation xml:lang="en">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.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="mean_object_size_min" type="xs:float" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">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.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="mean_object_size_max" type="xs:float" use="optional">
<xs:annotation>
<xs:documentation xml:lang="en">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.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="eps" type="xs:float" use="optional" default="0.01">
<xs:annotation>
<xs:documentation xml:lang="en">The absolute tolerance to be used for the ``mean_object_size`` assertion (defaults to 0.01).</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="exclude_labels" type="xs:string" use="optional">
bernt-matthias marked this conversation as resolved.
Show resolved Hide resolved
<xs:annotation>
<xs:documentation xml:lang="en">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.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="AssertHasJsonPropertyWithValue">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Expand Down
1 change: 1 addition & 0 deletions test/functional/tools/sample_tool_conf.xml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
<tool file="sam_to_unsorted_bam.xml" />
<tool file="top_level_data.xml" />
<tool file="validation_hdf5.xml" />
<tool file="validation_image.xml"/>
<tool file="validation_zip.xml" />
<tool file="validation_tar.xml" />
<tool file="validation_tar_gz.xml" />
Expand Down
Loading
Loading