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 through set_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. Pass remap_tints = TRUE when 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.