Masks
Added in version 1.5: The image-mask system is entirely new in 1.5. A mask lets an operation affect only part of an image, with a soft (feathered) boundary.
Ground truth: src/core/masks.c,h, the mask_t type and the mask
members of struct ffit in src/core/siril.h, blend_fits_with_mask
in src/core/processing.c, and the mask editor UI in
src/gui-gtk4/masks_gui.c.
What a mask is
A mask is a single-channel alpha map the same width and height as the image it
is attached to. The type is tiny (siril.h):
typedef struct _mask_t {
uint8_t bitpix; // 8, 16 or 32
void *data; // rx*ry samples: uint8_t, uint16_t or float
} mask_t;
bitpix selects the sample depth: 8-bit (uint8_t, 0–255),
16-bit (uint16_t, 0–65535) or 32-bit (float, 0.0–1.0). The
default depth is returned by get_default_mask_bitpix. In all cases the
value is an alpha weight: 0 = the operation has no effect at that pixel, full
scale = the operation applies fully.
How a mask attaches to a fits
Two members of struct ffit carry the mask:
mask_t *mask— the mask itself, or NULL.gboolean mask_active— whether the mask is currently in effect. Toggle it throughset_mask_active(which also notifies the GUI), never by writing the field directly.
The mask is owned by the fits: free_mask releases it, and the fits's
rwlock covers it like the pixel data. Like the rest of a public fits, it is
handled under the worker locking — you do not lock it yourself from a hook.
How a mask modulates an operation
When you submit a single-image operation with mask_aware = TRUE (see
generic_image_worker) and the fits has an active mask,
the worker blends the hook's output against the original pixels through the
mask (blend_fits_with_mask). Per pixel:
out = original + alpha * (processed - original)
where alpha is the normalised mask value. The blend is implemented for all
three mask depths and both image types (DATA_USHORT and DATA_FLOAT).
Warning
If your operation should respect masks but you leave mask_aware = FALSE,
the worker installs the processed pixels without blending — a silent
wrong result. Conversely, marking an op mask_aware that has no sensible
partial-application semantics is misleading; only set it for pixel
operations where partial application is meaningful.
Writing a mask-producing feature
Mask creation and editing go through generic_mask_worker with a
mask_hook (int (*)(struct generic_mask_args *)). The existing hooks in
masks.h are your templates — for example mask_from_stars_hook,
mask_from_lum_hook, mask_from_color_hook, mask_binarize_hook,
mask_blur_hook, mask_feather_hook, mask_invert_hook — each paired
with a *_log function for the HISTORY/undo text and a small user struct
(with a leading destructor) such as mask_from_stars_data.
Set mask_creation = TRUE for a hook that creates a mask; the worker then
calls set_mask_active on completion so the new mask takes effect. The
lower-level builders (mask_create_from_stars, mask_create_from_luminance,
mask_create_from_channel, mask_thresh/mask_binarize,
mask_feather, mask_scale, mask_change_bitpix …) do the actual work
and can be reused. Masks are also reachable from commands (e.g. a
mask_from_stars command in command.c) and from user polygons via
set_poly_in_mask.
Display
The mask is shown as a colour tint overlay on the viewports. Redraws go
through gui_iface:
gui_iface.queue_redraw_mask(remap_tints)/redraw_mask_idle— request a mask redraw from any thread. Passremap_tints = TRUEwhen the mask data changed (so the tinted viewports must be recomputed); FALSE when only visibility toggled and the tints are already current.gui_iface.on_mask_state_changed— called when a mask is removed or its active state changes (the mask worker's completion idle calls this).gui_iface.update_mask_enable(state)— syncs the enable toggle button.
Undo and geometry
A mask edit on gfit (non-command) saves an undo state before running, so
the previous mask can be restored.
Geometry-changing operations must keep the mask dimensionally consistent with
the image. Resize, rotate, crop and binning in src/algos/geometry.c
transform the mask alongside the pixels (resize_mask / bin_mask /
region copy) and only free it (free_mask + fit->mask = NULL +
gui_iface.on_mask_state_changed) if that transform fails. If you add a new
geometry operation, follow the same contract: transform the mask to the new
geometry, or drop it — never leave a mask whose dimensions no longer match its
image.