11import multiprocessing as mp
22import os
3+ import re
34
45from glob import glob
56from pathlib import Path
@@ -54,19 +55,15 @@ def _read_start_position_flamingo(path):
5455 return start_position
5556
5657
57- def read_metadata_flamingo (metadata_paths , center_tiles ):
58- start_positions = []
58+ def read_metadata_flamingo (metadata_path , offset = None ):
5959 resolution , unit = None , None
60- for path in metadata_paths :
61- resolution , unit = _read_resolution_and_unit_flamingo (path )
62- start_position = _read_start_position_flamingo (path )
63- start_positions .append (start_position )
6460
65- start_positions = np . array ( start_positions )
66- offset = np . min ( start_positions , axis = 0 ) if center_tiles else np . array ([ 0.0 , 0.0 , 0.0 ] )
61+ resolution , unit = _read_resolution_and_unit_flamingo ( metadata_path )
62+ start_position = _read_start_position_flamingo ( metadata_path )
6763
6864 def _pos_to_trafo (pos ):
69- pos -= offset
65+ if offset is not None :
66+ pos -= offset
7067
7168 # FIXME: dirty hack
7269 # scale = 4
@@ -93,11 +90,9 @@ def _pos_to_trafo(pos):
9390 }
9491 return trafo
9592
96- transformations = [
97- _pos_to_trafo (pos ) for pos in start_positions
98- ]
93+ transformation = _pos_to_trafo (start_position )
9994 # We have to reverse the resolution because pybdv expects ZYX.
100- return resolution [::- 1 ], unit , transformations
95+ return resolution [::- 1 ], unit , transformation
10196
10297
10398# TODO derive the scale factors from the shape rather than hard-coding it to 5 levels
@@ -106,15 +101,61 @@ def derive_scale_factors(shape):
106101 return scale_factors
107102
108103
104+ def flamingo_filename_parser (file_path , name_mapping ):
105+ filename = os .path .basename (file_path )
106+
107+ # Extract the timepoint.
108+ match = re .search (r'_t(\d+)_' , filename )
109+ if match :
110+ timepoint = int (match .group (1 ))
111+ else :
112+ timepoint = 0
113+
114+ # Extract the additional attributes.
115+ attributes = {}
116+ if name_mapping is None :
117+ name_mapping = {}
118+
119+ # Extract the channel.
120+ match = re .search (r'_C(\d+)_' , filename )
121+ channel = int (match .group (1 )) if match else 0
122+ channel_mapping = name_mapping .get ("channel" , {})
123+ attributes ["channel" ] = {"id" : channel , "name" : channel_mapping .get (channel , str (channel ))}
124+
125+ # Extract the tile.
126+ match = re .search (r'_R(\d+)_' , filename )
127+ tile = int (match .group (1 )) if match else 0
128+ tile_mapping = name_mapping .get ("tile" , {})
129+ attributes ["tile" ] = {"id" : tile , "name" : tile_mapping .get (tile , str (tile ))}
130+
131+ # Extract the illumination.
132+ match = re .search (r'_I(\d+)_' , filename )
133+ illumination = int (match .group (1 )) if match else 0
134+ illumination_mapping = name_mapping .get ("illumination" , {})
135+ attributes ["illumination" ] = {"id" : illumination , "name" : illumination_mapping .get (illumination , str (illumination ))}
136+
137+ # Extract D. TODO what is this?
138+ match = re .search (r'_D(\d+)_' , filename )
139+ D = int (match .group (1 )) if match else 0
140+ D_mapping = name_mapping .get ("D" , {})
141+ attributes ["D" ] = {"id" : D , "name" : D_mapping .get (D , str (D ))}
142+
143+ # BDV also supports an angle attribute, but it does not seem to be stored in the filename
144+ # "angle": {"id": 0, "name": "0"}
145+
146+ attribute_id = f"c{ channel } -t{ tile } -i{ illumination } -d{ D } "
147+ return timepoint , attributes , attribute_id
148+
149+
109150def convert_lightsheet_to_bdv (
110151 root : str ,
111- channel_folders : Dict [str , str ],
112- image_file_name_pattern : str ,
113152 out_path : str ,
153+ attribute_parser : callable = flamingo_filename_parser ,
154+ attribute_names : Optional [Dict [str , Dict [int , str ]]] = None ,
114155 metadata_file_name_pattern : Optional [str ] = None ,
115156 metadata_root : Optional [str ] = None ,
116157 metadata_type : str = "flamingo" ,
117- center_tiles : bool = True ,
158+ center_tiles : bool = False ,
118159 resolution : Optional [List [float ]] = None ,
119160 unit : Optional [str ] = None ,
120161 scale_factors : Optional [List [List [int ]]] = None ,
@@ -125,24 +166,14 @@ def convert_lightsheet_to_bdv(
125166 The data is converted to the bdv-n5 file format and can be opened with BigDataViewer
126167 or BigStitcher. This function is written with data layout and metadata of flamingo
127168 microscopes in mind, but could potentially be adapted to other data formats.
128- We currently don't support multiple timepoints, but support can be added if needed.
129169
130- This function assumes the following input data format:
131- <ROOT>/<CHANNEL1>/<TILE1>.tif
132- /<TILE2>.tif
133- /...
134- /<CHANNEL2>/<TILE1>.tif
135- /<TILE2>.tif
136- /...
170+ TODO explain the attribute parsing.
137171
138172 Args:
139- root: Folder that contains the folders with tifs for each channel.
140- channel_folders: Dictionary that maps the name of each channel to the corresponding folder name
141- underneath the root folder.
142- image_file_name_pattern: The pattern for the names of the tifs that contain the data.
143- This expects a glob pattern (name with '*') to select the corresponding tif files .
144- The simplest pattern that should work in most cases is '*.tif'.
173+ root: Folder that contains the image data stored as tifs.
174+ This function will take into account all tif files in folders beneath this root directory.
145175 out_path: Output path where the converted data is saved.
176+ attribute_parser: TODO
146177 metadata_file_name_pattern: The pattern for the names of files that contain the metadata.
147178 For flamingo metadata the following pattern should work: '*_Settings.txt'.
148179 metadata_root: Different root folder for the metadata. By default 'root' is used here as well.
@@ -170,60 +201,73 @@ def convert_lightsheet_to_bdv(
170201 if ext == "" :
171202 out_path = str (Path (out_path ).with_suffix (".n5" ))
172203
173- # Iterate over the channels
174- for channel_id , (channel_name , channel_folder ) in enumerate (channel_folders .items ()):
175-
176- # Get all the image file paths for this channel.
177- tile_pattern = os .path .join (root , channel_folder , image_file_name_pattern )
178- file_paths = sorted (glob (tile_pattern ))
179- assert len (file_paths ) > 0 , tile_pattern
204+ files = sorted (glob (os .path .join (root , "**/*.tif" ), recursive = True ))
205+ if metadata_file_name_pattern is None :
206+ metadata_files = [None ] * len (files )
207+ offset = None
208+ else :
209+ metadata_files = sorted (
210+ glob (
211+ os .path .join (root if metadata_root is None else metadata_root , f"**/{ metadata_file_name_pattern } " ),
212+ recursive = True
213+ )
214+ )
215+ assert len (metadata_files ) == len (files )
216+
217+ if center_tiles :
218+ start_positions = []
219+ for mpath in metadata_files :
220+ start_positions .append (_read_start_position_flamingo (mpath ))
221+ offset = np .min (start_positions , axis = 0 )
222+ else :
223+ offset = None
224+
225+ next_setup_id = 0
226+ attrs_to_setups = {}
227+
228+ for file_path , metadata_file in zip (files , metadata_files ):
229+ timepoint , attributes , aid = attribute_parser (file_path , attribute_names )
230+
231+ if aid in attrs_to_setups :
232+ setup_id = attrs_to_setups [aid ]
233+ else :
234+ attrs_to_setups [aid ] = next_setup_id
235+ setup_id = next_setup_id
236+ next_setup_id += 1
180237
181238 # Read the metadata if it was given.
182- if metadata_file_name_pattern is None : # No metadata given.
239+ if metadata_file is None : # No metadata given.
183240 # We don't use any tile transformation.
184- tile_transformations = [ None ] * len ( file_paths )
241+ tile_transformation = None
185242 # Set resolution and unit to their default values if they were not passed.
186243 if resolution is None :
187244 resolution = [1.0 , 1.0 , 1.0 ]
188245 if unit is None :
189246 unit = "pixel"
190247
191248 else : # We have metadata and read it.
192- metadata_pattern = os .path .join (
193- root if metadata_root is None else metadata_root ,
194- channel_folder , metadata_file_name_pattern
195- )
196- metadata_paths = sorted (glob (metadata_pattern ))
197- assert len (metadata_paths ) == len (file_paths )
198- resolution , unit , tile_transformations = read_metadata_flamingo (metadata_paths , center_tiles )
199-
200- if channel_name is None or channel_name .strip () == "" : #channel name is empty, assign channel id as name
201- channel_name = str (channel_id )
202-
203- for tile_id , (file_path , tile_transformation ) in enumerate (zip (file_paths , tile_transformations )):
204-
205- # Try to memmap the data. If that doesn't work fall back to loading it into memory.
206- try :
207- data = tifffile .memmap (file_path , mode = "r" )
208- except ValueError :
209- print (f"Could not memmap the data from { file_path } . Fall back to load it into memory." )
210- data = tifffile .imread (file_path )
211-
212- print ("Converting channel" , channel_id , "tile" , tile_id , "from" , file_path , "with shape" , data .shape )
213- if scale_factors is None :
214- scale_factors = derive_scale_factors (data .shape )
215-
216- pybdv .make_bdv (
217- data , out_path ,
218- downscale_factors = scale_factors , downscale_mode = "mean" ,
219- n_threads = n_threads ,
220- resolution = resolution , unit = unit ,
221- attributes = {
222- "channel" : {"id" : channel_id , "name" : channel_name }, "tile" : {"id" : tile_id , "name" : str (tile_id )},
223- "angle" : {"id" : 0 , "name" : "0" }, "illumination" : {"id" : 0 , "name" : "0" }
224- },
225- affine = tile_transformation ,
226- )
249+ resolution , unit , tile_transformation = read_metadata_flamingo (metadata_file , offset )
250+
251+ print (f"Converting tp={ timepoint } , channel={ attributes ['channel' ]} , tile={ attributes ['tile' ]} " )
252+ try :
253+ data = tifffile .memmap (file_path , mode = "r" )
254+ except ValueError :
255+ print (f"Could not memmap the data from { file_path } . Fall back to load it into memory." )
256+ data = tifffile .imread (file_path )
257+
258+ if scale_factors is None :
259+ scale_factors = derive_scale_factors (data .shape )
260+
261+ pybdv .make_bdv (
262+ data , out_path ,
263+ downscale_factors = scale_factors , downscale_mode = "mean" ,
264+ n_threads = n_threads ,
265+ resolution = resolution , unit = unit ,
266+ attributes = attributes ,
267+ affine = tile_transformation ,
268+ timepoint = timepoint ,
269+ setup_id = setup_id ,
270+ )
227271
228272
229273# TODO expose more arguments via CLI.
0 commit comments