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 support for floating point TIFF files in verification of image-based tool outputs #17797

Merged
merged 3 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 23 additions & 4 deletions lib/galaxy/tool_util/verify/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
from PIL import Image
except ImportError:
pass
try:
import tifffile
except ImportError:
pass

from galaxy.tool_util.parser.util import (
DEFAULT_DELTA,
Expand Down Expand Up @@ -496,14 +500,29 @@ def get_image_metric(
raise ValueError(f'No such metric: "{metric_name}"')


def _load_image(filepath: str) -> "numpy.typing.NDArray":
"""
Reads the given image, trying tifffile and Pillow for reading.
"""
# Try reading with tifffile first. It fails if the file is not a TIFF.
try:
arr = tifffile.imread(filepath)

# If tifffile failed, then the file is not a tifffile. In that case, try with Pillow.
except tifffile.TiffFileError:
with Image.open(filepath) as im:
arr = numpy.array(im)

# Return loaded image
return arr


def files_image_diff(file1: str, file2: str, attributes: Optional[Dict[str, Any]] = None) -> None:
"""Check the pixel data of 2 image files for differences."""
attributes = attributes or {}

with Image.open(file1) as im1:
arr1 = numpy.array(im1)
with Image.open(file2) as im2:
arr2 = numpy.array(im2)
arr1 = _load_image(file1)
arr2 = _load_image(file2)

if arr1.dtype != arr2.dtype:
raise AssertionError(f"Image data types did not match ({arr1.dtype}, {arr2.dtype}).")
Expand Down
88 changes: 50 additions & 38 deletions lib/galaxy/tool_util/verify/asserts/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
from PIL import Image
except ImportError:
pass
try:
import tifffile
except ImportError:
pass

if TYPE_CHECKING:
import numpy.typing
Expand Down Expand Up @@ -58,18 +62,17 @@ def assert_has_image_width(
"""
Asserts the specified output is an image and has a width of the specified value.
"""
buf = io.BytesIO(output_bytes)
with Image.open(buf) as im:
_assert_number(
im.size[0],
width,
delta,
min,
max,
negate,
"{expected} width {n}+-{delta}",
"{expected} width to be in [{min}:{max}]",
)
im_arr = _get_image(output_bytes)
_assert_number(
im_arr.shape[1],
width,
delta,
min,
max,
negate,
"{expected} width {n}+-{delta}",
"{expected} width to be in [{min}:{max}]",
)


def assert_has_image_height(
Expand All @@ -83,18 +86,17 @@ def assert_has_image_height(
"""
Asserts the specified output is an image and has a height of the specified value.
"""
buf = io.BytesIO(output_bytes)
with Image.open(buf) as im:
_assert_number(
im.size[1],
height,
delta,
min,
max,
negate,
"{expected} height {n}+-{delta}",
"{expected} height to be in [{min}:{max}]",
)
im_arr = _get_image(output_bytes)
_assert_number(
im_arr.shape[0],
height,
delta,
min,
max,
negate,
"{expected} height {n}+-{delta}",
"{expected} height to be in [{min}:{max}]",
)


def assert_has_image_channels(
Expand All @@ -108,18 +110,18 @@ def assert_has_image_channels(
"""
Asserts the specified output is an image and has the specified number of channels.
"""
buf = io.BytesIO(output_bytes)
with Image.open(buf) as im:
_assert_number(
len(im.getbands()),
channels,
delta,
min,
max,
negate,
"{expected} image channels {n}+-{delta}",
"{expected} image channels to be in [{min}:{max}]",
)
im_arr = _get_image(output_bytes)
n_channels = 1 if im_arr.ndim < 3 else im_arr.shape[2] # we assume here that the image is a 2-D image
_assert_number(
n_channels,
channels,
delta,
min,
max,
negate,
"{expected} image channels {n}+-{delta}",
"{expected} image channels to be in [{min}:{max}]",
)


def _compute_center_of_mass(im_arr: "numpy.typing.NDArray") -> Tuple[float, float]:
Expand All @@ -139,10 +141,20 @@ def _get_image(
) -> "numpy.typing.NDArray":
"""
Returns the output image or a specific channel.

The function tries to read the image using tifffile and Pillow.
"""
buf = io.BytesIO(output_bytes)
with Image.open(buf) as im:
im_arr = numpy.array(im)

# Try reading with tifffile first. It fails if the file is not a TIFF.
try:
im_arr = tifffile.imread(buf)

# If tifffile failed, then the file is not a tifffile. In that case, try with Pillow.
except tifffile.TiffFileError:
buf.seek(0)
with Image.open(buf) as im:
im_arr = numpy.array(im)

# Select the specified channel (if any).
if channel is not None:
Expand Down
1 change: 1 addition & 0 deletions packages/tool_util/setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ extended-assertions =
numpy
pysam
pillow
tifffile

[options.packages.find]
exclude =
Expand Down
Binary file added test-data/im4_float.tif
Binary file not shown.
5 changes: 5 additions & 0 deletions test/functional/tools/image_diff.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
<param name="in" value="im1_uint8.png" />
<output name="out" value="im1_uint8.png" compare="image_diff" metric="rms" eps="0" />
</test>
<!-- test pair of equal images (float tiff) -->
<test>
<param name="in" value="im4_float.tif" />
<output name="out" value="im4_float.tif" compare="image_diff" />
</test>
<!-- test pair of different images -->
<test>
<param name="in" value="im2_a.png" />
Expand Down
12 changes: 12 additions & 0 deletions test/functional/tools/validation_image.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@
</assert_contents>
</output>
</test>
<!-- Tests with float TIFF images -->
<test>
<param name="input" value="im4_float.tif" />
<output name="output">
<assert_contents>
<has_image_width width="25" />
<has_image_height height="25" />
<has_image_channels channels="1" />
<has_image_center_of_mass center_of_mass="11.75, 11.75" />
</assert_contents>
</output>
</test>
<!-- Tests with label images -->
<test>
<param name="input" value="im2_b.png" />
Expand Down
Loading