spacr.qt.wand_rescue

Rescues for a magic-wand flood that runs away.

A flood fill from one click is the fastest way to take a whole object, and it has one failure mode that matters: the object touches something bright that is not it – a debris streak, a saturated membrane seam, the rim of a well – and the flood walks out along that seam and swallows the field. The tolerance that takes the object correctly and the tolerance that escapes are often the same number, so “lower the tolerance” is not an answer: it shrinks the object as well.

Three independent rescues are offered here, in the order they run. Each catches the runaway at a different point, and each can be turned off:

  1. Directional runaway detection and trimming — trim_directional_runaway() measures flood width along scanlines extending from the seed. A sustained abrupt expansion marks a leak, and pixels beyond that position are removed. Detection at this stage enables the following two refinements.

  2. Intensity-constrained reflooding — wand_region() performs a binary search for the highest tested tolerance that does not trigger runaway detection. The resulting connected region replaces the straight-line directional cut.

  3. Gradient-based boundary refinement — taper_region_to_intensity() applies a watershed to a smoothed intensity gradient within a configurable band, moving the provisional boundary toward a nearby image edge.

Separately, cap_region_from_seed() bounds a flood that is simply too big, keeping the pixels geodesically nearest the click so the result is a bounded piece of the thing that was clicked rather than an arbitrary prefix of a scan order.

Ported from the standalone curation tool (plaque_assay_model/tools/curate_masks_qt.py), where these were built against crystal-violet plaque scans. The failure is not specific to that stain: a nucleus touching a bright fibre and a plaque touching a well rim are the same flood escaping down the same kind of seam.

Nothing here imports Qt, so the geometry is testable without a GUI.

Functions

cap_region_from_seed(→ numpy.ndarray)

Keep at most max_pixels of region, nearest the click.

flood_region(→ numpy.ndarray)

Boolean flood from (seed_x, seed_y), uncapped.

magic_wand(→ Tuple[numpy.ndarray, Dict[str, object]])

wand_region(), written into a copy of mask.

taper_region_to_intensity(→ numpy.ndarray)

Move a geometric edge onto the nearest real intensity edge.

