Skip to content

Commit 8e54ac5

Browse files
authored
Merge pull request #146 from khanlab/copilot/update-multiotsu-options
Replace fixed histogram range/bins in multiotsu with percentile-based range and bin-width; add threshold sweep QC report
2 parents e18facc + 8d4bb58 commit 8e54ac5

6 files changed

Lines changed: 397 additions & 16 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
@@ -403,6 +403,62 @@ specified by the ``{stain}`` wildcard.
403403
"../scripts/qc_objectstats.py"
404404

405405

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

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 & 6 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,45 @@
810
if __name__ == "__main__":
911
with get_dask_client(snakemake.config["dask_scheduler"], snakemake.threads):
1012

11-
# we use the default level=0, since we are reading in the n4 output, which is already downsampled if level was >0
12-
znimg = ZarrNii.from_ome_zarr(
13-
snakemake.input.corrected, **snakemake.params.zarrnii_kwargs
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}]"
1440
)
1541

16-
# first calculate histogram - using preset bins to avoid issues where bins are too large
17-
# because of high intensity outliers
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+
46+
# we use the default level=0, since we are reading in the n4 output, which is already downsampled if level was >0
47+
znimg = ZarrNii.from_ome_zarr(snakemake.input.corrected, **zarrnii_kwargs)
48+
49+
# calculate histogram using percentile-based range and bin-width-derived bin count
1850
(hist_counts, bin_edges) = znimg.compute_histogram(
19-
bins=snakemake.params.hist_bins, range=snakemake.params.hist_range
51+
bins=n_bins, range=[range_lo, range_hi]
2052
)
2153

2254
# get otsu thresholds (uses histogram)

0 commit comments

Comments
 (0)