Builds a floating-point mask from a combination of hand-drawn shapes and automatic content-driven detectors (range selection, diffuse structure, lightness, chrominance, star mask, hue-channel colour masks). The hand-drawn shapes define a region of interest; the chosen detector runs inside that region to produce the final mask. The result is pushed into SASpro as a new document and, optionally, attached as a mask layer on the source document for subsequent masked tools.
The core idea: shapes define where, detectors define what. The final mask is the pixel-wise product of a binary shape mask and a content-driven floating-point mask:
If no shapes are drawn, the shape mask is zero everywhere and the final mask is zero — use Select Entire Image to put a shape over the whole frame when you want a content-only mask. Edge Blur is applied once, after the product, as a final Gaussian smoothing stage.
The shape toolbar at the top offers three modes:
Shapes can be moved after they're drawn: click an unselected shape to select it, drag to reposition. The ellipse rotation handle is on top so it stays grabbable even when the ellipse is resized small.
Ctrl+Z — Undo the most recently added shape (removes it from the scene and invalidates the shape-mask cache).+ / = — Zoom in on the live-preview panel (when it has focus).- — Zoom out on the live-preview panel.F — Fit live-preview to window.Ctrl+Wheel — Zoom the drawing canvas (slower, finer step).Middle/Right-drag — Pan the live-preview panel.
Undo Shape and Clear Shapes both exist as explicit buttons. Undo removes just the last shape and refreshes the live preview; Clear removes every shape and refreshes.
Linear astronomical data is almost black on screen — nebula structure sits in the bottom few percent of the range, and drawing a shape around an invisible target is unpleasant. The Toggle Display Stretch button swaps the canvas background to a stretched copy of the image so you can see what you're drawing on.
The display stretch uses SASpro's stretch_color_image with a target median of 0.25 and unlinked channels when the input is RGB; for mono input, it uses a simple scale-to-target-median followed by a square-root curve. The stretch is applied once when the toggle flips on, and the view transform (zoom / centre) is preserved so toggling doesn't make the view jump.
Canvas zoom works three ways: the Zoom In, Zoom Out, and Fit to Preview buttons at the top of the dialog; Ctrl+Wheel over the canvas; and the + / − / F shortcuts in the live-preview panel. The canvas uses a cosmetic pen for shape outlines, so lines stay 2 px wide on screen at every zoom level — you don't get hair-thin lines at 800% zoom.
When Range Selection or Diffuse Structure is active, a floating Live Mask Preview window opens next to the dialog. It shows the current mask as a red translucent overlay on top of the base image, so you can see at a glance which pixels will be included (red) and which won't (clear).
The preview updates on a 120 ms debounce: moving a slider schedules a re-render for 120 ms after the last change, so dragging a slider doesn't fire a hundred renders per second. The preview runs on a downsampled copy of the image (max dimension 1000 px) for speed; the final Preview Mask button renders at full resolution.
The preview is zoomable and pannable with the same controls as the drawing canvas. It auto-opens when you switch to Range Selection or Diffuse Structure, and auto-closes when you switch to a mask type that doesn't use it (Binary, Lightness, Chrominance, Star Mask, Color).
The simplest mode. The final mask is just the rasterised shape mask — 1.0 inside any drawn polygon or ellipse, 0.0 outside. Edge Blur is still applied if non-zero, giving a feathered binary selection.
Use Binary when you want a clean geometric region (e.g. a rectangle around a galaxy for star removal, a ring around a comet nucleus) with no content-dependent fine-tuning.
Selects pixels by lightness value. Four sliders control the shape of the response curve:
Three checkboxes modify the behaviour:
m with 1 − m, so you select pixels outside the chosen band instead of inside.The sigma slider shows “σ = N px” in real pixels; the preview path scales this down by the preview downsample factor so the smoothing visually matches what the full-resolution mask will look like.
Finds coherent large-scale structure — galaxy halos, nebula emission, dust lanes — using SASpro's diffuse_structure_mask detector. Five parameters:
Two toggles:
The Quality dropdown controls the internal processing resolution:
The Auto button runs suggest_diffuse_structure_settings on the current image — it analyses the frame and chooses initial values for the five parameters that are usually a reasonable starting point for this image. The returned settings are mapped back onto the slider positions so you can still edit them.
Two component-extraction modes. Both use the shape mask as a binary gate and project a component channel through it: pixels outside shapes are 0; pixels inside shapes get the component's value at that pixel.
Chrominance requires RGB input; it falls back to zero on mono. Both modes apply Edge Blur if non-zero.
Detects stars with SEP and paints a disc for each detection into the mask. One slider plus optional Gaia augmentation:
The Trial Detect button runs SEP against the current image at the current threshold and reports the detection count without generating the full mask. The result is colour-coded:
pixstack error); raise the threshold significantly.Each detected star gets a disc of radius 1.5 · max(a, b) capped at 10 pixels, where a and b are the semi-major and semi-minor axes of SEP's elliptical fit. The cap keeps the mask from eating huge chunks of the image when a star saturates and SEP fits a giant ellipse to its spike structure.
The checkbox Augment with Gaia Local DB fills in the stars SEP missed — typically bright saturated stars (where SEP's background estimate breaks down and no detection fires) and faint catalogued stars that didn't pass the detection threshold. Requires:
The Max G spinner (6.0–20.0, default 17.0) controls how deep the catalogue query goes. 17 is a good default for most data; raise to 18–19 for very deep or narrowband imaging where the extra faint stars are worth masking.
The size of the Gaia-added discs is calibrated to this image's own PSF. SASpro takes the SEP detections already made, ranks their radii, ranks the Gaia G magnitudes in-frame, and fits a linear radius = α · G + β model by matching percentiles: the 95th-percentile SEP radius maps to the 5th-percentile (brightest) in-frame Gaia G, and the 5th-percentile SEP radius maps to the 95th-percentile (faintest) G. The fitted radius function is then clamped to [1, 15] pixels and used for every Gaia star added to the mask. If there are fewer than 15 SEP detections to fit against, a static fallback curve is used (10 px at G = 6, 2 px at G = 15, linear between).
Seven hue-based masks (Red, Orange, Yellow, Green, Cyan, Blue, Magenta). Each selects pixels whose HLS hue falls in a specific angular range on the colour wheel:
| Mask | Hue range (degrees) |
|---|---|
| Red | 0°–10° and 350°–360° (wraps across hue = 0) |
| Orange | 10°–40° |
| Yellow | 40°–70° |
| Green | 70°–170° |
| Cyan | 170°–200° |
| Blue | 200°–270° |
| Magenta | 270°–350° |
The output is a hard 0/1 mask — a pixel is either in the hue range or it isn't. Soften the edges with Edge Blur, or combine with Range Selection via the shape layer if you need chrominance-weighted selection.
Requires RGB input; falls back to zero on mono. The conversion uses OpenCV's COLOR_RGB2HLS and the resulting hue (0–180 in OpenCV's 8-bit encoding) is remapped to 0–360° before range testing.
A single slider (0–300 px) applies a final Gaussian blur to the mask after everything else. The kernel size is 2 · blur + 1 (always odd) and the sigma is OpenCV's auto-chosen value. Edge Blur is applied once, after both the shape mask and the content-driven mask have been combined and multiplied together.
Use small values (3–10 px) to soften hard polygon or ellipse boundaries; use larger values (30+) when the mask will drive a tool that benefits from a broad feather (noise reduction edge handling, blended star removal).
Preview Mask renders the full-resolution mask (not the preview-sized one) and opens a dedicated Mask Preview window. The preview supports zoom (buttons, mouse wheel, keyboard), pan (middle- or right-drag), and a Push as New Document button that opens the mask as a new SASpro view directly from the preview without closing the dialog.
OK generates the final mask (progress status shown at the bottom of the dialog), pushes it as a new document named “Mask” (or whatever name you give at the prompt), and — when launched via create_mask_and_attach() — also attaches it as a mask layer on the source document with name “Mask”, no inversion, full opacity, affect-mode, visible. The attached layer becomes the document's active mask, so the next tool that respects masks picks it up automatically.
Cancel closes the dialog without generating anything.
Equations and algorithms, numbered continuing from the usage sections above.
Polygons and ellipses are rasterised with OpenCV. The dialog maintains two caches: one at the full image resolution and one at the preview resolution (max 1000 px shortest side). Both are rasterised from the same scene-coordinate shape data, scaled by:
Polygon vertices are rounded to integer pixel coordinates and passed to cv2.fillPoly with value 1. Ellipses are drawn with cv2.ellipse using the shape's scene-coordinate centre, half-width and half-height each scaled independently (so non-uniform preview scaling is handled correctly), and the shape's rotation angle in degrees. Multiple shapes union together via consecutive fills into the same buffer. The caches invalidate when any shape is added, moved, resized, rotated, undone, or cleared.
The response curve is built from two independent linear ramps multiplied together. Rising edge:
Falling edge:
Combined response (trapezoid: zero below L−f, rising linearly through L, flat at 1 through U, falling linearly through U+f, zero above):
With screening enabled, the output is multiplied by the lightness value itself:
With invert enabled, the final mask is complemented:
The Gaussian blur is applied in-place via cv2.GaussianBlur(m, (0, 0), sigma) with the user's sigma; a zero kernel-size tells OpenCV to size the kernel automatically from sigma. In the preview path, the sigma is scaled by the preview-downsample factor so the softening looks the same as it will at full resolution.
The lightness component used by Range Selection and the Lightness mask type is the standard Rec. 601 weighted sum:
For mono input, L is simply the input image. These coefficients are the standard-definition luma weights from ITU-R BT.601 and are chosen to match human perceptual response (green is weighted highest because the eye is most sensitive to it).
The chrominance mask converts the image to the YCrCb colour space (via OpenCV's COLOR_RGB2YCrCb), extracts the Cr and Cb channels, and computes the distance of each pixel from the frame's mean (Cr, Cb) point:
Then normalise linearly to [0, 1]:
The ε term guards against a divide-by-zero on a perfectly neutral image. The result is a mask that's near zero on grey pixels and near one on strongly coloured pixels, independent of brightness — a dim red star and a bright red star get similar mask values.
The seven colour masks (Red, Orange, Yellow, Green, Cyan, Blue, Magenta) use OpenCV's HLS conversion (COLOR_RGB2HLS). OpenCV packs hue into the range 0–180 for 8-bit images; it's remapped back to 0–360° before range testing. Each colour has one or two angular intervals; a pixel is in the mask if its hue falls in any interval:
The wraparound case (shown on the right) only applies to Red, whose hue range straddles the 0° = 360° seam. All other colours have a single contiguous range and don't need the OR.
Star detection uses SEP (the Python port of SExtractor's core routines). The image is converted to mono via a channel-mean if needed, a background map is estimated with sep.Background, the background is subtracted, and sep.extract runs with the user's threshold expressed as a multiple of the global background RMS:
Before extraction, sep.set_extract_pixstack(5000000) raises the per-image active-pixel buffer from the default 300000 up to 5 million — wide-field frames with many large stars will otherwise overflow the stack and sep.extract raises an error. Each extracted object has a and b fields for the semi-major and semi-minor Gaussian-fit axes; the mask radius is:
The 10-pixel cap is important: SEP will happily fit an elliptical model to a saturated star's diffraction spikes and report a huge max(a, b) — without the cap, a single saturated star could paint out a quarter of the frame. The Trial Detect button runs the same background+extract chain and counts the result; the pixstack overflow case is caught specifically and reported in red so the user knows to raise the threshold rather than wonder why the mask isn't generating.
When the Gaia Local DB augment is enabled, the first step is to decide how big to draw each catalogued star's mask disc. SASpro doesn't know each Gaia star's SEP radius (because SEP didn't detect it), so it fits a linear radius = α · G + β model from the SEP detections already made, by rank-matching percentiles. Let rSEP be the array of radii from this frame's SEP detections and G be the array of in-frame Gaia G magnitudes:
where rhi = P95(rSEP), rlo = P5(rSEP), Glo = P5(G) (bright end), and Ghi = P95(G) (faint end). The intercept pins the fit to the bright anchor:
The final per-star radius is clamped to [1, 15] pixels:
If there are fewer than 15 SEP detections, or the percentile ranges collapse (G span under 0.5 mag, or rhi ≤ rlo), a static fallback curve is used:
with r = 10 for G ≤ 6 and r = 2 for G ≥ 15. The rank-matching approach is a pseudo-fit, not a true radius-magnitude relation — SEP's actual magnitudes aren't computed — but it gets the slope and anchor approximately right for the current image's PSF, which is what matters for visual match with the SEP-drawn discs.
The Gaia query needs the frame's RA/Dec extent. SASpro projects the four image corners through the WCS to get sky coordinates and takes the min/max of each. For declination this is straightforward; for RA, the min/max can wrap across the 0° = 360° seam if the field straddles it. SASpro detects this with the test ra_max − ra_min > 180° and, when true, passes the limits in reverse order (ra_hi = ra_max, ra_lo = ra_min) because the Local DB's find_stars_in_box expects that convention for wraparound queries.
All catalogued stars in the box are projected to pixel coordinates with world_to_pixel_values, and anything outside the [0, W) × [0, H) rectangle is dropped. For each remaining star, the centre pixel of the proposed Gaia disc is checked against the existing SEP mask; if that pixel is already masked (value > 0), the Gaia star is skipped. This 1-pixel duplicate guard is deliberately simple: SEP has an actual measurement for that star, so there's nothing to add by painting over it with the modelled radius.
The rotation handle sits 20 px above the top of the ellipse's bounding box. On mouse press, it captures three pieces of state: the ellipse centre in scene coordinates, the starting angle from centre to mouse, and the ellipse's current rotation. The starting angle is computed with atan2:
On mouse move, the current angle is recomputed from the current mouse position and the ellipse's rotation is set to:
The key design choice: this is an absolute rotation computed from the stored starting state, not a delta accumulated frame-by-frame. Frame-by-frame accumulation would drift because rounding errors would compound; capturing the start angle and start rotation once on press means the ellipse always matches the mouse angle exactly, regardless of how long or jerky the drag was. On release the state is cleared and the cursor returns to the open-hand icon.
The display stretch is applied only to the on-screen pixmap. When the toggle flips on, the dialog calls stretch_color_image(a, 0.25, linked=False, normalize=False) on RGB input — this is SASpro's standard preview stretch, targeted at a median of 0.25 with per-channel unlinked histograms. On mono input (or when the colour stretch isn't available), a simple fallback is used: compute the scale that would put the current median at 0.25, apply it, clip to [0, 1], and take the square root:
When the toggle flips, the canvas records its current view transform (scale and centre), swaps the pixmap, and restores the transform — so turning display stretch on or off doesn't make the view jump or re-fit.
The preview image is a downsampled copy, created once at dialog open:
with Dmax = 1000. If max(H, W) ≤ 1000, the preview is the full image at scale 1.0. Resampling uses cv2.INTER_AREA for downscaling (correct for shrinking, avoids moiré patterns on textured data).
Two separate shape-mask caches exist: one at full resolution for the final mask, one at preview resolution for the live overlay. Each is rasterised from the same scene-coordinate shape data but with its own (Wout, Hout). Both caches invalidate when any shape is added, moved, resized, rotated, undone, or cleared; parameter changes (sliders, checkboxes, mask-type dropdown) do not invalidate them, so moving a slider refreshes only the content-driven mask portion.
The live-preview re-render runs on a 120 ms single-shot timer: a slider change restarts the timer, so dragging a slider at any speed produces at most one render per 120 ms. The timer only fires when Range Selection or Diffuse Structure is active — other mask types either need a full-resolution computation (Star, Diffuse at Ultra) or are cheap enough that preview adds no value.
Edge Blur applies a single Gaussian smoothing to the final mask after all other processing. The kernel size is always odd:
with the sigma left for OpenCV to derive automatically from the kernel size via its internal sigma = 0.3 · ((k − 1) · 0.5 − 1) + 0.8 formula. The blur is applied to the clipped-to-[0, 1] mask output, so there's no range inflation on the edges.
fillPoly, ellipse), Gaussian blurring, colour-space conversions (COLOR_RGB2HLS, COLOR_RGB2YCrCb), and image resampling.COLOR_RGB2HLS implements the standard HLS transformation; the hue-angle slicing used by the colour masks is a direct consequence of this geometry.diffuse_structure_mask and suggest_diffuse_structure_settings live in SASpro's imageops module; see Credits in that module for its own references.