Align Red and Blue channels onto Green — correcting chromatic aberration, atmospheric dispersion, and sensor-level channel misalignment with five modes including two algorithms unique to SASpro.
Every mode in this tool does the same high-level thing: find the geometric relationship between each channel and the Green channel, then warp Red and Blue so they land on Green's pixel grid. Green itself is never changed — it's the reference frame. The output is a new RGB image where R, G, and B are pixel-aligned.
This matters because almost every color-processing step downstream — saturation enhancement, white balance, star color extraction, luminance recombination — assumes the three channels are already aligned. If they aren't, you get color fringing, star halos with cyan/magenta edges, and desaturated star cores. RGB Align is the fix-it step that goes between calibration/stretching and color-aware processing.
| Mode | What it fits | Use for |
|---|---|---|
| Planet Centroid (ADC) | Rigid translation (shift only) based on planet disc centroid matching. | Planetary imaging with atmospheric dispersion. Jupiter / Saturn / Mars from any ground-based rig where you weren't using (or your ADC wasn't perfectly tuned for) atmospheric dispersion correction. |
| EDGE | Homography (or affine fallback) fit to outer-ring stars only — center stars are deliberately excluded. | Corner-dominant lens chromatic aberration. Fast astrographs (f/2-f/4), camera lenses, or any optical system where CA is strongly radial and worst at the edges. |
| Homography | Standard 8-DOF projective fit across all detected stars via astroalign. | General-purpose. Works well for mild uniform CA across the whole frame. |
| Affine | 6-DOF affine fit (shift + scale + rotate + shear) across all detected stars. | When channels are mostly parallel with minor shift/scale differences. Simpler, more constrained than homography. |
| Poly 3 / Poly 4 | Polynomial residual (order 3 or 4) on top of a base fit. | Strong nonlinear distortion. Order 4 only when 3 isn't enough; needs plenty of matched stars to avoid over-fitting. |
Standard astroalign-based color alignment detects stars everywhere, builds triangle matches, solves for the best transform. Because central stars outnumber corner stars (there's more area in the middle of a frame than at the edges, geometrically), the fit is biased toward the center. Central stars have zero CA (they're on the optical axis) so they fit a near-identity transform perfectly, and the resulting whole-field solution leaves corner stars with as much CA as they started with.
EDGE flips this on its head: throw out the central stars entirely and solve only on outer-ring stars where the CA actually lives. Specifically:
The result: a transform that fits the corner misalignment where it matters, at the cost of being potentially less-perfect in the (nearly-zero-CA) center. Since the center starts near-perfect anyway, this trade is almost always correct.
Atmospheric dispersion separates light by wavelength as it refracts through the atmosphere — a planet imaged low on the horizon shows its Red, Green, and Blue images shifted vertically by a few pixels relative to each other. Hardware ADC prisms correct this at capture time; this mode corrects it in post. There are no stars to match on a planet image, so the standard star-matching approaches fail. Planet Centroid uses the planet disc itself:
Pass 1: Intensity-weighted centroid. For each channel, normalize to [0, 1] via percentile stretch, mask pixels above the signal floor threshold (default 0.05), and compute the mass-center of the resulting weighted disc. The centroid shift between R-and-G and between B-and-G gives the coarse per-channel offset. This is robust to limb darkening (bright limbs dominate the mass), hot pixels (negligible mass contribution), and degrades gracefully as the planet fills the frame — no morphological cleanup needed.
Pass 2: SSD gradient refinement. The centroid pass gets within a pixel or two but can't do sub-pixel accuracy — the planet's internal brightness distribution varies between channels (methane bands in R vs G on Jupiter, for example), biasing a pure brightness-centroid. Pass 2 fixes this by comparing structure instead of brightness: compute the Sobel gradient magnitude of both channels, normalize each to zero-mean and unit-variance, then brute-force search over a ±8-px window for the shift that minimizes the sum-of-squared-differences between the two gradient images. A quadratic polish on the SSD surface gives the final sub-pixel offset.
Output is a rigid translation (dx, dy) per channel — no rotation, no scale. Appropriate for ADC because the dispersion is a pure shift, not a warp.
These use astroalign (triangle-invariant matching + RANSAC) on all detected stars, then re-fit the transform in the chosen model's degrees of freedom:
All three run a "hungry" fallback pass if the first solve returns stars too clustered in the center: lower sigma (3.0 instead of 5.0), smaller min_area (3 instead of 5), higher max control points (120 instead of 50). This usually fixes the "central cluster" bias on the standard-mode solvers (the EDGE mode addresses it structurally instead).
EDGE and the standard star-matching modes need SEP to detect stars first. The SEP sigma spinbox sets the detection threshold:
Trial Detect output reads like sigma = 5.0, total stars (green): 342, outer-ring stars (used by EDGE): 61. If outer-ring count is below 20 for EDGE mode, lower sigma. If total count exceeds 10,000, raise it (dense star fields confuse the triangle matcher).
When Planet Centroid mode is selected, the SEP controls are hidden and the Signal floor spinbox appears instead:
Lower values make the centroid pull more weight from the dim outer limb; higher values ignore it entirely. For Jupiter, 0.05 is usually right. For Mars, 0.08-0.10 (the disc is small and bright). For Saturn with rings, 0.05-0.07 (lower includes the dim ring edges, which can bias the centroid vertically — raise if Saturn is giving odd results).
The Manual… button opens a secondary dialog with two direction pads (one for Red offset, one for Blue offset) and a live preview showing the current result. Each pad has arrow keys (↑↓←→) for 1-pixel nudges and a center ⟲ button for reset. The preview updates on a 60ms debounce — nudge rapidly and the preview catches up when you stop.
For planet alignment, the preview automatically centers on the planet (detected via the Green channel centroid, with a ROI sized to roughly 3.2× the disc radius). For deep-sky, it shows a center crop of ~45% of the frame width.
Create new document (keep original) checkbox controls whether the result replaces the current view's pixels or opens as a new document. Default is new document — safer for exploratory work, lets you compare before and after. Uncheck for in-place application that goes through the normal edit-history / undo stack.
The summary box below the progress bar shows the actual transforms applied:
For Planet Centroid mode, the summary reads as pure shifts (dx, dy) instead of matrices. For Manual mode it reads the applied offsets. The summary is also forwarded to the main window's log panel if available.
The tool exposes align_rgb_array(img, model, sep_sigma) as a direct function call for scripting: pass a 3-channel NumPy array, get back the aligned result. Model strings are planet-centroid, edge-sep, homography, affine, poly3, poly4. There's also a run_rgb_align_headless(main_window, document, preset) entry point for shortcut-system integration where the preset carries {"model": "...", "sep_sigma": N, "new_doc": true/false}.
The intensity-weighted centroid math, Sobel-SSD refinement with quadratic subpixel polish, EDGE ring geometry, and the hungry fallback.
Normalize the channel to [0, 1] via a robust percentile stretch (p0.5, p99.5) so hot pixels don't dominate the normalization:
Build a signal mask by thresholding at the signal floor φ (user-controlled, default 0.05):
Compute the intensity-weighted centroid over the mask:
The coarse per-channel shift is the centroid difference:
A minimum of 16 signal pixels is required; below that the mode fails over to astroalign. Compared to a bounding-box geometric center (naive approach), the intensity-weighted centroid handles limb darkening (brighter limb pixels pull the centroid toward the actual disc center) and remains stable as the planet grows to fill most of the frame — the geometric center would snap to the frame center when the disc touches multiple edges, which is useless.
After applying the coarse shift, pre-warp the source channel by (Δx0, Δy0). Compute Sobel gradient magnitude of both the shifted source and the reference:
Normalize each gradient image to zero mean and unit variance:
This normalization is critical for the planet-color-balance robustness — it removes any channel-dependent intensity response and leaves only spatial structure. The SSD on normalized gradients then measures geometric alignment independent of per-channel brightness.
Crop the inner 80% of each gradient image (10% border on each side) to avoid Sobel edge-effects at the frame boundary from the warpAffine's REPLICATE border mode. Then compute SSD over all displacements in a ±r window (default r = 8 px):
Brute-force search is used because the window is small (17 × 17 = 289 samples), the SSD surface is well-behaved near the minimum, and the overhead of a smarter optimizer isn't justified at this problem size. Each SSD evaluation uses only the overlapping portion of the shifted images (clipped at the crop boundaries) to avoid padding artifacts.
The brute-force search returns an integer-pixel minimum. To get sub-pixel accuracy, fit a parabola to the SSD values at (Δx* - 1, Δx*, Δx* + 1) and solve for the vertex. Standard three-point quadratic minimum formula with v-, v0, v+ as the three SSD values:
The clip to [-0.75, +0.75] prevents the subpixel correction from exceeding one pixel's worth of adjustment, which would indicate the parabolic fit has broken down (the true minimum is outside the ±1-pixel neighborhood). Applied separably in x and y, giving final sub-pixel shifts added to the integer brute-force result.
Total translation output = coarse centroid shift + brute-force integer SSD minimum + quadratic sub-pixel polish. Typical accuracy on a well-exposed planet is ~0.1 px, limited by image noise and gradient computation sensitivity rather than the algorithm itself.
Frame center and corner radius:
Per-star distance from center:
Two radial thresholds govern EDGE behavior. The inner cutoff drops stars from the central zone (where CA is near-zero):
The coverage gate ensures enough stars reach the very edge, where CA is strongest:
If fewer than 6 stars pass the outer threshold, EDGE declines (there's not enough edge signal to fit a reliable transform) and the pipeline falls back to standard astroalign on all stars. These two thresholds — 0.38 inner, 0.55 outer — are tunable constants (EDGE_INNER_FRAC and EDGE_FRAC in the worker class) chosen empirically to balance "throw away the useless center" against "keep enough stars to fit".
Rather than running triangle matching on just the outer-ring subset (which would be expensive and often fail with few points), EDGE pairs source and reference stars by proximity. For each reference outer-ring star, find its nearest neighbor in the source star set:
Accept the pair if the distance is below the tolerance τ = 10 px:
The 10-px tolerance is generous enough to accommodate ordinary chromatic aberration (typically 1-5 px) but tight enough to reject false matches between distinct stars. Setting τ too low (3-4 px) would reject legitimate CA-shifted matches; setting it too high (30+ px) would introduce bad pairs that would then poison the RANSAC fit.
Matched pairs feed into cv2.findHomography (RANSAC, 2.5-px inlier threshold, 2000 iterations, 0.999 confidence). If homography fails to solve or returns a degenerate matrix, fall back to cv2.estimateAffine2D with the same settings. If both fail, fall through to standard astroalign on the full star set.
Standard-mode solves (Homography / Affine / Poly) start with astroalign at sigma 5.0, min_area 5, max_control_points 50 — tuned for typical fields. If the resulting matched points are too clustered in the center (checked via the same radial geometry as EDGE: fewer than 6 points at > 0.55 × Rmax), astroalign re-runs with "hungry" settings: sigma 3.0, min_area 3, max_control_points 120. The hungry pass catches dimmer stars at the edges that the tighter default missed. If the hungry pass improves coverage, its results are used; otherwise the first pass's results stand.
This two-stage approach keeps the common case fast (default settings succeed on most frames) while automatically recovering on sparse or center-dominated fields.
Each channel is warped onto Green's pixel grid using OpenCV's Lanczos-4 interpolation with constant-zero border fill:
cv2.warpAffine with a 2×3 translation matrix.cv2.warpAffine with the fitted 2×3 affine matrix.cv2.warpPerspective with the fitted 3×3 homography.cv2.remap using map arrays computed by evaluating the polynomial on every output pixel's normalized coordinates.The warp is applied only to R and B. G is unchanged (it's the reference). The output image stacks the warped R, original G, and warped B into a new 3-channel array matching the input dtype. The simpler single-pass warp here (unlike the Stellar Alignment anti-ring blend) is appropriate because RGB alignment shifts are small (typically < 5 px) — Lanczos ringing is minimal and the sharpness benefit is worth more than ring suppression at this scale.
When Manual mode opens on a planet image, the preview ROI auto-centers on the planet. Detection uses the same intensity-weighted centroid as Planet Centroid mode, run on the Green channel. The planet's effective radius is estimated from the signal pixel count (assuming a circular disc):
The 3.2× multiplier gives roughly 3 planet-radii of context around the disc, enough to show the limb clearly without wasting preview space on empty sky. The clip bounds prevent the ROI from being either too small (hard to see fringing) or too large (slow preview updates). For deep-sky images or when planet detection fails, the ROI defaults to a 45%-of-frame center crop.
PolynomialTransform for the poly3/poly4 modes.