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 ("\n Done." )
237+
238+ # %% MAIN
239+ if __name__ == "__main__" :
240+ main ()
0 commit comments