Skip to content

Commit e4928c6

Browse files
Added scripts for generating test images
1 parent c9d866d commit e4928c6

3 files changed

Lines changed: 605 additions & 0 deletions

File tree

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""
2+
This script generates dummy Thorlabs RAW files along with matching XML metadata files.
3+
"""
4+
# %% IMPORTS
5+
import os
6+
import numpy as np
7+
import xml.etree.ElementTree as ET
8+
from xml.dom import minidom
9+
10+
# %% FUNCTIONS
11+
def write_example_xml(
12+
xml_path,
13+
*,
14+
X,
15+
Y,
16+
C,
17+
T,
18+
Z,
19+
pixel_size_um=0.5,
20+
z_step_um=1.0,
21+
time_interval_s=1.0,
22+
bits=16,
23+
):
24+
"""
25+
Create a minimal Thorlabs-like XML file that matches the expectations
26+
of read_thorlabs_raw().
27+
"""
28+
29+
root = ET.Element("ThorImage")
30+
31+
lsm = ET.SubElement(root, "LSM")
32+
lsm.set("pixelX", str(X))
33+
lsm.set("pixelY", str(Y))
34+
lsm.set("channel", str(C))
35+
lsm.set("pixelSizeUM", str(pixel_size_um))
36+
lsm.set("frameRate", "1.0")
37+
38+
wavelengths = ET.SubElement(root, "Wavelengths")
39+
for i in range(C):
40+
ET.SubElement(wavelengths, "Wavelength").set("index", str(i))
41+
42+
timelapse = ET.SubElement(root, "Timelapse")
43+
timelapse.set("timepoints", str(T))
44+
timelapse.set("intervalSec", str(time_interval_s))
45+
46+
camera = ET.SubElement(root, "Camera")
47+
camera.set("bitsPerPixel", str(bits))
48+
49+
zstage = ET.SubElement(root, "ZStage")
50+
zstage.set("steps", str(Z))
51+
zstage.set("stepSizeUM", str(z_step_um))
52+
53+
streaming = ET.SubElement(root, "Streaming")
54+
streaming.set("zFastEnable", "1" if Z > 1 else "0")
55+
56+
# pretty-print
57+
rough_string = ET.tostring(root, "utf-8")
58+
reparsed = minidom.parseString(rough_string)
59+
pretty_xml = reparsed.toprettyxml(indent=" ")
60+
61+
with open(xml_path, "w") as f:
62+
f.write(pretty_xml)
63+
64+
65+
def write_dummy_raw(
66+
raw_path,
67+
*,
68+
T,
69+
Z,
70+
C,
71+
Y,
72+
X,
73+
dtype=np.uint16,
74+
):
75+
"""
76+
Write a RAW file with a simple ramp pattern.
77+
Data layout is contiguous and matches reshape((T,Z,C,Y,X)).
78+
"""
79+
80+
total_elements = T * Z * C * Y * X
81+
data = np.arange(total_elements, dtype=dtype)
82+
data.tofile(raw_path)
83+
84+
85+
def generate_case(base_dir, name, *, T, Z, C, Y, X):
86+
os.makedirs(base_dir, exist_ok=True)
87+
88+
raw_path = os.path.join(base_dir, f"{name}.raw")
89+
xml_path = os.path.join(base_dir, f"{name}.xml")
90+
91+
print(f"Creating test case: {name}")
92+
print(f" Shape: T={T}, Z={Z}, C={C}, Y={Y}, X={X}")
93+
94+
write_dummy_raw(
95+
raw_path,
96+
T=T, Z=Z, C=C, Y=Y, X=X,
97+
dtype=np.uint16,
98+
)
99+
100+
write_example_xml(
101+
xml_path,
102+
X=X,
103+
Y=Y,
104+
C=C,
105+
T=T,
106+
Z=Z,
107+
pixel_size_um=0.5,
108+
z_step_um=1.0,
109+
time_interval_s=1.0,
110+
bits=16,
111+
)
112+
113+
# %% MAIN
114+
if __name__ == "__main__":
115+
out_root = "thorlabs_dummy_data"
116+
# prepend path to folder of this script:
117+
out_root = os.path.join(
118+
os.path.dirname(os.path.abspath(__file__)),
119+
out_root)
120+
121+
# Case 1: C=1, Z=1, T=1
122+
generate_case(
123+
os.path.join(out_root, "case_C1_Z1_T1"),
124+
"example_C1_Z1_T1",
125+
T=1,
126+
Z=1,
127+
C=1,
128+
Y=20,
129+
X=20,
130+
)
131+
132+
# Case 2: C=2, Z=1, T=1
133+
generate_case(
134+
os.path.join(out_root, "case_C2_Z1_T1"),
135+
"example_C2_Z1_T1",
136+
T=1,
137+
Z=1,
138+
C=2,
139+
Y=20,
140+
X=20,
141+
)
142+
143+
# Case 3: C=2, Z=10, T=5
144+
generate_case(
145+
os.path.join(out_root, "case_C2_Z10_T5"),
146+
"example_C2_Z10_T5",
147+
T=5,
148+
Z=10,
149+
C=2,
150+
Y=20,
151+
X=20,
152+
)
153+
154+
print("\nAll dummy Thorlabs RAW test cases created.")
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
"""
5+
Generate small dummy TIFF test files for OMIO.
6+
7+
Creates:
8+
* plain TIFFs with explicit axes metadata (tifffile metadata['axes'])
9+
* one OME-TIFF (C=2, Z=10, T=5, Y=20, X=20) with physical sizes and time increment
10+
* two paginated multi-series TIFF examples (minisblack and rgb)
11+
12+
All numeric stacks are uint16. Spatial calibration:
13+
* PhysicalSizeX = PhysicalSizeY = 0.19 µm
14+
* PhysicalSizeZ = 2.0 µm
15+
* TIFF resolution is set as (1/PhysicalSizeY, 1/PhysicalSizeX)
16+
17+
Adjust `OUT_DIR` as needed.
18+
"""
19+
# %% IMPORTS
20+
import os
21+
import numpy as np
22+
import tifffile
23+
# %% FUNCTIONS
24+
def ensure_dir(p: str) -> None:
25+
os.makedirs(p, exist_ok=True)
26+
27+
28+
def write_tif(
29+
path: str,
30+
data: np.ndarray,
31+
axes: str,
32+
*,
33+
compression_level: int = 3,
34+
physical_xy: float = 0.19,
35+
bigtiff: bool = False,
36+
ome: bool = False,
37+
extra_metadata: dict | None = None,
38+
photometric: str = "minisblack",
39+
) -> None:
40+
"""
41+
Write a TIFF (or OME-TIFF if ome=True) with axes metadata and XY resolution.
42+
"""
43+
md = {"axes": axes}
44+
if extra_metadata:
45+
md.update(extra_metadata)
46+
47+
tifffile.imwrite(
48+
path,
49+
data,
50+
compression="zlib",
51+
compressionargs={"level": int(compression_level)},
52+
resolution=(1.0 / float(physical_xy), 1.0 / float(physical_xy)),
53+
metadata=md,
54+
photometric=photometric,
55+
imagej=False,
56+
bigtiff=bool(bigtiff),
57+
ome=bool(ome),
58+
)
59+
60+
61+
def make_pattern(shape: tuple[int, ...], dtype=np.uint16) -> np.ndarray:
62+
"""
63+
Deterministic pattern (~~ramp~~ random) to make debugging easier than pure zeros.
64+
"""
65+
#n = int(np.prod(shape))
66+
#arr = np.arange(n, dtype=dtype).reshape(shape)
67+
arr = np.random.randint(0, 255, shape, dtype=dtype)
68+
return arr
69+
70+
71+
def main() -> None:
72+
# Change this to your desired output directory
73+
OUT_DIR = "tif_dummy_data"
74+
# prepend path to folder of this script:
75+
OUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), OUT_DIR)
76+
ensure_dir(OUT_DIR)
77+
78+
physical_xy = 0.19 # µm
79+
physical_z = 2.0 # µm
80+
time_increment = 3.0
81+
time_unit = "s"
82+
83+
# ---------------------------------------------------------------------
84+
# Requested stacks (Y=20, X=20 base)
85+
# ---------------------------------------------------------------------
86+
cases = [
87+
("YX", (20, 20), "YX"),
88+
("TYX_T1", (1, 20, 20), "TYX"),
89+
("ZTYX_Z1_T1", (1, 1, 20, 20), "ZTYX"),
90+
("CZTYX_C1_Z1_T1", (1, 1, 1, 20, 20), "CZTYX"),
91+
("CZTYX_C2_Z1_T1", (2, 1, 1, 20, 20), "CZTYX"),
92+
("CZTYX_C2_Z10_T1", (2, 10, 1, 20, 20), "CZTYX"),
93+
("TZCYX_T5_Z10_C2", (5, 10, 2, 20, 20), "TZCYX"),
94+
]
95+
96+
for name, shape, axes in cases:
97+
data = make_pattern(shape, dtype=np.uint16)
98+
out_path = os.path.join(OUT_DIR, f"tif/{name}.tif")
99+
ensure_dir(os.path.dirname(out_path))
100+
write_tif(
101+
out_path,
102+
data,
103+
axes,
104+
physical_xy=physical_xy,
105+
compression_level=3,
106+
photometric="minisblack",
107+
ome=False)
108+
print(f"Wrote TIFF: {out_path} shape={shape} axes={axes}")
109+
110+
# ---------------------------------------------------------------------
111+
# Also write an OME-TIFF with metadata:
112+
# ---------------------------------------------------------------------
113+
ome_shape = (5, 10, 2, 20, 20)
114+
dd = np.random.randint(0, 255, ome_shape).astype(np.uint8)
115+
116+
ome_out = os.path.join(OUT_DIR, "ome_tif/TZCYX_T5_Z10_C2.ome.tif")
117+
ensure_dir(os.path.dirname(ome_out))
118+
119+
ome_md = {
120+
"axes": "TZCYX",
121+
"PhysicalSizeX": float(physical_xy),
122+
"PhysicalSizeY": float(physical_xy),
123+
"PhysicalSizeZ": float(physical_z),
124+
"PhysicalSizeXUnit": "µm",
125+
"PhysicalSizeYUnit": "µm",
126+
"PhysicalSizeZUnit": "µm",
127+
"TimeIncrement": float(time_increment),
128+
"TimeIncrementUnit": str(time_unit),
129+
}
130+
write_tif(
131+
ome_out,
132+
dd,
133+
"TZCYX",
134+
physical_xy=physical_xy,
135+
compression_level=3,
136+
photometric="minisblack",
137+
ome=True,
138+
extra_metadata=ome_md)
139+
print(f"Wrote OME-TIFF: {ome_out} shape={ome_shape} axes=TZCYX")
140+
141+
# ---------------------------------------------------------------------
142+
# Paginated / multi-series TIFFs
143+
# ---------------------------------------------------------------------
144+
series0 = np.random.randint(0, 255, (16, 16, 3), dtype=np.uint8)
145+
series1 = np.random.randint(0, 255, (16, 16, 3), dtype=np.uint8)
146+
paged_rgb_path = os.path.join(OUT_DIR, "multiseries_tif/multiseries_rgb_with_equal_shapes.tif")
147+
ensure_dir(os.path.dirname(paged_rgb_path))
148+
with tifffile.TiffWriter(paged_rgb_path) as tif:
149+
tif.write(series0, photometric="rgb")
150+
tif.write(series1, photometric="rgb")
151+
print(f"Wrote paginated rgb TIFF: {paged_rgb_path}")
152+
"""
153+
if the image-slice shapes are identical, FIJI's Bio-Formats reader
154+
seems to interpret both pages as one multi-page RGB image, not as two
155+
separate series. Hence, we create some more examples with differing shapes.
156+
"""
157+
158+
series0 = np.random.randint(0, 255, (16, 16, 3), dtype=np.uint8)
159+
series1 = np.random.randint(0, 255, (17, 17, 3), dtype=np.uint8)
160+
paged_rgb_path = os.path.join(OUT_DIR, "multiseries_tif/multiseries_rgb_with_unequal_series.tif")
161+
ensure_dir(os.path.dirname(paged_rgb_path))
162+
with tifffile.TiffWriter(paged_rgb_path) as tif:
163+
tif.write(series0, photometric="rgb")
164+
tif.write(series1, photometric="rgb")
165+
print(f"Wrote paginated rgb TIFF: {paged_rgb_path}")
166+
167+
series0 = np.random.randint(0, 255, (16, 16, 3), dtype=np.uint8)
168+
series1 = np.random.randint(0, 255, (2, 32, 32), dtype=np.uint8)
169+
paged_rgb_path = os.path.join(OUT_DIR, "multiseries_tif/multiseries_rgb_minisblack_mixture.tif")
170+
ensure_dir(os.path.dirname(paged_rgb_path))
171+
with tifffile.TiffWriter(paged_rgb_path) as tif:
172+
tif.write(series0, photometric="rgb")
173+
tif.write(series1, photometric="minisblack")
174+
print(f"Wrote paginated rgb TIFF: {paged_rgb_path}")
175+
176+
series0 = np.random.randint(0, 255, (2, 32, 32), dtype=np.uint8)
177+
series1 = np.random.randint(0, 255, (2, 32, 32), dtype=np.uint8)
178+
paged_rgb_path = os.path.join(OUT_DIR, "multiseries_tif/multiseries_minisblack.tif")
179+
ensure_dir(os.path.dirname(paged_rgb_path))
180+
with tifffile.TiffWriter(paged_rgb_path) as tif:
181+
tif.write(series0, photometric="minisblack")
182+
tif.write(series1, photometric="minisblack")
183+
print(f"Wrote paginated rgb TIFF: {paged_rgb_path}")
184+
185+
data = np.random.randint(0, 255, (8, 2, 20, 20, 3), 'uint16')
186+
subresolutions = 2
187+
pixelsize = 0.29 # micrometer
188+
paged_rgb_path = os.path.join(OUT_DIR, "paginated_tif/paginated.ome.tif")
189+
ensure_dir(os.path.dirname(paged_rgb_path))
190+
with tifffile.TiffWriter(paged_rgb_path, bigtiff=True) as tif:
191+
metadata = {
192+
'axes': 'TCYXS',
193+
'SignificantBits': 8,
194+
'TimeIncrement': 0.1,
195+
'TimeIncrementUnit': 's',
196+
'PhysicalSizeX': pixelsize,
197+
'PhysicalSizeXUnit': 'µm',
198+
'PhysicalSizeY': pixelsize,
199+
'PhysicalSizeYUnit': 'µm',
200+
'Channel': {'Name': ['Channel 1', 'Channel 2']},
201+
'Plane': {'PositionX': [0.0] * 16, 'PositionXUnit': ['µm'] * 16},
202+
'Description': 'A multi-dimensional, multi-resolution image',
203+
'MapAnnotation': { # for OMERO
204+
'Namespace': 'openmicroscopy.org/PyramidResolution',
205+
'1': '256 256',
206+
'2': '128 128',
207+
},
208+
}
209+
options = dict(
210+
photometric='rgb',
211+
tile=(16, 16),
212+
compression='zlib',
213+
resolutionunit='CENTIMETER',
214+
maxworkers=2,
215+
)
216+
tif.write(
217+
data,
218+
subifds=subresolutions,
219+
resolution=(1e4 / pixelsize, 1e4 / pixelsize),
220+
metadata=metadata,
221+
**options)
222+
# write pyramid levels to the two subifds
223+
# in production use resampling to generate sub-resolution images
224+
for level in range(subresolutions):
225+
mag = 2 ** (level + 1)
226+
tif.write(
227+
data[..., ::mag, ::mag, :],
228+
subfiletype=1, # FILETYPE.REDUCEDIMAGE
229+
resolution=(1e4 / mag / pixelsize, 1e4 / mag / pixelsize),
230+
**options)
231+
# add a thumbnail image as a separate series
232+
# it is recognized by QuPath as an associated image
233+
thumbnail = (data[0, 0, ::8, ::8] >> 2).astype('uint8')
234+
tif.write(thumbnail, metadata={'Name': 'thumbnail'})
235+
236+
print("\nDone.")
237+
238+
# %% MAIN
239+
if __name__ == "__main__":
240+
main()

0 commit comments

Comments
 (0)