Skip to content

Commit 338675c

Browse files
Copilotakhanf
andauthored
Update multiotsu to use percentile range and bin width; add otsu threshold sweep QC report
Agent-Logs-Url: https://github.com/khanlab/SPIMquant/sessions/6e50e573-7300-48e8-9c59-9808da92d8f5 Co-authored-by: akhanf <11492701+akhanf@users.noreply.github.com>
1 parent 51adba5 commit 338675c

6 files changed

Lines changed: 403 additions & 14 deletions

File tree

spimquant/config/snakebids.yml

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -142,16 +142,18 @@ parse_args:
142142
- otsu+k3i2
143143
nargs: '+'
144144

145-
--seg_hist_range:
146-
help: "Range of intensities to use for histogram calculation in multiotsu segmentation. Only applicable when seg_method is otsu+k{}i{}. Specify 2 numbers, for min and max values. (default: %(default)s)"
147-
default:
148-
- 0
149-
- 1000
145+
--seg_hist_percentile_range:
146+
help: "Percentile range to use for histogram calculation in multiotsu segmentation. Only applicable when seg_method is otsu+k{}i{}. Specify 2 numbers for the low and high percentiles (default: %(default)s)"
147+
default:
148+
- 1
149+
- 99
150150
nargs: 2
151+
type: float
151152

152-
--seg_hist_bins:
153-
help: "Number of bins to use for histogram calculation in multiotsu segmentation. Only applicable when seg_method is otsu+k{}i{}. (default: %(default)s)"
154-
default: 1000
153+
--seg_hist_bin_width:
154+
help: "Bin width to use for histogram calculation in multiotsu segmentation. Only applicable when seg_method is otsu+k{}i{}. (default: %(default)s)"
155+
default: 1
156+
type: float
155157

156158

157159
--register_to_mri:

spimquant/workflow/Snakefile

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ if stain_for_reg is None:
8282

8383
stains_for_seg = list(set(config["stains_for_seg"]).intersection(set(stains)))
8484

85+
# seg methods that use multi-Otsu thresholding (otsu+k{}i{} pattern)
86+
otsu_seg_methods = [m for m in config["seg_method"] if m.startswith("otsu+")]
87+
8588
if len(stains_for_seg) == 0 or config["no_segmentation"]:
8689
do_seg = False
8790
do_coloc = False
@@ -223,6 +226,32 @@ rule all_fieldfrac_tune:
223226
),
224227

225228

