Skip to content

Commit a57d9fd

Browse files
committed
added confocal alignment functions
1 parent d5eae1c commit a57d9fd

5 files changed

Lines changed: 537 additions & 56 deletions

File tree

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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')

src/xenium_analysis_tools/alignment/sections_alignment.py

Whitespace-only changes.

src/xenium_analysis_tools/alignment/xenium_alignment.py

Whitespace-only changes.

0 commit comments

Comments
 (0)