Skip to content

feat(gui): Scribbler erase brush, proposal outline, multiplicative brush resize - #635

Merged
samlange04 merged 2 commits into
PyAutoLabs:mainfrom
samlange04:feature/scribbler-erase-and-review
Sep 30, 2026
Merged

samlange04 merged 2 commits into
PyAutoLabs:mainfrom
samlange04:feature/scribbler-erase-and-review

Conversation

@samlange04

Copy link
Copy Markdown
Collaborator

Summary

Scribbler (al.Scribbler / ag.Scribbler) could only add to a mask. Its second colour segment had no meaning, so an overshooting stroke meant starting over, and a mask could never be reopened and adjusted. In sustained use on HST samples two defects also surfaced: moving the mouse outside the image axes raised TypeError inside add_patch (xdata is None there), and the brush circle stopped following the cursor on the second GUI opened in one process because the motion handler never requested a redraw. Brush resizing by a fixed pixel step made moving between 1 px detail work and thick outskirts strokes take dozens of presses.

This PR adds an erase brush, a way to refine an existing mask, multiplicative brush resizing, and fixes the defects above. All changes are backwards compatible: every existing call site (image=, cmap=, mask_overlay=, show_mask()) behaves as before.

Scope agreed in https://github.com/orgs/PyAutoLabs/discussions/23. Companion docs PRs on the same branch name in autolens_workspace and autogalaxy_workspace.

Changes

  • Segment 2 (red) is an ERASE brush. New mask_from() returns (proposal | added) & ~erased. show_mask() still returns the add segment alone.
  • New proposal= argument: an existing boolean mask (drawn earlier, or for another waveband on the same grid) is outlined in white over the image and refined, not redrawn. Shape is validated against the image.
  • = / - resize the brush multiplicatively (brush_resize_factor, default 1.4x per press, always at least 1 px) with a min_radius floor of 1 px; the radius is echoed.
  • Motion events outside the axes are ignored; the brush is redrawn with draw_idle() on the motion path.
  • Figure build and event loop are split (_build_figure() / start(), block=), and the TkAgg-only window placement is guarded, so the callbacks are unit-testable with Agg.
  • A circle centred on pixel 0 is no longer dropped by the rasteriser (if not center[0] treated 0.0 as missing).
  • Key legend printed on start and shown as the axes title.

No public API is removed or changed; mask_from(), proposal=, brush_resize_factor= and min_radius= are additive. Downstream: the workspace GUI scripts switch from show_mask() to mask_from() in the companion PRs, but show_mask() keeps working.

Testing

  • Existing tests pass (pytest): full test_autogalaxy/ suite.
  • New tests added: test_autogalaxy/gui/test_scribbler.py drives the callbacks headlessly with synthetic events. Covers rasterisation, undo, drag painting, outside-axes handling, add-minus-erase, proposal refinement, explicit-proposal override, shape validation, and multiplicative resize with floor.
  • Tested manually with workspace examples: scripts/imaging/data_preparation/gui/mask.py from the companion workspace branch, checking the brush tracks the cursor, 2 erases, and the proposal outline shows.

Not covered by tests: the interactive Tk window itself.

Related Issues

Discussion: https://github.com/orgs/PyAutoLabs/discussions/23

Developed in an HST lens data-reduction pipeline and ported. Written with AI assistance (Claude Code); all changes reviewed and tested by me.

🤖 Generated with Claude Code

…ush resize

The mask-drawing GUI gains the pieces needed to REFINE a mask rather than only
draw one from scratch:

- Segment '2' (red) is now an ERASE brush. `mask_from()` returns
  `(proposal | added) & ~erased`; `show_mask()` is unchanged (add segment only)
  so existing callers keep working.
- New `proposal=` argument: an existing boolean mask (e.g. one drawn earlier, or
  drawn for another waveband on the same grid) is outlined in white over the
  image and edited with the two brushes instead of redrawn.
- '=' / '-' resize the brush multiplicatively (`brush_resize_factor`, default
  1.4x per press, at least 1 px) with a 1 px floor (`min_radius`), and echo the
  radius, so a few presses span detail work to thick outskirts strokes.
- Motion events outside the image axes are ignored instead of raising
  `TypeError` inside `add_patch` (xdata is None there), and the brush circle is
  redrawn on the motion path so it keeps tracking the cursor on the second GUI
  opened in one process.
- The figure build and the blocking event loop are split (`_build_figure` /
  `start`, `block=` argument) so the callbacks are unit-testable with the Agg
  backend; the TkAgg-only window placement is guarded. A circle centred on
  pixel 0 is no longer dropped by the rasteriser.

Tests: test_autogalaxy/gui/test_scribbler.py drives the callbacks headlessly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…osal in mask_from; test hygiene

- Drop origin= from the proposal contour so its outline sits on the imshow
  pixels (it was flipped vertically under imshow_origin: upper); keep a
  reference as _proposal_contour and test the outline bounds.
- Validate an explicit mask_from(proposal=...) shape via _validate_proposal.
- mask_from docstring: add/erase are the first/second segments.
- Tests: close figures after each test; assert draw_idle on in-axes motion.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Jammy2211

Copy link
Copy Markdown
Collaborator

Thanks @samlange04, this is a well-scoped contribution and the headless test harness is exactly what the GUI needed. I checked out the branch, ran the new tests (8 of the 11 go red against unfixed main, so they genuinely witness the fixes) and grepped every Scribbler consumer across the workspaces, HowTo repos and the Euclid pipeline: nothing uses segment 2 or radius_increment, so the erase brush and the new defaults are non-breaking.

One real bug turned up, and rather than bounce it back I pushed the fix onto your branch (commit adfd768, maintainer edits were enabled):

  • Proposal outline was mirrored vertically and offset half a pixel. ax.contour(..., origin=_conf_imshow_origin()) does not line up with imshow: every shipped config uses imshow_origin: upper, and contour's origin="upper" puts Z[0,0] at y = N - 0.5. A proposal covering rows/cols 2..5 on a 20x20 image was outlined at y 14..18 instead of 1.5..5.5. A centred circle hides this by symmetry, an extra-galaxies mask does not. Fix: drop the origin= kwarg (with no X/Y, contour places Z[0,0] at data (0,0), which matches imshow for either origin). A test with an asymmetric proposal now pins the outline bounds, and the contour set is kept on self._proposal_contour.
  • Small tidy-ups in the same commit: mask_from(proposal=...) now validates the shape like the constructor does (via _validate_proposal()), the mask_from docstring says first/second segment for custom segment_names, the tests close their figures via an autouse fixture, and the draw_idle() motion fix has a test.

test_autogalaxy/gui is 23 passed locally and CI is green on the new head. Please pull the branch, give the refine workflow a run against a real extra-galaxies mask so you can see the outline now sits on the right pixels, and if you are happy go ahead and merge (you have write on this repo). The workspace PRs stay draft until the next PyAutoGalaxy release, see my note there.

@samlange04
samlange04 merged commit 6799199 into PyAutoLabs:main Sep 30, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants