.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "auto_examples/Interactive/plot_segment_by_contrast.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code. .. rst-class:: sphx-glr-example-title .. _sphx_glr_auto_examples_Interactive_plot_segment_by_contrast.py: Interactive Contrast Segmentation =================================== Click on any region of the image to flood-fill all pixels of similar intensity — the union of all seeded regions is shown as a live semi-transparent overlay on the original image. **Interaction** +-----------------------------------+-----------------------------------------+ | Action | Effect | +===================================+=========================================+ | **Left-click** | Add a *positive* seed (green dot). | | | Flood-fill grows from that pixel. | +-----------------------------------+-----------------------------------------+ | **Shift + left-click** | Add a *negative* seed (red dot). | | | Subtracts that connected region from | | | the current mask. | +-----------------------------------+-----------------------------------------+ | **Hover + Delete / Backspace** | Remove the nearest seed within | | | 12 image-px of the cursor. | +-----------------------------------+-----------------------------------------+ | **+** / **=** | Increase tolerance (grow regions). | +-----------------------------------+-----------------------------------------+ | **-** | Decrease tolerance (shrink regions). | +-----------------------------------+-----------------------------------------+ | **c** (while focused) | Clear all seeds and reset mask. | +-----------------------------------+-----------------------------------------+ The current boolean mask numpy array is always accessible as ``mask``. .. note:: Move the cursor over the plot so it receives keyboard focus before pressing keys. The tolerance is shown in the plot title. .. GENERATED FROM PYTHON SOURCE LINES 37-238 .. raw:: html
.. code-block:: Python import numpy as np import anyplotlib as vw # ── Synthetic multi-region image ────────────────────────────────────────────── # Five Gaussian blobs at different intensity levels on a smooth background, # plus mild Poisson-like noise — gives interesting connected regions to segment. N = 256 rng = np.random.default_rng(7) xx, yy = np.meshgrid(np.arange(N), np.arange(N)) def _gauss(cx, cy, sigma, amplitude): return amplitude * np.exp(-((xx - cx)**2 + (yy - cy)**2) / (2 * sigma**2)) image = ( _gauss( 64, 72, 28, 0.85) # bright top-left blob + _gauss(190, 60, 22, 0.70) # mid top-right blob + _gauss(128, 128, 40, 0.55) # dim centre blob (large) + _gauss( 55, 195, 20, 0.90) # bright bottom-left blob + _gauss(200, 185, 30, 0.60) # mid bottom-right blob + 0.08 * rng.standard_normal((N, N)) # noise ) # Normalise to [0, 1] image = (image - image.min()) / (image.max() - image.min()) # ── Segmentation: pure-numpy BFS flood-fill ─────────────────────────────────── def _bfs_region(img, row: int, col: int, tol: float) -> np.ndarray: """Return a boolean mask for the connected region reachable from (row, col). Connectivity is 4-connected. A neighbour is accepted when ``|img[neighbour] - centre_value| <= tol``, where *centre_value* is the intensity of the seed pixel (fixed, not growing). """ H, W = img.shape seed_val = img[row, col] visited = np.zeros((H, W), dtype=bool) visited[row, col] = True stack = [(row, col)] while stack: r, c = stack.pop() for dr, dc in ((-1, 0), (1, 0), (0, -1), (0, 1)): nr, nc = r + dr, c + dc if 0 <= nr < H and 0 <= nc < W and not visited[nr, nc]: if abs(float(img[nr, nc]) - float(seed_val)) <= tol: visited[nr, nc] = True stack.append((nr, nc)) return visited def _compute_mask(img, pos_seeds, neg_seeds, tol): """Union of positive-seed BFS regions minus any negative-seed regions.""" if not pos_seeds: return np.zeros(img.shape, dtype=bool) combined = np.zeros(img.shape, dtype=bool) for r, c in pos_seeds: combined |= _bfs_region(img, r, c, tol) for r, c in neg_seeds: combined &= ~_bfs_region(img, r, c, tol) return combined # ── State ───────────────────────────────────────────────────────────────────── pos_seeds: list[tuple[int, int]] = [] # (row, col) neg_seeds: list[tuple[int, int]] = [] # (row, col) tolerance: float = 0.08 mask = np.zeros((N, N), dtype=bool) # exposed numpy array TOL_STEP = 0.01 TOL_MIN = 0.005 TOL_MAX = 0.40 SEED_RADIUS_PIXELS = 5 # marker radius for seed dots # ── Figure ──────────────────────────────────────────────────────────────────── fig, ax = vw.subplots(figsize=(520, 520), help="Left-click → add positive seed (grow mask)\n" "Shift + Left-click → add negative seed (shrink mask)\n" "Hover + Delete → remove nearest seed\n" "+ / - → increase / decrease tolerance\n" "c → clear all seeds") plot = ax.imshow(image) plot.set_colormap("gray") # ── Persistent marker groups ────────────────────────────────────────────────── # Create named groups once so _refresh() can update them with .set() instead of # clear_markers() + add_circles(). Placing the placeholder far off-screen means # empty groups render nothing without needing a special empty-list code path. _HIDDEN = [[-9999.0, -9999.0]] # off-screen placeholder for an empty group plot.add_circles(_HIDDEN, name="pos", facecolors="#00c853", edgecolors="#ffffff", radius=SEED_RADIUS_PIXELS) plot.add_circles(_HIDDEN, name="neg", facecolors="#b71c1c", edgecolors="#ffffff", radius=SEED_RADIUS_PIXELS) # ── Helpers: marker refresh and mask push ──────────────────────────────────── def _refresh(): """Recompute mask and push updated markers + overlay in one go. Updates the two persistent marker groups in-place (no clear → blank → add cycle) so there is no visible flicker when a seed is removed. Each group has its own fixed colour string so the JS fill_color field always receives a valid CSS colour (not a mixed list). """ global mask mask = _compute_mask(image, pos_seeds, neg_seeds, tolerance) # Update offsets for each group; fall back to off-screen placeholder when empty. pos_offsets = [(c, r) for r, c in pos_seeds] or _HIDDEN neg_offsets = [(c, r) for r, c in neg_seeds] or _HIDDEN plot.markers["circles"]["pos"].set(offsets=pos_offsets) plot.markers["circles"]["neg"].set(offsets=neg_offsets) # Transparent overlay — teal for positive mask regions. plot.set_overlay_mask(mask, color="#00e5ff", alpha=0.38) # ── Click handler ───────────────────────────────────────────────────────────── @plot.on_click def _on_click(event): """Left-click → positive seed; Shift+Left-click → negative seed.""" # img_x = column, img_y = row (image-pixel coordinates) col = int(round(float(event.img_x))) row = int(round(float(event.img_y))) # Clamp to image bounds col = max(0, min(N - 1, col)) row = max(0, min(N - 1, row)) if getattr(event, "shift_key", False): neg_seeds.append((row, col)) else: pos_seeds.append((row, col)) _refresh() # ── Key bindings ────────────────────────────────────────────────────────────── @plot.on_key('+') @plot.on_key('=') # '+' on most keyboards requires Shift; '=' is the unshifted key def _tol_up(event): """Increase tolerance → flood-fill grows to wider intensity range.""" global tolerance tolerance = min(TOL_MAX, round(tolerance + TOL_STEP, 4)) _refresh() print(f" tolerance = {tolerance:.3f}", end="\r") @plot.on_key('-') def _tol_down(event): """Decrease tolerance → flood-fill shrinks to narrower range.""" global tolerance tolerance = max(TOL_MIN, round(tolerance - TOL_STEP, 4)) _refresh() print(f" tolerance = {tolerance:.3f}", end="\r") @plot.on_key('c') def _clear(event): """Clear all seeds and reset the mask.""" pos_seeds.clear() neg_seeds.clear() _refresh() print(" seeds cleared", end="\r") @plot.on_key('Delete') @plot.on_key('Backspace') def _delete_nearest(event): """Remove the seed (positive or negative) nearest to the cursor.""" cx = float(event.img_x) cy = float(event.img_y) # img_y = row best_dist = float("inf") best_list = None best_idx = -1 for lst in (pos_seeds, neg_seeds): for i, (r, c) in enumerate(lst): d = (c - cx) ** 2 + (r - cy) ** 2 if d < best_dist: best_dist = d best_list = lst best_idx = i if best_list is not None and best_dist <= (12 ** 2): best_list.pop(best_idx) _refresh() fig .. rst-class:: sphx-glr-timing **Total running time of the script:** (0 minutes 0.604 seconds) .. _sphx_glr_download_auto_examples_Interactive_plot_segment_by_contrast.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: plot_segment_by_contrast.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: plot_segment_by_contrast.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: plot_segment_by_contrast.zip ` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_