split op• Data kinds: mask → table
• Call: import fullseye as fs; fs.ledger.blob_count_interval(region: 'Any', rel_lo: 'float' = 0.05, rel_hi: 'float' = 0.15, min_prominence: 'float' = 1.0, round_tol: 'Optional[float]' = None, connectivity: 'int' = 8) -> 'dict' (to call the implementation directly, import blob2d; blob2d.blob_count_interval(region: 'Any', rel_lo: 'float' = 0.05, rel_hi: 'float' = 0.15, min_prominence: 'float' = 1.0, round_tol: 'Optional[float]' = None, connectivity: 'int' = 8) -> 'dict'; from the registry, opsblob.get("blob_count_interval"))
Counts overlapping objects. When the count cannot be determined, returns an interval instead of a number (fail-closed).
Objects inside a merged clump appear as peaks of the distance transform. When two circles overlap, a neck forms, and the shallower the neck (the smaller the peak's prominence), the less it can be decided whether it is "2 or 1". blob_seeds + blob_split pick a single h and always return a single count —— even where the overlap is too deep to decide, a plausible number comes out. This op measures the prominence of each peak, treats peaks falling in the threshold band as "ambiguous", and returns the count as an interval from lo to hi. Only when the interval has width 0 does it return count.
Peak classification (prominence p, height h, relative prominence p/h; h corresponds to the inscribed-circle radius):
• p >= min_prominence and p/h >= rel_hi → certain peak (counted)
• p >= min_prominence and rel_lo <= p/h < rel_hi → ambiguous (not counted in lo, counted in hi)
• otherwise → noise (peaks caused by grid quantisation; measured below 0.6 px on ellipses)
Why relative prominence: when two circles of equal radius r overlap with overlap ratio o (centre distance d = 2r(1 − o)), the half-width of the neck is w = sqrt(r² − (d/2)²), and p/h = 1 − sqrt(1 − (1 − o)²) —— independent of the radius. An absolute h fits only one side in an image mixing large and small bubbles (the same shape as the gap poc_cell_counting recorded). The default band 0.05–0.15 corresponds to overlap ratios 0.69–0.47.
When round_tol is given, area that the peaks cannot explain is also rejected. Above an overlap ratio of 0.7 the neck is buried in grid quantisation and only one peak remains (2 objects look like 1). So the component area is compared with Σπh², the sum of the inscribed-circle areas of the certain and ambiguous peaks, and if area > (1 + round_tol + 1/h_min) · Σπh², the upper bound is opened (hi = None). 1/h_min accounts for grid quantisation (for discs of radius 8 px or more the ratio is at most 1.09, measured 2026-10-18). round_tol corresponds to the allowed aspect ratio − 1 (an ellipse with aspect ratio a/b has area ratio a/b). Use 0.1 for round objects such as bubbles, about 0.3 for irregular ones such as cells. None assumes no shape (deep overlaps are missed).
The upper bound of the interval is not a guarantee: hi is the number of objects that showed up as peaks; objects buried without leaving a peak (such as a small bubble eaten into a large one) are not included in hi either. The upper bound is opened only when the area check catches them. When determined is False, the only certain thing is that "the count cannot be stated", and the interval is a rough guide (in a semi-synthetic test with real bubble shapes, a clump answered as undetermined had a true count exceeding hi by 1. poc_dense_bubble_count).
region : array_like
2-D foreground (> 0 is foreground).
rel_lo, rel_hi : float
Band of relative prominence. 0 < rel_lo <= rel_hi < 1.
min_prominence : float
Absolute floor of prominence [px]. >= 0. Drops peaks caused by distance-transform quantisation.
round_tol : float or None
Tolerance of the area check (see above). None skips the check.
connectivity : {4, 8}
Connectivity used for components and water levels.
dict
count (int if the interval has width 0, otherwise None), lo,
hi (None if the upper bound is open), determined, reason,
labels (partition seeded only by certain peaks, int32, power diagram of inscribed circles), labels_hi
(partition also seeded by ambiguous peaks), components (per component label / lo /
hi (-1 if open) / status / area / area_ratio),
peaks (per peak component / row / col / height /
prominence / rel_prominence / status (0 noise, 1 ambiguous, 2 certain)),
params.
>>> import numpy as np
>>> yy, xx = np.mgrid[0:80, 0:120]
>>> two = ((yy - 40) ** 2 + (xx - 40) ** 2 < 400) | ((yy - 40) ** 2 + (xx - 76) ** 2 < 400)
>>> blob_count_interval(two)["count"] # overlap ratio 0.1: determined as 2
2
>>> deep = ((yy - 40) ** 2 + (xx - 50) ** 2 < 400) | ((yy - 40) ** 2 + (xx - 66) ** 2 < 400)
>>> r = blob_count_interval(deep, round_tol=0.1) # overlap ratio 0.6: undetermined
>>> r["count"] is None, r["lo"]
(True, 1)
Provenance (public literature only): Edelsbrunner, Letscher & Zomorodian, *Discrete Comput. Geom.* 28 (2002) 511 —— persistence / Vincent & Soille, *IEEE TPAMI* 13 (1991) 583 —— watershed / Aurenhammer, *SIAM J. Comput.* 16 (1987) 78 —— power diagram (partition).
• Sample-data catalog (download URLs / licences) — 2-D uses skimage.data (BSD/public domain) plus synthetic images; 3-D lists download URLs for real data sources (Stanford, PDS, …).
• Operator provenance and references — the sources of the research/methods this op family came from.
• The canonical algorithm (author, year) and its uses are named in the family usage guide above.
• poc_dense_bubble_count — py -3.11 examples/poc_dense_bubble_count.py
table as input)—
split)blob_distance · blob_seeds · blob_split
*Provenance: blob2d.py — BLOB operator registry. This per-op note is generated by tools/opdocs.py md (do not hand-edit).*
© 2026 Kazufumi Furuse — Fullseye operator documentation. Licensed under Apache-2.0.