.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "auto_examples/Interactive/plot_spectra_roi_inspector.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_spectra_roi_inspector.py: ROI-to-spectrum inspector for a 3-D EDS hyperspectral dataset. ============================================================== A synthetic ``(256, 256, 300)`` EDS datacube — one 300-channel spectrum per scan position. Four rectangular ROIs overlay the total-counts image (HAADF proxy). Entering an ROI **sums all spectra within the rectangle** (spatial sum over every scan position in the box) and displays the result in the top-right panel. Draggable coloured range widgets on the spectrum define the integration window for each element; each bar height is the **channel sum of the ROI spectrum within that window**. **Interaction** * **Move cursor inside an ROI** — spatially sums the spectra of all scan positions inside the box; updates the line plot and bars live. * **Drag an ROI rectangle** — repositions the ROI on the image. * **Release drag** — recomputes the spatial sum spectrum for the new position. * **Drag a coloured range widget** on the spectrum — adjusts the integration window for that element; bar heights update on every drag frame. .. GENERATED FROM PYTHON SOURCE LINES 25-266 .. raw:: html
.. raw:: html .. code-block:: Python import numpy as np import anyplotlib as apl # ── synthetic 3-D hyperspectral datacube ────────────────────────────────────── # Shape: (NY, NX, NC). dataset[y, x, :] is the 300-channel EDS spectrum at # scan position (x, y). Each pixel is an independent Poisson draw from the # expected spectrum for its phase. NY, NX, NC = 256, 256, 300 ENERGY = np.linspace(0.1, 3.0, NC) # keV EDS_ELEMENTS = ["O", "Fe", "Al", "Si"] _EDS_EV = [0.525, 0.710, 1.487, 1.740] # characteristic keV _EDS_WIN = [(0.45, 0.61), (0.64, 0.80), (1.40, 1.58), (1.65, 1.83)] _EDS_SIGMA = 0.025 _EDS_COLORS = ["#ff8a65", "#ba68c8", "#4fc3f7", "#aed581"] _PEAKS = np.array([ np.exp(-0.5 * ((ENERGY - ev) / _EDS_SIGMA) ** 2) for ev in _EDS_EV ]) # shape (4, NC) # Per-phase element weight vectors [O, Fe, Al, Si] and expected total # counts per pixel (determines peak-to-background ratio and brightness). _PHASE_DEFS = [ dict(weights=[0.10, 0.05, 0.65, 0.20], counts=80), # 0 Matrix dict(weights=[0.05, 0.08, 0.12, 0.75], counts=200), # 1 Precipitate A dict(weights=[0.12, 0.60, 0.18, 0.10], counts=150), # 2 Precipitate B dict(weights=[0.62, 0.12, 0.18, 0.08], counts=110), # 3 Grain Boundary ] def _expected_spectrum(phase_idx: int) -> np.ndarray: p = _PHASE_DEFS[phase_idx] bkg = 3.0 * np.exp(-ENERGY / 0.8) spec = bkg + (_PEAKS * np.array(p["weights"])[:, None]).sum(axis=0) * p["counts"] return np.clip(spec, 0, None).astype(np.float64) def _make_dataset(rng: np.random.Generator) -> tuple[np.ndarray, np.ndarray]: phases = np.zeros((NY, NX), dtype=np.int8) # 0 = Matrix # Precipitate A (Si-rich) — cluster in top-left quadrant for cx, cy, r in [(60, 60, 30), (75, 50, 22), (45, 75, 20)]: ys, xs = np.ogrid[:NY, :NX] phases[(xs - cx) ** 2 + (ys - cy) ** 2 < r ** 2] = 1 # Precipitate B (Fe-rich) — cluster in bottom-right quadrant for cx, cy, r in [(195, 195, 27), (180, 210, 20), (210, 180, 17)]: ys, xs = np.ogrid[:NY, :NX] phases[(xs - cx) ** 2 + (ys - cy) ** 2 < r ** 2] = 2 # Grain boundary — thin horizontal band phases[120:135, :] = 3 dataset = np.empty((NY, NX, NC), dtype=np.float32) flat = dataset.reshape(-1, NC) phases_flat = phases.ravel() for pidx, pdef in enumerate(_PHASE_DEFS): sel = phases_flat == pidx n = int(sel.sum()) if n == 0: continue lam = _expected_spectrum(pidx) flat[sel] = rng.poisson(lam, size=(n, NC)).astype(np.float32) return dataset, phases rng = np.random.default_rng(99) dataset, _phase_map = _make_dataset(rng) # Total-counts image used as the HAADF-proxy display image _display_img = dataset.sum(axis=2) # ── ROI definitions (r0, r1, c0, c1) in scan-pixel coordinates ──────────────── ROIS: dict[str, tuple[int, int, int, int]] = { "Matrix": ( 25, 100, 155, 230), "Precipitate A": ( 25, 100, 25, 100), "Precipitate B": (155, 230, 155, 230), "Grain Boundary": (115, 140, 25, 230), } _ROI_COLORS: dict[str, str] = { "Matrix": "#4fc3f7", "Precipitate A": "#aed581", "Precipitate B": "#ff8a65", "Grain Boundary": "#ba68c8", } def _sum_spectrum(r0: int, r1: int, c0: int, c1: int) -> np.ndarray: """Spatial sum of all spectra within the ROI box.""" r0 = max(0, min(NY - 1, r0)); r1 = max(1, min(NY, r1)) c0 = max(0, min(NX - 1, c0)); c1 = max(1, min(NX, c1)) return dataset[r0:r1, c0:c1, :].sum(axis=(0, 1)) def _roi_at(x: float, y: float) -> str | None: for name, (r0, r1, c0, c1) in ROIS.items(): if c0 <= x <= c1 and r0 <= y <= r1: return name return None # ── layout ───────────────────────────────────────────────────────────────────── fig = apl.Figure(figsize=(1100, 560)) gs = apl.GridSpec(2, 2, width_ratios=[1, 1], height_ratios=[1, 1]) ax_img = fig.add_subplot(gs[:, 0]) # total-counts image — left column ax_spec = fig.add_subplot(gs[0, 1]) # ROI sum spectrum — top right ax_bar = fig.add_subplot(gs[1, 1]) # element bar chart — bottom right img_plot = ax_img.imshow(_display_img, cmap="gray") _init_spec = _sum_spectrum(*ROIS["Matrix"]).astype(np.float32) spec_plot = ax_spec.plot(_init_spec, axes=[ENERGY], color=_ROI_COLORS["Matrix"], linewidth=1.5, units="keV", y_units="counts") bar_plot = ax_bar.bar(EDS_ELEMENTS, [0.0] * 4) # ── ROI rectangle overlays on the image ─────────────────────────────────────── _roi_widgets: dict[str, object] = {} for roi_name, (r0, r1, c0, c1) in ROIS.items(): w = img_plot.add_widget( "rectangle", x=float(c0), y=float(r0), w=float(c1 - c0), h=float(r1 - r0), color=_ROI_COLORS[roi_name], ) _roi_widgets[roi_name] = w status_label = img_plot.add_widget( "label", x=4, y=248, text="Move cursor into an ROI", color="#ffffff", fontsize=10, ) # ── adjustable range widgets on the spectrum ─────────────────────────────────── range_widgets: dict[str, object] = {} for elem, (lo, hi), color in zip(EDS_ELEMENTS, _EDS_WIN, _EDS_COLORS): range_widgets[elem] = spec_plot.add_range_widget(lo, hi, color=color) _current_spectrum: list[np.ndarray] = [_init_spec.copy()] def _channel_sum(x0: float, x1: float) -> float: """Sum of ROI spectrum counts within the energy window [x0, x1].""" mask = (ENERGY >= x0) & (ENERGY <= x1) return float(_current_spectrum[0][mask].sum()) if mask.any() else 0.0 def _update_bars() -> None: heights = np.array([ _channel_sum(range_widgets[e].x0, range_widgets[e].x1) for e in EDS_ELEMENTS ]) max_h = heights.max() or 1.0 bar_plot.set_data((heights / max_h).tolist()) for _rw in range_widgets.values(): _rw.add_event_handler(lambda event: _update_bars(), "pointer_move") _rw.add_event_handler(lambda event: _update_bars(), "pointer_up") _update_bars() # ── update helper ────────────────────────────────────────────────────────────── _current_roi: list[str | None] = [None] _roi_dragging = False def _update_for_roi(roi_name: str) -> None: _current_roi[0] = roi_name r0, r1, c0, c1 = ROIS[roi_name] _current_spectrum[0] = _sum_spectrum(r0, r1, c0, c1).astype(np.float32) spec_plot.set_data(_current_spectrum[0], x_axis=ENERGY) spec_plot.set_color(_ROI_COLORS[roi_name]) _update_bars() n_pixels = (r1 - r0) * (c1 - c0) status_label.set(text=f"ROI: {roi_name} ({n_pixels} px)") # ── event handlers ───────────────────────────────────────────────────────────── def _on_move(event) -> None: if _roi_dragging or event.xdata is None or event.ydata is None: return roi_name = _roi_at(event.xdata, event.ydata) if roi_name is None or roi_name == _current_roi[0]: return _update_for_roi(roi_name) def _on_enter(event) -> None: status_label.set(text="Move cursor into an ROI") def _on_leave(event) -> None: status_label.set(text="Move cursor over image to inspect") _current_roi[0] = None img_plot.add_event_handler(_on_move, "pointer_move") img_plot.add_event_handler(_on_enter, "pointer_enter") img_plot.add_event_handler(_on_leave, "pointer_leave") for roi_name, widget in _roi_widgets.items(): def _make_drag_handler(): def _on_drag(event) -> None: global _roi_dragging _roi_dragging = True return _on_drag def _make_release_handler(name, wgt): def _on_release(event) -> None: global _roi_dragging _roi_dragging = False x, y, w, h = wgt.x, wgt.y, wgt.w, wgt.h ROIS[name] = (int(y), int(y + h), int(x), int(x + w)) _update_for_roi(name) return _on_release widget.add_event_handler(_make_drag_handler(), "pointer_move") widget.add_event_handler(_make_release_handler(roi_name, widget), "pointer_up") fig.set_help( "Move cursor inside an ROI: spatial sum spectrum + bars\n" "Drag ROI rectangle: repositions ROI; release recomputes\n" "Drag a coloured range widget: adjust element integration window" ) fig # Interactive .. rst-class:: sphx-glr-timing **Total running time of the script:** (0 minutes 1.346 seconds) .. _sphx_glr_download_auto_examples_Interactive_plot_spectra_roi_inspector.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: plot_spectra_roi_inspector.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: plot_spectra_roi_inspector.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: plot_spectra_roi_inspector.zip ` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_