Four tools in one dialog. The Convolution tab blurs with a user-shaped PSF (useful for softening synthetic stars, generating calibration kernels, or deliberately matching another image's seeing). Deconvolution runs four classical sharpeners — Richardson–Lucy, Wiener, Larson–Sekanina (comet feature enhancer), and Van Cittert. PSF Estimator extracts a real stellar PSF from the frame with SEP and feeds it back to RL/Wiener. Classical Denoise hosts six denoisers (TV, NLM, Bilateral, Gaussian, Median, Wavelet) running on a background thread with a Cancel button.
The right-hand view shows the active image (or current preview result). Scroll-wheel and trackpad gestures zoom with cursor as the anchor — small trackpad swipes give fine control, click-wheel detents coarse, hold Ctrl for faster zoom in either case. Click-and-drag pans (scroll-hand). The zoom row above the view holds explicit buttons for Zoom In, Zoom Out, and Fit-to-Preview.
The checkbox above the view applies a statistical stretch to the preview only — the underlying pixel data is never modified by this control. It exists so you can judge denoise/deconv results on linear data without having to Push and then Undo a dedicated stretch. Target median picks the stretch strength (0.01–0.60, default 0.25). Linked keeps the three RGB channels tied together; uncheck to stretch each channel independently (useful when the image has a strong colour cast you want to see through). Both controls are disabled until the checkbox is on.
The 64×64 chip below the tabs always shows the PSF that will actually be used by Preview on the current tab. For the Convolution tab, that's the elliptical super-Gaussian built from the four sliders. For RL/Wiener on the Deconvolution tab, it's either those sliders' Gaussian or the custom stellar PSF when one is active. For other tabs the chip is empty. Change any slider and the chip updates live.
Global blend between the processed result and the original, applied at the end of every Preview on every tab:
Lets you “dial back” an aggressive deconv or denoise rather than having to re-run with weaker parameters. The slider accepts values from 0.00 (no change) to 1.00 (full effect).
If the destination document carries an active mask, the processed result is blended through it against the pre-processed image when Preview runs, so only masked regions are affected. The mask is nearest-neighbour resampled to match the image shape and broadcast to the right channel count. See § 6.6 for the formula.
The handle at the bottom-left of the controls captures the current tab's state as a preset — drag it onto the canvas to make a shortcut tile, or drop it onto an image to apply those settings headlessly. Each tab has its own preset schema: Convolution carries the four PSF sliders + strength; Deconvolution carries every slider on every algorithm's panel (plus the Larson–Sekanina centre if one has been clicked) so that Replay Last Action and the shortcut re-create the exact run; Classical Denoise carries the method + its parameters.
Blurs the image with a parametric elliptical super-Gaussian. All four sliders feed the live PSF chip; see § 6.1 for the formula.
⌈6σ⌉ and forced odd.Preview convolves each channel independently with reflect padding, so borders don't darken (see § 6.2).
Dropdown at the top of the tab chooses which algorithm Preview runs. The parameter panel below updates to show only that algorithm's controls. The PSF slider group is visible only for Richardson–Lucy and Wiener; the other two define their own kernels internally.
When a stellar PSF has been loaded from the PSF Estimator tab, a blue “Using Stellar PSF” bar appears above the parameter stack with a Disable Stellar PSF button. While the bar is lit, RL and Wiener use the measured PSF instead of the slider-built Gaussian, and the preview chip shows the stellar kernel. Clicking Disable, or changing any of the four PSF sliders, drops the custom PSF and reverts to the Gaussian.
Iterative multiplicative deconvolution. The classical choice for restoring stars and small-scale detail on photon-limited data.
Single-pass frequency-domain deconvolution. Much faster than RL and less prone to ringing, but less adaptive to local statistics.
Comet feature enhancer. Rotates a copy of the image by a small angle around a chosen centre, optionally shifts it radially, then divides or subtracts the rotated copy from the original. The result enhances anisotropic features (jets, tails, shells) by cancelling the roughly circular comet coma, and is then blended back into the original with either SoftLight or Screen.
The oldest practical deconvolution scheme — additive, non-blind, non-regularised. Fast, intuitive, and prone to runaway amplification with too many iterations. Useful when you want a mild sharpening pass without the full RL machinery, and the PSF is a smooth Gaussian.
Van Cittert uses an internally-fixed σ = 3 Gaussian PSF and is applied to each channel independently; the four PSF sliders at the top of the Deconvolution tab are ignored.
Measures the real point-spread function of the image by stacking normalised postage stamps around detected stars, using SEP for detection. The result can be sent straight into RL or Wiener as a custom PSF, or saved for use later.
(2w+1)×(2w+1). The
stamp has to comfortably contain the whole stellar profile
including a quiet annulus around it; too small clips the wings,
too large wastes memory on sky.Run SEP Extraction runs detection, stacks the stamps, and updates the small PSF preview below. Use as Current PSF loads that kernel into RL/Wiener, switches the algorithm to Richardson–Lucy, lights the Custom PSF bar on the Deconvolution tab, and refuses to reset until you click Disable or move a PSF slider. Save PSF… writes the kernel to FITS (float32, no header) or TIFF (float32) for reuse in other tools or later sessions.
Six classical denoisers under one roof, selected from a dropdown. Preview runs on a background thread with a Cancel button next to the status line — the dialog stays responsive and you can abort a too-slow run without force-killing SASpro. The strength and mask blends from § 1 apply on the GUI thread once the worker finishes.
Default on. For colour images, converts to Lab, denoises only the L* channel, converts back. Preserves chroma, which is nearly always what you want — running the per-channel path on RGB tends to desaturate and introduce colour mottling in low-SNR regions. Uncheck to run on each channel independently.
bilateralFilter. Works on 8-bit internally so very
fast but with limited dynamic range. Diameter is the
neighbourhood size; Sigma colour and Sigma space
tune how aggressively similar values are mixed.All algorithms operate on float32 [0,1]. Mask and strength blends apply uniformly at the end of every Preview path.
Both the Convolution tab and the slider path for RL/Wiener build
their kernel from the same four parameters: radius r,
kurtosis exponent β, aspect ratio a,
and rotation θ. With
σx = r,
σy = r/a, and the rotated grid:
normalised to unit sum so convolution preserves image mean. The
exponent β is the “kurtosis” slider: at
β = 1 the formula reduces to the standard elliptical
Gaussian; β > 1 produces a flat-topped, sharply
falling kernel (approaching a top-hat as β grows); β < 1
produces a peaked, long-wing kernel (approaching a Moffat profile for
small β). The kernel grid size is
⌈6σx⌉, forced odd. Results
are LRU-cached by (r, β, a, θ) so the live PSF preview and
repeated previews with the same kernel don't rebuild.
Each channel is reflect-padded by half the kernel size on each edge,
convolved via scipy.signal.fftconvolve in "same" mode,
then cropped back. Reflect padding (as opposed to zero padding) is
what keeps the image border from darkening when a large soft kernel
reaches past the frame — the reflected pixels present the kernel
with sensible data instead of zeros.
The classical multiplicative update:
where y is the observed image, h is the
PSF, h* is h flipped in both axes, and
∗ denotes convolution. The multiplicative form
guarantees non-negativity automatically. SASpro initialises
f0 from y clipped to
≥ 10-8, then runs the stated number of
iterations.
After each RL step, when a regulariser is selected:
with αL2 = 0.01 and
αTV = 0.01 hard-coded, chosen
to be gentle enough that the regulariser nudges rather than overrides
the data term. The Laplacian in the L2 term uses
scipy.ndimage.laplace; the TV step calls
skimage.restoration.denoise_tv_chambolle on the current
estimate. Each iteration ends with a clip to [0, 1] before the next
step.
When the checkbox is on, a single pass of
skimage.restoration.denoise_bilateral
(sigma_color = 0.08, sigma_spatial = 1)
runs after the last iteration. This is applied per-tile just before
blending (see below), so ring artefacts around bright edges are
suppressed without a big post-pass over the whole image.
Full-frame RL on a large image is slow and FFT-dominated, so SASpro
splits the frame into horizontal tiles, one per CPU core, deconvolves
them in parallel with a ThreadPoolExecutor, and seamlessly
blends them back together. Each tile is extended by
overlap = max(psf_h, psf_w)/2 + 15
rows on each side before the RL loop, so neighbouring tiles overlap
and the central rows are far from any padding-induced artefacts. The
overlapping rows are blended with a vertical ramp:
that goes smoothly from 0 at the tile's extreme edge to 1 in its core. Overlapping contributions are accumulated weighted by this ramp and divided by the summed weights:
which gives a smooth hand-off across tile seams. For colour images, each RGB channel (or the L* channel in L*-only mode) runs through this pipeline independently; the status label at the bottom-left of the dialog shows which channel and iteration count is active.
The PSF is embedded in a zero-padded, image-sized array, FFT'd, and the standard Wiener filter is applied in the frequency domain:
with K = λ for classical Wiener and
K = λ² for Tikhonov L2, where λ
is the Noise/Signal slider. The result is
IFFT(W · FFT(y)), clipped to [0, 1].
When de-ring is on, a single bilateral pass with the same parameters
as the RL de-ring runs on the output. Colour handling (L*-only vs
per-channel) is identical to RL.
For each pixel at image position (x, y) with respect to the chosen
centre (x0, y0), compute polar coordinates
(r, θ) and sample the image at the shifted polar
position:
using bilinear interpolation (SASpro's own
_bilinear_interpolate_gray on the vectorised polar grid,
not scipy.ndimage.map_coordinates, so the memory footprint
scales with image size only). Out-of-bounds samples return 0. The
resulting J is then combined with the original I
by the chosen operator:
The subtract variant is renormalised to [0, 1] so the final blend has a predictable range. For colour inputs the image is reduced to its luma before the LS step and then broadcast back to three channels for blending. The final composite with the original uses one of:
chosen to match the operator (Divide → SoftLight, Subtract → Screen); the dropdown lets you override.
Simple additive update toward the observed image:
with β the relaxation parameter and h a
fixed σ = 3 separable Gaussian built from a
1-D outer product. No stabilisation; stays well-behaved only for small
β and modest iteration counts. Each channel runs independently.
For each selected source i inside the saturation cap,
a (2w+1)×(2w+1) stamp is cut from the
background-subtracted frame around its integer-rounded centre,
flux-normalised, and summed:
Sources are sorted by SEP's reported peak value descending, so the brightest unsaturated stars dominate the first entries; the Max stars cap trims the tail. The final kernel is sum-normalised so convolution preserves image mean. If a source's stamp runs off the edge or has non-positive total flux it is skipped; if every candidate fails, Preview reports the error and no PSF is produced.
Each method dispatches through
_denoise_single_channel, so L*-only vs per-channel is
decided once at the top level. The underlying implementations are:
skimage.restoration.denoise_tv_chambolle with
channel_axis = None (2-D per-channel call).skimage.restoration.denoise_nl_means in fast mode,
with sigma from
skimage.restoration.estimate_sigma on the input
channel. Patch and search sizes are forced odd.cv2.bilateralFilter
on an 8-bit copy, converted back to float32 [0,1].scipy.ndimage.gaussian_filter.scipy.ndimage.median_filter
with the kernel size forced odd.skimage.restoration.denoise_wavelet with
method = BayesShrink and
sigma = None when the slider is at 0.Every result is clipped to [0, 1] before returning to the GUI thread.
After every Preview on every tab, the strength slider blends the processed result with the original:
and, if the active document carries a mask m ∈ [0, 1]:
in that order. The mask is read from the destination document, resized with nearest-neighbour sampling (OpenCV when available, NumPy index fallback otherwise), and broadcast to the right channel count.