Skip to content

feat(gui): Scribbler radial-subtracted side-by-side panels; cross-grid mask regrid - #641

Open
samlange04 wants to merge 4 commits into
PyAutoLabs:mainfrom
samlange04:feature/scribbler-radial-panels-regrid
Open

samlange04 wants to merge 4 commits into
PyAutoLabs:mainfrom
samlange04:feature/scribbler-radial-panels-regrid

Conversation

@samlange04

@samlange04 samlange04 commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #635 (Scribbler erase brush and refine-existing-mask). Two additions that came out of masking HST and JWST lens samples, plus a set of display fixes that a manual test of the GUI turned up (see Manual test below):

  • Radial-subtracted side-by-side display. On most galaxy-scale lenses the arcs are invisible under the deflector's light until its azimuthally-averaged radial profile is removed, so a mask drawn against the raw image either clips them or is drawn blind. A contaminant, on the other hand, is judged against the real sky. Scribbler(subtract_radial=True) shows both at once, and you can paint on either panel.
  • Cross-grid mask regrid. Multi-wavelength datasets carry one mask per waveband on grids of different pixel scale (e.g. cosmos_web_ring F150W 0.03"/419 px vs F277W 0.06"/209 px). feat(gui): Scribbler erase brush, proposal outline, multiplicative brush resize #635 lets a mask be reopened as a proposal, but only on the same grid. This adds nearest-neighbour transfer between uniform grids.
  • Display fixes (also change the feat(gui): Scribbler erase brush, proposal outline, multiplicative brush resize #635 GUI). Arcsinh colour stretch on every display, white / black brushes instead of green / red, haloed markers, a blank gutter, and window titles. These are the parts of this PR that a user of feat(gui): Scribbler erase brush, proposal outline, multiplicative brush resize #635 will notice: the brush colours and the colour scale of the single-panel window change too.

It also adds marked positions (small crosses) on the display. Every new argument defaults to the behaviour described here; stretch="linear" restores the previous colour scaling.

Scope agreed in https://github.com/orgs/PyAutoLabs/discussions/23. Companion docs PRs: PyAutoLabs/autolens_workspace#583 and PyAutoLabs/autogalaxy_workspace#254 (same branch name, stacked on autolens_workspace#579 / autogalaxy_workspace#251).

Changes

New autogalaxy/gui/display_util.py (pure numpy):

  • radial_median_subtract(array, exclude=None): subtracts the per-integer-radius median profile about the array centre. exclude drops known-bad pixels from the ring statistics, so a bright contaminant cannot stamp a dark annulus at its own radius. This is a display transform only; the mask is read back from brush positions.
  • arcsinh_stretch(array, clip_percentiles=(0.5, 99.5), vmin=None, vmax=None): maps an image onto [0, 1] through an arcsinh stretch about its median, scaled by a MAD estimate of the noise, so sky-level structure and a core thousands of sigma brighter are visible at once. vmin / vmax replace the percentile limits with data values. Falls back to the full range when the percentiles coincide (a flat image with a few outliers).
  • mask_boundary(mask): 1 px inner outline.
  • composite_panels / fold_panels: lay panels side by side with a gutter, and fold a scribble on the composite back onto one grid by OR. The gutter is NaN so imshow leaves it unpainted.
  • mask_regridded_from / mask_2d_regridded_from: nearest-neighbour transfer of a boolean mask between uniform grids by scaled (arcsec) coordinates.
    • Uses explicit floor indexing. autoarray's grid_pixel_centres_2d_slim_from truncates toward zero, which misplaces pixels on the negative edge.
    • No WCS, because workspace datasets carry none.
    • Its long-term home is autoarray.mask; it lives here while the GUI is its only caller.

Scribbler(subtract_radial=True, side_by_side=True, panel_gap=6):

  • The display becomes [radial-subtracted | as-observed], each panel stretched independently through arcsinh_stretch.
  • The proposal is outlined (black on a white halo) and the mask_overlay edge is drawn on both panels. The outline uses the no-origin= contour from adfd768, so it lines up with imshow on each panel.
  • Strokes on either panel fold onto the image's own grid, so get_scribble_masks() and mask_from() always return image-shaped arrays.

Scribbler(stretch="arcsinh") (default), single panel too:

  • Every display goes through arcsinh_stretch; vmin / vmax set the data values at its ends. stretch="linear" restores matplotlib's own scaling with norm (and norm="log"). A legacy Cmap-style object keeps its own norm.

Brushes and markers:

  • Add paints white, erase paints black (was green / red). No colour map contains either, so a stroke is never lost in the image, and the pair is told apart by every kind of colour vision. The cursor is a ring in the active brush's colour with a thin halo of the other, visible on any background and doubling as a mode indicator; brush_color= overrides it.
  • Scribbler(positions=..., position_marker_size=None): (y, x) scaled coordinates (a list or a Grid2DIrregular, e.g. from the positions GUI) are marked with a small closed cross, black on a white halo, on every panel. Default arm length scales with the image (1.5% of the shorter side, at least 3 px); the old 6 px open-centred cross was far too large on a 100 px image and left the marked pixel ambiguous. Display only; the crosses never enter the mask. positions_pixels() exposes the conversion to pixels, honouring pixel scales and origin.
  • Scribbler(title=...): the first line of the window says what is being masked and where the proposal came from (e.g. "Mask extra galaxies: F277W, proposing from F150W"). Below it the window lists the markers it is actually showing (outline = the mask being refined | black x = edge of the overlaid mask | + = marked positions) and then the keys, so a user with several windows open can tell them apart and never has to remember which marker is which.

Tests

test_autogalaxy/gui/test_display_util.py (new):

  • A pure radial profile subtracts to zero, and exclude keeps a contaminant out of the ring median.
  • arcsinh_stretch: unit interval, a 5-sigma pixel stays well clear of the floor where a linear scale puts it at 5e-4, explicit limits replace the percentiles, non-finite pixels map to 0, flat images do not divide by zero.
  • Outline width and edge handling, the composite/fold round trip, NaN gutter by default.
  • Regridding a centred disc from 0.03"/419 px to 0.06"/209 px matches the disc drawn directly on the destination grid exactly.
  • Upsampling and origin shift; pixels outside the footprint come out unmasked; the Mask2D wrapper.

Added to test_scribbler.py:

  • A stroke on the right panel lands on the same pixel as on the left; the gutter is unpainted.
  • The asymmetric-proposal outline sits on the same pixels on both panels.
  • Positions convert to the right pixels (with origin), draw one closed cross each on every panel, scale with the image or take an explicit size, and never enter the mask.
  • arcsinh is the default on single and side-by-side displays, honours vmin / vmax, stretch="linear" reproduces the old scaling, unknown stretches are rejected.
  • The brush ring and halo follow the active brush, strokes are painted white / black, an explicit brush_color is kept.
  • The window title lists exactly the markers present.

test_autogalaxy/gui: 56 passed.

Manual test

Run by hand on 2026-10-03 (this branch merged with current main), with the companion autolens_workspace scripts gui/mask_extra_galaxies_multi.py (COSMOS-Web ring, F150W then F277W), gui/mask_arcs.py and gui/mask.py (simulated simple datasets). The first pass found the defects fixed in the last commit: a linear min-to-max scale left the radial-subtracted panel blank (99% of the pixels within 1% of the range), the blue brush ring and the green / red strokes vanished against jet, the crosses were far too big, the zero-filled gutter painted jet blue as if the left image overflowed, and the white outline was lost on the brighter arcsinh sky. The second pass, with all fixes in, exercised both brushes, undo and resize, the side-by-side display, the F150W to F277W regrid with each of the apply / drawn-only / skip answers, the arc GUI with positions and overlay, the window titles, and closing a window unpainted.

Origin

Developed in the HST lens data-reduction pipeline (make_masks.py, make_arc_masks.py) and ported. PyAutoMind prompt: draft/feature/autogalaxy/scribbler_radial_panels_regrid.md.

🤖 Generated with Claude Code

samlange04 and others added 3 commits September 30, 2026 20:10
…d mask regrid

New `autogalaxy/gui/display_util.py` (pure numpy, unit-tested):

- `radial_median_subtract(array, exclude=None)`: subtract the azimuthally-averaged
  radial median profile, a display transform that lifts lensed arcs and companions
  out from under a smooth galaxy's light. `exclude` keeps a known contaminant out
  of the ring statistics so it cannot stamp a dark annulus at its own radius.
- `mask_boundary`: 1-px inner outline of a mask.
- `composite_panels` / `fold_panels`: lay panels side by side with a gutter, and
  fold a scribble drawn on the composite back onto one grid.
- `mask_regridded_from` / `mask_2d_regridded_from`: nearest-neighbour transfer of a
  boolean mask between uniform grids of different pixel scale / size by scaled
  coordinates (no WCS), so a mask drawn on one waveband can seed another. Long-term
  home is autoarray.mask; kept here while the GUI is its only caller.

`Scribbler(subtract_radial=True, side_by_side=True)` displays
[radial-subtracted | as-observed], each panel colour-scaled independently, with
any `proposal` outlined on both. Strokes on either panel fold onto the image's own
grid, so `get_scribble_masks()` / `mask_from()` always return image-shaped arrays.
The single-panel display path is unchanged.

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

`Scribbler(positions=..., position_marker_size=6)` draws each (y, x) scaled
coordinate (a list or `Grid2DIrregular`, e.g. the multiple-image positions from
the positions GUI) as four dark ticks on every panel while the mask is painted.

A cross rather than a ring: the proposal outline and the mask-overlay edge are
closed boundaries, and a third closed shape reads as one more region being
masked. The arms stop 2 px short of the centre so the marked pixel is never
covered, and they are dark because a bright marker cannot be told from the arc
flux it points at. Display only: the mask is read back from brush positions.
`positions_pixels()` exposes the (row, column) conversion, which honours the
image's pixel scales and origin.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Extends the single-panel outline-bounds test to the radial-subtracted
side-by-side display: the asymmetric proposal must be outlined over the
same pixels in the left panel and one image-width-plus-gap to the right.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ses, blank gutter, titles

Manual testing of the side-by-side Scribbler on the COSMOS-Web ring and the simulated
`simple` dataset found display defects, some of them inherited from the erase-brush PR:

- The display was scaled linearly from min to max, so a galaxy core thousands of sigma
  above the sky collapsed the whole field onto one colour and the radial-subtracted
  panel showed nothing. Every display (single panel too) is now mapped through an
  arcsinh stretch about its own sky level (`display_util.arcsinh_stretch`, MAD-scaled,
  clipped at the 0.5 / 99.5 percentiles); `vmin` / `vmax` set the data values at the
  ends of the stretch instead, and `stretch="linear"` restores the previous scaling
  (and `norm="log"`). A legacy `Cmap`-style object keeps its own norm.
- The brushes painted green and red, which vanish against a jet-coloured image and are
  not told apart by every kind of colour vision. Add now paints WHITE and erase BLACK:
  no colour map contains either. The cursor ring takes the active brush's colour with
  a thin halo of the other, so it is visible on any background and shows the mode;
  `brush_color=` overrides the ring colour.
- The position crosses had 6 px arms with an open centre regardless of image size, which
  on a 100 px image was far too large and left the marked pixel ambiguous. They are now
  closed black crosses with a white halo whose default arm scales with the image (1.5%
  of the shorter side, at least 3 px). The proposal outline gets the same black-on-white
  treatment, since plain white was lost on the brighter arcsinh sky.
- The gutter between panels was filled with 0.0 and so painted in the colour map's
  lowest colour, reading as the left image running past its own edge. `composite_panels`
  now fills the gutter with NaN so it is left unpainted.
- With several GUI windows in one session it was not obvious what each one was for.
  New `title=` goes on the first line of the window (e.g. "Mask extra galaxies: F277W,
  proposing from F150W"), and the window now lists the markers it is actually showing
  (proposal outline, black x = overlaid mask edge, + = marked positions) above the keys.

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

Copy link
Copy Markdown
Collaborator Author

Pushed 99643eb after a manual test of the GUI against current main (details now in the PR body under Manual test). Heads-up that part of it changes behaviour that #635 shipped with, so a user of the erase brush will notice:

  • Brush colours. Add now paints white and erase paints black (was green / red). On a jet-coloured image the green and red strokes and the blue brush ring were not visible, and green / red is not a pair every kind of colour vision can tell apart. White and black are in no colour map. The cursor ring is drawn in the active brush colour with a thin halo of the other, so it also shows which brush is active.
  • Colour scale. Every display, the single-panel one included, now goes through an arcsinh stretch about the sky (MAD-scaled, 0.5 / 99.5 percentile clip). The linear min-to-max scale put a galaxy core thousands of sigma above the sky into the range and crushed everything else onto one colour; on the COSMOS-Web ring the radial-subtracted panel was blank. vmin / vmax still work and set the data values at the ends of the stretch; stretch="linear" restores the old scaling and norm="log".
  • Markers. Proposal outline and position crosses are black on a white halo so they survive any background; crosses are small, closed, and scale with the image.
  • Titles. title= puts what the window is for on its first line, and the window lists the markers it is showing above the key legend.

The key legend and the workspace prose in PyAutoLabs/autolens_workspace#583 and PyAutoLabs/autogalaxy_workspace#254 say white / black now; the wave-1 docs PRs (#579 / #251) still say green / red and will be right once those two merge on top of them. If you would rather keep the green / red pair for #635 compatibility and only change the ring, say so and I will split it out.

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.

1 participant