Skip to content

Commit a9b9293

Browse files
authored
Exported images fail to render in qupath (#137)
* Custom removal of macro/label in OME metadata * Unittest the remove_ome_image_metadata function * PR miscs
1 parent 8fc45bb commit a9b9293

File tree

5 files changed

+187
-2
lines changed

5 files changed

+187
-2
lines changed

tests/__init__.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from tiledb.bioimg import ATTR_NAME
99
from tiledb.cc import WebpInputFormat
1010
from tiledb.bioimg.helpers import merge_ned_ranges
11+
import xml.etree.ElementTree as ET
1112

1213
DATA_DIR = Path(__file__).parent / "data"
1314

@@ -112,3 +113,96 @@ def generate_test_case(num_axes, num_ranges, max_value):
112113
expected_output = merge_ned_ranges(input_ranges)
113114

114115
return input_ranges, expected_output
116+
117+
118+
def generate_xml(has_macro=True, has_label=True, root_tag="OME", num_images=1):
119+
"""Generate synthetic XML strings with options to include 'macro' and 'label' images."""
120+
121+
# Create the root element
122+
ome = ET.Element(
123+
root_tag,
124+
{
125+
"xmlns": "http://www.openmicroscopy.org/Schemas/OME/2016-06",
126+
"xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
127+
"Creator": "tifffile.py 2023.7.4",
128+
"UUID": "urn:uuid:40348664-c1f8-11ee-a19b-58112295faaf",
129+
"xsi:schemaLocation": "http://www.openmicroscopy.org/Schemas/OME/2016-06 http://www.openmicroscopy.org/Schemas/OME/2016-06/ome.xsd",
130+
},
131+
)
132+
133+
# Create an instrument element
134+
instrument = ET.SubElement(ome, "Instrument", ID="Instrument:95")
135+
objective = ET.SubElement(
136+
instrument, "Objective", ID="Objective:95", NominalMagnification="40.0"
137+
)
138+
139+
# Create standard image elements
140+
for i in range(num_images):
141+
image = ET.SubElement(ome, "Image", ID=f"Image:{i}", Name=f"Image{i}")
142+
pixels = ET.SubElement(
143+
image,
144+
"Pixels",
145+
DimensionOrder="XYCZT",
146+
ID=f"Pixels:{i}",
147+
SizeC="3",
148+
SizeT="1",
149+
SizeX="86272",
150+
SizeY="159488",
151+
SizeZ="1",
152+
Type="uint8",
153+
Interleaved="true",
154+
PhysicalSizeX="0.2827",
155+
PhysicalSizeY="0.2827",
156+
)
157+
channel = ET.SubElement(
158+
pixels, "Channel", ID=f"Channel:{i}:0", SamplesPerPixel="3"
159+
)
160+
tiffdata = ET.SubElement(pixels, "TiffData", PlaneCount="1")
161+
162+
# Conditionally add 'macro' and 'label' images
163+
if has_label:
164+
label_image = ET.SubElement(ome, "Image", ID="Image:label", Name="label")
165+
pixels = ET.SubElement(
166+
label_image,
167+
"Pixels",
168+
DimensionOrder="XYCZT",
169+
ID="Pixels:label",
170+
SizeC="3",
171+
SizeT="1",
172+
SizeX="604",
173+
SizeY="594",
174+
SizeZ="1",
175+
Type="uint8",
176+
Interleaved="true",
177+
PhysicalSizeX="43.0",
178+
PhysicalSizeY="43.0",
179+
)
180+
channel = ET.SubElement(
181+
pixels, "Channel", ID="Channel:label:0", SamplesPerPixel="3"
182+
)
183+
tiffdata = ET.SubElement(pixels, "TiffData", IFD="1", PlaneCount="1")
184+
185+
if has_macro:
186+
macro_image = ET.SubElement(ome, "Image", ID="Image:macro", Name="macro")
187+
pixels = ET.SubElement(
188+
macro_image,
189+
"Pixels",
190+
DimensionOrder="XYCZT",
191+
ID="Pixels:macro",
192+
SizeC="3",
193+
SizeT="1",
194+
SizeX="604",
195+
SizeY="1248",
196+
SizeZ="1",
197+
Type="uint8",
198+
Interleaved="true",
199+
PhysicalSizeX="43.0",
200+
PhysicalSizeY="43.0",
201+
)
202+
channel = ET.SubElement(
203+
pixels, "Channel", ID="Channel:macro:0", SamplesPerPixel="3"
204+
)
205+
tiffdata = ET.SubElement(pixels, "TiffData", IFD="2", PlaneCount="1")
206+
207+
# Convert the ElementTree to a string
208+
return ET.tostring(ome, encoding="unicode")

tests/unit/test_helpers.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import xml.etree.ElementTree as ET
2+
13
import numpy as np
24
import pytest
35

@@ -8,9 +10,10 @@
810
get_rgba,
911
iter_color,
1012
merge_ned_ranges,
13+
remove_ome_image_metadata,
1114
)
1215

13-
from .. import generate_test_case
16+
from .. import generate_test_case, generate_xml
1417

1518

