diff --git a/lib/galaxy/tool_util/verify/assertion_models.py b/lib/galaxy/tool_util/verify/assertion_models.py
index 142c476ffdb6..4e184334e368 100644
--- a/lib/galaxy/tool_util/verify/assertion_models.py
+++ b/lib/galaxy/tool_util/verify/assertion_models.py
@@ -1452,6 +1452,12 @@ class has_size_model_nested(AssertionModel):
has_image_center_of_mass_channel_description = """Restricts the assertion to a specific channel of the image (where ``0`` corresponds to the first image channel)."""
+has_image_center_of_mass_slice_description = (
+ """Restricts the assertion to a specific slice of the image (where ``0`` corresponds to the first image slice)."""
+)
+
+has_image_center_of_mass_frame_description = """Restricts the assertion to a specific frame of the image sequence (where ``0`` corresponds to the first image frame)."""
+
has_image_center_of_mass_eps_description = (
"""The maximum allowed Euclidean distance to the required center of mass (defaults to ``0.01``)."""
)
@@ -1470,6 +1476,16 @@ class base_has_image_center_of_mass_model(AssertionModel):
description=has_image_center_of_mass_channel_description,
)
+ slice: typing.Optional[StrictInt] = Field(
+ None,
+ description=has_image_center_of_mass_slice_description,
+ )
+
+ frame: typing.Optional[StrictInt] = Field(
+ None,
+ description=has_image_center_of_mass_frame_description,
+ )
+
eps: Annotated[typing.Union[StrictInt, StrictFloat], BeforeValidator(check_non_negative_if_set)] = Field(
0.01,
description=has_image_center_of_mass_eps_description,
@@ -1548,6 +1564,116 @@ class has_image_channels_model_nested(AssertionModel):
has_image_channels: base_has_image_channels_model
+has_image_depth_depth_description = """Expected depth of the image (number of slices)."""
+
+has_image_depth_delta_description = """Maximum allowed difference of the image depth (number of slices, default is 0). The observed depth has to be in the range ``value +- delta``."""
+
+has_image_depth_min_description = """Minimum allowed depth of the image (number of slices)."""
+
+has_image_depth_max_description = """Maximum allowed depth of the image (number of slices)."""
+
+has_image_depth_negate_description = """A boolean that can be set to true to negate the outcome of the assertion."""
+
+
+class base_has_image_depth_model(AssertionModel):
+ """base model for has_image_depth describing attributes."""
+
+ depth: Annotated[typing.Optional[StrictInt], BeforeValidator(check_non_negative_if_set)] = Field(
+ None,
+ description=has_image_depth_depth_description,
+ )
+
+ delta: Annotated[StrictInt, BeforeValidator(check_non_negative_if_set)] = Field(
+ 0,
+ description=has_image_depth_delta_description,
+ )
+
+ min: Annotated[typing.Optional[StrictInt], BeforeValidator(check_non_negative_if_set)] = Field(
+ None,
+ description=has_image_depth_min_description,
+ )
+
+ max: Annotated[typing.Optional[StrictInt], BeforeValidator(check_non_negative_if_set)] = Field(
+ None,
+ description=has_image_depth_max_description,
+ )
+
+ negate: typing.Union[bool, str] = Field(
+ False,
+ description=has_image_depth_negate_description,
+ )
+
+
+class has_image_depth_model(base_has_image_depth_model):
+ r"""Asserts the output is an image and has a specific depth (number of slices).
+
+ The depth is plus/minus ``delta`` (e.g., ````).
+ Alternatively the range of the expected depth can be specified by ``min`` and/or ``max``."""
+
+ that: Literal["has_image_depth"] = "has_image_depth"
+
+
+class has_image_depth_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ has_image_depth: base_has_image_depth_model
+
+
+has_image_frames_frames_description = """Expected number of frames in the image sequence (number of time steps)."""
+
+has_image_frames_delta_description = """Maximum allowed difference of the number of frames in the image sequence (number of time steps, default is 0). The observed number of frames has to be in the range ``value +- delta``."""
+
+has_image_frames_min_description = """Minimum allowed number of frames in the image sequence (number of time steps)."""
+
+has_image_frames_max_description = """Maximum allowed number of frames in the image sequence (number of time steps)."""
+
+has_image_frames_negate_description = """A boolean that can be set to true to negate the outcome of the assertion."""
+
+
+class base_has_image_frames_model(AssertionModel):
+ """base model for has_image_frames describing attributes."""
+
+ frames: Annotated[typing.Optional[StrictInt], BeforeValidator(check_non_negative_if_set)] = Field(
+ None,
+ description=has_image_frames_frames_description,
+ )
+
+ delta: Annotated[StrictInt, BeforeValidator(check_non_negative_if_set)] = Field(
+ 0,
+ description=has_image_frames_delta_description,
+ )
+
+ min: Annotated[typing.Optional[StrictInt], BeforeValidator(check_non_negative_if_set)] = Field(
+ None,
+ description=has_image_frames_min_description,
+ )
+
+ max: Annotated[typing.Optional[StrictInt], BeforeValidator(check_non_negative_if_set)] = Field(
+ None,
+ description=has_image_frames_max_description,
+ )
+
+ negate: typing.Union[bool, str] = Field(
+ False,
+ description=has_image_frames_negate_description,
+ )
+
+
+class has_image_frames_model(base_has_image_frames_model):
+ r"""Asserts the output is an image and has a specific number of frames (number of time steps).
+
+ The number of frames is plus/minus ``delta`` (e.g., ````).
+ Alternatively the range of the expected number of frames can be specified by ``min`` and/or ``max``."""
+
+ that: Literal["has_image_frames"] = "has_image_frames"
+
+
+class has_image_frames_model_nested(AssertionModel):
+ r"""Nested version of this assertion model."""
+
+ has_image_frames: base_has_image_frames_model
+
+
has_image_height_height_description = """Expected height of the image (in pixels)."""
has_image_height_delta_description = """Maximum allowed difference of the image height (in pixels, default is 0). The observed height has to be in the range ``value +- delta``."""
@@ -1605,6 +1731,12 @@ class has_image_height_model_nested(AssertionModel):
has_image_mean_intensity_channel_description = """Restricts the assertion to a specific channel of the image (where ``0`` corresponds to the first image channel)."""
+has_image_mean_intensity_slice_description = (
+ """Restricts the assertion to a specific slice of the image (where ``0`` corresponds to the first image slice)."""
+)
+
+has_image_mean_intensity_frame_description = """Restricts the assertion to a specific frame of the image sequence (where ``0`` corresponds to the first image frame)."""
+
has_image_mean_intensity_mean_intensity_description = """The required mean value of the image intensities."""
has_image_mean_intensity_eps_description = """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``."""
@@ -1622,6 +1754,16 @@ class base_has_image_mean_intensity_model(AssertionModel):
description=has_image_mean_intensity_channel_description,
)
+ slice: typing.Optional[StrictInt] = Field(
+ None,
+ description=has_image_mean_intensity_slice_description,
+ )
+
+ frame: typing.Optional[StrictInt] = Field(
+ None,
+ description=has_image_mean_intensity_frame_description,
+ )
+
mean_intensity: typing.Optional[typing.Union[StrictInt, StrictFloat]] = Field(
None,
description=has_image_mean_intensity_mean_intensity_description,
@@ -1660,6 +1802,12 @@ class has_image_mean_intensity_model_nested(AssertionModel):
has_image_mean_object_size_channel_description = """Restricts the assertion to a specific channel of the image (where ``0`` corresponds to the first image channel)."""
+has_image_mean_object_size_slice_description = (
+ """Restricts the assertion to a specific slice of the image (where ``0`` corresponds to the first image slice)."""
+)
+
+has_image_mean_object_size_frame_description = """Restricts the assertion to a specific frame of the image sequence (where ``0`` corresponds to the first image frame)."""
+
has_image_mean_object_size_labels_description = """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``."""
has_image_mean_object_size_exclude_labels_description = """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``."""
@@ -1685,6 +1833,16 @@ class base_has_image_mean_object_size_model(AssertionModel):
description=has_image_mean_object_size_channel_description,
)
+ slice: typing.Optional[StrictInt] = Field(
+ None,
+ description=has_image_mean_object_size_slice_description,
+ )
+
+ frame: typing.Optional[StrictInt] = Field(
+ None,
+ description=has_image_mean_object_size_frame_description,
+ )
+
labels: typing.Optional[typing.List[int]] = Field(
None,
description=has_image_mean_object_size_labels_description,
@@ -1740,6 +1898,12 @@ class has_image_mean_object_size_model_nested(AssertionModel):
has_image_n_labels_channel_description = """Restricts the assertion to a specific channel of the image (where ``0`` corresponds to the first image channel)."""
+has_image_n_labels_slice_description = (
+ """Restricts the assertion to a specific slice of the image (where ``0`` corresponds to the first image slice)."""
+)
+
+has_image_n_labels_frame_description = """Restricts the assertion to a specific frame of the image sequence (where ``0`` corresponds to the first image frame)."""
+
has_image_n_labels_labels_description = """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``."""
has_image_n_labels_exclude_labels_description = """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``."""
@@ -1763,6 +1927,16 @@ class base_has_image_n_labels_model(AssertionModel):
description=has_image_n_labels_channel_description,
)
+ slice: typing.Optional[StrictInt] = Field(
+ None,
+ description=has_image_n_labels_slice_description,
+ )
+
+ frame: typing.Optional[StrictInt] = Field(
+ None,
+ description=has_image_n_labels_frame_description,
+ )
+
labels: typing.Optional[typing.List[int]] = Field(
None,
description=has_image_n_labels_labels_description,
@@ -1897,6 +2071,8 @@ class has_image_width_model_nested(AssertionModel):
has_size_model,
has_image_center_of_mass_model,
has_image_channels_model,
+ has_image_depth_model,
+ has_image_frames_model,
has_image_height_model,
has_image_mean_intensity_model,
has_image_mean_object_size_model,
@@ -1931,6 +2107,8 @@ class has_image_width_model_nested(AssertionModel):
has_size_model_nested,
has_image_center_of_mass_model_nested,
has_image_channels_model_nested,
+ has_image_depth_model_nested,
+ has_image_frames_model_nested,
has_image_height_model_nested,
has_image_mean_intensity_model_nested,
has_image_mean_object_size_model_nested,
@@ -1991,6 +2169,10 @@ class assertion_dict(AssertionModel):
has_image_channels: typing.Optional[base_has_image_channels_model] = None
+ has_image_depth: typing.Optional[base_has_image_depth_model] = None
+
+ has_image_frames: typing.Optional[base_has_image_frames_model] = None
+
has_image_height: typing.Optional[base_has_image_height_model] = None
has_image_mean_intensity: typing.Optional[base_has_image_mean_intensity_model] = None
diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py
index c56bd144bc61..d9972994d3fe 100644
--- a/lib/galaxy/tool_util/verify/asserts/image.py
+++ b/lib/galaxy/tool_util/verify/asserts/image.py
@@ -69,6 +69,24 @@
validators=["check_non_negative_if_set"],
),
]
+Depth = Annotated[
+ OptionalXmlInt,
+ AssertionParameter(
+ "Expected depth of the image (number of slices).",
+ json_type="typing.Optional[StrictInt]",
+ xml_type="xs:nonNegativeInteger",
+ validators=["check_non_negative_if_set"],
+ ),
+]
+Frames = Annotated[
+ OptionalXmlInt,
+ AssertionParameter(
+ "Expected number of frames in the image sequence (number of time steps).",
+ json_type="typing.Optional[StrictInt]",
+ xml_type="xs:nonNegativeInteger",
+ validators=["check_non_negative_if_set"],
+ ),
+]
WidthDelta = Annotated[
XmlInt,
AssertionParameter(
@@ -150,6 +168,60 @@
validators=["check_non_negative_if_set"],
),
]
+DepthDelta = Annotated[
+ XmlInt,
+ AssertionParameter(
+ "Maximum allowed difference of the image depth (number of slices, default is 0). The observed depth has to be in the range ``value +- delta``.",
+ json_type="StrictInt",
+ xml_type="xs:nonNegativeInteger",
+ validators=["check_non_negative_if_set"],
+ ),
+]
+DepthMin = Annotated[
+ OptionalXmlInt,
+ AssertionParameter(
+ "Minimum allowed depth of the image (number of slices).",
+ json_type="typing.Optional[StrictInt]",
+ xml_type="xs:nonNegativeInteger",
+ validators=["check_non_negative_if_set"],
+ ),
+]
+DepthMax = Annotated[
+ OptionalXmlInt,
+ AssertionParameter(
+ "Maximum allowed depth of the image (number of slices).",
+ json_type="typing.Optional[StrictInt]",
+ xml_type="xs:nonNegativeInteger",
+ validators=["check_non_negative_if_set"],
+ ),
+]
+FramesDelta = Annotated[
+ XmlInt,
+ AssertionParameter(
+ "Maximum allowed difference of the number of frames in the image sequence (number of time steps, default is 0). The observed number of frames has to be in the range ``value +- delta``.",
+ json_type="StrictInt",
+ xml_type="xs:nonNegativeInteger",
+ validators=["check_non_negative_if_set"],
+ ),
+]
+FramesMin = Annotated[
+ OptionalXmlInt,
+ AssertionParameter(
+ "Minimum allowed number of frames in the image sequence (number of time steps).",
+ json_type="typing.Optional[StrictInt]",
+ xml_type="xs:nonNegativeInteger",
+ validators=["check_non_negative_if_set"],
+ ),
+]
+FramesMax = Annotated[
+ OptionalXmlInt,
+ AssertionParameter(
+ "Maximum allowed number of frames in the image sequence (number of time steps).",
+ json_type="typing.Optional[StrictInt]",
+ xml_type="xs:nonNegativeInteger",
+ validators=["check_non_negative_if_set"],
+ ),
+]
MeanIntensity = Annotated[
OptionalXmlFloat,
AssertionParameter("The required mean value of the image intensities.", json_type=JSON_OPTIONAL_STRICT_NUMBER),
@@ -218,6 +290,20 @@
json_type="typing.Optional[StrictInt]",
),
]
+Slice = Annotated[
+ OptionalXmlInt,
+ AssertionParameter(
+ "Restricts the assertion to a specific slice of the image (where ``0`` corresponds to the first image slice).",
+ json_type="typing.Optional[StrictInt]",
+ ),
+]
+Frame = Annotated[
+ OptionalXmlInt,
+ AssertionParameter(
+ "Restricts the assertion to a specific frame of the image sequence (where ``0`` corresponds to the first image frame).",
+ json_type="typing.Optional[StrictInt]",
+ ),
+]
CenterOfMass = Annotated[
str,
AssertionParameter(
@@ -319,14 +405,15 @@ def assert_has_image_width(
max: WidthMax = None,
negate: Negate = NEGATE_DEFAULT,
) -> None:
- """Asserts the output is an image and has a specific width (in pixels).
+ """
+ Asserts the output is an image and has a specific width (in pixels).
The width is plus/minus ``delta`` (e.g., ````).
Alternatively the range of the expected width can be specified by ``min`` and/or ``max``.
"""
im_arr = _get_image(output_bytes)
_assert_number(
- im_arr.shape[1],
+ im_arr.shape[3], # Image axes are normalized like "TZYXC"
width,
delta,
min,
@@ -345,14 +432,15 @@ def assert_has_image_height(
max: HeightMax = None,
negate: Negate = NEGATE_DEFAULT,
) -> None:
- """Asserts the output is an image and has a specific height (in pixels).
+ """
+ Asserts the output is an image and has a specific height (in pixels).
The height is plus/minus ``delta`` (e.g., ````).
Alternatively the range of the expected height can be specified by ``min`` and/or ``max``.
"""
im_arr = _get_image(output_bytes)
_assert_number(
- im_arr.shape[0],
+ im_arr.shape[2], # Image axes are normalized like "TZYXC"
height,
delta,
min,
@@ -371,16 +459,16 @@ def assert_has_image_channels(
max: ChannelsMax = None,
negate: Negate = NEGATE_DEFAULT,
) -> None:
- """Asserts the output is an image and has a specific number of channels.
+ """
+ Asserts the output is an image and has a specific number of channels.
The number of channels is plus/minus ``delta`` (e.g., ````).
Alternatively the range of the expected number of channels can be specified by ``min`` and/or ``max``.
"""
im_arr = _get_image(output_bytes)
- n_channels = 1 if im_arr.ndim < 3 else im_arr.shape[2] # we assume here that the image is a 2-D image
_assert_number(
- n_channels,
+ im_arr.shape[-1], # Image axes are normalized like "TZYXC"
channels,
delta,
min,
@@ -391,31 +479,168 @@ def assert_has_image_channels(
)
+def assert_has_image_depth(
+ output_bytes: OutputBytes,
+ depth: Depth = None,
+ delta: DepthDelta = 0,
+ min: DepthMin = None,
+ max: DepthMax = None,
+ negate: Negate = NEGATE_DEFAULT,
+) -> None:
+ """
+ Asserts the output is an image and has a specific depth (number of slices).
+
+ The depth is plus/minus ``delta`` (e.g., ````).
+ Alternatively the range of the expected depth can be specified by ``min`` and/or ``max``.
+ """
+ im_arr = _get_image(output_bytes)
+ _assert_number(
+ im_arr.shape[1], # Image axes are normalized like "TZYXC"
+ depth,
+ delta,
+ min,
+ max,
+ negate,
+ "{expected} depth {n}+-{delta}",
+ "{expected} depth to be in [{min}:{max}]",
+ )
+
+
+def assert_has_image_frames(
+ output_bytes: OutputBytes,
+ frames: Frames = None,
+ delta: FramesDelta = 0,
+ min: FramesMin = None,
+ max: FramesMax = None,
+ negate: Negate = NEGATE_DEFAULT,
+) -> None:
+ """
+ Asserts the output is an image and has a specific number of frames (number of time steps).
+
+ The number of frames is plus/minus ``delta`` (e.g., ````).
+ Alternatively the range of the expected number of frames can be specified by ``min`` and/or ``max``.
+ """
+ im_arr = _get_image(output_bytes)
+ _assert_number(
+ im_arr.shape[0], # Image axes are normalized like "TZYXC"
+ frames,
+ delta,
+ min,
+ max,
+ negate,
+ "{expected} frames {n}+-{delta}",
+ "{expected} frames to be in [{min}:{max}]",
+ )
+
+
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:
+ im_arr_yx = im_arr.sum(axis=(0, 1, 4)) # Image axes are normalized like "TZYXC"
+ im_arr_yx = numpy.abs(im_arr_yx)
+ if im_arr_yx.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()
+ im_arr_yx = im_arr_yx / im_arr_yx.sum()
+ yy, xx = numpy.indices(im_arr_yx.shape)
+ return (im_arr_yx * xx).sum(), (im_arr_yx * yy).sum()
+
+
+def _move_char(s: str, pos_src: int, pos_dst: int) -> str:
+ s_list = list(s)
+ c = s_list.pop(pos_src)
+ if pos_dst < 0:
+ pos_dst = len(s_list) + pos_dst + 1
+ s_list.insert(pos_dst, c)
+ return "".join(s_list)
+
+
+def _swap_char(s: str, pos1: int, pos2: int) -> str:
+ s_list = list(s)
+ s_list[pos1], s_list[pos2] = s_list[pos2], s_list[pos1]
+ return "".join(s_list)
def _get_image(
output_bytes: bytes,
channel: Optional[Union[int, str]] = None,
+ slice: Optional[Union[int, str]] = None,
+ frame: Optional[Union[int, str]] = None,
) -> "numpy.typing.NDArray":
"""
- Returns the output image or a specific channel.
+ Returns the output image with the axes ``TZYXC``, optionally restricted to a specific `channel`, `slice`,
+ `frame`, or a combination thereof.
- The function tries to read the image using tifffile and Pillow.
+ The function tries to read the image using tifffile and Pillow. The image axes are normalized like ``TZYXC``,
+ treating sample axis ``S`` as an alias for the channel axis ``C``. For images which cannot be read by tifffile,
+ two- and three-dimensional data is supported. Two-dimensional images are assumed to be in ``YX`` axes order,
+ and three-dimensional images are assumed to be in ``YXC`` axes order.
"""
buf = io.BytesIO(output_bytes)
# Try reading with tifffile first. It fails if the file is not a TIFF.
try:
- im_arr = tifffile.imread(buf)
+ with tifffile.TiffFile(buf) as im_file:
+ assert len(im_file.series) == 1, f"Image has unsupported number of series: {len(im_file.series)}"
+ im_axes = im_file.series[0].axes
+
+ # Verify that the image format is supported
+ assert (
+ frozenset("YX") <= frozenset(im_axes) <= frozenset("TZYXCS")
+ ), f"Image has unsupported axes: {im_axes}"
+
+ # Treat sample axis "S" as channel axis "C" and fail if both are present
+ assert (
+ "C" not in im_axes or "S" not in im_axes
+ ), f"Image has sample and channel axes which is not supported: {im_axes}"
+ im_axes = im_axes.replace("S", "C")
+
+ # Read the image data
+ im_arr = im_file.asarray()
+
+ # Step 1. In the three steps below, the optional axes are added, of if they arent't there yet:
+
+ # (1.1) Append "C" axis if not present yet
+ if im_axes.find("C") == -1:
+ im_arr = im_arr[..., None]
+ im_axes += "C"
+
+ # (1.2) Append "Z" axis if not present yet
+ if im_axes.find("Z") == -1:
+ im_arr = im_arr[..., None]
+ im_axes += "Z"
+
+ # (1.3) Append "T" axis if not present yet
+ if im_axes.find("T") == -1:
+ im_arr = im_arr[..., None]
+ im_axes += "T"
+
+ # Step 2. All supported axes are there now. Normalize the order of the axes:
+
+ # (2.1) Normalize order of axes "Y" and "X"
+ ypos = im_axes.find("Y")
+ xpos = im_axes.find("X")
+ if ypos > xpos:
+ im_arr = im_arr.swapaxes(ypos, xpos)
+ im_axes = _swap_char(im_axes, xpos, ypos)
+
+ # (2.2) Normalize the position of the "C" axis (should be last)
+ cpos = im_axes.find("C")
+ if cpos < len(im_axes) - 1:
+ im_arr = numpy.moveaxis(im_arr, cpos, -1)
+ im_axes = _move_char(im_axes, cpos, -1)
+
+ # (2.3) Normalize the position of the "T" axis (should be first)
+ tpos = im_axes.find("T")
+ if tpos != 0:
+ im_arr = numpy.moveaxis(im_arr, tpos, 0)
+ im_axes = _move_char(im_axes, tpos, 0)
+
+ # (2.4) Normalize the position of the "Z" axis (should be second)
+ zpos = im_axes.find("Z")
+ if zpos != 1:
+ im_arr = numpy.moveaxis(im_arr, zpos, 1)
+ im_axes = _move_char(im_axes, zpos, 1)
+
+ # Verify that the normalizations were successful
+ assert im_axes == "TZYXC", f"Image axis normalization failed: {im_axes}"
# If tifffile failed, then the file is not a tifffile. In that case, try with Pillow.
except tifffile.TiffFileError:
@@ -423,9 +648,28 @@ def _get_image(
with Image.open(buf) as im:
im_arr = numpy.array(im)
+ # Verify that the image format is supported
+ assert im_arr.ndim in (2, 3), f"Image has unsupported dimension: {im_arr.ndim}"
+
+ # Normalize the axes
+ if im_arr.ndim == 2: # Append "C" axis if not present yet
+ im_arr = im_arr[..., None]
+ im_arr = im_arr[None, None, ...] # Prepend "T" and "Z" axes
+
+ # Verify that the normalizations were successful
+ assert im_arr.ndim == 5, "Image axis normalization failed"
+
# Select the specified channel (if any).
if channel is not None:
- im_arr = im_arr[:, :, int(channel)]
+ im_arr = im_arr[..., [int(channel)]]
+
+ # Select the specified slice (if any).
+ if slice is not None:
+ im_arr = im_arr[:, [int(slice)], ...]
+
+ # Select the specified frame (if any).
+ if frame is not None:
+ im_arr = im_arr[[int(frame)], ...]
# Return the image
return im_arr
@@ -434,17 +678,20 @@ def _get_image(
def assert_has_image_mean_intensity(
output_bytes: OutputBytes,
channel: Channel = None,
+ slice: Slice = None,
+ frame: Frame = None,
mean_intensity: MeanIntensity = None,
eps: MeanIntensityEps = 0.01,
min: MeanIntensityMin = None,
max: MeanIntensityMax = None,
) -> None:
- """Asserts the output is an image and has a specific mean intensity value.
+ """
+ Asserts the output is an image and has a specific mean intensity value.
The mean intensity value is plus/minus ``eps`` (e.g., ````).
Alternatively the range of the expected mean intensity value can be specified by ``min`` and/or ``max``.
"""
- im_arr = _get_image(output_bytes, channel)
+ im_arr = _get_image(output_bytes, channel, slice, frame)
_assert_float(
actual=im_arr.mean(),
label="mean intensity",
@@ -459,15 +706,18 @@ def assert_has_image_center_of_mass(
output_bytes: OutputBytes,
center_of_mass: CenterOfMass,
channel: Channel = None,
+ slice: Slice = None,
+ frame: Frame = None,
eps: CenterOfMassEps = 0.01,
) -> None:
- """Asserts the specified output is an image and has the specified center of mass.
+ """
+ Asserts the specified output is an image and has the specified center of mass.
Asserts the output is an image and has a specific center of mass,
or has an Euclidean distance of ``eps`` or less to that point (e.g.,
````).
"""
- im_arr = _get_image(output_bytes, channel)
+ im_arr = _get_image(output_bytes, channel, slice, frame)
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(center_of_mass_parts[0]), float(center_of_mass_parts[1]))
@@ -482,6 +732,8 @@ def assert_has_image_center_of_mass(
def _get_image_labels(
output_bytes: bytes,
channel: Optional[Union[int, str]] = None,
+ slice: Optional[Union[int, str]] = None,
+ frame: 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]]:
@@ -489,7 +741,7 @@ def _get_image_labels(
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)
+ im_arr = _get_image(output_bytes, channel, slice, frame)
def cast_label(label):
label = label.strip()
@@ -524,6 +776,8 @@ def cast_label(label):
def assert_has_image_n_labels(
output_bytes: OutputBytes,
channel: Channel = None,
+ slice: Slice = None,
+ frame: Frame = None,
labels: Labels = None,
exclude_labels: ExcludeLabels = None,
n: NumLabels = None,
@@ -532,14 +786,15 @@ def assert_has_image_n_labels(
max: NumLabelsMax = None,
negate: Negate = NEGATE_DEFAULT,
) -> None:
- """Asserts the output is an image and has the specified labels.
+ """
+ Asserts the output is an image and has the specified labels.
Labels can be a number of labels or unique values (e.g.,
````).
The primary usage of this assertion is to verify the number of objects in images with uniquely labeled objects.
"""
- present_labels = _get_image_labels(output_bytes, channel, labels, exclude_labels)[1]
+ present_labels = _get_image_labels(output_bytes, channel, slice, frame, labels, exclude_labels)[1]
_assert_number(
len(present_labels),
n,
@@ -555,6 +810,8 @@ def assert_has_image_n_labels(
def assert_has_image_mean_object_size(
output_bytes: OutputBytes,
channel: Channel = None,
+ slice: Slice = None,
+ frame: Frame = None,
labels: Labels = None,
exclude_labels: ExcludeLabels = None,
mean_object_size: MeanObjectSize = None,
@@ -562,14 +819,30 @@ def assert_has_image_mean_object_size(
min: MeanObjectSizeMin = None,
max: MeanObjectSizeMax = None,
) -> None:
- """Asserts the output is an image with labeled objects which have the specified mean size (number of pixels),
+ """
+ Asserts the output is an image with labeled objects which have the specified mean size (number of pixels),
The mean size is plus/minus ``eps`` (e.g., ````).
The labels must be unique.
"""
- 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)
+ im_arr, present_labels = _get_image_labels(output_bytes, channel, slice, frame, labels, exclude_labels)
+ assert (
+ im_arr.shape[-1] == 1
+ ), f"has_image_mean_object_size is undefined for multi-channel images (channels: {im_arr.shape[-1]})"
+
+ # Build list of all object sizes over all time-frames
+ object_sizes = sum(
+ [
+ # Iterate over all XYZC time-frames (axis C is singleton)
+ [(im_arr_t == label).sum() for label in present_labels]
+ for im_arr_t in im_arr
+ ],
+ [],
+ )
+
+ # Compute the mean object size and verify
+ actual_mean_object_size = numpy.mean([object_size for object_size in object_sizes if object_size > 0])
_assert_float(
actual=actual_mean_object_size,
label="mean object size",
diff --git a/lib/galaxy/tool_util/xsd/galaxy.xsd b/lib/galaxy/tool_util/xsd/galaxy.xsd
index 3f077b79abdb..c8090c919568 100644
--- a/lib/galaxy/tool_util/xsd/galaxy.xsd
+++ b/lib/galaxy/tool_util/xsd/galaxy.xsd
@@ -3051,6 +3051,16 @@ $attribute_list::5]]>
+
+
+
+
+
+
+
+
+
+
@@ -3097,6 +3107,82 @@ $attribute_list::5]]>
+
+
+
+ ``).
+Alternatively the range of the expected depth can be specified by ``min`` and/or ``max``.
+
+$attribute_list::5]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ``).
+Alternatively the range of the expected number of frames can be specified by ``min`` and/or ``max``.
+
+$attribute_list::5]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -3151,6 +3237,16 @@ $attribute_list::5]]>
+
+
+
+
+
+
+
+
+
+
@@ -3190,6 +3286,16 @@ $attribute_list::5]]>
+
+
+
+
+
+
+
+
+
+
@@ -3240,6 +3346,16 @@ $attribute_list::5]]>
+
+
+
+
+
+
+
+
+
+
diff --git a/test-data/im5_uint8.tif b/test-data/im5_uint8.tif
new file mode 100644
index 000000000000..5dca8cad973d
Binary files /dev/null and b/test-data/im5_uint8.tif differ
diff --git a/test-data/im6_uint8.tif b/test-data/im6_uint8.tif
new file mode 100644
index 000000000000..5221d413ea3f
Binary files /dev/null and b/test-data/im6_uint8.tif differ
diff --git a/test-data/im7_uint8.tif b/test-data/im7_uint8.tif
new file mode 100644
index 000000000000..c6f3d7ba20f9
Binary files /dev/null and b/test-data/im7_uint8.tif differ
diff --git a/test-data/im8_uint16.tif b/test-data/im8_uint16.tif
new file mode 100644
index 000000000000..be4c15380cea
Binary files /dev/null and b/test-data/im8_uint16.tif differ
diff --git a/test/functional/tools/validation_image.xml b/test/functional/tools/validation_image.xml
index a3c93ea84f67..c94c3581c3f1 100644
--- a/test/functional/tools/validation_image.xml
+++ b/test/functional/tools/validation_image.xml
@@ -11,7 +11,7 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+