Skip to content

Commit d489341

Browse files
Copilotakhanf
andauthored
Update overview and vessel roi_zoom to use ZarrNii
- qc_segmentation_overview.py: replace nibabel NIfTI loading with ZarrNii.from_ome_zarr() (downsample_near_isotropic=True); use get_zooms() for aspect ratio; import scipy.ndimage.zoom at top - qc.smk qc_segmentation_overview: inputs now spim (OME-Zarr) + mask.ozx at segmentation_level; add level/mask_level/zarrnii_kwargs params; increase threads/mem_mb to match histogram rule - qc.smk qc_vessels_overview: same changes, datatype='vessels' mask - qc.smk qc_vessels_roi_zoom: update to same ZarrNii pattern as qc_segmentation_roi_zoom (OME-Zarr + mask.ozx + dseg_nii); add level param; increase threads/mem_mb Agent-Logs-Url: https://github.com/khanlab/SPIMquant/sessions/ebb6ebb2-5a93-4243-ac48-35628311ecb3 Co-authored-by: akhanf <11492701+akhanf@users.noreply.github.com>
1 parent da74a64 commit d489341

2 files changed

Lines changed: 81 additions & 85 deletions

File tree

spimquant/workflow/rules/qc.smk

Lines changed: 41 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -62,29 +62,21 @@ saturation/clip fraction (percentage of voxels at the maximum bin).
6262
rule qc_segmentation_overview:
6363
"""Segmentation overview slice montage QC.
6464
65-
Displays sample slices in axial, coronal, and sagittal orientations with
66-
the segmentation field fraction overlaid on the SPIM background, and a
67-
max-intensity projection column for each orientation. Aspect ratio is
68-
corrected using voxel dimensions read from the NIfTI header. Useful for
69-
quickly identifying misregistration, segmentation artefacts, or coverage
70-
gaps.
65+
Loads SPIM data via ZarrNii (``downsample_near_isotropic=True``) and the
66+
raw binary segmentation mask at the corresponding pyramid level. Displays
67+
sample slices in axial, coronal, and sagittal orientations with the mask
68+
overlay, and a max-intensity projection column for each orientation.
69+
Aspect ratio is corrected using voxel spacings from ``ZarrNii.get_zooms()``.
7170
"""
7271
input:
73-
spim=bids(
74-
root=root,
75-
datatype="micr",
76-
stain="{stain}",
77-
level=config["registration_level"],
78-
suffix="SPIM.nii.gz",
79-
**inputs["spim"].wildcards,
80-
),
81-
fieldfrac=bids(
72+
spim=inputs["spim"].path,
73+
mask=bids(
8274
root=root,
8375
datatype="seg",
8476
stain="{stain}",
85-
level=config["registration_level"],
77+
level=config["segmentation_level"],
8678
desc="{desc}",
87-
suffix="fieldfrac.nii.gz",
79+
suffix="mask.ozx",
8880
**inputs["spim"].wildcards,
8981
),
9082
output:
@@ -96,10 +88,14 @@ gaps.
9688
suffix="segslices.png",
9789
**inputs["spim"].wildcards,
9890
),
99-
threads: 1
91+
threads: 8
10092
resources:
101-
mem_mb=8000,
102-
runtime=15,
93+
mem_mb=16000,
94+
runtime=30,
95+
params:
96+
level=config["registration_level"],
97+
mask_level=config["registration_level"] - config["segmentation_level"],
98+
zarrnii_kwargs={"orientation": config["orientation"]},
10399
script:
104100
"../scripts/qc_segmentation_overview.py"
105101

@@ -108,26 +104,18 @@ rule qc_vessels_overview:
108104
"""Vessel segmentation overview slice montage QC.
109105
110106
Identical visualisation to ``qc_segmentation_overview`` but applied to the
111-
vessel field-fraction mask. Shows whole-brain slice montages (3 orientations)
112-
and MIP with the vessel field-fraction overlay and physically correct aspect
113-
ratio.
107+
vessel binary mask. Loads data via ZarrNii with ``downsample_near_isotropic``
108+
for isotropic display and physically correct aspect ratio.
114109
"""
115110
input:
116-
spim=bids(
117-
root=root,
118-
datatype="micr",
119-
stain="{stain}",
120-
level=config["registration_level"],
121-
suffix="SPIM.nii.gz",
122-
**inputs["spim"].wildcards,
123-
),
124-
fieldfrac=bids(
111+
spim=inputs["spim"].path,
112+
mask=bids(
125113
root=root,
126114
datatype="vessels",
127115
stain="{stain}",
128-
level=config["registration_level"],
116+
level=config["segmentation_level"],
129117
desc="{desc}",
130-
suffix="fieldfrac.nii.gz",
118+
suffix="mask.ozx",
131119
**inputs["spim"].wildcards,
132120
),
133121
output:
@@ -139,10 +127,14 @@ ratio.
139127
suffix="vesselslices.png",
140128
**inputs["spim"].wildcards,
141129
),
142-
threads: 1
130+
threads: 8
143131
resources:
144-
mem_mb=8000,
145-
runtime=15,
132+
mem_mb=16000,
133+
runtime=30,
134+
params:
135+
level=config["registration_level"],
136+
mask_level=config["registration_level"] - config["segmentation_level"],
137+
zarrnii_kwargs={"orientation": config["orientation"]},
146138
script:
147139
"../scripts/qc_segmentation_overview.py"
148140

