Mask creation#8
Merged
CSSFrancis merged 3 commits intomainfrom May 2, 2026
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #8 +/- ##
==========================================
+ Coverage 87.63% 87.79% +0.16%
==========================================
Files 7 7
Lines 1876 1901 +25
==========================================
+ Hits 1644 1669 +25
Misses 232 232 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR adds a contrast-segmentation interactive example and introduces a general-purpose boolean mask overlay feature for anyplotlib 2D plots, along with frontend interaction/UI improvements (mask compositing, click detection, label styling, and view-state preservation).
Changes:
- Add
Plot2D.set_overlay_mask()and associated plot-state fields to push boolean overlay masks from Python to the frontend. - Implement client-side overlay-mask decoding/compositing in
figure_esm.js, plus improved click-vs-pan detection and label styling tweaks. - Add a new interactive example (
plot_segment_by_contrast.py) demonstrating region-growing segmentation with live overlays; remove two legacy top-level smoke-test scripts.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
test_pcolormesh.py |
Removes a legacy ad-hoc smoke-test script (not pytest-based). |
test_figure.py |
Removes a legacy ad-hoc smoke-test script (not pytest-based). |
anyplotlib/figure_plots.py |
Adds overlay-mask state + Python API (set_overlay_mask) for 2D plots. |
anyplotlib/figure_esm.js |
Adds overlay compositing + click detection; tweaks UI styling; preserves view on state pushes. |
Examples/Interactive/plot_segment_by_contrast.py |
New interactive segmentation example using the overlay-mask feature. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+673
to
+677
| // Preserve the current view (zoom/pan) so Python pushes don't reset it | ||
| if (p2.state && p2.kind === '2d') { | ||
| newState.zoom = p2.state.zoom; | ||
| newState.center_x = p2.state.center_x; | ||
| newState.center_y = p2.state.center_y; |
Comment on lines
+869
to
+890
| color : str, optional | ||
| CSS hex colour for the overlay, e.g. ``"#ff4444"``. Default red. | ||
| alpha : float, optional | ||
| Opacity in [0, 1]. Default 0.4 (40 % opaque). | ||
| """ | ||
| import base64 | ||
| if mask is None: | ||
| self._state["overlay_mask_b64"] = "" | ||
| self._state["overlay_mask_color"] = color | ||
| self._state["overlay_mask_alpha"] = float(alpha) | ||
| else: | ||
| arr = np.asarray(mask) | ||
| if arr.shape != (self._state["image_height"], self._state["image_width"]): | ||
| raise ValueError( | ||
| f"mask shape {arr.shape} does not match image " | ||
| f"({self._state['image_height']} x {self._state['image_width']})" | ||
| ) | ||
| # Convert to uint8: True/non-zero → 255, False/zero → 0 | ||
| u8 = (np.asarray(arr, dtype=bool).view(np.uint8) * 255).astype(np.uint8) | ||
| self._state["overlay_mask_b64"] = base64.b64encode(u8.tobytes()).decode("ascii") | ||
| self._state["overlay_mask_color"] = color | ||
| self._state["overlay_mask_alpha"] = float(alpha) |
Comment on lines
+854
to
+857
| def set_overlay_mask(self, mask: "np.ndarray | None", | ||
| color: str = "#ff4444", | ||
| alpha: float = 0.4) -> None: | ||
| """Set (or clear) a transparent boolean mask drawn over the image. |
Comment on lines
+2503
to
+2512
| // ── Click detection: short-duration + small-movement mousedown/up ──────── | ||
| // Criteria: candidate still alive (not cleared by mousemove) AND ≤300 ms. | ||
| // We also re-check final distance as a safety net for document-level moves | ||
| // that didn't fire our mousemove guard (e.g. rapid trackpad flicks). | ||
| if(p.clickCandidate){ | ||
| const _cc=p.clickCandidate; p.clickCandidate=null; | ||
| const _dx=cmx-_cc.mx, _dy=cmy-_cc.my; | ||
| const _dist2=_dx*_dx+_dy*_dy; | ||
| const _dt=Date.now()-_cc.t; | ||
| if(_dist2<=25&&_dt<=350){ |
Comment on lines
+553
to
+557
| // Preserve the current view (zoom/pan) so Python pushes don't reset it | ||
| if (p2.state && p2.kind === '2d') { | ||
| newState.zoom = p2.state.zoom; | ||
| newState.center_x = p2.state.center_x; | ||
| newState.center_y = p2.state.center_y; |
Comment on lines
+1020
to
+1026
| const mob64=st.overlay_mask_b64||''; | ||
| if(mob64){ | ||
| const mColor=st.overlay_mask_color||'#ff4444'; | ||
| const mAlpha=st.overlay_mask_alpha!=null?st.overlay_mask_alpha:0.4; | ||
| const mKey=mob64+'|'+mColor+'|'+mAlpha; | ||
| if(!p.maskCache||p.maskCache.key!==mKey){ | ||
| // Parse hex colour → r,g,b |
Comment on lines
+2519
to
+2526
| _emitEvent(p.id,'on_click',null,{ | ||
| img_x:imgX, img_y:imgY, | ||
| phys_x:physX, phys_y:physY, | ||
| shift_key:_cc.shiftKey, | ||
| mouse_x:_cc.mx, mouse_y:_cc.my, | ||
| }); | ||
| model.save_changes(); | ||
| return; |
| raise ValueError( | ||
| f"mask shape {arr.shape} does not match image " | ||
| f"({self._state['image_height']} x {self._state['image_width']})" | ||
| ) |
Comment on lines
+1018
to
+1023
| // overlay_mask_b64: base64 uint8 bytes (0|255), same iw×ih as image. | ||
| // Rendered at overlay_mask_alpha on top of the base image without clearing. | ||
| const mob64=st.overlay_mask_b64||''; | ||
| if(mob64){ | ||
| const mColor=st.overlay_mask_color||'#ff4444'; | ||
| const mAlpha=st.overlay_mask_alpha!=null?st.overlay_mask_alpha:0.4; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request introduces a new interactive example for contrast-based image segmentation and adds a general-purpose, efficient mask overlay feature to the
anyplotlibplotting library. The mask overlay allows boolean masks to be composited as semi-transparent colored overlays directly in the browser, with Python and JavaScript APIs for setting, clearing, and rendering overlays. Several UI and usability improvements are also included, such as improved click detection and more readable status labels.New Example and Features
plot_segment_by_contrast.pyinteractive example demonstrating region-growing segmentation with live mask overlays, keyboard/mouse controls, and real-time mask updates.set_overlay_maskmethod toPlot2Dinfigure_plots.py, allowing Python code to push boolean masks (as overlays) to the frontend, with customizable color and opacity.overlay_mask_b64,overlay_mask_color, andoverlay_mask_alphafields for mask overlay support.Frontend Rendering and Usability Improvements
figure_esm.js, decoding base64 masks and blending them as colored overlays at the correct zoom/pan, without flicker or performance issues.View State Preservation