Skip to content

Commit 9c2cfdf

Browse files
committed
added points df to zstacks
1 parent fc3b4c2 commit 9c2cfdf

1 file changed

Lines changed: 68 additions & 53 deletions

File tree

src/xenium_analysis_tools/alignment/generate_images.py

Lines changed: 68 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import spatialdata as sd
22
from 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
44
from pathlib import Path
55
import pandas as pd
66
import 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

162161
def 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

Comments
 (0)