trim_directional_runaway(→ Tuple[numpy.ndarray, ...)

Cut a flood where it suddenly widens, and say where it was cut.

wand_region(→ Tuple[numpy.ndarray, Dict[str, object]])

Flood from one click and apply whichever rescues are switched on.

Module Contents

spacr.qt.wand_rescue.cap_region_from_seed(region: numpy.ndarray, seed_yx: Tuple[int, int], max_pixels: int) → numpy.ndarray[source]

Keep at most max_pixels of region, nearest the click.

Nearest is measured through the region – breadth-first growth that may only step on flooded pixels – so the kept piece cannot jump a gap to a bright patch that merely happens to be close in a straight line. Eight-connected, because the piece being salvaged is a shape to keep whole, not a flood to grow.

Returns region unchanged when it already fits, and an empty mask when the seed is not inside it.

Parameters:
  • region – boolean mask of the flood, shape (H, W).

  • seed_yx – (row, column) of the click in pixels.

  • max_pixels – pixel budget; values below 1 are treated as 1.

spacr.qt.wand_rescue.flood_region(image: numpy.ndarray, seed_x: int, seed_y: int, tolerance: float) → numpy.ndarray[source]

Boolean flood from (seed_x, seed_y), uncapped.

A pixel joins when its distance from the seed’s value is at most tolerance – absolute difference on a grey image, Euclidean distance across channels on a colour one – and it is reachable from the seed through four-connected steps. Both rules are spacr.qt.mask_engine.magic_wand()’s, so the region this returns is the region that wand would fill given no pixel budget.

Uncapped on purpose: the runaway detector has to see how far the leak went to recognise it as one. A flood truncated at the budget looks like a large compact object, which is exactly what a leak is not.

Parameters:
  • image – 2-D greyscale image, or a colour image with channels last; pixel values are compared as float32.

  • seed_x – column of the click in pixels; a seed outside the image yields an all-False mask.

  • seed_y – row of the click in pixels.

  • tolerance – largest distance from the seed’s value a pixel may have and still join, in the image’s intensity units; negative values count as 0.

spacr.qt.wand_rescue.magic_wand(image: numpy.ndarray, mask: numpy.ndarray, seed_x: int, seed_y: int, tolerance: float, max_pixels: int = 100000, action: str = 'add', **settings) → Tuple[numpy.ndarray, Dict[str, object]][source]

wand_region(), written into a copy of mask.

Writes 255 where the region landed for action="add" and 0 for action="erase", matching spacr.qt.mask_engine.magic_wand(), and returns the report beside the new mask. A rejected flood leaves the mask untouched.

Parameters:
  • image – 2-D greyscale image, or a colour image with channels last; pixel values are compared as float32. None returns mask unchanged with a rejected report.

  • mask – 2-D mask of shape (H, W) that is copied and written into; None is returned as is with a rejected report.

  • seed_x – column of the click in pixels.

  • seed_y – row of the click in pixels.

  • tolerance – flood tolerance in the image’s intensity units, as in flood_region().

spacr.qt.wand_rescue.taper_region_to_intensity(image: numpy.ndarray, flooded_region: numpy.ndarray, provisional: numpy.ndarray, seed_yx: Tuple[int, int], sigma: float = 2.0, margin: int = 8, foreground_erode: int = 3) → numpy.ndarray[source]

Move a geometric edge onto the nearest real intensity edge.

A directional cut is a straight line and a geodesic cap is a circular arc; objects are neither. provisional is whichever of those the earlier rescues produced. Its interior, inset by foreground_erode, is marked as certainly object; the part of flooded_region that was thrown away, at least margin deep, is marked as certainly not. A watershed on the sigma-smoothed intensity gradient decides the band between them, so the final boundary follows an intensity change – up or down – instead of the cut.

The result never leaves the original flood, and always keeps the connected piece the click is in. If the discarded part is thinner than margin there is no room for a band, so its deepest quarter is used rather than giving up and leaving the straight edge.

Parameters:
  • image – greyscale image, or a colour image with channels last that is averaged to grey; its smoothed gradient drives the watershed.

  • flooded_region – boolean mask of the original, uncapped flood; the result never leaves it.

  • provisional – boolean mask produced by the earlier rescue (a straight cut or a cap); it is clipped to flooded_region and returned as is when the seed is outside it.

  • seed_yx – (row, column) of the click in pixels.

spacr.qt.wand_rescue.trim_directional_runaway(region: numpy.ndarray, seed_yx: Tuple[int, int], ratio: float = 2.0, warmup: int = 12, min_baseline: int = 8, confirm: int = 2) → Tuple[numpy.ndarray, Dict[str, int]][source]

Cut a flood where it suddenly widens, and say where it was cut.

Walking up, down, left and right from the clicked scanline, the flood’s width is a profile. An object’s own profile changes gradually; a leak into a seam appears as a step. A leak is called when confirm consecutive scanlines are all at least ratio times wider than the widest scanline established strictly before them, and everything from that scanline outward is removed.

The three guards exist because a naive step detector fires on the object itself:

  • warmup ignores the scanlines nearest the click, where a one-pixel-wide start doubling to two pixels is a ratio of 2.0 and means nothing;

  • min_baseline refuses to judge until the object has reached a real width, for the same reason;

  • confirm requires the expansion to persist, so a single noisy row cannot cut the object in half.

Parameters:
  • region – boolean mask of the flood, shape (H, W).

  • seed_yx – (row, column) of the click in pixels; the width profiles are walked outward from it, and a seed outside the mask’s bounds returns an untouched copy with no cuts.

Returns:

(trimmed, cuts). cuts maps each direction that leaked to the image coordinate the cut was made at, and is empty when nothing leaked – which is how a caller knows the flood was clean.

spacr.qt.wand_rescue.wand_region(image: numpy.ndarray, seed_x: int, seed_y: int, tolerance: float, max_pixels: int = 100000, **settings) → Tuple[numpy.ndarray, Dict[str, object]][source]

Flood from one click and apply whichever rescues are switched on.

Runs the three rescues in order – detect the runaway, replace the straight cut with an intensity border, taper what is left onto the local gradient – and then applies the pixel budget. Every step is optional and each is inert on a flood that did not run away: with no leak detected, this returns exactly flood_region()’s answer.

settings accepts the keys of RESCUE_DEFAULTS; anything missing takes the default.

Parameters:
  • image – 2-D greyscale image, or a colour image with channels last; pixel values are compared as float32.

  • seed_x – column of the click in pixels.

  • seed_y – row of the click in pixels.

  • tolerance – flood tolerance in the image’s intensity units, as in flood_region(); it is also the upper bound of the intensity-border search.

Returns:

(region, report). report names what happened – cuts (the directions that leaked), intensity_border and the refined_tolerance it settled on, tapered, capped, and the pixel counts before and after – so the caller can tell the user why the wand took what it took, and write it in the ledger.