@@ -162,7 +154,7 @@ quality within individual brain regions.
162154
root=root,
163155
datatype="seg",
164156
stain="{stain}",
165-
level=config['segmentation_level'],
157+
level=config["segmentation_level"],
166158
desc="{desc}",
167159
suffix="mask.ozx",
168160
**inputs["spim"].wildcards,
@@ -200,7 +192,7 @@ quality within individual brain regions.
200192
params:
201193
max_rois=25,
202194
n_cols=5,
203-
level=config['segmentation_level'],
195+
level=config["segmentation_level"],
204196
script:
205197
"../scripts/qc_segmentation_roi_zoom.py"
206198

@@ -209,28 +201,21 @@ rule qc_vessels_roi_zoom:
209201
"""Zoomed ROI montage QC for vessel segmentation.
210202
211203
Identical to ``qc_segmentation_roi_zoom`` but applied to the vessel
212-
field-fraction mask. Crops to each atlas region's bounding box and
213-
displays the best axial slice with the vessel field-fraction overlay.
204+
binary mask. Uses ZarrNii to load full-resolution data and
205+
ZarrNiiAtlas for atlas-based ROI cropping.
214206
"""
215207
input:
216-
spim=bids(
217-
root=root,
218-
datatype="micr",
219-
stain="{stain}",
220-
level=config["registration_level"],
221-
suffix="SPIM.nii.gz",
222-
**inputs["spim"].wildcards,
223-
),
224-
fieldfrac=bids(
208+
spim=inputs["spim"].path,
209+
mask=bids(
225210
root=root,
226211
datatype="vessels",
227212
stain="{stain}",
228-
level=config["registration_level"],
213+
level=config["segmentation_level"],
229214
desc="{desc}",
230-
suffix="fieldfrac.nii.gz",
215+
suffix="mask.ozx",
231216
**inputs["spim"].wildcards,
232217
),
233-
dseg=bids(
218+
dseg_nii=bids(
234219
root=root,
235220
datatype="parc",
236221
seg="{seg}",
@@ -256,13 +241,14 @@ displays the best axial slice with the vessel field-fraction overlay.
256241
suffix="vesselroimontage.png",
257242
**inputs["spim"].wildcards,
258243
),
259-
threads: 1
244+
threads: 32
260245
resources:
261-
mem_mb=8000,
246+
mem_mb=32000,
262247
runtime=15,
263248
params:
264249
max_rois=25,
265250
n_cols=5,
251+
level=config["segmentation_level"],
266252
script:
267253
"../scripts/qc_segmentation_roi_zoom.py"
268254

spimquant/workflow/scripts/qc_segmentation_overview.py

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
"""Segmentation overview QC: slice montage with field-fraction overlay.
1+
"""Segmentation / vessel overview QC: whole-brain slice montage with mask overlay.
22
33
Generates a multi-panel figure with sample slices in three anatomical
4-
orientations (axial, coronal, sagittal), each with the segmentation
5-
field-fraction overlaid on the SPIM background image, plus a
6-
max-intensity projection (MIP) column for each orientation.
4+
orientations (axial, coronal, sagittal), each with the binary segmentation or
5+
vessel mask overlaid on the SPIM background image, plus a max-intensity
6+
projection (MIP) column for each orientation.
77
8-
Voxel dimensions from the NIfTI header are used to preserve the correct
9-
physical aspect ratio in each panel.
8+
Data are loaded via ZarrNii so that the correct physical resolution and
9+
aspect ratio are applied automatically. The SPIM is loaded with
10+
``downsample_near_isotropic=True`` to obtain near-isotropic voxels.
11+
Voxel spacings are read with ``get_zooms()`` and used to compute the correct
12+
``imshow`` aspect ratio for each panel.
1013
1114
This is a Snakemake script that expects the ``snakemake`` object to be
1215
available, which is automatically provided when executed as part of a
@@ -17,9 +20,9 @@
1720

1821
matplotlib.use("agg")
1922
import matplotlib.pyplot as plt
20-
import nibabel as nib
2123
import numpy as np
2224
from scipy.ndimage import zoom
25+
from zarrnii import ZarrNii
2326

2427

2528
def _percentile_norm(arr, pct_low=1, pct_high=99):
@@ -38,18 +41,10 @@ def _sample_slices(size, n=5):
3841
return np.linspace(start, stop - 1, n, dtype=int)
3942

4043

41-
def _match_shape(source, target_shape):
42-
"""Zoom *source* array to *target_shape* if shapes differ."""
43-
if source.shape == target_shape:
44-
return source
45-
factors = [t / s for t, s in zip(target_shape, source.shape)]
46-
return zoom(source, factors, order=1)
47-
48-
4944
def _slice_aspect(zooms, step_axis):
5045
"""Compute imshow aspect ratio for a slice through *step_axis*.
5146
52-
NIfTI data is indexed (x, y, z) and ``np.rot90`` is applied before
47+
ZarrNii data is indexed (x, y, z) and ``np.rot90`` is applied before
5348
display, so the displayed image rows and columns are:
5449
5550
- step_axis=2 (Z-slice): rot90 of data[:, :, z] → rows=y, cols=x → dy/dx
@@ -69,19 +64,34 @@ def main():
6964
desc = snakemake.wildcards.desc
7065
subject = snakemake.wildcards.subject
7166

72-
spim_nib = nib.load(snakemake.input.spim)
73-
spim_data = spim_nib.get_fdata()
74-
ff_data = nib.load(snakemake.input.fieldfrac).get_fdata()
67+
spim_img = ZarrNii.from_ome_zarr(
68+
snakemake.input.spim,
69+
level=snakemake.params.level,
70+
downsample_near_isotropic=True,
71+
channel_labels=[stain],
72+
**snakemake.params.zarrnii_kwargs,
73+
)
74+
mask_img = ZarrNii.from_ome_zarr(
75+
snakemake.input.mask,
76+
level=snakemake.params.mask_level,
77+
downsample_near_isotropic=True,
78+
**snakemake.params.zarrnii_kwargs,
79+
)
7580

7681
# Voxel dimensions (mm) for physical aspect-ratio correction
77-
zooms = spim_nib.header.get_zooms()
82+
zooms = spim_img.get_zooms()
83+
84+
spim_data = spim_img.data[0].compute() # (X, Y, Z)
85+
mask_data = mask_img.data[0].compute() # (X, Y, Z), values 0–100
7886

79-
# Bring field-fraction image to the same voxel grid as SPIM
80-
ff_data = _match_shape(ff_data, spim_data.shape)
87+
# Bring mask to the same grid as SPIM if needed
88+
if mask_data.shape != spim_data.shape:
89+
factors = [t / s for t, s in zip(spim_data.shape, mask_data.shape)]
90+
mask_data = zoom(mask_data, factors, order=1)
8191

8292
spim_norm = _percentile_norm(spim_data)
83-
# Field fraction values are 0–100; normalise to 0–1 for display
84-
ff_norm = np.clip(ff_data / 100.0, 0.0, 1.0)
93+
# Mask values are 0–100 (field-fraction percent); normalise to 0–1 for display
94+
mask_norm = np.clip(mask_data / 100.0, 0.0, 1.0)
8595

8696
n_slices = 5
8797
orient_labels = ["Axial (Z)", "Coronal (Y)", "Sagittal (X)"]
@@ -104,12 +114,12 @@ def main():
104114
idx = [slice(None)] * 3
105115
idx[ax_idx] = int(sl)
106116
spim_sl = np.rot90(spim_norm[tuple(idx)])
107-
ff_sl = np.rot90(ff_norm[tuple(idx)])
117+
mask_sl = np.rot90(mask_norm[tuple(idx)])
108118

109119
ax = axes[row, col]
110120
ax.imshow(spim_sl, cmap="gray", vmin=0, vmax=1, aspect=aspect)
111-
ff_masked = np.ma.masked_where(ff_sl < 0.01, ff_sl)
112-
ax.imshow(ff_masked, cmap="hot", alpha=0.6, vmin=0, vmax=1, aspect=aspect)
121+
mask_masked = np.ma.masked_where(mask_sl < 0.01, mask_sl)
122+
ax.imshow(mask_masked, cmap="hot", alpha=0.6, vmin=0, vmax=1, aspect=aspect)
113123
ax.set_xticks([])
114124
ax.set_yticks([])
115125
if col == 0:
@@ -119,10 +129,10 @@ def main():
119129
# MIP column (last column)
120130
ax = axes[row, n_slices]
121131
mip_spim = np.rot90(np.max(spim_norm, axis=ax_idx))
122-
mip_ff = np.rot90(np.max(ff_norm, axis=ax_idx))
132+
mip_mask = np.rot90(np.max(mask_norm, axis=ax_idx))
123133
ax.imshow(mip_spim, cmap="gray", vmin=0, vmax=1, aspect=aspect)
124-
mip_ff_masked = np.ma.masked_where(mip_ff < 0.01, mip_ff)
125-
ax.imshow(mip_ff_masked, cmap="hot", alpha=0.6, vmin=0, vmax=1, aspect=aspect)
134+
mip_masked = np.ma.masked_where(mip_mask < 0.01, mip_mask)
135+
ax.imshow(mip_masked, cmap="hot", alpha=0.6, vmin=0, vmax=1, aspect=aspect)
126136
ax.set_xticks([])
127137
ax.set_yticks([])
128138
ax.set_title("MIP", fontsize=9)

0 commit comments

Comments
 (0)