Mosaic Master

Stitches multiple astrophotos into one seamless image by placing every panel on a shared celestial frame using its WCS, then blending the overlaps. Handles both standard WCS-based mosaics (the usual case) and a dedicated Seestar Mode for images from robot smart telescopes that arrive without plate solves. Includes two reprojection engines (exact SIP-aware per-pixel inverse mapping, and a fast single-homography shortcut), a two-stage brightness normalization system (sky pedestal match + overlap gain fit with sky pivot), a mixed-tone-scale detector that catches when linear and stretched panels have been mixed by mistake, and overlap-relative distance-transform seam blending.

Original work for SetiAstro Suite Pro. The entire mosaic pipeline is written from scratch by Franklin Marek: the exact SIP-aware per-pixel inverse mapping with tiled dispatch, forward-only footprint-bbox restriction, and quarter-pixel round-trip validation (so SIP non-convergence at the footprint edge is caught rather than silently miscomposited); the destination-WCS linearization decision (SIP is only valid inside its fit domain, so the mosaic canvas must be pure linear TAN while source panels keep their SIP); the pedestal coverage-preservation technique (add +1.0 before warping so the valid-pixel footprint survives Lanczos4 border fill, 12-px coverage erosion to clear interpolation ringing, strip pedestal before accumulation); the two-stage normalization (per-panel sky-level estimator with tiled sigma-clipping, then an overlap gain fit solving “reference ~ a×(source−p) + p + b” pivoted on the common sky level so gain only scales signal above sky, reduced to 16×16 block means to escape sky-noise OLS attenuation); the mixed-tone-scale detector (per-panel MAD ratio flagged against the Histogram tool's reference MAD algorithm); the panel ordering (center-first + max pairwise overlap, deliberately not union-box which would invent overlap in L-shaped layouts); the pre-blur distance-transform feathering with the raised-power sharpness control that converts a global coefficient into an overlap-relative blend ratio after accumulation; the SIP autodisable that inspects actual A/B polynomial coefficients rather than mere keyword presence; the Seestar Mode iterative pipeline with center-first base selection by star count, WCS-rough and physics-rough transform fallbacks, feathered sum-and-count accumulation, and brightness-ratio pixel rejection; the Add from View multi-pick with WCS tagging; the Reproject Mode dispatcher; the mosaic WCS merge policy; and the ASTAP+Astrometry.net plate-solve fallback for unsolved panels. The external tools (astroalign, ASTAP, Astrometry.net, photutils) are credited separately in their sections and in the final Credits box.
Workflow
Add Image or Add from View to load panels (FITS with WCS gives the cleanest results) — pick Reprojection mode (Exact — SIP-aware remap is the default and the right choice for anything with meaningful distortion) — leave Normalize images on for brightness-matched seams — Align and Create Mosaic — preview opens with Push-to-New-View button, or use Save to New View directly.

The core of the normalization pipeline: once two panels have been placed on the common frame, their overlap pixels are modelled as a linear match on the common sky level p:

\mathbf{R} \approx a \cdot (\mathbf{S} - p) + p + b

Pivoting about the sky decorrelates a and b: the pedestal match earlier in the pipeline already set p to the reference frame's sky level, so what's left between the panels is a scale factor on signal above sky plus a tiny residual offset. §15.11 explains why that pivot matters and §15.12 explains why the fit runs on 16×16 block means instead of per-pixel.

Contents
1. Dialog layout
2. Add Image / Add from View
3. Mode flags
4. Reprojection mode
5. Transform method
6. Normalize images
7. Autostretch previews only
8. Preview Selected
9. Align and Create Mosaic
10. Save to New View
11. Settings (wrench)
12. SIP auto-disable
13. Plate solving fallback
14. Seestar Mode
15. Technical details
15.1 Panel ordering
15.2 Mosaic WCS linearization
15.3 Output bounding box
15.4 Exact SIP-aware remap
15.5 Round-trip validation
15.6 Footprint-bbox restriction
15.7 Fast homography mode
15.8 Pedestal coverage preservation
15.9 Edge trim for Lanczos ringing
15.10 Sky pedestal match
15.11 Overlap gain fit with sky pivot
15.12 Block-mean reduction
15.13 Robust iteration
15.14 Mixed tone-scale detection
15.15 Seam feathering
15.16 Star alignment refinement
15.17 Polynomial warp
15.18 SIP coefficient detection
15.19 ASTAP integration
15.20 Astrometry.net integration
15.21 Seestar Mode internals

1. Dialog layout

The dialog is a single scrolling column: a top instruction block, a row of action buttons (Add Image, Add from View, Remove Selected, Preview Selected, Align and Create Mosaic, Save to New View, wrench for Settings), a row of mode checkboxes, several option controls, the loaded-panels list, status label, and a progress spinner that activates during alignment. It's modeless and stays on top so you can keep working with the view it feeds back to.

2. Add Image / Add from View

Add Image

Opens a file picker for multiple image files. Accepted formats: PNG, JPG, TIFF, FITS, FITZ, XISF, and the common DSLR raws (CR2, NEF, ARW, DNG, RAF, ORF, RW2, PEF). Each file is loaded through SASpro's shared image-I/O layer, the WCS is parsed (if present), and the panel is listed with a [WCS] tag when a usable solution was found.

Add from View

Pulls panels from any open SASpro view instead of disk. Opens a multi-select picker listing every open view that has image data, each row tagged with its size and whether it has a WCS. Buttons: All, None, and Only solved (checks only views that carry a WCS). Views already added to the current mosaic are shown but disabled, so you can't duplicate a panel accidentally.

Pulling from views instead of disk is often faster when you've just finished a batch of plate solves — the WCS already lives in memory alongside the image.

Remove Selected

Removes the selected panel from the list. If you want to clear everything, select all with Ctrl+A and remove.

3. Mode flags

Three checkboxes in a mutually-exclusive-ish row control how panels are aligned:

Force Blind Solve (ignore existing WCS)

Re-plate-solves every panel even if the FITS header already carries a WCS. Useful when you suspect the embedded solution is wrong or stale, or when you want to replace a low-quality solve with a fresh one. Panels without any WCS are solved anyway (whether this is checked or not).

Seestar Mode

Switches to the iterative robot-telescope pipeline (§14, §15.21). Panels are aligned by astroalign star matching with WCS-rough or physics-rough transform seeds, not by WCS reprojection. The output canvas grows outward from a chosen base panel. Designed specifically for ZWO Seestar S30/S50 and similar smart telescopes whose exports lack plate solves.

Disable Star Alignment (WCS placement only)

Skips the post-reprojection star-match refinement. Panels are placed by their WCS alone and blended. This is the correct choice when panels have little or no overlap but all carry a reliable plate solve (SIP-rich mosaics in particular), and it's auto-enabled by SASpro when SIP is detected on the panels (§12 explains the logic).

Seestar Mode and WCS-only placement are mutually exclusive — enabling one disables the other.

4. Reprojection mode

Pick how each panel is warped onto the mosaic canvas:

Exact — SIP-aware remap (recommended)

Per-pixel inverse mapping with full SIP support on the source panel. For every destination pixel, the mosaic WCS is used to find the (RA, Dec) that pixel represents on the sky, then the source panel's WCS inverse (with its SIP polynomial) is applied to find which source pixel to sample. Resampled with Lanczos4, tiled 512×512 for memory and progress reporting, with every mapped coordinate validated by a forward round-trip (§15.5) so SIP non-convergence at the footprint edge is caught rather than silently miscomposited. This is the right choice for any mosaic involving panels with SIP distortion solutions.

Fast — Homography (Global H)

Fits a single 3×3 homography per panel from a 9-point WCS-derived control grid, then warps with cv2.warpPerspective. Perfect for linear TAN panels (no SIP) because a homography can exactly represent the full gnomonic composition between two tangent-plane projections centred on nearby points. For panels with meaningful SIP distortion, a global homography cannot represent the per-pixel polynomial corrections and stars near the edges can double, so this mode is best reserved for small-field SIP-free panels.

5. Transform method

Only applies to the optional star-alignment refinement that runs after WCS reprojection on panels without SIP (SIP-rich panels skip refinement entirely — see §12). Four choices:

6. Normalize images

Toggles the two-stage brightness-matching pipeline. On by default, and almost always correct to leave on. The stages:

Legacy fallback: if the sky estimator fails on the reference panel (unusual — it usually only happens with extremely low star count or heavy clipping), SASpro falls back to a simple median match using the first panel's median as the target.

7. Autostretch previews only

When on, panel previews and the final mosaic preview are rendered with SASpro's shared stretch for display, while the underlying mosaic data stays linear for downstream processing. When off, previews show the raw linear values clipped to [0, 1] — useful if you want to see what the mosaic actually looks like without stretch, but most linear astronomical data appears nearly black. Default: on.

8. Preview Selected

Opens a non-modal Mosaic Preview window for the selected panel. Shows the image with autostretch (controlled by the checkbox inside the preview window itself, independent of the main dialog's autostretch toggle), zoom buttons (In / Out / Fit to Preview), click-drag panning, mouse-wheel zoom, a Reapply Stretch button, and a Push to New View button that sends the current preview array into a new SASpro view.

9. Align and Create Mosaic

The main entry point. Steps, in order:

  1. Plate-solve unsolved panels — or every panel if Force Blind Solve is on. ASTAP first (if its path is set in Preferences), falling back to Astrometry.net blind solve (§13).
  2. Order panels — by proximity to the field centre and maximum overlap with already-placed panels (§15.1).
  3. Build the mosaic WCS — take the first ordered panel's WCS, strip its SIP (destination must be linear TAN — §15.2), compute the bounding box that covers all panels (§15.3), shift CRPIX so the mosaic occupies [0, W]×[0, H].
  4. Estimate reference sky — from the first panel, used as the pedestal match target.
  5. Mixed-scale check — compute per-panel MAD, warn if the spread between panels exceeds 8× (§15.14).
  6. For each panel: apply the sky pedestal match (§15.10), add the +1.0 coverage pedestal (§15.8), reproject to the mosaic frame (exact or fast), compute the real footprint by eroding the pedestal coverage, strip the pedestal, apply the overlap gain fit if there's enough overlap (§15.11), compute distance-transform feather weights (§15.15), accumulate sum += image × weight and W += weight.
  7. Final blend — divide the accumulated sum by the weight everywhere the weight is nonzero. Pixels never covered by any panel stay at zero.
  8. Preview — open a Mosaic Preview window on the result.

A modal progress dialog tracks progress per panel across four substeps (reproject, align, match brightness, blend) and can be cancelled mid-run between panels.

10. Save to New View

Finalizes the mosaic and creates a new SASpro view containing it. The mosaic's WCS header (the exact astrometric solution for the output canvas, since every panel was reprojected into it) is attached under metadata['original_header'] so the rest of SASpro (plate viewer, re-open, FITS save, Stellar Alignment, SFCC, etc.) treats the mosaic as a plate-solved image. On Seestar Mode mosaics with no WCS, a minimal header is attached instead.

11. Settings (wrench)

Click the wrench button to open the Mosaic Settings dialog:

12. SIP auto-disable

After every panel-set change (Add Image, Add from View, re-plate-solve), SASpro inspects the panels' WCS solutions and decides whether to default to WCS-only placement:

Once you click the checkbox yourself, auto-disable never fires again that session — your choice sticks.

The reason: a per-pixel 4th-order SIP resample can't be reconciled by a single global affine refinement. On a corner panel, pushing it to match stars in one quadrant trades for mis-match in the opposite quadrant. Panels that are already well-solved by SIP are better left where WCS put them.

13. Plate solving fallback

For panels without a usable WCS (or every panel, if Force Blind Solve is on), Mosaic Master runs the two-stage plate-solve fallback:

ASTAP (first attempt)

Runs Han Kleijn's ASTAP external program as a subprocess, writing a temporary stretched normalized FITS copy for the solve. ASTAP is called blind (-r 179 -fov 0 -z 0 -wcs -sip) — no RA/Dec seed assumed, SIP distortion written out. On success, the .wcs sidecar is merged into the solved header, numeric WCS keys are coerced to float/int, and (for on-disk FITS panels) the file is updated in-place with the new WCS.

If ASTAP's path isn't set in Preferences, SASpro prompts for the executable — or, if you skip the prompt, falls back directly to Astrometry.net.

Astrometry.net (fallback)

If ASTAP fails or isn't configured, uploads to nova.astrometry.net via the web API (requires an API key from api/astrometry_key, prompted if missing). Login → upload → poll submission → poll calibration (up to ~15 minutes each). On success, builds a TAN WCS header from the returned RA/Dec/pixscale/orientation and merges it into the panel. The TAN construction, following the standard FITS WCS convention:

CD1_1 = -s cos(θ) CD1_2 = s sin(θ) CD2_1 = -s sin(θ) CD2_2 = -s cos(θ)

with s in degrees/pixel and θ the orientation in radians. For on-disk FITS panels, the file is updated in-place; views and non-FITS sources get the WCS attached in memory only.

14. Seestar Mode

Seestar Mode is a parallel pipeline designed for the ZWO Seestar S30/S50 and similar robot smart telescopes whose exports don't include plate solves but do include enough header metadata (RA, Dec, pixel size, focal length) to seed rough transforms. See §15.21 for the full internals; the user-facing picture:

  1. Base panel selection — from the panels closest to the field centre (top 5 by WCS-centre distance), pick the one with the most detected stars.
  2. Grow the mosaic outward — sorted by distance from the field centre, align each remaining panel against the current partial mosaic using astroalign.find_transform.
  3. Fallback to rough transforms — if astroalign fails on a panel, try a WCS-based rough transform (if the panel has one), or a physics-based one derived from RA, Dec, pixel size and focal length. If one of those works, use it as a seed and refine via overlap-restricted astroalign.
  4. Expand the canvas as needed — each new panel can grow the mosaic bounds; mosaic_sum and mosaic_count are re-allocated and copied into the larger canvas.
  5. Accumulate with feathering and brightness rejection — distance-transform feather (20-px radius) within the valid warped-ones mask, exclude pixels where the new panel is less than 90% of the mosaic's existing brightness (catches warped-image stray pixels that would show as hotspots).
  6. Retry failed panels once — after the first pass, re-attempt alignment of any panel that failed. Often works because the mosaic grew in the first pass and now includes more sky the panel can match.
  7. Final blend — mosaic_sum / mosaic_count everywhere the count is nonzero, final max-normalize to [0, 1].

Each panel is also run through polynomial gradient removal (degree-2 fit on a downsampled copy, see §15.22) before alignment so the per-panel sky is flat going in — important because Seestar panels tend to have strong vignetting that would otherwise dominate the astroalign star detection.

15. Technical details

Panel ordering, reprojection, normalization and blending.

15.1 Panel ordering

Alignment is sequential — each panel is matched against the mosaic as it exists so far. The order matters: a panel with no overlap with any already-placed panel can't be star-aligned at all. SASpro orders panels so the first is near the field centre and each later one maximises overlap with the already-placed set.

For every panel, sample 64 points around the perimeter in pixel space, forward through the panel's WCS (via all_pix2world — forward SIP is a plain polynomial evaluation, no iteration), take the resulting (RA, Dec) set as a sky bounding box. Project all boxes onto a TAN tangent plane centred on the mean sky position (RA wrap handled via unit vectors, not raw differences):

\xi = \frac{\cos\delta \sin(\alpha - \alpha_0)}{\sin\delta_0 \sin\delta + \cos\delta_0 \cos\delta \cos(\alpha - \alpha_0)}
\eta = \frac{\cos\delta_0 \sin\delta - \sin\delta_0 \cos\delta \cos(\alpha - \alpha_0)}{\sin\delta_0 \sin\delta + \cos\delta_0 \cos\delta \cos(\alpha - \alpha_0)}

then compute 2D rectangles in (ξ, η) space and run the ordering:

  1. First panel: the one whose rectangle centre is closest to (0, 0).
  2. Each next panel: the one with the largest pairwise overlap fraction against any already-placed panel:
f_{\mathrm{ov}}(A, B) = \frac{\mathrm{area}(A \cap B)}{\min(\mathrm{area}(A),\;\mathrm{area}(B))}
i^{*} = \arg\max_{i \notin \mathrm{placed}} \;\max_{j \in \mathrm{placed}}\; f_{\mathrm{ov}}(B_i, B_j)

Taking max over placed panels (not union) is deliberate: in an L-shaped layout, the union bounding box would fill in the hollow and “see” overlap where there's actually no shared sky. Pairwise max only reports real overlap with real panels.

Panels that genuinely share no sky with any already-placed panel are placed last and logged (“shares no sky with any placed panel — WCS placement, pedestal-only sky match”). They still get added to the mosaic via WCS placement; they just can't be star-refined.

15.2 Mosaic WCS linearization

The mosaic's destination WCS is linear TAN with no SIP, even when the source panels carry SIP. The reason: SIP is a polynomial correction that's only valid inside the frame it was fit to. The mosaic canvas is much larger than any single panel, so extrapolating a panel's SIP polynomial out to the full mosaic frame would diverge badly in the corners.

SASpro linearizes the destination by setting wcs.sip = None on a deep-copied WCS object and stripping -SIP from the CTYPEs. Source panels keep their SIP intact — that's what makes the inverse mapping correct per-pixel. Only the destination coordinate frame is forced linear.

15.3 Output bounding box

With the reference WCS (first ordered panel, linearized) picked, every panel's four corners are transformed to the reference frame via SkyCoord → skycoord_to_pixel. The bounding box of all transformed corners gives the mosaic's extent. CRPIX of the mosaic WCS is shifted by (-min_x, -min_y) so the mosaic occupies [0, W]×[0, H]. The mosaic's final WCS header is then this shifted copy, which is also what gets attached to the output view.

15.4 Exact SIP-aware remap

Per-pixel inverse mapping, tiled 512×512. For each tile:

  1. Generate destination pixel coordinates (x_d, y_d) on the tile grid.
  2. Forward through the (linear) destination WCS: (α, δ) = wcs_dst.pix2world(x_d, y_d). Exact and cheap — no SIP to invert.
  3. Inverse through the (SIP-rich) source WCS: (x_s, y_s) = wcs_src.all_world2pix(α, δ). This is the iterative SIP inverse.
  4. Reject (x_s, y_s) outside the source panel bounds (with 1-pixel margin for Lanczos).
  5. Round-trip validate every surviving coordinate (§15.5).
  6. Resample the source panel at the surviving coordinates via cv2.remap with INTER_LANCZOS4 and borderValue=0.

Progress is reported per tile so the UI stays responsive. Astropy's non-convergence warning on all_world2pix is caught silently because the subsequent validation handles edge cases properly — the warning would otherwise fire noisily on every panel at footprint edges.

15.5 Round-trip validation

A diverged SIP inverse that happens to land back inside the source frame still produces nonsense — all_world2pix can return a “solution” that doesn't actually reproject back to the sky it was asked for. SASpro validates by forward-SIP evaluating every mapped pixel and comparing to the asked-for sky position:

\|(\alpha', \delta') - (\alpha, \delta)\| < 0.25 \cdot s_{\mathrm{px}}

with the comparison in angular units and s_px the source panel's pixel scale. The RA difference is cos(δ)-weighted so the metric is proper angular distance:

\Delta\alpha = (\alpha_2 - \alpha_1) \cdot \cos(\delta),\quad \Delta\delta = \delta_2 - \delta_1

Pixels failing the quarter-pixel tolerance are marked invalid and left black (border fill). On a well-solved mosaic, rejection is near zero in the panel interior and up to a few percent at the footprint edge, where SIP non-convergence is expected behaviour. The log reports the rejection rate per panel.

15.6 Footprint-bbox restriction

Running the exact remap over the entire mosaic canvas would call the SIP inverse on billions of coordinates that have no business touching the source panel. SASpro restricts the remap to a bounding box containing just the panel's footprint in the destination frame, computed forward-only (all_pix2world → wcs_world2pix) over 96 perimeter sample points:

Outside this box, the SIP inverse is never called, pixels stay at the border-fill value (zero), and the pedestal coverage test correctly reports them as uncovered.

15.7 Fast homography mode

For panels without SIP, a single 3×3 homography is enough to represent the full gnomonic composition between the two tangent-plane projections (source panel's TAN and mosaic's TAN). SASpro samples 9 control points — corners, edge midpoints, centre — and fits the homography via cv2.findHomography(method=RANSAC, ransacReprojThreshold=2.5):

  1. Source pixel coords (x, y) → sky (α, δ) via source WCS.
  2. Sky (α, δ) → destination pixel coords (x', y') via mosaic WCS.
  3. Fit the (x, y) → (x', y') homography.

Then cv2.warpPerspective with INTER_LANCZOS4 for the actual warp. The result is cached per source WCS so subsequent panels reusing the same WCS object skip the fit.

Correctness: exact for SIP-free TAN panels on small fields (where the tangent-plane approximation holds across the panel). For panels with real SIP distortion, the global homography collapses the per-pixel polynomial corrections into a single average linear warp, and stars near the edges can double by a few pixels.

15.8 Pedestal coverage preservation

During warping, OpenCV fills pixels outside the source frame with borderValue=0. Any linear astronomical data also has values near zero in the sky background. If you tried to compute “which pixels are inside the warped panel” by thresholding > 0, the sky background would be indistinguishable from border fill.

SASpro's fix: add a +1.0 pedestal before warping, so the panel's data lives at [1.0, ~2.0] while the border fill stays at 0.0. The coverage test becomes trivial:

I' = I + P
\mathrm{coverage} = \{(x, y) : I'(x, y) > 0.995 \cdot P\}

After the coverage mask is extracted (and eroded — §15.9), the pedestal is stripped and any value outside the coverage region is zeroed:

I_{\mathrm{out}} = (I_{\mathrm{warped}} - P) \cdot \mathbb{1}_{\mathrm{coverage}}

Astroalign and the subsequent brightness match see pedestal-free data, so they're not biased by the +1.0 offset.

15.9 Edge trim for Lanczos ringing

Lanczos4 interpolation spans an 8×8 kernel. When Lanczos is applied near the border between real data and the zero border fill, it rings — overshoot pushes some pixels briefly above the pedestal threshold even though they're contaminated by the zero border. The overshoot can be larger than 0.005·P, meaning the coverage test would wrongly include contaminated ring pixels.

SASpro erodes the raw coverage mask by 12 pixels using an elliptical structuring element (cv2.getStructuringElement(cv2.MORPH_ELLIPSE)) to clear the contamination band. A 12-px erosion is a tiny loss on a typical mosaic panel (0.5% of area), well worth paying to keep the seam clean of ringing artefacts.

15.10 Sky pedestal match

For each panel, the sky level is measured via estimate_background_level():

  1. Tile the panel into a 16×16 grid. Each tile must contain at least 60% finite valid pixels.
  2. Rank tiles by luminance median and keep the darkest 15%. Luminance ranking (not per-channel) means all channels sample the same patches of sky, preserving the panel's background colour.
  3. Sigma-clip the kept pixels: 3 iterations of σ = 1.4826 · MAD, hard upper threshold at 2.5σ (to kill stars and nebulosity), loose lower threshold at 6·2.5σ (to avoid clipping legitimate sky noise).
  4. The sky level μ_i is the per-channel median of the clipped set; the sky noise σ_i is 1.4826 · MAD.

Applied to every panel before reprojection, with the reference panel's sky μ_ref as the common target:

I'_i = I_i - \mu_i + \mu_{\mathrm{ref}}

After this step every panel has the same sky colour and the same sky level as the reference panel. Signal above sky is untouched.

15.11 Overlap gain fit with sky pivot

Pedestal match handles DC offset but doesn't address panels taken at slightly different gains or in slightly different filters. For every overlap between the current panel and the already-accumulated mosaic (where coverage from both exists and total overlap is at least 5000 pixels), SASpro solves a linear match pivoted on the sky:

\mathbf{R} \approx a \cdot (\mathbf{S} - p) + p + b

with R the running mosaic estimate at the overlap pixels, S the new panel's reprojected pixels, and p the common sky level (set to μ_ref because Stage 1 already shifted both to that pivot). Applied as:

\mathrm{out} = (\mathrm{src} - p) \cdot a + p + b

Why pivot on sky: in a non-pivoted R ≈ a·S + b fit, a and b are correlated. A gain error attributed to a leaks into b and vice versa. Pivoting on the sky level separates them: a acts only on signal above sky (where it belongs — linear detector gain), b acts only on residual sky offset (where it belongs — any imperfection left from the Stage 1 pedestal match).

The gain is clamped to prevent pathological fits:

a_{\mathrm{out}} = \mathrm{clip}(a,\;0.05,\;20)

If the gain gets clamped, SASpro logs it. A clamped gain is almost always a sign of insufficient overlap or heavy contamination in the overlap region, and the clamp prevents a wildly wrong fit from catastrophically dimming or brightening the panel.

15.12 Block-mean reduction

The overlap gain fit is not done per-pixel. A sky-dominated overlap has pathologically low signal variance — most of the pixel-to-pixel variation is read noise and sky noise, not actual signal. An OLS slope on per-pixel data is attenuated by:

a_fit = a_true · var(signal) / (var(signal) + var(noise))

and collapses toward zero when noise dominates signal.

SASpro reduces the overlap to 16×16 block means before fitting:

S_k = \frac{1}{|B_k|} \sum_{(x, y) \in B_k} I(x, y)

Noise averages down as 1/block (so by a factor of 16 at the pixel level, 256 in variance). Real signal doesn't average down — a star in the block stays bright, a galaxy arm stays an arm. After reduction, the signal-to-noise ratio in the fit input is improved by a factor of ~16, enough to make the fit well-conditioned.

Blocks with less than 90% valid coverage (i.e. where part of the block falls outside the overlap mask) are discarded.

15.13 Robust iteration

The block-mean fit is iterated 4 times with robust sigma-clipping on the residuals. Standard sigma clipping with the median absolute deviation estimator:

\mathrm{MAD} = \mathrm{median}(|x - \mathrm{median}(x)|)
\sigma \approx 1.4826 \cdot \mathrm{MAD}
\mathrm{keep} = \{i : |r_i - \mathrm{median}(r)| < k \cdot \sigma\}

with k = 2.5. Blocks whose residuals fall outside this envelope (typically contaminated by diffraction spikes, cosmic rays missed by rejection, galaxy structure extending into the sky, etc.) are dropped and the fit is redone. Iteration stops early when the inlier set stops changing or drops below 16 blocks.

15.14 Mixed tone-scale detection

Linear astronomical panels and stretched (display-scaled) panels can look the same at first glance but have wildly different pixel distributions: a linear panel's MAD is pathologically tiny (order 1e-3) because almost all pixels sit near the sky floor; a stretched panel's MAD is 1–2 orders of magnitude higher. Normalization cannot blend across those scales — a gain fit between a linear source and a stretched reference would produce a huge a that distorts stars.

SASpro computes per-panel MAD using the exact algorithm from the Histogram tool (median absolute deviation of finite-pixel values, subsampled to 5M pixels with a seeded RNG on very large panels, averaged across colour channels). If the max/min ratio across panels exceeds 8×:

\frac{\max_i(\mathrm{MAD}_i)}{\min_i(\mathrm{MAD}_i)} > 8

SASpro prompts the user with a plain-language warning (“Looks like you've mixed linear and stretched panels”), lists the panels' MAD values with the names, and asks whether to continue. The 8× threshold is a judgement call — consistently-stretched panel sets tend to have MADs within 2× of each other, so 8× is firmly in “something's off” territory.

15.15 Seam feathering

Overlapping panels are blended by a distance-transform feather. For each panel's coverage mask:

  1. Compute the Euclidean distance transform d(x, y) — distance from (x, y) to the nearest coverage boundary. Pixels at the centre of the panel are far from any edge; pixels right at the edge are near zero.
  2. Apply a 15×15 Gaussian blur to smooth tiny boundary irregularities.
  3. Normalize by dividing by 100 (keeps d^p in float32 range at high p).
  4. Raise to the blend sharpness power p (settings; default 2.0):
w_i(x, y) = d_i(x, y)^{\,p}

The weighted mosaic is accumulated as:

\mathrm{sum}(x, y) = \sum_i I_i(x, y) \cdot w_i(x, y)
W(x, y) = \sum_i w_i(x, y)

and the final mosaic is sum / W. The elegant consequence of raising the weights to a power before accumulation is that the ratio of weights, which is what sets the actual blend, becomes:

\frac{w_i}{\sum_j w_j} = \frac{d_i^{\,p}}{\sum_j d_j^{\,p}}

This ratio spans exactly the available overlap region — wide where the overlap is generous, tight where the overlap is thin. It auto-scales to the geometry of every seam without any seam-specific tuning. The power p controls how sharply the handoff happens within whatever overlap is available: p = 1 gives linear ramp, higher p concentrates the handoff near the seam midline.

15.16 Star alignment refinement

For panels without SIP (where WCS-only placement is accurate but not perfect), an optional star refinement runs after reprojection:

  1. Compute the overlap between the newly reprojected panel's coverage and the mosaic's existing weight map (coverage & (W > 0)).
  2. If the overlap is smaller than 2000 pixels, skip refinement (not enough stars for a reliable match).
  3. Otherwise, restrict both the panel and the mosaic estimate to the overlap bounding box.
  4. Call astroalign.find_transform on the restricted images with a sigma backoff ladder (120 → 50 → 15 → 5): higher sigma first for isolated bright stars, lower if that fails. Serialised by a module-level lock because SEP (the backend) isn't thread-safe.
  5. Astroalign returns a similarity transform (rotation + uniform scale + translation). SASpro extracts the 2×3 affine matrix.
  6. Decompose the matrix for sanity checking — log a warning if rotation > 0.5°, scale differs from 1 by > 1%, or translation > 25 px. These are large residuals for a WCS-placed panel and signal that astroalign may be fitting error that WCS didn't actually have.
  7. Warp the panel with the affine matrix using cv2.warpAffine(LANCZOS4).
  8. If the transform mode is Homography or Polynomial Warp, run a further refinement pass that re-detects stars in the overlap and fits the higher-DOF model.

Restricting the match to the overlap bounding box (not the whole canvas) is critical: feeding the whole canvas to astroalign spreads control points across regions that can't correspond, starving RANSAC of usable bright anchors.

15.17 Polynomial warp

Polynomial Warp fits two polynomials of degree n in (x, y) through the matched star correspondences:

u = \sum_{i+j \leq n} a_{ij} \cdot x^i y^j
v = \sum_{i+j \leq n} b_{ij} \cdot x^i y^j

Degree is set in Settings (1–6, default 3). The coefficients are solved by least squares (np.linalg.lstsq) over the matched points, then a dense displacement map is evaluated on the full image grid and the panel is remapped via cv2.remap(INTER_LANCZOS4). Useful for Seestar panels or any blind-solved mosaic where the per-panel distortion isn't captured by any linear or projective model.

15.18 SIP coefficient detection

The SIP auto-disable logic (§12) needs to distinguish real SIP solutions from declared-but-empty SIP (ASTAP occasionally writes A_ORDER keywords without actual coefficients, for example). SASpro checks for genuine SIP by inspecting the WCS object's SIP structure:

Only when all three conditions are met does SASpro consider the panel “has SIP” for the auto-disable fraction count. Declared SIP without coefficients is treated as no SIP.

15.19 ASTAP integration

External process via Qt's QProcess. SASpro writes a normalized temporary FITS copy of the panel (stretched for star visibility, same approach as the plate solver's shared _normalize_for_astap), invokes ASTAP as:

astap -f <tmp.fit> -r 179 -fov 0 -z 0 -wcs -sip

and waits up to 5 minutes for completion. On success, both the FITS header ASTAP wrote and any .wcs sidecar are parsed and merged. Numeric WCS keys (CRPIX, CRVAL, CD, SIP coefficients, orders) are coerced to proper float/int types because ASTAP occasionally writes them as strings, which trips astropy's type checks later.

Required WCS keys (CTYPE1, CTYPE2, RADECSYS, WCSAXES) are added if missing with sensible defaults (RA---TAN, DEC--TAN, ICRS, 2). For on-disk FITS panels, the file is saved back through SASpro's shared image-I/O with the solved header; for views and non-FITS sources, the WCS is attached in memory only.

15.20 Astrometry.net integration

Via the Astrometry.net web API (nova.astrometry.net/api). The API key is read from api/astrometry_key and prompted via a dialog if missing. The sequence:

  1. Login — POST login with the API key, receive a session token.
  2. Upload — POST upload with the panel (FITS, converted via generate_minimal_fits_header if needed), receive a submission ID.
  3. Poll submission — GET submissions/<subid> every 10 seconds, up to 90 attempts (15 minutes), waiting for the job ID to appear.
  4. Poll calibration — GET jobs/<jobid>/calibration/ every 10 seconds, up to 90 attempts, waiting for the calibration dict with ra, dec, pixscale, orientation.
  5. Construct TAN WCS — build the CD matrix from scale and orientation (equations in §13), merge into the panel's header.

All HTTP requests are routed through robust_api_request, which raises HTTPError on bad status, parses JSON, and prompts the user to restart on network errors or invalid responses rather than silently failing. For on-disk FITS panels, the file is updated in-place with the new WCS keys.

15.21 Seestar Mode internals

The Seestar pipeline is a separate implementation path (align_images_seestar_mode) because robot smart telescopes work differently from mounted astrophotography: panels arrive one at a time from the device's slew-and-shoot sequence, each carrying its own rough RA/Dec but no plate solve.

Base selection

For every loaded panel that has a header, extract the image centre's world coordinates via WCS.all_pix2world. Average these to get the field centre. Sort panels by angular distance from the field centre. Take the top 5 closest-to-centre panels, run DAOStarFinder on each (counting detected stars at 5σ threshold with fwhm = 3.0), pick the one with the most stars as the base — a star-rich base gives astroalign more anchors to work with on every subsequent panel.

Rough transform construction

Two independent methods are tried as seeds for astroalign:

s_{\mathrm{px}} = 206265 \cdot \frac{p_{\mu m}}{f_{\mathrm{mm}}}

Convert the RA/Dec difference between panels (with cos(δ) correction on RA) into pixel offsets, assemble a translation-only 2×3 matrix. Zero rotation (Seestar panels on the mount share orientation to within a fraction of a degree).

Iterative accumulation

For each panel in centre-first order:

  1. Debayer if the FITS header has BAYERPAT.
  2. Run polynomial gradient removal (degree 2, downsampled 5×) to flatten the panel's background.
  3. Stretch to match the base panel's median.
  4. Try astroalign.find_transform against the current mosaic (reduced to greyscale via mean, lightly stretched for star visibility).
  5. If astroalign fails, fall back to one of the rough transforms, then refine via refine_via_overlap which runs astroalign on the overlap region only.
  6. Compute the canvas size needed to hold both the current mosaic and the warped new panel. Allocate larger mosaic_sum and mosaic_count arrays and copy the existing mosaic into them.
  7. Warp the new panel into the enlarged canvas with the adjusted transform.
  8. Build three masks: border (10-px exclusion zone around the warped panel), valid (where warping found actual data via a warped ones image), brightness (where new panel ≥ 0.9 × mosaic estimate, to reject stray pixels from warping artefacts).
  9. Combine the three masks, compute distance-transform feather weights (20-px radius) within the valid mask, accumulate sum += warped × w and count += w.

Retry pass

Panels that fail alignment on the first pass are re-attempted once, after the mosaic has grown. Often works because the mosaic now covers more sky that the failed panel can match against.

Final blend

Running mean per pixel: mosaic = sum / count where count is nonzero. Final max-normalize to [0, 1].

15.22 Polynomial gradient removal (Seestar)

Before Seestar panels enter the alignment pipeline, SASpro flattens the per-panel background with a polynomial fit (PolyGradientRemoval):

  1. Unlinked linear stretch (using SASpro's shared Numba stretch utility).
  2. Downsample by 5× (area interpolation).
  3. Generate ~100 sample points via the same algorithm used by SASpro's Automatic Background Extraction (ABE) tool, with an exclusion mask so stars and bright features are avoided.
  4. At each sample point, take the median of a 15×15 patch (robust to outliers).
  5. Fit a polynomial background of configurable degree (default 2) via least squares (np.linalg.lstsq).
  6. Upscale the background to full size (Lanczos4) and subtract.
  7. Re-normalize median to the original.
  8. Unstretch back to the linear domain.

Without this step, Seestar panels' heavy vignetting dominates astroalign's star detection and the matching rate collapses.

Credits. The exact SIP-aware per-pixel inverse mapping with tiled dispatch, forward-only footprint-bbox restriction, and quarter-pixel round-trip validation; the destination-WCS linearization decision; the pedestal coverage-preservation technique (+1.0 pedestal, 0.995 threshold, 12-px edge trim for Lanczos4 ringing); the two-stage normalization (per-panel tiled sigma-clipped sky estimator plus sky-pivoted overlap gain fit R ≈ a·(S−p) + p + b reduced to 16×16 block means with 4-iteration robust MAD sigma-clipping and [0.05, 20] gain clamp); the mixed tone-scale detector (per-panel MAD matched to the Histogram tool's algorithm, 8× spread trigger); the panel ordering (centre-first + max pairwise overlap, deliberately not union-box); the pre-blur raised-power distance-transform feathering with sharpness power that converts a global coefficient into an overlap-relative blend ratio; the SIP coefficient-based auto-disable for the star refinement path; the Seestar Mode iterative pipeline with centre-first base selection by star count, WCS-rough and physics-rough transform fallbacks, feathered sum-and-count accumulation with brightness-ratio pixel rejection, canvas-expanding warping, failed-panel retry pass; the Add-from-View multi-pick dialog with WCS tagging; the Reproject Mode dispatcher; and the ASTAP-first then Astrometry.net fallback plate-solve chain are all original work by Franklin Marek for SetiAstro Suite Pro.

External tools and libraries: Astroalign (Beroiz, Cabral & Sanchez 2020, A&C 32:100384) provides the star-match transform estimation, with its own triangle invariant matcher internally driven by SEP (Barbary 2016, JOSS 1:58) derived from Source Extractor (Bertin & Arnouts 1996, A&AS 117:393). ASTAP is Han Kleijn's standalone plate solver (external process). Astrometry.net is Lang et al. (2010, AJ 139:1782) via the web API at nova.astrometry.net. Star detection for Seestar base selection uses DAOStarFinder from photutils, implementing Stetson (1987, PASP 99:191)'s DAOPHOT. The Automatic Background Extraction sample-point generator used by the Seestar polynomial gradient removal is SASpro's own ABE implementation. FITS WCS standards: TAN gnomonic projection follows Greisen & Calabretta (2002, A&A 395:1061); SIP distortion polynomial follows Shupe et al. (2005, ADASS XIV). RANSAC (used by the Fast Homography fit and all downstream OpenCV robust estimators) is Fischler & Bolles (1981, Comm. ACM 24(6):381). OpenCV provides the warp (warpAffine, warpPerspective, remap), homography fit (findHomography), distance transform, Gaussian blur, erode, and resampling filters (Lanczos4, area, cubic, nearest). Astropy provides FITS I/O, the WCS and SIP classes, fit_wcs_from_points, SkyCoord, skycoord_to_pixel, and the sigma-clipped statistics used by the sky estimator and DAOStarFinder prep. NumPy provides the array engine. PyQt6 provides the UI framework (including QProcess for driving ASTAP). requests provides the HTTP transport for the Astrometry.net API.