1619
def test_color_iterator():
@@ -56,3 +59,35 @@ def test_get_pixel_depth():
5659
def test_validate_ingestion(num_axes, num_ranges, max_value):
5760
input_ranges, expected_output = generate_test_case(num_axes, num_ranges, max_value)
5861
assert merge_ned_ranges(input_ranges) == expected_output
62+
63+
64+
@pytest.mark.parametrize("macro", [True, False])
65+
@pytest.mark.parametrize("has_label", [True, False])
66+
@pytest.mark.parametrize("num_images", [1, 2, 3])
67+
@pytest.mark.parametrize("root_tag", ["OME", "InvalidRoot"])
68+
def test_remove_ome_image_metadata(macro, has_label, num_images, root_tag):
69+
original_xml_string = generate_xml(
70+
has_macro=macro, has_label=has_label, num_images=1, root_tag=root_tag
71+
)
72+
73+
excluded_metadata = remove_ome_image_metadata(original_xml_string)
74+
if root_tag == "OME":
75+
parsed_excluded = ET.fromstring(excluded_metadata)
76+
77+
# Assert if "label" subelement is present
78+
assert (
79+
parsed_excluded.find(
80+
".//{http://www.openmicroscopy.org/Schemas/OME/2016-06}Image[@ID='Image:label'][@Name='label']"
81+
)
82+
is None
83+
)
84+
85+
# Assert if "macro" subelement is present
86+
assert (
87+
parsed_excluded.find(
88+
".//{http://www.openmicroscopy.org/Schemas/OME/2016-06}Image[@ID='Image:macro'][@Name='macro']"
89+
)
90+
is None
91+
)
92+
else:
93+
assert remove_ome_image_metadata(original_xml_string) is None

tiledb/bioimg/converters/base.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
iter_levels_meta,
5252
iter_pixel_depths_meta,
5353
open_bioimg,
54+
remove_ome_image_metadata,
5455
resolve_path,
5556
validate_ingestion,
5657
)
@@ -512,6 +513,12 @@ def to_tiledb(
512513

513514
if not exclude_metadata:
514515
original_metadata = reader.original_metadata
516+
else:
517+
if ome_xml := reader.original_metadata.get("ome_metadata"):
518+
pruned_metadata = remove_ome_image_metadata(ome_xml)
519+
original_metadata = (
520+
{"ome_metadata": pruned_metadata} if pruned_metadata else {}
521+
)
515522

516523
with rw_group:
517524
rw_group.w_group.meta.update(

tiledb/bioimg/converters/ome_tiff.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@
3434
from tiledb.highlevel import _get_ctx
3535

3636
from .. import ATTR_NAME, EXPORT_TILE_SIZE, WHITE_RGBA
37-
from ..helpers import get_decimal_from_rgba, get_logger_wrapper, get_rgba, iter_color
37+
from ..helpers import (
38+
get_decimal_from_rgba,
39+
get_logger_wrapper,
40+
get_rgba,
41+
iter_color,
42+
)
3843
from .axes import Axes
3944
from .base import ImageConverterMixin
4045
from .io import as_array

tiledb/bioimg/helpers.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import logging
44
import os
5+
import re
56
import sys
7+
import xml.etree.ElementTree as ET
68
from pathlib import Path
79
from typing import (
810
Any,
@@ -14,6 +16,7 @@
1416
Optional,
1517
Sequence,
1618
Tuple,
19+
Union,
1720
)
1821
from urllib.parse import urlparse
1922

@@ -456,3 +459,44 @@ def merge_ned_ranges(
456459
merged_ranges_per_axis = [merge_ranges(ranges) for ranges in ranges_per_axis]
457460

458461
return tuple(merged_ranges_per_axis)
462+
463+
464+
def remove_ome_image_metadata(xml_string: str) -> Union[str, Any]:
465+
"""
466+
This functions parses an OME-XML file and removes the `macro` and `label` metadata
467+
of the image that it accompanies.
468+
:param xml_string: OME-XML string to remove metadata from
469+
:return: OME-XML string with metadata removed
470+
"""
471+
if not xml_string.lstrip().startswith("<OME") or not xml_string:
472+
return None
473+
474+
# Parse the XML string
475+
root = ET.fromstring(xml_string)
476+
477+
# Extract the namespace from the root element's tag
478+
namespace = root.tag.split("}")[0].strip("{") # Extract namespace
479+
ns = {"ome": namespace}
480+
481+
# Find all images
482+
images = root.findall("ome:Image", ns)
483+
484+
# Iterate over images and remove those with Name 'macro' or 'label'
485+
for image in images:
486+
name = image.attrib.get("Name")
487+
if name in ["macro", "label"]:
488+
root.remove(image)
489+
490+
# Return the modified XML as a string
491+
# Regular expression pattern to match 'ns0', 'ns0:', or ':ns0'
492+
pattern = r"ns0:|:ns0|ns0"
493+
494+
# Substitute the matches with an empty string
495+
return re.sub(
496+
pattern,
497+
"",
498+
ET.tostring(
499+
root,
500+
encoding="unicode",
501+
),
502+
)

0 commit comments

Comments
 (0)