@@ -135,32 +135,34 @@ def _default_image_models_kwargs(image_models_kwargs: dict | None):
135135 return image_models_kwargs
136136
137137
138- def _is_color_valid (channel_name : str ) -> bool :
139- """The color is valid if it contains a wavelength (e.g., `550`) or is known by the Xenium Explorer"""
140- known_colors = set (ExplorerConstants .KNOWN_CHANNELS .keys ())
141- contains_wavelength = bool (re .search (r"(?<![0-9])[0-9]{3}(?![0-9])" , channel_name ))
142- return contains_wavelength or channel_name in known_colors
138+ def _to_color (channel_name : str , is_wavelength : bool , colors_iterator : list ):
139+ if is_wavelength :
140+ return channel_name
141+ if channel_name in ExplorerConstants .KNOWN_CHANNELS :
142+ return f"{ channel_name } (color={ ExplorerConstants .KNOWN_CHANNELS [channel_name ]} )"
143+ return f"{ channel_name } (color={ colors_iterator .pop ()} )"
143144
144145
145146def _set_colors (channel_names : list [str ]) -> list [str ]:
146147 """
147148 Trick to provide a color to all channels on the Xenium Explorer.
148149
149- Some colors are automatically colored by the Xenium explorer (e.g., DAPI is colored in blue).
150150 But some channels colors are set to white by default. This functions allows to color these
151151 channels with an available wavelength color (e.g., `550`).
152152 """
153- colors_valid = [_is_color_valid (name ) for name in channel_names ]
154-
155- already_assigned_colors = {ExplorerConstants .KNOWN_CHANNELS .get (name ) for name in channel_names }
156- available_colors = sorted (list (set (ExplorerConstants .COLORS ) - already_assigned_colors ))
157-
158- n_invalid = len (colors_valid ) - sum (colors_valid )
159- color_indices = list (np .linspace (0 , len (available_colors ) - 1 , n_invalid ).round ().astype (int ))
153+ existing_wavelength = [
154+ bool (re .search (r"(?<![0-9])[0-9]{3}(?![0-9])" , c )) for c in channel_names
155+ ]
156+ valid_colors = [c for c in ExplorerConstants .COLORS if c != ExplorerConstants .NUCLEUS_COLOR ]
157+ n_missing = sum (
158+ not is_wavelength and not c in ExplorerConstants .KNOWN_CHANNELS
159+ for c , is_wavelength in zip (channel_names , existing_wavelength )
160+ )
161+ colors_iterator : list = np .repeat (valid_colors , ceil (n_missing / len (valid_colors ))).tolist ()
160162
161163 return [
162- name if is_valid else f" { name } (color= { available_colors [ color_indices . pop ()] } )"
163- for name , is_valid in zip (channel_names , colors_valid )
164+ _to_color ( c , is_wavelength , colors_iterator )
165+ for c , is_wavelength in zip (channel_names , existing_wavelength )
164166 ]
165167
166168
@@ -240,41 +242,44 @@ def align(
240242 sdata .add_image (image .name , image , overwrite = overwrite )
241243
242244
243- def _get_channel_names_xenium_if (element , names ):
244- for child in element :
245- _get_channel_names_xenium_if (child , names )
246- if element .attrib :
247- if "Name" in element .attrib :
248- names .append (element .attrib ["Name" ])
249- return names
245+ def _ome_channels_names (path : str ):
246+ import xml .etree .ElementTree as ET
247+
248+ tiff = tf .TiffFile (path )
249+ omexml_string = tiff .pages [0 ].description
250+
251+ root = ET .fromstring (omexml_string )
252+ namespaces = {"ome" : "http://www.openmicroscopy.org/Schemas/OME/2016-06" }
253+ channels = root .findall ("ome:Image[1]/ome:Pixels/ome:Channel" , namespaces )
254+ return [c .attrib ["Name" ] if "Name" in c .attrib else c .attrib ["ID" ] for c in channels ]
250255
251256
252- def xenium_if (path : Path ) -> SpatialImage :
253- """Read the IF image associated to Xenium data
257+ def ome_tif (path : Path ) -> SpatialImage :
258+ """Read an `.ome.tif` image. This image should be a 2D image (with possibly multiple channels).
259+ Typically, this function can be used to open Xenium IF images.
254260
255261 Args:
256- path: Path to the `.ime .tif` IF image
262+ path: Path to the `.ome .tif` image
257263
258264 Returns:
259- A `SpatialImage` representing the Xenium IF image
265+ A `SpatialImage`
260266 """
261267 image_models_kwargs = _default_image_models_kwargs (None )
262-
263- image : da .Array = imread (path )
264- image = image .rechunk (chunks = image_models_kwargs ["chunks" ])
265-
266268 image_name = Path (path ).absolute ().name .split ("." )[0 ]
269+ image : da .Array = imread (path )
267270
268- import xml .etree .ElementTree as ET
271+ if image .ndim == 4 :
272+ assert image .shape [0 ] == 1 , f"4D images not supported"
273+ image = da .moveaxis (image [0 ], 2 , 0 )
274+ log .info (f"Transformed 4D image into a 3D image of shape (c, y, x) = { image .shape } " )
275+ elif image .ndim != 3 :
276+ raise ValueError (f"Number of dimensions not supported: { image .ndim } " )
269277
270- with tf .TiffFile (path ) as tif :
271- page_series = tif .series [0 ]
272- desc = page_series [0 ].description
273- root = ET .fromstring (desc )
274- names = _get_channel_names_xenium_if (root , [])
278+ image = image .rechunk (chunks = image_models_kwargs ["chunks" ])
275279
276- if len (names ) != len (image ):
277- names = [str (i ) for i in range (len (image ))]
278- log .warn (f"Channel names couldn't be read. Using { names } instead." )
280+ channel_names = _ome_channels_names (path )
281+ if len (channel_names ) != len (image ):
282+ channel_names = [str (i ) for i in range (len (image ))]
283+ log .warn (f"Channel names couldn't be read. Using { channel_names } instead." )
279284
280- return SpatialImage (image , dims = ["c" , "y" , "x" ], name = image_name , coords = {"c" : names })
285+ return SpatialImage (image , dims = ["c" , "y" , "x" ], name = image_name , coords = {"c" : channel_names })
0 commit comments