|
1 | 1 | """ROI-cropped segmentation montage QC. |
2 | 2 |
|
3 | 3 | For each brain region in the atlas parcellation (resampled to subject space), |
4 | | -crops the SPIM image and the field-fraction mask to the region's bounding box |
5 | | -and shows the central axial slice with the field-fraction overlay. This |
6 | | -provides a detail-level view of segmentation quality within individual brain |
7 | | -regions, complementing the whole-brain overview in ``qc_segmentation_overview``. |
| 4 | +crops the SPIM image and the segmentation mask to a fixed 2D bounding box |
| 5 | +at the region centroid. This provides a detail-level view of segmentation |
| 6 | +quality within individual brain regions, complementing the whole-brain |
| 7 | +overview in ``qc_segmentation_overview``. |
8 | 8 |
|
9 | | -Voxel dimensions from the NIfTI header are used to preserve the correct |
| 9 | +Voxel dimensions from the OME-Zarr image are used to preserve the correct |
10 | 10 | physical aspect ratio in each panel. |
11 | 11 |
|
12 | 12 | This is a Snakemake script that expects the ``snakemake`` object to be |
|
18 | 18 |
|
19 | 19 | matplotlib.use("agg") |
20 | 20 | import matplotlib.pyplot as plt |
| 21 | +from zarrnii import ZarrNii, ZarrNiiAtlas |
21 | 22 | import nibabel as nib |
22 | 23 | import numpy as np |
23 | 24 | import pandas as pd |
@@ -74,28 +75,36 @@ def main(): |
74 | 75 | max_rois = snakemake.params.max_rois |
75 | 76 | n_cols = snakemake.params.n_cols |
76 | 77 |
|
77 | | - spim_nib = nib.load(snakemake.input.spim) |
78 | | - spim_data = spim_nib.get_fdata() |
79 | | - ff_data = nib.load(snakemake.input.fieldfrac).get_fdata() |
80 | | - dseg_data = nib.load(snakemake.input.dseg).get_fdata().astype(int) |
| 78 | + spim_img = ZarrNii.from_ome_zarr(snakemake.input.spim,level=snakemake.params.level, downsample_near_isotropic=True,channel_labels=[snakemake.wildcards.stain]) |
| 79 | + mask_img = ZarrNii.from_ome_zarr(snakemake.input.mask,level=0) |
| 80 | + |
| 81 | + atlas = ZarrNiiAtlas.from_files(snakemake.input.dseg_nii,snakemake.input.label_tsv) |
| 82 | + |
| 83 | + dseg_data = atlas.dseg.data.compute() |
| 84 | + |
| 85 | + #spim_data = spim_nib.get_fdata() |
| 86 | + #ff_data = nib.load(snakemake.input.fieldfrac).get_fdata() |
| 87 | + #dseg_data = nib.load(snakemake.input.dseg).get_fdata().astype(int) |
81 | 88 |
|
82 | 89 | # Voxel dimensions (mm) for physical aspect-ratio correction |
83 | | - zooms = spim_nib.header.get_zooms() |
84 | | - dx, dy = float(zooms[0]), float(zooms[1]) |
85 | | - # Axial (Z) slice: after rot90, rows=y, cols=x → aspect = dy/dx |
86 | | - aspect_axial = dy / dx |
| 90 | + #zooms = spim_nib.header.get_zooms() |
| 91 | + #dx, dy = float(zooms[0]), float(zooms[1]) |
| 92 | + ## Axial (Z) slice: after rot90, rows=y, cols=x → aspect = dy/dx |
| 93 | + #aspect_axial = dy / dx |
| 94 | + |
| 95 | + |
87 | 96 |
|
88 | 97 | # Bring all volumes to the SPIM grid |
89 | | - ff_data = _match_shape(ff_data, spim_data.shape, order=1) |
90 | | - dseg_data = _match_shape(dseg_data, spim_data.shape, order=0).astype(int) |
| 98 | + #ff_data = _match_shape(ff_data, spim_data.shape, order=1) |
| 99 | + #dseg_data = _match_shape(dseg_data, spim_data.shape, order=0).astype(int) |
91 | 100 |
|
92 | 101 | # Global normalisations |
93 | | - spim_norm = _percentile_norm(spim_data) |
| 102 | + #spim_norm = _percentile_norm(spim_data) |
94 | 103 | # Field fraction values are 0–100; normalise to 0–1 for display |
95 | | - ff_norm = np.clip(ff_data / 100.0, 0.0, 1.0) |
| 104 | + #ff_norm = np.clip(ff_data / 100.0, 0.0, 1.0) |
96 | 105 |
|
97 | 106 | # Load atlas label table |
98 | | - label_df = pd.read_csv(snakemake.input.label_tsv, sep="\t") |
| 107 | + label_df = atlas.labels_df |
99 | 108 |
|
100 | 109 | # Keep non-background labels that are present in this subject's dseg |
101 | 110 | present_ids = set(np.unique(dseg_data)) - {0} |
@@ -151,35 +160,43 @@ def main(): |
151 | 160 | label_id = int(row["index"]) |
152 | 161 | label_name = str(row.get("name", label_id)) |
153 | 162 |
|
154 | | - roi_mask = dseg_data == label_id |
155 | | - bbox = _get_bounding_box(roi_mask, pad=5) |
156 | | - if bbox is None: |
157 | | - ax.text( |
158 | | - 0.5, |
159 | | - 0.5, |
160 | | - f"{label_name}\n(empty)", |
161 | | - ha="center", |
162 | | - va="center", |
163 | | - transform=ax.transAxes, |
164 | | - fontsize=7, |
165 | | - color="gray", |
166 | | - ) |
167 | | - ax.axis("off") |
168 | | - continue |
| 163 | + |
| 164 | + #get cropped images for this label |
| 165 | + bbox_min, bbox_max = atlas.get_region_bounding_box(region_ids=label_id) |
| 166 | + center_coord = bbox_max - bbox_min |
| 167 | + spim_crop = spim_img.crop_centered(center_coord, patch_size=(256,256,1))[0] |
| 168 | + mask_crop = mask_img.crop_centered(center_coord, patch_size=(256,256,1))[0] |
| 169 | + |
| 170 | +# roi_mask = dseg_data == label_id |
| 171 | +# |
| 172 | +# bbox = _get_bounding_box(roi_mask, pad=5) |
| 173 | +# if bbox is None: |
| 174 | +# ax.text( |
| 175 | +# 0.5, |
| 176 | +# 0.5, |
| 177 | +# f"{label_name}\n(empty)", |
| 178 | +# ha="center", |
| 179 | +# va="center", |
| 180 | +# transform=ax.transAxes, |
| 181 | +# fontsize=7, |
| 182 | +# color="gray", |
| 183 | +# ) |
| 184 | +# ax.axis("off") |
| 185 | +# continue |
169 | 186 |
|
170 | 187 | # Crop to ROI bounding box |
171 | | - spim_crop = spim_norm[bbox] |
172 | | - ff_crop = ff_norm[bbox] |
| 188 | +# spim_crop = spim_norm[bbox] |
| 189 | +# ff_crop = ff_norm[bbox] |
173 | 190 |
|
174 | 191 | # Choose the Z-slice within the crop with the most field-fraction signal |
175 | | - z_best = _select_best_z_slice(ff_crop) |
| 192 | +# z_best = _select_best_z_slice(ff_crop) |
176 | 193 |
|
177 | | - spim_sl = np.rot90(spim_crop[:, :, z_best]) |
178 | | - ff_sl = np.rot90(ff_crop[:, :, z_best]) |
| 194 | + spim_sl = np.rot90(spim_crop[:, :, 0]) |
| 195 | + mask_sl = np.rot90(mask_crop[:, :, 0]) |
179 | 196 |
|
180 | 197 | ax.imshow(spim_sl, cmap="gray", vmin=0, vmax=1, aspect=aspect_axial) |
181 | | - ff_masked = np.ma.masked_where(ff_sl < 0.01, ff_sl) |
182 | | - ax.imshow(ff_masked, cmap="hot", alpha=0.6, vmin=0, vmax=1, aspect=aspect_axial) |
| 198 | + mask_masked = np.ma.masked_where(mask_sl < 0.01, mask_sl) |
| 199 | + ax.imshow(mask_masked, cmap="hot", alpha=0.6, vmin=0, vmax=1, aspect=aspect_axial) |
183 | 200 | ax.set_title(label_name, fontsize=7, pad=2) |
184 | 201 | ax.set_xticks([]) |
185 | 202 | ax.set_yticks([]) |
|
0 commit comments