229+
rule all_otsu_hist_qc:
230+
"""Target rule to generate otsu threshold sweep QC HTML reports.
231+
232+
Produces one HTML report per subject/stain/method combination for every
233+
otsu+k{}i{} segmentation method. Each report shows the multi-Otsu
234+
histogram alongside 2D crops at a sweep of threshold values so that the
235+
optimal threshold can be identified visually. Only meaningful when the
236+
active ``seg_method`` contains at least one ``otsu+k{}i{}`` entry.
237+
"""
238+
input:
239+
inputs["spim"].expand(
240+
bids(
241+
root=root,
242+
datatype="qc",
243+
stain="{stain}",
244+
desc="{desc}",
245+
suffix="otsuthreshqc.html",
246+
**inputs["spim"].wildcards,
247+
),
248+
stain=stains_for_seg,
249+
desc=otsu_seg_methods,
250+
)
251+
if (do_seg and len(otsu_seg_methods) > 0)
252+
else [],
253+
254+
226255
rule all_register:
227256
input:
228257
inputs["spim"].expand(

spimquant/workflow/rules/qc.smk

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,62 @@ specified by the ``{stain}`` wildcard.
396396
"../scripts/qc_objectstats.py"
397397

398398

399+
rule qc_otsu_threshold_sweep:
400+
"""Threshold sweep QC HTML report for multiotsu segmentation.
401+
402+
Sweeps over a range of threshold values (spanning the configurable
403+
percentile range of the bias-field corrected image) and generates 2D
404+
crops at evenly-spaced positions in the image, one figure per threshold
405+
value. The otsu histogram PNG produced by the ``multiotsu`` rule is
406+
embedded at the top of the report. The resulting HTML report can be
407+
visually assessed to select the optimal threshold before running the full
408+
segmentation pipeline.
409+
410+
Only applicable when ``seg_method`` uses the ``otsu+k{}i{}`` pattern.
411+
"""
412+
input:
413+
corrected=bids(
414+
root=work,
415+
datatype="seg",
416+
stain="{stain}",
417+
level=str(config["segmentation_level"]),
418+
desc="corrected{method}".format(method=config["correction_method"]),
419+
suffix="SPIM.ome.zarr",
420+
**inputs["spim"].wildcards,
421+
),
422+
thresholds_png=bids(
423+
root=root,
424+
datatype="seg",
425+
stain="{stain}",
426+
level=str(config["segmentation_level"]),
427+
desc="{desc}",
428+
suffix="thresholds.png",
429+
**inputs["spim"].wildcards,
430+
),
431+
output:
432+
html=bids(
433+
root=root,
434+
datatype="qc",
435+
stain="{stain}",
436+
desc="{desc}",
437+
suffix="otsuthreshqc.html",
438+
**inputs["spim"].wildcards,
439+
),
440+
threads: 4
441+
resources:
442+
mem_mb=32000,
443+
runtime=30,
444+
params:
445+
n_thresholds=10,
446+
n_crops=5,
447+
patch_size=300,
448+
level=config["segmentation_level"],
449+
hist_percentile_range=[float(x) for x in config["seg_hist_percentile_range"]],
450+
zarrnii_kwargs={"orientation": config["orientation"]},
451+
script:
452+
"../scripts/qc_otsu_threshold_sweep.py"
453+
454+
399455
rule qc_roi_summary:
400456
"""Per-ROI summary QC: top-region bar plots for a single subject.
401457

spimquant/workflow/rules/segmentation.smk

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@ rule multiotsu:
119119
**inputs["spim"].wildcards,
120120
),
121121
params:
122-
hist_bins=int(config["seg_hist_bins"]),
123-
hist_range=[int(x) for x in config["seg_hist_range"]],
122+
hist_bin_width=float(config["seg_hist_bin_width"]),
123+
hist_percentile_range=[float(x) for x in config["seg_hist_percentile_range"]],
124124
otsu_k=lambda wildcards: int(wildcards.k),
125125
otsu_threshold_index=lambda wildcards: int(wildcards.i),
126126
zarrnii_kwargs={"orientation": config["orientation"]},

spimquant/workflow/scripts/multiotsu.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import numpy as np
2+
13
from dask_setup import get_dask_client
24
from zarrnii import ZarrNii
35
from zarrnii.analysis import compute_otsu_thresholds
@@ -8,15 +10,47 @@
810
if __name__ == "__main__":
911
with get_dask_client(snakemake.config["dask_scheduler"], snakemake.threads):
1012

13+
zarrnii_kwargs = snakemake.params.zarrnii_kwargs
14+
pct_lo, pct_hi = snakemake.params.hist_percentile_range
15+
bin_width = snakemake.params.hist_bin_width
16+
17+
# load a downsampled version to estimate the percentile-based range
18+
print("estimating intensity range from downsampled image...")
19+
znimg_ds = None
20+
for ds_level in [5, 4, 3, 2, 1]:
21+
try:
22+
candidate = ZarrNii.from_ome_zarr(
23+
snakemake.input.corrected, level=ds_level, **zarrnii_kwargs
24+
)
25+
znimg_ds = candidate
26+
break
27+
except Exception:
28+
pass
29+
30+
if znimg_ds is None:
31+
znimg_ds = ZarrNii.from_ome_zarr(
32+
snakemake.input.corrected, **zarrnii_kwargs
33+
)
34+
35+
data_ds = znimg_ds.data.compute().ravel().astype(np.float32)
36+
range_lo = float(np.percentile(data_ds, pct_lo))
37+
range_hi = float(np.percentile(data_ds, pct_hi))
38+
print(
39+
f" 📊 percentile range [{pct_lo}%, {pct_hi}%]: [{range_lo:.3f}, {range_hi:.3f}]"
40+
)
41+
42+
# compute number of bins from bin width
43+
n_bins = max(2, int(np.ceil((range_hi - range_lo) / bin_width)))
44+
print(f" 📊 bins: {n_bins} (bin width: {bin_width})")
45+
1146
# we use the default level=0, since we are reading in the n4 output, which is already downsampled if level was >0
1247
znimg = ZarrNii.from_ome_zarr(
13-
snakemake.input.corrected, **snakemake.params.zarrnii_kwargs
48+
snakemake.input.corrected, **zarrnii_kwargs
1449
)
1550

16-
# first calculate histogram - using preset bins to avoid issues where bins are too large
17-
# because of high intensity outliers
51+
# calculate histogram using percentile-based range and bin-width-derived bin count
1852
(hist_counts, bin_edges) = znimg.compute_histogram(
19-
bins=snakemake.params.hist_bins, range=snakemake.params.hist_range
53+
bins=n_bins, range=[range_lo, range_hi]
2054
)
2155

2256
# get otsu thresholds (uses histogram)

0 commit comments

Comments
 (0)