1+
2+ from spatialdata .transformations import Scale , Identity , Sequence , set_transformation , get_transformation
3+ from spatialdata .models import Image3DModel
4+ import spatialdata as sd
5+ import xarray as xr
6+ import dask .array as da
7+ import zarr
8+ from xenium_analysis_tools .utils .sd_utils import add_micron_coord_sys
9+ import pandas as pd
10+ import numpy as np
11+ # from bioio_sldy import Reader
12+ import pandas as pd
13+ import yaml
14+ import os
15+
16+ def get_confocal_image_sizes (img_name , cf_raw_path , overlap = 0.1 ):
17+ confocal_notes = pd .read_csv (cf_raw_path / 'notes.csv' )
18+ capture_name = confocal_notes .loc [confocal_notes ['note' ] == img_name , 'capture names' ].values [0 ]
19+ imgdir_path = cf_raw_path / f"{ capture_name } .imgdir"
20+ if not imgdir_path .exists ():
21+ imgdir_path = list (cf_raw_path .glob (f"*{ capture_name } *.imgdir" ))[0 ]
22+
23+ sample_npy = list (imgdir_path .glob ("ImageData_*.npy" ))[0 ]
24+ shape = np .load (sample_npy , mmap_mode = 'r' ).shape # (Z, Y, X)
25+
26+ yaml_path = imgdir_path / 'StagePositionData.yaml'
27+ with open (yaml_path , 'r' ) as f :
28+ stage_data = yaml .safe_load (f )
29+
30+ coords = np .array (stage_data ['StructArrayValues' ]).reshape (- 1 , 3 )
31+ step_x = np .abs (np .diff (coords [:, 0 ]))
32+ step_x = np .median (step_x [step_x > 1.0 ])
33+
34+ phys_x = step_x / (shape [2 ] * (1 - overlap ))
35+
36+ return {
37+ 'sizeZ' : shape [0 ],
38+ 'sizeY' : shape [1 ],
39+ 'sizeX' : shape [2 ],
40+ 'sizeC' : 1 , # Confocal captures are usually single channel per dir
41+ 'physical_pixel_size_x' : phys_x ,
42+ 'physical_pixel_size_y' : phys_x , # Typically square
43+ 'physical_pixel_size_z' : 1.0 # Placeholder if not in YAML
44+ }
45+
46+ # def get_confocal_image_sizes(img_name, cf_raw_path):
47+ # sld_path = list(cf_raw_path.glob('*.sldy'))[0]
48+ # r = Reader(str(sld_path))
49+ # confocal_notes = pd.read_csv(cf_raw_path / 'notes.csv')
50+ # capture_name = confocal_notes.loc[confocal_notes['note']==img_name,'capture names'].values[0]
51+ # img_reader = None
52+ # for i in range(len(r._images)):
53+ # if r._images[i].image_directory.stem==capture_name:
54+ # img_reader = r._images[i]
55+ # break
56+ # if img_reader is None:
57+ # raise ValueError(f"Could not find capture name {capture_name} in confocal sldy file")
58+
59+ # reader_attrs = list(img_reader.__dict__.keys())
60+ # size_attrs = [attr for attr in reader_attrs if 'size' in attr]
61+ # size_attrs = {attr: getattr(img_reader, attr) for attr in size_attrs}
62+ # return size_attrs
63+
64+ def generate_confocal_sdata (zarr_path , raw_confocal_path = None ):
65+ cf_name = zarr_path .stem
66+ cf_dt = create_datatree_from_zarr (zarr_path , chan_name = cf_name )
67+
68+ cf_sdata = sd .SpatialData (
69+ images = {cf_name : cf_dt }
70+ )
71+
72+ if raw_confocal_path :
73+ # cf_sizes = get_confocal_image_sizes(cf_name, raw_confocal_path)
74+ cf_sizes = get_confocal_image_sizes (cf_name , raw_confocal_path )
75+ cf_sdata [cf_name ].attrs .update (cf_sizes )
76+ if not cf_sizes ['physical_pixel_size_x' ]== cf_sizes ['physical_pixel_size_y' ]:
77+ raise ValueError (f"Confocal pixel sizes in X and Y do not match for { cf_name } !" )
78+ else :
79+ cf_sdata = add_micron_coord_sys (cf_sdata , pixel_size = cf_sizes ['physical_pixel_size_x' ])
80+
81+ return cf_sdata
82+
83+ def create_datatree_from_zarr (zarr_path , chan_name = 'chan' ):
84+ root = zarr .open_group (zarr_path , mode = 'r' )
85+ data_tree_obj = xr .DataTree ()
86+
87+ for scale_level in sorted (list (root .keys ())):
88+ # Load the image data at this scale level
89+ level_array = da .from_zarr (str (zarr_path / scale_level ))
90+ level_array = np .expand_dims (level_array , axis = 0 ) # Add c dimension
91+ # Convert to xarray DataArray
92+ data_array = xr .DataArray (
93+ level_array ,
94+ dims = ['c' , 'z' , 'y' , 'x' ]
95+ )
96+ parsed_array = Image3DModel .parse (
97+ data_array ,
98+ dims = ['c' , 'z' , 'y' , 'x' ],
99+ c_coords = chan_name ,
100+ chunks = 'auto' ,
101+ )
102+ scale_key = f'scale{ scale_level } '
103+ data_tree_obj [scale_key ] = xr .Dataset ({'image' : parsed_array })
104+ # Set up scale transformation for non-zero scales
105+ if scale_key != 'scale0' :
106+ scale_factors = np .array (data_tree_obj [f'scale0' ].image .shape ) / np .array (data_tree_obj [scale_key ].image .shape )
107+ scale_transform = Scale (scale_factors , axes = data_tree_obj [scale_key ].image .dims )
108+ sequence = Sequence ([scale_transform , Identity ()])
109+ set_transformation (data_tree_obj [scale_key ].image , sequence , to_coordinate_system = "global" )
110+ else :
111+ set_transformation (data_tree_obj [scale_key ].image , Identity (), to_coordinate_system = "global" )
112+ return data_tree_obj
113+
114+
115+
116+ # Coped from capsule 4 to keep track of overlap blending code
117+ def generate_fused_confocal_images (data_asset , overlap = 0.1 , img_layers = 6 ):
118+ notes = pd .read_csv (os .path .join (data_asset , 'notes.csv' ))
119+ notes = notes [notes ['note' ]!= 'qc' ].reset_index (drop = True )
120+ today_str = datetime .now ().strftime ('%Y-%m-%d_%H-%M-%S' )
121+ processed_dir = os .path .join (data_asset .replace ('/data/' ,'/scratch/' )+ '_processed_' + today_str )
122+ for idx , row in notes .iterrows ():
123+ image_dir = os .path .join (data_asset ,[d for d in os .listdir (data_asset ) if d .endswith ('.dir' )][0 ] ,row ['capture names' ]+ '.imgdir' )
124+ zarr_filename = os .path .join (processed_dir , row ['note' ] + '.zarr' )
125+ tif_filename = zarr_filename .replace ('.zarr' ,'.tif' )
126+ if os .path .exists (zarr_filename ):
127+ print ('skipping' , data_asset , row ['note' ])
128+ continue
129+
130+ try :
131+ yaml_file = os .path .join (image_dir ,'StagePositionData.yaml' )
132+ StagePositionData = open_yaml (yaml_file )
133+ positions = (np .array (StagePositionData ['StructArrayValues' ])/ 100 ).astype (int )
134+ except :
135+ print ('no position data for' , data_asset , row ['note' ])
136+ continue
137+ for p in range (3 ):
138+ positions [p ::3 ] = rankdata (- positions [p ::3 ], method = 'dense' )- 1
139+
140+ locs = positions .reshape (- 1 ,3 )[:,:- 1 ][:,::- 1 ].astype (int )
141+ n_rows = np .max (locs [:,0 ])+ 1
142+ n_cols = np .max (locs [:,1 ])+ 1
143+
144+ if n_rows * n_cols == 1 :
145+ print ('no fusion needed for' , data_asset , row ['note' ])
146+ file = os .path .join (image_dir , [f for f in os .listdir (image_dir ) if f .endswith ('.npy' ) and f .startswith ('ImageData' )][0 ])
147+ image = np .load (file )
148+
149+ else :
150+ files = [f for f in os .listdir (image_dir ) if f .endswith ('.npy' ) and f .startswith ('ImageData' )]
151+ files = np .sort (files )
152+ image_ = np .load (os .path .join (image_dir , files [0 ]))
153+ n_tiles = len (locs )
154+ x_size = image_ .shape [1 ]
155+ y_size = image_ .shape [2 ]
156+ z_size = image_ .shape [0 ]
157+ image = np .zeros ((z_size ,int (x_size * (n_rows - overlap * (n_rows - 1 ))), int (y_size * (n_cols - overlap * (n_cols - 1 )))),dtype = np .uint16 )
158+ print ('fusing' , data_asset , row ['note' ])
159+ for ind_tile in range (n_tiles ):
160+ tile_ = np .load (os .path .join (image_dir , files [ind_tile ]))
161+ for z in tqdm (range (z_size ), desc = f'Fusing tile { ind_tile + 1 } /{ n_tiles } ' ):
162+
163+ tile = tile_ [z ]
164+ if locs [ind_tile ][0 ] == 0 :
165+ x_start = 0
166+ x_end = x_size
167+ else :
168+ x_start = int (x_size * (locs [ind_tile ][0 ]* (1 - overlap )))
169+ x_end = x_start + x_size
170+
171+
172+ if locs [ind_tile ][0 ] < np .max (locs ,axis = 0 )[0 ]:
173+ tile [- int (overlap * x_size ):, :] = tile [- int (overlap * x_size ):, :]* (1 - sigmoid_vector (int (overlap * x_size ), y_size ))
174+
175+ if locs [ind_tile ][0 ] > 0 :
176+ tile [:int (overlap * x_size ), :] = tile [:int (overlap * x_size ), :]* sigmoid_vector (int (overlap * x_size ), y_size )
177+
178+ if locs [ind_tile ][1 ] == 0 :
179+ y_start = 0
180+ y_end = y_size
181+ else :
182+ y_start = int (y_size * (locs [ind_tile ][1 ]* (1 - overlap )))
183+ y_end = y_start + y_size
184+
185+
186+ if locs [ind_tile ][1 ] < np .max (locs ,axis = 0 )[1 ]:
187+ tile [:, - int (overlap * y_size ):] = tile [:, - int (overlap * y_size ):]* (1 - sigmoid_vector (int (overlap * y_size ),x_size ).T )
188+
189+ if locs [ind_tile ][1 ] > 0 :
190+ tile [:, :int (overlap * y_size )] = tile [:, :int (overlap * y_size )]* sigmoid_vector (int (overlap * y_size ),x_size ).T
191+
192+ image [z ,x_start :x_end , y_start :y_end ] += tile
193+ os .makedirs (processed_dir , exist_ok = True )
194+ tiff .imwrite (tif_filename , image )
195+ store = zarr .storage .LocalStore (zarr_filename )
196+ root = zarr .group (store = store , zarr_format = 2 )
197+
198+ # Define a scaler for creating the image pyramid
199+ scaler = Scaler (method = 'nearest' , max_layer = img_layers ) # Create 4 levels in the pyramid
200+ # Write the image data with pyramid
201+ write_image (image , root , scaler = scaler , axes = 'zyx' )
0 commit comments