11import spatialdata as sd
22from xenium_analysis_tools .utils .sd_utils import add_micron_coord_sys
3- from spatialdata .models import Image3DModel , Labels3DModel
3+ from spatialdata .models import Image3DModel , Labels3DModel , PointsModel
44from pathlib import Path
55import pandas as pd
66import numpy as np
@@ -63,9 +63,9 @@ def get_zstacks_dict(zstacks_folder, channels=['gcamp', 'dextran']):
6363
6464 for stack_ind , stack_folder in enumerate (stack_dirs ):
6565 stack_info = {
66- 'stack_name ' : stack_folder .name ,
67- 'stack_size ' : _extract_stack_size (stack_folder .name ),
68- 'stack_channels ' : [ch for ch in channels if ch in stack_folder .name .lower ()],
66+ 'zstack_name ' : stack_folder .name ,
67+ 'zstack_size ' : _extract_zstack_size (stack_folder .name ),
68+ 'zstack_channels ' : [ch for ch in channels if ch in stack_folder .name .lower ()],
6969 'metadata_jsons' : {'registration' : None , 'roi_groups' : None , 'scanimage' : None },
7070 'channel_tifs' : {}
7171 }
@@ -92,9 +92,9 @@ def get_zstacks_dict(zstacks_folder, channels=['gcamp', 'dextran']):
9292
9393 return zstacks_dict
9494
95- def _extract_stack_size ( stack_name ):
95+ def _extract_zstack_size ( zstack_name ):
9696 """Extract width x height x depth from stack name."""
97- size_pattern = re .search (r'(\d+)x(\d+)x(\d+)' , stack_name )
97+ size_pattern = re .search (r'(\d+)x(\d+)x(\d+)' , zstack_name )
9898 if size_pattern :
9999 width , height , depth = map (int , size_pattern .groups ())
100100 return {"width" : width , "height" : height , "depth" : depth }
@@ -110,7 +110,7 @@ def _categorize_json_file(filename_lower):
110110 return 'scanimage'
111111 return None
112112
113- def get_zstack (zstacks_dict , zstack_ind = None , zstack_name = None , stack_size = None , channels = None ):
113+ def get_zstack (zstacks_dict , zstack_ind = None , zstack_name = None , zstack_size = None , zstack_channels = None ):
114114 if zstack_ind is not None :
115115 if zstack_ind not in zstacks_dict :
116116 raise ValueError (f"Z-stack index { zstack_ind } not found in zstacks_dict." )
@@ -127,37 +127,36 @@ def _find_matches(criterion_func, criterion_name, criterion_value):
127127 return zstacks_dict [matches [0 ]]
128128
129129 # Handle multiple matches with optional channel filtering
130- if channels is not None :
130+ if zstack_channels is not None :
131131 channel_matches = [
132132 i for i in matches
133- if set (zstacks_dict [i ]['stack_channels ' ]) == set (channels )
133+ if set (zstacks_dict [i ]['zstack_channels ' ]) == set (zstack_channels )
134134 ]
135135 if len (channel_matches ) == 1 :
136136 return zstacks_dict [channel_matches [0 ]]
137137 elif len (channel_matches ) > 1 :
138- raise ValueError (f"Multiple z-stacks found with { criterion_name } { criterion_value } and channels { channels } . Found { len (channel_matches )} matches." )
138+ raise ValueError (f"Multiple z-stacks found with { criterion_name } { criterion_value } and channels { zstack_channels } . Found { len (channel_matches )} matches." )
139139 else :
140- raise ValueError (f"No z-stack found with { criterion_name } { criterion_value } and channels { channels } ." )
141-
140+ raise ValueError (f"No z-stack found with { criterion_name } { criterion_value } and channels { zstack_channels } ." )
142141 raise ValueError (f"Multiple z-stacks found with { criterion_name } { criterion_value } . Found { len (matches )} matches. Consider specifying channels parameter." )
143142
144143 if zstack_name is not None :
145144 return _find_matches (
146- lambda stack : stack ['stack_name ' ] == zstack_name ,
145+ lambda stack : stack ['zstack_name ' ] == zstack_name ,
147146 "Z-stack name" , zstack_name
148147 )
149148
150- if stack_size is not None :
149+ if zstack_size is not None :
151150 return _find_matches (
152151 lambda stack : (
153- stack ['stack_size ' ]['width' ] == stack_size ['width' ] and
154- stack ['stack_size ' ]['height' ] == stack_size ['height' ] and
155- stack ['stack_size ' ]['depth' ] == stack_size ['depth' ]
152+ stack ['zstack_size ' ]['width' ] == zstack_size ['width' ] and
153+ stack ['zstack_size ' ]['height' ] == zstack_size ['height' ] and
154+ stack ['zstack_size ' ]['depth' ] == zstack_size ['depth' ]
156155 ),
157- "Stack size" , stack_size
156+ "Stack size" , zstack_size
158157 )
159158
160- raise ValueError ("Either zstack_ind, zstack_name, or stack_size must be provided." )
159+ raise ValueError ("Either zstack_ind, zstack_name, or zstack_size must be provided." )
161160
162161def get_alignment_data_paths (dataset_id ,
163162 data_root = Path ('/root/capsule/data' ),
@@ -182,68 +181,84 @@ def get_alignment_data_paths(dataset_id,
182181
183182 return paths
184183
185- def get_zstack_sdata (stack , zstack_masks = None , use_shared_coords = True ):
184+ def get_label_params (label_obj , id_name = 'cell' ):
185+ from skimage .measure import regionprops
186+ labels = label_obj .values
187+ props = regionprops (labels )
188+ data = [
189+ {f'{ id_name } _id' : p .label ,
190+ 'z' : p .centroid [0 ],
191+ 'y' : p .centroid [1 ],
192+ 'x' : p .centroid [2 ],
193+ 'area' : p .area ,
194+ 'bbox' : p .bbox }
195+ for p in props
196+ ]
197+ df = pd .DataFrame (data )
198+ return df
199+
200+ def get_zstack_sdata (stack , zstack_masks = None , get_centroids_as_points = True ):
186201 # Create the z-stack image array
187- num_channels = len (stack ['stack_channels ' ])
202+ num_channels = len (stack ['zstack_channels ' ])
188203 chans = []
189204 if num_channels > 1 :
190205 for ch_ind in range (num_channels ):
191206 chan_array = create_zstack_array (tif_path = stack ['channel_tifs' ][ch_ind ]['chan_tif_path' ],
192- fov_x_um = stack ['stack_size ' ]['width' ],
193- fov_y_um = stack ['stack_size ' ]['height' ],
194- fov_z_um = stack ['stack_size ' ]['depth' ])
207+ fov_x_um = stack ['zstack_size ' ]['width' ],
208+ fov_y_um = stack ['zstack_size ' ]['height' ],
209+ fov_z_um = stack ['zstack_size ' ]['depth' ])
195210 chans .append (chan_array )
196211 zstack_img = xr .concat (chans , dim = 'c' )
197- zstack_img ['c' ] = stack ['stack_channels ' ]
212+ zstack_img ['c' ] = stack ['zstack_channels ' ]
198213 else :
199214 zstack_img = create_zstack_array (tif_path = stack ['channel_tifs' ][0 ]['chan_tif_path' ],
200- fov_x_um = stack ['stack_size' ]['width' ],
201- fov_y_um = stack ['stack_size' ]['height' ],
202- fov_z_um = stack ['stack_size' ]['depth' ])
203- zstack_img ['c' ] = stack ['stack_channels' ]
204-
205- if use_shared_coords :
206- reg_json_path = stack ['metadata_jsons' ]['registration' ]
207- with open (reg_json_path ) as f :
208- reg_json = json .load (f )
209- if 'z_steps' in reg_json .keys () and len (reg_json ['z_steps' ])== zstack_img .sizes ['z' ]:
210- print ("Using shared z coordinates for images" )
211- zstack_img .coords ['z' ] = reg_json ['z_steps' ]
215+ fov_x_um = stack ['zstack_size' ]['width' ],
216+ fov_y_um = stack ['zstack_size' ]['height' ],
217+ fov_z_um = stack ['zstack_size' ]['depth' ])
218+ zstack_img ['c' ] = stack ['zstack_channels' ]
212219
213220 # Parse into Image3DModel
214221 zstack_img = Image3DModel .parse (
215222 zstack_img ,
216223 dims = ['c' , 'z' , 'y' , 'x' ],
217- c_coords = stack ['stack_channels ' ],
224+ c_coords = stack ['zstack_channels ' ],
218225 chunks = 'auto' ,
219226 )
220227
221- # Make the SpatialData object
222- zstack_sdata = sd .SpatialData (
223- images = {'zstack' : zstack_img },
224- )
225-
226228 if zstack_masks is not None :
229+ zstack_labels = {}
227230 # Get labels for each channel
228231 for mask_ind , masks in zstack_masks ['channel_tifs' ].items ():
229- channel_name = zstack_masks ['stack_channels ' ][mask_ind ]
232+ channel_name = zstack_masks ['zstack_channels ' ][mask_ind ]
230233 zstack_label = create_zstack_array (tif_path = masks ['chan_tif_path' ],
231- fov_x_um = zstack_masks ['stack_size ' ]['width' ],
232- fov_y_um = zstack_masks ['stack_size ' ]['height' ],
233- fov_z_um = zstack_masks ['stack_size ' ]['depth' ],
234+ fov_x_um = zstack_masks ['zstack_size ' ]['width' ],
235+ fov_y_um = zstack_masks ['zstack_size ' ]['height' ],
236+ fov_z_um = zstack_masks ['zstack_size ' ]['depth' ],
234237 add_chan = False )
235238
236- if use_shared_coords :
237- if 'z_steps' in reg_json .keys () and len (reg_json ['z_steps' ])== zstack_label .sizes ['z' ]:
238- print ("Using shared z coordinates for labels" )
239- zstack_label .coords ['z' ] = reg_json ['z_steps' ]
240-
241239 zstack_label = Labels3DModel .parse (
242240 zstack_label ,
243241 dims = ['z' , 'y' , 'x' ],
244242 chunks = 'auto' ,
245243 )
246- zstack_sdata .labels [f"{ channel_name } _labels" ] = zstack_label
244+ zstack_labels [f"{ channel_name } _labels" ] = zstack_label
245+
246+ if get_centroids_as_points :
247+ zstack_points = {}
248+ # Get label parameters add as points
249+ for label_name , labels_obj in zstack_labels .items ():
250+ chan_name = label_name .replace ('_labels' ,'' )
251+ cells_df = get_label_params (labels_obj , id_name = chan_name )
252+ print (f"# { chan_name } segmented cells: { len (cells_df )} " )
253+ cells_df = PointsModel .parse (cells_df )
254+ zstack_points [f"{ chan_name } _cells" ] = cells_df
255+
256+ # Assemble SpatialData
257+ zstack_sdata = sd .SpatialData (
258+ images = {'zstack' : zstack_img },
259+ labels = {** zstack_labels } if zstack_masks is not None else {},
260+ points = {** zstack_points } if (zstack_masks is not None and get_centroids_as_points ) else {}
261+ )
247262
248263 # Determine pixel sizes
249264 if zstack_sdata ['zstack' ].attrs ['pixel_size_um_x' ] == zstack_sdata ['zstack' ].attrs ['pixel_size_um_y' ]:
0 commit comments