Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Helia logo

Helia is a free, open-source stacker for pictures of the Sun. You point a telescope and a camera at the Sun and record a video, a few thousand frames long. Each frame is blurred a little differently by the air above you, because the atmosphere is never still. Helia ranks the frames by sharpness, lines them up (the whole disc, and many small patches of it, because the air bends the image differently across the Sun), and averages the best ones into one sharp picture, written as a 16-bit TIFF, an 8-bit PNG or a FITS file. It knows the common solar filters (H-alpha, calcium K and H, magnesium, sodium, helium D3, H-beta and white light), picks sensible settings for them, and can colour the result for sharing.

This technique is called lucky imaging. It does the job of tools such as AutoStakkert!, in a single program that runs on Linux, Windows and macOS, on x86_64 and ARM, and does its heavy maths on whatever GPU you have.

Helia is written in Rust. It comes as three front ends over the same engine:

  • helia-gui, a desktop app that walks you through the steps. See Using the desktop app.
  • helia, a command line tool for scripts and batch work. See The command line.
  • helia serve, an HTTP and WebSocket API for notebooks and other programs. See The API.

Where to start

Copyright (C) 2026 Laurence Horrocks-Barlow. Helia is free software under the GNU General Public License, version 3 or (at your option) any later version, with no warranty, and with the additional terms in ADDITIONAL-TERMS.md that keep the author's name and this dedication with the program; the About page of the desktop app, helia --version and the contributing notes have the details. Helia is made by Laurence Horrocks-Barlow. If you are interested in anything else I make, visit 12hobbies.studio.

Dedication

Helia is dedicated to my daughter, the light of my life. Her middle name is Helia, because she has an energetic spark. She has not stayed still since the day she was born, which suits software whose whole job is catching the sharp moments in something that never stops moving.

Status

Helia is in early development. The stacking pipeline, the command line, the desktop app and the API work today, with sharpening, deconvolution and everyday adjustments, and it applies dark and flat frames and a bad-pixel map before it analyses anything. Drizzle and the other planned features are listed in the Roadmap, and each page of this guide says plainly when something is not available yet. The plan and every decision taken so far are kept in the project's private issue tracker.

Pre-release archives for Linux (x86_64 and aarch64) and Windows x86_64 are available to early-access users (contact 12hobbies.studio); macOS is built from source. See Installation.

Supported platforms and GPUs

Helia runs its heavy maths through wgpu, which talks to whatever graphics API your system has:

PlatformGPU APIHardware
LinuxVulkan, GLES fallbackAMD, NVIDIA, Intel, Raspberry Pi 4/5
WindowsDX12, VulkanAMD, NVIDIA, Intel, Qualcomm
macOSMetalApple silicon, Intel Macs

With no usable GPU, Helia falls back to a multi-threaded CPU path that gives the same results. GPU acceleration explains how the choice is made and what to do when your GPU is not found.

Optional machine-learning features are planned (they would run through ONNX Runtime on CUDA, ROCm, DirectML, OpenVINO or CoreML, with experimental Coral Edge TPU support). None of that exists yet.

How this book is organised

  • The User guide takes you from installing Helia to a finished image, and explains what each setting does.
  • The Reference pages list every option. The command line and API references are generated from the code, so they cannot drift from it.
  • The Developer guide covers the architecture, how to build and test, and how to contribute.
  • The Roadmap lists what is planned, with links to the issues.

Helia is licensed under GPL-3.0-or-later. If it helps your imaging, you can support its development through the links in .github/FUNDING.yml in the repository.


Helia is part of 12hobbies.studio.

Download and verify

This page takes you from nothing to a program you have checked is the one the project published. It is the first of three: this one, then Installation for your system, then First launch.

A pre-release

Helia is at version 0.x, which the project calls a pre-release: the stacking, sharpening, colour and export described in this manual all work and are tested, but some things the roadmap promises are not there yet (drizzle is the biggest) and the interface and file notes can still change between releases. Pre-releases are marked as such. Nothing is stored or sent anywhere unless you ask: Helia has no telemetry. See the roadmap for what is planned.

Get the files

Releases are not public yet: Helia is in early access, so get in touch through 12hobbies.studio and you will be sent the newest release. Each release contains the files below. Download the archive for your system and its checksum.

You haveDownloadChecksum
Windows 10 or 11, 64-bithelia-<version>-x86_64-pc-windows-msvc-setup.exe (installer), or the .zipSHA256SUMS, or the .sha256 file beside it
Debian, Ubuntu, Raspberry Pi OShelia_<version>_amd64.deb or helia_<version>_arm64.debSHA256SUMS
Fedora and other RPM systemshelia-<version>-1.x86_64.rpm or ...aarch64.rpmSHA256SUMS
Any LinuxHelia-<version>-x86_64.AppImage or ...-aarch64.AppImage, or the .tar.gzSHA256SUMS
macOScoming: Helia-<version>-macos.dmgSHA256SUMS
Windows on ARMnot publishedbuild from source: Installation

Every file in the release, the Windows ones included, has its line in SHA256SUMS. The Windows files are attached by a second build and can appear a little after the Linux ones. The release also carries two *.cdx.json files, the lists of libraries inside each program (a software bill of materials). You do not need them to run Helia.

Check the download

A checksum is a short fingerprint of a file. If your copy gives the same fingerprint as the one the project published, the file arrived complete and unaltered. Do this before you run the program.

On Windows, in PowerShell, in the folder you downloaded to (the same works for the -setup.exe):

(Get-FileHash helia-0.2.0-x86_64-pc-windows-msvc.zip -Algorithm SHA256).Hash.ToLower()
Get-Content helia-0.2.0-x86_64-pc-windows-msvc.zip.sha256

The two lines must match. For version 0.2.0 both read:

27930fae6620307520ebdc4af51e472756934ba7f7d50986f4528bf2f2ac498b

(the second line also names the file after the hash). Use the version you downloaded, not this one.

On Linux, in the folder holding the file and SHA256SUMS (a .deb, .rpm, AppImage or archive):

sha256sum -c SHA256SUMS --ignore-missing
helia-0.2.0-x86_64-unknown-linux-gnu.tar.gz: OK

--ignore-missing skips the lines for archives you did not download. If you see FAILED, download the file again; if it fails a second time, do not run it, and tell the project (see Sending feedback).

The programs are not code-signed yet. Windows SmartScreen shows "Windows protected your PC" the first time you run the installer; choose More info, then Run anyway. The checksum above is how you know it is the file the project published. See Installation.

Next

  • Installation: the Windows installer, the .deb, .rpm and AppImage, uninstalling, Raspberry Pi notes, and the optional ffmpeg for MP4 files.
  • First launch: what you will see when you start it.

Installation

There are two ways to get Helia: install a release (Windows, Debian and Ubuntu, Fedora, any Linux through an AppImage; macOS is coming) or build it from source (every platform, a few minutes). Releases are built by the project and shared with early-access users (contact 12hobbies.studio; releases are not public yet).

You get two programs:

  • helia, the command line tool (which also runs the API server);
  • helia-gui, the desktop app.

Download a release

Each release has an installer or package for your system, the plain archives, a SHA256SUMS file and the release notes. Releases before 1.0 are marked as pre-releases. Pick one of the rows that matches your system.

PlatformInstall withFileStatus
Windows 10 or 11, 64-bitthe installerhelia-<version>-x86_64-pc-windows-msvc-setup.exeavailable
Windowsunzip and runhelia-<version>-x86_64-pc-windows-msvc.zipavailable
Debian, Ubuntu, Raspberry Pi OS, Mint.debhelia_<version>_amd64.deb, helia_<version>_arm64.debavailable
Fedora, RHEL and relatives.rpmhelia-<version>-1.x86_64.rpm, helia-<version>-1.aarch64.rpmavailable
Any Linux with glibc 2.35 or newerAppImageHelia-<version>-x86_64.AppImage, Helia-<version>-aarch64.AppImageavailable
Any Linuxunpack the archivehelia-<version>-<arch>-unknown-linux-gnu.tar.gzavailable
macOSdisk imageHelia-<version>-macos.dmgcoming
Windows ARM64not published; build from source

The Windows files are attached by a second build, so they can appear a little after the Linux ones. Download and verify shows how to check a download against its checksum before you run it.

Windows

Run helia-<version>-x86_64-pc-windows-msvc-setup.exe.

The first run shows a SmartScreen warning. The installer is not code-signed yet (#70), so Windows says "Windows protected your PC" and "Unknown publisher". After you have checked the file's checksum, choose More info, then Run anyway. The warning goes away once the project has a signing certificate.

The installer:

  1. shows the GPL and the additional terms that go with it (you must read, not sign: Helia is free software and the pages are there so you know what you are getting);

  2. asks Who is this for? The default, Just for me, installs to %LOCALAPPDATA%\Programs\Helia and needs no administrator rights. All users installs to C:\Program Files\Helia; if you are not an administrator the installer asks Windows for permission and starts again with the choice made, so the licence pages show a second time;

  3. offers these parts, ticked as shown:

    PartDefaultWhat it does
    Heliaalwayshelia-gui.exe (the app), helia.exe (the command line tool), the licences
    Start menu shortcutonHelia in the Start menu
    Desktop shortcutoffan icon on the desktop
    Add the helia command to PATHofflets you type helia in any new terminal
    Open with Heliaonlists Helia under Open with for .ser, .avi and .fits files. It never becomes the default program for them

Helia then appears in Settings > Apps > Installed apps with its version and publisher, and uninstalls from there. Double-clicking helia-gui.exe opens the window with no console behind it; helia.exe is the command line tool and stays a console program.

To install for a whole lab without the pages, from a terminal:

.\helia-0.5.0-x86_64-pc-windows-msvc-setup.exe /S /ADDPATH /DESKTOP

The switches are /S (silent), /AllUsers (needs an administrator), /ADDPATH, /DESKTOP, /NOSTART, /NOASSOC and /D=C:\Where\To\Install (last, with no quotes). Without /AllUsers the install is for the current user and needs no administrator rights, even when silent. With /AllUsers from a prompt that is not elevated the installer stops with exit code 740 and installs nothing; run it from an administrator prompt. The start menu shortcut and Open with are on unless you pass /NOSTART or /NOASSOC; the desktop shortcut and the PATH entry are off unless you pass /DESKTOP or /ADDPATH.

Uninstall: Settings > Apps > Installed apps > Helia > Uninstall, or run uninstall.exe in the install folder with /S. For an all-users install add /AllUsers and run it from an administrator prompt (uninstall.exe /AllUsers /S); the entry in Settings does that for you. It removes the programs, the shortcuts, the Open with entries and the PATH entry. It leaves your settings (%APPDATA%\helia) and your images alone; delete the folder if you want them gone.

If you prefer no installer, unzip the .zip anywhere and run helia-gui.exe.

If something goes wrong on Windows

What you seeWhat it means and what to do
"Windows protected your PC", "Unknown publisher"SmartScreen, because the installer is not code-signed yet. Check the checksum, then More info > Run anyway. If the button is missing, More info has not been pressed yet.
"This app has been blocked by your organisation", or SmartScreen offers no way to run itA policy on a managed PC. Ask your administrator, or use the .zip, which needs no installer.
"Installing for all users needs administrator rights" (exit code 740 when silent)You passed /AllUsers without elevation. Run the installer from an administrator prompt, or leave out /AllUsers.
"Error opening file for writing: helia-gui.exe"Helia is running, so Windows will not replace the file. Close it, then choose Retry.
helia is "not recognised" in a terminalThe PATH entry is off by default and a terminal that was open during the install keeps its old environment. Open a new terminal; if it still fails, run the installer again and tick Add the helia command to PATH, or call helia.exe by its full path.
Helia is missing from Open withThe Open with Helia part was unticked (or /NOASSOC was used). Run the installer again with it ticked. Helia never becomes the default program for the files: choose it under Open with, then Choose another app if the first list does not show it.
Helia is not under Installed appsAn all-users install is listed for every user, a current-user install only for the account that ran it. Look under the account that installed it.
The uninstaller left a folderIt removes only the files it installed. Your settings (%APPDATA%\helia) and any file you added to the install folder stay; delete the folder by hand.

Debian, Ubuntu and Raspberry Pi OS

sudo apt install ./helia_0.5.0_amd64.deb      # arm64 on a Raspberry Pi

(apt install ./file.deb also pulls in the packages Helia recommends, such as the Vulkan loader and Mesa drivers; plain dpkg -i does not.) You get helia and helia-gui in /usr/bin, a Helia entry in your applications menu with its icon, and Helia in the Open with list for SER, AVI and FITS files. Install ffmpeg too if you want to open MP4 files.

Uninstall: sudo apt remove helia. (sudo apt purge helia is the same; the package keeps no configuration. Your settings are in ~/.config/helia.)

If something goes wrong on Debian and relatives

  • dpkg: dependency problems or libc6 (>= 2.35) but ... is installed: the system is too old (Ubuntu 20.04 and Debian 11 are). Build from source, or use a newer system.
  • Unable to locate package ./helia_..., or Unsupported file ... given on commandline: apt install needs the path to start with ./ (or be absolute), and the file must be the one for your CPU (amd64 on a PC, arm64 on a Raspberry Pi with a 64-bit system; dpkg --print-architecture tells you).
  • helia: command not found right after installing: open a new terminal. The programs are in /usr/bin, so a fresh shell finds them.
  • No Helia entry in the applications menu, or it is not under Open with: log out and in, or run sudo update-desktop-database and sudo update-mime-database /usr/share/mime. The package installs the desktop file and a MIME type for SER files (video/x-astro-ser); AVI and FITS are known to the system already.
  • The window opens but the top bar says CPU: no Vulkan driver was found. Install libvulkan1 and mesa-vulkan-drivers, or NVIDIA's driver. See GPU acceleration.

Fedora and other RPM systems

sudo dnf install ./helia-0.5.0-1.x86_64.rpm   # aarch64 on ARM

It installs the same files in the same places, and the troubleshooting notes for the .deb apply, with sudo dnf install ./file.rpm for apt install ./file.deb and the architectures x86_64 and aarch64 (uname -m tells you which). Uninstall: sudo dnf remove helia. The RPM is built for glibc 2.35 or newer, which every supported Fedora has. It has not been installed on a Fedora machine by the project's own tests (there is none), only unpacked and checked on Debian; report what you find.

AppImage

One file, no installation, runs on most Linux desktops:

chmod +x Helia-0.5.0-x86_64.AppImage
./Helia-0.5.0-x86_64.AppImage                  # the app
./Helia-0.5.0-x86_64.AppImage --cli --version  # the command line tool

Pass a capture to open it: ./Helia-0.5.0-x86_64.AppImage capture.ser. If it says it cannot mount itself, your system lacks FUSE; run it with APPIMAGE_EXTRACT_AND_RUN=1 or install libfuse2. An AppImage does not add a menu entry by itself; tools such as AppImageLauncher do that, or copy the .desktop file from the tarball. Uninstall: delete the file.

If something goes wrong with the AppImage

  • Permission denied: the file is not executable. chmod +x it; some browsers and file managers drop the permission.
  • It starts and stops at once with a message about fusermount or libfuse.so.2: see the FUSE note above (APPIMAGE_EXTRACT_AND_RUN=1 or libfuse2).
  • It will not run from a folder: a few systems mount /tmp or the home folder with noexec, which stops AppImages and extracted copies. Put the file in a folder that is not mounted that way.
  • The command line tool is the same file with --cli first: ./Helia-0.5.0-x86_64.AppImage --cli stack capture.ser -o out.tif.
  • Open with is not set up: an AppImage does not register file types. Pass the capture on the command line, or integrate the AppImage with a tool such as AppImageLauncher.

macOS

macOS builds are coming. The project has no Mac to build on yet (#75), so there is no download today; build from source as described below. The packaging is written and waits for a Mac build machine: a Helia.app in a disk image, universal for Apple silicon and Intel, with .ser, .avi and .fits listed under Open With.

When it arrives it will not be signed or notarised until the project has an Apple Developer ID, so macOS Gatekeeper will say Helia "cannot be opened because the developer cannot be verified". To open it the first time: drag Helia to Applications, right-click (or Control-click) Helia and choose Open, then Open again in the dialog. Or open System Settings > Privacy & Security, scroll to the message about Helia, and choose Open Anyway. You only do this once. Uninstall: drag Helia to the Bin; settings are in ~/.config/helia.

The plain archives

The .tar.gz and .zip archives hold the same programs without an installer, for people who want to put them somewhere themselves:

# in the folder you downloaded the archive and SHA256SUMS to
sha256sum -c SHA256SUMS --ignore-missing
tar -xzf helia-<version>-x86_64-unknown-linux-gnu.tar.gz
cd helia-<version>-x86_64-unknown-linux-gnu
./helia --version
./helia-gui

Each archive holds helia, helia-gui, the licences, this project's README and changelog, and THIRD-PARTY-LICENSES.txt for the libraries inside. The Linux one also has share/applications/helia.desktop and the icons under share/icons; copy share/ into ~/.local to get a menu entry.

Things to know about the Linux downloads

  • They need glibc 2.35 or newer: Ubuntu 22.04, Debian 12, Raspberry Pi OS (Bookworm), Fedora 36 or anything newer. On an older system, build from source.
  • They do not bundle a graphics driver. Install the Vulkan loader and a driver (libvulkan1 and mesa-vulkan-drivers, or NVIDIA's driver); without one Helia uses the CPU. The .deb and .rpm recommend them. The desktop app also needs the usual X11 or Wayland and libxkbcommon libraries, which a desktop installation already has.
  • The aarch64 packages are cross-compiled and checked by running the binaries under an emulator, not on real Raspberry Pi hardware. Report what you find.
  • Each release also carries two CycloneDX SBOM files (*.cdx.json) listing the libraries each program was built from. Every package carries the licences: /usr/share/doc/helia in the .deb and .rpm, inside the AppImage, in the install folder on Windows.

Open a capture from your file manager

With the installer or a package you can right-click a .ser, .avi or .fits file and choose Open with > Helia. The same works from a terminal: helia-gui capture.ser. Helia remembers the size of its window and, under Recent in the top bar, the last eight captures you opened.

Optional: ffmpeg for MP4 and other video

Helia reads SER and uncompressed AVI by itself. To open MP4, MOV, M4V, MKV and WebM files, and AVI files that use a compressed codec (MJPEG, H.264), it uses the free ffmpeg program if it is installed. Nothing else changes if it is not: Helia never bundles or links ffmpeg, and without it those files are refused with a message that says how to install it.

You need ffmpeg and ffprobe (they come together) at version 4.4 or newer.

SystemInstall
Windowswinget install Gyan.FFmpeg (or choco install ffmpeg), then open a new terminal
macOSbrew install ffmpeg
Debian, Ubuntu, Raspberry Pi OSsudo apt install ffmpeg
Fedorasudo dnf install ffmpeg (enable RPM Fusion first on Fedora)
Anything elsea static build from ffmpeg.org/download.html

Check it with ffmpeg -version. Helia finds the programs on your PATH. To use others, set HELIA_FFMPEG and HELIA_FFPROBE to their full paths. The helia info command on a video file is a quick test that it all works, and Troubleshooting lists what the errors mean.

Decoded frames are written to a cache directory (the operating system's cache folder by default, --cache-dir or HELIA_CACHE_DIR to choose another), and removed when Helia is done with them. See Capturing for why raw SER or AVI is still better.

Build from source

What you need first

  • A current stable Rust toolchain. The minimum version is the rust-version in the repository's Cargo.toml (1.95 at the time of writing). Install it with rustup.
  • A source checkout of the repository.
  • Optionally a GPU with a working driver. Helia runs without one; see GPU acceleration.

Windows

  1. Install the Visual Studio Build Tools with the "Desktop development with C++" workload. Rust needs the Microsoft linker.
  2. Install Rust with rustup-init.exe from rustup.rs, choosing the default x86_64-pc-windows-msvc host (or the ARM64 one on a Windows on Arm machine).
  3. In a terminal, in the checkout:
cargo build --release

The programs are target\release\helia.exe and target\release\helia-gui.exe. Helia uses DX12 or Vulkan; your graphics driver provides them. If a corporate policy blocks programs built in the checkout (Windows Application Control can do this to build scripts), point CARGO_TARGET_DIR at a folder that policy allows.

Linux, including Raspberry Pi

Install a C toolchain, the Vulkan loader and, for the desktop app, the windowing libraries. On Debian and Ubuntu:

sudo apt-get install build-essential pkg-config libvulkan1 mesa-vulkan-drivers \
    libxkbcommon-dev libwayland-dev

mesa-vulkan-drivers covers AMD, Intel and the Raspberry Pi; NVIDIA owners use NVIDIA's own driver instead. Then:

cargo build --release

On a Raspberry Pi use a 64-bit operating system (Raspberry Pi OS 64-bit or Ubuntu for Pi). Helia lists the Pi 4 and Pi 5 as supported GPUs, through the Mesa Vulkan driver with GLES as a fallback. The project's automated tests run on 64-bit ARM Linux with a software GPU, but not on Raspberry Pi hardware, so report what you find. Compiling on a Pi is slow; cross-compiling from a PC is an option.

macOS

Install the Xcode command line tools (xcode-select --install) and Rust. Helia uses Metal, which needs no extra setup on Apple silicon or Intel Macs.

cargo build --release

Check it works

target/release/helia --version
target/release/helia info path/to/capture.ser

To try Helia without a capture, write a synthetic one with cargo xtask sample, described in the Quick start.

To install the programs on your PATH instead of running them from target:

cargo install --path crates/helia-cli
cargo install --path crates/helia-gui

First launch

You have downloaded, checked and installed or unpacked Helia (see Download and verify and Installation). This page is what happens next.

Check the programs run

Helia is two programs, helia (the command line tool) and helia-gui (the app). After a Debian, Ubuntu or Fedora package they are in /usr/bin, so in any new terminal:

helia --version
helia-gui --version

From the Windows installer, helia is only on the PATH if you ticked Add the helia command to PATH (and opened a new terminal afterwards). Otherwise run it by its folder, by default %LOCALAPPDATA%\Programs\Helia: & "$env:LOCALAPPDATA\Programs\Helia\helia.exe" --version. From the zip or a tarball, run .\helia.exe --version or ./helia --version in the folder you unpacked. From an AppImage, ./Helia-<version>-x86_64.AppImage --cli --version runs the command line tool and the plain file runs the app.

Each program prints its version, when and how it was built and the licence. Neither opens a window; this is the quick test that the program starts on your machine. On Windows helia-gui.exe is a window program with no console of its own, so in PowerShell its text can arrive after the next prompt has appeared; helia.exe is the better tool for this test. A line such as wgpu: 30.0.1 is the graphics library Helia was built with, not a requirement on you.

Start the desktop app

Choose Helia in the Start menu (Windows installer) or the applications menu (.deb and .rpm), double-click helia-gui.exe or the AppImage, or run helia-gui in a terminal. The first start after installing from Windows can show the SmartScreen warning described in Installation. A window opens with the nine steps down the left and a dark, empty viewer. The top bar offers About, Open capture and Export.

If Helia finds a graphics card it says so in the top bar, for example Vulkan and the adapter's name. If it finds none, it says CPU and works anyway; the answers are the same, only slower for sharpening previews. See GPU acceleration.

The first-run strip

The first time, a Quick start strip lists the three steps: open a capture, press Stack with recommended settings, pick a colour and export. Press Got it and it never returns. Helia remembers that, and the look you choose for each filter, in a small settings file (see Settings file). Nothing else is saved, and nothing is sent over the network.

Open your first capture

Press Ctrl+O, drop a file on the window, pick one under Recent in the top bar, or right-click a .ser, .avi or .fits file and choose Open with, Helia (or run helia-gui capture.ser). Helia remembers the size of its window and the last eight captures. If you have no capture to hand, any SER or AVI of the Sun will do, or a short phone MP4 if you have installed ffmpeg. From here, follow the step-by-step walkthrough: it goes from the Open step to a finished picture with a screenshot at every step.

If you prefer a terminal, the Quick start shows the same journey on the command line, and the recipes are short, complete jobs for the common captures.

Quick start

A few steps take you from a capture to a sharp, coloured, shareable picture: open the file, press Stack with recommended settings, then look at the sharpening, choose a colour and export. Helia works out the rest. This page shows the desktop app first, then the command line. You need Helia built or installed as described in Installation.

Have a capture ready

Use a SER or uncompressed AVI file of the Sun (or, with the optional ffmpeg installed, an MP4), ideally a few hundred frames or more. Capturing for Helia explains how to record a good one.

Put the filter in the file name. Helia reads the name of the file, the folder it is in, the SharpCap sidecar and the SER header for the filter, and recommends settings for it: 2026-10-19_Sun_Ha_001.ser, CaK_session/capture.ser and sodium-d.avi are all recognised. Part of a word does not count, so Hampshire is not H-alpha. If Helia cannot tell, it asks once. The words it looks for are in Colourising.

No capture to hand? From a source checkout you can write a synthetic one (a made-up sun with known blur and shifts, 400 frames, 320 by 288 pixels):

cargo xtask sample synthetic-sun_Ha.ser

It is a way to try the tools, not a picture of the real Sun. The _Ha in the name makes Helia treat it as an H-alpha capture.

With the desktop app

Start the app, optionally with a file:

helia-gui synthetic-sun_Ha.ser

The first time, a short Quick start strip reminds you of these steps. Press Got it and it never comes back; Helia remembers in its settings file.

The first screen: the Quick stack card with the detected filter

  1. Open. Press Ctrl+O (or choose Open capture) and pick the file, or drop it on the window. The Quick stack card appears on the right. It says which filter Helia found and from what, and lists the settings it will use: the colour channel (for colour cameras), the share of frames to keep, and the alignment point size for your image. If it guessed wrong, press the right filter; if the filter is not in the list, press Not sure for plain defaults.

  2. Stack. Press Stack with recommended settings. A notice above the picture says what Helia is doing (scoring the frames, aligning them, stacking) with a progress bar and the time left, and a Cancel button. When it finishes, a message says how many frames went into the stack and how long it took, and then fades away.

    A finished stack, with the message that reports it

  3. Sharpen. Helia moves on to the Sharpen step. The recommended wavelets for your filter are already applied, so the picture is sharpened; press Before / after to compare it with the plain stack, drag a slider to change something, or press Off to skip it. Sharpening can create artefacts, so look closely; see Sharpening and adjustments.

    The Sharpen step with the picture split into before and after

  4. Choose a colour. Next is the Colour step. The stack is shown in the colours of your filter, such as orange for H-alpha. Pick another look if you like, turn on Invert to bring out prominences, or keep it grey. Colour is for sharing; see Colourising.

    The Colour step

  5. Export. Choose TIFF, PNG or FITS and press Save as, or press Ctrl+E. The step says exactly what will be written, such as 16-bit RGB TIFF, H-alpha colours, with the edits. A FITS file is the unsharpened stack unless you turn on Edit the FITS too.

    The Export step

Every step in the rail on the left carries a one-line hint about what to do next: Open a capture, Ready: press Stack, Done: sharpen, colour and export. If you want to change a setting, the other steps hold them, and Advanced settings at the foot of the right-hand panel shows the backend, demosaic method and channel. Using the desktop app describes every control.

With the command line

First, check Helia can read the file:

helia info synthetic-sun_Ha.ser
file:        synthetic-sun_Ha.ser
format:      SER
size:        320 x 288
colour:      Mono
depth:       16 bit
byte order:  header flag says little-endian, pixel data decoded as little-endian
frames:      400
timestamps:  absent

Then stack it. With nothing but an input and an output, Helia detects the filter, the channel and the alignment point size itself:

helia stack synthetic-sun_Ha.ser -o sun.tif

The output extension picks the format: .tif for a 16-bit TIFF, .png for an 8-bit PNG, .fits for FITS. Helia prints the choices it made, a line as each stage starts, and then a summary. This is the real output for the synthetic capture above, run on the CPU of a 24-thread server (--backend cpu); the timings are that machine's, not a promise:

input:    SER 320x288 16-bit, 400 frames
backend:  CPU (24 threads)
settings: H-alpha (from "synthetic-sun_Ha"); keep 25%, alignment points 32 px
tip:      add --colour auto for H-alpha colours
[   0.0s] scoring frames
[   0.1s] building reference
[   0.1s] aligning frames
[   0.1s] measuring alignment points
[   0.3s] stacking
[   0.3s] finishing
[   0.3s] writing
frames:  400 total, 100 kept, 200 registered
points:  123 of 399 alignment points active
disc:    centre (164.5, 146.9), radius 100.4 px
detail:  stack 0.00526 vs best single frame 0.00525 (1.00x)
timing:  scoring frames 0.06s, building reference 0.01s, aligning frames 0.03s, measuring alignment points 0.12s, stacking 0.06s, finishing 0.00s (total 0.31s)
wrote:   sun.tif

Reading it:

  • settings is what Helia chose and why. A colour camera in H-alpha would also say red channel. Anything you pass on the command line (--keep, --ap-size, --channel) wins over the recommendation.
  • kept is the number of frames that went into the stack. The default is the best 25 per cent (12 per cent for the photospheric filters, white light and Na D); change it with --keep 10% or --keep-frames 500.
  • registered is how many frames were aligned. With multi-point alignment Helia registers twice as many frames as it keeps, so each alignment point can choose its own best ones. See How stacking works.
  • points is how many alignment points were bright and detailed enough to measure.
  • detail compares the sharpness of the stack with that of the best single frame (a synthetic capture has nothing to sharpen, so it reads 1.00x), and timing gives the time of each stage.

--quiet hides the stage lines, the progress bar and the summary.

The file is greyscale. To colour it for your filter, add --preset, which also applies the filter's recommended channel and frame share:

helia stack synthetic-sun_Ha.ser -o sun-ha.tif --preset ha
helia stack cak.ser -o cak.png --preset cak --invert
helia presets                      # the presets and what each does

--preset colours .tif and .png output. A .fits file stays mono and unaltered; the preset is noted in its header. See Colourising for what each look is and why.

Useful variations:

helia stack sun.ser -o sun.fits --keep 10%     # fewer, better frames, FITS output
helia stack sun.ser -o sun.tif --ap-size 48    # smaller alignment points
helia stack sun.ser -o sun.tif --backend cpu   # force the CPU
helia stack sun.ser -o sun.tif --quiet         # no progress bar, no summary

Add --sharpen wavelet to sharpen the stack with the recommended values for the filter (and unsharp, rl or none); it runs on the mono stack before any colour. A FITS file stays the unsharpened stack unless you add --sharpen-fits. helia edit sharpens and adjusts an image that already exists. See Sharpening and adjustments. The full list of options is in The command line and the CLI reference.

What next

Step by step

This part of the manual follows one job from start to finish in the desktop app, a screenshot at every step. It goes from a raw video of the Sun to a finished, coloured, sharpened picture, and says at each step what the controls do and what to look for. Read it once in order; afterwards the steps are reference.

  1. Open a capture: the header, what Helia will use, the overrides
  2. Quick stack: the one-button route
    • Calibrate (optional): dark, flat and bad pixels, before anything else
  3. Analyse: which frames are sharp
  4. Align: alignment points, exclusions, the limb fit
  5. Stack: combining the best frames
  6. Sharpen: wavelets, unsharp mask, deconvolution
  7. Adjust: denoise, levels, curve, crop
  8. Colour: the filter looks
  9. Export: TIFF, PNG and FITS
  10. About
  11. Send feedback

The captures used

Three real captures of the Sun, taken on 17 July 2022 with a ZWO ASI482MC colour camera, are used throughout. They are copies, renamed so that the pictures show no folder names.

Name in the picturesWhat it isUsed for
disc-sample.serA full disc, 1920 x 1080, 16-bit, 100 framesThe main walkthrough: analyse, align, stack, sharpen, adjust, colour, export
ha-sample.aviAn H-alpha capture at 720 x 540, 8-bit, 1000 frames, with the SharpCap CameraSettings.txt beside itThe Open step with a filter Helia recognises from the file name
bayer-sample.ser1920 x 1080, 8-bit, 631 frames, a SER whose header says mono although the pixels are a colour mosaicThe header view and the Bayer notice

You can follow the same steps with your own captures; the numbers on your screen will differ.

How the screenshots were made

They are real: the application itself, running on those captures with its real worker threads, drawn to an image by the test harness and saved without being touched up. The graphics adapter named in the top bar is the one of the machine that made them (an NVIDIA A100 on a Linux build server), not a recommendation. Where a picture of the finished image is shown (the before and after pairs, the colour gallery) it is the image Helia writes, cropped or reduced in size, not a window.

The set is made by one test, real_capture_shots in crates/helia-gui/src/real_shots.rs, which does nothing unless it is given a capture. See Building and testing. The other pictures in the book, under images/ui, are the synthetic snapshots that the ordinary test run checks against the interface.

The app has its own scripted mode, helia-gui --open FILE --shots DIR, which writes screenshots of a live window as it opens and stacks a capture. It needs a window that is being painted and stops at the Export step, so the book uses the off-screen test, which also reaches Sharpen, Adjust, Colour and About.

1. Open a capture

Press Ctrl+O, choose Open capture in the top bar, or drop a file on the window. Helia reads SER and uncompressed AVI files, and with the optional ffmpeg installed also MP4, MOV, MKV and WebM (see Installation).

While it opens, a notice above the picture says Opening the file and indexing its frames. Nothing is loaded into memory: the frames stay on disk and are decoded as you scrub.

The Open step on a full-disc SER: the top bar with the capture details, the pipeline rail, the first frame, the quality curve and the Quick stack card

What you see

  • Top bar. The Helia name with a Pre-release badge while the version is below 1.0, then the file name, the camera, the frame size, the bit depth with the Bayer pattern, and the number of frames (a long line is cut short with an ellipsis). On the right, the compute backend, then Feedback, About, Open capture and Export.
  • Pipeline rail on the left: nine steps, each with its state and a one-line hint. Click a step to select it.
  • Viewer in the middle: the first frame, a frame scrubber (drag it, or use the arrow keys) and the quality curve, every frame's sharpness sorted from best to worst. The dashed line marks where the frames you will keep end.
  • Inspector on the right: the settings of the selected step. In Open it holds the notice about the file, the Quick stack card, the file facts and the overrides.

In the picture above, Helia noticed something about the file: the header flag says big-endian, but the pixel data only makes sense as little-endian; Helia reads it that way. That is a common quirk of SER files and it is handled for you. Every such decision is written down in the notice, never made silently.

A capture Helia recognises

Helia reads the filter from the file name, the folder, the SharpCap sidecar and the SER header. This capture is called ha-sample.avi, so Helia knows it is H-alpha and says where it got that from:

The Open step for an AVI called ha-sample: "H-alpha detected from ha-sample", with the colour channel, frame share and alignment point size it recommends

The card lists what it will use: the red channel (H-alpha is deep red, so a colour camera records it almost only in the red pixels), the best 25 per cent of the frames (250 of 1,000) and an alignment point size. The note above the card says the AVI does not state its Bayer pattern but the SharpCap sidecar does, so Helia debayers it as RGGB. If the filter's name is not in the file name, the card asks Which filter was this? and you press the right one, or Not sure for plain defaults, as in the first picture.

When the header is wrong

Some recorders label a colour camera's raw frames as mono. If you stacked such a file as it says, you would get a fine grid in the result. Helia checks the pixels of a SER file that says mono, and when it finds a mosaic it tells you:

The Open step for a SER whose header says mono: the Check this card, the Helia will use list with each value's source, the header as written and the overrides

Read the picture from the top:

  1. Check this explains, in words, what the file says and what the pixels say. Here the header says mono (ColorID 0), the pixels are an RGGB mosaic, and Helia will debayer as RGGB. A short message also appears at the bottom of the window.
  2. Helia will use lists the Bayer pattern, bit depth, byte order and row order with where each came from: header, sidecar, detection or override. Anything that is not straight from the header is shown in the accent colour. Here the Bayer pattern is RGGB, from detection.
  3. SER header, as written in the file shows every field exactly as stored: FileID, ColorID with its meaning, endianness, size, depth, frame count, observer, instrument, telescope, and whether times are set.
  4. The overrides, the rows of buttons at the foot: Bayer pattern, bit depth, byte order and row order. Each reopens the capture with your choice, and the list above updates to show override as the source.

If Helia is right, do nothing. If you know better, press the pattern that is true (or Mono if the file really is mono). If the colours come out swapped after stacking, try the other pattern of the same family: RGGB or BGGR, GRBG or GBRG. The recipe Fixing a Bayer grid covers it, and helia info on the command line prints the same lists.

Overrides belong to the file: opening another capture starts again from Auto.

Next

Quick stack is the one-button route. If you want to see each step instead, go on to Analyse.

2. Quick stack

The Stack with recommended settings button on the Open step does the whole middle of the job for you: it applies the settings on the card, scores and aligns the frames, stacks the best of them, and moves on to Sharpen with the filter's recommended wavelets already applied.

The Quick stack card on the full-disc capture: the filter question, the channel, the share of frames and the point size, and the button

Use it when you want a good first picture. The rest of the steps then become a place to adjust, not a place you must visit.

What happens when you press it

  1. A notice above the picture names the current stage, with a progress bar, the time left, and a Cancel button. The stages are Scoring the frames, Building the reference image, Aligning the frames, Measuring the alignment points, Stacking the best frames and Finishing the image.
  2. When it ends, a message at the bottom right says how it went. For the full-disc capture: Stacked 25 of 100 frames in 1.9 s. For the 1,000-frame H-alpha capture: Stacked 250 of 1,000 frames in 2.7 s. (The times are those of one machine with a GPU, not a promise.)
  3. The viewer switches to the stacked image and the Export button lights up.
  4. Helia moves on to Sharpen.

If something looks wrong, the settings are in the other steps, and pressing the button again puts the recommended settings back.

Do I need the other steps?

Often not. The steps exist so that you can look closer when a result disappoints:

IfLook at
Frames you expected to be best are ranked lowAnalyse
The grid of alignment points sits on text or skyAlign
The stack is soft, or too noisyStack and Sharpen
The brightness or contrast is offAdjust
You want colour, or want it differentlyColour

Calibrate (optional)

The second step in the rail. It is optional, and skipped by the quick stack, but if you recorded darks and flats it is the first thing to do: everything after it works on the calibrated frames.

The Calibrate step with a dark and a flat chosen: the frame before (left) and after (right), and the masters in force

The picture is synthetic on purpose: a sun seen through a sensor with a strong vignette, dozens of dust shadows, a fixed pattern and sixty hot pixels, which the masters made from that sensor's own darks and flats take out. On a real capture the effect is subtler and this is how you check it: drag the line across the picture and watch the dust, the fall-off at the edge and the speckle in the sky.

What to do

  1. Press File under Dark and pick a master dark (master-dark.fits from helia calibrate), or Folder and pick a folder of dark captures. The rail shows Preparing while Helia reads or builds it.
  2. Do the same for the Flat. If the flat is captures, a Dark for the flat slot appears; give it darks at the exposure of the flats.
  3. Leave Bad pixels empty and the hot pixels of the dark and the dead pixels of the flat are found and replaced, or give your own map.
  4. Read what the inspector says under In force: how many frames each master combines, the exposure and a fingerprint of the pixels, and any warning. A flat with Newton rings or an etalon's fringes is flagged here, and so is strong vignetting; see Etalons and Newton rings.
  5. Press Save the masters to write them to a folder, so that next time you can pick the files and skip the combining.
  6. Press Stack (or Stack with recommended settings) as usual. The frames are scored, aligned and stacked after calibration, and the file records the masters.

A master must come from the same camera, mode and region as the capture; Helia refuses a mismatch and says which. Changing a file scores the capture again, and a stack made before the change is out of date.

Next

Analyse.

3. Analyse

As soon as a capture opens, Helia scores every frame for sharpness, in the background. The rail shows Scoring and then Scored; while it works a notice shows a count and the time left. You can look at the capture meanwhile.

The Analyse step: the sharpness metric choice, the first frame and the quality curve, sorted

The quality curve

Under the viewer, Quality, sorted plots every frame's score from the best on the left to the worst on the right. Read it like this:

  • A curve that falls steeply at the left and then flattens means a few frames were much sharper than the rest, which is typical in poor seeing. Keeping fewer frames helps.
  • A curve that is nearly flat means the seeing was steady, or the frames are all about as good, and keeping more frames costs little.
  • The dashed line marks where the kept frames end, the shaded area is the kept set, and the white dot is the frame on screen. Hover to read a rank; click to jump to that frame.

The scrubber reads, for example, Frame 1 / 100 · quality 0.42 · rank 72. Quality runs from 0 (the worst frame) to 1 (the best); rank 1 is the sharpest. So this first frame is not one of the best, which is why Helia ranks the frames itself and does not trust the order they were recorded in.

The metric

The inspector offers two ways to score a frame:

  • Gradient energy, the default, is the mean squared gradient of the frame.
  • Laplacian variance is the variance of the Laplacian.

Both ignore overall brightness, so a brighter frame does not look sharper. Changing the metric scores the capture again. Most users never change it; see How stacking works.

Next

Align.

4. Align

The air bends the picture differently across the disc from moment to moment, so Helia does not only line the whole frame up with the others. It also measures the drift of many small windows, the alignment points (APs), and shifts each part of each frame separately. This step shows and controls those windows.

The Align step: the alignment point grid over the disc, the point size choices, and the exclude regions panel

The grid

Turn the grid on with the Alignment points chip above the picture. Each thin square is one alignment point. In the picture there are 612 of them, 64 pixels across, laid over the disc only: a point is drawn only where the stack will use it, which means its centre is inside the fitted limb of the Sun, and no part of it touches burned-in text or a region you excluded.

Point size

The inspector offers sizes from 32 to 96 pixels in steps of 8. The size Helia recommends for your image is already selected (64 here, 32 for the smaller H-alpha AVI).

  • Smaller points follow finer detail and distortion, but need contrast to lock on to. On a smooth, low-contrast area they cannot, and Helia marks them inactive.
  • Larger points are steadier on soft detail but average the distortion over a wider patch.

The card shows how many points are active, that is bright and detailed enough to measure. After a stack, a second card shows the median drift of the frames from the reference, in pixels.

Auto

The Auto chip lets Helia choose when it stacks, instead of a fixed size. It fits the limb of the reference to get the image scale in arcseconds per pixel, registers a dozen of the frames it will stack and measures how far each alignment point strays from its frame's median shift, then picks the window, its spacing and how far to search. The line under the chips says what to expect, and after a stack what was chosen. Picking a size turns Auto off.

The Align step with Auto on, before any stack: the note says what will happen

After a stack the grid drawn on the frame is the chosen size.

The Align step after a stack with Auto on: the note names the size, spacing, search range, image scale and local warp

These pictures are of a synthetic capture. The note reads, for instance, Chosen by the last stack: alignment points 32 px every 16 px, search 4 px (8.79 arcsec per pixel; local warp 0.35 px (3.03 arcsec)). How the choice is made and how it measured on real captures is in How stacking works.

Exclude regions

A burned-in capture timestamp is found automatically (look at the top left of the frame: Helia kept the grid off the date text). For anything else, such as a logo or a stray label, press Add region, then set its x, y, width w and height h in pixels. The rectangle is outlined in amber on the frame, and alignment and quality scoring stay off it.

The Align step with one exclusion rectangle drawn over the timestamp at the top left of the frame

Excluded areas are flat in the stacked result, so do not exclude part of the Sun you care about. Use the minus button to remove a region. Changing the list scores the capture again, because the scores depend on it.

How whole frames are aligned

Before the points, each whole frame is lined up with a reference. Helia chooses:

  • the solar limb when the full disc is in view and the fit is confident, which uses the edge of the Sun and so ignores sunspots and the timestamp;
  • phase correlation for a cropped region, where there is no whole disc to fit.

After a stack, a line under the Stack button says which was used and how confident it was, for example Aligned by limb fit, confidence 0.90; hover it for the reason. The app has no switch for it. To force one, use --align limb or --align phase on the command line, or align in an API job.

Prominences

By default points stay on the disc. To let them follow prominences just outside the limb, use --prominence-band PX on the command line.

Next

Stack.

5. Stack

This step holds the settings of the combine, and the button that runs it.

The Stack step: the share of frames to keep, how to combine them, sigma clipping and the Stack button

The settings

  • Keep best frames is the share of frames, ranked by quality, that go into the stack, from 5 to 100 per cent in steps of 5. The line underneath turns it into a count: here 25 per cent of 100 frames is 25. Fewer frames means a sharper but noisier picture; more means smoother and softer. The recommended share for a filter is the best starting point: 25 per cent for H-alpha, sodium, He D3 and white light, 30 per cent for the fainter lines (see Colour presets).

  • Combine is a weighted mean: each kept frame counts in proportion to its quality. A plain mean is listed but not available yet.

  • Outliers deals with pixels that are far from the others, such as a passing bird or a hot pixel: Sigma throws them out, Huber gives them less weight and Winsor pulls them in. It is off by default and makes the run slower.

  • Colour is Filter look (one plane, coloured later in the Colour step) or True colour, which stacks red, green and blue of a raw colour capture on one alignment and balances them: for white light. Its Auto balance or No balance and Align channels (red and blue onto green, for atmospheric dispersion) appear when it is chosen, and the chip is disabled for a capture that is not a Bayer mosaic. See True colour.

    The Stack step with True colour chosen and a stack done: the note gives the gains and the channel shifts

  • Crop is Off (the whole frame) or Disc, which cuts the result to a square around the fitted solar disc with a margin you set (10 per cent of the radius to start with; prominences seldom reach 5 per cent). The note under it says what will happen and, after a stack, the size it was cropped to. A capture with no clear disc is left whole and the note says so.

  • Stack N frames starts the run.

The Stack step with the Disc crop on and the margin slider, after a stack: the note says the last stack was cropped to 241 x 241 px

While it runs

A notice above the picture names the stage, shows the percentage and the time left, and has a Cancel button; the thin bar in the inspector shows overall progress. Cancelling stops the run without losing your settings. These are the stages:

Stacking in progress: a notice naming the stage with its percentage and time left

  1. Scoring the frames, if not already done.
  2. Building the reference image: a first stack of the best frames to align against.
  3. Aligning the frames: the whole-frame step.
  4. Measuring the alignment points: each point's drift in each frame.
  5. Stacking the best frames: the weighted mean with every point's shift applied.
  6. Finishing the image.

The result

When it finishes, a message reports the frames used and the time taken, the viewer switches to the stacked image, Export lights up and Helia moves on to Sharpen.

The stacked result on the Sharpen step, with the message "Stacked 25 of 100 frames in 1.9 s"

The stack is mono. It looks soft compared with a good single frame at first glance, and that is expected: it is smooth because the noise has been averaged away, and the sharpening step is where the fine detail is brought back. For how the maths works, see How stacking works.

Next

Sharpen.

6. Sharpen

A stack is clean but soft. This step brings the fine detail back. It works on the mono stack, before any colour, and is non-destructive: your choices are a list of operations applied to the stack for the preview and again, at full size, when you export.

Sharpening can create artefacts. Halos beside bright edges, ringing at the limb and round sunspots, and noise that turns into a texture that looks like detail. None of the methods can add detail that the telescope and the seeing did not record. Ease off as soon as the picture looks different in kind rather than just crisper.

Limb darkening

The first control is Limb darkening: Off, Model or Profile, and the law to fit (Linear, Quadratic, Neckel). The line under the chips is the fit made on your stack, for example quadratic: u1 = 0.470, u2 = 0.046; limb at 48% of the centre; rings differ from the law by 0.67% rms. Model divides the disc by that law and Profile by the measured radial profile, so the sharpening below sees flat contrast from the centre to the limb. Only the disc changes. It is off by default; try it with Before / after, and see Limb darkening for what the fit measures on real captures.

The Sharpen step with Model chosen under Limb darkening, split before and after

The method chips

The Sharpen step with Wavelets selected: a gain and a noise cut for each of the six layers

The row of chips at the top picks the method: Off, Wavelets, Unsharp or Deconvolution. Helia has already switched on the recommended wavelets for your filter, so the picture you see when you arrive is sharpened. Press Off to see the plain stack. Every slider updates the picture as you drag it, and a line says how long the last preview took (42 ms in the picture).

Two chips above the picture help you judge: Before / after splits the picture at a line you can drag (the stack as it was on the left, with your edits on the right), and Full size renders the preview at full size rather than shrunk. A preview is a close look, not a proof: what is written is always made from the full-size stack.

Wavelets

The image is split into six layers by the size of detail. Layer 1 holds the finest detail and most of the noise, layer 2 fine detail such as granulation and fibrils, and higher layers bigger structure. Each layer has a gain (1 leaves it alone, above 1 sharpens it) and a noise cut (detail weaker than this many noise sigmas is treated as noise and removed). The recommended values for a filter are in Sharpening and adjustments. Reset to recommended puts them back.

Here is the effect on the real full-disc stack. This is a crop of the stacked image, the plain stack on the left and the recommended wavelets on the right:

A crop of the stack: before sharpening on the left, after the recommended wavelets on the right

The change is modest, and it should be: this is 25 frames, in an ordinary afternoon's seeing. The filament at the left and the bright plage at the top right are crisper and the background grain is finer, not coarser.

Unsharp mask

The Sharpen step with the unsharp mask: radius, amount and threshold

The radius is the size of the blur that is subtracted, which picks the size of detail that is boosted. The amount is how much of the difference is added back (0.6 is gentle, 2 strong). The threshold leaves weaker detail alone so noise is not boosted. It is a light, predictable touch:

A crop of the stack: before on the left, after the unsharp mask on the right

Deconvolution

The Sharpen step with Richardson-Lucy deconvolution: blur, iterations, noise control and the limb guard

Richardson-Lucy deconvolution tries to undo the blur itself, by repeated correction. It has the blur to undo (the PSF), the number of iterations, a noise control and a limb and sunspot guard that stops the bright halo that a bright edge would otherwise grow. It is the strongest and the riskiest method.

A crop of the stack: before on the left, after twelve iterations of deconvolution on the right

On this capture it gains little and makes the grain coarser, which is why wavelets are the default: stop when noise starts to look like texture. The page Sharpening and adjustments has the measurements on four real captures.

My presets

My presets, below the controls, keeps the whole set of sharpening and adjustments under a name. Type a name and press Save; press a preset to use it. The command line takes the same presets with --edits.

Next

Adjust.

7. Adjust

Everyday edits, each optional and each recorded in the file. They are applied after the sharpening, in a fixed order: denoise (before the sharpening), levels, curve, crop, rotate, flip, resize.

The Adjust step with denoise, levels and a gentle curve switched on

The screenshot has three adjustments switched on, which the rail counts (3 adjustments) and the pill at the foot of the picture mentions (adjusted).

  • Denoise is a soft threshold on the finest wavelet layers. Strength is the threshold on layer 1 in noise sigmas (1.5 here) and Layers is how many layers it treats. It lowers noise on flat sky a good deal and leaves the contrast of a sharp edge alone.
  • Levels: a black point, a white point and a gamma. Raising the black point a little (0.02 here) deepens the sky; a gamma above 1 (1.15) brightens the mid-tones. Auto levels sets the points to the 0.1 per cent and 99.9 per cent points of the stack; Reset puts them back.
  • Curve: a tone curve through up to 16 points, drawn as a smooth curve that never overshoots. Press Add point, set its input and output, and the picture follows. Here a point at 0.5 in, 0.45 out gently darkens the mid-tones.
  • Crop (a rectangle in pixels, or the whole image), Rotate and flip (quarter turns, a fine angle, mirrors) and Resize (10 to 400 per cent) are below the curve; scroll the inspector to reach them.

A line under the controls gives the size of the picture that will be written. Reset all adjustments switches everything off.

Tips

  • Use levels and gamma first: they are the least likely to hurt.
  • Denoise before you push the sharpening, not after: noise that is not there cannot be sharpened into texture.
  • A curve is for looks, not measurement. If you want to measure the Sun, export the FITS and leave it unedited (see Export).

Next

Colour.

8. Colour

For a stack made with True colour (white light, a colour camera) this step shows the gains and the channel shifts instead of the look cards: the stack is colour already and no look applies.

The Colour step after a true-colour stack

Colour is for sharing. The stack stays mono; the look you choose here is applied to it when you view and when you export. A narrow-band solar capture is one colour of light, so the colours are a convention (orange for H-alpha, violet for calcium), not a measurement. See Colourising for the reasoning.

The Colour step with H-alpha chosen: one card for each look, shown as a ramp from black to white, with its wavelength

Each card shows a look as a ramp from black to white, with its filter and wavelength. Click one and the viewer shows the stack in those colours at once. Under the cards a line says why the look is what it is.

Every look

The same stack in each of the eight looks, reduced in size. The first card in the app, Mono, is the stack in grey.

The same stack in H-alpha, Ca II K, Ca II H, Mg I b, Na I D, He I D3, H-beta and white light colours

LookWavelengthColourUse it for
H-alpha656.3 nmorange-redThe chromosphere: filaments, plage, prominences
Ca II K393.4 nmvioletThe chromosphere and plage in calcium
Ca II H396.8 nmviolet, a touch redderThe same, so a K and H pair can be told apart
Mg I b517.3 nmgreenThe lower chromosphere
Na I D589.0 and 589.6 nmyellow-orangeSodium
He I D3587.6 nmyellowFaint prominences and flares
H-beta486.1 nmblue-cyanThe chromosphere in H-beta
White lightcontinuumwarm neutralThe photosphere: sunspots and granulation

The right look is the one of your filter. The look Helia recommends comes from the file name, folder, sidecar or header. The app remembers the look you chose for each filter, so the next capture in the same filter starts with it.

The two controls

  • Invert makes a negative before colouring, with a dark disc and bright prominences and filaments, which shows prominences well.
  • Strength mixes between grey (0 per cent) and the full colour (100 per cent).

Continue to export goes on.

The finished picture

Here is the finished full-disc stack: the recommended wavelets, H-alpha colours, written as it would be to TIFF (this is a reduced copy):

The finished H-alpha picture of the full disc

Next

Export.

9. Export

Choose a format, press Save as, and say where to put it. Ctrl+E does the same from any step.

The Export step: the format choices, what will be written, and the Save as button

FormatWhat it isUse it for
TIFF 16-bitGreyscale or RGB, 16 bits per channelYour master picture, and any further editing
PNG 8-bitGreyscale or RGB, 8 bits per channelSharing on the web and in messages
FITS32-bit floating point; mono, or a three-plane cube for a true-colour stackMeasurement and further processing

The step says exactly what will be written, here 16-bit RGB TIFF, H-alpha colours, with the edits, and what produced it: the edits (the number of sharpening and adjustment operations) and the colour look. The edits run on the full-size stack, then the colour is applied. Writing happens in the background with a notice, and a message confirms where the file went.

What the files say

Every file notes how it was made, so a picture can be repeated and understood later. For TIFF the note is in the ImageDescription tag, for PNG in the Description chunk, and for FITS in HISTORY cards. It lists the edits in words and as a list that can be reused, and the colour look. The Bayer decisions (including a header that was wrong) are recorded too. See What the files say.

FITS and SOLARNET

A FITS file is the unsharpened stack, because it is for further processing; the edits go into TIFF and PNG. Turn on Edit the FITS too if you want them in it, and the step says which it will be. The header carries observation keywords in the SOLARNET convention, partially compliant (SOLARNET = 0.5), plus what Helia knows of the capture. The details, including how to give your observing site for the position keywords (--site), are in Output formats.

From the command line

The extension picks the format:

helia stack disc-sample.ser -o sun.tif --preset ha --sharpen wavelet
helia stack disc-sample.ser -o sun.png --preset ha --sharpen wavelet
helia stack disc-sample.ser -o sun.fits

Next

About.

10. About

The About button in the top bar, or F1, opens the About page, which replaces the viewer until you press Esc or Close.

The About page: the logo, the dedication, the version block and the licence

From the top:

  • The dedication. Helia is dedicated to the author's daughter, whose middle name is Helia. The dedication is part of the licence terms and is preserved in every copy.
  • Version. The version (with a note that it is a pre-release, below 1.0), and how this copy was built: the git commit (with +dirty if the tree had uncommitted changes), the build time in UTC, the build profile, the target, the Rust and wgpu versions, and the graphics adapter in use, or CPU. Copy version info puts those lines on the clipboard, which is what to paste into a bug report. The screenshot is of a test build: a release shows its own commit.
  • Copyright and licence. Helia is free software under the GNU General Public Licence version 3 or later, with additional terms under section 7 of that licence: the copyright line and the dedication must be kept, and modified versions must be marked as changed. The page links to the full text.
  • Links: the source, the licence text, the releases page and, at the foot, the third-party licences. In a release archive that list is THIRD-PARTY-LICENSES.txt beside the program.

The page makes no network connection. A link opens in your browser only when you click it.

Next

Send feedback.

11. Send feedback

Helia is a pre-release, and the quickest way to make it better is to tell the project what is wrong or missing.

Version note. The Feedback button is in the release after v0.2.0. The released v0.2.0 does not have it; use the issue tracker as described under Without the button below.

With the button

A Feedback button in the top bar (and Ctrl+Shift+F), at any step, takes a picture of the Helia window and opens a page where you:

  1. draw rectangles on the picture round the problem;
  2. say whether it is a bug, a feature request, a question or something else;
  3. write a title and a description;
  4. choose whether to include the screenshot and system details, and look at exactly what will be sent;
  5. press Send.

The feedback page: the screenshot with a rectangle round the problem, the kind of feedback, a title and description, and the checkboxes

It is sent to the Helia developers and filed in the Helia issue tracker. Three things to know:

  • Nothing is sent until you press Send. Helia has no telemetry: it never reports usage or errors by itself.
  • Check the screenshot. It is filed with your report. The screenshot is of the Helia window, which can show the file name of your capture. Untick Include the screenshot if in doubt.
  • If the service cannot be reached nothing is lost: you can save the report as a file, or open the new-issue page in your browser with the text filled in (that only works if you have an account on the project's issue tracker).

There is a matching command for people without a display, helia feedback. The whole walk-through, what is and is not included, and what happens when the service is down, are in Sending feedback.

Without the button

Contact the author through 12hobbies.studio. A report is most useful with:

  • the output of helia --version or the Copy version info button on About;
  • your operating system, your graphics card and driver;
  • what you did, what you expected and what happened;
  • the output of helia info on the capture, which has no image data in it, and if you can, a short capture (a few hundred frames) that shows the problem.

H-alpha, full disc

Goal: a sharp, orange full-disc picture of the chromosphere from an H-alpha capture, as a 16-bit TIFF.

You need: a SER (best) or AVI of the full disc, a few hundred frames or more, with the filter in the name (for example 2026-10-19_Sun_Ha_001.ser). See Capturing for Helia.

In the app

  1. Open the file (Ctrl+O). Helia reads the filter from the name, so the Quick stack card says H-alpha detected and offers the red channel and the best 25 per cent of the frames. If the name does not say, press H-alpha.
  2. Press Stack with recommended settings.
  3. On Sharpen, the recommended H-alpha wavelets are already on (gains 1.50, 1.45, 1.25, 1.05; noise cuts 1.8, 1.0, 0.5). Use Before / after to check they help, and ease a gain down if the grain looks coarse.
  4. On Colour, H-alpha is already selected. Try Invert for prominences.
  5. Export a TIFF 16-bit.

The pictures for each step, from a real capture, are in the walkthrough.

On the command line

helia stack disc-sample.ser -o sun-ha.tif --preset ha --sharpen wavelet

This is the real output for a 100-frame, 1920 x 1080, 16-bit SER on a laptop with a mid-range GPU (the times are that machine's, not a promise):

input:    SER 1920x1080 16-bit, 100 frames
note:     The header flag says big-endian, but the pixel data only makes sense as little-endian; Helia reads it that way.
backend:  GPU (NVIDIA GeForce RTX 3050 Ti Laptop GPU (Vulkan, DiscreteGpu))
settings: H-alpha (chosen); red channel, keep 25%, alignment points 64 px
edit:     1. wavelet sharpen, 6 layers, gains 1.50,1.45,1.25,1.05,1.00,1.00, noise thresholds 1.80,1.00,0.50,0.00,0.00,0.00 (x MAD sigma)
[   0.8s] building reference
[   1.2s] aligning frames
[   1.9s] stacking
[   2.1s] sharpening and editing
[   2.1s] colourising
[   2.1s] writing
frames:  100 total, 25 kept, 50 registered
points:  615 of 2135 alignment points active
align:   phase correlation (limb fit not confident enough, 0.42); confidence mean 1.00, least 1.00 (sampled)
masked:  196 x 20 at (0, 0), flat in the result
disc:    centre (788.4, 553.1), radius 447.2 px
detail:  stack 0.00106 vs best single frame 0.00115 (0.92x)
sharpened: detail 0.00137 after the edits, 0.00106 before (1.29x)
noise:   finest-layer sigma 0.00017 before, 0.00001 after the edits
timing:  scoring frames 0.52s, building reference 0.12s, aligning frames 0.24s, measuring alignment points 0.32s, stacking 0.45s, finishing 0.01s (total 2.18s)
wrote:   sun-ha.tif

Reading it:

  • settings is what Helia chose and why. --preset ha gives the red channel, 25 per cent of frames and the orange look.
  • masked is the burned-in timestamp it found and kept alignment off.
  • align says whole frames were aligned by phase correlation because the limb fit was not confident (the disc is not entirely in the frame, or the limb is soft). It is a normal outcome; see Align.
  • sharpened is Helia's detail measure, about 1.29 times higher after the wavelets, with the finest-layer noise much lower.

The result, reduced in size:

The finished H-alpha picture

If it is not right

  • A fine grid: see Fixing a Bayer grid.
  • Soft: keep fewer frames (--keep 15%) in good seeing, or look at the quality curve in Analyse.
  • Halos or grain after sharpening: lower the gains, or see Troubleshooting.
  • Prominences too faint: add Invert, or a second export with stronger levels. Prominences and disc differ greatly in brightness; a single picture cannot show both fully (HDR compositing is on the roadmap).

White-light granulation

Goal: the fine cell pattern of the photosphere (granulation) and sunspot structure, from a white-light capture.

How far this is tested. The defaults below were measured on real captures of the photosphere through a sodium D filter (a colour camera, 8 and 16-bit), with helia measure, and checked by eye on a crop. The project still has no white-light capture of its own, so the white-light preset borrows the sodium result; treat it as where to start, and look at your own picture.

You need: a capture of the Sun through a white-light solar filter (a Baader film, or a Herschel wedge with a neutral density filter), with white, whitelight or wl in the file name, recorded with a short exposure at a good image scale. Granulation is small, of the order of an arc second across; see Sampling.

Settings

Press White light on the Quick stack card, or use --preset white. The preset is already tuned for granulation, because that is what a white-light picture is for:

SettingDefaultWhy
Frames kept12%Granulation has low contrast, so only the sharpest frames show it. Keeping 25% averaged soft frames over the granulation scale and lost it
Alignment point size32 px at full-HD scale; from the image scale when it is knownTwo to three granules (a granule is about 1.5 arcsec). Seeing distorts a granule-sized patch differently from the next; small points follow it
Wavelet gains1.50, 1.80, 1.50, 1.10, 1, 1Granulation sits on layers 2 and 3, so they are lifted most
Wavelet noise cuts1.0, 0, 0, 0, 0, 0The finest layer keeps its cut, so pixel noise is not boosted. A cut on layers 2 and 3 removes low-contrast granulation along with the noise, so they have none

The sodium D preset (--preset na) takes the same frame share and point size, with gains 1.20, 1.80, 1.50, 1.10 and noise cuts 2.0, 0, 0, 0, and stacks the green channel. The chromospheric presets (H-alpha, Ca K and H, He D3, H-beta) are not tuned for granulation and keep their general values.

When the image scale is unknown, as it is before the limb has been fitted, Helia uses 32 pixels for a capture about 1080 pixels high and scales that with the short side of the frame. Give --ap-size if you know better.

On the command line

helia stack white_001.ser -o granulation.tif --preset white
helia measure granulation.tif

Everything above is a default, so the first command needs no more. To try other values, --keep 15% --ap-size 40 --sharpen wavelet --wavelet-gains 1.5,2.0,1.6 replaces them (--wavelet-gains replaces the first layers and leaves the rest at the recommended values), and helia measure says whether granulation went up.

Measuring granulation

helia stack also prints a granulation report for a white-light or sodium stack, and writes it into the FITS header; see the granulation and seeing report.

helia measure takes a finished image (TIFF, PNG or FITS) and prints the band-pass contrast, the standard deviation over the mean, of a 240 pixel crop at the centre of the disc in three bands:

BandDetailWhat dominates
fine, 0 to 1 pxPixel-scale structureNoise
granulation, 1 to 4 pxCells of a few pixelsGranulation, plus some noise
meso, 4 to 12 pxLarger groupings, slow variationBrightness changes, seeing

Compare images of the same capture, scale and bit depth. A rise in the granulation band with the fine band flat or falling is granulation; a rise in both is mostly noise, so look at the crop as well.

Measured with helia stack --preset na --sharpen wavelet on the sodium captures, 1920 x 1080 unless stated, with the 0.2.0 defaults (keep 25%, 64 px points, all channels, general wavelets) against the current ones:

CaptureGranulation band beforeafterFine band beforeafter
Sun_Test.ser, 8-bit1.54%2.94%1.64%1.48%
15_20_57.ser, 16-bit1.98%3.23%2.95%2.13%
15_32_09.avi, 8-bit, 720 x 5400.55%1.52%0.23%0.45%

The fine band falls or stays level while the granulation band rises, and the cells are visible in a crop where before they were buried in the hatch of the colour mosaic.

In the app

The White light and Na D buttons set all of this. To go further, on Align choose a smaller point size; on Stack drag Keep best frames down to 10 per cent; on Sharpen raise the gains of layers 2 and 3. Use Before / after at Full size and zoom to 100 per cent to judge.

What to watch for

  • Grain is not granulation. With few frames the noise has a similar size. If the "cells" differ from one export to the next with different frame counts, it was noise.
  • Ringing round sunspots from too much gain. The deconvolution guard exists for this; with wavelets, ease the gain on layers 3 and 4.
  • 8-bit captures limit granulation. Granulation is a few per cent of the brightness and one 8-bit step is about 0.4 per cent of full scale, so an 8-bit capture shows it coarsely however it is processed. Record 12 or 16-bit if the camera allows it (see Capturing).
  • A FITS file is the unsharpened stack, so measurements are not affected by any of this.

Related: Sharpening and adjustments.

Calcium K

Goal: a violet picture of the Ca II K network and plage from a calcium K capture.

Not tested on a real capture. Helia's Ca K settings come from its filter preset; the project has no Ca K capture of its own yet, so this page has no real output to show. The advice follows what the preset does and why.

You need: a capture through a Ca II K filter (393.4 nm), with CaK in the file name or folder (CaK_session/capture.ser works). Ca K is at the violet edge of what a sensor sees and the line is faint, so the signal is weak and the noise is the problem.

Settings

Press Ca II K on the Quick stack card, or use --preset cak. The preset recommends:

  • the blue channel for a colour camera, because the blue pixels are the ones that see 393 nm (a mono camera has only one channel);
  • 30 per cent of the frames, a few more than for H-alpha, to average the noise down;
  • gentler sharpening: wavelet gains 1.30, 1.40, 1.25, 1.05 with higher noise cuts (2.5, 1.5, 0.8, 0.3) and a short deconvolution of 8 iterations. Sharpening noise only makes a prettier mess.
helia stack cak_001.ser -o cak.tif --preset cak --sharpen wavelet
helia stack cak_001.ser -o cak-negative.png --preset cak --sharpen wavelet --invert

The second command inverts before colouring, which makes a negative; it shows filaments and prominences well, and some people prefer it for calcium. Try both.

Tips

  • Exposure and gain: the line is faint, so light is the constraint. Keep the exposure short enough to freeze the seeing and raise the gain until the histogram sits at about 50 to 70 per cent of full scale; many frames beat few long ones. See Exposure, gain and bit depth.
  • Do not push the wavelets. If the picture looks processed, lower layer 1 and 2 gains first.
  • Ca H (--preset cah) takes the same settings and a slightly redder violet, so a K and H pair can be told apart.

Related: Colour presets.

MP4 from a phone or DSLR

Goal: a good first picture of the Sun from a video recorded on a phone held to an eyepiece, or from a DSLR or mirrorless camera.

No output is shown here because the machine used for this manual has no ffmpeg. The commands, options and messages are as documented elsewhere in this manual; the messages are listed in Troubleshooting.

What to expect

An MP4 is compressed, and the compression throws away fine detail first, which is exactly what stacking needs. Helia can stack it and the picture will be a big step up from a single frame, but a raw SER or AVI from an astronomy camera keeps more. See When an MP4 is fine. When you open one, the Open step says so: lossy 8-bit 4:2:0 video (h264): fine for quick results, SER/AVI raw captures keep more detail.

1. Install ffmpeg

Helia never bundles ffmpeg; it runs it when it is installed (version 4.4 or newer).

SystemInstall
Windowswinget install Gyan.FFmpeg, then open a new terminal
macOSbrew install ffmpeg
Debian, Ubuntu, Raspberry Pi OSsudo apt install ffmpeg

Check with ffmpeg -version. See Installation.

2. Record well

  • Solar filter first. Never point a phone or camera at the Sun without a proper solar filter over the front of the telescope.
  • Lock exposure and focus, so the brightness does not wander. Record a minute or more, at the highest frame rate the phone offers, with the Sun fully in frame.
  • Keep the phone steady (a clamp), and use a self-timer or remote so you do not shake it.
  • Put the filter in the file name when you copy it off the phone: sun_Ha_001.mp4 or sun_white_001.mp4. A phone names videos VID_2026...mp4, which tells Helia nothing.

3. Open and stack

In the app, open the file as for any other. A notice shows Converting the video with ffmpeg with a percentage while the frames are decoded once to a cache folder; afterwards scrubbing is as quick as for a SER file. The decoded frames are uncompressed, so the cache needs room for the whole video; Helia checks first.

On the command line:

helia info sun_white_001.mp4
helia stack sun_white_001.mp4 -o sun.tif --preset white --sharpen wavelet

helia info is the quick test that ffmpeg is working. To use a drive with more room for the decoded frames, add --cache-dir /big/disk.

4. Choose what to keep

  • Keep the recommended share of frames to begin with, and compare with a smaller and a larger one on the quality curve (Analyse). With a lossy video the sharpness measure partly ranks how the encoder behaved, so the best frames are less clear cut than in a raw capture.
  • Go easy on the sharpening. Wavelets at the recommended values are fine; deconvolution will turn compression blocks into texture.

Colour video

A phone video is already a colour picture. Helia stacks colour video as luminance, or as the red, green or blue channel you choose (--channel, or the Channel chips under Advanced settings). For a white-light video leave it at luminance (lum) or choose green (g). The stack is mono, and the colour of the finished picture comes from the look you choose; see Can it stack colour?.

Fixing a Bayer grid

Symptom: the stacked picture is covered by a fine grid or chessboard, with neighbouring pixels alternating between bright and dark.

Cause: a colour camera records one colour per pixel in a repeating 2 x 2 pattern (a Bayer mosaic). The pictures must be demosaiced before they are stacked. If a file says it is mono although the pixels are a mosaic, a stacker that trusts the header stacks the mosaic itself.

The picture shows the same 120 x 80 pixel patch of a real capture, magnified, once read as mono and once as Helia reads it by default:

A patch of a stack read as mono, showing a grid of dark dots, beside the same patch debayered

Behind a narrow-band filter such as H-alpha the grid is stark, because the blue pixels receive almost no light.

Helia usually fixes it for you

For a SER file that says mono, Helia measures the four pixel positions of a 2 x 2 cell on the disc in a few frames. If they differ in the way a colour-filter array would make them, it says so and debayers. Check what it decided:

helia info bayer-sample.ser
Header, as written in the file
  ColorID:           0 (MONO)
  PixelDepth:        8 bit
  FrameCount:        631
  ...

Helia will use
  Bayer pattern:     RGGB           (detection)
  Bit depth:         8 bit          (header)
  Byte order:        not used (8-bit) (header)
  Row order:         top to bottom  (header)

Mosaic check (2x2 pixel positions, share of full scale)
  means:             79.6%  77.6%  77.4%  0.3%
  spread:            135.0% over 5 frame(s)

warning:  The SER header says mono (ColorID 0), but the frames hold a Bayer mosaic that looks like RGGB: ...

Read the second list: the Bayer pattern is RGGB and its source is detection, so the header was overruled by the pixels. The four numbers are the average brightness at the four positions of a cell. Here one position is 0.3 per cent while the others are about 78 per cent: the blue pixel of an H-alpha capture, which sees almost nothing. In the app the same message is the Check this card on the Open step.

If a stack that you made with an older Helia shows the grid, stack it again.

If it still shows, or the colours are wrong

Tell Helia what you know:

helia stack capture.ser -o out.tif --bayer rggb    # force a pattern
helia stack capture.ser -o out.tif --bayer none    # it really is mono

In the app, use the Bayer pattern buttons at the foot of the Open step: Auto, Mono, RGGB, GRBG, GBRG, BGGR. The list above them then shows override as the source.

  • Red and blue swapped. The mosaic says which diagonal is green but not which of the other two pixels is red. Helia settles it from the layout a SharpCap sidecar states, the colour the filter passes, the camera model, and finally by taking the dimmer of the two as blue. If you stack a single channel (H-alpha takes red) a wrong pick matters, and the result looks like noise: try the other pattern of the same family, RGGB or BGGR, GRBG or GBRG.
  • A mosaic too faint to see. If the four positions are within 2 per cent of each other the data is called mono; between 2 and 5 per cent Helia cannot tell and uses the sidecar's colour space if there is one. Give --bayer yourself.
  • An upside-down picture is a different problem: --flip-rows (or Bottom to top under Row order), and give --bayer for the picture after the flip.

More: Troubleshooting.

Batch processing on the command line

Goal: stack a whole session, or many captures, without opening the app.

Every helia stack run needs only an input and an output. Helia reads the filter from each file name, so with names such as Sun_Ha_001.ser and Sun_CaK_002.ser one command suits both and each gets its own recommended settings.

A folder of captures

In a POSIX shell (Linux, macOS, or Git Bash on Windows):

mkdir -p stacks
for f in captures/*.ser; do
  helia stack "$f" -o "stacks/$(basename "$f" .ser).tif" --sharpen wavelet --quiet \
    || echo "failed: $f"
done

In PowerShell:

New-Item -ItemType Directory -Force stacks | Out-Null
Get-ChildItem captures\*.ser | ForEach-Object {
  helia stack $_.FullName -o "stacks\$($_.BaseName).tif" --sharpen wavelet --quiet
  if ($LASTEXITCODE -ne 0) { Write-Warning "failed: $($_.Name)" }
}

Both carry on after a failing capture, so one bad file does not hide the others. --quiet hides the progress bar and the summary; the exit status is non-zero on failure. Leave --quiet off to keep each run's summary in a log:

helia stack "$f" -o "stacks/$(basename "$f" .ser).tif" > "logs/$(basename "$f" .ser).txt" 2>&1

Test the batch on one file first

helia info captures/Sun_Ha_001.ser     # what Helia will use, and any notice
helia stack captures/Sun_Ha_001.ser -o test.tif --sharpen wavelet

Other formats and options

  • A PNG for sharing and a FITS for measurement from the same capture: run it twice, with -o x.png --preset ha and -o x.fits.
  • All sharpening, colour and alignment options work in a batch; see the CLI reference.
  • Make the same edits to existing images with helia edit, with a saved preset:
for f in stacks/*.tif; do
  helia edit "$f" -o "edited/$(basename "$f")" --edits "H-alpha gentle" --quiet
done

One at a time, or all at once

The GPU is one device, so running several helia stack jobs at once rarely helps. Run them in turn, as above. If you want a queue that other programs can add to, use the API, which runs one job at a time by default.

Scripting with the API

Goal: drive Helia from your own Python, a notebook or any program that can speak HTTP.

The server is the helia serve command. It runs the same pipeline as helia stack with the same options. The guide is The API and the full list of operations is the API reference.

1. Start the server

helia serve --data-root ~/captures
listening on http://127.0.0.1:7870
data root: /home/you/captures
warning: no token set; any local process can read files under the data root
  • Every path in a request is relative to --data-root, and cannot escape it.
  • It listens on the loopback interface only. To listen on another address you must set a token (--token-file, or HELIA_TOKEN); see the security model.

2. A complete script

This script, which uses only the Python standard library, queues a job, watches its progress and downloads the result. It was run against the released v0.2.0 for this manual.

#!/usr/bin/env python3
"""Stack a capture through a running `helia serve` and download the result.

    helia serve --data-root ~/captures
    python stack_via_api.py sun.ser stacks/sun.tif --preset ha --sharpen wavelet

Standard library only. Paths are relative to the server's data root.
"""
import argparse
import json
import os
import sys
import time
import urllib.error
import urllib.request

BASE = os.environ.get("HELIA_URL", "http://127.0.0.1:7870/api/v1")
TOKEN = os.environ.get("HELIA_TOKEN")  # only if the server was started with one


def call(method, path, body=None, raw=False):
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(BASE + path, data=data, method=method)
    if data is not None:
        req.add_header("Content-Type", "application/json")
    if TOKEN:
        req.add_header("Authorization", f"Bearer {TOKEN}")
    try:
        with urllib.request.urlopen(req) as resp:
            payload = resp.read()
    except urllib.error.HTTPError as err:
        detail = json.loads(err.read() or b"{}").get("error", {})
        sys.exit(f"{method} {path}: {err.code} {detail.get('code')}: {detail.get('message')}")
    return payload if raw else json.loads(payload)


def main():
    ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    ap.add_argument("input", help="capture, relative to the data root")
    ap.add_argument("output", help="result, relative to the data root (.tif, .png or .fits)")
    ap.add_argument("--preset", help="filter id: ha, cak, cah, mg, na, he-d3, hbeta, white")
    ap.add_argument("--sharpen", choices=["wavelet", "unsharp", "rl", "none"])
    ap.add_argument("--keep", help="share of frames to keep, e.g. 20%%")
    args = ap.parse_args()

    config = {k: v for k, v in {
        "preset": args.preset, "sharpen": args.sharpen, "keep": args.keep,
    }.items() if v}
    job = call("POST", "/jobs", {
        "input": args.input, "output": args.output,
        "config": config, "overwrite": True,
    })
    print("queued job", job["id"])

    while job["state"] in ("queued", "running"):
        time.sleep(0.5)
        job = call("GET", f"/jobs/{job['id']}")
        print(f"  {job['state']:9} {job.get('stage') or '':24} {job['progress']:4.0%}")

    if job["state"] != "succeeded":
        sys.exit(f"job {job['state']}: {job.get('error')}")

    local = os.path.basename(args.output)
    with open(local, "wb") as f:
        f.write(call("GET", f"/jobs/{job['id']}/result", raw=True))
    print("saved", local)


if __name__ == "__main__":
    main()

Run on a 100-frame capture, it prints:

queued job 1
  running   opening                    0%
  running   reference                 25%
  running   global_align              31%
  running   local_align               65%
  running   stack                     95%
  succeeded write                    100%
saved disc.tif

3. Things you can ask

  • GET /health and GET /version: is it up, and what is it (version, commit, backend, limits).
  • GET /presets: the filter presets and their colours.
  • POST /captures/inspect: what a file contains, with the same notices helia info prints (for example the Bayer notice).
  • GET /captures/frame: one decoded frame as an image, for your own viewer.
  • POST /jobs, GET /jobs/{id}, DELETE /jobs/{id}: queue, watch and cancel.
  • GET /jobs/{id}/preview.png and /result: a PNG preview and the file itself.
  • GET /jobs/{id}/events: a WebSocket stream of progress, instead of polling.
  • GET /openapi.json and GET /docs: the machine-readable description and a small browsable page, served by the running server.

A job's config takes the same choices as the command line: keep, ap_size, preset, sharpen, edits, colour and so on. The errors are always {"error": {"code": ..., "message": ...}} with a stable code, which is what the script above prints. See The API.

4. Notebooks and other languages

Nothing here is Python-specific: it is JSON over HTTP, and a WebSocket for events. In a notebook, replace urllib with requests if you have it; the API guide shows that form.

Capturing for Helia

Stacking can only recover what the capture contains. This page says what to record so that Helia has the most to work with. The reasoning, and the sources for it, are in the imaging methods research; that document marks each claim as checked against a source or as background knowledge, and so does this page where it matters.

The short version

  • Save as SER, 16-bit if your camera offers it. An MP4 from a camera or phone works for quick results, but it has been compressed and loses detail (see below).
  • Use a short exposure, a few milliseconds or less, so each frame freezes the seeing.
  • Record many frames, at 50 frames per second or more, by cropping the sensor to a region of interest.
  • Set the gain so the histogram peaks at roughly half to two thirds of full scale, and never clip.
  • Sample the image properly: roughly f/13 for a 3.76 micron pixel in white light (table below).
  • Keep the files: darks and flats are not applied yet, but you will want them.

SER, AVI or MP4

SERAVI (uncompressed)MP4, MOV, MKV, WebM (compressed)
Made for astronomyYesNoNo
Raw sensor data (Bayer)Yes, with a pattern codeOnly 8-bit, and the pattern is not stored in the fileNo: already converted to colour and compressed
Bit depth8 or 168 or 16 (Y16) in the formats Helia readsUsually 8-bit, 4:2:0 colour
Per-frame timestampsOptionalNoNot used
CompressionNoneNoneLossy (H.264, H.265, MJPEG and similar)
NeedsNothingNothingThe optional ffmpeg program

Prefer SER. It is simple, uncompressed and carries the colour layout.

Helia also reads uncompressed AVI, including the large OpenDML files (over 1 GB) that SharpCap writes. It reads BI_RGB, Y800/GREY, Y16 and 24-bit BGR video. A compressed AVI (for example MJPG) is handed to ffmpeg, like an MP4, described next. For a raw colour camera recorded to AVI, SharpCap also writes a CameraSettings.txt next to the video, and Helia reads it to learn the Bayer pattern. See Troubleshooting if the colours or the channel look wrong.

When an MP4 is fine

Helia reads MP4, MOV, M4V, MKV and WebM files, and compressed AVI, through ffmpeg when it is installed (see Installation). Helia itself stays free of video codecs; it runs ffmpeg as a separate program.

Raw SER or AVI captures keep more detail, for reasons that matter to stacking:

  • The codec throws away fine, low-contrast detail first, and granulation is exactly that. What was thrown away cannot be stacked back.
  • H.264 and H.265 compress in blocks and across frames, which adds blocking and smears detail from one frame into the next. The sharpness measure then partly ranks how the encoder behaved, not how the air behaved.
  • Typical video is 8-bit with colour detail halved in each direction (4:2:0), and a camera that records MP4 has already demosaiced and sharpened the picture.

An MP4 is fine for a quick look at a day's seeing, for a camera or phone with no other output, or for an archive you have no raw copy of. Helia then says so: helia info, the Open step of the desktop app and the API's inspect response all carry a note such as "lossy 8-bit 4:2:0 video (h264): fine for quick results, SER/AVI raw captures keep more detail". If you can choose, record SER.

Two practical points. Frames are decoded once, by ffmpeg, into an uncompressed file in a cache directory, and removed afterwards, so you need free space for the whole video as raw frames: a 4K clip of 7,500 frames is about 60 GB. Helia checks the space before it starts and says what is missing. And colour video is stacked as luminance, or as the red, green or blue channel you choose with --channel (the desktop app has the same chips).

Why a SER header can be wrong

A SER header is 178 bytes the recording program fills in once, from what it believes about the camera. It is not checked against the pixels, and programs differ in what they believe. Known ways it goes wrong:

  • Colour mode. SharpCap has written raw Bayer data from a colour camera (a ZWO ASI482MC, for example) with ColorID 0, which means mono. Nothing in the file then says the frames are a mosaic. Helia checks the pixels of a SER file that says mono; see Troubleshooting. The same camera and program do not always do it: of two SER files from one ASI482MC session in Helia's test captures, the 16-bit one says BAYER_RGGB and the 8-bit one says MONO. Check with helia info after the first capture of a session rather than assuming.
  • Byte order. The flag for the byte order of 16-bit pixels has been written inverted by some programs. Helia checks it against the data.
  • Bit depth. A program can write the depth of its sensor (12 bit) or its mode where the data is stored as 16.
  • Dates. The DateTime fields are often zero, and the per-frame timestamps at the end of the file are optional.

Run helia info on the first capture of a session: it prints every header field as written next to the values Helia will use, and says where each came from. If the header is wrong in a way Helia does not catch, --bayer, --bit-depth, --endian and --flip-rows on the command line (and the same choices in the app's Open step and in the API) override it; the choice is recorded in the output file.

Exposure, gain and bit depth

  • Exposure. Keep it short enough to freeze the seeing, which is a few milliseconds in the research notes (that figure is background knowledge, not a measured value). In white light this is easy. In H-alpha through a narrowband filter you are short of light, and the trade between exposure and noise becomes the main constraint.
  • Gain. Raise it until the histogram sits at about 50 to 70 per cent of full scale, without clipping. Many frames at higher gain beat fewer frames with longer exposures: stacking averages the read noise down, and frame selection throws away the frames that moved during a long exposure.
  • Bit depth. Use 16-bit if you can. Granulation contrast is only a few per cent in white light, and 8-bit steps are about 0.4 per cent each, which is a real limit on a single frame (stacking dithers it). Helia reads 8 and 16-bit data and works in 32-bit floating point. 12 or 16-bit helps most where the detail is faintest, and granulation is: helia measure shows how much of it a stack holds.

Region of interest and frame rate

The atmosphere keeps changing while you record, and the sharp moments are brief. A study of 160 Hz solar sequences at the GREGOR telescope (Denker and others, 2018) concluded that rates above about 50 frames per second are needed to catch a good share of the best moments. On USB cameras the frame rate rises almost in proportion as the region of interest shrinks, so cropping to the part of the Sun you care about is the usual way to reach 100 to 300 frames per second.

How many frames? Helia keeps the best 25 per cent by default (12 per cent for white light and sodium D, where granulation needs only the sharpest frames), so a capture of a few thousand frames leaves hundreds in the stack. Longer captures help, but the Sun changes: granulation evolves over minutes, and for long captures or mosaics the field rotation and the Sun's own rotation matter (derotation is on the Roadmap).

Sampling: focal ratio and pixel size

To record all the detail your telescope can deliver, the pixel pitch p should be no larger than wavelength x f-ratio / 2, which means a focal ratio of at least 2 x p / wavelength. Worked values:

WavelengthPixel sizeFocal ratio for Nyquist sampling
550 nm (white light)3.76 micronsf/13.7
550 nm (white light)2.4 micronsf/8.7
656.3 nm (H-alpha)3.76 micronsf/11.5
393.4 nm (calcium K)3.76 micronsf/19.1

Seeing blurs the image below the diffraction limit, so in poor seeing a little under-sampling costs little. In good seeing, slightly over-sampling (up to about one and a half times Nyquist) helps the sub-pixel alignment and later sharpening. Helia does not compute your sampling for you yet, and sizes its alignment points in pixels, so if you change your focal ratio you may want to change --ap-size too (see How stacking works).

Darks and flats

Helia applies dark and flat frames, and a map of bad pixels, before it analyses anything: see Calibration. Record them in the same session as the Sun, because they are cheap and you cannot take them afterwards:

  • Flats matter a great deal for solar work. Dust on the sensor window stays put while the image moves, so a missing flat leaves a fixed pattern that stacking can make look sharper, and after local alignment it can be mistaken for granulation. Take flats at the same focus as your science data. In an etalon-based H-alpha system the interference fringes move with focus, so a flat at another focus will not match them.
  • Darks matter little for bright white-light frames, but in H-alpha, where the signal is low, they remove hot pixels and fixed-pattern noise. Record 50 to 100 with the lens capped, at the exposure and gain you used on the Sun.
  • Darks for the flats at the exposure of the flats, if that differs from the Sun's (it usually does): 20 to 50 frames.

The research notes also point out that an etalon passes a slightly different wavelength across the field unless the beam is nearly telecentric, so a disc brightness gradient is normal for such systems. A flat corrects the brightness across the field, not the shift of the pass-band, so Helia warns when a flat shows strong vignetting; see the sweet spot.

Naming and keeping files

For SharpCap AVI captures keep the CameraSettings.txt file next to the video with the name SharpCap gave it (capture.avi goes with capture.CameraSettings.txt). Helia finds it by that name.

Calibration: darks, flats and bad pixels

Calibration takes out of every frame what belongs to the camera and the optics in front of it and not to the Sun. It is optional, and it is the one step in Helia that needs frames you record separately.

Why it matters more for the Sun

Dust on the sensor window, the fringes of an etalon, a vignette and a camera's own fixed pattern are all fixed to the sensor. The Sun is not: seeing moves it over the sensor, and Helia's alignment moves every patch of every frame to line it up. A pattern that stays put while the picture moves is, to the alignment, not part of the picture; it comes out of the stack as faint detail that was never there, and a sharpening step then makes it crisper, not fainter. Calibrating first, before the frames are scored, aligned or stacked, takes it out while it is still a fixed pattern that can be measured.

What to record

WhatHowHow many
DarksLens capped (or the telescope covered), the same exposure and gain as the Sun, the same temperature if you can50 to 100
FlatsAn evenly lit field at the same focus, gain and set-up as the Sun: the clear sky or a screen through the filter, moved a little during the capture so any structure averages out. Aim for about half of full scale20 to 50
Darks for the flatsLens capped, at the exposure of the flats. The flats' exposure is almost always different from the Sun's20 to 50
Bias (optional)The shortest exposure the camera allows, lens capped. Only needed to scale a dark to another exposure50

Record them in the same session, in the same file format as the Sun (SER, AVI or FITS stills), at the same size and region of interest: a master made at another size, mode or mosaic is refused. A flat for a different focus is worse than no flat for the reason given under Etalons and Newton rings.

Building masters

helia calibrate --darks darks.ser --flats flats.ser --flat-darks flat-darks.ser \
                --preset ha -o masters/

Each input is a SER, AVI or video file, a FITS still, or a folder of them; give several paths to combine several. It writes master-dark.fits, master-flat.fits (and master-bias.fits, master-flat-dark.fits) and bad-pixels.fits to the folder and prints the line to use them. The options are in The command line.

  • Masters are single planes, as stored. For a raw colour camera they are the mosaic, before any demosaicing, so a master made from one capture applies to any other of the same camera and mode.
  • Combining. Each pixel of the master is the sigma-clipped mean of that pixel in every frame (values more than three standard deviations out are dropped, and the rest averaged), or the median with --combine median. A cosmic ray or a satellite in one frame does not reach the master. Frames are used up to a memory limit of 3 GiB (many hundreds of 2 megapixel frames); --max-frames sets it by hand.
  • The flat. The dark for the flats is subtracted from each flat frame, the frame is scaled to a mean of one in each colour of the mosaic (so a lamp or sky that drifts while you record is not a pattern), the frames are combined, and the result is normalised to a mean of one within each colour. Per colour, so that the colour balance of the sensor is not changed by the flat: it corrects the shape of the field, not the white balance. A flat from another program (a FITS or TIFF with no Helia header) is normalised the same way when it is read.
  • Written with a record. Each master's FITS header holds how many frames made it, how they were combined, the exposure and the input's name, and a fingerprint of its pixels (FNV-1a 64, to tell masters apart in a log; not a cryptographic hash). Reading a master back checks the fingerprint, so a file that was edited or damaged is refused.

Using them

helia stack sun.ser -o sun.tif --dark masters/master-dark.fits \
            --flat masters/master-flat.fits --bad-pixels masters/bad-pixels.fits

--dark and --flat also take the captures themselves (a file or a folder), which Helia combines on the spot; build the masters once with helia calibrate if you will stack the session more than once. In the desktop app it is the Calibrate step, and in the API the calibration object of a job (The API).

What is done to each frame

In this order, on the stored pixels of each frame as it is read, before it is demosaiced, scored or aligned:

  1. Dark. raw - dark.
  2. Flat. / flat, where the flat is at least 0.05. Pixels under that read as dead and are replaced as bad pixels, not divided by (a gain of more than 20 would only amplify noise).
  3. Clamp at zero.
  4. Bad pixels are replaced by the median of the good pixels around them of the same colour: the eight at distance one, or at distance two in a raw colour mosaic, widened to a second and a third ring when fewer than four are good.

Because it is done in the one place that serves frames, the CPU, the GPU's decode kernel and every stage after it see exactly the same numbers. The calibration itself runs on the CPU as each frame is read; the calibrated frame then goes to the device as 32-bit floats (twice the bytes of a 16-bit frame), and everything from the decode onwards runs where it did before. The parity tests compare the decoded frames, and a whole calibrated stack, on both backends.

Scaling the dark

A dark taken at another exposure can be scaled: --scale-dark multiplies the dark by the ratio of the exposures (the Sun's comes from the SharpCap file beside the capture). With a master bias (--bias) only the part above the bias is scaled; without one the whole dark is, which also scales the offset, and Helia says so. If either exposure is unknown the dark is used as it is, and Helia says that too. For the short exposures of solar work the dark current is negligible and a dark at about the right exposure and gain is simply used.

Bad pixels

Hot and cold pixels are found three ways, and merged:

  • In the dark: a pixel more than 6 robust standard deviations (--hot-sigma) above the median of its same-colour neighbours is hot, and one that far below is cold. A deviation under 0.012 of full scale never counts, so a dark that is nearly noise-free does not condemn pixels that differ by one grey level.
  • In the flat: a pixel that reads under half of its neighbours is cold or dead. Dust is not: a shadow covers many pixels together.
  • In the light frames (helia calibrate --lights capture.ser): a pixel whose mean over the capture stands out from its neighbours by more than 6 local deviations and more than 6 standard errors of the mean, lies outside the range of its neighbours (so a pixel on the slope of the limb is never one), and fluctuates no more than its neighbours do. The Sun moves over the sensor and a bad pixel does not. Burned-in text is as stuck as a bad pixel and is left out. This is for cameras you have no darks for; it finds pixels that stay off, which includes outliers of the pixel-to-pixel response as well as hot ones.

With a dark or a flat and no map, helia stack finds the first two itself (--no-cosmetic stops it). Your own list is a text file with an x y pair per line, counted from 0 at the top left, # starting a comment.

A flat from the Sun itself: displaced-disc frames

A diffuser or sky flat cannot reproduce everything the Sun's own light does to the sensor: the fringes of an etalon depend on the solar spectrum and the focus of the moment. If you can move the Sun over the sensor, Helia can solve the flat from the Sun frames themselves (#52; Kuhn, Lin and Loranz 1991). The sensor's gain stays where it is while the Sun moves, so the two can be told apart from the data alone.

helia calibrate --displaced sun-moved.ser --darks darks.ser -o masters/
helia stack capture.ser -o stack.tif --dark masters/master-dark.fits \
            --flat masters/master-flat.fits
# or in one go, from the frames themselves:
helia stack capture.ser -o stack.tif --dark darks.ser --displaced sun-moved.ser

What to capture. The ordinary Sun capture, but with the mount off tracking, or nudged between frames, so the disc lands in different places on the sensor: at least six frames, 30 pixels or more between the two furthest (Helia refuses less), a tenth of the field is better, and 20 to 60 frames in all. The limb must show in each frame (a disc cut off by the edge of the field is fine). Switch the timestamp overlay off, and take darks at the same exposure. Keep the Sun and the seeing steady; the Sun is taken to be the same in every frame.

How it works. Frame k is the gain g times the Sun O moved by s_k: frame_k(x) = g(x) O(x - s_k). The displacements come from the limb fit of each frame. Two steps alternate, 30 times: the Sun given the gain (move each frame back, divide by the gain, average with weights g^2), and the gain given the Sun (the least-squares ratio over the frames, using only where the Sun is bright and down-weighting what does not fit). A colour mosaic is solved one colour plane at a time, and a colour the filter leaves empty (the blue pixels behind a sodium filter) is left at 1.

What cannot be recovered. A smooth change of the gain across the field cannot be told from a smooth part of the Sun (limb darkening, a gradient across the disc): moving by s changes a smooth mode of wavelength L by only s / L of itself. So by default the solution is high-passed: divided by its own Gaussian blur, with a width of a quarter of the span of the displacements (6 to 120 pixels), which keeps the dust, the fringes and the column pattern and drops the vignetting. The vignetting comes from an ordinary flat (or is left alone: it is mild next to dust). --displaced-keep-smooth (helia calibrate) keeps every scale, which is only right when the Sun was moved by much more than anything smooth in it. The printed selfcal: line gives the span and the wavelength above which the gain is not recovered well (eight times the span).

How well it works

On synthetic frames (a disc with limb darkening and granulation in a 520 x 420 field, a gain with 25% vignetting, 14 dust rings of 4 to 12% and a column pattern, noise of 0.2% of the mean, the Sun moved up to 90 pixels in each direction), cargo test -p helia-core selfflat. The error is the RMS of the recovered gain over the true gain, after matching their means, over the middle of the field:

TestResult
10, 25 and 50 frames, displacements from the limb0.54%, 0.11% and 0.08%
The same 25 frames with the true displacements0.11% against 0.11% from the limb
Displacement of each frame found from the limbwithin 0.04 px of the truth
Pixel noise of 0.005, 0.02 and 0.05 on a disc of 0.5 to 1 (25 frames)0.20%, 0.75%, 1.9%
Error in the displacements, 0.1, 0.2, 0.4 px0.15%, 0.22%, 0.40%
The Sun changing during the capture (a smooth random field of 2% and 6%)0.64% and 1.9%
The Sun moved 35 px against 139 px1.2% against 0.23%
Moved under 30 pxrefused
A colour mosaic0.36%
The dark taken off, or not (offset of 15% of the mean)0.44% against 0.56%
The default high-pass (52 px), against the high-passed truth0.86%, the smooth part 0.18%

Every row is under 1% except where the test says what degrades it, and the default takes out the smooth part, which the data cannot determine.

On the real captures

The project's captures were recorded tracking, so the Sun drifts by 3 to 13 pixels in three of them and 84 pixels in the fourth (15_32_09.avi); none was made to be displaced, and the set does not hold the displaced frames of the issue. What can be checked is whether the solution is real structure, by solving from the even frames and the odd frames of 120 spread over the capture (cargo run --release -p helia-core --example selfflat_split -- capture):

CaptureSpanFlat RMSDifference between halvesCorrelation
15_32_09.avi84 px0.66%0.31%0.90
Sun_Test.ser16 px1.42%0.22%0.99

Both halves agree, so there is structure fixed to the sensor in both. Whether it is the sensor or the Sun is the question the span answers, and the answer is the other result: stacking Sun_Test.ser with the flat solved from its own drift (a span of 16 px, now refused) lowered the granulation band of helia measure from 1.847% to 1.486%: the solution had taken the granulation with it. With the 84 px of 15_32_09.avi the same stack changed it by 0.6% (0.971% to 0.965%), the fine band by 3% (0.314% to 0.305%). So on captures of this kind a flat from the capture's own drift does not help, which is why a span under 30 px is refused. It needs frames made for it.

Etalons and Newton rings

A flat divides out what is fixed to the sensor and the optics. Two things that look fixed are not:

  • Newton rings and etalon fringes move with focus, tilt and temperature. A flat taken at another focus does not match the lights: it leaves the rings in, or adds a second set. Take the flat at the science focus and tilt, through the filter, and look at the result.
  • The etalon's sweet spot. A Fabry-Perot etalon passes a slightly different wavelength across the field unless the beam is nearly parallel, so the line moves off the filter's centre towards the edge. The disc is dimmer and has less contrast there, and no flat puts the line back. A flat corrects the brightness, not the shift. Keep the area you care about near the centre.

helia calibrate reads the flat for these and warns, with numbers: the edge of the field as a share of the centre (a warning under 75%), rings in the radial profile (half their peak-to-peak size over 0.4% and at least three crossings of the trend), and the share of the field more than 5% below its surroundings (dust, a warning over 0.5%). --preset ha adds the H-alpha advice whatever the numbers say. The same warnings appear in the app's Calibrate step and in summary.calibration of an API job. They are heuristics on a flat, not a measurement of your etalon.

What the files say

The masters applied are recorded in the output:

FileWhere
FITSPRSTEPn cards DARK-SUBTRACTION, FLAT-FIELDING and BAD-PIXEL-CORRECTION (before FRAME-SELECTION), and HISTORY lines
TIFFthe ImageDescription tag
PNGthe Description text chunk

Each line names the master: Calibration: flat-fielded (master flat of 40 frame(s), sigma-clip 3, exposure 12.0000 ms, from flats.ser, fp:8e47efb3405e3915). The PRSTEP names are Helia's own; see Output formats.

Does it work?

The tests answer on a synthetic sensor with a known truth, and the real captures answer what they can.

Synthetic. A synthetic sun is recorded 40 times through a sensor with a vignette that leaves 70% at the corners, six dust shadows up to 15% deep, a per-pixel gain spread of 1%, a fixed offset pattern of 0.4% plus 0.2% per column, twelve hot pixels and six dead ones. The darks and flats of the same sensor are made into masters, and the stack is compared with the stack of the same frames recorded by a perfect sensor (the residual is the root-mean-square difference, after the one scale factor a flat leaves, since it normalises to a mean of one and not to the response of the sensor):

CalibrationResidual pattern (rms)Below uncalibrated
none0.01476
dark only0.007066.4 dB
flat only0.01623-0.8 dB
dark, flat and bad pixels0.0002136.9 dB

The test requires 20 dB. The pieces behave: a master of 40 darks has an error of 0.0016 against 0.0098 for one frame; the master flat is within 0.003 of the true response; all 18 hot and dead pixels are found with none missed and none invented; in light frames alone, with no darks, all the stuck pixels on the disc are found with none invented. Richardson-Lucy sharpening of the stacks (a Gaussian of 1.5 pixels, 20 iterations) takes the uncalibrated residual from 0.01476 to 0.01566 (6% worse) and the calibrated one from 0.00021 to 0.00029, still 34 dB under the uncalibrated one.

Real captures. There are no darks or flats for the ASI482MC that took the 2022-07-17 captures in the project's test data, and the calibration captures that exist (an ASI071MC, 2456 x 2462, already stacked) are for another camera, so none of what follows is a measurement of a real dark or flat. What could be tried:

  • The sharpening page reports that Richardson-Lucy raises the finest-layer noise of 14_42_57.avi (1920 x 1080, 8-bit, 630 frames, RGGB) six-fold and puts it down to a fixed pattern. Repeated here: 0.00031 to 0.00200 (6.5x). Substituting what the capture can tell about the sensor, a dark of per-column offsets read from the sky beside the disc (0.47% rms), changes it to 0.00026 to 0.00197, and adding a column flat estimated from the disc to 0.00049 to 0.00222. Choosing one colour plane (--channel r, g, b) gives 0.00027 to 0.00198, 0.00026 to 0.00121 and 0.00036 to 0.00257. So these stand-ins do not fix it.
  • Looking at where the amplified energy is explains why. Of the high-passed energy inside the disc, 2.2% is above 0.3 cycles per pixel in the stack and 69.5% after Richardson-Lucy. The checkerboard of the Bayer grid is 0% before and 2.3% after; the band of vertical stripes is 4.2% before and 1.4% after (a smaller share of a larger total); the rms of the column profile of the high-pass, the vertical stripes themselves, goes from 0.388% to 0.409% of the disc level, 5% more. The sensor does have vertical stripes in this capture, but they are not what Richardson-Lucy amplifies: the six-fold rise is broadband noise at the finest scale, which no dark or flat removes. A stronger reading would need an independent measure of the noise (two stacks of disjoint halves of the capture), which was not made.
  • helia calibrate --lights found no stuck pixels in 14_42_57.avi, 552 in 15_20_57.ser, 706 in Sun_Test.ser and 3 in 15_32_09.avi. Replacing them changed none of the stacks or the sharpening figures by a visible amount.
  • The columns of the sky beside the disc do differ (0.47% rms, 2.3% peak to peak of full scale), which is the fixed pattern a real dark would have caught, and the flats a real capture would give are what could not be tried.

Run on a 24-core machine with an NVIDIA A100, at the change that added this page.

What it does not do

  • No column-noise removal and no flat from the Sun itself. A flat from the displaced disc (Kuhn, Lin and Loranz), which would plug in here, is #52.
  • A colour SER (three planes per pixel) is calibrated as the mean of its planes, as it is read for stacking; masters must be made from the same kind of capture. FITS stills with three planes (a debayered RGB image) are not read.
  • One flat, one dark. There is no library that picks the master for the temperature or the date; you name the files.
  • GPU. The calibration arithmetic itself runs on the CPU as frames are read. A GPU kernel for it is possible and has not been written; what it would save, against the decode, has not been measured.

Using the desktop app

helia-gui takes a capture from a file on disk to a stacked, sharpened and coloured image in nine steps, shown down the left of the window. You do not have to visit them all: open a file and press Stack with recommended settings, and Helia does the rest (see Quick start). The steps are there when you want to look closer or change something. The screenshots on this page are produced by the test suite from a synthetic capture, so they always match the current interface.

The window has four areas:

  • the top bar, with the capture details, the compute backend and the Open and Export buttons;
  • the pipeline rail on the left, one row per step, each with its state and a one-line hint about what to do next;
  • the viewer in the middle, with a frame scrubber and the quality curve underneath;
  • the inspector on the right, which shows the settings of the selected step.

Click a step in the rail to select it. Everything that takes time (decoding, scoring, stacking, colouring, file dialogs) runs on background threads, so the window stays responsive and a stack can be cancelled at any moment.

What the window tells you

The rail hints. Under each step is a plain sentence about what to do next, which changes as you go: Open a capture, Scoring the frames, please wait, Ready: press Stack, Done: sharpen, colour and export, Save as TIFF, PNG or FITS. A step that has nothing to do yet says what it is waiting for, such as Open a capture first.

Notices while it works. Whenever Helia is busy, a notice appears above the picture: the stage in plain words, a progress bar, and a Cancel button where stopping makes sense. Anything that takes longer than about a seventh of a second gets one, so the window never looks frozen. Quicker work does not flash.

A scoring run in progress: the stage, a count, the time left and Cancel

NoticeShown while
Opening the file and indexing its framesa capture is opened (no bar: the length is not known)
Converting the video with ffmpega video is converted to a capture, where that is offered
Decoding the framea frame is read for the viewer
Scoring N framesevery frame is scored; the bar counts frames and shows the time left
Building the reference image, Aligning the frames, Measuring the alignment points, Stacking the best frames, Finishing the imagethe stages of a stack, one overall bar for the run
Colourising the imagea colour look is applied to the stack
Writing the filethe result is saved

The time left appears once enough of the work is done to estimate it (after a second and a twentieth of the way through), and is rounded to whole seconds. Screen readers are given the same sentence: the stage, the percentage and the time left.

Messages when it is done. A short message appears at the bottom right when something finishes, such as Stacked 250 of 1,000 frames in 2.1 s or Saved /path/sun.tif. It fades after a few seconds, or press Close. A failure, such as Could not open file: not a SER file, stays until you press Dismiss. The same failure twice shows once.

1. Open

The Open step, with the Quick stack card

Choose Open capture in the top bar (or press Ctrl+O), or drop a file on the window. A Recent menu appears next to it once you have opened something: it lists the last eight captures, newest first, and opens one when you choose it (a file that has gone is dropped with a message). You can also start Helia with a capture: helia-gui capture.ser, or Open with > Helia in your file manager once Helia is installed. Helia reads SER files and uncompressed AVI files, including the large OpenDML files SharpCap writes. With the optional ffmpeg installed it also opens MP4, MOV, M4V, MKV and WebM files and compressed AVI files (see Installation).

A video is decoded once, by ffmpeg, before anything else happens, and the top bar shows "Decoding video with ffmpeg" with a percentage while it runs; opening another file cancels it. Afterwards scrubbing is as quick as for a SER file, because the frames are on disk uncompressed. Changing the Channel chip decodes the video again, because ffmpeg extracts the colour channel.

The Open step for an MP4

When the input is lossy video the Open inspector says so (here, "lossy 8-bit 4:2:0 video (h264): fine for quick results, SER/AVI raw captures keep more detail"). See Capturing for Helia. If ffmpeg is missing, an error strip above the viewer says how to install it.

Quick stack. Once a capture is open, the top of the inspector is a card that says which filter Helia found (from the file name, the folder, the SharpCap sidecar or the SER header; see Colourising) and lists the settings it will use: the colour channel (shown only for a raw colour camera), the share of frames to keep with the count, and the alignment point size. One primary button, Stack with recommended settings, applies those settings and runs the stack. Underneath, a row of filter buttons lets you correct the guess, and Not sure returns to plain defaults. The card keeps its advice short; the other steps hold the settings if you want to change them, and a change made there stays until you press the button again, which puts the recommended settings back.

First-run tips. The first time you use the app, a strip above the picture lists the three steps. Got it hides it for good; Helia remembers in its settings file (see Settings file).

The top bar then shows, in order: the file name, the camera model (taken from the SharpCap CameraSettings.txt next to an AVI, when there is one), the frame size, the bit depth with the Bayer pattern, and the number of frames.

The header, and what Helia will use

The Open step for a SER file that says mono but holds an RGGB mosaic: the notice, the values Helia will use and where each came from, the header as written, and the overrides

Under the file details the Open inspector shows two lists, so a header that is wrong cannot go unnoticed:

  • Helia will use: the Bayer pattern, bit depth, byte order and row order the capture is decoded with, each followed by where it came from: header (the file's own), sidecar (SharpCap's CameraSettings.txt), detection (measured from the pixels) or override (your choice, below). Anything that is not from the header is shown in the accent colour.
  • SER header, as written in the file: every field, decoded: FileID, LuID, ColorID with its meaning, the LittleEndian flag, width, height, pixel depth, frame count, observer, instrument, telescope, DateTime (local) and DateTimeUTC as dates (not set when the recorder left them at zero), and whether the per-frame timestamps are present with the first and the last. For an AVI it lists the codec, pixel format, row order and the sidecar's fields instead.

When the file disagrees with itself or with you, a Check this card above the Quick stack card says so in words. The screenshot is the case it exists for: the header says mono (ColorID 0), the pixels are an RGGB mosaic, and Helia debayers as RGGB and tells you why. A brief message also appears at the bottom right when the capture opens. helia info prints the same lists and notices (see The command line).

Overrides. Below the lists, chips say what the file gets wrong. Each one reopens the capture, and the lists above update to show the override as the source:

  • Bayer pattern: Auto trusts the header, but checks a SER file labelled mono for a hidden mosaic; Mono treats the frames as monochrome whatever the header or sidecar say; RGGB, GRBG, GBRG and BGGR force a pattern.
  • Bit depth: Auto, 8-bit or 16-bit, instead of the header's.
  • Byte order of 16-bit pixels: Auto, Little-endian or Big-endian, instead of the header flag.
  • Row order: as stored, or bottom to top for a file stored upside down.

Bit depth, byte order and row order apply to SER files; their chips are greyed out for an AVI. The overrides belong to the file: opening a different capture starts again from Auto. They are written into the exported file (see What the files say).

2. Calibrate

The Calibrate step with a dark and a flat chosen: the frame before (left) and after (right) calibration, and the masters in force

Optional. Calibration takes out what belongs to the sensor and the optics in front of it and not to the Sun: dust shadows, vignetting, the fringes of an etalon, a fixed pattern, hot and dead pixels. It is applied to every frame as it is read, before the frame is demosaiced, scored or aligned, so everything after it (the quality curve, the alignment points, the stack, the sharpening) sees the calibrated frames. See Calibration for what to capture and why.

The inspector has a place for each file:

  • Dark is a master dark (a FITS file from helia calibrate, or any FITS or TIFF) or dark captures to make one from. File picks a file; Folder picks a folder of captures (SER, AVI or FITS stills).
  • Flat is the same for the flat. When it is captures, a Dark for the flat appears: darks at the exposure of the flats. Without them the flat keeps its offset and corrects too much in the dim parts.
  • Bad pixels is a map (FITS) or a text list of x y pairs. Leave it empty and Helia finds the hot pixels of the dark and the cold ones of the flat itself.
  • Fix hot and cold pixels turns that correction off.
  • Scale the dark by exposure is for darks taken at another exposure from the Sun; the Sun's exposure is read from the SharpCap file beside the capture.

While the masters are prepared the rail shows Preparing; then it names what is in force, and the inspector lists each master (frames used, how they were combined, a fingerprint of its pixels) and any warning: a flat with rings, a dark that could not be scaled, a flat built without a dark. Before / after in the toolbar splits the picture, the frame as recorded on the left and calibrated on the right; drag the line. Save the masters writes master-dark.fits, master-flat.fits and bad-pixels.fits to a folder, so the next session can use the files instead of the captures.

Changing a file scores the capture again, and a stack made before the change is out of date: press Stack again. The masters are checked against the capture when it opens (same size, same mosaic) and a mismatch is explained in the inspector. Nothing here changes the capture file.

The Calibrate step before anything is chosen

3. Analyse

The Analyse step

As soon as a capture opens, every frame is scored for sharpness in the background. The rail shows Scoring and then Scored. The inspector lets you choose the sharpness metric:

  • Gradient energy, the default, is the mean squared gradient of the frame;
  • Laplacian variance is the variance of the Laplacian.

Both ignore overall brightness and the sky pedestal, so a brighter frame does not look sharper. Changing the metric scores the capture again.

4. Align

The Align step with the alignment point grid

Seeing bends the image differently across the disc, so Helia aligns small windows called alignment points as well as the whole frame. The grid is drawn over the viewer as thin squares. The inspector offers the point size, from 32 to 96 pixels in steps of 8: small points follow finer detail but need more contrast to lock on. The size Helia recommends for your image is already selected.

The Auto chip beside the sizes lets Helia choose when it stacks: it fits the limb for the image scale, probes the seeing on a dozen frames, and picks the size, the spacing and how far to search. The note under the chips explains this before the stack and, afterwards, says what was chosen (for example alignment points 32 px every 16 px, search 4 px, 8.79 arcsec per pixel, local warp 0.35 px); the grid then shows the chosen size. Picking a size turns Auto off. See How stacking works.

The Align step with Auto on, after a stack

The card shows how many points are active, that is, bright and detailed enough to be measured. After a stack, a second card shows the median drift of the frames from the reference, in pixels.

Points are drawn only where the stack will use them: the centre of the window must be inside the fitted limb of the sun, and no window may touch burned-in text (which Helia finds by itself) or a rectangle you exclude.

Exclude regions lists rectangles that alignment and quality scoring must stay off, for a logo or a stray label that the automatic text detection does not cover. Add region creates one at the top-left corner; set its x, y, width (w) and height (h) in pixels with the boxes, and use the minus button to remove it. Each rectangle is outlined in amber on the frame. Excluded areas are flat in the stacked result. Changing the list scores the capture again, because the scores depend on it.

5. Stack

The Stack settings

  • Keep best frames is the share of frames, ranked by quality, that go into the stack, from 5 to 100 per cent in steps of 5. The line underneath converts it to a frame count.

  • Combine is a weighted mean, which weights each frame by its quality. An unweighted mean is not available yet.

  • Outliers guards against samples that do not belong, such as a hot pixel, a satellite trail or a cloud dimming a few frames. Off is the plain weighted mean; Sigma drops samples beyond 2.5 standard deviations of the mean, Huber gives samples beyond 1.345 less weight the further they are, and Winsor clamps them to 2 standard deviations. It is off by default and makes the run slower, because the frames are read twice. See How stacking works for what each costs.

  • Colour chooses what is stacked. Filter look (the default) stacks one plane, coloured later from a filter's look in the Colour step. True colour, for white light with a raw colour camera, stacks red, green and blue on one alignment and gives a colour picture; Auto balance or No balance and Align channels (red and blue onto green) go with it. The chip is disabled for a capture that is not a Bayer mosaic. After a stack the note under it gives the gains and the channel shifts. See True colour.

    The Stack step with True colour, after a stack

  • Crop cuts the stack to the solar disc: Off keeps the whole frame, Disc crops to a square around the fitted limb with a margin you set with the slider (0 to 50 per cent of the radius, 10 to start). It applies to every export, FITS included. The note under it says what will happen and, after a stack, the size it was cropped to, or why it was not (no clear disc). See How stacking works.

  • Stack N frames starts the run.

The Stack settings with the disc crop on, after a stack

When it has finished, a line under the button says how whole frames were aligned and how confident that was, for example Aligned by limb fit, confidence 0.90; hover it for the reason. Helia picks the method itself: the solar limb when the full disc is in view, phase correlation for a cropped region. The app has no switch for it; use --align on the command line or align in an API job to force one.

Stacking in progress

While it runs, a notice above the picture names the current stage and shows the percentage and the time left, and the thin bar in the inspector shows overall progress. Cancel, in either place, stops the run without losing the settings.

The stacked result

When it finishes, a message reports the frames used and the time taken, the viewer switches to the stacked image, the Export button lights up and Helia moves on to the Sharpen step.

6. Sharpen

The Sharpen step, with the picture split into before and after

The stack is sharpened on the mono image, before any colour. The recommended wavelets for your filter are already on, so the picture you see here is sharpened; press Off to see the plain stack. The method chips choose between Wavelets, Unsharp and Deconvolution, and each shows its own controls: a gain and a noise cut for each wavelet layer, the radius, amount and threshold of the unsharp mask, or the blur, iterations, noise control and limb guard of the deconvolution. Every slider updates the picture as you drag it. Reset to recommended puts the filter's values back. My presets keeps and recalls whole settings under a name.

At the top of the step, Limb darkening divides the disc by its brightness profile before the sharpening runs, so the sharpening sees flat contrast from the centre to the limb. Model divides by the fitted law (the chips under it choose Linear, Quadratic or Neckel), Profile by the measured radial profile, and Off, the default, leaves the disc alone. The line under the chips is the fit made on your stack: the coefficients, how bright the limb is next to the centre and how far the rings stray from the law. Only the disc changes; the gain eases to 1 at the limb. See Limb darkening.

The Sharpen step with the limb darkening flattened

The toolbar gains two chips here and on the next step: Before / after splits the picture at a line you can drag, the stack as it was on the left and with your edits on the right; Full size renders the preview at full size instead of shrunk. The inspector says how long the last preview took.

Sharpening can create artefacts; the step says so, and so does Sharpening and adjustments, which explains each method, when to use it and what the recommended values are. The file notes what you did, and a FITS file is the unsharpened stack unless you ask otherwise in Export.

7. Adjust

The Adjust step

Everyday edits, each optional: Denoise, Levels (black point, white point, gamma and an Auto levels button), a tone Curve (click and drag its points, or type their input and output), Crop (a rectangle in pixels, or the whole image), Rotate and flip (quarter turns, a fine angle, mirrors) and Resize (10% to 400%), and, for lines with prominences, the Prominence layer (a stretched off-limb layer joined to the disc through a feathered edge; see Prominences and the disc together). The line under the controls gives the size of the picture that will be written, and Reset all adjustments switches everything off. The adjustments are applied after the sharpening, in a fixed order, and recorded in the file.

Editing the curve

The curve plot is a control you can drag, with the mouse or the keyboard:

  • Click empty space in the plot to add a point there; keep the button down and drag to place it.
  • Drag a point to move it. The pointer turns into a hand over a point.
  • Double-click a point, or right-click it, to remove it. The end points at input 0 and 1 cannot be removed; they only move up and down.
  • With the plot focused (click it, or Tab to it), [ and ] choose the previous or next point, the arrow keys nudge the chosen point by 0.005 (hold Shift for 0.05) and Delete removes it.

A point can never be dragged past its neighbours. It stays a little (0.005) to the right of the point on its left and to the left of the one on its right, and its output stays between theirs, so the curve never falls: a darker input is never made brighter than a lighter one. This is the same rule the program enforces for a curve typed into the boxes under the plot (a curve that breaks it is held back and the line under the plot says why), so a dragged curve is always valid. There are at most 16 points. Add point and Reset still work, and the boxes under the plot show and edit the exact numbers.

8. Colour

The Colour step

Colour is for sharing; a filter stack stays mono. (A true-colour stack is colour already: the step then shows its gains and channel shifts instead of the cards, because no look applies, and sharpening and adjustments run on each channel.)

The Colour step after a true-colour stack

Each card shows a look as a ramp from black to white, with its filter and wavelength: Mono, then H-alpha, Ca II K, Ca II H, Mg I b, Na I D, He I D3, H-beta and White light. Click one and the viewer shows the stack in those colours at once. Under the cards, a line says why the look is what it is.

  • Invert makes a negative before colouring, which shows filaments and prominences well.
  • Strength mixes between grey and the full colour.
  • Continue to export goes on.

The app remembers the look you chose for each filter. The look you leave here is what Export writes, and the viewer, on this step and the next, shows exactly that. Colourising explains the presets and the maths.

9. Export

The Export step

Choose TIFF 16-bit, PNG 8-bit or FITS (32-bit float) and press Save as, or press Ctrl+E from anywhere. The step says what will be written, for example 16-bit RGB TIFF, H-alpha colours. TIFF and PNG carry the sharpening, the adjustments and the colour look; FITS is always mono, and is the unsharpened stack unless you turn on Edit the FITS too (the step says which it will be). Every file notes the edits and the look used (see What the files say and Sharpening and adjustments). The edits run on the full-size stack, then the colour. The file is written in the background, with an Applying the edits and writing the file notice, and a message confirms where it went.

The viewer

The toolbar above the image has toggle chips:

  • Alignment points shows or hides the grid;
  • Showing frame or Showing stack switches between the current capture frame and the stacked result (Space does the same). On the Sharpen, Adjust, Colour and Export steps the stack is shown as it will be written, sharpened, adjusted and coloured, and the pill at the bottom of the picture names what has been done;
  • Before / after and Full size, on the Sharpen and Adjust steps (see above).

On the right of the toolbar are the number of points and their size, and the zoom controls: minus, the zoom readout (press it to fit the image to the window) and plus. You can also scroll to zoom around the pointer, drag to pan and double-click to fit.

Under the viewer, the frame scrubber reads, for example, Frame 42 / 100 · quality 0.71 · rank 18. Quality runs from 0 for the worst frame to 1 for the best, on the same scale as the curve below, so it falls steadily as the rank number rises, and rank 1 is the sharpest. Drag the slider or use the arrow keys to move through the capture.

Below that, Quality, sorted plots every frame's score from best to worst. The dashed line marks where the frames you keep end, the shaded area is the kept set, and the white dot is the frame on screen. Hover the curve to read a rank and click it to jump to that frame.

Advanced settings

The foot of the inspector has an Advanced settings button. It is closed by default, so the everyday path is not crowded; press it to show three groups of settings that apply to every step:

  • Backend: Auto uses the best adapter wgpu finds and the CPU otherwise; CPU and GPU force the choice (see GPU acceleration). The chip in the top bar shows the graphics API and the adapter that is in use, or CPU. A long adapter name is shortened with an ellipsis; hover the chip for the whole name.
  • Debayer: the demosaic method (Bilinear or MHC, Malvar, He and Cutler). This matters only for colour sensor captures.
  • Channel: the channel that is analysed and stacked (R, G, B or Lum). The recommended one for your filter is selected when a colour capture opens.

Settings file

Helia keeps a small text file of choices it should not ask twice: whether the first-run tips were dismissed, the colour look last chosen for each filter, the backend, export format, outlier rejection and full-size preview you last chose, the folders you last opened a capture from and exported to (the file dialogs start there), the last eight captures you opened (the Recent menu), the size of the window when you last closed it (not saved while it is maximised or full screen), and (only if you set it) the address of the feedback service. The keep percentage, the alignment point size, Auto, the crop and true colour are not remembered: the first two come from each capture's recommendation, the others start off. (Saved sharpening presets are separate files in their own folder; see Sharpening and adjustments.) It is $HELIA_GUI_CONFIG if that is set, otherwise %APPDATA%\helia\gui.conf on Windows or $XDG_CONFIG_HOME/helia/gui.conf (~/.config/helia/gui.conf) on Linux and macOS. The format is one key=value per line, for example:

tips_dismissed=true
colour.ha=ha,false,1.00
colour.cak=cak,true,0.80
backend=auto
export_format=tiff
robust=off
full_preview=false
last_open_dir=/home/me/captures
last_export_dir=/home/me/stacks
window=1500x920
recent=/home/me/captures/sun.ser
recent=/home/me/captures/limb.ser
feedback_url=https://feedback.example.org

recent lines are newest first. Bug reports leave the recent list out, like the folders. robust is off, sigma, huber or winsor; a file from before the robust modes may still hold sigma_clip=true, which is read as sigma. Delete the file to start afresh. A missing or damaged file is not an error (an unknown value is ignored and the default stays), and the app writes nothing until you change something it remembers.

Log file and bug report

Helia (the app, the command line and helia serve) writes a small log so a problem can be looked into afterwards. It is helia.log in %LOCALAPPDATA%\helia\logs on Windows, or in $XDG_STATE_HOME/helia (~/.local/state/helia) on Linux and macOS; $HELIA_LOG_DIR overrides the folder. Each line is a UTC time, a level, the program and a message, for example 2026-10-19T12:00:00Z INFO helia-gui: opened sun.ser, 1920x1080, 8-bit RGGB, 631 frames. It records what was started, which capture file was opened (its name, never its folder), what was exported and any error. It never holds image data, and a path in an error message is cut down to the file name. At 512 KiB the log rotates into helia.log.1 and helia.log.2 and the oldest is dropped, so it stays under about 1.5 MiB. Nothing is sent anywhere.

Save bug report on the About page writes a .zip of four text files for you to attach to an issue: summary.txt (the version block, operating system and GPU adapter), capture.txt (the open capture's header in one line), settings.txt (the settings file above, without folder names) and log-tail.txt (the last 300 log lines). It holds no images and no screenshot; open it and read it before you send it. It is separate from Send feedback, which sends a screenshot and a description to the relay when you press Send.

Small windows

Below about 1100 pixels wide the rail and the inspector collapse so the viewer keeps its space. Two buttons appear in the top bar to show either one; only one is open at a time.

About

The sun in the top bar, the About button next to it, or F1 opens the About page, which replaces the viewer until you press Esc or Close. It leads with the dedication, then gives the version and how this copy was built: git commit (+dirty if the tree had uncommitted changes), build time in UTC, build profile, target, Rust and wgpu versions, and the GPU adapter in use or CPU. Copy version info puts those lines on the clipboard for a bug report. Below are the copyright, the GPL notice with its no-warranty paragraph, links to the source, the licence text and the releases page, and the third-party licences (from THIRD-PARTY-LICENSES.txt beside the program in release archives). The page makes no network connections; a link opens in your browser only when you click it. While Helia is below version 1.0.0 the page, the top bar (a Pre-release badge beside the name) and the window title (Helia 0.3.0 (pre-release)) all say that this is a pre-release, and the version block has a release: pre-release line. Send feedback on this page opens the feedback page, below.

The About page

Feedback

The Feedback button in the top bar (or Ctrl+Shift+F), at any step, takes a picture of the Helia window and opens a page where you draw rectangles round the problem, say whether it is a bug, a feature request, a question or something else, and send it as an issue on the project's forge. Nothing is sent until you press Send, and Helia has no telemetry. Sending feedback has the whole walk-through, what is and is not included, and what happens when the feedback service cannot be reached.

The feedback page

Keyboard shortcuts

KeysAction
Ctrl+OOpen a capture
Ctrl+EExport the result
Left, RightPrevious or next frame
Shift+Left, Shift+RightTen frames back or forward
Home, EndFirst or last frame
SpaceSwitch between the frame and the stacked result
F1Open or close the About page (Esc also closes it)
Ctrl+Shift+FSend feedback (Esc closes the page)
Tab, Shift+TabMove between controls

The arrow keys and Space act on the viewer when no control has keyboard focus. Every control has an accessible name and a tooltip, and the window works with screen readers through AccessKit.

Testing the screenshots

The images above are checked by cargo test -p helia-gui (see Building and testing). The tests render the window at 1440 by 900 pixels (the Open header view is taller, at 1440 by 2600, to show the whole inspector) on a software GPU, from a synthetic capture with injected state, and fail if the result differs from the committed image. They run only when HELIA_GPU_TESTS=software is set, and otherwise print a note and pass. To regenerate the images after an intended change:

UPDATE_SNAPSHOTS=1 HELIA_GPU_TESTS=software cargo test -p helia-gui snapshots

Sharpening and adjustments

A stack is cleaner and smoother than any single frame, but it is not as sharp as the telescope could be. Sharpening brings the fine detail back. Helia offers three methods, each with recommended values for your filter, and a set of everyday adjustments: denoise, levels, a curve, crop, rotate, flip and resize.

Everything here is non-destructive. The stack is never changed. Your choices are an ordered list of operations that Helia applies to the stacked image: on a shrunk copy for the live preview, and on the full-size stack when you export. The list is written into the output file, so a picture always says how it was made.

Sharpening runs on the mono stack, before colour. Detail and noise are measurements; colour is presentation, and is applied to the sharpened image afterwards (see Colourising).

Sharpening can create artefacts. Every method can draw halos beside bright edges, ring around the limb and round sunspots, turn noise into a texture that looks like detail, and make a soft image look processed. None of them can add detail the telescope and the seeing did not record. Use Before / after to compare, and ease off as soon as the picture starts to look different in kind rather than just crisper. If the recommended values look overdone for your capture, they are overdone: turn them down.

Which method

MethodWhat it doesReach for it whenRisk
Wavelets (the default)Splits the image into layers by detail size (an a-trous transform), lifts or lowers each layer, and removes what is not clearly above the noise in eachAlmost always: it sharpens fine and medium detail and keeps noise down at the same timeLow if the noise cuts are set; noise turns to grain if they are zero
Unsharp maskAdds back a share of the difference between the image and a blur of itYou want a light, predictable touch, or one size of detailHalos beside sharp edges at high amounts
Deconvolution (Richardson-Lucy)Undoes a known blur, the point spread function (PSF), by repeated correctionThe stack is good, the blur is about the same everywhere and you want the best the data can giveThe most: ringing, speckle and over-sharpening. The limb guard (below) controls the worst of it

Wavelets

The image is smoothed repeatedly with a small spline (the a-trous, or "with holes", transform: 1 4 6 4 1 over 16, with the taps spread twice as far apart at every step). The difference between two steps is a layer. Layer 1 holds the finest detail and most of the noise; layer 2 fine detail such as granulation and fibrils; each higher layer larger structure. This is the transform behind Registax.

Each layer has two numbers:

  • Gain. 1 leaves the layer alone, above 1 sharpens it, below 1 softens it.
  • Noise cut. Detail weaker than this many noise sigmas is taken to be noise and removed. 0 keeps everything. The noise level of each layer is measured from the image itself, from the median absolute deviation of the layer (a few bright features do not move it), so the cut follows your capture rather than a fixed number.

A layer with gain 1 and noise cut 0 is left exactly as it is: with every layer at its neutral setting the result is the input, bit for bit.

Unsharp mask

The radius (a Gaussian sigma in pixels) is the size of the blur that is subtracted, so it picks the size of detail that is boosted. The amount is how much of the difference is added back: 0.6 is gentle, 2 strong. The threshold leaves detail weaker than it alone, so noise is not boosted.

Deconvolution

Richardson-Lucy deconvolution estimates the sharp image that, blurred by the PSF, gives the stack you have, by repeating a multiplicative correction. Helia adds total-variation (TV) regularisation (Dey et al., 2006), which keeps noise from being amplified into speckle.

  • Blur to undo (PSF). Auto is a Gaussian sized from the image scale: Helia measures the radius of the disc in pixels, from that works out the arc seconds per pixel, and assumes a stack that is about 2 arc seconds across at half maximum. It never goes below 0.7 pixel, because a smaller blur is below the sampling and there is nothing to recover. Gaussian is the same shape with a sigma you choose, for seeing and a slightly soft focus. Airy is the diffraction pattern of a circular aperture, for a sharp, steady image through a small telescope; give the radius of its first dark ring in pixels (on the command line, airy-scope:656,100,0.9 works it out from the wavelength in nanometres, the aperture in millimetres and the pixel scale in arc seconds).
  • Iterations. More is sharper and noisier; stop when noise starts to look like texture.
  • Noise control (TV). The weight of the regularisation, 0 to 0.05.
  • Limb and sunspot guard. Deconvolving a bright edge leaves a bright halo inside it and a dark one outside; the solar limb and the edge of a sunspot are the worst cases. For every pixel the guard measures how large a step lies within reach of the blur, as a share of the image's range, and where that is large (the limb, an umbra) it holds the result inside the range of the original image around that pixel. A true sharpening of an edge has its values on the two sides of the edge, so the edge still steepens, but a halo beyond them cannot form. Granulation and filaments, which have little contrast, are untouched. The guard is on by default; its strength runs from 0 (off) to 1.

How well the guard works is measured, not asserted. On a synthetic limb (a flat disc on dark sky, blurred by a Gaussian of sigma 2 pixels, with noise, then deconvolved for 30 iterations) the largest excursion beyond either plateau within 12 pixels of the limb is 16.7% of the step without the guard and 1.1% with it. Round a synthetic sunspot of the same blur the figures are 10.9% and 1.9%. The test suite fails if the guarded figures reach 2%.

The values come with the filter presets and follow the same rule as the rest of the recommendations: a safe starting point, never the last word. Faint lines (Ca II K and H, He I D3, H-beta) are noisy, so they get higher noise cuts, gentler gains and a shorter deconvolution. White light has the most signal and the finest structure, so it takes the strongest settings. H-alpha sits between, with its fibrils on layers 2 and 3.

The two photospheric filters, white light and Na I D, are tuned for granulation. It sits on layers 2 and 3 at a few per cent contrast, so those layers get the largest gains and no noise cut (a cut there removes granulation with the noise, which is how the 0.2.0 defaults lost it); the finest layer keeps its cut so pixel noise is not boosted. See White-light granulation, which also shows how to measure the result with helia measure.

FilterWavelet gains, layers 1 to 6Noise cuts, layers 1 to 6Unsharp amountDeconvolution iterations
H-alpha1.50, 1.45, 1.25, 1.05, 1, 11.8, 1.0, 0.5, 0, 0, 00.6012
Ca II K and H1.30, 1.40, 1.25, 1.05, 1, 12.5, 1.5, 0.8, 0.3, 0, 00.458
Mg I b1.40, 1.40, 1.20, 1.05, 1, 12.2, 1.3, 0.6, 0, 0, 00.5010
Na I D1.20, 1.80, 1.50, 1.10, 1, 12.0, 0, 0, 0, 0, 00.5010
He I D31.30, 1.35, 1.20, 1.05, 1, 12.5, 1.5, 0.8, 0.2, 0, 00.408
H-beta1.30, 1.40, 1.25, 1.05, 1, 12.5, 1.5, 0.8, 0.3, 0, 00.458
White light1.50, 1.80, 1.50, 1.10, 1, 11.0, 0, 0, 0, 0, 00.8020
Not recognised1.50, 1.45, 1.25, 1.05, 1, 11.8, 1.0, 0.5, 0, 0, 00.6012

The unsharp radius follows the image scale (1.4 times the default blur, between 1 and 3 pixels). The full table, with the thresholds, TV weights and denoise strengths, is generated from the code: see Colour presets.

Limb darkening

The disc is brighter at the centre than at the limb, because the line of sight reaches a cooler, shallower layer there. With mu the cosine of the angle from the local vertical (sqrt(1 - (r/R)^2) at radius r on a disc of radius R), the brightness relative to the centre is a law L(mu):

Law (--limb-law)L(mu)Coefficients
linear1 - u (1 - mu)u
quadratic (default)1 - u1 (1 - mu) - u2 (1 - mu)^2u1, u2
neckel1 - sum a_k (1 - mu^k), k = 1 to 5a1 to a5

The fit. Helia fits the law to the azimuthal median of the stack, in rings one pixel wide about the fitted disc centre, out to 97% of the radius (the limb itself is blurred by the seeing). A median ignores sunspots, granulation, filaments and plage, which cover a minority of each ring. The sky level just outside the limb is subtracted first. The fit is linear in the coefficients and exact; the five-term form has a light ridge, because the powers of mu are nearly collinear and the rings barely determine five numbers (compare the law, not the terms). Every helia stack prints it and the API returns it:

limb:    quadratic: u1 = 0.470, u2 = 0.046; limb at 48% of the centre; rings differ from the law by 0.67% rms

helia measure image.fits --limb fits all three laws to an existing image and prints the measured profile beside them. The fit measures the image, not the Sun: an uncorrected vignette, a radial gradient from an etalon, or a disc that is cut by the frame all read as limb darkening. Flats (Calibration) remove the first; the last is the reason for the partial-disc row below.

Flatten. --flatten model (or the Model chip in the Sharpen step) divides the disc by the fitted law; --flatten profile divides it by the measured profile itself (smoothed over a few rings), which also removes any other radial gradient. It runs first in the list, before denoise and sharpening, so the wavelets and deconvolution see flat contrast and a gain set for the centre is right at the limb too. It works on helia edit as well, and as an edit {"op": "flatten", "source": "model", "law": "quadratic"} in a preset or an API job. What it leaves alone:

  • Everything outside the limb: prominences have their own brightness, which has nothing to do with the photosphere's darkening.
  • The last 1.5% of the radius: the gain eases to 1 there. The limb is a blur of disc and sky, and a gain of two or three would turn it into a bright rim.
  • The bright end of the disc: the gain is scaled so the 99.9th percentile of the disc is where it was. Dividing out the darkening brightens the limb side, and an image with a brighter side (the etalon sweet spot in these captures) would otherwise clip at white.

With no clear disc (a cropped region) the edit leaves the image alone, and the limb: line says so. A flat disc is for looking and for sharpening and compositing; the stack itself and the FITS file are not flattened unless you ask (--sharpen-fits applies the edits to FITS too).

The Sharpen step with the limb darkening flattened by the fitted law, split before and after

The picture is a synthetic capture; the fit under the chips is the one made on that stack.

What the real captures say

The four sodium captures (ASI482MC, Na D, 589.3 nm, stacked with the Na D preset), fitted with helia measure --limb:

CaptureDiscLinear uQuadratic u1, u2Limb level (linear, quadratic)Rings off the law (quadratic)
Sun_Test.serwhole0.5390.546, -0.01246%, 47%0.71%
15_20_57.serwhole0.4970.470, 0.04650%, 48%0.67%
15_32_09.aviwhole0.5550.515, 0.07044%, 41%0.79%
14_42_57.avi63% of the limb0.7200.710, 0.01828%, 27%3.58%

The five-term fit of the three whole discs gives a1 = 0.44 to 0.45, a2 = 0.04 to 0.14, a3 = 0.02 to 0.04, a4 = -0.001 to 0.07, a5 = -0.10 to -0.08, with the limb at 47% to 52% of the centre.

Against the literature, as far as it could be checked from the sources available while writing this (the Neckel and Labs 1994 coefficient table itself was not available, so the five-term values are not compared):

  • Allen's Astrophysical Quantities (Cox 2000), as quoted for the Sun at 550 nm: quadratic u1 = 0.47, u2 = 0.23, a limb at 30% of the centre.
  • The linear coefficient is 0.56 at 600 nm and 0.95 at 320 nm (Tatum, Stellar Atmospheres, chapter 6); darkening is weaker towards the red.

The Na D passband is between those two, and the fits sit where that puts them. The linear coefficient of the three whole discs (0.50 to 0.56) brackets the 0.56 at 600 nm, and the quadratic u1 of 15_20_57.ser (0.470) is the 0.47 of the 550 nm table. The limb is brighter than at 550 nm (41% to 48% against 30%), which is the direction a redder band predicts. Caveats on all of this: u2 is poorly determined (it is -0.012 on one capture and 0.23 in the table) because u1 and u2 trade against each other when the rings stop at mu = 0.24; a narrow line image is not a continuum, and the D lines are formed higher than the continuum, so line-core darkening differs from continuum darkening; the cameras are not radiometrically calibrated and the captures are not flat-fielded; and the green channel of a colour camera is a broad band. The partial disc (14_42_57.avi) is outside what the method supports: the rings are not full circles, the fitted radius is larger than the frame and the fit is a poor one (3.6% rms), so its coefficients are not a measurement of anything.

Flattening 15_20_57.ser with either source takes the fitted linear coefficient from 0.497 to -0.000 (model) and 0.000 (profile), with the rings 0.74% and 0.35% off a flat line (the profile removes the whole radial gradient, the model only the law). The granulation does not move: the granulation band of helia measure is 2.050% before, 2.052% with the model and 2.049% with the profile, the fine band 1.573%, 1.572% and 1.571%. This capture also has a bright patch on one side that is not radial (the filter's sweet spot); flattening leaves it, as it should, and the bright-end scaling is what stops it clipping.

Adjustments

The Adjust step holds the everyday edits. Each is optional and each is recorded in the file.

  • Denoise. A wavelet soft threshold on the finest layers: strength is the threshold of layer 1 in noise sigmas, and each coarser layer gets less. On flat sky it lowers the noise a good deal; across a sharp edge it leaves the contrast alone (see the tests, below). It runs before sharpening.
  • Levels. A black point, a white point and a gamma. Above gamma 1 the mid-tones are brighter. Auto levels sets the points to the 0.1% and 99.9% points of the stack. Levels never lower a darker pixel below a lighter one, and between the two points they can be undone exactly.
  • Curve. A tone curve through up to 16 points, drawn as a monotone cubic, so it never overshoots between the points and never makes a darker input brighter than a lighter one. In the app you click the plot to add a point, drag a point to move it and double-click it to remove it; with the plot focused, [ and ] choose a point and the arrow keys nudge it. A dragged point stops at its neighbours (0.005 apart in input, and between their outputs), which is what keeps the curve rising. See Editing the curve.
  • Crop. A rectangle, in pixels. Exact.
  • Rotate and flip. Quarter turns and mirrors move whole pixels and are exact. Other angles are interpolated (Lanczos-3) and enlarge the canvas to hold the whole picture; the corners are filled with the level of the image border.
  • Resize. A scale from 10% to 400%. Shrinking averages exactly the area each new pixel covers; enlarging interpolates with Lanczos-3.

The order is fixed: flatten (if asked), denoise, the sharpening method, levels, the curve, crop, rotate, flip, resize. For another order, write the list by hand (see saved presets below).

The live preview

In the app, every slider updates the picture as you drag it. The preview is made from the stack shrunk by a power of two until it is at most 1280 pixels on a side, the sizes in pixels (radii, crop) shrink with it, and the wavelet layers finer than the preview can show are left out. It is therefore a close look, not a proof: what is written is always made from the full-size stack. Press Full size in the toolbar when you want the preview at full size to judge fine detail at 100% zoom; it is slower and a notice says so.

The work runs on the GPU when there is one, and on the CPU otherwise, off the window's thread. One preview is made at a time: while you drag, the latest position is picked up when the last render ends, so the window never queues work it will throw away. The inspector shows how long the last preview took. Anything slower than about a seventh of a second gets a notice, as everything else does.

Before / after splits the picture: the stack as it was on the left, with your edits on the right, and a line to drag. Both halves have the same crop, rotation and size, so they line up.

The Sharpen step with the wavelet layers, and the picture split into before and after

Deconvolution has a blur, iterations, noise control and the guard:

The Sharpen step with Richardson-Lucy deconvolution

The Adjust step:

The Adjust step with denoise, levels and a curve

Saved presets

My presets, in the Sharpen step, keeps the whole list (sharpening and adjustments) under a name. Type a name and press Save; press a preset to use it, or the minus beside it to delete it. A preset is a small readable file in the presets folder: $HELIA_EDIT_PRESETS if that is set, otherwise %APPDATA%\helia\edit-presets on Windows or $XDG_CONFIG_HOME/helia/edit-presets (~/.config/helia/edit-presets) elsewhere. For example:

{
  "name": "H-alpha gentle",
  "filter": "ha",
  "edits": [
    {"op": "wavelet", "layers": [{"gain": 1.3, "threshold": 2.0}, {"gain": 1.3, "threshold": 1.0}]},
    {"op": "levels", "gamma": 1.1}
  ]
}

A file can also hold just the list, [ ... ]. Missing values take their defaults. The app shows one operation of each kind in the fixed order; a preset with repeats or another order is still run in full by the command line and the API.

The operations are objects with an op of unsharp (radius, amount, threshold), wavelet (layers, each with gain and threshold), deconvolve (psf, iterations, tv, protect; the psf is {"kind": "auto"}, {"kind": "gaussian", "sigma": 1.6} or {"kind": "airy", "radius": 2.2}), denoise (strength, layers), levels (black, white, gamma, out_black, out_white), curve (points, each [in, out]), crop (x, y, width, height), rotate (degrees), flip (axis: horizontal or vertical) and resize (scale, or width and height).

On the command line

helia stack sharpens with --sharpen wavelet, unsharp, rl or none (the default), using the recommended values of the filter; every value has a flag that overrides it:

helia stack sun_Ha.ser -o sun.tif --sharpen wavelet
helia stack sun_Ha.ser -o sun.tif --sharpen wavelet --wavelet-gains 2.0,1.5 --wavelet-noise 2.5
helia stack sun_Ha.ser -o sun.tif --sharpen unsharp --unsharp-radius 1.8 --unsharp-amount 0.9
helia stack sun_Ha.ser -o sun.tif --sharpen rl --rl-iterations 25 --rl-psf airy-scope:656,100,0.9
helia stack sun_Ha.ser -o sun.tif --sharpen wavelet --denoise --gamma 1.2 --crop 100,80,1600,900
helia stack sun_Na.ser -o sun.tif --flatten model --sharpen wavelet
helia edit stack.fits -o flat.tif --flatten profile --limb-law neckel

A flag for one method without that method is an error that says which method to add. Lists such as --wavelet-gains 2.0,1.5 replace the first layers and leave the others at their recommended values.

helia edit applies the same operations to an image that already exists, a TIFF, PNG or FITS, mono or colour:

helia edit stack.fits -o sharp.tif --sharpen wavelet --preset ha
helia edit sun.tif -o sun-cropped.tif --crop 200,150,1200,900 --rotate 90 --resize 50%
helia edit stack.fits -o again.tif --edits "H-alpha gentle"
helia edit stack.fits -o x.tif --sharpen rl --save-edits "My deconvolution"

--edits takes the name of a saved preset or a path to a .json file, and the other flags are added after it. --save-edits keeps the resulting list as a preset. A colour image is edited channel by channel and stays colour. When the image is mono, helia edit prints how much finer detail the edits gave it and what they did to the noise (see below).

In the API

A job's config takes sharpen (wavelet, unsharp, rl or none, with the filter's recommended values) and edits, a list of operations in the form above, applied after sharpen. See The API.

What the files say

The edits are recorded, in words and as the list itself, so that a file says how it was made and the edit can be repeated:

FileWhere the note is
TIFFthe ImageDescription tag
PNGthe Description text chunk
FITSHISTORY cards in the header

The note reads, for example: Helia edits, applied in order to the stack (2 operations); sharpening runs on the mono stack before any colour, then 1. wavelet sharpen, 6 layers, gains 1.50,1.45,..., noise thresholds ..., and last Helia edit list (JSON): followed by the whole list as JSON.

FITS is the unsharpened stack by default. A FITS file is for further processing and measurement, so Helia writes the stack as it is and says so in the header; the edits go into TIFF and PNG. If you want the sharpening in the FITS file too, ask for it: the Edit the FITS too switch in the Export step, --sharpen-fits on the command line, or "sharpen_fits": true in a job. The file then holds the edited mono image, and its header says so.

Does it work?

The tests check the claims on synthetic images where the truth is known:

  • Deconvolution recovers detail. A synthetic sun with granulation and sunspots, blurred by a Gaussian of sigma 1.8 pixels with noise: after 25 iterations the root-mean-square error against the clean truth falls from 0.0349 to 0.0163 (the test requires below 80% of the start). The same with an Airy blur: from 0.0180 to 0.0044.
  • The limb guard holds. See the figures above.
  • Wavelet layers at gain 1 and noise cut 0 return the input exactly.
  • Levels and curves rise monotonically for every valid setting, and levels undo exactly between their black and white points.
  • Crop, rotate, flip and resize move the right pixels: a quarter turn clockwise sends the top-left corner to the top-right, four turns return the original, and halving averages 2 by 2 blocks.
  • Denoise lowers the noise on flat regions from 0.0202 to 0.0048 (the test requires under 60% of the start) while the contrast of an edge goes from 0.401 to 0.390 (the test requires above 95%).
  • The GPU agrees with the CPU on every operation, within 1e-4 for the filter operations and 2e-3 after 12 iterations of deconvolution (see The GPU parity rule).

On real captures

Four captures of the Sun from 2022-07-17 (a ZWO ASI482MC; the files are not in the repository, and Helia could not tell their filter from the names, so the generic recommended values apply) were stacked with the defaults, written as FITS (the unsharpened stack), and then sharpened by each method with its recommended values. The figures are from the helia edit summary. Detail is Helia's existing sharpness metric, the gradient energy of the image after a 2 pixel Gaussian smoothing, as a ratio to the stack; noise is the median-based sigma of the finest wavelet layer, which is mostly noise, before and after.

CaptureWavelets: detail, noiseUnsharp mask: detail, noiseDeconvolution: detail, noise
15_20_57.ser, 1920 x 1080, 16-bit, 100 frames1.25x, 0.00028 to 0.000011.03x, 0.00028 to 0.000281.03x, 0.00028 to 0.00028
15_32_09.avi, 720 x 540, 8-bit, 1000 frames1.25x, 0.00036 to 0.000101.03x, 0.00036 to 0.000361.03x, 0.00036 to 0.00039
14_42_57.avi, 1920 x 1080, 8-bit, 630 frames1.22x, 0.00031 to 0.000081.03x, 0.00031 to 0.000311.02x, 0.00031 to 0.00200
Sun_Test.ser, 1920 x 1080, 8-bit, 631 frames1.24x, 0.00679 to 0.000011.04x, 0.00679 to 0.008351.02x, 0.00679 to 0.00777

What it says:

  • Wavelets raise the detail metric by about a quarter on all four and take most of the noise out of the finest layer at the same time. With the white-light values the gain is 1.29x to 1.32x. (After a noise cut the median-based noise figure is close to zero by construction: most of the layer is zero.)
  • Unsharp mask and deconvolution barely move this metric. That is partly the metric: it smooths by 2 pixels, and these two work below that scale (the default unsharp radius is 1 pixel and the automatic blur is 0.7 pixel, the smallest allowed). It is not only the metric, though. On 14_42_57.avi the deconvolution raises the finest-layer noise six-fold. This was first put down to a fixed pattern of the sensor (faint vertical stripes), and Calibration measures it: the sensor does have stripes, but they grow by 5% under deconvolution, while the share of the energy above 0.3 cycles per pixel goes from 2% to 70%. So the rise is broadband noise at the finest scale, which no dark or flat removes, and a calibration estimated from the capture itself did not change it. Look at the picture, not the number, and take the wavelets.

On these stacks the recommended wavelets are the method to start with, and deconvolution gains little and risks a lot. That is one capture session through one camera, not a general result.

How fast

The live preview renders at most 1280 pixels on a side. Best of five on an NVIDIA A100 (Vulkan) and on the same machine's 24 CPU threads (not otherwise idle), cargo test -p helia-gpu --test parity preview_latency -- --ignored --nocapture:

Edit, 1280 x 960CPUGPU
Wavelets, 6 layers (recommended)25 ms20 ms
Unsharp mask6 ms7 ms
Denoise and levels21 ms16 ms
Deconvolution, 20 iterations, Gaussian blur433 ms74 ms
Deconvolution, 20 iterations, Airy blur1549 ms112 ms

On the GPU every preview is within the 150 ms the sliders aim for. On the CPU the filters are too, but deconvolution is not, and the app says so with a notice (Sharpening and colouring the preview) while it works. A one-off helia edit of a 1920 x 1080 image, process start to file written, took 0.1 to 0.3 s for wavelets on the CPU and about 1 s on the GPU (most of it opening the device), and 0.3 to 1.4 s for deconvolution at its recommended iterations on either. The app opens the device once and keeps it.

Limits

  • Geometry (crop, rotate, flip, resize) always runs on the CPU; it costs next to nothing beside a filter.
  • Deconvolution handles one PSF for the whole image. Seeing that changes across the disc is handled by the alignment points of the stack, not here.
  • An Airy blur is limited to a first dark ring of 6 pixels; wavelet transforms to 8 layers; curves to 16 points; a list to 64 operations.
  • The preview approximates the export (see above); it is not a bit-exact rehearsal of it.

Colourising

A solar capture through a narrow filter is a picture of one colour of light, so the stack is monochrome. The orange of H-alpha, the violet of calcium and the yellow of sodium are choices made when a picture is shared, not something the camera measured. Helia gives each common solar line a recommended look, and applies it only when you ask for colour.

Colour is presentation, not measurement. For a filter capture the stack stays mono. A FITS file is never coloured by a look and its pixels are never touched. (White light with a colour camera is the exception: see True colour.) Colour goes to a 16-bit RGB TIFF and an 8-bit PNG, and every file Helia writes records which look was used (see What the files say). If you need numbers, use the FITS file; if you want to share a picture, use the colour one.

True colour

The looks below are for narrow-band captures, where the camera records one colour of light and the colours are a convention. White light taken with a colour camera is different: the Sun really is several colours at once, and the camera records all of them in its Bayer mosaic. --colour true (API colour: "true", the True colour chip in the Stack step) stacks that.

  • One alignment. The frames are scored, aligned globally and measured at the alignment points on the luminance (the mean of the three planes), once. The red, green and blue planes are then stacked with those same shifts, alignment point field and frame weights, so the three planes see the same sky. It costs three stacks (and three reads of the capture) rather than one.
  • Channel alignment. The air disperses colours, so at a low Sun red and blue sit a fraction of a pixel to either side of green. Each of the stacked red and blue planes is registered to the green by phase correlation and moved by what that finds (--no-channel-align skips it). A shift under 0.05 px is noise and one over 6 px or with a poor lock (confidence under 0.3) is not believed; the history and summary.true_colour.channels say what was applied. The shift is measured on the planes as demosaiced: the Malvar-He-Cutler method borrows green's edges into red and blue, so it hides about a quarter of a displacement (on the synthetic test, 0.45 of 0.6 px), and the planes then agree with each other, which is what the picture needs.
  • White balance (--white-balance). auto, the default, makes the middle of the disc (inside 80% of the radius) neutral: gains for red and blue that bring their means to the green's. off keeps the sensor's own balance, and 1.9,1,1.5 gives the three gains. A raw sensor does not weigh the colours as the eye does, so off is rarely what you want for a picture. A channel that holds far less than the green (blue behind a sodium filter) is held at a gain of 4 with a warning, rather than amplified into noise: true colour is for white light.
  • Scale. All three channels share one scale, chosen so that the 99.99th percentile of the brightest channel of each pixel is 1. Nothing clips, and the balance of the colours is the white balance, not the scale.
  • Edits. Sharpening, levels and the other edits run on each channel, as they do for any colour image, so a limb-darkening flatten also corrects the colour of the limb, which darkens differently in each channel. Areas that were flattened in every frame (burned-in text, exclusions) are made grey, so the balance does not tint them.
  • Files. An RGB TIFF or PNG, or a FITS cube of three planes; see Output formats. A look (--preset colours, --invert, --colour-strength) and --channel do not apply, and asking for them is an error. The luminance is what the alignment, the limb fit, the crop and the summary use.

It needs a raw colour capture (a Bayer mosaic): a mono capture, or one that is already demosaiced, has no colours to stack and is refused before any work is done.

What the real captures say

The four captures available are all ASI482MC (RGGB), and three of them are sodium, where the blue pixels are empty. helia stack --colour true --preset white --keep 12% on them (the gains include the common scale):

CaptureGains (red, green, blue)Channel alignment
Sun_Test.ser (sodium)0.96, 1.02, 4.10 (blue held at the cap)red +0.89, +0.00 px; blue not moved
15_20_57.ser (sodium)1.07, 1.00, 3.99 (blue held)red +1.18, -0.09 px; blue +0.06, -0.01 px
15_32_09.avi (sodium)0.82, 1.31, 5.23 (blue held)red +1.83, -0.25 px; blue +0.40, 0.00 px
14_42_57.avi (broad, low contrast)0.76, 1.09, 0.96red +0.11, +0.10 px; blue -0.17, -0.14 px

The sodium captures show why the cap exists (an uncapped blue gain of 15 to 23 would be all noise) and that true colour is not for them: their red and green planes show different things through a sodium filter, so the red shift of a pixel or two is the planes disagreeing, not dispersion. 14_42_57.avi is the one broad-band capture; its planes agree to under a fifth of a pixel, and the CPU and the GPU (an A100) give the same gains and the same shifts to three decimals (5.4 s and 4.0 s for 630 frames). There is no good white-light capture in the test set, so the colour of a real white-light disc has not been checked. What is checked, with known answers, is a synthetic colour disc: the balance, the dispersion (and that planes of the same scene are found not to move), the crop and the files; and the device against the CPU, with and without alignment points, to 4e-7 in the samples.

The presets

Each preset is a filter, with a recommended way to process its captures and a look. The table is generated from the code that colours the pictures, so it is always what Helia does. The ramp runs from black to white through the colours of the look; the swatch after it is what a mid-grey becomes.

IdFilterWavelengthHueRamp, black to whiteMid-grey becomesBayer channelFrames kept
haH-alpha656.3 nmorange-red #A95A21red (r)25%
cakCa II K393.4 nmviolet #5D3D9Bblue (b)30%
cahCa II H396.8 nmviolet #703096blue (b)30%
mgMg I b517.3 nmgreen #429B42green (g)30%
naNa I D589.0 / 589.6 nmyellow-orange #A06212green (g)12%
he-d3He I D3587.6 nmyellow #9B8617all combined (lum)25%
hbetaH-beta486.1 nmblue-cyan #2474A0blue (b)30%
whiteWhite lightcontinuumwarm neutral #807A70all combined (lum)12%

Why each preset recommends what it does:

  • H-alpha (ha): H-alpha is deep red, so a colour camera records it almost only in the red pixels; the green and blue pixels add noise, not detail.
  • Ca II K (cak): Ca K is at the violet edge of vision and a sensor sees it in its blue pixels. The line is faint, so a few more frames (30%) are kept to average the noise down.
  • Ca II H (cah): Ca H sits 3.4 nm from Ca K, so it takes the same channel and frame share. It is drawn a touch redder so the two can be told apart in a pair.
  • Mg I b (mg): 517.3 nm is green, which is where the green pixels of a Bayer sensor respond. The line is dim, so 30% of the frames are kept.
  • Na I D (na): 589 nm is yellow, which a Bayer sensor splits between its red and green pixels, and the sodium filter passes almost nothing to the blue ones. Averaging all three channels mixes those empty blue pixels into the stack and prints the mosaic as a fine diagonal hatch over the granulation, so the green channel (two pixels in four) is stacked: on the owner's captures it holds 1.4 to 2.1 times the granulation contrast of the luminance. It shows granulation, so only the best 12% of frames are kept and the alignment points are a few granules wide.
  • He I D3 (he-d3): 587.6 nm is yellow-green, shared by the red and green pixels, so the luminance of all three is used. It is drawn yellower than sodium.
  • H-beta (hbeta): 486.1 nm is blue-cyan and a sensor sees it in its blue pixels. It is faint, so 30% of the frames are kept.
  • White light (white): A white-light capture already carries all three colours, so the luminance is stacked, and a slight warm tint stands in for the solar spectrum. It shows granulation, so only the best 12% of frames are kept and the alignment points are a few granules wide.

The numbers behind each colour, for anyone who wants to reproduce it:

IdTone exponentGains (R, G, B)Exponents (R, G, B)
ha0.851.00, 0.85, 0.600.70, 1.50, 2.60
cak0.900.85, 0.78, 1.001.35, 1.90, 0.80
cah0.900.90, 0.70, 1.001.15, 2.10, 0.85
mg0.900.80, 1.00, 0.751.80, 0.80, 1.70
na0.901.00, 0.74, 0.400.75, 1.05, 2.80
he-d30.901.00, 0.92, 0.450.80, 0.90, 2.60
hbeta0.900.55, 0.85, 1.002.20, 1.00, 0.75
white1.001.00, 0.96, 0.881.00, 1.00, 1.00

The recommended sharpening of each filter (see Sharpening and adjustments). Wavelet gains and noise cuts are for layers 1 to 6, finest first; the unsharp radius comes from the image scale, and the deconvolution blur is measured from the disc.

IdWavelet gainsWavelet noise cutsUnsharp amountUnsharp thresholdDeconvolution iterationsTV weightDenoise
ha1.50, 1.45, 1.25, 1.05, 1.00, 1.001.80, 1.00, 0.50, 0.00, 0.00, 0.000.600.003120.0041.5
cak1.30, 1.40, 1.25, 1.05, 1.00, 1.002.50, 1.50, 0.80, 0.30, 0.00, 0.000.450.00480.0052.0
cah1.30, 1.40, 1.25, 1.05, 1.00, 1.002.50, 1.50, 0.80, 0.30, 0.00, 0.000.450.00480.0052.0
mg1.40, 1.40, 1.20, 1.05, 1.00, 1.002.20, 1.30, 0.60, 0.00, 0.00, 0.000.500.004100.0051.8
na1.20, 1.80, 1.50, 1.10, 1.00, 1.002.00, 0.00, 0.00, 0.00, 0.00, 0.000.500.003100.0051.5
he-d31.30, 1.35, 1.20, 1.05, 1.00, 1.002.50, 1.50, 0.80, 0.20, 0.00, 0.000.400.00480.0052.0
hbeta1.30, 1.40, 1.25, 1.05, 1.00, 1.002.50, 1.50, 0.80, 0.30, 0.00, 0.000.450.00480.0052.0
white1.50, 1.80, 1.50, 1.10, 1.00, 1.001.00, 0.00, 0.00, 0.00, 0.00, 0.000.800.002200.0031.0

Why each filter is sharpened the way it is:

  • H-alpha (ha): H-alpha has plenty of signal and its fibrils and spicules sit on layers 2 and 3, so those layers are lifted most; the finest layer gets a high noise threshold.
  • Ca II K (cak): Ca K is faint and noisy, so the gains are gentler, the noise thresholds higher and the deconvolution short: sharpening noise only makes a prettier mess.
  • Ca II H (cah): Ca H is as faint as Ca K and takes the same careful settings.
  • Mg I b (mg): Mg b2 is dim, so the noise thresholds are a little higher than for H-alpha and the deconvolution shorter.
  • Na I D (na): Sodium D is photospheric: granulation is its finest low-contrast detail. Layers 2 and 3 (granule size) are lifted most and carry no noise threshold, because a threshold there removes granulation with the noise; the finest layer keeps its threshold.
  • He I D3 (he-d3): He D3 shows only in faint prominences and flares, so it is sharpened with the most restraint.
  • H-beta (hbeta): H-beta is faint, like the calcium lines, and takes the same gentle settings.
  • White light (white): White light has the most signal and the finest structure (granulation), so it takes the strongest gains and the longest deconvolution. Layers 2 and 3 (granule size) are lifted most and carry no noise threshold, because a threshold there removes granulation with the noise.

The processing recommendations apply to raw colour cameras and to the share of frames kept:

  • Bayer channel. A colour sensor records each wavelength mostly in one kind of pixel, so stacking that plane alone (--channel r for H-alpha) avoids the noise of the other two. A mono camera has no channels and ignores this.
  • Frames kept. Faint lines (calcium, magnesium, H-beta) are noise limited, so 30 per cent of the frames are kept instead of 25 to average more of it away.
  • Alignment point size is not a property of the filter. Helia chooses it from the image size, about a sixteenth of the short side rounded to a multiple of 8 and kept between 32 and 96 pixels.

Every recommendation is only a default. --keep, --channel and --ap-size (and the same controls in the app and the API) override it.

How the colours are made

Colourising is the classic amateur method: levels and gamma on the grey value, then a gain and an exponent for each of red, green and blue. For a mono value v from 0 to 1, a preset with tone exponent g, gains k and exponents e gives, for each channel,

t   = v ^ g                      (after inversion: v becomes 1 - v)
rgb = mix(t, min(1, k * t ^ e), strength)

Each exponent is positive, so every channel rises steadily from black to its gain: a darker input never gets a brighter output. The result is sampled once into a table of 4096 colours, which the CPU and the GPU both look up with the same linear interpolation, so they agree to within a rounding error.

  • Invert turns the picture negative before it is coloured. A disc-inverted H-alpha image shows filaments and prominences clearly, and is common practice. An inverted picture is exactly the normal look read backwards: a pixel of value v gets the colour that the normal look gives to 1 - v.
  • Strength mixes between grey (0 per cent) and the full colour (100 per cent). It is a way to keep a hint of colour, or none.
  • Mono is the preset-free choice: the grey stack as it is, optionally inverted.

Where the conventions come from

There is no formal standard for solar false colour. The project's research notes (State of the art, October 2026, section 6.2) record that amateur practice is greyscale, pseudo-colour, partly inverted and inverted pseudo-colour variants per filter, that H-alpha is commonly rendered red-orange, and that no source for a Ca K false colour was found. Helia therefore follows the colour of the line's wavelength where the eye can see it (H-alpha red, magnesium green, sodium and helium yellow, H-beta blue-cyan), a conventional violet for the two calcium lines at the edge of vision, and a slightly redder violet for Ca H so a pair can be told apart. They are a starting point; change the look in the app if you prefer another.

In the desktop app

After a stack, Helia moves to the Colour step (between Sharpen and Export).

The Colour step with the H-alpha look chosen

  • The cards show each look as a ramp from black to white, with its name and wavelength. Click one and the stack in the viewer changes straight away; it is made on a background thread, so the window stays responsive.
  • Invert and Strength change the preview as you move them.
  • The text under the cards says why the preset recommends what it does.
  • The app remembers the look you last chose for each filter, in its settings file, so the next H-alpha capture starts with the look you used last time.

A capture with the filter in its name starts with that filter's own look, so the usual path needs no choices at all. The Export step says what will be written, and the viewer shows the stack as it will be exported.

On the command line

helia stack ha.ser -o ha.tif --preset ha          # recommended settings and colour
helia stack ha.ser -o ha.png --preset ha --invert # a shareable inverted picture
helia stack Sun_CaK.ser -o cak.tif --colour auto  # colour from the detected filter
helia stack sun.ser -o sun.tif --colour mono      # plain grey, even if detected
helia stack ha.ser -o ha.tif --colour cak         # a different look for the same stack
helia stack ha.ser -o ha.tif --preset ha --colour-strength 0.6
helia presets
  • --preset <id> says which filter the capture was taken with. It applies the recommended channel and frame share and colours a .tif or .png output.
  • --colour <auto|mono|id> chooses only the colour. auto uses the preset you gave or the filter Helia detected (and fails with a clear message if there is none).
  • --invert and --colour-strength adjust the look.
  • Without --preset or --colour, the output is grey, and Helia prints a tip if it detected a filter.
  • For .fits, colour options are noted in the header and the pixels stay unaltered mono.

See the CLI reference for every option.

Exporting a colour table

The look of a filter can be written as a colour table, to colour the same stack in another program so that the picture matches one made here (#23):

helia lut ha -o ha.cube                 # a 1D .cube LUT, 256 entries
helia lut ha -o ha.lut                  # an ImageJ / Fiji .lut
helia lut cak -o cak.csv --size 1024    # value,red,green,blue as text
helia lut ha -o ha-strip.png --invert --strength 0.8
FormatFileRead by
cubea 1D .cube: a title, the domain and size rows of red, green, blueDaVinci Resolve, Adobe tools and others
csvvalue,red,green,blue, one row per entryspreadsheets and scripts
imageja .lut of 768 raw bytes (256 red, 256 green, 256 blue)ImageJ and Fiji
pnga strip image size wide, 32 highanything that loads an image

--format says the format, or the extension of -o does; --size is 2 to 4096 entries (an ImageJ table is always 256); --invert and --strength are the colour options of the stack. The table is the one Helia colours with, resampled by the same linear interpolation, so a 4096-entry table reproduces it exactly and the .cube of a filter applied to its mono stack gives the same picture up to the rounding of the other program. In the app, the Colour step has the same four formats under Colour table, and the API serves GET /api/v1/presets/{id}/lut?format=cube&size=256&invert=false&strength=1. Colour is presentation, not measurement: the table says nothing about the Sun.

In the API

GET /api/v1/presets lists the presets with their recommendations and colour parameters, and POST /api/v1/captures/inspect reports the settings it would use for a capture (recommended). A job takes preset, colour, invert and colour_strength in its config. See The API.

How the filter is recognised

Helia looks for the filter in these places, most trusted first, and stops at the first match:

  1. the file name (without its extension);
  2. the name of the folder the file is in;
  3. the SharpCap CameraSettings.txt next to it: its Filter, Filter Name, Filter Wheel, Target, Description and Notes entries;
  4. the instrument, telescope and observer strings of a SER header;
  5. the name of the folder above.

The text is split into words at punctuation, at a lower-case to upper-case change (SunHa is Sun and Ha) and between letters and digits (cak3934). A word, or two or three words run together (Ca II K is caiik), must then match exactly, ignoring case:

FilterWords
H-alphaha, halpha, h-alpha, hydrogen alpha, 656
Ca II Kcak, ca k, ca ii k, calcium k, 393
Ca II Hcah, ca h, ca ii h, calcium h, 396
Mg I bmg, mgb, mgi, magnesium, 517
Na I Dna, nad, sodium, 589
He I D3d3, hed3, helium, 587
H-betahb, hbeta, h-beta, 486
White lightwhite, whitelight, wl, continuum, cont

A number such as 656 counts only on its own, so a date like 2026-05-17 is not mistaken for a wavelength. A word inside a longer word never counts: Hampshire is not H-alpha and Sha_nal is nothing. If the filter is not recognised, the app asks once with a chooser, and the command line uses plain defaults and says filter not recognised.

What the files say

Every file records the look it was written with, in words, so a coloured picture cannot be mistaken for a measurement later:

FileWhere the note is
TIFFthe ImageDescription tag
PNGthe Description text chunk (and Software: Helia)
FITSHISTORY cards in the header

The note reads like Colour is presentation, not measurement: preset ha (H-alpha 656.3 nm, orange-red), invert off, strength 1.00. The FITS pixels are written exactly as they would be without it; only the header differs.

The same places also record how the capture was read, whether or not a colour look was chosen, so a stack can be traced back later. These lines are always written:

Capture read as: Bayer RGGB (detection), 16-bit (header), little-endian (detection)
SER header: ColorID 0 (MONO), 16-bit, big-endian
Bayer detection: 2x2 position means 79.6% / 77.6% / 77.4% / 0.3% of full scale, spread 135%, 5 frames
Overrides: --bayer none, --bit-depth 16

The first says which Bayer pattern was used (or none (mono)), the bit depth and (for 16-bit data) the byte order, each with its source (header, sidecar, detection or override), and adds rows flipped when --flip-rows was given. The second is the SER header's own claim, the third appears only when a mosaic was detected, and the fourth only when you overrode something. When the stack was calibrated there is one more line for each master (Calibration: dark subtracted (master dark of 64 frame(s), sigma-clip 3, exposure 0.6400 ms, from darks.ser, fp:...), the same for the flat and for the bad pixels); see Calibration. TIFF joins the lines with ; , PNG puts them in the text chunk, and FITS has one HISTORY card (or more, when a line is long) for each.

What it does not do

  • It colours the stacked image only. Prominence and disc compositing into one high-dynamic-range picture is separate work, tracked in issue #23; colourising here is the colour part of that issue.
  • There are no user-defined palettes or exported lookup tables yet.
  • Sharpening comes before colouring, and is its own step (Sharpening and adjustments): the colour is applied to the sharpened mono image, never the other way round.

Prominences and the disc together

A narrow-band stack holds two pictures at once. The disc is bright and the prominences above the limb are a few per cent of it, so one tone curve cannot show both: lift the prominences and the disc burns out (#23). The prominence layer makes the second exposure from the same stack and joins it to the disc. It is an edit like the others, for H-alpha and the other lines with prominences, and it runs after the sharpening and before levels and the curve.

The Adjust step with the prominence layer on

Using it

  • App: the Adjust step, under Prominences: Prominence layer on, then the sliders. The preview shows the composite as you move them.

  • Command line: --prominence, in helia stack and helia edit, with --prominence-strength, --prominence-stretch, --prominence-level, --prominence-feather, --prominence-reach and --prominence-gate:

    helia stack sun_Ha.ser -o sun.tif --preset ha --sharpen wavelet --prominence --colour auto
    helia edit sun.fits -o sun.png --preset ha --sharpen wavelet --prominence
    
  • API: an edits entry, {"op": "prominence"}, with the same values as fields (strength, stretch, level, feather, reach, gate); see The API.

FITS output stays the unedited stack unless you ask for --sharpen-fits, as for the other edits.

Adjusting it like a prominence processor

Programs such as IMPPG (GPL, by Michal Wisniewski; its README describes a tone curve applied last, brightness-adaptive unsharp masking whose amount differs for the disc and the dim prominences, and live preview of a selection) make the disc and the prominences separately adjustable. The same controls are here, as sliders with a live preview in the Adjust step, where the layer is the first thing in the panel:

  • Strength, Stretch, Level and Gate: how bright and how far into the noise the off-limb layer is lifted (the boost).
  • Join offset and Feather: where the join of layer and disc sits, in disc radii from the fitted limb, and how soft it is.
  • Disc gamma and Layer gamma: a separate tone for the disc and for the layer (--prominence-disc-gamma, --prominence-layer-gamma, --prominence-offset). Both are 1 by default, which leaves the disc bit-identical.

Helia's own tone curve and levels follow in the same step and apply to the whole picture. No IMPPG code was used; only its public README was read to match what users expect.

How it works

  1. The limb. The disc is fitted as a circle (fit_disc), the same fit the alignment and the crop use. Everything at the limb and a few pixels beyond it (the seeing blurs the edge) is still treated as disc.
  2. The glow. Scattered light and sky fall away from the limb, and are often brighter on one side. The background at a radius and angle is the median of the pixels at that radius in a window of 45 degrees, found for 24 sectors and interpolated between them. A prominence fills a small part of such a window, so the median is the glow and not the prominence. Where a ring has too few pixels (the frame cuts it) the all-round median is used.
  3. The signal. The frame less that background, blurred by a Gaussian of 1.2 pixels, less a gate, floored at zero. The gate is --prominence-gate noise sigmas (3 by default; the noise is the median absolute deviation of the outer part of the field) and never less than 0.4% of the disc's brightness, below which the background model's own error is as big as the noise and stretching it would show the model.
  4. The stretch. The signal as a share of its own 99.5th percentile just outside the limb, through asinh(k x) / asinh(k) (--prominence-stretch, k = 6; 0 is linear) and held at 1, scaled so the brightest prominences reach --prominence-level (0.75) of the disc's brightness. The reference is never less than 8 noise sigma, so a field with no prominences stays dark and is not stretched into noise. --prominence-strength multiplies the signal before the stretch.
  5. The join. Inside the limb the disc is untouched. Across a feather of --prominence-feather (1.2% of the radius) on each side of the limb the two are mixed by a smooth step. Outside, the layer replaces the picture, at the black level the stack had (the median beyond --prominence-reach radii, 1.4 by default, when the frame holds enough of it). The layer fades to black at the reach.

The colour table is applied afterwards, so the layer takes the look of the filter.

What was measured

Known answers. The unit tests render a limb-darkened disc with a glow that falls off from the limb, noise and three prominences, and check that the disc is identical well inside the limb, that the brightest prominence (11% of the disc with the glow, before) is lifted to above 40% and more than three times its brightness, that a faint one is lifted too and the order of brightness is kept, that the glow above the limb is taken off and the far sky is the black level, that the join across the limb has no rim, no overshoot and no step back, that a field with no prominences is not lifted into noise (under 0.5% of the pixels above 5% of the disc), and that a frame with no disc passes unchanged.

The H-alpha captures (15_32_09.avi and 15_37_19.avi, 720 by 540, 1000 frames, the best 25% stacked, wavelet sharpened): the brightness of the brightest 1% of the ring from 1.02 to 1.15 radii, as a share of the disc's, and the median of that ring (the glow):

CaptureBrightest 1% beforeafterRing median beforeafter
15_32_090.330.690.0870.000
15_37_190.250.710.0200.000

The share of the ring above 5% of the disc falls from 97% to 10% for the first (the glow was above 5% nearly all round; what is left is the prominences) and stays 4% for the second (a clean sky), and the disc inside 0.9 of the radius is bit-identical. The brightness of the field beyond 1.35 radii varies by 1.4% of the disc before and 0.001% after for the first (it was glow and vignetting; it is the flat black level now), and by 9.8% before and 1.0% after for the second (that capture has a shifted band across the frame that the layer cannot remove completely). In the pictures the prominences on the left limb and the large one at the bottom of the first capture, which the plain stack shows as a smudge in the glow, stand out with their shape.

Limits

  • One pass of one exposure. The faintest prominences are limited by the noise of the stack; a second, longer exposure for the prominences is not combined (yet), so a capture whose prominences are lost in the noise stays lost.
  • The glow model assumes a prominence fills less than half of a 45 degree window. A prominence group wider than that raises the background under it and is dimmed.
  • The limb is a circle. A disc cut by the frame, or out of round, leaves a ring of disc just outside the fitted limb that the feather has to hide.
  • The stretch is a presentation choice, like colour: it makes prominences visible, not measurable. The FITS file, which keeps the unedited stack, is the measurement.

Reference: every option

SettingCLI (helia stack, helia edit)API (edits entry {"op": "prominence"})App slider (Adjust step)RangeDefault
Prominence layer--prominencethe entry itselfProminence layer toggleon or offoff
Strength--prominence-strengthstrengthStrength, 0.1 to 40 to 1001
Stretch (asinh k)--prominence-stretchstretchStretch, 0 to 300 to 10006
Level (share of the disc)--prominence-levellevelLevel, 0.1 to 1.2above 0 to 20.75
Feather (radii)--prominence-featherfeatherFeather, 0 to 0.060 to 0.20.012
Reach (radii)--prominence-reachreachReach, 1.1 to 21.05 to 31.4
Gate (noise sigma)--prominence-gategateGate, 0 to 60 to 203
Join offset (radii)--prominence-offsetoffsetJoin offset, -0.05 to 0.1-0.1 to 0.20
Disc gamma--prominence-disc-gammadisc_gammaDisc gamma, 0.4 to 30.2 to 51
Layer gamma--prominence-layer-gammalayer_gammaLayer gamma, 0.4 to 30.2 to 51

The tuning flags need --prominence. The slider ranges are narrower than the accepted ranges. A value outside the accepted range is 422 invalid_config in the API and an error on the command line. The edit runs after the sharpening and before levels and the curve, once per list.

Tutorial: bringing out prominences in H-alpha

A worked example on a real H-alpha capture: 15_32_09.avi (720 by 540, mono, 1000 frames, the disc 223 pixels in radius). It shows how to make the prominences above the limb readable without burning out the disc, and how to adjust the result the way a prominence processor such as IMPPG lets you. The method and measurements are in Prominences and the disc together.

1. Stack, sharpen and colour as usual

helia stack 15_32_09.avi -o ha-plain.tif --preset ha --sharpen wavelet --colour auto

The H-alpha stack coloured, without the prominence layer

The disc is fine, but the prominences on the left limb and at the bottom are a faint smudge inside a halo of scattered light.

2. Turn on the prominence layer

In the app this is the Adjust step, and the Prominence layer is the first section. Switch it on and the preview shows the composite. On the command line:

helia stack 15_32_09.avi -o ha-prom.tif --preset ha --sharpen wavelet \
    --prominence --colour auto

The same stack with the prominence layer: the glow is gone and the prominences stand out

With the defaults the brightest 1% of the ring just outside the limb goes from 0.33 to 0.69 of the disc's brightness, the glow in that ring from 0.087 to 0, and the disc inside 0.9 of the radius is bit-identical.

3. Adjust it

Work through the sliders in this order (all have the live preview; defaults in brackets):

  1. Level (0.75): how bright the brightest prominence should be next to the disc. Raise it for a bolder picture.
  2. Strength (1) and Stretch (6): how hard the faint parts are lifted. Strength scales the signal before the stretch; Stretch bends the curve (0 is linear). Faint wisps appear as Stretch rises, until the noise does.
  3. Gate (3 sigma): how much noise is cut away. Raise it if the sky speckles, lower it to let the faintest structure through.
  4. Join offset (0) and Feather (0.012 of the radius): where the disc meets the layer and how soft the join is. If a thin dark or bright ring shows at the limb, widen the feather or move the join out by a few thousandths of a radius.
  5. Layer gamma (1) and Disc gamma (1): a separate tone for the layer and for the disc, like the separate adjustment of disc and prominences in IMPPG. Raise the layer gamma to darken faint haze; raise the disc gamma to bring out filaments on a bright disc. With both at 1 nothing but the layer changes.
  6. Reach (1.4 radii): how far out the layer goes before it fades to black. Lower it when the frame is cut off close to the limb.

Levels and the tone curve below the layer apply to the whole picture afterwards.

4. Export a colour table

To colour the same stack in another program: Colour step, Colour table, or helia lut ha -o ha.cube.

What to expect and what can go wrong

What you seeWhyWhat to do
Dark arcs next to the limbThe glow is much stronger on one sideRaise Gate a little; widen Feather
Speckle all round the discStretch too high for the noiseLower Stretch or Strength, raise Gate
No prominences appearThe capture has none, or they are below the noiseCheck the sharpened stack; stack more frames
The layer looks flat near a frame edgeThe ring is cut by the frameLower Reach
The picture is unchangedThe limb fit failed (no clear disc)Frame the whole disc

On the second capture of that day (15_37_19.avi) the same settings take the brightest 1% of the ring from 0.25 to 0.71 of the disc.

The command line

helia has these subcommands:

CommandWhat it does
helia stackStack a capture into one image.
helia infoPrint what a capture file contains, without stacking.
helia measureMeasure the granulation contrast of a finished image.
helia presetsList the filter presets and what each does.
helia serveRun the HTTP API.
helia feedbackSend a bug report, feature request or question, to the Helia issue tracker. See Sending feedback.

Every option is listed in the CLI reference, which is generated from the program itself. This page explains how to use them. helia --help and helia <command> --help print the same information.

helia info

helia info capture.ser

The output has up to four parts. This is Sun_Test.ser, an ASI482MC colour camera behind an H-alpha filter, recorded by SharpCap as a file that says it is mono:

file:               Sun_Test.ser
format:             SER

Header, as written in the file
  FileID:            LUCAM-RECORDER
  LuID:              0
  ColorID:           0 (MONO)
  LittleEndian:      0 (big-endian)
  Width:             1920 px
  Height:            1080 px
  PixelDepth:        8 bit
  FrameCount:        631
  Observer:          (empty)
  Instrument:        (empty)
  Telescope:         (empty)
  DateTime (local):  not set
  DateTimeUTC:       not set
  Timestamps:        absent

Helia will use
  Bayer pattern:     RGGB           (detection)
  Bit depth:         8 bit          (header)
  Byte order:        not used (8-bit) (header)
  Row order:         top to bottom  (header)

Mosaic check (2x2 pixel positions, share of full scale)
  means:             79.6%  77.6%  77.4%  0.3%
  spread:            135.0% over 5 frame(s)

warning:  The SER header says mono (ColorID 0), but the frames hold a Bayer mosaic ...
  1. Header, as written in the file. Every field of a SER header, decoded: FileID, LuID, ColorID with its meaning, the LittleEndian flag, size, pixel depth, frame count, observer, instrument, telescope, DateTime and DateTimeUTC as dates (not set when zero), and whether per-frame timestamps are present, with the first and the last. For an AVI it prints the codec, pixel format, size, row order and frame count, then what the SharpCap CameraSettings.txt next to it says (its path, the camera, the colour space and the Debayer Preview).
  2. Helia will use. The Bayer pattern, bit depth, byte order and row order the capture will be decoded with, each with where it came from: header, sidecar, detection (measured from the pixels) or override (an option you gave).
  3. Mosaic check. Only for a SER file that says mono: the mean level of the four pixel positions of a 2 x 2 cell on the solar disc and their spread (see Troubleshooting).
  4. Notices. warning: when the file disagrees with itself or with you and Helia has chosen, note: for anything else worth knowing. helia stack prints the same notices.

helia info accepts the same --bayer, --bit-depth, --endian and --flip-rows as helia stack, to show what an override would change before you stack.

For an MP4, MOV, M4V, MKV or WebM file, or an AVI with a compressed codec, it asks ffprobe (part of the optional ffmpeg) and prints the container, codec and profile, the frame size (after any rotation the file asks for), the pixel format and bit depth, the frame count, frame rate and duration, and, for lossy video, a note: line such as "lossy 8-bit 4:2:0 video (h264): fine for quick results, SER/AVI raw captures keep more detail". Nothing is decoded, so it is quick even for a large file.

Run it first on a new kind of capture. Most problems with colour data show up here. When the byte order under Helia will use differs from the LittleEndian flag under the header, Helia has corrected a mislabelled file (see Troubleshooting).

helia measure

helia measure sun.tif

Prints the band-pass contrast (standard deviation over mean) of a 240 pixel crop at the centre of the disc, in three bands: fine (0 to 1 px), granulation (1 to 4 px) and meso (4 to 12 px). It reads a TIFF, PNG or FITS file; a colour image is averaged to grey. --crop PX changes the crop. Use it to compare settings on one capture; see White-light granulation.

helia measure sun.fits --limb fits the three limb-darkening laws (linear, quadratic, Neckel and Labs) to the disc instead, and prints the coefficients, the limb level and the measured profile beside each law. See Limb darkening. It then prints the granulation report of the granulation and seeing report: RMS contrast, cell size and the power spectrum's roll-off. --scale ARCSEC gives sizes in arcseconds; --aperture MM (with --wavelength NM and --scale) adds the diffraction cut-off. helia stack prints the same report for a white-light or sodium stack, and --aperture-mm MM adds the cut-off.

helia calibrate

Builds master calibration files from calibration captures, for helia stack --dark --flat --bad-pixels. Each input is a SER, AVI or video file, a FITS still, or a folder of them. See Calibration for what to capture and why.

helia calibrate --darks darks.ser --flats flats.ser --flat-darks flat-darks.ser \
                --preset ha -o masters/
# wrote:    masters/master-dark.fits  (master dark of 120 frame(s), sigma-clip 3, ...)
# wrote:    masters/master-flat.fits  (master flat of 40 frame(s), ...)
# wrote:    masters/bad-pixels.fits  (37 pixel(s) (31 hot, 6 cold), from the dark and flat)
# next:     helia stack <capture> -o out.tif --dark masters/master-dark.fits ...
  • --darks, --flats, --flat-darks, --bias: the inputs (each takes several paths). --flat-darks are darks at the exposure of the flats; without them, or --bias, the flat keeps its offset and the command warns.
  • --output-dir DIR (-o): where the files go, calibration by default: master-dark.fits, master-flat.fits, master-bias.fits, master-flat-dark.fits and bad-pixels.fits, whichever were asked for.
  • --combine median|sigma|sigma:K: how frames are combined, per pixel. The default sigma rejects values more than 3 standard deviations out and averages the rest. --max-frames N limits the frames used; by default as many as fit in 3 GiB.
  • --lights CAPTURE: also look for stuck pixels in a light capture: pixels that read the same wrong value in every frame while the Sun moves. Useful when there are no darks. --no-bad-pixels skips the search, --hot-sigma K changes how far from its neighbours a hot pixel of the dark must be (default 6).
  • --displaced PATH...: captures of the Sun at many places on the sensor (the mount off tracking, or nudged), to solve the flat from instead of --flats (they conflict). Needs 6 frames or more and 30 pixels or more between the furthest; uses up to 60 spread over the input (--max-frames). The darks are taken off. By default only the structure finer than a quarter of the displacements is kept (dust, fringes, the column pattern); --displaced-keep-smooth keeps every scale. The selfcal: line gives the misfit, how much of the sensor the Sun covered and the scale above which the gain is not recovered well. See A flat from the Sun itself.
  • --preset ha: adds the advice for that filter to the warnings.
  • The same --bayer, --bit-depth, --endian and --flip-rows as helia stack.

The flat is checked as it is made: the command prints how far the edge of the field falls below the centre, the size of any rings and how much of the field dust shadows, and warns about strong vignetting (an etalon's sweet spot), rings (Newton rings and etalon fringes) and dust. It exits with 0 when it wrote the files, whatever it warned about.

helia stack

The minimum is an input and an output:

helia stack capture.ser -o stack.tif

The output extension chooses the format: .tif or .tiff for a 16-bit TIFF, .png for an 8-bit PNG, .fits or .fit for a 32-bit floating-point FITS. Anything else is refused before any work starts. An existing output file is overwritten without asking. See Output formats.

What Helia decides for you

With nothing but an input and an output, helia stack already does the right thing for most captures. It reads the filter from the file name, the folder, the SharpCap sidecar or the SER header (see Colourising) and prints what it chose on the settings: line:

  • the colour channel of a raw colour camera (red for H-alpha, blue for calcium, green for magnesium, all three for sodium, helium and white light);
  • the share of frames to keep (25 per cent, or 30 for the faint lines);
  • the alignment point size, about a sixteenth of the short side of the image, in steps of 8 from 32 to 96 pixels.

Any option you give wins over the recommendation. If the filter is not recognised, the defaults are all channels combined and 25 per cent.

--preset ha|cak|cah|mg|na|he-d3|hbeta|white names the filter yourself: it applies the same recommendations and also colours a .tif or .png output. helia presets lists them. The colour options are in Colourising.

Choosing the frames

  • --keep 25% (the default) keeps a share of the frames, ranked by quality. A bare number such as --keep 500 is a count.
  • --keep-frames 500 is the same as a count and cannot be combined with --keep.
  • --metric gradient (the default) or --metric laplacian chooses how sharpness is measured. Both ignore overall brightness.

Fewer frames give a sharper but noisier stack. Start at 25 per cent and look at the Quality, sorted curve in the desktop app to see where your capture drops off.

Alignment

  • --ap-size 64 is the size, in pixels, of each alignment point. The default is chosen from the image size (see above). Smaller points follow finer detail but need more contrast to lock on; the smallest allowed size is 8 (the API also caps it at 1024).
  • --ap-auto chooses the alignment point size, spacing and search range instead of a fixed size (it excludes --ap-size). Helia fits the solar limb of the reference for the image scale, then registers a dozen of the frames it is about to stack and measures how far each point strays from its frame's median shift (the local warp of the seeing). The window follows the detail in arcseconds (2.5 granules for white light and sodium, 90 arcsec otherwise), clamped to 32 to 96 pixels, with the usual half-window spacing; the search radius covers three times the warp (4 to 12 pixels). With no limb the size is the one recommended for the frame. An auto-ap: line says what was chosen: alignment points 32 px every 16 px, search 5 px (2.16 arcsec per pixel; local warp 0.88 px (1.89 arcsec)). Off by default, so existing runs do not change.
  • --ap-passes 2 stacks once and measures the alignment points again against that stack, which has less noise than the reference; the default 1 measures them once. --ap-smooth on replaces shifts that disagree with their neighbours by the neighbours' median. --ap-residual measures the points a last time against the finished stack and prints how far the shifts used are from that (refine: line). All need multipoint and are off by default; see Refining the shifts.
  • --seeing estimates the seeing from the capture after the stack: the Fried parameter r0 from the spectral ratio of tiles of the disc (it needs --aperture-mm, and --wavelength-nm when the filter is not known) and a relative motion index from how much neighbouring alignment points move against each other, over 120 frames spread across the capture. See Seeing estimation.
  • --sunspots finds the sunspots on the stack (white light, whole disc): groups, the Wolf number, areas corrected for foreshortening and positions. It prints the report after the stack, writes NAME-sunspots.csv, .json and .png beside the output and adds SPOT* cards to a FITS output. --sunspot-umbra, --sunspot-penumbra, --sunspot-min-area, --sunspot-max-area, --sunspot-group-gap and --wolf-k tune it; --north-angle, --celestial-north-up and --mirror say how the image sits on the Sun. The same works on any stacked image with helia sunspots. See Sunspots.
  • --align auto|phase|limb chooses how whole frames are aligned. auto, the default, fits the solar limb when the whole disc is in the frame and the fit is confident, and uses phase correlation otherwise (a cropped region, a disc cut by the frame, no disc). phase and limb force one; with limb, a frame whose limb cannot be fitted still falls back to phase correlation, and a capture with no disc at all is an error. The end of a run prints an align: line with the choice, the reason and the confidence. See How stacking works.
  • --no-multipoint aligns whole frames only. It is faster, and a useful check: if the multi-point result looks worse than the global one, the points are locking on the wrong thing.

Prominences

--prominence (with helia stack and helia edit) composites a stretched off-limb layer with the disc after the sharpening: the glow outside the limb is taken off, what is left is stretched and joined to the disc through a feathered edge from the limb fit. --prominence-strength, --prominence-stretch, --prominence-level, --prominence-feather, --prominence-reach and --prominence-gate tune it. See Prominences and the disc together.

Colour tables

helia lut <preset> -o FILE writes a filter's colour look as a .cube, .csv, ImageJ .lut or PNG strip, for other programs; --format, --size, --invert and --strength choose how. See Colourising.

Limb darkening

Every run fits the limb darkening of the stack and prints it, for example limb: quadratic: u1 = 0.470, u2 = 0.046; limb at 48% of the centre; rings differ from the law by 0.67% rms. --limb-law linear|quadratic|neckel chooses the law (default quadratic). --flatten model|profile also divides the disc by the fitted law or by the measured profile, before the sharpening, in every output but FITS (add --sharpen-fits). helia edit takes the same flags. See Limb darkening.

Cropping to the disc

  • --autocrop cuts the result to a square around the fitted solar disc, in every output format (a FITS file records the step and keeps the limb in its own pixels). --autocrop-margin 10 is the room left beyond the limb as a percentage of the radius (default 10, 0 to 200); prominences seldom reach 5%. A disc near the frame edge moves the square inwards; a disc larger than the frame keeps the whole of that side. With no clear disc (a cropped region, a prominence frame) the image is left whole and a crop: line says why.

Keeping alignment off text and sky

Helia finds a burned-in timestamp by itself, and places alignment points only where the centre of the point is inside the fitted limb of the sun. These options adjust that; the background is in How stacking works.

  • --exclude x,y,w,h keeps alignment and scoring off a rectangle, in pixels from the top-left corner. Repeat it for several. The area is flat in the result. Use it for anything the automatic detection does not cover.
  • --prominence-band 20 allows points up to 20 pixels outside the limb, so that they can follow prominences. The default is 0.
  • --no-disc-mask and --no-overlay-detect switch the limb fit and the text detection off. The summary prints masked: and disc: lines for what was found.

Calibration

  • --dark PATH subtracts a master dark: a FITS file from helia calibrate (or any FITS or TIFF), or dark captures to build one from on the spot (a SER, AVI or FITS file, or a folder).
  • --flat PATH divides by a master flat, or builds one from flat captures. With captures, --flat-dark PATH gives the darks at the flats' exposure. --displaced PATH solves the flat from captures of the Sun at many places on the sensor instead (it conflicts with --flat; up to 60 frames are used).
  • --bad-pixels FILE replaces the pixels of a map (bad-pixels.fits, or a text file with an x y pair per line, from 0 at the top left) with the median of their neighbours. Without it, when there is a dark or a flat, the hot pixels of the dark and the dead ones of the flat are found and replaced; --no-cosmetic stops that.
  • --scale-dark scales the dark by the ratio of the exposures (the Sun's comes from the SharpCap file beside the capture). --bias PATH gives it a master bias to scale around. --calibration-combine is --combine of helia calibrate, for masters built on the spot.

The masters are checked against the capture before any work starts: a master from another camera or region (a different size or mosaic) is refused with a message. The calibration is applied to every frame as it is read, before it is demosaiced, scored or aligned, so the whole run sees calibrated frames. The calibrate: lines at the start of the run name each master, and the output records them (PRSTEP cards and HISTORY in FITS, the description in TIFF and PNG). See Calibration.

Stacking

  • The default is a quality-weighted mean.
  • --robust sigma|huber|winsor guards against outlier samples (hot pixels, satellite and plane trails, a cloud dimming a few frames): sigma drops samples further than K standard deviations from the mean, huber gives them less weight the further they are, winsor clamps them to K standard deviations. --robust-k K sets K (defaults 2.5, 1.345 and 2). All three read the capture twice and are off by default (--robust none). The measured behaviour is in How stacking works.
  • --sigma-clip 2.5 is the older spelling of --robust sigma --robust-k 2.5.

Video files

helia stack takes an MP4, MOV, M4V, MKV or WebM file, or a compressed AVI, in place of a SER file, if the optional ffmpeg is installed:

helia stack sun.mp4 -o stack.tif

The video is decoded once into an uncompressed SER file in a cache directory, and the stack runs from that, so it is as fast as a SER capture after the decode. The file is deleted when the run ends. --cache-dir DIR (or the HELIA_CACHE_DIR environment variable) picks the directory; the default is the operating system's cache folder. Helia checks there is room first: a 4K clip of 7,500 frames needs about 60 GB. --channel r|g|b makes ffmpeg extract that colour plane while decoding (lum, the default, is the luminance). The summary gains a note: line when the video is lossy. HELIA_FFMPEG and HELIA_FFPROBE name the programs if they are not on your PATH.

Colour cameras

  • --bayer auto|none|rggb|bggr|grbg|gbrg says how to treat raw colour data. auto (the default) trusts the file's header, or the SharpCap sidecar for an AVI; but for a SER file that says it is mono it also checks the pixels for a hidden Bayer mosaic and debayers when it finds one, saying so. none forces mono whatever the header says; a pattern name forces that pattern. Both skip the check of the pixels. For rggb and the others, name the pattern of the picture as it will be stacked (after --flip-rows, if you give it).

  • --colour true stacks the red, green and blue planes of a raw colour capture on one alignment and writes a colour picture (RGB TIFF or PNG, or a three-plane FITS cube): true colour for white light. --white-balance auto|off|R,G,B (default auto, the disc centre made neutral) and --no-channel-align (do not register red and blue to green) belong to it, and --channel, --invert and --colour-strength do not apply. It is refused for a capture that is not a Bayer mosaic. See True colour.

Reading a file whose header is wrong

For a SER file, to say what the header gets wrong (see Capturing for Helia):

  • --bit-depth N uses N bits per sample instead of the header's. 1 to 8 read one byte per pixel, 9 to 16 two; samples are scaled to the range of their container, as for the header's own depth.
  • --endian little|big sets the byte order of 16-bit samples instead of the header flag and Helia's check of it.
  • --flip-rows reads the rows bottom to top.

An AVI describes its own samples, so these three are refused for an AVI. Whatever you give is printed as a note: and recorded in the output file, as the Overrides: line (see What the files say).

  • --debayer mhc (the default) or bilinear chooses the demosaicing method.
  • --channel lum|r|g|b chooses which plane is analysed and stacked. The default is the preset's channel (see above), or lum, the mean of the three, when there is no preset. For H-alpha that is r, as explained in Troubleshooting.

Backend

--backend auto|cpu|gpu; see GPU acceleration.

Colour

  • --preset <filter> colours the output (see above).
  • --colour auto|mono|<filter> chooses only the colour: auto is the preset you gave or the filter Helia detected, mono is grey.
  • --invert makes a negative before colouring; --colour-strength 0.6 mixes 60 per cent colour with grey.

Colour goes to .tif (16-bit RGB) and .png (8-bit RGB). A .fits output is never coloured. Every file notes the look used.

A .fits output carries the SOLARNET observation keywords: capture times, exposure, telescope and observer from the SER header, the filter and its wavelength, the processing steps, and a helioprojective coordinate system from the limb fit. --site LAT,LON,ALT (or the HELIA_SITE environment variable) adds the observing site as OBSGEO-X/Y/Z. Every keyword is listed in Output formats.

Sharpening and adjustments

  • --sharpen wavelet|unsharp|rl|none sharpens the stack with the recommended values of the filter. The default is none. It runs on the mono stack, before any colour.
  • --wavelet-gains, --wavelet-noise (lists, finest layer first), --unsharp-radius, --unsharp-amount, --unsharp-threshold, --rl-iterations, --rl-tv, --rl-psf (auto, gaussian:SIGMA, airy:RADIUS or airy-scope:NM,MM,ARCSEC_PER_PX) and --rl-guard override individual values; each needs its method.
  • --denoise [STRENGTH], --denoise-layers, --black, --white, --gamma, --curve IN:OUT,..., --crop X,Y,W,H, --rotate DEG, --flip-h, --flip-v and --resize 50% (or 1200x800) are the everyday adjustments.
  • --edits NAME|FILE starts from a saved preset; --save-edits NAME keeps the list as one.
  • --sharpen-fits applies the edits to a FITS output too. By default a FITS file is the unsharpened stack.

The edits run in this order: those of --edits, denoise, the sharpening method, levels, the curve, crop, rotate, flip, resize. The summary says how much finer detail they gave the stack and what they did to the noise. See Sharpening and adjustments for what each does and the risks.

Output and exit status

The result goes to the file named by -o (long form --output); the extension chooses the format: .tif or .tiff, .png, or .fits or .fts.

While it works, helia stack prints the settings it chose, a line as each stage starts with the time since the start ([ 0.3s] stacking), a progress bar and a summary. All of it goes to standard error, so the output file is the only thing on disk and you can redirect the messages. -q or --quiet hides all of it. The exit status is 0 on success; on any error helia prints helia: <message> on standard error and exits with status 1.

helia edit

Sharpen and adjust an image that already exists: a TIFF, PNG or FITS, mono or colour.

helia edit stack.fits -o sharp.tif --sharpen wavelet --preset ha
helia edit sun.tif -o small.png --crop 200,150,1200,900 --resize 50%
helia edit sun.tif -o sun2.tif --edits "H-alpha gentle" --gamma 1.1

It takes the same sharpening and adjustment options as helia stack, plus --preset <filter> for the recommended values (the filter is otherwise read from the file name or folder). A colour image is edited channel by channel and stays colour. The output records what was done, with where it came from. With nothing to do it says so and exits with status 1.

helia serve

Starts the API server. The options are in the CLI reference and the behaviour is in The API. The first line it prints is listening on http://ADDRESS, which scripts can read to learn the port when they ask for port 0.

helia feedback

Files a bug, a feature request or a question as an issue on the project's forge, through the feedback service, for people who work without the desktop app:

helia feedback --kind bug --title "Stack fails on my AVI" --description "..." \
    --screenshot problem.png --system-info

--dry-run prints what would be sent and sends nothing; if the service cannot be reached, --save report.zip keeps the report and the command prints the pre-filled new-issue address. Your report is filed in the Helia issue tracker, and Helia sends nothing you did not ask it to. Sending feedback explains the options and the privacy rules.

Batch processing

Stack every SER in a folder, with a different output name for each. In a POSIX shell:

for f in captures/*.ser; do
  helia stack "$f" -o "stacks/$(basename "$f" .ser).tif" --quiet || echo "failed: $f"
done

In PowerShell:

Get-ChildItem captures\*.ser | ForEach-Object {
  helia stack $_.FullName -o "stacks\$($_.BaseName).tif" --quiet
  if ($LASTEXITCODE -ne 0) { Write-Warning "failed: $($_.Name)" }
}

Both stop a failing capture from hiding the others. For more control, queue jobs through the API from a script.

The API

helia serve exposes the stacker as a JSON API, so scripts, notebooks and other programs can drive Helia without touching the command line or the GUI. It is the same pipeline as helia stack, with the same options.

The server is the helia-server crate. The CLI embeds it, and so can the GUI or your own Rust program (Server::builder()).

This page is the guide: how to run the server, how to call it, and the security model. The complete list of operations, parameters and types is the API reference, generated from the server's own OpenAPI document, so it always matches the code. A running server also serves that document at /api/v1/openapi.json and a small browsable page at /api/v1/docs.

Running it

helia serve --data-root ~/captures
# listening on http://127.0.0.1:7870
OptionDefaultMeaning
--bind ADDR127.0.0.1:7870Listen address. A non-loopback address is refused unless a token is set.
--data-root DIRcurrent directoryEvery path a client sends must stay inside this directory.
--token TnoneBearer token. Visible in the process list; prefer the next two.
--token-file FILEnoneRead the token from a file (whitespace trimmed).
HELIA_TOKENnoneEnvironment variable, used when neither option is given.
--workers N1Jobs that run at once. The GPU is one device, so leave this at 1 unless you stack on the CPU.
--max-queue N16Queued plus running jobs allowed; more get 429.
--cors-origin ORIGINnoneAllow a browser page on this origin (repeatable).
--allow-host HOSTnoneAccept this Host header on a loopback bind, for a reverse proxy (repeatable).

Port 0 picks a free port; the first line printed is listening on http://127.0.0.1:PORT. Stop with Ctrl-C: running jobs are cancelled and open event streams are closed.

All routes are under /api/v1. Errors are always

{"error": {"code": "path_outside_root", "message": "path resolves outside the data root"}}

where code is stable and safe to match on. Request bodies are JSON (Content-Type: application/json) and limited to 64 KiB.

Paths

Paths in requests are relative to the data root (absolute paths are accepted only if they point inside it). Responses show paths relative to the root with forward slashes. Outputs are written inside the root too; missing parent directories are created.

Authentication

With a token set, every request except GET /api/v1/health and the static GET /api/v1/docs page needs

Authorization: Bearer <token>

and gets 401 unauthorized otherwise. The WebSocket endpoint takes the same header; browsers cannot set it on a WebSocket, so use a non-browser client or a proxy that adds it.

export H='Authorization: Bearer my-token'

Endpoints

GET /health

curl http://127.0.0.1:7870/api/v1/health
# {"status":"ok"}

GET /version

Server version, how it was built (commit, date, profile, target, Rust and wgpu versions), copyright and licence, backend availability (the GPU is probed once, on first use), limits and whether a token is required.

curl -H "$H" http://127.0.0.1:7870/api/v1/version
{"name":"helia-server","version":"0.5.0","api":"v1",
 "build":{"commit":"1a2b3c4d","dirty":false,"built":"2026-10-19 09:30 UTC","profile":"release",
          "target":"x86_64-unknown-linux-gnu","rust":"rustc 1.95.0 (...)","wgpu":"30.0.1"},
 "licence":{"copyright":"Copyright (C) 2026 Laurence Horrocks-Barlow",
            "licence":"GPL-3.0-or-later","source":"https://example.org/helia-source"},
 "backends":{"cpu":{"available":true},
             "gpu":{"available":true,"adapter":"NVIDIA ... (Vulkan, DiscreteGpu)","software":false}},
 "limits":{"workers":1,"max_queue":16,"max_body_bytes":65536,"active_jobs":0},
 "auth_required":true}

GET /presets

The filter presets: for each one its id, name, wavelength, the Bayer channel and share of frames it recommends, why, and the colour parameters. Colour is presentation, not measurement; see Colourising.

curl -H "$H" http://127.0.0.1:7870/api/v1/presets
{"presets":[{"id":"ha","name":"H-alpha","wavelength":"656.3 nm","hue":"orange-red",
  "summary":"...","reason":"...","channel":"r","keep_percent":25.0,
  "colour":{"gamma":0.85,"gains":[1.0,0.85,0.6],"exponents":[0.7,1.5,2.6],
            "mid_grey_rgb":[169,90,33]}}, "..."]}

The ids are ha, cak, cah, mg, na, he-d3, hbeta and white.

GET /presets/{id}/lut

A filter's colour table as a file for other programs: format (cube, the default, csv, imagej or png), size (2 to 4096, default 256), invert and strength. 404 for an unknown preset, 422 invalid_config for a bad value. See Colourising.

curl -H "$H" -o ha.cube "http://127.0.0.1:7870/api/v1/presets/ha/lut?format=cube"

POST /captures/inspect

Header information for a .ser, .avi or video (.mp4, .mov, .mkv, .webm, .m4v) capture: the header as written, what Helia will use and where each value came from, and what to tell the user. Video needs the optional ffmpeg on the server; only ffprobe runs for an inspect, so it is quick. For a SER file that says it is mono, a few frames are read to check the pixels for a Bayer mosaic. The body may also carry the overrides bayer, bit_depth, endian and flip_rows (as in a job's config, below) to see what they would change.

curl -H "$H" -H 'Content-Type: application/json' \
  -d '{"path":"2026-10-01/sun.ser"}' http://127.0.0.1:7870/api/v1/captures/inspect
{"path":"2026-10-01/sun.ser","format":"ser","width":1936,"height":1096,
 "bit_depth":16,"colour":"mono","bayer":null,"frames":4000,
 "observer":null,"instrument":"ZWO ASI174MM","telescope":null,"timestamps":true,
 "fourcc":null,"sidecar":null,
 "recommended":{"filter":"ha","filter_source":"2026-10-01_Ha","channel":"lum",
                "keep_percent":25.0,"ap_size":72},
 "header":{"file_id":"LUCAM-RECORDER","lu_id":0,"colour_id":0,
           "colour_meaning":"MONO","little_endian":true,"width":1936,
           "height":1096,"pixel_depth":16,"frame_count":4000,"observer":"",
           "instrument":"ZWO ASI174MM","telescope":"","date_time":null,
           "date_time_utc":"2026-10-01 09:12:03.250",
           "timestamps":{"count":4000,"first":"2026-10-01 09:12:03.250",
                         "last":"2026-10-01 09:12:35.240"}},
 "effective":{"bayer":{"value":"none","source":"header"},
              "bit_depth":{"value":16,"source":"header"},
              "byte_order":{"value":"little","source":"header"},
              "flip_rows":{"value":false,"source":"header"}},
 "detection":{"frames_used":5,"region":[724,420,488,256],
              "position_means":[0.51,0.51,0.51,0.51],"spread":0.003,
              "verdict":"mono","pattern":null,"red_blue_basis":null,"reason":null},
 "overrides":[],"notices":[]}

colour is mono, bayer, rgb, bgr or video, and bayer and bit_depth are the values Helia will decode with, so for a SER file that says mono but holds a mosaic colour is bayer and bayer is the detected pattern. The new fields say how Helia got there:

  • header: a SER header exactly as written, decoded (null for an AVI). colour_meaning is the meaning of colour_id (MONO, BAYER_RGGB, ...), date_time and date_time_utc are decoded dates or null when unset, and timestamps gives the count and the first and last of the per-frame timestamps (UTC), or null when the file has none.
  • effective: the bayer pattern (or "none"), bit_depth, byte_order (little or big) and flip_rows in force, each with a source of header, sidecar, detection or override.
  • detection: the mosaic check, present when it ran (a SER file that says mono, with bayer not overridden): the means of the four pixel positions of a 2 x 2 cell, their spread, and a verdict of mosaic, mono or inconclusive (with the pattern and the red_blue_basis for a mosaic, or the reason).
  • overrides: what the request overrode, as command-line flags.
  • notices: what to tell the user, each with a level (info or warning) and its text. A SER file that says mono but holds a mosaic gives a warning that names the pattern and the measurement.

For a video, format is its extension (mp4, mov, mkv, webm, m4v, or avi for a compressed AVI), codec and frame_rate are set, width and height are after any rotation, and quality_note carries the lossy-input caveat when there is one, for example "lossy 8-bit 4:2:0 video (h264): fine for quick results, SER/AVI raw captures keep more detail" (it is null for everything else). For AVI files, sidecar carries what a SharpCap CameraSettings.txt next to the capture says (colour space, Bayer hint, frame count, camera section names) and fourcc the codec tag.

recommended is what a job would use if given no settings: the filter Helia detected (filter, an id from GET /presets, or null) and the text it found it in (filter_source), the Bayer channel it would stack (lum for a capture that is not raw colour), the share of frames to keep and the alignment point size.

Videos

A job whose input is a video decodes it first (the stage is reported as quality, at 0 per cent, until the stacking passes begin), into the directory given by helia serve --cache-dir (or HELIA_CACHE_DIR), and removes the decoded file when the job ends. Cancelling the job kills ffmpeg. A frame preview of a video decodes only up to that frame. The job's channel setting chooses the colour plane ffmpeg extracts. Video errors have their own codes: ffmpeg_unavailable (503, not installed or too old), truncated_video (422), insufficient_storage (507, not enough room to decode); other unreadable files are invalid_capture (422). The path sandbox applies as for any file, and no text from a request is ever passed to ffmpeg as an option.

GET /captures/frame

An 8-bit greyscale PNG of one frame, auto-stretched (0.1 to 99.9 percentiles).

QueryDefault
pathrequiredcapture
index0frame number
channellumr, g, b or lum, for Bayer captures and video
max_widthnonedownscale to at most this width, 16 to 8192
bayer, bit_depth, endian, flip_rowsas for a jobhow to read the capture, as in a job's config
curl -H "$H" -o frame.png \
  'http://127.0.0.1:7870/api/v1/captures/frame?path=sun.ser&index=120&max_width=800'

POST /jobs

Queue a stack. Returns 202 with the job.

curl -H "$H" -H 'Content-Type: application/json' -d '{
  "input": "sun.ser",
  "output": "stacks/sun.tif",
  "config": {"preset": "ha", "backend": "auto"}
}' http://127.0.0.1:7870/api/v1/jobs

config takes the same knobs as helia stack; every field is optional:

FieldDefaultSame as
keep"25%"--keep ("25%" or a count such as "500")
keep_framesnone--keep-frames (excludes keep)
ap_sizefrom the image size--ap-size (8 to 1024)
ap_autofalse--ap-auto; excludes ap_size, needs multipoint; what it chose is in summary.auto_ap
ap_passes1--ap-passes (1 or 2; needs multipoint)
ap_smoothfalse--ap-smooth on; needs multipoint
aperture_mmnone--aperture-mm; adds the diffraction cut-off to summary.granulation
sunspotsnone--sunspots; an object (empty for the defaults) with umbra, penumbra, min_area, max_area, group_gap_deg, wolf_k, north_angle_deg, celestial_north_up, mirror; the result is in summary.sunspots, GET /jobs/{id}/sunspots.csv and sunspots.png, and the SPOT* FITS cards; see Sunspots
seeingfalse--seeing; the estimate is in summary.seeing, r0 needs aperture_mm
autocropfalse--autocrop; the region kept is in summary.crop
autocrop_margin10--autocrop-margin (per cent of the radius, 0 to 200; needs autocrop)
colournone--colour (auto, mono, a preset id or true); true is true colour, see below
white_balance"auto"--white-balance (auto, off, "R,G,B"); only with colour: "true"
align_channelstruefalse is --no-channel-align; only with colour: "true"
limb_law"quadratic"--limb-law (linear, quadratic, neckel); the fit is in summary.limb_darkening
flattenoff--flatten (model, profile); runs before sharpen and edits
multipointtruefalse is --no-multipoint
backend"auto"--backend (auto, cpu, gpu)
sigma_clipoff--sigma-clip
robust"none"--robust (none, sigma, huber, winsor; excludes sigma_clip)
robust_k2.5, 1.345 or 2 by kind--robust-k (needs robust)
bayer"auto"--bayer (auto, none, rggb, grbg, gbrg, bggr)
bit_depththe SER header's--bit-depth (1 to 16; SER only)
endianthe header flag, checked--endian (little, big; SER only)
flip_rowsfalse--flip-rows (SER only)
channelthe preset's, else "lum"--channel (r, g, b, lum)
debayer"mhc"--debayer (bilinear, mhc)
metric"gradient"--metric (gradient, laplacian)
exclude[]--exclude, as a list of {"x":0,"y":0,"w":200,"h":20} (pixels from the top-left corner; w and h at least 1)
prominence_band0--prominence-band (pixels outside the limb where points are still allowed)
disc_masktruefalse is --no-disc-mask
detect_overlaytruefalse is --no-overlay-detect
presetnone--preset (an id from GET /presets)
colournone--colour ("auto", "mono" or an id)
invertfalse--invert
colour_strength1.0--colour-strength (0 to 1)
sharpennone--sharpen (wavelet, unsharp, rl, none), with the filter's recommended values
edits[]further operations, applied in order after sharpen (see below)
sharpen_fitsfalse--sharpen-fits: apply sharpen and edits to FITS output too
calibrationnone--dark, --flat, --bad-pixels and friends, as an object (see below)

Unknown fields are rejected. The output extension picks the format (.tif, .tiff, .png, .fits, .fit, .fts). An existing output is refused (409 output_exists) unless the request sets "overwrite": true; two active jobs cannot share an output (409 output_in_use). The input is opened before queueing, so an unreadable capture fails immediately with 422.

Calibration

config.calibration applies dark, flat and bad-pixel calibration to every frame before it is demosaiced, scored or aligned. Every path is relative to the data root and names a master file (FITS from helia calibrate) or raw calibration frames to build one from (a SER, AVI or FITS file, or a folder of them); at least one of dark, flat and bad_pixels is needed.

curl -H "$H" -H 'Content-Type: application/json' -d '{
  "input": "sun.ser", "output": "stacks/sun.fits",
  "config": {"calibration": {"dark": "cal/master-dark.fits",
                             "flat": "cal/master-flat.fits"}}
}' http://127.0.0.1:7870/api/v1/jobs
FieldDefaultSame as
darknone--dark
flatnone--flat
displacednone--displaced: captures of the Sun at many places on the sensor, to solve the flat from; excludes flat
flat_darknone--flat-dark: darks at the exposure of the flats, when flat is captures
biasnone--bias
bad_pixelsnone--bad-pixels (FITS map or x y text list)
cosmetictruefalse is --no-cosmetic
scale_darkfalse--scale-dark
combine"sigma"--calibration-combine (median, sigma, sigma:K)

A missing path is 404 not_found, a bad combine 422 invalid_config. A master of the wrong size or mosaic fails the job with invalid_calibration or stack_failed and a message that says why. The job's summary.calibration lists the masters applied, one line each, and any warning about them (a flat with rings, a dark that could not be scaled); it is empty for a job without calibration. The result's header or description records them too.

Colour and presets

Settings left out of config come from the filter and the image: the filter is preset if given, otherwise detected from the file name, folder, sidecar or header (the same rules as the command line; see Colourising). It supplies the Bayer channel and the share of frames to keep, and the image size supplies ap_size; a value you give always wins. POST /captures/inspect shows what would be chosen.

A job colours its output when preset or colour says so. preset colours a .tif (16-bit RGB) or .png (8-bit RGB) output with that filter's look. colour chooses only the look: "auto" is the preset in force (a job fails with no_filter if there is none), "mono" is plain grey and an id names a look. invert and colour_strength adjust it. A .fits output stays mono and unaltered, with the look noted in its header, along with SOLARNET metadata (times, exposure, filter, processing, sky coordinates; see Output formats). site ("lat,lon,alt") adds the observing site as OBSGEO-X/Y/Z. When the job has finished, summary.bayer and summary.bayer_source say which Bayer pattern the frames were decoded with (null for mono) and where it came from, and summary.notices carries the notices of the inspection (for example a Bayer mosaic found in a file whose header says mono); how the capture was read is also written into the output file. summary.filter and summary.filter_source say which filter was in force ("chosen" when preset gave it) and summary.colour describes the look written, in the words the file carries. An unknown preset or colour is a 422 invalid_config that lists the valid ids.

align chooses how whole frames are aligned: "auto" (the default: fit the solar limb when the whole disc is in the frame and the fit is confident, else phase correlation), "phase" or "limb". A finished job's summary.alignment says what happened: requested, method ("limb" or "phase"), reason, the mean confidence and confidence_min of the frames (0 to 1; absent when not measured), fallbacks (frames that wanted the limb but used phase correlation), sampled (the confidence comes from a sample of frames, as after a GPU run) and limb, the reference's fitted [x, y, radius] in pixels. See How stacking works.

Sharpening and edits

sharpen picks a method with the recommended values of the filter in force, and edits adds operations after it: a list of objects, each with an op of unsharp, wavelet, deconvolve, denoise, prominence, levels, curve, crop, rotate, flip or resize and that operation's parameters. Anything left out takes its default. The operations are described in Sharpening and adjustments.

curl -H "$H" -H 'Content-Type: application/json' -d '{
  "input": "sun_Ha.ser",
  "output": "stacks/sun.tif",
  "config": {
    "sharpen": "wavelet",
    "edits": [
      {"op": "levels", "gamma": 1.2},
      {"op": "crop", "x": 100, "y": 80, "width": 1600, "height": 900}
    ]
  }
}' http://127.0.0.1:7870/api/v1/jobs

{"op": "prominence"} composites a stretched off-limb prominence layer with the disc (fields strength, stretch, level, feather, reach, gate; see Prominences); put it after the sharpening and before levels.

The edits run on the mono stack in the order given, before any colour. A request with an unknown operation or an out-of-range value is a 422 invalid_config; a crop that does not fit this capture fails the job (failed, code invalid_config) once the capture is open. The job reports an edit stage between finish and colourise. summary.edits lists the lines recorded in the file, summary.edited says whether they were applied to the pixels, and summary.width and summary.height are the size of the written image. FITS output is the unedited stack unless sharpen_fits is true, and its summary.edits then says so. GET /presets includes each filter's recommended sharpening under sharpen.

Jobs run on a bounded pool (--workers, default 1) in submission order. When --max-queue jobs are queued or running, new ones get 429 queue_full.

GET /jobs, GET /jobs/{id}

{"id":3,"state":"running","stage":"stack","stage_progress":0.42,"progress":0.78,
 "queue_position":null,"input":"sun.ser","output":"stacks/sun.tif",
 "config":{"keep":"25%","...":"..."},
 "created_at":1790000000000,"started_at":1790000000100,"finished_at":null,
 "run_seconds":12.3,"timings":[],"backend":"GPU (...)","summary":null,"error":null}
  • state: queued, running, succeeded, failed or cancelled.
  • stage: opening (the capture is being opened and indexed), quality, reference, global_align, local_align, stack, finish, then colourise (only when the output is coloured) and write. stage_progress is 0 to 1 within the stage; progress is an approximate overall figure (stage weights are fixed, not measured). The stage names stream on the events WebSocket too. opening, colourise and write do not move progress: it is 0 while the capture opens and already 1 when the pipeline ends, so the stage name is what tells you the job is still colouring or writing.
  • queue_position: 0 is next to run.
  • Once finished: timings per stage, and summary with frame counts, alignment points and output size. On failure error is {code, message}.

GET /jobs lists remembered jobs, oldest first. The newest 100 finished jobs are kept.

DELETE /jobs/{id}

  • Queued job: cancelled at once (202, state cancelled).
  • Running job: the pipeline's cancel flag is set (202); poll until the state is cancelled. No output is written.
  • Finished job: the record is forgotten (204). The output file stays.

GET /jobs/{id}/result

The written TIFF, PNG or FITS file (image/tiff, image/png or application/fits), streamed. 409 not_ready unless the job succeeded.

curl -H "$H" -o sun.tif http://127.0.0.1:7870/api/v1/jobs/3/result

GET /jobs/{id}/preview.png

An 8-bit PNG of the result (square-root stretch of the normalised stack), at most 2048 pixels wide. ?max_width= shrinks it further. 409 not_ready unless the job succeeded.

GET /jobs/{id}/sunspots.csv and sunspots.png

For a job that had sunspots in its config: the spot table as CSV, and the overlay (the flattened disc with penumbra, umbra, limb and group numbers) as a PNG. 404 sunspots_missing when the job did not ask. The counts, the Wolf number, the areas, P, B0, L0 and both tables are also in summary.sunspots. See Sunspots.

curl -H "$H" -o spots.csv http://127.0.0.1:7870/api/v1/jobs/3/sunspots.csv
curl -H "$H" -o spots.png http://127.0.0.1:7870/api/v1/jobs/3/sunspots.png

GET /jobs/{id}/events (WebSocket)

Text frames, one JSON object each:

{"type":"snapshot","job":{"id":3,"state":"queued","...":"..."}}
{"type":"state","job":3,"state":"running","error":null}
{"type":"progress","job":3,"stage":"quality","stage_progress":0.5,"progress":0.1}
{"type":"state","job":3,"state":"succeeded","error":null}

The first message is always a snapshot. Progress messages are thinned to about one per half a percent. The server closes the stream after the final state message; a job that has already finished yields the snapshot and then closes.

websocat -H "$H" ws://127.0.0.1:7870/api/v1/jobs/3/events

GET /openapi.json, GET /docs

openapi.json is generated from the code (utoipa) and always matches the server. /docs is a small page of our own that lists the operations from that document; it is static and public, and has a box for the token.

Python

import time
import requests

BASE = "http://127.0.0.1:7870/api/v1"
s = requests.Session()
s.headers["Authorization"] = "Bearer my-token"   # omit if the server has no token

def api(method, path, **kw):
    r = s.request(method, BASE + path, **kw)
    if not r.ok:
        raise RuntimeError(r.json()["error"])
    return r

print(api("GET", "/version").json()["backends"])

info = api("POST", "/captures/inspect", json={"path": "sun.ser"}).json()
print(info["frames"], "frames,", info["width"], "x", info["height"])

job = api("POST", "/jobs", json={
    "input": "sun.ser",
    "output": "stacks/sun.tif",
    "config": {"keep": "20%", "ap_size": 48},
    "overwrite": True,
}).json()

while job["state"] in ("queued", "running"):
    time.sleep(1)
    job = api("GET", f"/jobs/{job['id']}").json()
    print(f"{job['state']:9} {job.get('stage') or '':13} {job['progress']:.0%}")

if job["state"] != "succeeded":
    raise SystemExit(job["error"] or job["state"])

with open("sun.tif", "wb") as f:
    f.write(api("GET", f"/jobs/{job['id']}/result").content)
with open("sun.png", "wb") as f:
    f.write(api("GET", f"/jobs/{job['id']}/preview.png").content)

Polling is enough for scripts; use the WebSocket (for example with the websockets package, passing the Authorization header) when you want live progress without a delay.

Security model

The server reads and writes files on behalf of whoever can reach it, so it is conservative by default.

  • Loopback only. The default bind is 127.0.0.1. A non-loopback address (0.0.0.0, a LAN address) is refused at start-up unless a token is set. There is no TLS: put a reverse proxy in front for anything beyond a trusted network, and give it --allow-host for the public name.

  • Bearer token. Compared in constant time. Only /health and the static /docs page are open. Use a long random token and keep it in a file (--token-file, mode 600) rather than on the command line.

  • Data root confinement. Every input, output, capture and preview path is resolved against --data-root and refused with 403 when it:

    • contains a .. component (path_traversal), even if it would land inside;
    • is absolute and outside the root, or resolves outside it through a symbolic link, junction or dangling link (path_outside_root).

    For outputs that do not exist yet, the nearest existing parent is resolved and checked, so a link in the parent chain cannot redirect a write. Checks happen when the request arrives; do not let untrusted local users write to the data root, since a link swapped in between check and use is not defended against. A SharpCap sidecar is only read when it also resolves inside the root.

  • Host check. On a loopback bind, requests whose Host is not localhost or a loopback address are refused (403 bad_host). This stops a web page from reaching the server through DNS rebinding.

  • No CORS by default. A browser page on another origin cannot read responses. --cors-origin opts one origin in; combine it with a token.

  • Resource limits. 64 KiB request bodies, a bounded job queue (429), at most 100 remembered finished jobs, previews capped at 2048 pixels (frame previews at 8192), existing files never overwritten without overwrite.

  • Path errors do not echo absolute server paths; job records show paths relative to the root.

Without a token any process on the machine (and any local user) can read captures under the data root and write image files there. The server prints a warning in that case; use a token on shared machines.

How stacking works

This page explains what Helia does between a capture and a finished image, in plain terms, and which setting changes which step. The precise maths and the module layout are in the Architecture page.

The problem

Between the Sun and your telescope is a few kilometres of moving air. In any one short exposure the image is shifted, stretched and blurred by it, and the pattern changes within milliseconds. Most frames are poor. Every now and then, for a moment, the air over your telescope is steady and a frame is sharp.

Lucky imaging records thousands of frames, keeps the lucky ones, lines them up and averages them. Averaging many aligned frames also cancels random noise.

Scoreframes Keep thebest Build areference Alignwhole frame Align eachpoint Weightedstack Blend andwrite The capture is streamed through these steps; frames are never all held in memory.

0. Calibration (optional)

Before anything else, frames can be calibrated: a master dark is subtracted, the result is divided by a master flat and bad pixels are replaced by the median of their neighbours of the same colour. It happens as each frame is read, on the stored pixels before any demosaicing, so the ranking, the alignment and the stack all see calibrated frames. The reason it comes first is that the sensor's pattern does not move with the Sun: alignment warps the picture, and a pattern that stayed put turns into detail that moves. See Calibration.

1. Ranking the frames

Helia gives every frame a sharpness score. The default metric, gradient energy, is the average squared difference between neighbouring pixels: a sharp frame has strong edges and fine contrast, a blurred one does not. The alternative, Laplacian variance, measures the spread of the second derivative. Choose between them with --metric or the sharpness metric in the app.

Two details make the score trustworthy on the Sun:

  • A small amount of image noise would raise a gradient score, so Helia estimates the noise floor of each frame and subtracts it.
  • Overall brightness and the pedestal of the sky do not change the score, so a frame that is merely brighter does not rank higher.

The frames are then sorted best first. You choose how many to keep with --keep (a share such as 25%, the default; 12% for white light and Na D) or --keep-frames (a count). The app draws the sorted scores as the Quality, sorted curve, with a dashed line where the kept frames end.

kept best frameworst frame quality Illustrative shape, not measured data.

The frames you keep are not all treated alike. Each gets a weight between 0.2 (the worst kept frame) and 1 (the best), following a smooth S-shaped curve, so sharp frames dominate the average but borderline frames still help with the noise. This is a smooth curve and not a cut-off.

2. A reference to line up against

To align frames you need something to align them to. Helia averages the best few frames (five by default), after aligning them to each other, into a reference image. Because they are the sharpest frames, the reference is already a decent picture, and the other frames are measured against it.

3. Aligning the whole frame

The air tilts the image, so every frame sits a little differently on the sensor. Helia finds each frame's shift relative to the reference by one of two methods. This is the global alignment. It removes the largest part of the blur, which is plain image motion.

  • Phase correlation compares the frames in the frequency domain, which is fast and robust, and then refines the answer to a fraction of a pixel. It needs detail in the picture to lock onto, and it is the method for a cropped region of the Sun. With a GPU it runs on the device using Helia's own Fourier transform; the CPU fits the peak either way. See GPU acceleration.
  • Limb fit finds the edge of the solar disc, fits a circle to it, and takes the shift from where the circle's centre moved. A full-disc frame has no small patch to correlate, but it has a sharp circular edge. Helia casts 720 rays out from the disc, finds the fall in brightness along each to a fraction of a pixel, and fits the circle with a robust fit that ignores prominences, flares and sunspot edges. It runs on the CPU.

Helia chooses for you. With --align auto (the default) it fits the limb of the reference. If the whole disc is in the frame and the fit is confident (0.8 or more, see below) it aligns by limb fit. If the frame cuts the limb off, there is no disc, or the fit is shaky, it uses phase correlation. Force one with --align phase or --align limb. The choice, the reason and the confidence are shown at the end of a run on the command line (the align: line), in the app (under the Stack button, with the reason on hover) and by the API (summary.alignment).

Confidence is a number from 0 to 1 for each frame.

  • For the limb fit it is high when the edge is located precisely and consistently, and the visible limb is spread widely enough to pin the centre down. It comes from the predicted error of the centre in pixels (from the scatter of the edge points, and from refitting without each eighth of the limb in turn): 0.5 is a predicted error of 0.15 pixel and 0.9 is about 0.05 pixel. It is reduced when some edge points disagree with the circle or many rays found no edge.
  • For phase correlation it comes from the sharpness of the correlation peak against its surroundings: 0 for a peak-to-sidelobe ratio of 4 or less, 1 at 10 or more.

A frame whose limb fit is not confident (below 0.5) is aligned by phase correlation instead, and the summary counts those frames. When phase correlation runs on the GPU, which does not return a confidence, Helia re-measures eight frames spread over the candidates on the CPU and reports those, marked sampled.

When the limb is cut by the frame the radius is held at the reference's and only the centre is fitted, because a short arc cannot tell a bigger circle from a shifted one. That is why auto leaves such captures to phase correlation.

How accurate is it?

Measured, not promised. On synthetic discs with known shifts (the tests in crates/helia-core/src/align/limb.rs and tests/limb_align.rs) the shift error of the limb fit is:

CaseError
12 discs at random sub-pixel positions, blur 2 px, noise 2%worst 0.014 px
40 frames shifted by up to 15 px, blur 1.5 to 4 px, noise 3%mean 0.021 px, worst 0.066 px
Limb cut by the frame (about 40% of it outside), 20 framesworst 0.020 px
A prominence and a bright flare near the limb0.023 px
Whole pipeline, textured synthetic capture of 40 frameslimb worst 0.026 px, phase correlation worst 0.048 px

On real frames there is no ground truth, so the limb fit is compared with phase correlation on the same frames (the best 25% of the first 200 frames of each capture, aligned to the pipeline's reference, CPU run on a 24-thread server):

CaptureDisc in frameauto choosesReference limb confidenceLimb minus phase
15_32_09.avi (720 x 540 after 2x binning)wholelimb fit0.89median 0.09 px, worst 0.32 px
15_20_57.ser (1920 x 1080, raw colour)wholephase correlation0.4215 of 25 frames fell back; the rest differ by up to 0.9 px
Sun_Test.ser (1920 x 1080, mono)wholephase correlation0.7121 of 50 frames fell back; median 0.30 px, worst 1.5 px
14_42_57.avi (1920 x 1080)63% of the limbphase correlation0.01every frame fell back

So the limb fit and phase correlation agree to about a tenth of a pixel when the limb is clean, and auto uses the limb only then. In the other captures the seeing deforms the limb enough that its centre wanders by several tenths of a pixel against the content, the confidence is lower, and auto stays with phase correlation. In a global-only stack of these captures the two methods gave the same sharpness to within 0.5% (limb over phase correlation: 1.000 for 15_32_09.avi, 1.002 for 15_20_57.ser, 1.005 for Sun_Test.ser, 1.000 for 14_42_57.avi), which is a tie, not a win. Whether a better result is possible with the limb on the noisier captures is not established; use --align limb to try it.

4. Aligning small patches: alignment points

Global alignment is not enough on the Sun. The air does not bend the image by the same amount everywhere: over a patch a few arcseconds across the distortion is roughly uniform, but the disc covers tens of thousands or more of such patches. After a global shift, one part of the disc may still be a pixel off while another is spot on, and the sharp moments differ from place to place too. So Helia aligns many small points as well as the whole frame.

Helia lays a grid of overlapping square windows, the alignment points, over the reference. The default is 64 pixels across, overlapping by half; you change the size with --ap-size or the point size in the app.

no detail: skipped Schematic: real grids have many more points.

For each point and each frame Helia measures the local shift by correlating the window against the reference, searching a small distance around the global shift, and scores how sharp that window is in that frame. Then each point picks its own best frames: a frame that was sharp over the left of the disc and soft on the right contributes to the left only. To make this possible, Helia aligns twice as many frames as you keep, so each point has a choice.

Points are placed only where there is something to align: the centre of a point's window must be inside the solar disc (see Keeping alignment off text and sky). Some points cannot be measured: flat regions, flat regions, or a straight stretch of limb, where the pattern fixes the shift across the edge but not along it. Those points are not allowed to guess. They take the distance-weighted average of their neighbours' shifts instead (or the global shift when there are no neighbours). The app reports how many points are active, meaning bright and detailed enough to be measured.

Alignment points are correlated on a smoothed copy of the frame with the slow brightness gradient removed, because on low-contrast detail such as H-alpha the limb darkening would otherwise flatten the correlation peak.

Choosing the points automatically

--ap-auto (the Auto chip) chooses the point size, spacing and search range from the capture instead of one size for everything.

  • Scale. The reference's limb fit gives the radius in pixels; the Sun is 959.6 arcseconds in radius, so the scale is that over the radius. The window is the size of the detail in arcseconds over the scale: 3.75 arcseconds (2.5 granules) for white light and sodium, 90 arcseconds for the other filters (a starting point; no chromospheric capture has tuned it), rounded to a multiple of 8 and kept within 32 to 96 pixels. Without a limb the recommended size for the frame is used.
  • Seeing. A dozen of the frames about to be stacked are registered globally and a coarse grid of 48 pixel points is measured in each. The local warp is the root mean square distance of a point's shift from the median shift of its frame, over the locked points of all the probe frames. The search radius covers three times that warp, between 4 and 12 pixels.
  • Spacing is half a window, as before.

What the real captures said. All four are ASI482MC sodium captures, stacked with the Na D preset on the CPU; the figure is the granulation band (1 to 4 px) of helia measure at the disc centre, so higher is more granulation contrast:

CaptureScaleWarpAuto chose32 px48 px64 px96 px
Sun_Test.ser2.16 arcsec/px0.88 px32 px, search 51.847%1.814%1.792%1.779%
15_20_57.ser2.16 arcsec/px0.23 px32 px, search 42.050%2.041%2.039%2.036%
15_32_09.avi4.31 arcsec/px0.23 px32 px, search 40.971%0.962%0.957%0.952%
14_42_57.avi1.44 arcsec/px1.59 px32 px, search 70.170%0.158%0.156%0.152%
  • At these scales a granule is under three pixels, so the rule lands on the 32 pixel floor on every capture. Helia's automatic size is the preset's, and the auto run's granulation figure is the --ap-size 32 one to the last digit of the table. The window only grows on a scale finer than about 0.12 arcsec per pixel, or for a filter with larger detail.
  • Smaller windows read higher in the granulation band (at 24 px: 1.884% on Sun_Test.ser, 2.064% on 15_20_57.ser, 0.980% on 15_32_09.avi, 0.210% on 14_42_57.avi). On 14_42_57.avi, where only 63% of the limb is in view and the contrast is low, the meso band (4 to 12 px) rises from 0.39% at 32 px to 0.54% at 24 px and 1.38% at 12 px. That is blocking from windows that lock on noise, not detail, so 32 pixels stays the floor.
  • Spacing, with auto's 32 pixel windows: a step of 8, 12 or 16 px (75% to 50% overlap) gave the same granulation band on the first three captures (within 1%), while a step of 24 or 32 px (little or no overlap) lost 1% to 5% of it and blocked 14_42_57.avi (meso 0.39% to 1.6%). Half a window is kept.
  • The search radius made no measurable difference here (14_42_57.avi at 7 px against 4: 0.169% against 0.170%). It is there so a capture with large local motion does not lose its points at the edge of the search.

Refining the shifts

Two options, both off by default, work on the table of shifts the points produce (#4).

  • --ap-passes 2 stacks once, then measures every point again against that stack instead of against the reference, which is only the best few frames averaged and so noisier and softer. The second table is what the final stack uses. The cost is one more measuring pass and one more stack: about 30% more time on the sodium captures (3.0 to 3.8 s on the GPU, 4.6 to 6.7 s on the CPU for Sun_Test.ser).
  • --ap-smooth on replaces a shift that sits further from the median of its neighbours (the points within one and a half grid steps) than three times the neighbours' own robust spread, and further than 0.3 px. Such a point has usually locked on the next granule over. A real local distortion varies smoothly, so its neighbours spread out and nothing is replaced. --ap-smooth off is the default.

What the real captures said. The same four sodium captures, Na D preset, GPU. The three granulation columns are the bands of helia measure (fine 0 to 1 px, granulation 1 to 4 px, meso 4 to 12 px). The residual is what --ap-residual prints: the shifts are measured once more against the finished stack, and the figure is the RMS distance in pixels from the shifts that were used (smaller means the stack agrees better with the frames it was made from).

CaptureSettingFineGranulationMesoResidual (px)Shifts replaced
15_20_57.serone pass1.573%2.050%1.720%0.0420
two passes1.575%2.050%1.719%0.0170
Sun_Test.serone pass1.074%1.847%1.634%0.3680
smoothing1.074%1.847%1.633%0.369705 of 370120
two passes1.068%1.880%1.657%0.1020
both1.068%1.880%1.657%0.1071215 of 370120
14_42_57.avione pass0.109%0.170%0.388%1.1120
smoothing0.110%0.170%0.388%1.09211594 of 440791
two passes0.105%0.172%0.400%0.6370
both0.107%0.172%0.399%0.66624516 of 440791
15_32_09.avione pass0.314%0.971%0.791%0.0450
two passes0.314%0.971%0.791%0.0180
  • The second pass cuts the residual by a factor of 1.7 to 3.6 on every capture, and adds 1.8% to the granulation band on Sun_Test.ser (1.847% to 1.880%) and 1.2% on 14_42_57.avi, the two captures with the larger local warp (0.88 and 1.59 px). Where the warp is small (0.23 px on 15_20_57.ser and 15_32_09.avi) the reference is already good and nothing moves.
  • Smoothing changed the granulation band by less than 0.1% everywhere. It replaces 2.6% of the shifts on 14_42_57.avi, the low-contrast capture where mismatches are common, and lowers the residual there by 2%. Elsewhere it makes no measurable difference.
  • Synthetic data with a known truth (48 frames of a warped, noisy synthetic sun, 32 px points, the test refinement_does_not_hurt_on_noisy_warped_data) did not show the second pass as an accuracy gain: the RMS error against the clean sun was 0.0471 with one pass and 0.0528 with two at noise 0.02, and 0.0556 against 0.0606 at noise 0.2, with sharpness slightly higher (1% at both). That warp is much stronger than the seeing in the captures (2 px amplitude over 90 px, against windows of 32 px), so a window averages a distorted patch whichever reference it uses. Treat the second pass as a refinement for captures with real local motion, and confirm with --ap-residual and helia measure.

Keeping alignment off text and sky

Capture programs such as SharpCap can draw the date and time into the picture. That text is bright, sharp and different in every frame, which makes it the worst possible thing to align on, and the empty sky around the disc has nothing to align on at all. Before it scores anything, Helia therefore:

  • Finds burned-in text. It looks at about 16 frames spread over the capture for pixels that flip between white and dark, in a small area at a frame edge. The unchanging part of the same line (the date) is added to it. The area is then replaced by one flat level, the median of the pixels around it, in every frame, so that scoring, global alignment, alignment points and the stack all agree. A timestamp therefore disappears from the result instead of being smeared. Switch this off with --no-overlay-detect.
  • Fits the limb. A circle is fitted to the steepest edge between sky and disc on the reference image. The fit ignores points that disagree with the circle (a prominence, a flare) and works when the frame cuts off part of the disc. An alignment point is used only when the centre of its window is inside the circle. --prominence-band <px> allows points that far outside the limb, so that they can follow prominences; the default is 0. If no clear disc is found (a capture of something other than the Sun) nothing is masked this way. Switch it off with --no-disc-mask.
  • Honours your rectangles. --exclude x,y,w,h (repeat it for several), the exclude list of an API job, and the Exclude regions list in the app keep anything else out: a logo, a stray label, a hot pixel cluster. No alignment point window may overlap such a rectangle, and like detected text the area is flat in the result.

5. Stacking

Each point's chosen frames are moved by their measured shifts (sampled at sub-pixel positions) and combined as a weighted mean. That is the default, and for a seeing-selected stack it is the right answer: the weights already say how much each frame is worth.

Three optional guards deal with samples that do not belong, such as a hot pixel a dark frame missed, a satellite or plane trail, a passing bird or a cloud dimming a few frames. They are chosen with --robust (or robust in an API job, or the Outliers chips in the app) and all work the same way: a first pass over the frames finds the weighted mean and spread at every pixel, a second pass decides what each sample is worth from those. Memory does not grow with the number of frames, and the capture is read twice instead of once.

--robustA sample beyond K standard deviations of the mean is...Default K
nonekept (the weighted mean)
sigmadropped2.5
huberdown-weighted by K sigma over its distance (a Huber M-estimator step)1.345
winsorclamped to K sigma from the mean (a winsorised mean)2

--sigma-clip K is the older spelling of --robust sigma --robust-k K. At a pixel with fewer than three samples nothing is changed. All of them are off by default, and none is a cure for bad alignment.

How they compare, measured on synthetic data with a known answer (200 frames of a 48 by 48 scene, noise 0.02 on a signal of 0.2 to 0.8, quality weights 0.5 to 1.5, default K; cargo test -p helia-core --all-features --test robust_stack -- --nocapture, commit f7aa530). Errors are RMSE against the truth, times 1000:

DamageShare of samplesmeansigmahuberwinsor
none (clean)01.521.581.551.52
hot pixels (+1.2)0.1%3.421.531.561.61
1%15.101.493.284.71
5%63.353.2822.5933.96
plane trails (+0.5, 2 px wide)0.1%2.061.541.521.52
1%5.741.491.782.10
5%25.371.519.3413.92
cloud (frames at 40%)0.5%2.171.501.511.51
1%3.821.491.601.71
5%17.391.526.099.02

On clean data the cost is small: against the weighted mean, the mean level changes by at most 0.001% and the contrast of the fine texture by 0.04% (sigma), 0.13% (Huber) and 0.008% (winsor), well inside the 1% budget. A 16 by 16 patch that 5% of the frames sample 2.5 px away (a bad alignment point shift) has an RMSE of 1.86 for the mean and 1.70, 1.71 and 1.76 for sigma, Huber and winsor: a modest gain, not a repair.

Two things to know. The spread comes from the same samples, so a heavy contamination inflates it and the soft forms then let part of an outlier through; a hard clip is the best at removing gross outliers (a hot pixel, a trail) and the soft forms pay for it only in the 5% case. In exchange the soft forms are gentler on real, asymmetric spread: with one sample in five pulled down by 0.05 the result moves from the weighted mean by 0.89 (sigma), 1.29 (Huber) and 0.39 (winsor), times 1000. Winsor is therefore the gentlest on real data and sigma the strongest on gross outliers; Huber at its customary 1.345 sits between them on outliers and moves the result most on real spread, so raise K for it with --robust-k if that matters. With real lucky-imaging stacks, where the spread between frames is partly signal, leave it off unless the capture has an artefact to remove.

6. Blending and writing

The per-point stacks overlap, so Helia blends them with smooth raised-cosine weights that sum to one where windows overlap. This avoids visible seams. Where no point could be measured, the globally aligned stack fills in. The result is scaled so that its 99.99th percentile is 1 (full scale), and written as a 16-bit TIFF, an 8-bit PNG or a 32-bit FITS. See Output formats.

Cropping to the disc

--autocrop (the Crop chip) runs after the stack. The limb is fitted on the stacked image, which is sharper and quieter than a single frame, and the result is cut to a square around it with a margin, 10 per cent of the radius by default. The pixels are the stack's own (cutting the uncropped image by the same rectangle gives the same result), and the disc is reported in the cropped image's pixels, so a FITS header's limb and plate scale stay right. A disc near an edge moves the square inwards, a disc larger than the frame keeps the whole of that side, and with no clear disc nothing is cropped. On the four captures the crops were 979 x 979, 978 x 978, 490 x 490 and 1460 x 1080 pixels (the last disc is larger than the frame, so its short side is whole).

7. Colour (optional)

Sharpening and the everyday adjustments are applied after the stack is finished, as a recorded list of operations on the mono stack (see Sharpening and adjustments). The stack itself is never changed.

Colour is applied after the sharpening, to the finished image, and only when you ask for it. The stack is a single channel; a preset for the filter (orange for H-alpha, violet for calcium and so on) maps its grey values to colours through a table, for a 16-bit RGB TIFF or an 8-bit PNG. FITS stays mono. The colour is a presentation choice, and the file records it. See Colourising.

True colour is the other way round: with --colour true the three planes of a raw colour capture are stacked on the one alignment above (the frames are scored and aligned once, on the luminance, and the planes use the same shifts, alignment point field and weights), the red and blue stacks are registered to the green by phase correlation to remove dispersion, and a white balance and one common scale make the picture. See True colour.

What Helia does not do yet

  • Two-pass refinement of the alignment points against a first stack (#4).
  • Drizzle (#6).

See the Roadmap.

The granulation and seeing report

A stack of the Sun's surface is only as good as the detail it holds. For white-light and sodium D captures (the photospheric filters, the ones that show granulation) Helia turns the finished stack into a short report you can compare between runs, settings, nights and equipment (#56). It is printed by helia stack after the stack, written into the FITS header, returned by the API as summary.granulation, shown as one line in the Stack step of the app, and available for any finished image with helia measure.

Hydrogen-alpha, calcium and the other chromospheric filters have no granulation to measure, so they get no report.

What it measures

The report is made on the central 240 x 240 pixel square of the disc, which keeps it away from the limb in most captures. Before anything is measured the square is flattened: it is divided by a copy of itself blurred by an eighth of its side, which removes limb darkening, vignetting and any slow gradient but not granules. Everything below is therefore independent of how bright the capture was.

  • RMS contrast. The standard deviation of the flattened square. This is the number that matters scientifically: it falls with seeing and defocus and rises with sharpening, so report it on the unsharpened stack (the FITS file is that). It depends on bit depth and noise, so quote the conditions.
  • Power spectrum. The azimuthally averaged power of the square, with its noise floor taken from the highest frequencies. Two landmarks are read from it: the roll-off, the frequency where the power above the noise has fallen to 10% of its peak (seeing and defocus move it down), and the resolved frequency, the highest one still above 1% of the peak (noise limits it). They are given in cycles per pixel, as a period in pixels, and in arcseconds when the image scale is known (the fitted limb gives it).
  • Diffraction cut-off. With the aperture (--aperture-mm, API aperture_mm) Helia also gives D / lambda in cycles per pixel, taking 550 nm for white light and 589.3 nm for sodium, and how much of it the data reaches. No stack resolves all of it: the optics' transfer function falls to zero there.
  • Cell size. The square is band-passed at the scale where the contrast peaks, the bright cells are labelled and counted, and the report gives the mean spacing of granules (the square root of the area per cell) and the mean diameter of those that lie wholly inside. It is only meaningful when a granule covers at least three pixels; the report says when it does not.
$ helia stack Sun_Test.ser -o sun.fits --preset na --aperture-mm 80
...
granulation: region:         240 x 240 at (708, 416)
granulation: RMS contrast:   3.756%
granulation: cell size:      pitch 13.65 px (29.45 arcsec), diameter 7.69 px (16.59 arcsec) (309 cells)
granulation: power spectrum: roll-off 0.146 cycles/px (period 6.86 px, 14.79 arcsec), resolved to 0.292 (period 3.43 px, 7.39 arcsec)
granulation: telescope:      diffraction cut-off 1.419 cycles/px lies above the sampling limit (0.5): the pixel scale, not the telescope, limits the detail; resolved 58% of the sampling limit
granulation: noise floor:    3.091e-5
granulation: note:           a granule (1.5 arcsec) spans 0.7 px at this scale; below 3 px the cell size measures larger structure and noise, not granules. The contrast and the spectrum still compare runs of the same capture.

The Stack step of the app shows the RMS contrast in one line once a white-light or sodium stack has finished, with the whole report in the tooltip.

The Stack step after a white-light stack: the lines "Seeing good, r0 8.4 cm" and "Granulation 4.12% RMS" under the alignment line

(The picture is the app's synthetic demo capture with fixed example figures in the report lines, not measurements of it; the seeing line is the one of Seeing estimation.)

The FITS header gets GRANRMS, GRANPITC, GRANDIAM, GRANPARC, GRANGPIX, GRANROLL, GRANRESO and, with the aperture, GRANCUT.

On any image: helia measure

helia measure sun.fits --scale 2.16 --aperture 150 --wavelength 589.3

prints the three band-pass contrasts of helia measure and the report above. --scale is arcseconds per pixel; --aperture needs --wavelength and --scale.

How well it tells good from poor

Known-answer tests (cargo test -p helia-core granulation) on synthetic Voronoi mosaics with a known cell size and contrast, blurred by known Gaussians and with added noise:

TestResult
Mean cell size, 8, 12 and 16 pxrecovered to 3.3%, 3.3% and 0.3%
RMS contrast against the truth, blur 0 to 3 pxwithin 3%, and falls with blur
Brightness x4 and a 25% gradientcontrast within 3%, cell size within 5%
Band-limited to 0.2 cycles per pixelresolved frequency 0.200
Roll-off against blur of 0.6, 1.0, 1.6, 2.4 px0.113, 0.108, 0.096, 0.071 cycles per pixel
More noisethe noise floor rises by over 20 times; the resolved frequency does not rise

On the project's four sodium captures (ASI482MC; Na D preset, GPU; FITS output, so unsharpened). Each is stacked three ways: the preset's best 12% of frames with alignment points; all frames (--keep 100%); the best 12% with global alignment only (--no-multipoint). The eye ranks the first above the others, and the report agrees:

CaptureStackRMS contrastRoll-off (period, px)Resolved (period, px)
15_20_57.serbest 12%, points4.17%6.73.0
all frames3.99%7.13.1
best 12%, global4.14%6.73.0
Sun_Test.serbest 12%, points3.76%6.93.4
all frames3.44%8.34.3
best 12%, global3.64%6.93.4
15_32_09.avibest 12%, points1.95%13.36.3
all frames1.86%14.16.5
best 12%, global1.91%15.06.5
14_42_57.avibest 12%, points1.39%34.326.7
all frames0.53%48.021.8
best 12%, global0.55%34.312.0
  • The contrast ranks all four captures as expected: alignment points above global alignment, and a selected 12% above all the frames. The roll-off period agrees or ties on every pair (it cannot tell points from global alignment on the first two captures, whose seeing is good).
  • 14_42_57.avi is the blocky capture with no visible granulation: its contrast is mostly compression blocks and its resolved frequency flips between stacks, so read nothing into it beyond "much worse".
  • The cell size is not in the table because it is not granulation on these captures. At 2.16 arcsec per pixel (Sun_Test.ser, 15_20_57.ser), 4.31 (15_32_09.avi) and 1.44 (14_42_57.avi) a 1.5 arcsec granule is 0.3 to 1 pixel, so the cells found are 9 to 14 pixels across (20 to 30 arcsec): larger structure and noise texture. The report prints a note saying so. The cell size becomes meaningful at about 0.5 arcsec per pixel or finer.
  • The report is repeatable: it uses no random numbers.

Limits

  • The region is the disc centre. A spot, a facula or a filament there raises the contrast; check the crop, and use helia measure --crop or crop the image first.
  • A sharpened image reads higher. Compare sharpened with sharpened, or use the FITS stack.
  • Hydrogen-alpha has no granulation: no report is made for it.

Seeing estimation

Helia can estimate the seeing from the capture itself, with no reference star (#49). It gives two numbers that answer different questions:

  • r0, the Fried parameter, from the spectral ratio. An absolute figure: the diameter of the patch of atmosphere over which the wavefront is flat to about a radian, at the capture's wavelength and scaled to 500 nm. D / r0 (the aperture over r0) says how many seeing cells the telescope looks through, and with it how rare a lucky frame is. It needs the telescope aperture.
  • A motion index from the alignment points. A relative figure: how much neighbouring alignment points move against each other from frame to frame. It needs nothing but the capture, and is not an absolute r0.

Switch it on with --seeing (add --aperture-mm 150 for r0), seeing and aperture_mm in an API job, or set the telescope aperture under Advanced settings in the app (the Stack step then shows a seeing line). It reads about 120 more frames, spread evenly over the capture, after the stack: about two seconds on the project's captures. The result is printed after the stack, written into the FITS header (SEEMOTPX, SEEMOTAS, SEEDR0, SEER0500, SEEGRADE) and returned by the API as summary.seeing.

$ helia stack Sun_Test.ser -o sun.fits --preset na --seeing --aperture-mm 80
...
seeing:  followed 16 tiles of 64 px through 120 frames spread over the capture
seeing:  motion index:   0.274 px (0.59 arcsec) RMS differential motion per axis between points 72 px (154 arcsec) apart (14 pairs); relative, not r0
seeing:  spectral ratio: D/r0 8.2, r0 1.0 cm at the capture's wavelength, 0.8 cm at 500 nm (18 rings, 16 tiles, tiles differ by 25%)
seeing:  note:           a 64 px tile spans 138 arcsec, more than one isoplanatic patch (a few arcseconds); the method is validated for one transfer function per tile, so r0 is indicative and no grade is given
seeing:  note:           the cut-off (1.42 cycles/px) is above the sampling limit, so fine detail is aliased and the r0 is indicative; a scale of 0.76 arcsec per pixel or finer avoids it

(The aperture here is a guess: the project's captures do not say what telescope they were taken with.)

The spectral ratio

For an object that does not change during the burst, the Fourier transform of a tile of frame k is F_k = O S_k: the object's transform times the transfer function of that frame's atmosphere and telescope. Average over the frames, once aligned, and the ratio |<F>|^2 / <|F|^2> equals |<S>|^2 / <|S|^2>. The object cancels (von der Luehe 1984), leaving a curve that depends only on D / r0 and on the frequency in units of the telescope's cut-off D / lambda. Helia matches the measured curve to the one a Kolmogorov atmosphere gives, which it holds as a table made by Monte Carlo over phase screens (cargo run --release -p helia-core --example seeing_table; the structure of the screens is checked against Noll's tilt-removed variance, 0.134 (D / r0)^(5/3), to within 1%).

In practice: up to 16 tiles of 64 x 64 pixels, the ones with the most detail and wholly inside the disc, are followed through the sample by correlation. Each tile is cut at its integer position, the sub-pixel remainder removed in Fourier space, and the mean transform and the mean power accumulated. The noise is measured on the tiles (Immerkaer's estimator) and its power taken out of the denominator; the finite-sample bias of |<F>|^2 is taken out of the numerator. Rings of the spectrum with a signal to noise above three, between 5% and 90% of the cut-off (and below the sampling limit), are fitted by least squares.

The frames are a sample of all the frames, not the best ones: this is the seeing of the session, not of its lucky moments.

Validation on synthetic data

Kolmogorov phase screens, a granulation-like object, known D / r0 and noise (cargo test -p helia-core seeing):

TestResult
D / r0 3, 5, 8, 12, 160 frames, 4 tilesrecovered 2.84, 4.94, 7.25, 9.71 (-5%, -1%, -9%, -19%)
Object: granulation, limb, pore, D / r0 65.85, 5.50, 5.76: within 10% of each other
Noise 0.02 and 0.05 (of a mean of 1), estimated or known6.03 and 5.67 for a truth of 6
The same atmosphere at 656 nm and 393 nmr0 at 500 nm 17.9 and 19.6 cm: within 10%
Cut-off at twice the sampling limit, aliased5.25 for a truth of 5
Two independent patches in a tile3.92 for a truth of 4 (one patch: 3.86)

It loses sensitivity as D / r0 grows (above about 12 only the lowest frequencies carry any signal, and the estimate reads low); the report says so.

What limits it on real data

  • The aperture must be right. It sets the cut-off, and r0 is D over the fitted D / r0. A wrong aperture gives a wrong r0 in proportion.
  • Sampling. When the telescope's cut-off D / lambda is above 0.5 cycles per pixel (for 80 mm at 589 nm, a scale coarser than 0.76 arcsec per pixel) detail aliases. The synthetic test above shows aliasing does not by itself spoil the estimate, but the report says so as a caution.
  • Tile size. The method assumes one transfer function per tile, that is a tile within the isoplanatic patch, a few arcseconds in daytime. A 64 pixel tile is that only at about 0.1 arcsec per pixel or finer. When a tile spans more than 15 arcseconds (a scale coarser than 0.23 arcsec per pixel) the report gives r0 as indicative and no grade, because it has not been validated there: the simulation above covers only the limiting case of independent patches, and real neighbouring patches are partly correlated.
  • Few frames. Under 40 frames the report makes no r0.
  • The object must not change during the burst. Granulation evolves in minutes; a capture of a minute or two is fine.

The grade

From r0 at 500 nm: 10 cm or more is excellent, 7 to 10 good, 4 to 7 fair, 2.5 to 4 poor, under 2.5 bad. These are day-time solar thresholds, not a standard.

The alignment point motion index

Tip and tilt of the whole frame (and tracking error and wind shake) move every patch of the Sun together, so they cancel when the shifts of two neighbouring alignment points are subtracted. What remains is the differential image motion, which turbulence causes (Scharmer and van Werkhoven 2010). Helia takes the tiles above, measures their shift in each of the sampled frames, subtracts neighbours (points up to one and a half grid spacings apart), removes each pair's mean difference (the static offset between the reference and the scene) and takes the RMS of what is left, per axis, in pixels and in arcseconds.

It is a relative index, for comparing captures taken through the same optics at the same scale. The patches look through the atmosphere at different field angles, so it depends on how high the turbulence is: for the same r0, turbulence at 5 km gives more differential motion than turbulence at the ground. On synthetic phase screens at D / r0 6, with tiles 16 pupil pixels apart at height:

Where the turbulence isIndex
All at the ground0.0000
Half at the ground, half at height0.0486
All at height0.0667

and for turbulence at height it grows monotonically with D / r0 (3 to 12) with a log-log slope of 0.83, the 5/6 the theory gives for the RMS of a differential tilt.

On the real captures

The motion index of the whole of each capture (a sample of 120 frames; the fourth capture is too blocky to lock tiles):

CaptureIndexPairsAutoap's warp
15_20_57.ser (2.16 arcsec/px)0.19 px, 0.41 arcsec180.23 px
Sun_Test.ser (2.16 arcsec/px)0.27 px, 0.59 arcsec140.88 px
15_32_09.avi (4.31 arcsec/px)0.16 px, 0.69 arcsec260.23 px
14_42_57.avinot enough points locked1.59 px

The ranking agrees with the warp --ap-auto measures on a different principle (the spread of a point's shift about its frame's median).

The independent check on the real data: the quarter of the frames the stacker ranks sharpest (a half for the 100-frame capture) should read as better seeing than the quarter it ranks worst, in the same capture. cargo test -p helia-core --test seeing_real with HELIA_REAL_DATA set (Na D, green channel, 240 frames, the aperture taken as 80 mm, so only the comparison means anything):

CaptureMotion index, best / worstD / r0, best / worst
15_20_57.ser0.147 / 0.184 px3.63 / 4.37
Sun_Test.ser0.147 / 0.220 px5.01 / 7.36
15_32_09.avi0.133 / 0.138 px15.17 / 15.40

Both rank the best frames as the better seeing on all three captures. On 15_32_09.avi the difference is small (a few per cent): the capture is short and steady.

The absolute values are not to be trusted on these captures: at 1.4 to 4.3 arcsec per pixel a 64 pixel tile is 90 to 280 arcsec wide, far beyond an isoplanatic patch, and Helia says so and gives no grade. The method is meant for scales near 0.1 arcsec per pixel; on a capture like that --seeing --aperture-mm is the one to use. There is no such capture in the test set yet, so the absolute r0 is validated on synthetic data only.

Using it

  • To compare nights or settings with the motion index: same telescope, same scale, same filter.
  • For r0 and D / r0: give the aperture, use a scale of 0.1 to 0.3 arcsec per pixel, and expect a grade.
  • To choose how many frames to keep: the lower D / r0 is, the larger the share of good frames (Fried 1978). D / r0 under 4 makes a high keep share (25% or more) reasonable; above 10, keep fewer. Automatic keep share from this number is future work (#21, #25).

Sources

  • von der Luehe, O. (1984), JOSA A 1(5), 510-519: estimating Fried's parameter from a time series of an arbitrary resolved object imaged through atmospheric turbulence.
  • Scharmer, G. B. and van Werkhoven, T. I. M. (2010), A&A 513, A25: S-DIMM+ height characterisation of day-time seeing using solar granulation.
  • Noll, R. J. (1976), JOSA 66, 207: Zernike polynomials and atmospheric turbulence.
  • Immerkaer, J. (1996), Computer Vision and Image Understanding 64, 300: fast noise variance estimation.
  • Lane, R. G., Glindemann, A. and Dainty, J. C. (1992), Waves in Random Media 2, 209: simulation of a Kolmogorov phase screen.

Sunspots, groups and the Wolf number

Helia can find the sunspots on a white-light stack, group them, count them and measure their areas (#57). It gives:

  • the number of groups g and of individual spots s;
  • the relative Wolf number R = k (10 g + s), with the observer factor k you set (1 is the raw count);
  • each spot's area in millionths of the solar hemisphere (MSH), as seen and corrected for foreshortening, and its umbra alone;
  • each spot's and group's position on the Sun: latitude, central meridian distance and, when the capture has a time, Carrington longitude;
  • an overlay picture: the flattened disc with the penumbra (yellow) and umbra (red) outlined, the limb (cyan), the group numbers (green) and an arrow to solar north.

It is for white light, with the whole disc in the frame. In H-alpha or calcium the contrast of a spot is different and the filaments and plage give false spots; the thresholds below mean nothing there.

The Stack step after a stack with the sunspot search on

Using it

$ helia sunspots stack.tif --date 2025-09-17T16:14:00 --csv spots.csv --json spots.json --overlay spots.png
file:        stack.tif
image:       1080 x 1920
disc:        centre (554.5, 980.8), radius 401.7 px
time:        2025-09-17T16:14:00.000 UTC
7 groups, 14 spots; Wolf number 84.0 (k = 1)
area 1218 millionths of the hemisphere corrected (795 as seen), umbra 380
P 24.49, B0 7.16, L0 247.82 degrees
group 1: 1 spot, 1 umbra, lat +33, L 174, 326 MSH (98 as seen)
...
spot group    x     y    lat   cmd   lon    mu umbrae  as seen corrected umbra (MSH)
   1     1   229   778 +32.5 -73.8 174.0  0.30      1       98       326         114
  • On a stack as it is made: helia stack capture.ser -o sun.fits --sunspots (white-light captures; the time comes from the capture). The table, JSON and overlay are saved beside the output as sun-sunspots.csv, .json and .png, the summary is printed, and a FITS output carries it in its header.
  • On any stacked image: helia sunspots image.tif. The time comes from --date (UTC, 2025-09-17T16:14:00), from the frame stamps of the capture the image was made from (--capture file.ser), or from the DATE-AVG or DATE-OBS card of a FITS file.
  • In the API: "sunspots": {} in a job's config; see The API.
  • In the app: the Sunspots chips in the Stack step. After the stack the step shows the count, Show overlay puts the overlay in the viewer, and Save table and overlay writes the CSV, JSON and PNG.

How it works

  1. Flatten. The limb darkening is fitted and divided out (Sharpening), so one threshold means the same thing at the centre and near the limb. The disc centre and radius come from the same fit.
  2. Quiet level. The image is smoothed a little (a Gaussian of about a pixel per 400 px of disc radius). The quiet-Sun level at each pixel is the mean of the surrounding disc (a box 40% of the radius across) with the dark and the bright pixels left out, found in four passes that tighten from 60% to 93%. A gradient the flattening cannot remove (a vignette, a thin cloud, a one-sided filter) is then not read as spot. "Dark" below means a share of this local level, above the sky.
  3. Penumbra and umbra. A pixel darker than the penumbra threshold (0.88 by default) is spot; a spot's pixels darker than the umbra threshold (0.55) are its umbra. 0.88 and 0.55 are the middle of a typical penumbra (0.75 of the photosphere) against the photosphere and of an umbra (0.3) against the penumbra, because a blurred edge sits half way. They are the options --sunspot-penumbra and --sunspot-umbra.
  4. Spots. Connected regions (eight-neighbour) of at least 4 pixels and 2 MSH are spots. Each umbra of 2 pixels or more counts as one individual spot for the Wolf number, and a region with no umbra (a pore) counts as one.
  5. Groups. Two spots whose edges are closer than 3 degrees on the Sun (--sunspot-group-gap) are in one group, and the grouping is transitive. The edge of a spot is taken as a circle of the spot's own corrected area about its centroid. This is not the Zurich classification, and the Wolf number depends on how groups are drawn: it is a relative figure.
  6. Foreshortening. A pixel at a distance u from the disc centre, in radii, is 1 / mu of its picture area on the Sun, with mu = sqrt(1 - u^2). Helia sums 1 / mu over every pixel of the spot, so a spot that spans a lot of the disc is treated correctly. The hemisphere is 2 pi R^2 pixels, so one MSH is R^2 pi / 500000 pixels (1.6 pixels at a radius of 500). The corrected area is what the spot really covers; "as seen" is what it covers in the picture.
  7. Position. The pixel position, the disc centre and radius and the orientation give a point on the disc; with B0 (the tilt of the Sun's axis towards us) that is a heliographic latitude and a longitude from the central meridian. With the capture time L0 makes it a Carrington longitude.

Spots nearer the limb than 97% of the radius are not measured: the limb fit and the flattening are least certain there, and a pixel is foreshortened by more than four to one. A dark region larger than 6000 MSH (the largest groups ever recorded are about that size, --sunspot-max-area) is left out with a note: it is a cloud, a vignette or an uneven flat, not a spot.

Time, orientation and the Sun's axis

P (position angle of the Sun's axis), B0 and L0 come from the Sun's position at the capture time, from the low-precision series of Meeus, Astronomical Algorithms, chapters 25 and 29 (good to about 0.01 degree; checked against the book's worked example). The time is the mean of the frame stamps of the frames that were stacked (SER files have them), else the header's start time. Without a time B0 is taken as 0 and longitudes are measured from the central meridian only; the report says so.

Helia does not know how your camera sits on the sky. By default solar north is taken to be up. If it is not:

  • --north-angle DEG turns solar north counter-clockwise from the top of the image (API north_angle_deg);
  • --celestial-north-up is for an equatorial mount without a flip: solar north is then P degrees counter-clockwise from the top (the Sun's axis is P east of celestial north), and P is taken from the capture time;
  • --mirror is for a mirror image (a diagonal without a flip, an odd number of reflections).

A wrong orientation does not change the counts or the areas, only latitude and longitude, and so the foreshortening is unaffected too.

The output

The CSV has one row per spot:

ColumnMeaning
spot, groupNumbers from 1; groups are numbered largest first
x_px, y_pxCentroid, pixels, top row 0
lat_degHeliographic latitude, degrees north
cmd_degCentral meridian distance, degrees, west positive
lon_degCarrington longitude; empty without a capture time
muCosine of the angle from the disc centre at the centroid
umbraeIndividual spots in the region (at least 1)
umbra_px, penumbra_pxPixels of umbra, and of penumbra without it
projected_mshWhole spot as seen, MSH
area_mshWhole spot, corrected for foreshortening, MSH
umbra_mshUmbra, corrected, MSH
darkestDarkest smoothed intensity inside, a share of the quiet disc

The JSON adds the groups, the disc, the time, P, B0, L0, the thresholds and the notes. The FITS header gets SPOTNGRP, SPOTNSPT, SPOTWOLF, SPOTK, SPOTAREA (corrected), SPOTPROJ, SPOTUMBR, SPOTTHRU, SPOTTHRP, SPOTP, and the SOLARNET observer coordinates CRLT_OBS (B0) and CRLN_OBS (L0).

What was measured

Known answers. The unit tests render a limb-darkened disc with spots of known size and heliographic position (including ones near the limb), blurred and noisy, and check what Helia recovers: the counts are exact for isolated spots, a spot's corrected area is within 5% of the truth and its umbra within 15% (the umbra of the smallest, 8 pixels across, is the least sure), the latitude and central meridian distance within 0.5 degree, a near-limb spot is seen at under 0.6 of its corrected area, groups form and split at the separation asked for, two umbrae in one penumbra count as two spots, and a clean disc has none. The ephemeris matches Meeus's worked example for 1992 October 13 (P 26.27, B0 5.99, L0 238.63).

A real white-light disc. A phone video of the Sun on 2025-09-17 (2025-09-17-161400-Solar.mp4, 1080 by 1920, disc radius 402 px) stacked at 6, 12, 25 and 50% of the frames gives 7 groups and 14, 14, 13 and 13 spots: Wolf number 84, 84, 83 and 83. It has a dark region at the bottom of the disc (a gradient across the frame, 37 600 MSH) which is left out by the size limit and which would otherwise have been the biggest "group". The spot areas are 17 to 326 MSH, and a group at central meridian distance +1.7 degrees is seen at the same size as it is corrected. There is no reference count for that day among the captures, so the Wolf number is not compared with the official series, which needs a calibrated k for each observer and instrument.

Limits

  • The count depends on resolution and seeing: small spots and pores are missed, and k for your telescope is something to calibrate against the published series.
  • White light only.
  • The group definition is a distance, not the Zurich classification.
  • The positions assume the image is orientated as you say (see above) and the limb fit is a circle, so a clipped or out-of-round disc gives a worse position.
  • The Sun's ephemeris is a series good to about 0.01 degree, ignoring the minute between UT and terrestrial time.

Reference: every option

SettingCLIAPI (config.sunspots)AppRangeDefault
Find sunspotshelia stack --sunspots; helia sunspots IMAGEsunspots (an object; {} for the defaults)Stack step, Sunspots: Off / Findon or offoff
Umbra threshold--sunspot-umbraumbranot offeredabove 0 and below the penumbra0.55
Penumbra threshold--sunspot-penumbrapenumbranot offeredabove the umbra, below 10.88
Smallest spot (MSH)--sunspot-min-areamin_areanot offered0 to max_area2
Largest spot (MSH)--sunspot-max-areamax_areanot offeredabove min_area6000
Group gap (degrees)--sunspot-group-gapgroup_gap_degnot offered0 to 903
Observer factor k--wolf-kwolf_kObserver factor k (0.1 to 5)above 01
North angle (degrees, counter-clockwise from the top)--north-anglenorth_angle_degnot offeredany0
Celestial north is up--celestial-north-upcelestial_north_upCelestial north is upon or offoff
Mirror image--mirrormirrornot offeredon or offoff
Time of the image (UTC)helia sunspots --date TIME or --capture FILE.ser (a FITS file's DATE-AVG or DATE-OBS otherwise); in stack the capture'sthe capture'sthe capture'sYYYY-MM-DDThh:mm:ssnone
Outputs--csv FILE, --json FILE, --overlay FILE; stack writes NAME-sunspots.csv, .json, .pngsummary.sunspots, GET /jobs/{id}/sunspots.csv, GET /jobs/{id}/sunspots.pngShow overlay, Save table and overlay

Thresholds are shares of the quiet disc's brightness above the sky after the limb darkening is flattened. A value outside its range is 422 invalid_config in the API and an error on the command line.

Tutorial: counting sunspots on a white-light stack

This walks through a real capture from start to finished table: a white-light video of the Sun taken on 2025-09-17 (2025-09-17-161400-Solar.mp4, a phone through a solar filter, 1080 by 1920 pixels, the disc 402 pixels in radius). You need Helia 0.5 or later and one white-light capture with the whole disc in the frame. The method is described in Sunspots, groups and the Wolf number.

1. Stack the capture

In the app, open the file, choose the White light filter in the Open step, keep the defaults (the best 25%), and press Stack. On the command line:

helia stack 2025-09-17-161400-Solar.mp4 -o solar.tif --preset white --channel g

White light stacks the green channel: it is the sharpest and the least affected by atmospheric dispersion. Do not sharpen yet: the detection works on the flattened disc, and sharpening adds ringing at the limb and around spots. (A FITS output also carries the capture time.)

2. Find the spots

On the command line, with the capture time (UTC) so that the Sun's tilt is known:

helia sunspots solar.tif --date 2025-09-17T16:14:00 \
    --csv spots.csv --json spots.json --overlay spots.png

In the app, set Sunspots to Find in the Stack step before pressing Stack (the time comes from the capture if it has timestamps; without one there is no Carrington longitude), then use Show overlay and Save table and overlay. Leave the observer factor k at 1 for the raw count.

What you should see:

disc:        centre (554.5, 980.8), radius 401.7 px
7 groups, 14 spots; Wolf number 84.0 (k = 1)
area 1218 millionths of the hemisphere corrected (795 as seen), umbra 380
P 24.49, B0 7.16, L0 247.82 degrees

The overlay: penumbra in yellow, umbra in red, group numbers in green, the limb in cyan, an arrow to solar north

The overlay of this capture, cropped to the disc. Penumbrae are outlined yellow, umbrae red, the limb cyan and each group is numbered in green; the white line is solar north.

Check the overlay against the picture before you trust the numbers: every outlined region should be a spot you can see, and every visible spot should be outlined.

3. Read the table

spots.csv has a row per spot: area_msh is its corrected area in millionths of the visible hemisphere, projected_msh the area as seen, lat_deg and lon_deg its heliographic position (Carrington longitude, from the time you gave). The spot at central meridian distance -73.8 degrees is seen at 98 MSH but is 326 MSH on the Sun: that is the foreshortening correction at work. A spot near the middle of the disc (mu near 1) has the two areas equal. The full column list is in the user guide.

4. If the north is not up

By default solar north is taken to be at the top of the image. If your camera is elsewhere, latitude and longitude will be wrong (counts and areas are not): add --north-angle DEG (counter-clockwise from the top), or --celestial-north-up for an equatorial mount with the image north up, or --mirror for a mirror image. The app offers only Celestial north is up; use the command line for other angles.

What to expect and what can go wrong

What you seeWhyWhat to do
Hundreds of tiny "spots" over the whole discNot white light (H-alpha and calcium have dark filaments and plage), or a very noisy stackUse a white-light stack; raise --sunspot-min-area
A big false group along one side of the discA cloud, vignette or uneven flat: dark regions over 6000 MSH are dropped with a note, smaller ones are notRe-stack on a clear frame range, or lower --sunspot-penumbra
A visible pore is missingPores under 4 pixels or 2 MSH are not countedLower --sunspot-min-area
Two groups that are really oneThe groups are further apart than 3 degreesRaise --sunspot-group-gap
"no clear solar disc"The whole disc is not in frame, or the contrast is too lowFrame the whole disc
Wolf number differs from the official onek is 1 and groups are drawn by distance, not Zurich classCalibrate k against the published series for your instrument

The result is stable across the stack: keeping 6, 12, 25 or 50% of the frames of this capture gave 7 groups and 14, 14, 13 and 13 spots (Wolf 84, 84, 83, 83).

GPU acceleration

Helia does its heavy maths on the GPU when there is one, through wgpu, a Rust library that speaks Vulkan, DX12, Metal and OpenGL. There is nothing to install beyond your normal graphics driver, and no vendor-specific toolkit: the same code runs on AMD, NVIDIA, Intel, Apple and Raspberry Pi graphics.

Backends

PlatformAPIs wgpu usesNotes
LinuxVulkan, then GLESNeeds a Vulkan driver for your GPU (Mesa for AMD, Intel and Raspberry Pi; NVIDIA's own driver).
WindowsDX12, VulkanProvided by the graphics driver.
macOSMetalBuilt in.

Choosing between GPU and CPU

The --backend option of helia stack (and the Backend setting in the desktop app, and backend in an API job) takes three values:

ValueBehaviour
auto (default)Ask wgpu for the best hardware adapter. If it finds none, or only a software adapter, use the CPU path.
cpuAlways use the CPU.
gpuUse a GPU. If there is no real GPU, fall back to wgpu's software adapter; if there is no adapter at all, fail with an error.

Helia prints which one it chose. On the command line it is the backend: line of the summary, for example backend: GPU (<adapter> (Vulkan, DiscreteGpu)) or backend: CPU (16 threads). The desktop app shows it in a chip in the top bar, and the API reports it under backends in GET /api/v1/version.

The CPU path

The CPU path is not a poor relation. It is the specification: the GPU kernels are tested to give the same results as the CPU code (see The GPU parity rule). It is multi-threaded and vectorised, so a machine without a usable GPU still stacks, only more slowly, and the output is equivalent either way.

What runs on the GPU today

  • Colouring the finished stack through a colour preset: a table lookup with linear interpolation, one thread per pixel. It matches the CPU reference to within 1e-5 per sample.
  • Sharpening and the everyday edits (levels, curves, unsharp mask, a-trous wavelets and denoise, Richardson-Lucy deconvolution): separable convolution passes, element-wise kernels and, for an Airy blur, a direct 2-D convolution. The image stays on the device across a run of these operations. The noise level of a wavelet layer is estimated from a few thousand sampled pixels, which are the only values that come back to the host in between. They match the CPU reference to within 1e-4 (about 1e-6 observed); the live preview in the app uses them. Crop, rotate, flip and resize run on the CPU.
  • Scoring every frame, straight from the stored bytes: decode, debayer, blur and the sharpness reduction all run on the device.
  • Global registration by phase correlation, with Helia's own mixed-radix FFT (lengths up to 2048) written in WGSL.
  • Alignment point correlation.
  • Stacking.

Frames are sent in batches with two in flight, so uploading the next batch overlaps work on the current one.

The robust stacking modes (--robust sigma|huber|winsor) run on the GPU for the whole-frame stack, in two passes; with alignment points their per-point tiles still combine on the CPU. Still on the CPU: registration of captures whose transform length would exceed 2048, and, by design, fitting the correlation peaks and interpolating the shifts. The GPU returns the correlation surfaces, and the same CPU code makes the decision on either path, so the CPU and GPU paths give the same answer (registration agrees to about 1e-5 px).

Measured speed

Wall-clock seconds for helia stack with --keep 25%, end to end. Measured 2026-10-18 on a laptop: 16-thread CPU and an NVIDIA RTX 3050 Ti Laptop GPU (4 GB, Vulkan), commit ae49ddc. The laptop GPU throttles when hot, so expect run-to-run variation of 10% or so.

CaptureFramesCPU, beforeCPU, nowGPU, beforeGPU, now
1920x1080 16-bit Bayer SER1008.32.913.21.6
1920x1080 8-bit mono SER63131.115.040.85.8
720x540 8-bit Bayer AVI100015.55.420.52.9
1920x1080 8-bit Bayer AVI63070.519.273.110.7

"Before" is the first release of the stacker, where only scoring and accumulation ran on the GPU. Use helia stack --timings to see where the time goes on your own machine.

On a data-centre GPU (NVIDIA A100) these small captures were slower than on the same machine's CPU, which points at start-up and batch size rather than compute; that is being looked at (#10).

Software adapters

A software adapter is a GPU implemented on the CPU: WARP on Windows, lavapipe (Mesa) on Linux. They exist so that programs run on machines with no GPU, and so that Helia's automated tests can exercise the GPU code on ordinary build servers. They are slower than Helia's own multi-threaded CPU path.

How wgpu ranks adapters matters here. Helia asks for the high-performance choice, and wgpu orders what it finds as discrete GPU, integrated GPU, other, virtual GPU and, last, a CPU-hosted adapter. auto skips a CPU-hosted adapter altogether, so a Linux machine with no GPU but with Mesa's lavapipe installed uses Helia's faster CPU path (backend: CPU (...) in the summary). --backend gpu still accepts a software adapter when there is no real GPU, which is mainly useful for testing. Helia's own tests ask for the software adapter explicitly (see Building and testing).

Troubleshooting

The summary says backend: CPU and you expected a GPU.

  • Linux: install the Vulkan loader and your driver (sudo apt-get install libvulkan1 mesa-vulkan-drivers, or NVIDIA's driver), then check that vulkaninfo --summary (from the vulkan-tools package) lists your GPU. If it does not, Helia cannot see it either.
  • Windows: update your graphics driver from the manufacturer. Remote desktop sessions and some virtual machines hide the GPU; run Helia on the local console.
  • macOS: nothing to install; Metal is built in.
  • A laptop with two GPUs: Helia asks for the high-performance one. If the system gives it the integrated GPU, use your system's graphics settings to run Helia on the discrete GPU.
  • Run helia stack ... --backend gpu. Unlike auto, it reports an error when no adapter can be opened, and the message says why.

"no suitable GPU adapter found" or "could not open GPU device". These come from --backend gpu. The first means no adapter of the requested kind exists; the second means an adapter exists but refused to open (a driver problem). Updating the driver is the usual fix.

The GPU is used but the result differs very slightly from the CPU. That is expected. Floating-point sums are not bit-for-bit identical across devices; the tests bound the difference (see the parity page).

A bug that only happens on the GPU. Re-run with --backend cpu. If the CPU result is right, report it with your operating system, GPU, driver version and helia --version (see Contributing).

Sending feedback

Helia is a pre-release, and the quickest way to make it better is to tell us what is wrong or missing. There is a Feedback button in the top bar, at every step, and a matching command for people without a display.

The top bar: the Pre-release badge beside the name, and the Feedback button

Privacy first

  • Nothing is sent until you press Send. Opening the page, drawing on the screenshot and typing send nothing, and nothing you type is saved to disk.
  • Helia has no telemetry. It never sends usage, crash or error reports of its own accord. The only network request Helia ever makes about feedback is the one you start with Send (or with helia feedback).
  • It goes to the Helia developers. Your feedback is sent to them and filed in the Helia issue tracker, which may be visible to more people than the developers, now or later. Look at the screenshot before you send it: it is a picture of the Helia window, and the window can show the file name of your capture, your GPU and anything else on the screen inside it. Untick Include the screenshot if in doubt.
  • You see exactly what will be sent. Preview what will be sent shows the report as text and the screenshot with your rectangles on it.
  • System details are optional and limited. If you tick Include system details, the report carries the Helia version, commit, build date and target, the Rust and wgpu versions, your GPU adapter, your operating system, and the open capture's file name and header summary (size, bit depth, frame count). It never carries image data, a folder path, your name, a host name or an address.

In the app

Press Feedback in the top bar, or Ctrl+Shift+F. (The About page has a Send feedback button too. F1 still opens the About page.) Helia takes a picture of its own window and then shows the feedback page in place of the viewer.

The feedback page: the screenshot with a rectangle drawn round the problem, the kind of feedback, a title and description, and the checkboxes

  1. Highlight the problem. Drag on the screenshot to draw a rectangle round it. Draw as many as you need; Undo removes the last, Clear removes them all. If the screenshot would show something private, untick Include the screenshot.
  2. Choose the kind: Bug, Feature request, Question or Other.
  3. Write a title (one line, up to 120 characters) and a description (up to 6500 characters; the rest of the 8000 is kept for the system details).
  4. Decide about system details and look at Preview what will be sent.
  5. Press Send.

Sending runs in the background and the window stays usable. A notice says Preparing your feedback (Helia asks the service for a small puzzle and solves it, which takes well under a second; it keeps casual spam out) and then Sending your feedback, with a Cancel button. When the service has filed the issue the page shows its number as a link, and a notice confirms it.

Press Esc or Close to leave the page; what you typed is kept until you send it or quit.

When the service cannot be reached

If the feedback service is down, busy or unreachable, or you are offline, nothing is lost. The page says what happened and offers:

The feedback page after a failure, with Save report, Open in browser and Edit and try again

  • Save report writes a .zip with report.json (including your highlights as numbers), screenshot.png (the screenshot with your rectangles drawn on) and screenshot-clean.png. Attach it to an issue yourself, or send it to the author.
  • Open in browser opens the project's new-issue page with the title and text already filled in. It only works if you have an account on the project's issue tracker, and the screenshot cannot be attached from here, so attach screenshot.png from the saved report.
  • Edit and try again returns to the form.

A bug report for an issue

If you would rather open an issue yourself, Save bug report on the About page writes a .zip of text only (version, GPU, the open capture's header, settings and the end of the log) to attach to it. See Log file and bug report.

From the command line

helia feedback --kind bug --title "Stack fails on my AVI" \
    --description "Fails at the alignment stage with ..." \
    --screenshot problem.png --highlight 40,60,200,120 --system-info

--kind is bug (the default), feature, question or other. --description - reads the text from standard input. --screenshot takes a PNG (check it first: it is filed in the issue tracker) and --highlight x,y,w,h draws a rectangle on it, in the screenshot's pixels; give it more than once for more rectangles. --system-info adds the version, commit, target and operating system. --dry-run prints exactly what would be sent and sends nothing. If the service cannot be reached, --save report.zip keeps the report, and the command prints the pre-filled new-issue address. See the CLI reference.

Changing the service address

Normally you do not need to. For a private or test service, set the environment variable HELIA_FEEDBACK_URL, or put feedback_url=https://... in the app's settings file, or use --url on the command line. The variable wins over the file, and --url wins over the variable. The address must be HTTPS (plain HTTP only for localhost).

For developers

The wire format, the proof of work and the answers are in the feedback protocol.

Troubleshooting and FAQ

When something looks wrong, run helia info on the capture first. It prints every field of the file's header, what Helia will use instead where the two differ, and why. It reads the header and, for a SER file that says it is mono, a few frames to check the pixels. Most of the problems below show up there.

Opening captures

"file truncated: header needs N bytes, stream has M"

A SER file records its frame count in the header. If the recording stopped badly (a crash, a full disk, a copy that did not finish), the file holds fewer frames than the header promises, and Helia refuses it rather than guess. Copy the file again. If it is genuinely cut short, this version of Helia cannot read it.

"ffmpeg was not found"

You opened an MP4, MOV, M4V, MKV or WebM file, or a compressed AVI, and Helia could not find ffmpeg. Those formats are read through the optional ffmpeg program. Install it (the message lists the command for each system, and Installation has them too), open a new terminal so PATH is refreshed, and try again. If it is installed somewhere unusual, set HELIA_FFMPEG and HELIA_FFPROBE to the full paths of ffmpeg and ffprobe.

"ffmpeg is too old"

Helia needs ffmpeg 4.4 or newer. Update it with your package manager, or use a static build from ffmpeg.org and point HELIA_FFMPEG and HELIA_FFPROBE at it.

"the video is truncated or damaged"

A recording that stopped badly often has no index (an MP4's moov box is written at the end) or ends mid-frame. Helia reports it rather than stacking a partial video without telling you. Try recovering the file with ffmpeg -i broken.mp4 -c copy fixed.mp4, or record to SER. A few frames missing at the very end of a video (up to 1 per cent) are tolerated.

"decoding needs about N MiB of free space"

Compressed video is decoded once into an uncompressed file in a cache directory, and that needs room for every frame at full size. Free some space, point --cache-dir (or the HELIA_CACHE_DIR environment variable) at a bigger disk, or open a shorter clip. The file is removed when Helia is done; if Helia was killed, a leftover helia-video-*.ser file older than a day is deleted the next time a video is decoded.

"unsupported AVI video format"

Helia reads uncompressed AVI itself (BI_RGB, Y800/GREY, Y16, 24-bit BGR). An AVI with another codec, for example MJPG, goes to ffmpeg like an MP4. If you still see this message, the file is not a plain AVI at all: run helia info on it. See Capturing for Helia.

My AVI is huge, or reported as having the wrong number of frames

Files over 1 GB are written as a chain of OpenDML sections. Helia supports them: it finds the frames by walking the file's own chunks, not by trusting the index at the end, because capture programs often write that index wrongly or leave it out. Run helia info and compare the frame count with what your capture program said. If helia info is wrong for a file that plays elsewhere, please report it with the file's first few hundred megabytes if you can.

SharpCap SER files and the byte-order flag

A 16-bit SER file has a header flag that says whether the pixel bytes are little-endian or big-endian. SharpCap files have been seen with the flag cleared (big-endian) while the data is really little-endian, which makes the image look like harsh noise. Helia checks: when the flag says big-endian it looks at the first frame, and decodes it the way that gives a smooth image. helia info shows both sides, the flag as written under the header and the order used under Helia will use:

  LittleEndian:      0 (big-endian)
  Byte order:        little-endian  (detection)

If the two differ, Helia has corrected a mislabelled file and nothing needs doing. Data that really is big-endian is left alone. If Helia gets it wrong (the picture is harsh noise), say which with --endian little or --endian big.

The header gets the bit depth wrong, or the picture is upside down

A recorder can also write the wrong bit depth into the header. The usual sign is that helia info or the app refuses the file as truncated, or that the picture is striped or looks like noise. Use --bit-depth 8 or --bit-depth 16 to say what the pixels really are (any value from 1 to 8 reads one byte per pixel, 9 to 16 two). A file stored with the bottom row first (rare in SER) is read the right way up with --flip-rows. All of these apply to SER files, and the same choices are in the Open step of the app and in the API (bit_depth, endian, flip_rows). See Capturing for Helia.

A grid or chessboard in the stack (a Bayer mosaic that was not removed)

A raw colour camera records one colour per pixel in a repeating 2 x 2 pattern. The picture has to be demosaiced (debayered) before it is stacked. If it is not, the stack carries the pattern: a fine grid or chessboard, with neighbouring pixels alternating between bright and dark. Behind a narrow-band filter such as H-alpha it is stark, because the blue pixels receive almost no light.

The usual cause is a SER file whose header says mono (ColorID 0) although the frames are raw Bayer data. SharpCap does this for some colour cameras (a ZWO ASI482MC, for example). Helia no longer trusts such a header blindly. For a SER file that says mono it measures the four pixel positions of a 2 x 2 cell on the solar disc in a few frames; if they differ by 5 per cent or more in a way a colour-filter array would produce, it reports a mosaic, names the pattern and debayers it. It says so:

warning:  The SER header says mono (ColorID 0), but the frames hold a Bayer mosaic
that looks like RGGB: the four pixel positions of a 2x2 cell ... Helia will
debayer them as RGGB. ...

The desktop app shows the same message above the Quick stack card in the Open step, and the API returns it in notices (see The API). Nothing is changed silently: helia info and the app's Helia will use list give the Bayer pattern with its source (detection), and the choice is recorded in the output file.

If a stack made before this check shows the grid, stack it again.

If the check is wrong, tell Helia what you know:

helia stack capture.ser -o out.tif --bayer none     # it really is mono
helia stack capture.ser -o out.tif --bayer bggr     # a different pattern

Two cases need your eye:

  • Red and blue swapped. The geometry of the mosaic says which diagonal is green but not which of the other two pixels is red. Helia settles it from the layout a SharpCap sidecar states, the colour the filter passes (red for H-alpha, blue for calcium K), the usual layout of the camera model, and finally by taking the dimmer of the two as blue. The message says which. For a stack of a single channel (the Ha preset takes the red one) a wrong pick matters; if the result looks like noise, try the other pattern of the same family (rggb or bggr, grbg or gbrg).
  • A mosaic too faint to see. If the colours are balanced so that all four pixel positions are within 2 per cent of each other, the data is called mono; between 2 and 5 per cent Helia cannot tell, and uses the sidecar's colour space if there is one. Give --bayer yourself.

Colours are wrong, or a colour camera looks like a grey mosaic

Raw colour cameras store a Bayer mosaic. In a SER file the pattern is normally in the header (and see the previous section when the header says mono). An AVI cannot say whether 8-bit data is raw colour or plain mono, so for SharpCap AVI files Helia reads the CameraSettings.txt beside the video (same name, .CameraSettings.txt in place of .avi). helia info shows the sidecar it found, its colour space and its Debayer Preview, or no sidecar rows.

If there is no sidecar or the guess is wrong, say what you have:

helia stack capture.avi -o out.tif --bayer rggb

The patterns are rggb, grbg, gbrg and bggr; none forces mono. For a bottom-up AVI the pattern means the picture the right way up. If the picture looks like a fine chessboard, or the colour planes are swapped, try the other patterns. Many ZWO colour sensors are RGGB, and Helia assumes RGGB for a raw colour sidecar. The pixel check described above runs for SER files only; an AVI has no header field to doubt, so its sidecar and --bayer decide.

H-alpha with a colour camera

The H-alpha line is red (656 nm). On a colour camera it reaches mainly the red pixels of the sensor, so the green and blue pixels carry little more than noise. Averaging the three planes mixes that noise in and softens the result. Helia uses the red plane for H-alpha by itself when it knows the filter: put Ha in the file name, or give --preset ha:

helia stack ha.ser -o ha.tif --preset ha

If Helia did not recognise the filter (the settings: line says filter not recognised), the default is all three planes; name the plane yourself with --channel r. In the app, the Quick stack card picks the filter and the channel with it; Advanced settings at the foot of the inspector has the channel buttons. For a white-light colour capture, lum or g is usually the better choice. Mono cameras are unaffected by these options.

Helia picked the wrong filter, or none

Helia reads the filter from the file name, then the folder, then the SharpCap sidecar and the SER header (see Colourising). A whole word has to match, so rename the file to include Ha, CaK, Mg, Na or similar, or name the filter yourself: --preset on the command line, the filter buttons in the Quick stack card. A filter you choose always wins over the one detected.

The result

The stack looks soft

Answer these in order. Each is cheaper to fix than the next, and the first two decide how sharp the stack can be at all.

  1. How good were the best frames? A stack cannot be sharper than the sharp moments it is built from. Look at the Quality, sorted curve in the Analyse step. If the best frames are only a little better than the middle of the capture, the air was the limit. helia stack --seeing --aperture-mm 100 (give your aperture) prints the Fried parameter r0 and D / r0, and a grade: 10 cm or more is excellent, 7 to 10 good, 4 to 7 fair, 2.5 to 4 poor, under 2.5 bad. See Seeing estimation. Nothing later in the chain makes up for bad seeing.
  2. How many frames did you keep? Keep fewer: --keep 10%. If the sorted curve falls steeply, a smaller share helps; if it is flat, it will not. A lower D / r0 allows a larger share.
  3. Are the alignment points doing their job? Open the Align step and check how many points are active. Photospheric targets (white light, Na D) need small points, a few granules across: --ap-auto picks the size, spacing and search range from the image scale and the seeing, and --ap-passes 2 measures the shifts a second time against the first stack. Compare with --no-multipoint and try a different --ap-size. See How stacking works.
  4. Is it only the stack that is soft? A stack is smoother and cleaner than any single frame, but not as sharp as the telescope can be. Sharpen it: the Sharpen step, or --sharpen wavelet on the command line. Judge with Before / after and Full size, because the preview is a shrunk copy. The method, the sliders and the recommended values for each filter are in Sharpening and adjustments and the step-by-step Sharpen.

About the wavelets. Wavelet sharpening in Helia is an a-trous transform with a gain and a noise cut for each of six layers. Users coming from RegiStax have reported that they cannot reach the same sharpness with it, and suspect that the gains Helia allows on the fine layers are much smaller than the 10 to 100 times they use there. Making the wavelet controls behave like RegiStax 6, with comparable ranges, is in progress under issue #131 and is not finished. Until it lands, do not expect the same look from the same numbers, and do not read this page as a promise of how the result will look.

Sharpening left halos, ringing or grain

  • Sharpening can create artefacts. Use Before / after and ease off: lower the gains of the wavelet layers (layer 1 and 2 first), raise their noise cuts, lower the unsharp amount, or use fewer deconvolution iterations.
  • Ringing at the limb or round sunspots after deconvolution: keep the limb and sunspot guard at 1, and check that the blur (PSF) is not larger than the real one: too big a blur over-corrects.
  • Grain or speckle that looks like texture is noise that was boosted. Raise the noise cut of the finest layers, turn on Denoise in the Adjust step, or stop the deconvolution sooner.
  • The preview is a shrunk copy; press Full size to judge fine detail.

The red channel is clipped, or the disc is a flat white plate

This is issue #119, found on the H-alpha captures of 2022-07-17 and 2026-10-20 (a ZWO ASI482MC, AVI and SER files). Behind a narrow-band filter one colour plane of the sensor carries nearly all the light. H-alpha is red, so the red pixels do. An exposure long enough to show the faint leak of the line, or the prominences, pushes those pixels to the top of the sensor's range, and almost every lit pixel of the disc then sits at one value: 96 to 100% of them, 255 in the AVIs and 203 in the SER files. A plane that is flat holds no detail, so a stack of it is a white disc with prominences round it and no filaments, plage or fibrils. The green plane, which carries the leak of the line at 40 to 60% of full scale, has all of that. Measured on 15_37_19.avi, the granulation-band contrast of the stack is 0.14% from the red plane and 1.39% from the green. A white-light capture exposed too long does the same, to a lesser degree (14_42_57.avi: red 50% and blue 21% clipped).

What Helia does about it (the fix, pull request #125, is on main and comes in the release after 0.5.0): before stacking, helia stack and API jobs read six frames, find the planes that are mostly flat at their own ceiling and stack the brightest plane that is not. Empty planes (blue behind a red filter) are never chosen. A note in the settings says what happened:

the red plane is clipped (red 98% of its lit pixels sit at the sensor limit, so
the disc is flat there); stacking the green plane instead. ...
  • A channel you name is never changed. --channel r stacks red, clipped or not. Leave --channel off to let the fallback work.
  • The desktop app does not do it yet. Its Advanced settings keep explicit channel buttons: choose G yourself when the red result is a flat plate.
  • If every usable plane is clipped, the note says so and nothing is replaced: the exposure is too long for any of them.
  • The cure is at the telescope: a shorter exposure or a lower gain, so the histogram of the bright plane stays clear of the right-hand edge. See Exposure, gain and bit depth.

A burned-in timestamp, or a label, shows in the result

Helia finds a burned-in timestamp (the date and time that SharpCap and other programs draw into the picture) by itself, keeps alignment and quality scoring off it, and replaces it with a flat patch of sky in the result. The command line summary lists what it masked (masked:), and the points off the disc and off the text are simply not used. See Keeping alignment off text and sky.

If some other text or a mark is still in the way, give its rectangle with --exclude x,y,w,h (pixels from the top-left corner; repeat for several), or add it under Exclude regions in the Align step of the app. If Helia masks something it should not (it only looks for small, bright, changing areas at the edge of the frame), switch the automatic detection off with --no-overlay-detect. If the disc fit goes wrong and alignment points are missing on a real disc, use --no-disc-mask, or --prominence-band to let points sit a little outside the limb.

Nothing improves, or the alignment points are all inactive

The point count in the app (and points: in the command line summary) says how many points were bright and detailed enough to measure. Very low-contrast frames, or too small a point size, leave few active points. Try a larger --ap-size. If almost none are active, check the capture: an exposure so short that the frame is nearly black, or a sensor region with no disc, leaves nothing to lock on.

GPU

Helia says backend: CPU, or cannot find my GPU

See the troubleshooting section of GPU acceleration. The short version is: check your driver, then try --backend gpu to get an error message that says why.

Reporting a problem

Open About and choose Save bug report, then send the .zip to the author (via 12hobbies.studio). It has the version, your GPU, the open capture's header and the end of Helia's log, all as text you can read first. The log itself is helia.log; where it lives, and what it does and does not record, is in Log file and bug report.

Questions

Does Helia need a GPU? No. It uses one when it finds one and otherwise runs on the CPU with the same results.

Does Helia use the network? Only when you send feedback. The stacker and the desktop app otherwise contain no code that makes network connections; the feedback client connects to the feedback service when you press Send (or run helia feedback) and at no other time. helia serve listens on the address you give it, on the loopback interface unless you say otherwise; see The API for its security model.

Does it modify my captures? No. It only reads them. Output files are written where you say, and the command line overwrites an existing output file without asking.

Can it stack colour? A colour camera capture is stacked as one plane (--channel, which Helia chooses from the filter), because narrow-band solar data is one colour of light. The stack is mono. Helia can then colour it for sharing with a look for each filter (orange for H-alpha, violet for calcium and so on), into a 16-bit RGB TIFF or an 8-bit PNG; FITS stays mono. See Colourising. A true three-colour stack of a white-light capture is not built.

Can I calibrate with darks and flats? Yes: see Calibration and Capturing for Helia. Build masters with helia calibrate, or name the captures in the Calibrate step of the app.

Where do I report a bug? Press Feedback in the desktop app (Ctrl+Shift+F), or run helia feedback: it files it in the issue tracker for you, with a screenshot you can draw on, even if you have no account on the forge. See Sending feedback. Or use the project's issue tracker directly, with your operating system, GPU and driver, the output of helia --version, and if you can a short capture (a few hundred frames) that shows the problem.

Frequently asked questions

Short answers, with a link to the page that has the whole story. Problems with a particular message or picture are in Troubleshooting.

Getting it

Where do I download it? Releases are not public yet: Helia is in early access, so contact 12hobbies.studio. See Download and verify.

Is it free? Yes: GNU General Public Licence version 3 or later, with additional terms under section 7 that keep the copyright line and the dedication. See About.

Which systems? Windows 10 and 11 (x86_64), Linux x86_64 and aarch64 (including Raspberry Pi 4 and 5 on a 64-bit system) are published. macOS and Windows on ARM are built from source. See Installation.

Why does it say pre-release? It is below version 1.0. The features in this manual work and are tested; some planned ones (derotation, drizzle) are not built yet. See the roadmap.

Using it

Do I need a GPU? No. It uses one when it finds one and otherwise runs on the CPU with the same results. See GPU acceleration.

What do I record? SER, 16-bit if possible, short exposures, many frames. See Capturing for Helia.

Can I use a phone video? Yes, with ffmpeg installed. See MP4 from a phone.

How many frames should I keep? The recommended share for the filter (25 or 30 per cent) is a good start. Fewer is sharper and noisier. See Stack.

Which channel, or band, should I use for my filter? Let Helia choose: it reads the filter from the file name, folder or sidecar and applies the preset below. The choice follows which pixels of a colour sensor the light reaches. On a mono camera none of this applies.

FilterColour plane stackedFrames keptWhy
H-alphared25%656 nm is deep red; green and blue pixels add noise, not detail
Ca II K, Ca II Hblue30%violet light reaches the blue pixels; the line is faint
Mg I bgreen30%517 nm is green; the line is dim
Na I Dgreen12%the filter leaves the blue pixels empty, and averaging them in prints the mosaic over the granulation; green is two pixels in four
He I D3all three combined25%587.6 nm is shared between red and green
H-betablue30%486 nm reaches the blue pixels; faint
White lightall three combined12%the continuum carries all three colours; shows granulation

Name another plane with --channel r|g|b|lum. A plane that turns out to be clipped is replaced by the next one (see The red channel is clipped), unless you named it. The reasons, with the numbers, are in Colour presets. One caution: the 2022 captures that were first taken for sodium were later identified, from their plane statistics, as H-alpha (red clipped, blue empty, prominences in red; pull request #128, which corrects those labels). The sodium choice of green therefore rests on those captures, not on a real Na D filter, and no true sodium capture is in the test set yet. If your Na D result looks wrong, try --channel lum and --channel r and compare.

My image looks soft. Check the seeing, then the share of frames kept, then the alignment points, then sharpen. See The stack looks soft. Wavelets that behave like RegiStax's are in progress under issue #131; Helia does not promise the same look yet.

The red channel is clipped, or the disc is a flat white plate. The exposure was long enough to push the red pixels to the sensor limit. Helia stacks the next plane by itself unless you named one. See The red channel is clipped.

Which sharpening method? Wavelets, at the recommended values, almost always. See Sharpen.

Does it change my captures? No. It only reads them.

Can I get the picture without colour? Yes: pick Mono in the Colour step, or export FITS, which is mono for a filter stack (and a red, green, blue cube for a true-colour one).

What is the colour, really? A convention. Narrow-band solar data is one colour of light; orange for H-alpha, violet for calcium and so on are for sharing. See Colourising.

Why is the FITS not sharpened? It is for measurement and further processing. Turn on Edit the FITS too (or --sharpen-fits) if you want it sharpened.

Privacy and network

Does Helia use the network? Only when you send feedback. The stacker and the desktop app otherwise make no network connections of their own; links open in your browser only when you click them. The feedback client, in the app and as helia feedback, connects to the feedback service when you press Send (or run the command) and at no other time. helia serve listens where you tell it, on the loopback interface unless you say otherwise. See Send feedback.

Does it send usage data? No. There is no telemetry.

Problems

A grid across the picture. Fixing a Bayer grid.

It picked the wrong filter. Press the right one on the Quick stack card, or add --preset. See Troubleshooting.

The sharpening looks overdone. Turn it down: it is. See Troubleshooting.

Where do I report a bug? See Send feedback.

Command line reference

Every subcommand also accepts -h and --help; helia itself also accepts -V and --version. Exit status is 0 on success and 1 on any error, with the message on standard error.

helia

Solar lucky-imaging stacker

helia <COMMAND>

Subcommands:

  • stack: Stack a capture into a single sharp image
  • calibrate: Build master dark, flat and bad-pixel files from calibration captures, for helia stack --dark --flat --bad-pixels
  • edit: Sharpen and adjust an existing TIFF, PNG or FITS image
  • info: Print what a capture file contains: every header field as written, what Helia will use instead where it differs, and why
  • measure: Measure how much granulation an image holds: the band-pass contrast (standard deviation over mean) of a disc-centre crop in three bands, for comparing settings on the same capture
  • sunspots: Find the sunspots on a stacked white-light image: groups, the relative Wolf number and areas corrected for foreshortening, as a table, JSON and an overlay picture
  • serve: Run the HTTP/JSON API server
  • presets: List the filter presets (helia stack --preset ...)
  • lut: Write a filter's colour table as a file other programs read (.cube, .csv, ImageJ .lut or a PNG strip), so a picture coloured elsewhere matches one coloured by Helia
  • feedback: Send feedback (a bug, a feature request or a question). It is sent to the Helia developers and filed in the Helia issue tracker, through the feedback service

helia stack

Stack a capture into a single sharp image

helia stack [OPTIONS] --output <OUTPUT> <INPUT>

Arguments:

  • <INPUT>: Capture file: .ser or .avi, or a compressed video (.mp4, .mov, .mkv, .webm, .m4v) read through the optional ffmpeg program (required)

Options:

  • -o, --output <OUTPUT>: Output image; the extension picks the format (.tif/.tiff, .png or .fits/.fit/.fts) (required)
  • --keep <KEEP>: Share of frames to keep, e.g. 25%
  • --keep-frames <N>: Number of frames to keep
  • --ap-size <AP_SIZE>: Alignment point size in pixels. Default: chosen from the image size
  • --ap-auto: Choose the alignment point size, spacing and search range from the image scale (the fitted limb) and the seeing (a probe of a dozen frames), instead of one size for every capture. What was chosen is printed
  • --ap-passes <N>: Alignment point passes: 1 measures the points against the reference built from the best few frames, 2 stacks once and measures them again against that stack (less noise, more detail to lock on). Needs multipoint. Default: 1
  • --ap-smooth <on|off>: Replace alignment point shifts that disagree with their neighbours (a mismatch on a neighbouring granule) by the neighbours' median. Needs multipoint. Default: off
    • on: Enabled
    • off: Disabled
  • --ap-residual: Measure the alignment points once more against the finished stack and print how far the shifts that were used are from that (px RMS): a check of the registration, at the cost of one more measuring pass
  • --autocrop: Crop the result to the solar disc plus a margin (FITS too). A capture without a clear disc is left as it is, with a note
  • --seeing: Estimate the seeing from the capture: the Fried parameter r0 from the spectral ratio of tiles of the disc over a sample of all the frames (needs --aperture-mm), and a relative index from how much neighbouring alignment points move against each other. Printed after the stack and written into the FITS header. Reads about 120 more frames
  • --sunspots: Find the sunspots on the stack: groups, the relative Wolf number, areas corrected for foreshortening. Printed after the stack, written into the FITS header, and saved beside the output as NAME-sunspots.csv, .json and .png. For a white-light capture with the whole disc in view; the time is the capture's
  • --sunspot-umbra <SHARE>: Umbra threshold: pixels darker than this share of the quiet disc (after the limb darkening is flattened out) are umbra (default 0.55)
  • --sunspot-penumbra <SHARE>: Penumbra threshold: pixels darker than this share of the quiet disc are spot. Raise it to take in fainter penumbrae and pores (default 0.88)
  • --sunspot-min-area <MSH>: Smallest spot to keep, in millionths of the visible hemisphere (default 2)
  • --sunspot-max-area <MSH>: Largest spot, in millionths of the hemisphere; a dark region bigger than this (a cloud, a vignette) is left out (default 6000)
  • --sunspot-group-gap <DEG>: Spots whose edges are closer than this on the Sun, in degrees, form one group (default 3)
  • --wolf-k <K>: Observer factor k of the Wolf number R = k (10 g + s) (default 1)
  • --north-angle <DEG>: How far solar north is turned counter-clockwise from the top of the image, in degrees. Default 0: solar north up (default 0)
  • --celestial-north-up: The image has celestial north up (an equatorial mount): solar north is then turned by the position angle P of the Sun at the capture time, added to --north-angle
  • --mirror: The image is a mirror image (west is on the left)
  • --wavelength-nm <NM>: Wavelength in nanometres for --seeing when the filter is not known (the filter's own wavelength is used otherwise)
  • --aperture-mm <MM>: Telescope aperture in millimetres. With it, the granulation report of a white-light or sodium stack also gives the diffraction cut-off and how much of it the data resolves, and --seeing gives r0 and a grade
  • --autocrop-margin <PERCENT>: Margin of --autocrop as a percentage of the disc radius (default 10)
  • --preset <FILTER>: The filter the capture was taken with (ha, cak, cah, mg, na, he-d3, hbeta, white): applies its recommended channel and frame share and colours a .tif or .png output. Without it the filter is read from the file name, folder or sidecar when possible; see helia presets
  • --colour <COLOUR>: Colour of the output: auto (the preset), mono, a preset id, or true (true colour from a raw colour camera: red, green and blue stacked on one alignment, channels aligned and white balanced, written as an RGB TIFF or PNG or a three-plane FITS). The other looks colour a .tif or .png from the mono stack; FITS output is mono for them
  • --invert: Invert the picture before colouring (shows filaments and prominences)
  • --white-balance <BALANCE>: White balance of --colour true: auto (make the disc centre neutral), off (the sensor's own balance) or three gains R,G,B such as 1.9,1,1.5 (default auto)
  • --no-channel-align: With --colour true, do not register red and blue to green (atmospheric dispersion). On by default
  • --colour-strength <S>: Mix of colour and grey from 0 to 1 (default 1)
  • --site <LAT,LON,ALT>: Observing site as LAT,LON,ALT (degrees north, degrees east, metres), for a FITS output's OBSGEO-X/Y/Z. Without it the HELIA_SITE environment variable is used, else the keywords are left out
  • --align <ALIGN>: How whole frames are aligned: auto (fit the solar limb when the full disc is in view and the fit is confident, else phase correlation), phase (phase correlation, best for a cropped region) or limb (fit the limb; frames without a usable limb still use phase correlation) (default auto)
  • --sharpen-fits: Apply the sharpening and other edits to FITS output too. By default a FITS file is the unsharpened stack, for further processing elsewhere
  • --sharpen <METHOD>: Sharpening: wavelet (Registax-style layers, the usual choice), unsharp, rl (Richardson-Lucy deconvolution) or none. The default is none
  • --unsharp-radius <PX>: Unsharp mask: Gaussian radius (sigma) in pixels
  • --unsharp-amount <A>: Unsharp mask: how much detail is added back (0.6 is gentle, 2 is strong)
  • --unsharp-threshold <T>: Unsharp mask: detail weaker than this (0 to 1) is left alone, so noise is not boosted
  • --wavelet-gains <G1,G2,...>: Wavelets: gain of layers 1, 2, 3 ... (comma separated; 1 leaves a layer alone, above 1 sharpens it). Layers not listed keep the recommended gain
  • --wavelet-noise <T1,T2,...>: Wavelets: noise threshold of layers 1, 2, 3 ... in noise sigmas (0 keeps all detail; 2 removes what is not clearly above the noise)
  • --rl-iterations <N>: Richardson-Lucy: iterations (more is sharper and noisier)
  • --rl-tv <L>: Richardson-Lucy: total-variation weight, 0 to 0.05 (keeps noise from becoming speckle)
  • --rl-psf <PSF>: Richardson-Lucy: the blur to undo. auto (from the image scale), gaussian:SIGMA, airy:RADIUS (first dark ring, pixels) or airy-scope:NM,MM,ARCSEC_PER_PX
  • --rl-guard <S>: Richardson-Lucy: strength of the limb and sunspot ringing guard, 0 (off) to 1
  • --flatten <SOURCE>: Divide the disc by its limb-darkening profile before anything else, so sharpening sees flat contrast: model (the fitted law) or profile (the measured azimuthal median). Outside the limb nothing changes
  • --limb-law <LAW>: The limb-darkening law to fit: linear, quadratic (the default) or neckel (Neckel and Labs, five terms). helia stack prints the fit
  • --prominence: Composite a stretched prominence layer with the disc: outside the fitted limb the scattered light is taken off and what is left is stretched to read like the disc, joined to it by a feathered edge. For H-alpha and the other lines with prominences. Runs after the sharpening
  • --prominence-strength <S>: How hard the prominence layer is lifted: 1 refers it to the brightest prominence found
  • --prominence-stretch <K>: The k of the asinh stretch of the layer; 0 is linear, larger lifts the faint parts more (default 6)
  • --prominence-level <SHARE>: Brightness of the brightest prominence as a share of the disc's (default 0.75)
  • --prominence-feather <SHARE>: Half-width of the feathered join at the limb, as a share of the disc radius (default 0.012)
  • --prominence-reach <RADII>: How far out the layer reaches, in disc radii (default 1.4)
  • --prominence-gate <SIGMA>: Noise sigmas taken off the layer before the stretch (default 3)
  • --prominence-offset <SHARE>: Move the join of layer and disc, as a share of the disc radius added to the fitted limb (negative inside the limb, default 0)
  • --prominence-disc-gamma <G>: Gamma of the disc's tone above the sky while the layer is on (default 1: the disc is left alone)
  • --prominence-layer-gamma <G>: Gamma of the stretched layer's tone (default 1; above 1 darkens its faint parts)
  • --denoise <STRENGTH>: Wavelet denoise; the strength is in noise sigmas for the finest layer (default: the filter's recommended value)
  • --denoise-layers <N>: Layers the denoise treats, from the finest
  • --black <V>: Levels: input value (0 to 1) that becomes black
  • --white <V>: Levels: input value (0 to 1) that becomes white
  • --gamma <G>: Levels: mid-tone gamma (above 1 brightens)
  • --curve <IN:OUT,...>: A tone curve through IN:OUT points, for example 0.25:0.15,0.75:0.85
  • --crop <X,Y,W,H>: Crop to X,Y,W,H in pixels (top-left origin)
  • --rotate <DEG>: Rotate clockwise by this many degrees (multiples of 90 are exact)
  • --flip-h: Mirror left to right
  • --flip-v: Mirror top to bottom
  • --resize <SIZE>: Resize: 50% or 0.5, 1200x800, 1200x (width) or x800 (height)
  • --edits <NAME|FILE>: Start from a saved edit preset (a name from the presets folder, or a .json file); the options above are added after it
  • --save-edits <NAME>: Save the resulting edit list as a preset under this name, for --edits
  • --no-multipoint: Skip multi-point alignment (global alignment only)
  • --exclude <X,Y,W,H>: Keep alignment and quality scoring off this rectangle, given as x,y,w,h in pixels (top-left origin); repeat for several. The area is flattened in every frame, so it is flat in the result too. Burned-in timestamps are found automatically; use this for anything else (a logo, a hot pixel cluster) (repeatable)
  • --prominence-band <PX>: Place alignment points up to this many pixels outside the fitted limb, so they can follow prominences (0 keeps them on the disc) (default 0)
  • --no-disc-mask: Do not fit the limb; alignment points may sit anywhere there is contrast
  • --no-overlay-detect: Do not look for burned-in text (a capture timestamp) to mask
  • --backend <BACKEND>: Compute backend (default auto)
  • --dark <PATH>: Master dark (FITS from helia calibrate, or any FITS or TIFF), or dark captures to build one from (a SER, AVI or FITS file, or a folder). Subtracted from every frame before it is demosaiced or aligned
  • --flat <PATH>: Master flat, or flat captures to build one from. Divided out of every frame after the dark
  • --displaced <PATH>: Captures of the Sun at many places on the sensor, to solve a flat from (see helia calibrate --displaced); used when no --flat is given. The dark is taken off first
  • --displaced-keep-smooth: With --displaced, keep every scale of the solution (see helia calibrate --displaced-keep-smooth)
  • --flat-dark <PATH>: Dark captures at the exposure of the flats, when --flat is captures
  • --bias <PATH>: Master bias (or captures). Only used with --scale-dark
  • --bad-pixels <FILE>: Bad-pixel map: bad-pixels.fits from helia calibrate, or a text file with an x y pair (from 0, top-left) per line. Without it, hot pixels of the dark and dead pixels of the flat are found and corrected
  • --no-cosmetic: Do not look for hot and cold pixels in the dark and the flat
  • --scale-dark: Scale the dark by the ratio of the exposures (the Sun's exposure comes from the SharpCap sidecar). Best with --bias
  • --calibration-combine <CALIBRATION_COMBINE>: How frames are combined into a master: median, sigma or sigma:K (default sigma)
  • --sigma-clip <K>: Enable sigma clipping at this many standard deviations (off by default). Same as --robust sigma --robust-k K
  • --robust <ROBUST>: Outlier-robust combination of the frames at each pixel: sigma drops outliers, huber down-weights them, winsor clamps them. Two passes over the frames. Default none, the weighted mean
    • none: Weighted mean of every sample (the default)
    • sigma: Drop samples further than K sigma from the mean (default K 2.5)
    • huber: Huber M-estimator: down-weight samples beyond K sigma in proportion to their distance (default K 1.345)
    • winsor: Winsorised mean: clamp samples to K sigma from the mean (default K 2)
  • --robust-k <K>: Threshold of --robust in standard deviations. Default: 2.5 for sigma, 1.345 for huber, 2 for winsor
  • --bayer <BAYER>: Bayer handling: auto, none, rggb, bggr, grbg or gbrg. auto trusts the header, but for a SER file labelled mono it also checks the pixels for a hidden Bayer mosaic (some recorders label raw colour data as mono) and debayers when it finds one. none treats the frames as mono whatever the header or sidecar say; a pattern forces it. For AVI the pattern otherwise comes from the SharpCap sidecar (default auto)
  • --bit-depth <BITS>: Bits per sample, instead of the SER header's: 1 to 8 read one byte per pixel, 9 to 16 two bytes. For a header that gets the depth wrong. SER only
  • --endian <ORDER>: Byte order of 16-bit samples, instead of the SER header flag (little or big). SER only
  • --flip-rows: Read the rows bottom to top, for a file stored upside down. Patterns given to --bayer describe the picture after the flip. SER only
  • --channel <CHANNEL>: Colour channel to stack from a Bayer capture. Default: the preset's channel, else lum
    • r: Red plane only (usual for H-alpha)
    • g: Green plane only
    • b: Blue plane only
    • lum: Mean of the three planes
  • --debayer <DEBAYER>: Demosaicing method (default mhc)
    • bilinear: Plain bilinear interpolation
    • mhc: Malvar-He-Cutler, edge aware
  • --metric <METRIC>: Sharpness metric used to rank frames (default gradient)
    • gradient: Gradient energy
    • laplacian: Laplacian variance
  • --cache-dir <DIR>: Directory for compressed video (.mp4, .mov, .mkv, .webm, compressed .avi) decoded by ffmpeg. The decoded frames are uncompressed, so this needs room for the whole video (checked first) and is cleaned up afterwards. Default: the operating system's cache directory, or the HELIA_CACHE_DIR environment variable
  • -q, --quiet: Hide the progress bar and the summary
  • --timings: Print where the time went: wall time per stage and busy time per kind of work

helia calibrate

Build master dark, flat and bad-pixel files from calibration captures, for helia stack --dark --flat --bad-pixels

helia calibrate [OPTIONS]

Options:

  • --darks <PATH>: Dark captures (lens capped, same exposure and gain as the Sun) (repeatable)
  • --flats <PATH>: Flat captures (an even field, at the focus and set-up of the Sun) (repeatable)
  • --displaced <PATH>: Captures of the Sun at many places on the sensor (the mount off tracking, or nudged between frames), to solve the flat from instead of from --flats: it then holds the dust and fringes of the observing moment, without a diffuser. The darks (same exposure as the Sun) are taken off first. Needs at least six frames with the limb clear (repeatable)
  • --displaced-keep-smooth: With --displaced, keep every scale of the solution, not only the structure finer than a quarter of the displacements (the smooth scales cannot be told from the Sun's own limb darkening unless it moved far)
  • --flat-darks <PATH>: Dark captures at the exposure of the flats. Without them the flats keep their offset (or lose the bias, if --bias is given) (repeatable)
  • --bias <PATH>: Bias captures (the shortest exposure). Only needed to scale a dark to another exposure (repeatable)
  • --lights <CAPTURE>: A light capture: pixels that read the same wrong value in every frame while the Sun moves (stuck pixels) are added to the bad-pixel map. Useful when there are no darks
  • -o, --output-dir <DIR>: Folder for master-dark.fits, master-flat.fits, master-bias.fits and bad-pixels.fits (created if needed) (default calibration)
  • --combine <COMBINE>: How frames are combined: median, sigma (clip at 3 standard deviations) or sigma:K (default sigma)
  • --max-frames <N>: Use at most this many frames per master, spread over the input (default: as many as fit in 3 GiB)
  • --no-bad-pixels: Do not look for hot, cold and stuck pixels
  • --hot-sigma <K>: A pixel this many robust standard deviations off its neighbours in the dark is hot (default 6)
  • --preset <FILTER>: The filter the flats were taken through (ha, cak, ...): adds the advice that goes with it, such as the etalon's sweet spot and Newton rings for H-alpha
  • --bayer <BAYER>: Bayer handling: auto, none, rggb, bggr, grbg or gbrg. auto trusts the header, but for a SER file labelled mono it also checks the pixels for a hidden Bayer mosaic (some recorders label raw colour data as mono) and debayers when it finds one. none treats the frames as mono whatever the header or sidecar say; a pattern forces it. For AVI the pattern otherwise comes from the SharpCap sidecar (default auto)
  • --bit-depth <BITS>: Bits per sample, instead of the SER header's: 1 to 8 read one byte per pixel, 9 to 16 two bytes. For a header that gets the depth wrong. SER only
  • --endian <ORDER>: Byte order of 16-bit samples, instead of the SER header flag (little or big). SER only
  • --flip-rows: Read the rows bottom to top, for a file stored upside down. Patterns given to --bayer describe the picture after the flip. SER only
  • -q, --quiet: Hide the progress messages

helia edit

Sharpen and adjust an existing TIFF, PNG or FITS image

helia edit [OPTIONS] --output <OUTPUT> <INPUT>

Arguments:

  • <INPUT>: Image to edit: .tif, .tiff, .png or .fits (mono or RGB) (required)

Options:

  • -o, --output <OUTPUT>: Output image; the extension picks the format. A colour image stays colour (TIFF or PNG) (required)
  • --preset <FILTER>: The filter the image was taken with, for the recommended sharpening values (ha, cak, ...). Without it the filter is read from the file name or folder when possible
  • --sharpen <METHOD>: Sharpening: wavelet (Registax-style layers, the usual choice), unsharp, rl (Richardson-Lucy deconvolution) or none. The default is none
  • --unsharp-radius <PX>: Unsharp mask: Gaussian radius (sigma) in pixels
  • --unsharp-amount <A>: Unsharp mask: how much detail is added back (0.6 is gentle, 2 is strong)
  • --unsharp-threshold <T>: Unsharp mask: detail weaker than this (0 to 1) is left alone, so noise is not boosted
  • --wavelet-gains <G1,G2,...>: Wavelets: gain of layers 1, 2, 3 ... (comma separated; 1 leaves a layer alone, above 1 sharpens it). Layers not listed keep the recommended gain
  • --wavelet-noise <T1,T2,...>: Wavelets: noise threshold of layers 1, 2, 3 ... in noise sigmas (0 keeps all detail; 2 removes what is not clearly above the noise)
  • --rl-iterations <N>: Richardson-Lucy: iterations (more is sharper and noisier)
  • --rl-tv <L>: Richardson-Lucy: total-variation weight, 0 to 0.05 (keeps noise from becoming speckle)
  • --rl-psf <PSF>: Richardson-Lucy: the blur to undo. auto (from the image scale), gaussian:SIGMA, airy:RADIUS (first dark ring, pixels) or airy-scope:NM,MM,ARCSEC_PER_PX
  • --rl-guard <S>: Richardson-Lucy: strength of the limb and sunspot ringing guard, 0 (off) to 1
  • --flatten <SOURCE>: Divide the disc by its limb-darkening profile before anything else, so sharpening sees flat contrast: model (the fitted law) or profile (the measured azimuthal median). Outside the limb nothing changes
  • --limb-law <LAW>: The limb-darkening law to fit: linear, quadratic (the default) or neckel (Neckel and Labs, five terms). helia stack prints the fit
  • --prominence: Composite a stretched prominence layer with the disc: outside the fitted limb the scattered light is taken off and what is left is stretched to read like the disc, joined to it by a feathered edge. For H-alpha and the other lines with prominences. Runs after the sharpening
  • --prominence-strength <S>: How hard the prominence layer is lifted: 1 refers it to the brightest prominence found
  • --prominence-stretch <K>: The k of the asinh stretch of the layer; 0 is linear, larger lifts the faint parts more (default 6)
  • --prominence-level <SHARE>: Brightness of the brightest prominence as a share of the disc's (default 0.75)
  • --prominence-feather <SHARE>: Half-width of the feathered join at the limb, as a share of the disc radius (default 0.012)
  • --prominence-reach <RADII>: How far out the layer reaches, in disc radii (default 1.4)
  • --prominence-gate <SIGMA>: Noise sigmas taken off the layer before the stretch (default 3)
  • --prominence-offset <SHARE>: Move the join of layer and disc, as a share of the disc radius added to the fitted limb (negative inside the limb, default 0)
  • --prominence-disc-gamma <G>: Gamma of the disc's tone above the sky while the layer is on (default 1: the disc is left alone)
  • --prominence-layer-gamma <G>: Gamma of the stretched layer's tone (default 1; above 1 darkens its faint parts)
  • --denoise <STRENGTH>: Wavelet denoise; the strength is in noise sigmas for the finest layer (default: the filter's recommended value)
  • --denoise-layers <N>: Layers the denoise treats, from the finest
  • --black <V>: Levels: input value (0 to 1) that becomes black
  • --white <V>: Levels: input value (0 to 1) that becomes white
  • --gamma <G>: Levels: mid-tone gamma (above 1 brightens)
  • --curve <IN:OUT,...>: A tone curve through IN:OUT points, for example 0.25:0.15,0.75:0.85
  • --crop <X,Y,W,H>: Crop to X,Y,W,H in pixels (top-left origin)
  • --rotate <DEG>: Rotate clockwise by this many degrees (multiples of 90 are exact)
  • --flip-h: Mirror left to right
  • --flip-v: Mirror top to bottom
  • --resize <SIZE>: Resize: 50% or 0.5, 1200x800, 1200x (width) or x800 (height)
  • --edits <NAME|FILE>: Start from a saved edit preset (a name from the presets folder, or a .json file); the options above are added after it
  • --save-edits <NAME>: Save the resulting edit list as a preset under this name, for --edits
  • --backend <BACKEND>: Compute backend (default auto)
  • -q, --quiet: Hide the summary

helia info

Print what a capture file contains: every header field as written, what Helia will use instead where it differs, and why

helia info [OPTIONS] <INPUT>

Arguments:

  • <INPUT>: Capture file: .ser or .avi, or a video (.mp4, .mov, .mkv, .webm, .m4v) read through the optional ffmpeg program (required)

Options:

  • --bayer <BAYER>: Bayer handling: auto, none, rggb, bggr, grbg or gbrg. auto trusts the header, but for a SER file labelled mono it also checks the pixels for a hidden Bayer mosaic (some recorders label raw colour data as mono) and debayers when it finds one. none treats the frames as mono whatever the header or sidecar say; a pattern forces it. For AVI the pattern otherwise comes from the SharpCap sidecar (default auto)
  • --bit-depth <BITS>: Bits per sample, instead of the SER header's: 1 to 8 read one byte per pixel, 9 to 16 two bytes. For a header that gets the depth wrong. SER only
  • --endian <ORDER>: Byte order of 16-bit samples, instead of the SER header flag (little or big). SER only
  • --flip-rows: Read the rows bottom to top, for a file stored upside down. Patterns given to --bayer describe the picture after the flip. SER only

helia measure

Measure how much granulation an image holds: the band-pass contrast (standard deviation over mean) of a disc-centre crop in three bands, for comparing settings on the same capture

helia measure [OPTIONS] <INPUT>

Arguments:

  • <INPUT>: Image: .tif, .tiff, .png or .fits (mono or RGB; RGB is averaged to grey) (required)

Options:

  • --crop <PX>: Side of the disc-centre crop in pixels (default 240)
  • --limb: Instead of the granulation bands, fit the limb-darkening laws (linear, quadratic, Neckel and Labs) to the disc and print the coefficients and the measured profile
  • --scale <ARCSEC>: Image scale in arcseconds per pixel, to give sizes in arcseconds
  • --aperture <MM>: Telescope aperture in millimetres; with --wavelength and --scale it gives the diffraction cut-off of the power spectrum
  • --wavelength <NM>: Wavelength in nanometres (for --aperture)

helia sunspots

Find the sunspots on a stacked white-light image: groups, the relative Wolf number and areas corrected for foreshortening, as a table, JSON and an overlay picture

helia sunspots [OPTIONS] <INPUT>

Arguments:

  • <INPUT>: Stacked image: .tif, .tiff, .png or .fits (mono or RGB; RGB is averaged to grey), white light, with the whole disc in view (required)

Options:

  • --date <TIME>: When the image was taken, UTC, such as 2026-10-20T11:30:00. Gives P, B0 and L0 for the Carrington longitude. Without it, a FITS file's DATE-AVG or DATE-OBS is used, else B0 is taken as 0
  • --capture <FILE>: The capture (a .ser) the image was stacked from: its frame timestamps give the time
  • --csv <FILE>: Write the spot table as CSV
  • --json <FILE>: Write the whole report as JSON
  • --overlay <FILE>: Write the overlay picture (spots outlined on the flattened disc) as PNG
  • --sunspot-umbra <SHARE>: Umbra threshold: pixels darker than this share of the quiet disc (after the limb darkening is flattened out) are umbra (default 0.55)
  • --sunspot-penumbra <SHARE>: Penumbra threshold: pixels darker than this share of the quiet disc are spot. Raise it to take in fainter penumbrae and pores (default 0.88)
  • --sunspot-min-area <MSH>: Smallest spot to keep, in millionths of the visible hemisphere (default 2)
  • --sunspot-max-area <MSH>: Largest spot, in millionths of the hemisphere; a dark region bigger than this (a cloud, a vignette) is left out (default 6000)
  • --sunspot-group-gap <DEG>: Spots whose edges are closer than this on the Sun, in degrees, form one group (default 3)
  • --wolf-k <K>: Observer factor k of the Wolf number R = k (10 g + s) (default 1)
  • --north-angle <DEG>: How far solar north is turned counter-clockwise from the top of the image, in degrees. Default 0: solar north up (default 0)
  • --celestial-north-up: The image has celestial north up (an equatorial mount): solar north is then turned by the position angle P of the Sun at the capture time, added to --north-angle
  • --mirror: The image is a mirror image (west is on the left)

helia serve

Run the HTTP/JSON API server

helia serve [OPTIONS]

Options:

  • --bind <BIND>: Address to listen on; a non-loopback address needs a token (default 127.0.0.1:7870)
  • --data-root <DIR>: Directory all request paths must stay inside (default: the current directory)
  • --token <TOKEN>: Bearer token. Visible in the process list; prefer --token-file or the HELIA_TOKEN environment variable
  • --token-file <FILE>: File holding the bearer token
  • --workers <WORKERS>: Jobs that run at once (default 1)
  • --max-queue <MAX_QUEUE>: Queued plus running jobs allowed before answering 429 (default 16)
  • --cors-origin <ORIGIN>: Allow browser access from this origin (repeatable). Off by default
  • --cache-dir <DIR>: Directory for compressed video decoded by ffmpeg (default: the operating system's cache directory, or HELIA_CACHE_DIR)
  • --allow-host <HOST>: Accept this Host header on a loopback bind (repeatable), e.g. behind a proxy

helia presets

List the filter presets (helia stack --preset ...)

helia presets

helia lut

Write a filter's colour table as a file other programs read (.cube, .csv, ImageJ .lut or a PNG strip), so a picture coloured elsewhere matches one coloured by Helia

helia lut [OPTIONS] --output <OUTPUT> <PRESET>

Arguments:

  • <PRESET>: The filter whose colour look to write (ha, cak, cah, mg, na, he-d3, hbeta, white); see helia presets (required)

Options:

  • -o, --output <OUTPUT>: Output file. The extension picks the format: .cube, .csv, .lut (ImageJ) or .png, unless --format says (required)
  • --format <FORMAT>: File format: cube, csv, imagej or png
  • --size <N>: Entries in the table, 2 to 4096 (an ImageJ table always has 256) (default 256)
  • --invert: Invert the picture before colouring (the table runs from white to black)
  • --strength <S>: Mix of colour and grey from 0 to 1 (default 1)

helia feedback

Send feedback (a bug, a feature request or a question). It is sent to the Helia developers and filed in the Helia issue tracker, through the feedback service

helia feedback [OPTIONS] --title <TITLE>

Options:

  • --kind <KIND>: What kind of feedback this is (default bug)
    • bug: Something is broken or wrong
    • feature: A request for something new
    • question: A question about using Helia
    • other: Anything else
  • --title <TITLE>: One line that says what it is (at most 120 characters) (required)
  • --description <DESCRIPTION>: The details (at most 8000 characters). - reads standard input (default ``)
  • --screenshot <FILE.png>: A PNG screenshot to attach (at most 5 MiB; larger ones are shrunk). Check it shows nothing private: it is filed in the issue tracker
  • --highlight <X,Y,W,H>: A rectangle to highlight on the screenshot, as x,y,w,h in its pixels (repeatable)
  • --system-info: Include system details: Helia version, commit and target, and your operating system. Never a host name, user name or path
  • --url <URL>: Feedback service address (default: HELIA_FEEDBACK_URL, else the built-in public Helia feedback service)
  • --dry-run: Show the report that would be sent, and send nothing
  • --save <FILE.zip>: If the service cannot be reached, also save the report to this .zip

API reference

Solar lucky-imaging stacker. All paths are relative to the server's data root. Version 0.5.0 of the document; licence GPL-3.0-or-later.

The narrative guide, with examples and the security model, is in The API. The machine-readable document is openapi.json, and a running server serves it at /api/v1/openapi.json.

All paths below are relative to /api/v1.

Operations

GET /captures/frame

One frame of a capture as an auto-stretched 8-bit greyscale PNG.

ParameterInTypeRequiredDescription
pathquerystringyesCapture path relative to the data root.
indexqueryintegernoFrame index (default 0).
channelqueryChannelArgnor, g, b or lum (default) for Bayer captures.
max_widthqueryintegernoDownscale to at most this width (16 to 8192).
bayerquerystringnoBayer handling, as for jobs: auto (default), none or a pattern.
bit_depthqueryinteger (int32)noBits per sample instead of the SER header's.
endianquerystringnolittle or big.
flip_rowsquerybooleannoRead the rows bottom to top.
StatusMeaningBody
2008-bit greyscale PNGimage/png

POST /captures/inspect

Header information for a .ser, .avi or video capture, without reading frames.

Request body:

StatusMeaningBody
200OKapplication/json: CaptureInfo

GET /docs

A small HTML page that lists the operations of the OpenAPI document; needs no token.

Needs no token.

StatusMeaningBody
200OKtext/html

GET /health

Liveness probe. Always answers {"status":"ok"}; needs no token.

Needs no token.

StatusMeaningBody
200OKapplication/json: Health

GET /jobs

Remembered jobs, oldest first. The newest 100 finished jobs are kept.

StatusMeaningBody
200OKapplication/json: array of JobView

POST /jobs

Queue a stack. Answers 202 with the job; poll it or follow its events.

Request body:

StatusMeaningBody
202Acceptedapplication/json: JobView
429queue full (queue_full)application/json: ErrorEnvelope

GET /jobs/{id}

One job: state, stage, progress, timings and summary.

ParameterInTypeRequiredDescription
idpathinteger (int64)yesjob id
StatusMeaningBody
200OKapplication/json: JobView
404unknown job idapplication/json: ErrorEnvelope

DELETE /jobs/{id}

Cancel a queued or running job, or forget a finished one.

A finished job's output file stays on disk.

ParameterInTypeRequiredDescription
idpathinteger (int64)yesjob id
StatusMeaningBody
202cancellation requestedapplication/json: JobView
204finished job forgotten
404unknown job idapplication/json: ErrorEnvelope

GET /jobs/{id}/events

WebSocket stream of job events: a snapshot, then state and progress messages.

The server closes the stream after the final state message.

ParameterInTypeRequiredDescription
idpathinteger (int64)yesjob id
StatusMeaningBody
101WebSocket; text frames are JobEvent JSON

GET /jobs/{id}/preview.png

An 8-bit PNG of the result (square-root stretch). Only once the job has succeeded.

ParameterInTypeRequiredDescription
idpathinteger (int64)yesjob id
max_widthqueryintegernoDownscale to at most this width (16 to 8192; stored previews are at most 2048 wide).
StatusMeaningBody
200PNG previewimage/png
409the job has not succeeded (not_ready)application/json: ErrorEnvelope

GET /jobs/{id}/result

The written TIFF or FITS file, streamed. Only once the job has succeeded.

ParameterInTypeRequiredDescription
idpathinteger (int64)yesjob id
StatusMeaningBody
200the stacked TIFF, PNG or FITS file
409the job has not succeeded (not_ready)application/json: ErrorEnvelope

GET /jobs/{id}/sunspots.csv

The sunspot table of a finished job that asked for sunspots, as CSV.

ParameterInTypeRequiredDescription
idpathinteger (int64)yesjob id
StatusMeaningBody
200one row per spottext/csv
404the job did not ask for sunspots (sunspots_missing)application/json: ErrorEnvelope
409the job has not succeeded (not_ready)application/json: ErrorEnvelope

GET /jobs/{id}/sunspots.png

The sunspot overlay of a finished job that asked for sunspots: the flattened disc with penumbra, umbra and group numbers drawn on it.

ParameterInTypeRequiredDescription
idpathinteger (int64)yesjob id
StatusMeaningBody
200overlay PNGimage/png
404the job did not ask for sunspots (sunspots_missing)application/json: ErrorEnvelope
409the job has not succeeded (not_ready)application/json: ErrorEnvelope

GET /openapi.json

This API as an OpenAPI document.

StatusMeaningBody
200OpenAPI 3 document

GET /presets

GET /presets: the filter presets, their recommended settings and colours.

StatusMeaningBody
200OKapplication/json: PresetList

GET /presets/{id}/lut

A preset's colour table as a file other programs read.

ParameterInTypeRequiredDescription
idpathstringyespreset id, see GET /presets
formatquerystringnocube (default), csv, imagej or png.
sizequeryintegernoEntries, 2 to 4096 (default 256; an imagej table always has 256).
invertquerybooleannoInvert before colouring.
strengthquerynumber (float)noMix of colour and grey, 0 to 1 (default 1).
StatusMeaningBody
200the table: a .cube, .csv, ImageJ .lut or a PNG strip
404no such presetapplication/json: ErrorEnvelope
422a bad format, size or strengthapplication/json: ErrorEnvelope

GET /version

Server version, backend availability, limits and whether a token is required.

The GPU is probed once, on first use.

StatusMeaningBody
200OKapplication/json: VersionInfo

Schemas

AlignmentSummary

Global alignment: the method that ran, why, and its confidence.

FieldTypeRequiredDescription
confidencenumber (float), may be nullnoMean confidence of the frames, 0 to 1. Absent when it was not measured.
confidence_minnumber (float), may be nullnoLeast confidence of any frame, 0 to 1.
fallbacksintegeryesFrames that wanted the limb fit but were aligned by phase correlation.
limbarray of number (float), may be nullnoFitted limb of the reference: centre x, centre y and radius, pixels.
methodstringyesWhat ran: limb or phase.
reasonstringyesWhy, in a sentence (for auto, whether the full disc was in view).
requestedstringyesWhat config.align asked for: auto, phase or limb.
sampledbooleanyesThe confidence comes from a sample of the frames (a GPU run).

AutoApSummary

What automatic alignment points chose and what they were based on.

FieldTypeRequiredDescription
arcsec_per_pixelnumber (float), may be nullnoImage scale from the fitted limb, arcseconds per pixel. Absent when there was no limb.
search_radiusintegeryesSearch radius around the global shift, pixels.
sizeintegeryesWindow edge, pixels.
stepintegeryesDistance between window centres, pixels.
textstringyesThe choice in a sentence.
warp_pxnumber (float), may be nullnoLocal warp of the seeing, pixels (root mean square distance of an alignment point's shift from its frame's median). Absent when too few points locked.

BackendArg

Compute backend request.

One of auto, cpu, gpu.

Backends

Backend availability.

FieldTypeRequiredDescription
cpuCpuInfoyesCPU reference path.
gpuGpuInfoyeswgpu path.

BuildDetails

How the server binary was built.

FieldTypeRequiredDescription
builtstringyesBuild time, UTC.
commitstringyesShort git commit hash (unknown outside a git checkout).
dirtybooleanyesBuilt from a tree with uncommitted changes.
profilestringyesCargo profile.
ruststringyesCompiler version.
targetstringyesTarget triple.
wgpustringyeswgpu version.

CalibrationConfig

Calibration masters for a job. Each path is relative to the data root and names a master file (FITS, from helia calibrate) or raw calibration frames to build one from (a SER, AVI or FITS file, or a folder of them).

FieldTypeRequiredDescription
bad_pixelsstring, may be nullnoBad-pixel map: a FITS map from helia calibrate, or a text file with an x y pair (from 0, top-left) on each line.
biasstring, may be nullnoMaster bias, or bias captures. Only used with scale_dark.
combinestringnoHow frames are combined into a master: median, sigma or sigma:K. Default sigma. Default "sigma".
cosmeticbooleannoWithout bad_pixels, find the hot pixels of the dark and the cold ones of the flat and correct them. Default true. Default true.
darkstring, may be nullnoMaster dark, or dark captures.
displacedstring, may be nullnoCaptures of the Sun at many places on the sensor, to solve a flat from when there is no flat (self-calibration: dust and fringes included). The dark is taken off first. Excludes flat.
flatstring, may be nullnoMaster flat, or flat captures.
flat_darkstring, may be nullnoDark captures at the exposure of the flats, when flat is captures.
scale_darkbooleannoScale the dark by the ratio of the exposures (read from the SharpCap sidecar of the capture). Default false.

CaptureInfo

Header information for a capture.

FieldTypeRequiredDescription
bayerstring, may be nullnoBayer pattern (rggb ...) when raw colour.
bit_depthinteger (int32)yesBits per sample.
codecstring, may be nullnoVideo codec (h264, ...), for video read through ffmpeg.
colourstringyesPixel layout: mono, bayer, rgb, bgr, or video for compressed video, which ffmpeg converts to luminance or one colour channel.
detectionDetectionInfo or nullno
effectiveEffectiveInfoyesWhat Helia decodes the capture with, and where each value came from.
formatstringyesser, avi, or the extension of a video read through ffmpeg (mp4, mov, mkv, webm, m4v; also avi for a compressed AVI).
fourccstring, may be nullnoAVI FourCC.
frame_ratenumber (double), may be nullnoFrames per second, for video.
framesintegeryesNumber of frames.
headerSerHeaderInfo or nullno
heightinteger (int32)yesHeight in pixels.
instrumentstring, may be nullnoSER instrument string.
noticesarray of NoticeInfoyesWhat to tell the user about how the file was read.
observerstring, may be nullnoSER observer string.
overridesarray of stringyesThe overrides in force, as command-line flags.
pathstringyesCapture path relative to the data root.
quality_notestring, may be nullnoQuality caveat when the input is lossy video, for example "lossy 8-bit 4:2:0 video (h264): fine for quick results, SER/AVI raw captures keep more detail".
recommendedRecommendedyesThe settings a job would use if none were given.
sidecarSidecarInfo or nullno
telescopestring, may be nullnoSER telescope string.
timestampsboolean, may be nullnoSER per-frame timestamps present.
widthinteger (int32)yesWidth in pixels.

ChannelArg

Colour plane taken from a Bayer capture.

One of r, g, b, lum.

ChannelSummary

How red and blue were registered to green in a true-colour stack.

FieldTypeRequiredDescription
appliedbooleanyesThe plane was moved. False when the shift was too small, too large or not trustworthy.
channelstringyesred or blue.
confidencenumber (float)yesConfidence of the correlation, 0 to 1.
shiftarray of number (float)yesShift measured, pixels: [dx, dy].

CpuInfo

CPU availability (always true).

FieldTypeRequiredDescription
availablebooleanyesAlways true.

DebayerArg

Demosaicing method.

One of bilinear, mhc.

DetectionInfo

The test for a hidden Bayer mosaic.

FieldTypeRequiredDescription
frames_usedintegeryesFrames measured.
patternstring, may be nullnoPattern found, for mosaic.
position_meansarray of number (double)yesMean level (0 to 1) of the pixels at even x/even y, odd x/even y, even x/odd y and odd x/odd y.
reasonstring, may be nullnoWhy it could not tell, for inconclusive.
red_blue_basisstring, may be nullnoHow red was told from blue, for mosaic.
regionarray of integeryesArea measured: x, y, width, height.
spreadnumber (double)yes(largest - smallest) / average of the four means.
verdictstringyesmosaic, mono or inconclusive.

EffectiveInfo

What Helia decodes the capture with.

FieldTypeRequiredDescription
bayerSourcedTextyesBayer pattern (rggb ...), or none for mono.
bit_depthSourcedNumberyesBits per sample.
byte_orderSourcedTextyesByte order of 16-bit samples: little or big.
flip_rowsSourcedFlagyesRows are read bottom to top.

ErrorBody

Machine-readable error body.

FieldTypeRequiredDescription
codestringyesStable snake_case identifier, safe to match on.
messagestringyesHuman-readable explanation.

ErrorEnvelope

Envelope returned for every non-2xx response.

FieldTypeRequiredDescription
errorErrorBodyyesThe error.

GpuInfo

GPU availability.

FieldTypeRequiredDescription
adapterstring, may be nullnoAdapter description.
availablebooleanyesA GPU backend can be created (a software adapter counts).
softwarebooleanyesThe adapter is a software rasteriser.

GranulationSummary

Granulation statistics of the stack (helia measure, the quality report).

FieldTypeRequiredDescription
arcsec_per_pixelnumber (float), may be nullnoImage scale, arcseconds per pixel (absent without a fitted limb).
cell_diameter_pxnumber (float)yesMean diameter of whole granules, pixels.
cell_pitch_pxnumber (float)yesMean spacing of granules, pixels.
cutoff_freqnumber (float), may be nullnoDiffraction cut-off D / lambda in cycles per pixel; needs aperture_mm and a fitted limb.
linesarray of stringyesThe report in lines, as the command line prints it.
resolved_freqnumber (float)yesHighest frequency (cycles per pixel) above 1% of the peak power.
rms_contrastnumber (float)yesStandard deviation over mean of the flattened disc-centre region (a fraction, 0.04 is 4%).
rolloff_freqnumber (float)yesSpatial frequency (cycles per pixel) where the noise-subtracted power spectrum has fallen to 10% of its peak.

Health

Liveness probe.

FieldTypeRequiredDescription
statusstringyesAlways ok.

InspectRequest

POST /captures/inspect body.

FieldTypeRequiredDescription
bayerstring, may be nullnoauto (default), none, rggb, grbg, gbrg or bggr: see what an override would change.
bit_depthinteger (int32), may be nullnoBits per sample instead of the SER header's (1 to 16).
endianstring, may be nullnolittle or big, instead of the SER header flag.
flip_rowsbooleannoRead the rows bottom to top.
pathstringyesCapture path relative to the data root.

JobConfig

The same knobs as helia stack.

FieldTypeRequiredDescription
alignstringnoHow whole frames are aligned: auto (fit the solar limb when the full disc is in view and the fit is confident, else phase correlation), phase, or limb. The choice and its confidence come back in summary.alignment. Default "auto".
align_channelsbooleannoWith colour: "true", register red and blue to green (atmospheric dispersion). Default true. Default true.
ap_autobooleannoChoose the alignment point size, spacing and search range from the image scale (the fitted limb) and the seeing (a probe of a dozen frames). Excludes ap_size; needs multipoint. What was chosen comes back in summary.auto_ap. Default false.
ap_passesinteger (int32), may be nullnoAlignment point passes, 1 or 2. 2 stacks once and measures the points again against that stack. Default 1; needs multipoint.
ap_sizeinteger, may be nullnoAlignment point size in pixels (8 to 1024). Absent: chosen from the image size.
ap_smoothbooleannoReplace alignment point shifts that disagree with their neighbours by the neighbours' median. Needs multipoint. Default false.
aperture_mmnumber (float), may be nullnoTelescope aperture in millimetres, for the diffraction cut-off in summary.granulation and for r0 in summary.seeing.
autocropbooleannoCrop the result to the solar disc plus autocrop_margin, FITS too. A capture without a clear disc is left uncropped; summary.crop is then absent. Default false.
autocrop_marginnumber (float), may be nullnoMargin of autocrop as a percentage of the disc radius, 0 to 200. Default 10.
backendBackendArgnoCompute backend. Default "auto".
bayerstringnoauto, none, rggb, grbg, gbrg or bggr. auto trusts the file's header, but checks a SER file labelled mono for a hidden Bayer mosaic and debayers when it finds one. none treats the frames as mono whatever the header says; a pattern forces it. Default "auto".
bit_depthinteger (int32), may be nullnoBits per sample instead of the SER header's (1 to 8 read one byte per pixel, 9 to 16 two). SER only.
calibrationCalibrationConfig or nullno
channelChannelArg or nullno
colourstring, may be nullnoColour of the output: auto (the preset in force), mono, a preset id, or true. true is true colour from a raw colour camera: red, green and blue stacked on one alignment, channels aligned, white balanced, written as an RGB TIFF or PNG or a three-plane FITS cube; it needs a Bayer capture and excludes channel, invert and colour_strength. The other looks colour a .tif or .png from the mono stack, and FITS output is mono and unaltered for them. Absent: the preset's colours if one was given, else mono.
colour_strengthnumber (float)noMix of colour and grey, 0 to 1. Default 1.0.
debayerDebayerArgnoDemosaicing method. Default "mhc".
detect_overlaybooleannoLook for burned-in text (a SharpCap timestamp) and mask it. Default true.
disc_maskbooleannoFit the limb and place alignment points on the disc only. Default true.
editsarray of objectnoFurther edits, applied in order after sharpen: objects with an op of unsharp, wavelet, deconvolve, denoise, levels, curve, crop, rotate, flip or resize and that operation's parameters (see the sharpening and adjustments page of the user guide). Unset parameters take their defaults. Default [].
endianstring, may be nullnoByte order of 16-bit samples instead of the SER header flag: little or big. SER only.
excludearray of RegionnoRectangles to keep alignment and scoring off. They are flattened in every frame and so are flat in the result. Burned-in timestamps are found automatically (see detect_overlay). Default [].
flattenstring, may be nullnoFlatten the disc's limb darkening before anything else: model (divide by the fitted law) or profile (divide by the measured azimuthal median). Outside the limb nothing changes. It runs first, so sharpen and edits see flat contrast. A capture with no clear disc is left as it is and summary.limb_darkening is absent.
flip_rowsbooleannoRead the rows bottom to top. SER only. Default false.
invertbooleannoInvert before colouring (shows filaments and prominences). Default false.
keepstring, may be nullnoShare ("25%") or count ("500") of frames to keep. Default 25%.
keep_framesinteger, may be nullnoNumber of frames to keep; excludes keep.
limb_lawstring, may be nullnoThe limb-darkening law fitted to the stack and used by flatten: linear, quadratic (the default) or neckel (five terms). The fit comes back in summary.limb_darkening whether or not flatten is set.
metricMetricArgnoFrame ranking metric. Default "gradient".
multipointbooleannoMulti-point alignment; false is global alignment only. Default true.
presetstring, may be nullnoFilter preset (ha, cak, cah, mg, na, he-d3, hbeta, white; see GET /presets): its channel and frame share apply wherever channel and keep are absent, and a .tif or .png output is coloured with it. Absent: the filter is detected from the file name, folder, sidecar or header, for the settings only.
prominence_bandintegernoPixels outside the fitted limb where alignment points are still allowed; 0 keeps them on the disc. Default 0.
robustRobustArg or nullno
robust_knumber (float), may be nullnoThreshold of robust in standard deviations. Default 2.5 for sigma, 1.345 for huber, 2 for winsor.
seeingbooleannoEstimate the seeing from the capture (summary.seeing): the Fried parameter from the spectral ratio, which needs aperture_mm, and a relative alignment point motion index. Default false.
sharpenstring, may be nullnoSharpening of the stack: wavelet (Registax-style layers, the usual choice), unsharp, rl (Richardson-Lucy) or none, with the recommended values of the filter in force. Absent: none. It runs first, on the mono image, before edits and before any colour.
sharpen_fitsbooleannoApply sharpen and edits to FITS output too. By default a FITS file is the unsharpened stack. Default false.
sigma_clipnumber (float), may be nullnoSigma clipping threshold in standard deviations; off when absent. Same as robust: "sigma" with robust_k.
sitestring, may be nullnoObserving site as "lat,lon,alt" (degrees north, degrees east, metres), written to a FITS output as OBSGEO-X/Y/Z. Absent: the server's HELIA_SITE environment variable, else the keywords are left out.
sunspotsSunspotsConfig or nullno
white_balancestring, may be nullnoWhite balance of colour: "true": auto (make the disc centre neutral), off (the sensor's own balance) or "R,G,B" gains such as "1.9,1,1.5". Default auto. Only with colour: "true".

JobEvent

Message on the /events WebSocket.

One of:

  • Full job view, sent once on connect. Fields: job (JobView), type = snapshot.
  • State change. Fields: error (ErrorBody or null), job (integer (int64)), state (JobState), type = state.
  • Progress tick. Fields: job (integer (int64)), progress (number (float)), stage_progress (number (float)), stage (string), type = progress.

JobRequest

POST /jobs body.

FieldTypeRequiredDescription
configJobConfignoStacking settings.
inputstringyesCapture path (.ser or .avi), relative to the data root.
outputstringyesOutput path; .tif/.tiff or .fits/.fit/.fts.
overwritebooleannoReplace an existing output file.

JobState

Lifecycle state.

One of queued, running, succeeded, failed, cancelled.

JobSummary

What a finished stack contains.

FieldTypeRequiredDescription
alignmentAlignmentSummaryyesHow whole frames were aligned, and how confident that was.
alignment_points_activeinteger, may be nullnoActive alignment points (multi-point only).
alignment_points_totalinteger, may be nullnoAlignment points in the grid (multi-point only).
auto_apAutoApSummary or nullno
bayerstring, may be nullnoBayer pattern the frames were decoded with (rggb ...); absent for mono.
bayer_sourcestringyesWhere that came from: header, sidecar, detection or override.
calibrationarray of stringyesThe calibration masters applied, one line each, and any warning about them (a flat with rings, a dark that could not be scaled). Empty when the job had no calibration.
colourstring, may be nullnoHow the output was coloured, in words; absent for a mono output.
cropRegion or nullno
editedbooleanyesThe edits were applied to the pixels of the output.
editsarray of stringyesThe edits recorded in the output file, one line each. For FITS output without sharpen_fits this is the note that the file is unedited.
filterstring, may be nullnoPreset id in force (chosen or detected), if any.
filter_sourcestring, may be nullnoWhere the filter was read from (chosen, or the text it was found in).
framesintegeryesFrames in the capture.
granulationGranulationSummary or nullno
heightintegeryesOutput height.
keptintegeryesFrames in the kept set.
limb_darkeningLimbSummary or nullno
noticesarray of stringyesThings worth knowing about how the capture was read, such as a Bayer mosaic found in a file whose header says mono.
registeredintegeryesFrames registered.
seeingSeeingSummary or nullno
sunspotsSunspotsSummary or nullno
true_colourTrueColourSummary or nullno
widthintegeryesOutput width.

JobView

Public view of a job.

FieldTypeRequiredDescription
backendstring, may be nullnoBackend used.
configJobConfigyesThe settings in force.
created_atinteger (int64)yesCreation time, Unix milliseconds.
errorErrorBody or nullno
finished_atinteger (int64), may be nullnoEnd time, Unix milliseconds.
idinteger (int64)yesJob id.
inputstringyesInput path relative to the data root.
outputstringyesOutput path relative to the data root.
progressnumber (float)yesApproximate overall progress, 0 to 1.
queue_positioninteger, may be nullnoPosition among queued jobs (0 is next), while queued.
run_secondsnumber (float), may be nullnoSeconds spent running so far or in total.
stagestring, may be nullnoCurrent stage name, once running.
stage_progressnumber (float)yesProgress within the current stage, 0 to 1.
started_atinteger (int64), may be nullnoStart time, Unix milliseconds.
stateJobStateyesState.
summaryJobSummary or nullno
timingsarray of StageTimingyesPer-stage wall times of a finished run.

LicenceDetails

Licensing of the software.

FieldTypeRequiredDescription
copyrightstringyesCopyright line.
licencestringyesSPDX licence identifier.
sourcestringyesSource code location.

LimbSummary

A limb-darkening law fitted to the stack's azimuthal median profile.

FieldTypeRequiredDescription
coefficientsarray of number (float)yesThe coefficients, I(mu) / I(1) = 1 - sum c_k (1 - mu^k) for neckel, 1 - u (1 - mu) - u2 (1 - mu)^2 for the others.
lawstringyeslinear, quadratic or neckel.
limb_levelnumber (float)yesThe law at the limb as a share of the disc centre.
namesarray of stringyesNames of the coefficients, in order (u1, u2, or a1 to a5).
radius_pxnumber (float)yesFitted disc radius, pixels.
rmsnumber (float)yesRoot mean square departure of the profile from the law, as a share of the centre intensity.
textstringyesThe fit in a sentence.

Limits

Server limits.

FieldTypeRequiredDescription
active_jobsintegeryesJobs queued or running now.
max_body_bytesintegeryesRequest body limit in bytes.
max_queueintegeryesQueued plus running jobs allowed.
workersintegeryesConcurrent jobs.

MetricArg

Sharpness metric.

One of gradient, laplacian.

NoticeInfo

A message about how the capture was understood.

FieldTypeRequiredDescription
levelstringyesinfo or warning.
textstringyesThe message.

PresetColour

The colour parameters of a preset.

FieldTypeRequiredDescription
exponentsarray of number (float)yesPer-channel exponent (red, green, blue).
gainsarray of number (float)yesPer-channel gain (red, green, blue).
gammanumber (float)yesTone exponent applied before colouring (below 1 brightens).
mid_grey_rgbarray of integer (int32)yesWhat a mid-grey (0.5) becomes, 8-bit red, green, blue.

PresetList

GET /presets body.

FieldTypeRequiredDescription
presetsarray of PresetViewyesEvery preset.

PresetSharpen

The recommended sharpening values of a preset (what sharpen in a job uses for this filter).

FieldTypeRequiredDescription
denoisenumber (float)yesWavelet denoise strength.
reasonstringyesWhy they are what they are.
rl_iterationsinteger (int32)yesRichardson-Lucy iterations.
rl_tvnumber (float)yesRichardson-Lucy total-variation weight.
unsharp_amountnumber (float)yesUnsharp mask amount.
unsharp_thresholdnumber (float)yesUnsharp mask threshold (image units, 0 to 1).
wavelet_gainsarray of number (float)yesWavelet gain of layers 1 to 6.
wavelet_noisearray of number (float)yesWavelet noise threshold of layers 1 to 6, in noise sigmas.

PresetView

One filter preset.

FieldTypeRequiredDescription
channelstringyesBayer channel stacked (r, g, b or lum).
colourPresetColouryesColour parameters. Colour is presentation, not measurement.
huestringyesHue in words.
idstringyesId to give as preset or colour in a job: ha, cak, ...
keep_percentnumber (float)yesShare of frames kept, percent.
namestringyesPlain name.
reasonstringyesWhy the recommendations are what they are.
sharpenPresetSharpenyesRecommended sharpening values.
summarystringyesOne line on what it is for.
wavelengthstringyesWavelength as printed in the docs.

What Helia would do with a capture if given no settings.

FieldTypeRequiredDescription
ap_sizeintegeryesAlignment point size in pixels.
channelstringyesBayer channel it would stack (r, g, b, lum).
filterstring, may be nullnoPreset id detected from the file name, folder, sidecar or SER header.
filter_sourcestring, may be nullnoThe text the filter was read from.
keep_percentnumber (float)yesShare of frames it would keep, percent.

Region

A rectangle of the frame, in pixels from the top-left corner.

FieldTypeRequiredDescription
hintegeryesHeight; at least 1.
wintegeryesWidth; at least 1.
xintegeryesLeft edge.
yintegeryesTop edge.

RobustArg

Outlier-robust combination of the frames at each pixel.

One of none, sigma, huber, winsor.

SeeingSummary

Seeing estimated from the capture.

FieldTypeRequiredDescription
d_over_r0number (float), may be nullnoD / r0 from the spectral ratio; needs aperture_mm.
framesintegeryesFrames followed (spread over the whole capture).
gradestring, may be nullnoexcellent, good, fair, poor or bad, from r0_500nm_cm.
linesarray of stringyesThe report in lines, as the command line prints it.
motion_arcsecnumber (float), may be nullnoThe same in arcseconds, with a fitted limb.
motion_pxnumber (float), may be nullnoRMS differential motion between neighbouring alignment points, per axis, pixels. A relative index, not r0.
r0_500nm_cmnumber (float), may be nullnoFried parameter at 500 nm, centimetres.
r0_cmnumber (float), may be nullnoFried parameter at the capture's wavelength, centimetres.
tilesintegeryesTiles of the disc followed.

SerHeaderInfo

A SER header exactly as written in the file, decoded.

FieldTypeRequiredDescription
colour_idinteger (int32)yesColorID as stored.
colour_meaningstringyesIts meaning (MONO, BAYER_RGGB ...).
date_timestring, may be nullnoDateTime (local) decoded; absent when unset.
date_time_utcstring, may be nullnoDateTimeUTC decoded; absent when unset.
file_idstringyesFileID.
frame_countinteger (int32)yesFrame count.
heightinteger (int32)yesHeight in pixels.
instrumentstringyesInstrument.
little_endianbooleanyesLittleEndian flag as stored.
lu_idinteger (int32)yesLuID.
observerstringyesObserver.
pixel_depthinteger (int32)yesBits per sample.
telescopestringyesTelescope.
timestampsTimestampInfo or nullno
widthinteger (int32)yesWidth in pixels.

SidecarInfo

SharpCap sidecar contents that Helia uses.

FieldTypeRequiredDescription
bayer_hintstring, may be nullnoBayer pattern implied by the sidecar.
colour_spacestring, may be nullnoColour Space value.
frame_countinteger, may be nullnoFrame count from the sidecar.
pathstringyesSidecar path relative to the data root.
raw_colourboolean, may be nullnoWhether the sidecar says the data is raw colour.
sectionsarray of stringyesSection headings (camera model).

SourcedFlag

A switch and where its setting came from.

FieldTypeRequiredDescription
sourcestringyesheader, sidecar, detection or override.
valuebooleanyesThe value.

SourcedNumber

A number Helia uses and where it came from.

FieldTypeRequiredDescription
sourcestringyesheader, sidecar, detection or override.
valueinteger (int32)yesThe value.

SourcedText

A value Helia uses and where it came from.

FieldTypeRequiredDescription
sourcestringyesheader, sidecar, detection or override.
valuestringyesThe value.

StageTiming

Wall time of one stage.

FieldTypeRequiredDescription
secondsnumber (float)yesSeconds.
stagestringyesStage name.

SunspotGroupRow

One group of sunspots.

FieldTypeRequiredDescription
area_mshnumber (float)yesCorrected, millionths of the hemisphere.
cmd_degnumber (float)yesArea-weighted central meridian distance, degrees.
idintegeryesNumber from 1, largest first.
lat_degnumber (float)yesArea-weighted latitude, degrees north.
lon_degnumber (float), may be nullnoArea-weighted Carrington longitude, degrees (needs time).
projected_mshnumber (float)yesAs seen, millionths of the visible hemisphere.
spotsintegeryesSpots in it.
umbra_mshnumber (float)yesUmbrae corrected, millionths of the hemisphere.
umbraeintegeryesUmbrae in it.

SunspotRow

One sunspot.

FieldTypeRequiredDescription
area_mshnumber (float)yesCorrected for foreshortening, millionths of the hemisphere.
cmd_degnumber (float)yesCentral meridian distance, degrees, west positive.
groupintegeryesIts group.
idintegeryesNumber from 1.
lat_degnumber (float)yesHeliographic latitude, degrees north.
lon_degnumber (float), may be nullnoCarrington longitude, degrees (needs time).
munumber (float)yesCosine of the angle from disc centre.
penumbra_pxintegeryesPenumbra pixels.
projected_mshnumber (float)yesAs seen, millionths of the visible hemisphere.
umbra_mshnumber (float)yesUmbra corrected, millionths of the hemisphere.
umbra_pxintegeryesUmbra pixels.
umbraeintegeryesUmbrae in it.
xnumber (float)yesCentroid, pixels.
ynumber (float)yesCentroid, pixels, top row 0.

SunspotsConfig

Tuning of the sunspot detection; every field is optional.

FieldTypeRequiredDescription
celestial_north_upbooleannoThe image has celestial north up: add the position angle P of the Sun at the capture time to north_angle_deg. Default false.
group_gap_degnumber (float), may be nullnoSpots closer than this on the Sun, degrees, form one group. Default 3.
max_areanumber (float), may be nullnoLargest spot, millionths of the hemisphere; bigger dark regions are left out as a cloud or vignette. Default 6000.
min_areanumber (float), may be nullnoSmallest spot, millionths of the hemisphere. Default 2.
mirrorbooleannoThe image is a mirror image. Default false.
north_angle_degnumber (float), may be nullnoCounter-clockwise angle from the top of the image to solar north, degrees. Default 0.
penumbranumber (float), may be nullnoPenumbra threshold, a share of the quiet disc (0 to 1). Default 0.88.
umbranumber (float), may be nullnoUmbra threshold, a share of the quiet disc (0 to 1). Default 0.55.
wolf_knumber (float), may be nullnoObserver factor k of the Wolf number. Default 1.

SunspotsSummary

Sunspots on the stack: counts, areas and the table.

FieldTypeRequiredDescription
area_mshnumber (float)yesEverything corrected for foreshortening, millionths of the hemisphere.
b0_degnumber (float), may be nullnoHeliographic latitude of the disc centre, degrees (needs time).
discarray of number (float)yesDisc centre x, centre y and radius the positions are measured on, pixels.
group_tablearray of SunspotGroupRowyesOne row per group.
groupsintegeryesSunspot groups, g.
knumber (float)yesThe observer factor used.
l0_degnumber (float), may be nullnoCarrington longitude of the disc centre, degrees (needs time).
linesarray of stringyesThe report in lines, as the command line prints it.
notesarray of stringyesThings to be careful about.
p_degnumber (float), may be nullnoPosition angle of the solar axis, degrees (needs time).
projected_mshnumber (float)yesEverything as seen, millionths of the visible hemisphere.
spot_tablearray of SunspotRowyesOne row per spot.
spotsintegeryesIndividual spots, s (umbrae and pores).
timestring, may be nullnoCapture time used, ISO 8601 UTC; absent when the capture had none.
umbra_mshnumber (float)yesUmbrae corrected, millionths of the hemisphere.
wolfnumber (float)yesRelative Wolf number k (10 g + s).

TimestampInfo

SER trailer timestamps.

FieldTypeRequiredDescription
countintegeryesHow many.
firststring, may be nullnoFirst, UTC.
laststring, may be nullnoLast, UTC.

TrueColourSummary

What a true-colour job did.

FieldTypeRequiredDescription
channelsarray of ChannelSummary, may be nullnoRed and blue against green; absent when align_channels was off.
gainsarray of number (float)yesGains applied to red, green and blue, including the common scale that keeps the picture inside white.
notesarray of stringyesThe notes written into the file's history, including warnings (a channel held at the largest automatic gain, a shift not applied).

VersionInfo

/version response.

FieldTypeRequiredDescription
apistringyesAPI version in the URL prefix.
auth_requiredbooleanyesWhether a token is required.
backendsBackendsyesBackends.
buildBuildDetailsyesBuild record.
licenceLicenceDetailsyesCopyright and licence.
limitsLimitsyesLimits.
namestringyesPackage name.
versionstringyesPackage version.

Output formats

Helia reads captures and writes one stacked image. This page says exactly what goes into and out of those files today.

What Helia reads

FormatExtensionsNotes
SER (version 3).serMono, raw Bayer (RGGB, GRBG, GBRG, BGGR), RGB and BGR; 8 and 16 bit; per-frame timestamps are read when present.
AVI, uncompressed.aviBI_RGB, Y800/GREY, Y16, and 24-bit BGR; AVI 1.0 and OpenDML (over 1 GB); top-down and bottom-up.

A SER header is read, not believed blindly: Helia checks a file that says it is mono for a hidden Bayer mosaic and the byte-order flag against the data, and --bayer, --bit-depth, --endian and --flip-rows override it (see Troubleshooting). What was decided is recorded in every output file (below).

Samples are scaled to the range 0 to 1 by the size of their container (255 for one byte, 65535 for two), not by the bit depth in the header, because cameras differ on whether 12-bit data is stored at the low or high end of 16 bits. Colour captures are reduced to one plane for stacking; see --channel and --debayer in the CLI reference.

Helia also contains a reader for FITS images (BITPIX 8, 16 and -32, with BZERO and BSCALE, and a BAYERPAT keyword). helia stack does not accept FITS as input, but helia edit reads a TIFF (8, 16 or 32-bit float grey, 8 or 16-bit RGB), a PNG (8 or 16-bit grey or RGB) or a FITS file as the image to edit.

What Helia writes

The output extension chooses the format, in the command line, the app and the API alike.

ExtensionFormat
.tif, .tiff16-bit TIFF, greyscale or RGB
.png8-bit PNG, greyscale or RGB
.fits, .fit, .fts32-bit floating-point FITS: mono, or a three-plane cube (red, green, blue) for a true-colour stack

Any other extension is rejected before stacking starts. The case of the extension does not matter.

The stacked image is normalised so that its 99.99th percentile is 1. Sharpening and the other edits (see Sharpening and adjustments) are applied to it next, and colour last; the result is then written.

A colour look is applied only to TIFF and PNG, and only when one is chosen (--preset or --colour, preset or colour in an API job, or the Colour step of the app). See Colourising. A FITS file is never coloured by a look and its pixels are never changed by one.

True colour (--colour true, colour: "true") is different: the stack itself is colour, made from the red, green and blue planes of a raw colour capture. It goes to a 16-bit RGB TIFF, an 8-bit RGB PNG or a FITS cube of three planes (see True colour).

FITS cube

A true-colour FITS file has NAXIS 3 and NAXIS3 3: plane 1 is red, plane 2 green and plane 3 blue, each 32-bit float, with CTYPE3 = 'RGB' and a comment saying so. The header carries the same SOLARNET keywords as a mono file (the limb is in the pixels of the cube) and a PRSTEP card each for the white balance and the channel alignment. helia edit reads such a file as colour.

TIFF

  • One uncompressed image, 16 bits per sample: greyscale, or three samples per pixel (RGB) when coloured.
  • Samples are clamped to 0 to 1 and scaled to 0 to 65535. Values that are not finite are written as 0.
  • The ImageDescription tag always says how the capture was read: the Bayer pattern, bit depth and byte order with their sources, the SER header's own claim, the mosaic measurement when one was detected, and any overrides, in lines joined with ; (see What the files say). When edits were applied it then lists them (a heading, one line per operation, then the list as JSON), and when a preset or colour was chosen it ends with that (for example Colour is presentation, not measurement: preset ha (H-alpha 656.3 nm, orange-red), invert off, strength 1.00). The other tags are the structural ones that describe the image.

PNG

  • One 8-bit image: greyscale, or RGB when coloured. Samples are clamped to 0 to 1 and scaled to 0 to 255, so it is for sharing; use TIFF to keep 16 bits.
  • A Description text chunk carries the same lines as the TIFF ImageDescription (how the capture was read, the edits when applied, and the colour look when one was chosen), and a Software chunk says Helia.

FITS

  • A single image HDU, BITPIX = -32 (IEEE single precision), two axes.
  • Values are written as computed, without clamping, so a pixel can exceed 1.0 if the brightest few hundredths of a per cent of the stack do.
  • Rows are written bottom row first, as the FITS convention requires, so the image appears the right way up in FITS viewers.
  • The header carries the SOLARNET observation keywords described below, so that astropy, sunpy and other solar tools can read the time, place, exposure, filter, processing and sky coordinates.

Structure keywords

KeywordValue
SIMPLET
BITPIX-32
NAXIS2
NAXIS1width in pixels
NAXIS2height in pixels
CREATOR'Helia'
HISTORYone or more cards of up to 72 characters: how the capture was read, the edits (when they were applied to the pixels, or a note that the file is the unedited stack), then the colour look, in words
END

The FITS pixels are the unsharpened stack by default; sharpening and the other edits are applied to them only when asked for (--sharpen-fits, sharpen_fits in a job, or Edit the FITS too in the app), and the header then says so.

SOLARNET keywords

Helia follows the SOLARNET Metadata Recommendations (Haugan and Fredvik, arXiv:2011.12139). A keyword whose value Helia cannot know is left out, never guessed, and a COMMENT card in the header says which ones are missing and why.

KeywordValueWritten when
EXTNAME'STACKED-IMAGE'always
SOLARNET0.5: partially compliant. Helia does not claim full compliance, which needs more keywords than a stacker can know.always
OBS_HDU1always
DATEUTC time the file was writtenalways
DATE-BEGUTC time the first contributing frame beganthe capture is a SER file with timestamps (else the SER header start time if it is set)
DATE-OBSthe same as DATE-BEG, for readers that only know the older keywordwith DATE-BEG
DATE-AVGmean UTC time of the frames that were keptwith DATE-BEG
DATE-ENDUTC time the last kept frame ended (its start plus its exposure)per-frame SER timestamps
TIMESYS'UTC'with DATE-BEG
TELESCOPtelescope namethe SER header has one
INSTRUMEinstrument name from the SER header, else the camera model in the SharpCap sidecareither has one
OBSERVERobserver namethe SER header has one
FILTERthe filter preset's name (for example H-alpha)a preset was chosen or detected
WAVELNTHline centre in nanometres, in air (H-alpha 656.28, Ca II K 393.37, Ca II H 396.85, Mg I b2 517.27, Na I D 589.3 as the mean of D1 and D2, He I D3 587.56, H-beta 486.13)a line preset, not white light
WAVEUNIT-9: WAVELNTH is in units of 10^-9 mwith WAVELNTH
WAVEREF'air'with WAVELNTH
TEXPOSURexposure of one frame, secondsthe SharpCap sidecar (Exposure=) is present
XPOSUREaccumulated exposure: TEXPOSUR times NSUMEXPwith TEXPOSUR
NSUMEXPnumber of frames that were kept and summedalways
OBSGEO-X, OBSGEO-Y, OBSGEO-Zobserver position in metres, ITRS (WGS 84), from the sitea site was given (see below)

The processing is recorded as one numbered group per step, in the order they ran: PRSTEPn (the kind of step), PRPROCn (what did it), PRPVERn (the Helia version, for example 'helia 0.5.0') and PRPARAn (the settings, shortened to fit a card). The steps Helia writes are

PRSTEPnWritten whenPRPARAn says
DARK-SUBTRACTIONa master dark was applied (first of the steps, before FRAME-SELECTION)the master: frames, how they were combined, exposure, source and a fingerprint of its pixels
FLAT-FIELDINGa master flat was appliedthe same, for the flat
BAD-PIXEL-CORRECTIONbad pixels were replacedhow many, and how many of them hot, cold or stuck
FRAME-SELECTIONalwaysthe sharpness metric and how many of how many frames were kept
DEBAYERthe capture is raw colourdemosaicing method and colour channel
MASKINGtext or exclusions were flattened, or the limb was fittedhow many regions, and whether the limb fit ran
GLOBAL-ALIGNMENTalwayshow many frames built the reference and the mean confidence of the frames' alignment; PRPROCn names the method that ran (limb fit or phase correlation, see How stacking works)
LOCAL-ALIGNMENTmulti-point alignment ranthe alignment point size
STACKINGalwaysthe combining mode and resampling kernel
NORMALISATIONthe stack was normalised (the default)99.99th percentile scaled to 1

The three calibration steps are Helia's own words for them: the SOLARNET recommendation lists PRSTEPn values only as examples, and these have not been checked against a reader that insists on a fixed vocabulary.

A HISTORY card also names the Helia version, and carries how the capture was read (Bayer pattern, bit depth and byte order with their sources, the SER header's own claim, the mosaic measurement and any overrides) and the colour look when one was chosen (see Colourising).

World coordinates

When a limb was fitted (see How stacking works) the header carries a helioprojective coordinate system in the TAN projection (Thompson 2006), so that sunpy.map.Map builds a helioprojective map from the file:

KeywordValue
WCSNAME'Helioprojective (limb fit, approximate)'
CTYPE1, CTYPE2'HPLN-TAN', 'HPLT-TAN'
CUNIT1, CUNIT2'arcsec'
CRPIX1, CRPIX2disc centre from the limb fit, in FITS pixels (counted from 1, the second axis from the bottom row)
CRVAL1, CRVAL20, 0: the disc centre is taken as the Sun's centre
CDELT1, CDELT2arcseconds per pixel: the apparent solar radius divided by the fitted limb radius in pixels
CROTA20. The orientation of the picture on the sky is not known, so this does not mean that solar north is up.
LONPOLE180
RSUN_REF695700000.0: the nominal solar radius in metres
RSUN_ARC, RSUN_OBSthe apparent solar radius in arcseconds that CDELT was computed from (RSUN_OBS is the same value, for sunpy)

The coordinate system is approximate. The apparent solar radius comes from the date (the mean frame time) and a low-precision formula for the Earth to Sun distance, which agrees with the sunpy ephemeris to a few hundredths of an arcsecond; with no date it assumes 1 AU, which can be wrong by up to 1.7 per cent, and the header says so. No observer position (DSUN_OBS, HGLN_OBS, HGLT_OBS) is written, so a reader derives it from the date and assumes an Earth-based observer. If no limb is found there is no world coordinate system and no RSUN_* keywords.

Giving the observing site

OBSGEO-X, OBSGEO-Y and OBSGEO-Z need the telescope's position, which no capture file holds. Give it as LAT,LON,ALT: degrees north, degrees east, and metres above the WGS 84 ellipsoid (ALT may be left out for 0).

Front endHow
Command line--site 51.5,-0.1,30
API"site": "51.5,-0.1,30" in the job's config
Desktop appthe HELIA_SITE environment variable
Any of themHELIA_SITE=51.5,-0.1,30 is used when nothing more specific is given

Without a site the three keywords are left out, a COMMENT card says so, and the command line prints a note.

Reading the file

import sunpy.map
m = sunpy.map.Map("sun.fits")      # a helioprojective GenericMap
print(m.scale, m.rsun_obs, m.reference_date, m.exposure_time)

The test suite does exactly this on a stack of a synthetic capture and asserts the header values: crates/helia-core/tests/solarnet_fits.rs and solarnet_check.py. The solarnet CI job installs astropy and sunpy for it.

Commentary

The HISTORY cards are text of up to 72 characters each (longer lines are split, and anything that is not printable ASCII is written as ?). They record how the capture was read, and the look that a coloured copy of the same stack would use, so the data and the colour file can be matched later; the pixels are exactly those written without them.

The FITS writer in helia-core can also write unsigned 16-bit data (BITPIX = 16 with BZERO = 32768 and BSCALE = 1), but nothing in the command line, the app or the API selects it today.

Not written yet: BUNIT (the stack is normalised, not calibrated), WAVEMIN/WAVEMAX (the filter bandwidth is not known), DSUN_OBS and the heliographic observer keywords, and any rotation of the picture on the sky.

IdFilterWavelengthHueRamp, black to whiteMid-grey becomesBayer channelFrames kept
haH-alpha656.3 nmorange-red #A95A21red (r)25%
cakCa II K393.4 nmviolet #5D3D9Bblue (b)30%
cahCa II H396.8 nmviolet #703096blue (b)30%
mgMg I b517.3 nmgreen #429B42green (g)30%
naNa I D589.0 / 589.6 nmyellow-orange #A06212green (g)12%
he-d3He I D3587.6 nmyellow #9B8617all combined (lum)25%
hbetaH-beta486.1 nmblue-cyan #2474A0blue (b)30%
whiteWhite lightcontinuumwarm neutral #807A70all combined (lum)12%

Why each preset recommends what it does:

  • H-alpha (ha): H-alpha is deep red, so a colour camera records it almost only in the red pixels; the green and blue pixels add noise, not detail.
  • Ca II K (cak): Ca K is at the violet edge of vision and a sensor sees it in its blue pixels. The line is faint, so a few more frames (30%) are kept to average the noise down.
  • Ca II H (cah): Ca H sits 3.4 nm from Ca K, so it takes the same channel and frame share. It is drawn a touch redder so the two can be told apart in a pair.
  • Mg I b (mg): 517.3 nm is green, which is where the green pixels of a Bayer sensor respond. The line is dim, so 30% of the frames are kept.
  • Na I D (na): 589 nm is yellow, which a Bayer sensor splits between its red and green pixels, and the sodium filter passes almost nothing to the blue ones. Averaging all three channels mixes those empty blue pixels into the stack and prints the mosaic as a fine diagonal hatch over the granulation, so the green channel (two pixels in four) is stacked: on the owner's captures it holds 1.4 to 2.1 times the granulation contrast of the luminance. It shows granulation, so only the best 12% of frames are kept and the alignment points are a few granules wide.
  • He I D3 (he-d3): 587.6 nm is yellow-green, shared by the red and green pixels, so the luminance of all three is used. It is drawn yellower than sodium.
  • H-beta (hbeta): 486.1 nm is blue-cyan and a sensor sees it in its blue pixels. It is faint, so 30% of the frames are kept.
  • White light (white): A white-light capture already carries all three colours, so the luminance is stacked, and a slight warm tint stands in for the solar spectrum. It shows granulation, so only the best 12% of frames are kept and the alignment points are a few granules wide.

The numbers behind each colour, for anyone who wants to reproduce it:

IdTone exponentGains (R, G, B)Exponents (R, G, B)
ha0.851.00, 0.85, 0.600.70, 1.50, 2.60
cak0.900.85, 0.78, 1.001.35, 1.90, 0.80
cah0.900.90, 0.70, 1.001.15, 2.10, 0.85
mg0.900.80, 1.00, 0.751.80, 0.80, 1.70
na0.901.00, 0.74, 0.400.75, 1.05, 2.80
he-d30.901.00, 0.92, 0.450.80, 0.90, 2.60
hbeta0.900.55, 0.85, 1.002.20, 1.00, 0.75
white1.001.00, 0.96, 0.881.00, 1.00, 1.00

The recommended sharpening of each filter (see Sharpening and adjustments). Wavelet gains and noise cuts are for layers 1 to 6, finest first; the unsharp radius comes from the image scale, and the deconvolution blur is measured from the disc.

IdWavelet gainsWavelet noise cutsUnsharp amountUnsharp thresholdDeconvolution iterationsTV weightDenoise
ha1.50, 1.45, 1.25, 1.05, 1.00, 1.001.80, 1.00, 0.50, 0.00, 0.00, 0.000.600.003120.0041.5
cak1.30, 1.40, 1.25, 1.05, 1.00, 1.002.50, 1.50, 0.80, 0.30, 0.00, 0.000.450.00480.0052.0
cah1.30, 1.40, 1.25, 1.05, 1.00, 1.002.50, 1.50, 0.80, 0.30, 0.00, 0.000.450.00480.0052.0
mg1.40, 1.40, 1.20, 1.05, 1.00, 1.002.20, 1.30, 0.60, 0.00, 0.00, 0.000.500.004100.0051.8
na1.20, 1.80, 1.50, 1.10, 1.00, 1.002.00, 0.00, 0.00, 0.00, 0.00, 0.000.500.003100.0051.5
he-d31.30, 1.35, 1.20, 1.05, 1.00, 1.002.50, 1.50, 0.80, 0.20, 0.00, 0.000.400.00480.0052.0
hbeta1.30, 1.40, 1.25, 1.05, 1.00, 1.002.50, 1.50, 0.80, 0.30, 0.00, 0.000.450.00480.0052.0
white1.50, 1.80, 1.50, 1.10, 1.00, 1.001.00, 0.00, 0.00, 0.00, 0.00, 0.000.800.002200.0031.0

Why each filter is sharpened the way it is:

  • H-alpha (ha): H-alpha has plenty of signal and its fibrils and spicules sit on layers 2 and 3, so those layers are lifted most; the finest layer gets a high noise threshold.
  • Ca II K (cak): Ca K is faint and noisy, so the gains are gentler, the noise thresholds higher and the deconvolution short: sharpening noise only makes a prettier mess.
  • Ca II H (cah): Ca H is as faint as Ca K and takes the same careful settings.
  • Mg I b (mg): Mg b2 is dim, so the noise thresholds are a little higher than for H-alpha and the deconvolution shorter.
  • Na I D (na): Sodium D is photospheric: granulation is its finest low-contrast detail. Layers 2 and 3 (granule size) are lifted most and carry no noise threshold, because a threshold there removes granulation with the noise; the finest layer keeps its threshold.
  • He I D3 (he-d3): He D3 shows only in faint prominences and flares, so it is sharpened with the most restraint.
  • H-beta (hbeta): H-beta is faint, like the calcium lines, and takes the same gentle settings.
  • White light (white): White light has the most signal and the finest structure (granulation), so it takes the strongest gains and the longest deconvolution. Layers 2 and 3 (granule size) are lifted most and carry no noise threshold, because a threshold there removes granulation with the noise.

Configuration options

Helia has no configuration file for stacking. Every setting is chosen per run, in the same way in all three front ends. (The desktop app keeps a small settings file of its own for things it should not ask twice; see Using the desktop app.) This table shows each setting under its name in the command line, the API job config object, and the desktop app.

SettingCommand lineAPI config fieldDesktop appDefault
Share of frames to keep--keep 25%keep ("25%" or "500")Keep best frames, 5 to 100 per cent in steps of 5the preset's (25% or 30%), else 25%
Number of frames to keep--keep-frames Nkeep_framesnot available (use the share)none
Sharpness metric--metric gradient or laplacianmetricSharpness metricgradient
Alignment point size (pixels)--ap-size Nap_size (8 to 1024)Point size: 32 to 96 in steps of 8chosen from the image size
Automatic alignment points--ap-autoap_auto (excludes ap_size)Align step: Auto chipoff
Alignment point passes--ap-passes 1 or 2ap_passesnot available1
Smooth outlier alignment point shifts--ap-smooth on or offap_smoothnot availableoff
Telescope aperture (mm)--aperture-mm MMaperture_mmnot availablenone
Find sunspots--sunspots (and helia sunspots IMAGE); --sunspot-umbra, --sunspot-penumbra, --sunspot-min-area, --sunspot-max-area, --sunspot-group-gap, --wolf-k, --north-angle, --celestial-north-up, --mirrorsunspots (an object with umbra, penumbra, min_area, max_area, group_gap_deg, wolf_k, north_angle_deg, celestial_north_up, mirror)Stack step: Sunspots chips, observer factor, celestial northoff
Estimate the seeing--seeingseeingStack step: the seeing line (always on; aperture under Advanced settings)off
Crop to the disc--autocropautocropStack step: Crop, Disc chipoff
Crop margin (per cent of the radius)--autocrop-margin Pautocrop_margin (0 to 200)Stack step: Margin slider, 0 to 5010
Prominence layer--prominence, --prominence-strength, --prominence-stretch, --prominence-level, --prominence-feather, --prominence-reach, --prominence-gatean edits entry {"op": "prominence", ...}Adjust step: Prominence layer and its slidersoff
Colour table exporthelia lut PRESET -o FILE (--format, --size, --invert, --strength)GET /presets/{id}/lutColour step: Colour tablenot applicable
Limb-darkening law fitted--limb-law linear, quadratic or neckellimb_lawSharpen step: law chips under Limb darkeningquadratic
Flatten the limb darkening--flatten model or profileflatten (or a flatten edit)Sharpen step: Limb darkening Off, Model, Profileoff
Global alignment method--align auto, phase or limbalignchosen automatically; shown after a stackauto
Multi-point alignment--no-multipoint turns it offmultipoint (true or false)always onon
Sigma clipping--sigma-clip Ksigma_clip (a number)Outliers: Sigma chip (fixed at 2.5)off
Robust combination--robust none, sigma, huber or winsorrobustOutliers: Off, Sigma, Huber, Winsornone (weighted mean)
Robust threshold (standard deviations)--robust-k Krobust_knot available (the default for the kind)2.5 sigma, 1.345 huber, 2 winsor
Compute backend--backend auto, cpu or gpubackendBackend: Auto, CPU, GPUauto
Bayer pattern--bayer auto, none, rggb, grbg, gbrg or bggrbayerBayer pattern in the Open inspectorauto (the header, with a pixel check of a SER file that says mono)
Bit depth of a SER file--bit-depth N (1 to 16)bit_depthBit depth in the Open inspector: Auto, 8-bit, 16-bitthe SER header's
Byte order of a SER file--endian little or bigendianByte order in the Open inspectorthe header flag, checked against the data
Row order of a SER file--flip-rowsflip_rowsRow order in the Open inspectortop to bottom
Colour channel--channel r, g, b or lumchannelAdvanced settings: Channelthe preset's, else lum
Demosaicing method--debayer bilinear or mhcdebayerDebayer: methodmhc
Filter preset--preset ha (see helia presets)presetQuick stack card: the filter buttonsdetected from the file name, folder, sidecar or header
Colour--colour auto, mono or a preset idcolourColour step: the look cardsnone (mono); the preset's look in the app
Invert before colouring--invertinvertColour step: Invertoff
Colour strength--colour-strength S (0 to 1)colour_strengthColour step: Strength1
Output formatextension of -oextension of outputExport: TIFF 16-bit, PNG 8-bit or FITSTIFF in the app; by extension elsewhere
Observing site (FITS OBSGEO-X/Y/Z)--site LAT,LON,ALTsite ("lat,lon,alt")the HELIA_SITE environment variablenone; HELIA_SITE if set
Hide progress and summary-q, --quietnot applicablenot applicableshown

The settings that are not exposed (the search radius, correlation thresholds, smoothing and similar internals of the alignment) are fixed to the defaults in MultipointConfig and PhaseOptions in helia-core. They can be changed from Rust code through the library, but not from the programs.

Server settings

These apply to helia serve only. See The API.

SettingOptionDefault
Listen address--bind127.0.0.1:7870
Directory requests must stay inside--data-rootcurrent directory
Bearer token--token, --token-file, or the HELIA_TOKEN environment variablenone
Concurrent jobs--workers1
Queued plus running jobs allowed--max-queue16
Browser origins allowed--cors-origin (repeatable)none
Extra accepted Host headers--allow-host (repeatable)none

Environment variables

VariableUsed byEffect
HELIA_TOKENhelia serveBearer token, used when neither --token nor --token-file is given.
HELIA_SITEhelia stack, helia serve, helia-guiObserving site LAT,LON,ALT for a FITS output, used when --site or the job's site is not given.
HELIA_GPU_TESTStest suitessoftware, hardware or none; see Building and testing.
HELIA_FEEDBACK_URLhelia feedback, helia-guiAddress of the feedback service, instead of the built-in public Helia feedback service. HTTPS only (HTTP for localhost). Overrides feedback_url in the app's settings file; helia feedback --url overrides this. See Sending feedback.
HELIA_REAL_DATAtest suitesDirectory of real captures for the integration tests.
UPDATE_SNAPSHOTSGUI snapshot testsSet to 1 to regenerate the screenshots.
HELIA_LOG_DIRhelia, helia serve, helia-guiFolder for the log file helia.log, instead of the OS default. See Log file and bug report.
HELIA_GUI_CONFIGhelia-guiPath of the app's settings file; see Using the desktop app.

Feedback protocol

This page specifies how Helia sends feedback to the feedback relay, so that the relay and any other client can be written against it. The Rust types are in the helia-feedback crate (protocol and pow modules); the relay can depend on that crate with default-features = false and use them directly, so the code and this page cannot drift apart.

Why there is a relay

Registration on the project's forge is closed, so people without an account cannot open an issue there. The relay accepts a report from the app, checks it, and files the issue as a bot account. The app and helia feedback use it unless told otherwise (see Configuration).

The relay is only ever contacted when a person presses Send in the app or runs helia feedback. Helia has no telemetry and never contacts the relay on its own.

Overview

  1. GET /api/v1/challenge returns a challenge.
  2. The client finds a nonce that solves it (proof of work).
  3. POST /api/v1/feedback sends the report, with the proof, and optionally a screenshot.
  4. The relay answers with the new issue's number and address.

All requests are over HTTPS. A client accepts plain http:// only for a loopback address (tests and local development).

GET /api/v1/challenge

Answer 200:

{
  "challenge": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
  "difficulty": 20,
  "expires": "2026-10-19T20:15:00Z"
}
FieldMeaning
challenge32 random bytes as 64 hexadecimal characters.
difficultyThe number of leading zero bits the hash must have. 20 takes a client about a million hashes, well under a second.
expiresAn RFC 3339 time after which the relay refuses this challenge.

A client refuses a difficulty above 28 rather than work for minutes.

Proof of work

A nonce, an unsigned 64-bit integer, solves a challenge when

SHA-256( challenge_bytes || nonce as 8 little-endian bytes )

has at least difficulty leading zero bits. challenge_bytes are the 32 bytes the hexadecimal challenge decodes to, not the text. The client searches nonces from 0 upwards and sends the first that works.

The relay recomputes the hash, checks the difficulty it issued, checks that the challenge was issued by it and has not expired, and accepts each challenge once.

Test vector. Challenge 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f (the bytes 0 to 31):

DifficultySmallest solving nonceDigest
1210480008085f99feb36603e7f084fc76b5d4e781c672ec1b097e39b730fee1066e72
1614417000011acd5d18fd8abb6021f278532519f3226a075b84d2cbbcf9281dc06f13f

The same vector is a unit test in helia-feedback (pow::tests).

POST /api/v1/feedback

Content-Type: multipart/form-data, with these parts:

PartTypeRequiredContent
reportapplication/jsonyesThe report, below.
screenshotimage/pngnoThe clean screenshot, at most 5 MiB (5 242 880 bytes). The highlights are not drawn on it; the relay draws them from the JSON.

The report

{
  "kind": "bug",
  "title": "The sharpening preview is blurry at 100%",
  "description": "I zoomed to 100% on the Sharpen step...",
  "app": {
    "name": "helia",
    "version": "0.3.0",
    "commit": "1a2b3c4d",
    "target": "x86_64-unknown-linux-gnu",
    "gpu": "Vulkan: Example GPU"
  },
  "os": "Debian GNU/Linux 13 (trixie) (x86_64)",
  "highlights": [{ "x": 1010, "y": 90, "w": 400, "h": 150 }],
  "pow": { "challenge": "0001...1e1f", "nonce": 14417 }
}
FieldTypeRules
kindstringbug, feature, question or other.
titlestring1 to 120 characters after trimming.
descriptionstring0 to 8000 characters. When the sender chose to include system details this ends with a line Open capture: <file name>, <width>x<height>, <bit depth>, <frames> frames; never a folder path.
appobject or nullPresent only when the sender chose to include system details. gpu may be null. commit ends in +dirty for a modified source tree.
osstring or nullAs app. Never a host name or user name.
highlightsarrayAt most 32 rectangles, each with w and h above zero, in the pixels of the screenshot that is sent (the client rescales them if it had to shrink the image to fit 5 MiB). Empty when there is no screenshot.
powobjectThe challenge exactly as issued and the nonce found. Required by the relay today (see Authentication).

Unknown members are ignored. Every limit above is enforced by the relay as well as by the client: the types and Report::validate in helia-feedback are the single definition.

The relay is expected to file the issue with the title [<Kind label>] <title> (Bug, Feature request, Question, Other), the description, a block of the system details, the screenshot with the highlights drawn on it, and the label for the kind. It should treat every field as untrusted text.

Answers

StatusMeaningBody
201Filed.{"issue_number": 81, "issue_url": "https://tracker.example/helia/issues/81"}
400The report breaks a rule, or the proof of work is wrong, expired or used.{"error": {"code": "...", "message": "..."}}
413The request or the screenshot is too large.the same error object
415A part has the wrong media type (the screenshot is not a PNG).the same error object
429Too many reports. A Retry-After header gives the wait in seconds.any
503The relay is degraded (for example, it cannot reach the forge).any

code is a short machine-readable word such as title_too_long, bad_pow or screenshot_too_large; message is for people and is shown to the sender.

What the client does when this fails

The same 429, 503, connection failures and anything unexpected all lead to the same place: the app tells the sender what happened and offers Save report (a .zip with report.json, screenshot.png with the highlights drawn on, and screenshot-clean.png) and Open in browser (the forge's new-issue page with the title and text filled in, for people who have an account). helia feedback prints the same address and, with --save, writes the zip. See Sending feedback.

Authentication

How a submission proves it is not a flood is deliberately separate from the report format. In the client it sits behind one small interface, RelayAuth in helia_feedback::client: it supplies the HTTP agent and prepares an AuthProof that goes into the report.

MethodStateHow
Proof of workImplementedThe challenge and nonce above, in the pow member.
Mutual TLSPlannedThe project will eventually have an API that accepts it with a TLS client certificate. A client presents the certificate when it connects; no proof of work is needed, so pow is absent from the report.

Adding the planned method means adding another RelayAuth implementation; the feedback page, the command and the report format do not change. It is not implemented yet, and a relay should keep requiring pow until it is.

Versioning

The path carries the version (/api/v1/). A change that is not backwards compatible gets a new path; adding an optional member to the report is compatible.

Keyboard shortcuts

The desktop app's shortcuts, in one place.

KeysAction
Ctrl+OOpen a capture
Ctrl+EExport the result
Left, RightPrevious or next frame
Shift+Left, Shift+RightTen frames back or forward
Home, EndFirst or last frame
SpaceSwitch between the frame and the stacked result
F1Open or close the About page (Esc also closes it)
Tab, Shift+TabMove between controls

The arrow keys and Space act on the viewer when no control has keyboard focus.

In the tone curve plot (Adjust step)

Click the plot, or Tab to it, then:

KeysAction
[ and ]Choose the previous or next point
Arrow keysNudge the chosen point by 0.005
Shift+arrow keysNudge it by 0.05
Delete, BackspaceRemove the chosen point (not the end points)

With the mouse

ActionEffect
Scroll over the pictureZoom around the pointer
Drag the picturePan
Double-click the pictureFit it to the window
Click the zoom readoutFit it to the window
Click the quality curveJump to that frame
Drag the frame scrubberMove through the capture
Click empty space in the tone curve plotAdd a curve point
Drag a curve pointMove it (it stops at its neighbours)
Double-click or right-click a curve pointRemove it
Drag a file onto the windowOpen it

Accessibility

Every control has an accessible name and a tooltip, and the window works with screen readers through AccessKit. Progress notices announce the stage, the percentage and the time left in words.

Glossary

Alignment point (AP). A small square window on the disc. Helia measures how each frame has drifted at each point and shifts that part of the frame back, so the distortion the atmosphere adds across the Sun is corrected locally. See Align.

Artefact. A feature in a picture that is not in the Sun: a halo beside a sharp edge, ringing round a sunspot, noise turned into texture, a grid from a mosaic.

Bayer pattern. The repeating 2 x 2 arrangement of red, green and blue filters on a colour camera's sensor (RGGB, GRBG, GBRG or BGGR). Raw captures hold one colour per pixel until they are debayered. See Fixing a Bayer grid.

Bit depth. Bits per pixel in the capture, 8 or 16. More bits keep fainter contrast.

Channel. One colour plane of a colour camera: red, green, blue, or the combined luminance (lum). Narrow-band solar data is one colour of light, so Helia stacks one channel; the recommended one depends on the filter.

Point spread function (PSF). The blur that the telescope, the air and the camera put on a point of light. Deconvolution tries to undo it.

Debayer, demosaic. Turning a one-colour-per-pixel mosaic into a full-colour picture. Helia offers bilinear and Malvar-He-Cutler (the default).

Deconvolution. Sharpening by undoing a known blur. Helia uses Richardson-Lucy with a limb and sunspot guard.

FITS. The astronomy image format. Helia writes 32-bit floating-point mono FITS with solar metadata (SOLARNET keywords) in the header.

Frame. One picture in the capture video.

Gain (sensor). The camera's amplification. In Helia's sharpening, the gain of a wavelet layer is its multiplier.

Granulation. The fine cell pattern of the Sun's visible surface, a few per cent in contrast, seen in white light.

Limb. The edge of the solar disc. Helia can fit it to align whole frames.

Lucky imaging. Recording many short frames, keeping the sharpest and averaging them, so the best moments of the seeing win. What Helia does.

Noise cut. In wavelet sharpening, detail weaker than this many noise sigmas in a layer is treated as noise and removed.

Phase correlation. A way of finding the shift between two pictures by comparing their frequency content. Helia uses it to align whole frames when the limb fit is not confident.

Pre-release. A version before 1.0.

Quality. A frame's sharpness score, scaled so that the best frame in a capture is 1 and the worst is 0. Rank is the position when the frames are sorted by it; rank 1 is the sharpest.

SER. A simple uncompressed video format made for astronomy, which stores the colour layout and optional timestamps. Preferred over AVI and MP4.

Seeing. The blurring and shifting of the image by turbulence in the atmosphere. It changes many times a second, which is why frames differ.

SharpCap sidecar. The CameraSettings.txt file SharpCap writes beside an AVI, which Helia reads to learn the camera, the Bayer pattern and sometimes the filter.

Sigma clipping. Throwing out pixels that are far from the average of the frames before averaging them.

SOLARNET. The metadata recommendations for solar image files, which Helia follows (partially) in FITS output.

Stack. The single picture made by combining the best frames. Also the verb.

TIFF, PNG. The picture formats Helia writes: 16-bit TIFF and 8-bit PNG, greyscale or colour.

Unsharp mask. Sharpening by adding back a share of the difference between the picture and a blurred copy.

Wavelet layers. A way of splitting a picture by the size of detail. Layer 1 is the finest detail (and most of the noise); higher layers are larger structure. Each layer has a gain and a noise cut.

Weighted mean. An average in which each frame counts in proportion to its quality.

Architecture

Helia turns a capture of the Sun (thousands of seeing-blurred frames) into one sharp image. This page is the map: which crate does what, how data flows, and the rules that keep the CPU and GPU paths honest.

Crate map

CrateKindRole
helia-corelibraryAll image maths and file formats. No GPU code, #![forbid(unsafe_code)].
helia-gpulibrarywgpu compute implementation of the Backend trait that lives in helia-core.
helia-serverlibraryaxum HTTP/JSON API over the pipeline (inspect, previews, jobs, WebSocket events, OpenAPI). Embedded by the CLI; the GUI can embed it too. See the API chapter of the book.
helia-feedbacklibraryThe feedback relay protocol (wire types, proof of work), screenshot highlights and burn-in, the relay client with its authentication interface, and the save and browser fallbacks. The relay reuses the protocol types with default-features = false.
helia-clibinary heliahelia stack, helia calibrate, helia info, helia presets and helia serve, with stage lines and a progress bar.
helia-guibinary helia-guiegui front end: pipeline rail, viewer, quality curve, inspector, activity notices and toasts, Quick stack and Colour steps.
fuzzcargo-fuzz crate, not a workspace memberFeeds arbitrary bytes to the SER, FITS and AVI readers, and the ffprobe JSON and sidecar parsers of the video path.

helia-core modules:

  • io: ser (SER v3 reader and writer), avi (uncompressed AVI, including OpenDML), fits (reader and writer, with HISTORY cards), tiff (16-bit grey and RGB writer), png (8-bit grey and RGB writer), sidecar (SharpCap CameraSettings.txt hints), solarnet (the SOLARNET FITS keywords: capture times, exposure, site, filter wavelength, processing steps and the helioprojective coordinate system from the limb fit), ffmpeg (compressed video through an optional external ffmpeg, below), source (the FrameSource trait that hides which container a capture came from, and OpenOptions, the user's overrides), detect (finds a Bayer mosaic in frames a SER header calls mono, from the 2 x 2 pixel-position means on the disc) and capture (opens a capture with the overrides and returns a CaptureReport: the header as written, the values used with their source, the detection and the notices, which helia info, the app's Open step and the API all print).
  • calibrate: dark, flat and bad-pixel calibration. master builds a master dark, bias or flat from calibration captures (SER, AVI, video, FITS stills or a folder of them) by median or sigma-clipped mean, scaling and normalising a flat per colour of the mosaic; badpix finds hot, cold and stuck pixels and replaces them with the median of their same-colour neighbours; diag reads a flat for Newton rings, etalon fringes, vignetting and dust; spec turns "this dark, this flat, this map" (master files or the captures to make them from) into a Calibration, which Calibration::plan checks against the frames and compiles to per-pixel tables; source::CalibratedSource wraps a capture so that every frame it serves is calibrated, in read and in the raw bytes the CPU and GPU decoders share (as f32, before debayering, scoring or alignment).
  • debayer: bilinear and Malvar-He-Cutler demosaicing to a mono image, with a choice of red, green, blue or mean channel.
  • quality: sharpness metrics, ranking, Keep (a percentage or a count) and the soft weighting curve.
  • align: global (FFT phase correlation), limb (global shift from a robust circle fit to the solar limb, with a confidence), method (the choice between the two, per-frame confidence and the report shown to users) and multipoint (alignment point grid, local shifts, blending).
  • resample, stack: sub-pixel interpolation and the accumulators.
  • backend: the Backend and Accumulator traits and the CPU implementation.
  • mask: what the alignment must not look at. detect_overlay finds burned-in text, fit_disc fits the limb, and MaskedSource wraps a capture so that masked rectangles read as one flat level, in read and in the raw bytes the GPU decodes.
  • pipeline: the end-to-end driver.
  • about: the author, copyright and licence notices, and the build record (commit, date, profile, target, compiler) that build.rs captures. Shared by --version, the About page and GET /api/v1/version.
  • colour: the solar filter presets (a look, a recommended Bayer channel and frame share each), the colour mapping sampled into a 4096-entry ColourLut, and the CPU reference apply_lut; also detection of the filter from text and the alignment point size for an image scale. Backend::colourise defaults to the CPU reference and the GPU backend overrides it with a kernel.
  • recommend: the "just press Stack" settings for a capture, from the filter (chosen, or read from the file name, folder, sidecar and SER header) and the image size, including the default sharpening of the filter.
  • edit: sharpening and everyday edits as an ordered, recorded EditList of Ops (serde, so JSON in presets, the API and file metadata): wavelet (the a-trous transform, unsharp mask, denoise), deconv (Richardson-Lucy with total variation, the PSFs and the limb and sunspot ringing guard), tone (levels and a monotone curve), geometry (crop, rotate, flip, resize), defaults (the per-filter values, which live beside the colour presets in colour), options (named options such as the command line's to a list), parse, store (saved presets) and conv (the convolutions). Backend::apply_edits defaults to the CPU reference, edit::apply_cpu, and the GPU backend overrides it.
  • output: the last step, shared by the command line, the app and the API: edits, then colour, then the file with the edits recorded in it; FITS is the unedited stack unless asked. io::any reads TIFF, PNG and FITS back for helia edit.
  • testutil (feature testutil): synthetic suns with known ground truth.

Compressed video

MP4, MOV, M4V, MKV, WebM and AVI files with a compressed codec go through the ffmpeg and ffprobe programs, found on the PATH or in HELIA_FFMPEG and HELIA_FFPROBE. Helia never links or bundles them; they are an optional external tool, run as child processes from an argument vector (never a shell), with the input as an absolute path after -i, -protocol_whitelist file, and no user text as an option.

The pipeline reads a capture more than once and out of order, and the GUI scrubs it, but video codecs only seek to key frames. So io::ffmpeg decodes a video once into an uncompressed SER file in a cache directory (the OS cache folder, --cache-dir or HELIA_CACHE_DIR), with a free-space check first, and the capture is then an ordinary SER source: random access, the concurrent decoder, GPU decode. 8-bit video stays 8-bit (gray) and anything deeper is gray16le; a colour channel chosen with --channel is extracted by ffmpeg (extractplanes), so the cache holds one plane. ffmpeg applies the file's rotation. The file is deleted when the last handle to it drops (a VideoSource or the keep_alive handle the GUI holds), cancelling kills the child, and an older leftover is swept on the next decode.

Streaming each pass straight from ffmpeg was the alternative, and measured slower. On a 3840 by 2160 8-bit H.264 clip (898 frames, 25 fps, 96 Mbit/s), on an AMD EPYC 7543 with 24 threads and an A100 (the project's GPU build machine), at this change: decoding into the cache took 12.7 s (about 70 frames per second, including writing 7.5 GB), and the rest of the stack, from the cache, 16.3 s. ffmpeg alone decodes about 100 to 125 frames per second there, so a streamed pass over the same 898 frames costs 7 to 9 s, and the pipeline makes at least four (score, global alignment, alignment points, stacking), plus the GUI's random access. The cache costs disk space instead: a frame is width * height bytes (8.3 MB at 4K), which is why the free-space check exists. Numbers for other hardware will differ; the ratio is what carries over.

Data flow

capture file --FrameSource--> raw frame --debayer--> mono Frame (f32, 0..1)
  0a. calibrate (optional): (raw - dark) / flat, bad pixels replaced, applied to
      the stored plane of every frame as it is read, before the debayer
  0. mask burned-in text and user exclusions (flat in every frame read below)
  1. score every frame            (quality, noise floor subtracted)
  2. rank; keep best N            (--keep 25% or --keep-frames N)
  3. reference = aligned mean of the best few frames
  4. register candidates globally: limb fit when the full disc is in view and
     the fit is confident, else phase correlation (sub-pixel), each with a
     confidence
  5. multipoint: per alignment point and frame, measure the local shift
     by correlation, score the window, and let each point pick its own
     best frames
  6. stack: weighted mean of each point's frames (optionally sigma clip, Huber
     or winsorised: two passes, the second using the first's mean and spread)
  7. blend the points with raised-cosine weights; the global stack fills
     featureless areas
  8. normalise and write 16-bit TIFF or FITS (the FITS header carries SOLARNET
     metadata built from the capture, the limb fit and the list of steps run)
  9. optionally sharpen and adjust the mono stack (`edit`), then colour it
     through a preset's table and write an RGB TIFF or PNG (FITS stays mono and
     is the unedited stack unless asked; the edits and the look are recorded in
     the file)

Frames are streamed in batches of about two per thread and are never all held in memory. The capture is read once to score, once to register, once to measure alignment points and once or twice to stack (twice only with a robust mode: sigma clip, Huber or winsorised mean). Progress and cancellation are reported through a callback and an AtomicBool.

Where the time goes

helia stack --timings prints wall time per stage and busy time per kind of work summed over threads (helia_core::profile). Wall time says what to wait for; busy time says what to optimise, because stages overlap decoding with maths on every thread.

How the hot paths are built:

  • Decode. io::raw::RowLayout turns stored bytes into normalised rows; FileFrames reads with positional I/O (safe std, no memory map, so helia-core stays free of unsafe) and implements SharedFrames, which the pipeline calls from many threads at once. For a Bayer capture the rows go straight into Debayer::apply_rows, a fixed 5x5 stencil per Bayer phase, so no raw f32 frame is ever materialised. Sources that cannot be read concurrently use the sequential FrameSource::read path.
  • Quality. Row-based 3x3 blur and gradient with no per-pixel closures; WindowScorer reuses one blurred frame for every alignment-point window.
  • Limb fit. 720 rays from the reference disc sample the lightly blurred frame every half pixel; the edge on a ray is the centroid of the fall in brightness where it exceeds a quarter of its peak, which averages noise over the whole edge and is unbiased for a symmetric blur. A Tukey-weighted Gauss-Newton fit gives the circle; a second pass uses a window sized from the first pass's edge width. The centre error is the larger of the model estimate and a delete-one-eighth jackknife, which is honest about noise shared by neighbouring rays. A limb cut by the frame holds the reference radius. The fit always runs on the CPU, in parallel over frames; frames below the confidence floor fall back to the phase correlator.
  • Global alignment. Phase correlation works on the half spectrum of the real frame (realfft along rows, complex transforms down the columns, stored transposed). Plans, filters, the reference spectrum and the working buffers live in the PhaseCorrelator and are shared by all threads.
  • Alignment points. Correlation sums are running sums plus one vectorised dot product per offset.
  • Stacking. resample_region filters each source row horizontally once and combines rows vertically.

Conventions worth knowing:

  • A shift s means frame(x) = reference(x - s); aligning samples the frame at x + s.
  • Selection is global and per point. The best keep frames by global quality form the kept set. With multipoint on, keep * 2 frames are registered and each alignment point stacks its own best keep of them by local sharpness.
  • Weights are a smooth function of quality over the selected frames, not a cutoff, so marginal frames still contribute a little.
  • Masking happens at the source. Pipeline samples about 16 frames, flags pixels that flip between white and dark (a timestamp), adds the user's rectangles and wraps the capture in MaskedSource, so scoring, registration, alignment points and the stack all see the same flat patch on the CPU and the GPU. The limb is fitted on the reference (a robust circle through the steepest edge of each scan line) and an alignment point is placed only when its centre is inside it, plus an optional prominence band, and its window overlaps no masked rectangle.
  • Alignment points are correlated on a smoothed, background-subtracted copy of the frame (a Gaussian band-pass), because on low-contrast H-alpha detail the slow limb-darkening gradient otherwise flattens the correlation peak.
  • A point with a weak or ridge-like correlation (a straight stretch of limb constrains the shift across the edge only) takes the inverse-distance average of its neighbours' shifts instead of its own noisy estimate.
  • Everything that deposits a frame onto the output goes through the Accumulator interface (or TileStack for tiles). Drizzle will be another Deposit variant; today it returns Unsupported.
  • The robust modes (StackMode::SigmaClip, Huber, Winsor) add a second streaming pass: pass one gathers the weighted mean and spread per pixel, pass two sums with weights from RobustKind::apply. For whole frames this is the RobustAccumulator interface (CPU in backend.rs, GPU in robust_accumulate.rs); for alignment point tiles it is TileStack. Memory stays O(pixels).

API server

helia-server is a thin layer over Pipeline; it adds no image maths.

  • Server::builder() validates settings (a non-loopback bind needs a token) and canonicalises the data root. Server::router() gives an in-process axum::Router (used by the tests); Server::bind() takes the socket.
  • paths::Sandbox is the only way a client string becomes a PathBuf: .. is rejected, then the path is canonicalised through its deepest existing ancestor (so symlinks are followed) and must stay under the root.
  • jobs::JobManager holds the registry and a FIFO worker pool (a tokio semaphore sized by --workers; the GPU is one device, so the default is 1). Each job runs Pipeline::run in spawn_blocking with its own AtomicBool; the progress callback updates the job record and feeds a broadcast channel that the WebSocket handler forwards. Lock order: registry, then job state.
  • A middleware does the loopback Host check (DNS-rebinding defence) and bearer authentication; CORS is a separate outer layer, off by default.
  • The OpenAPI document is generated from #[utoipa::path] annotations, so it cannot drift from the handlers' signatures.
  • A job that was not given channel, keep or ap_size takes them from helia_core::recommend once the capture is open, then reports the stages opening, the pipeline's, and (when the output is coloured) colourise and write. GET /presets is rendered from helia_core::colour.
  • ap_auto and autocrop set StackConfig::auto_ap and autocrop. The choice of alignment points (helia_core::autoap) happens in Pipeline::run after the reference exists: a limb fit gives the scale, a probe of twelve candidates measures the local warp, autoap::choose returns the size, spacing and search radius, and the provenance records what ran, not what was asked for. The crop (helia_core::autocrop) runs last, after normalisation, so levels do not depend on it; StackOutput::disc is in the cropped image's pixels while grid, masked and reference stay in the capture's. The jobs report both in summary.auto_ap and summary.crop.
  • helia_core::limbdark fits a law to the azimuthal median profile of the stack (LimbFit, linear in the coefficients, solved by Gaussian elimination) and builds a FlattenPlan: a gain table of 513 nodes over the radius. The flatten edit (Op::Flatten) must be first in a list, because it measures the stack itself; EditList::resolve fits it (State::Ready or State::NoDisc) and the CPU (FlattenPlan::apply) and the GPU (edit_flatten.wgsl) read the same table the same way, so parity is by construction. The pipeline fits the configured law on every stack (StackOutput::limb) without changing it.
  • The prominence layer (Op::Prominence, helia_core::edit::prominence) is a CPU-only edit, run on the host like the geometry edits (the device path hands it to apply_op). It fits the disc on the frame it is given, models the glow as a table of ring medians in 24 angular sectors (windows of three, interpolated), blurs and gates the excess, stretches it with asinh and joins it to the disc with a feathered smoothstep. Inside the limb the output is the input, bit for bit. helia_core::lut_export turns a ColourLut into .cube, CSV, ImageJ .lut or PNG; the CLI (helia lut), the API (/presets/{id}/lut) and the Colour step all call it.
  • True colour (helia_core::truecolour, StackConfig::true_colour): run runs the usual stages on the luminance (run switches the channel before it calls run_impl), then stack_plane three times with with_channel red, green and blue over the one StackStage (candidates, shifts, scores, reference, point set and table), the stack stage's progress scaled to thirds. compose registers red and blue to green, balances and scales, StackOutput::colour holds the picture and image its luminance. The backend stacks each plane as it would a mono stack, so GPU parity is the existing stack parity plus the true_colour_stack_matches_the_cpu_reference test. OutputPlan::rgb carries the picture to prepare (edits per channel by edit_rgb) and finish (save_rgb: TIFF, PNG or a FITS cube from write_fits_planes).
  • JobConfig mirrors helia stack's options. The CLI keeps its own clap types; a change to one needs the same change in the other.

The CPU is the specification

CpuBackend defines the correct result. Every GPU kernel has a CPU reference and a parity test (crates/helia-gpu/tests/parity.rs) that compares the two:

  • accumulated sums within 1e-4 per unit weight, validity maps exactly equal;
  • two-pass robust stacks (sigma, Huber, winsor) within 2e-4 per pixel;
  • sharpness within 2e-3 relative;
  • colouring within 1e-5 per sample (the same table, the same interpolation);
  • every edit operation within 1e-4 (the filters) or 2e-3 (iterated deconvolution), observed about 1e-6 or better.

Observed differences are several orders tighter (about 1e-6 and 1e-7) because the GPU kernels use the CPU's tap weights and the same operation order. The trait lives in helia-core, so the core builds and tests without any GPU dependency, and tolerances are stated next to the tests rather than hidden.

GPU backend selection

helia-gpu asks wgpu for a high-performance adapter across Vulkan, DX12, Metal and GL.

  • --backend auto (default): take the adapter wgpu ranks best (discrete, integrated, other, virtual), otherwise the CPU path. A CPU-hosted software adapter (WARP, lavapipe, llvmpipe) is never chosen, because it is slower than the multi-threaded CPU path (GpuBackend::is_software, #28).
  • --backend cpu: always the CPU.
  • --backend gpu: a real GPU, falling back to wgpu's software adapter (WARP on Windows, lavapipe on Linux); an error if neither exists.

A process has one device per adapter request: GpuBackend::shared hands every caller the same Arc, and the device is torn down when the last holder drops it. Creating and destroying devices is serialised process-wide (several at once hung the NVIDIA Vulkan driver), and a device waits for its queue to go idle before it is released. The wgpu instance is created once per backend set and never dropped: destroying the last instance makes the Vulkan loader unload the driver libraries, and any thread that has called into a driver then jumps into unmapped memory when it exits, because its thread-local destructors point into the unloaded library (#31). A static device that is never dropped had the mirror-image problem at process exit.

A backend takes over whole pipeline stages (Backend::score_frames, register_frames, measure_points, stack_frames). Each receives the capture as SharedFrames (stored bytes, read by the backend itself) and returns Ok(None) to decline, in which case the pipeline runs the CPU code for that stage. The GPU backend uploads the stored bytes of a batch of frames, converts and demosaics them in a compute shader, and keeps them on the device for the stage; only small results (sums, sample arrays, correlation surfaces) come back. A helper thread reads the next batch from the file while the device works on the current one, and the scoring and alignment point stages keep two batches in flight (two sets of device buffers) so the host's finishing work overlaps the device's.

What runs where today: frame scoring (gradient energy and Laplacian variance), global registration, alignment point measurement and mean stacking (tiles and whole frame) all run on the GPU from the stored bytes. Sigma-clipped stacking stays on the CPU (it needs two passes), as does registration of frames whose transform lengths exceed 2048. Backend::supports tells the pipeline which stages a backend implements, so the CPU keeps its single pass over the frames for registration and alignment points while a device that takes the points gets the global shifts first.

Registration on the device follows the CPU correlator step for step: windowed rows transform to the non-negative half spectrum (stored transposed), columns follow, the cross-power spectrum with the reference is whitened and band-passed, and two inverse transforms give the correlation surface. The transforms are in-place decimation-in-time FFTs of one line in workgroup memory (radices 2, 3, 4 and 5, so the same 5-smooth lengths as the CPU, up to 2048). The peak and the refinement iterations are further kernels; only the shifts are read back.

The alignment point stage returns correlation surfaces and window sums; the peak fit (fit_peak), the sharpness formula (combine_sums) and the interpolation of unreliable points (plan_fill) are the CPU's own functions, so the two sides cannot disagree on decisions, only on float rounding. Device kernels follow three rules learned from the WARP adapter: every thread of a workgroup reaches each barrier (no early return before one), per-batch parameters live in buffers rewritten before the submission that reads them, and workgroup memory is not zero-filled (every kernel writes it before it reads it; the zero-fill made large arrays compile for tens of seconds). HELIA_GPU_TRACE=1 prints wall time and per-kernel GPU time per stage.

Tests choose their adapter with HELIA_GPU_TESTS: software requests the fallback adapter, hardware the normal one, unset or none skips GPU tests with a printed note. With software or hardware set, a missing adapter is a failure so CI cannot skip silently.

The desktop app

helia-gui is a thin shell: all image work stays in helia-core and helia-gpu. Its modules:

  • theme: colour tokens (Palette), the embedded IBM Plex fonts (SIL OFL 1.1) and the egui style. A light palette would be a second Palette constant.
  • model: the view model, with no egui widgets and no I/O. Step statuses, overall progress, the sorted quality curve and its geometry, keep and threshold maths, zoom and pan, alignment point rectangles, label formatting. This is where the unit tests are.
  • worker: every slow operation on a background thread. One long-lived thread decodes preview frames (the newest request wins, so scrubbing never queues) and uploads textures; other threads open captures, score, run the pipeline with progress and a cancel flag, probe GPU adapters and run file dialogs. They talk to the UI through one Event channel and call request_repaint; the UI thread never blocks.
  • app: state, event handling, shortcuts and the responsive rule (panels collapse below 1100 px). Decoded frames are cached as textures, at most 40, evicting the frame furthest from the current one.
  • capture_view: the Open step's notices, header and "Helia will use" lists and override chips, drawn from the CaptureReport the worker returns.
  • edits: the Sharpen and Adjust settings and how they become an edit list, the preview size (a power of two within 1280 pixels) and the geometry-only list for the "before" half of a split view. Unit tested.
  • topbar, centre, inspector, editors, curve, widgets: drawing. Custom widgets report AccessKit labels and roles. A preview is one background job at a time (Workers::render): the edits on a shrunk copy of the stack, then the colour, uploaded as a texture; a change while one runs is folded into the next. The backend is kept between previews so a GPU device is opened once.
  • demo: --open <capture> --shots <dir> runs a scripted stack and saves screenshots, for checking real captures by eye.

The window is snapshot tested with egui_kittest (snapshots module): a synthetic capture from helia_core::testutil, injected state at each step, and a software wgpu adapter. The PNGs live in docs/book/src/images/ui/ and are embedded in the user guide. The test runs only when HELIA_GPU_TESTS=software and fails on more than 8 pixels differing at the default per-pixel tolerance; UPDATE_SNAPSHOTS=1 regenerates the images.

Real captures

crates/helia-core/tests/real_data.rs runs against real SharpCap and SER captures when HELIA_REAL_DATA points at them. The files are large and are never stored in the repository (*.ser and *.avi are ignored). They run every night on the GPU runner (real-data-nightly), where the four captures are staged and hash-checked.

Research

Helia's design rests on two research reviews. They are working documents for designers rather than user documentation, and they say how far each claim was checked.

  • Imaging methods: models, methods and recommendations for solar lucky imaging. It covers the atmosphere, capture, quality metrics, registration, stacking, professional restoration methods, post-processing, solar-specific workflow, machine learning, a comparison of existing tools, the recommended pipeline and how to test it.
  • State of the art, October 2026: a refresh of the first review, with a method matrix, proposed milestones and a checklist for keeping it current at each release. It also lists corrections to the first document.

Both mark claims [V] when the source was read during the review and [M] when stated from background knowledge and not re-checked. Treat [M] as probably right, and confirm before relying on it.

The milestones in the Roadmap come from the second review.

Solar lucky imaging: models, methods and recommendations for Helia

Status: research review, 2026-10-17. Audience: Helia designers (open-source, Rust, wgpu solar stacker).

0. How to read this document

Every non-obvious claim carries a citation. Two tags show how far each claim was checked:

  • [V] the source page, abstract or code was read (or its relevant text appeared in a search result) during this review.
  • [M] stated from background knowledge of the literature and not re-checked here. Treat as "probably right, confirm before relying on it". Bibliographic details marked [M] (volume, page) are the most likely to contain slips.

Where I looked and found nothing, the text says so. Notably, I found no peer-reviewed head-to-head comparison of frame-quality metrics on solar data beyond Denker et al. (2018), and no documented amateur use of speckle masking or MFBD on solar video. Both gaps are stated again where they matter.

The section order follows the brief (1 to 10), then the recommended pipeline and test plan.


1. Atmospheric model

1.1 Kolmogorov turbulence and the Fried parameter

Refractive-index fluctuations in the turbulent atmosphere follow Kolmogorov statistics (inertial-range structure function proportional to r^(2/3)), which gives a phase power spectrum proportional to |k|^(-11/3) [M]. Fried (1966, JOSA 56, 1372, "Optical resolution through a randomly inhomogeneous medium for very long and very short exposures") [M] defined the coherence diameter r0: the aperture over which the rms phase error is about one radian. Two consequences matter for design:

  • r0 scales as lambda^(6/5), so seeing is better in the red than the blue. A lecture-level statement of the scaling and of a typical night-time r0 of about 10 cm at 500 nm is in Sheffield PHY217 L10 [V, secondary]. Daytime solar r0 is generally smaller and varies from a few centimetres in poor conditions to roughly 10 cm or more at good sites; I could not retrieve a primary table of daytime values (Rimmele & Marino 2011 is the right reference, see below) so numbers here are order-of-magnitude [M].
  • Seeing-limited FWHM is about 0.98 lambda/r0. For r0 = 5 cm at 550 nm that is about 2.2 arcsec, whereas a 100 mm aperture has a diffraction FWHM near 1.4 arcsec at 550 nm (about 1.65 arcsec at H-alpha, using the 1.22 lambda/D Rayleigh radius). Hence a small amateur aperture is often within a factor of two of its diffraction limit even in moderate seeing, which is why simple lucky imaging works well for amateur solar apertures and why large apertures (D much greater than r0) benefit most from speckle or MFBD.

The key reference for the physical regime of daytime solar turbulence and its correction is Rimmele & Marino (2011), "Solar Adaptive Optics", Living Reviews in Solar Physics 8, 2 [V for existence and scope, numbers not retrieved].

1.2 Tip-tilt dominates, high orders are what lucky imaging selects

Noll (1976, JOSA 66, 207, "Zernike polynomials and atmospheric turbulence") [M] gives the Kolmogorov Zernike variances: about 87 per cent of the uncorrected phase variance is in global tip and tilt. This explains the two-part structure of every lucky-imaging pipeline: first remove image motion (registration), then select or weight frames by how little high-order aberration they contain. It also explains why a pure shift-and-add (registration without selection) already helps: motion blur is the largest single term.

1.3 Probability of a good frame

Fried (1978), "Probability of getting a lucky short-exposure image through turbulence", JOSA 68, 1651 [V for citation]. A secondary source quotes the approximation P ≈ 5.6 exp(-0.1557 (D/r0)^2) for the chance of a near-diffraction-limited short exposure (arXiv:1603.00729, secondary, check constants against Fried before use). The exponential in (D/r0)^2 is the design-relevant fact: the useful fraction collapses fast as D/r0 grows. For a 100 mm refractor with r0 of 5 cm (D/r0 = 2) the exponent is small and most frames are usable; for a 300 mm aperture (D/r0 = 6) good frames are rare and local, not global, selection becomes essential.

Law, Mackay & Baldwin (2006), "Lucky imaging: high angular resolution imaging in the visible from the ground", A&A 446, 739 [V], demonstrated the method experimentally on a 2.56 m telescope: near-diffraction-limited images in good seeing and a resolution gain of roughly 2.5 to 4 in poorer seeing, with selection fractions chosen by the guide-star measure. That work is nocturnal and uses a bright-star Strehl proxy; the Sun supplies no such star, which is the central difficulty for Section 3.

1.4 Isoplanatic patch: why the Sun forces local processing

The isoplanatic angle is the field over which the PSF can be treated as constant. For turbulence concentrated near the ground (the daytime case, driven by solar heating) the patch is small. Evidence:

  • A 1990s Dutch Open Telescope (DOT) speckle proposal states a daytime isoplanatic patch of about 5 arcsec (Rutten, DOT speckle proposal 1992) [V, via search snippet].
  • A Big Bear Solar Observatory proposal puts the patch at roughly 10 arcsec in visible light in good seeing, and notes that flares and CMEs span fields about ten times larger (BBSO, ATI 2013) [V, snippet]; Rimmele's DST adaptive-optics paper calls the patch "a severe limitation" for extended solar fields (ao4elt proceedings) [V, snippet].
  • MFBD practice uses sub-fields of about 4 to 5 arcsec square because space-invariance of the PSF holds only inside an isoplanatic patch (arXiv:2205.13650) [V, snippet].
  • A 2017 A&A study states that reaching the diffraction limit both inside and outside the isoplanatic patch requires post-facto reconstruction (A&A 2017, 10.1051/0004-6361/201731275) [V, snippet; title not retrieved].

Arithmetic for the full disc (mine): the disc subtends about 1920 arcsec across, area about 2.9 million square arcsec. A 5 arcsec patch tiles it about 116,000 times, a 10 arcsec patch about 29,000 times. The PSF (and the image displacement) is therefore different over tens of thousands of independent regions in every frame, and a global registration shift is only the mean of a smoothly varying displacement field. This is the physical justification for multi-point (alignment-point, AP) processing: AP size should be tied to the isoplanatic scale, not chosen as a pixel count. In plate-scale terms, an AP box of about 5 to 10 arcsec is 17 to 33 px at 0.3 arcsec/px.

Practical corollaries:

  1. The largest anisoplanatic term is differential image motion (local tilt). That is exactly what a local-shift AP grid removes (Section 4).
  2. Higher-order anisoplanatic blur varies on the same scale but cannot be removed by shifting; only per-patch frame selection (local lucky imaging) and per-patch restoration (speckle, MFBD) address it.
  3. A solar disc has structure of strongly varying contrast (limb, granulation, pores, plage, filaments); APs on flat or dark regions yield unreliable shifts and must be rejected or filled from a smooth interpolation (PSS and Siril both do this, Section 4).

1.5 Time scales and frame rate

Denker et al. (2018), "Image Quality in High-resolution and High-cadence Solar Imaging", Solar Physics [V abstract], analysed 160 Hz G-band and blue-continuum HiFI sequences at the 1.5 m GREGOR telescope and concluded that fast cadence and millisecond exposures are not enough to match the coherence time of daytime seeing, and that acquisition rates above about 50 Hz are needed to capture a substantial fraction of the best-seeing moments. Their standard HiFI mode selects 2 x 100 frames out of a 10 second burst. Short-exposure frame selection for solar work goes back to Scharmer (1989) and Scharmer & Löfdahl (1991), cited in that paper [V, secondary].

Design consequences: (a) exposure should be at or below the seeing coherence time (a few milliseconds; I did not retrieve a primary value so this is [M]); (b) the 50 Hz floor is a pertinent target for the capture guidance Helia gives users; (c) because the frozen-turbulence lifetime is short, consecutive frames are strongly correlated only at the highest rates, which affects how many independent samples a capture contains.


2. Capture

Helia is mostly a processing tool, but its design must state what input it expects, because capture decisions set the information ceiling.

2.1 Exposure, gain, bit depth, ROI, frame rate

  • Exposure: short enough to freeze the seeing (Section 1.5); in white light this is easy (sub-millisecond to a few ms with a safe-filtered refractor), in H-alpha through a narrowband etalon it is photon-limited and the trade-off between exposure and noise becomes the main constraint [M].
  • Gain: raise gain until the histogram sits at roughly 50 to 70 per cent of full scale but never clip. Lucky imaging favours more frames at higher gain over longer exposures, because stacking averages read noise down and selection removes motion-blurred frames. Quantitative gain advice is camera-specific [M].
  • Bit depth: 12-bit sensors stored as 16-bit, or 8-bit from USB-limited modes. Granulation contrast is only a few per cent in white light at the native resolution, so 8-bit quantisation (about 0.4 per cent per step) is a real limit on a single frame, though stacking dithers it. Helia should read 8 and 16-bit natively and accumulate in f32.
  • ROI and frame rate: frame rate scales almost inversely with ROI area on USB cameras. Capturing a region of interest at 100 to 300 fps is the standard route to the 50 Hz+ regime of Section 1.5. A full-disc capture at 32 arcmin needs very short focal length and gives coarse sampling; high-resolution work is therefore ROI-based and full-disc work is mosaic-based (Section 8).
  • Duration: because the Sun evolves, granulation changes on a time scale of minutes; derotation matters for long captures or mosaics (Section 8.5).

2.2 Sampling: f-ratio versus pixel size

An imaging system with circular aperture D has an optical cutoff frequency D/lambda (cycles per radian). Nyquist sampling of that cutoff needs a pixel pitch p <= lambda F / 2 where F is the focal ratio, i.e. F >= 2p/lambda (standard sampling theory, e.g. Goodman, Introduction to Fourier Optics) [M]. Worked values:

lambdapixel pNyquist F-number
550 nm (white light)3.76 um13.7
550 nm2.4 um8.7
656.3 nm (H-alpha)3.76 um11.5
393.4 nm (Ca II K)3.76 um19.1

Seeing-limited data are blurred below the diffraction limit, so for poor seeing slight undersampling loses little; for good seeing and best results slightly oversampling (F up to about 1.5x Nyquist) lets sub-pixel registration and sharpening work well. Amateur community rules of thumb (F ratio about 3 to 5 times pixel size in microns for planets) are consistent with this; I did not find a primary solar-specific source. Helia should (a) compute and display the sampling ratio q = lambda F / (2 p) from user-entered optics, (b) warn when q < 1 (undersampled, drizzle becomes relevant, Section 5.3), and (c) set AP sizes and wavelet scales in arcseconds or in units of the diffraction FWHM, not in pixels.

2.3 Calibration frames: darks, flats, and the H-alpha etalon

  • Darks / bias: matter little for bright white-light frames at short exposure, but a master dark removes hot pixels and fixed-pattern noise that otherwise stack coherently when the image shifts less than the noise. Needed for H-alpha, where signal is low.
  • Flats: essential for solar work because dust shadows on the sensor window and filter are conspicuous on a smooth disc and, after local de-warping, can be misread as granulation. Because the Sun shifts relative to the sensor during a capture, dust stays fixed while the image moves, so a good flat removes it, and a poor flat leaves a fixed pattern that the multi-frame process sharpens.
  • Newton's rings and etalon fringes: in etalon-based H-alpha systems the interference between flat surfaces (etalon, camera window) produces fringes (dehilster.info on removing Newton rings [V, snippet]). A British Astronomical Association overview of narrowband solar equipment discusses the same (BAA) [V, listing only]. One amateur observation notes that flats made by defocusing do not reproduce the ring pattern because the rings shift with focus; flats should be taken at the same focus and tilt as the science data, e.g. by moving the telescope across the disc (a "flat from the sun's own image" using a large random displacement, then median-combined) [M].
  • Etalon sweet spot: a Fabry-Perot etalon's pass-band shifts with the angle of incidence of the light, so uniform filtering across the field requires an almost telecentric beam. One source: "The etalon will only give reasonably consistent filtration across an image if the light is almost parallel" (scopetrader/H-alpha build discussion) [V, snippet]. Practical effect for software: the disc has a brightness gradient and bandpass-driven contrast changes across the field (the "sweet spot"), so Helia needs (a) a flat/gradient normalisation prior to quality ranking, and (b) an option to compute quality only inside a user mask.

2.4 File formats: SER versus AVI versus FITS

FormatProsConsHelia stance
SERDesigned for astro video; raw 8 to 16-bit; Bayer codes; per-frame UTC timestamps in an optional trailer; no compression (Hahn, SER format [V]; 178-byte header per Siril docs [V, snippet])No formal version tag: ColorID values 0 to 19 are version 2, 100 to 101 version 3 (FFmpeg ticket [V, snippet]); endianness flag is historically inconsistent across capture programs [M]MVP primary input
AVIUbiquitousCompressed or demosaiced in many cameras, lossy codecs destroy fine detail and add blocking that shifts quality metrics, hard to get frame timestampsLater, via an optional ffmpeg path only; warn user about lossy codecs
FITSScientific standard, header metadata, 16/32-bit float, cubesRarely produced by capture software as video; largeMVP for stacked outputs and cubes; frame-cube input as secondary
TIFF / PNG sequencesSimpleMany files, timestamps lostOptional

The SER trailer timestamps are valuable for solar work: they allow derotation and seeing-versus-time plots. Helia should read the header defensively (check file length against frame_count * frame_size), honour the endianness flag but also sanity-check it against pixel histograms, and fall back to file modification time if the trailer is absent.


3. Frame and region quality metrics

3.1 Candidate metrics

For a frame or an AP patch I:

MetricFormula (sketch)Notes
Gradient energy / Tenengradsum of (Gx^2 + Gy^2) with Sobel, optionally thresholdedResponds to edges; noise adds a positive bias, so noise floor subtraction is needed.
Laplacian variancevar(Laplace(I))Used by PSS ("local_contrast_laplace") and Siril; strongly noise-sensitive because the Laplacian amplifies high frequencies.
Normalised variancevar(I)/mean(I)Cheap; sensitive to illumination and to low-frequency structure, less to seeing.
Fourier band powerintegral ofI(k)
RMS contrast of granulationstd(I)/mean(I) on a flattened regionThe scientifically meaningful quantity in white light; compared to simulation in speckle work. Contrast of granulation saturates at the true (instrument-smoothed) value, so it tracks seeing only monotonically.
Median-filter gradient similarity (MFGS)similarity between gradient images of the frame and its median-filtered versionUsed for GREGOR HiFI quality assessment (Denker et al. 2018 [V]); original definition and authorship not verified here.

What the implementations actually do:

  • AutoStakkert!3 offers gradient-based and Laplace-based estimators with a "noise robust" parameter; default 4, raise it for noisy data, lower it for excellent data to concentrate on small features; a local (AP) estimator ranks frames at each AP and keeps (by default) the best 50 per cent per AP (community guides summarised via search, autostakkert.com) [V, snippets; no official manual retrieved].
  • PlanetarySystemStacker provides three: "xy gradient" (mean finite-difference gradient magnitude after striding), "Laplace" (standard deviation of the Laplacian of the downsampled frame; the docstring says variance but the code returns the standard deviation) and "Sobel"; qualities can be divided by the frame's average brightness (alignment_points.py, miscellaneous.py) [V, code read through a fetch].
  • Siril uses a Laplacian measure with brightness normalisation on by default and ranks per AP as well as globally (Siril multipoint docs) [V].
  • Solar professional: Denker et al. (2018) recommend MFGS for a priori quality assessment of large HiFI archives, and show that a modified MFGS reveals field and structure dependence of the metric (arXiv:1802.00760) [V abstract].

3.2 Which correlates best with true seeing?

Honest answer: I could not find a peer-reviewed study that compares these metrics against a known-truth seeing measure on solar data. The nearest items:

  • Denker et al. (2018) compare metrics on HiFI data and recommend MFGS, but the abstract does not name the alternatives or give rank correlations [V abstract; full text not read].
  • The hybrid lucky-imaging paper arXiv:2012.05480 scores output images with seven measures including Tenengrad and Laplacian, which tests output sharpness metrics, not frame-selection metrics [V, snippet].
  • General focus-measure comparisons (e.g. Groen et al. 1985, Cytometry; Pech-Pacheco et al. 2000, ICPR) [M] show that gradient and Laplacian measures behave well on high-SNR data but diverge under noise.
  • The review of digital lucky imaging, arXiv:2411.00338, lists intensity-variance, Strehl and related measures but gives no ranking [V, snippet].

Theory gives clear guidance:

  1. The measure must respond to power at spatial frequencies the seeing removes: frequencies between roughly r0/lambda and D/lambda, i.e. the mid-to-high band. Very low frequencies (brightness gradients, limb) are irrelevant; frequencies above the diffraction cutoff are pure noise.
  2. Photon and read noise add a positive, frame-independent, but brightness-dependent bias to any energy-type metric. Without correction, a bright noisy frame outranks a dim sharp one. Both Siril (brightness normalisation) and AutoStakkert (noise-robust parameter) address this partly.
  3. A band-limited Fourier power metric with an estimated noise floor subtracted is the most defensible: it is the sum over the band of (|F(k)|^2 minus the noise power), and the noise power can be estimated per frame from the power above the diffraction cutoff, where there is no signal. This reduces the bias directly. It is a recommendation from first principles rather than a published comparison: mark as hypothesis; test in Helia's synthetic harness (Section 12).
  4. Global versus local: the isoplanatic argument (Section 1.4) says that a single global score hides the fact that, within a frame, some patches are sharp and others not. All three open implementations compute both a global rank (for choosing a reference and a coarse cut) and an AP-local rank (for the actual selection). Siril and PSS blend patches and fade frames near the cutoff rather than cut hard (Siril: "frames near the cutoff fade in smoothly") [V].

3.3 Recommendation for Helia

ItemVerdict
Gradient energy (Sobel/Tenengrad) with noise-floor subtraction and brightness normalisationMVP: cheap, GPU-trivial, matches AS!3/PSS behaviour.
Laplacian std/varianceMVP as a second selectable metric for compatibility and comparison.
Band-limited Fourier power with per-frame noise floorMVP+ / early later: needs an FFT on GPU anyway; evaluate against gradient on synthetic data first.
MFGSLater: implement for parity with professional archives; cheap (median filter + gradient).
Granulation RMS contrastReport as a diagnostic (frame statistic), not the ranking driver.
Global-only selectionNot enough on the Sun; use only for reference-frame choice.
Local per-AP selection with soft cutoffMVP (this is the core of multipoint lucky imaging).

Implementation note: compute all patch metrics from one shared pass over the frame stored as f32 in a GPU buffer; the metric can be reduced per AP by workgroup reduction, no readback needed other than the ranks.


4. Registration

4.1 Models

  1. Global translation: remove whole-frame motion (dominated by tip/tilt, mount flexure, wind). Needed before any local step so the local search window can be small.
  2. Local translation per AP: estimate the shift (dx, dy) of each patch relative to a reference; this is a piecewise approximation of the local-tilt field.
  3. Dense warp: interpolate AP shifts to a smooth displacement field, resample each frame once (destretching).

4.2 Estimators

  • Normalised cross-correlation (NCC): peak of the correlation of a template with the search window; robust to additive brightness offsets when zero-mean; direct implementation costs O(W^2 S^2) per AP for template size W and search range S, which is small for AP work (W = 32 to 64, S = ±8) and parallelises naturally on a GPU (one workgroup per AP and frame).
  • Phase correlation (Kuglin & Hines 1975, ICCS) [M]: inverse FFT of the normalised cross-power spectrum; delta-like peak, contrast-independent, but requires windowing to avoid edge leakage and is dominated by high-frequency noise unless band-limited. The PSS translation routine is a plain phase correlation returning whole-pixel shifts [V code]; ImPPG's alignment uses phase correlation on high-contrast features or limb stabilisation with optional sub-pixel accuracy (ImPPG README) [V].
  • Sub-pixel peak refinement: (a) biquadratic/paraboloid fit on the 3 x 3 neighbourhood (November & Simon 1988 found a biquadratic surface best in their numerical tests, per arXiv:0707.1994 snippet [V, secondary]; PSS sub_pixel_solve does exactly this with a precomputed normal-equation matrix and accepts a correction only if within about one pixel [V code]); (b) matrix-multiply DFT upsampling around the coarse peak, Guizar-Sicairos, Thurman & Fienup (2008), "Efficient subpixel image registration algorithms", Opt. Lett. 33, 156 [V], which reaches the accuracy of full FFT upsampling at a small fraction of the cost and memory (their example: 1/20 pixel on 1024 x 1024 would need a 20,480 x 20,480 inverse FFT by brute force). PSS also calls scikit-image phase_cross_correlation with upsample_factor=10 (the "Subpixel" method) [V code].
  • Optical flow (Lucas-Kanade, Horn-Schunck, or modern dense flow): dense by construction, so it needs no AP grid, but granulation is low-contrast and evolves, which violates brightness constancy; flow is also expensive for thousands of frames. It is a plausible refinement after AP-based registration but not an MVP candidate [M]. Local correlation tracking (LCT) used in solar physics (November & Simon 1988; also FLCT, a fast LCT implementation) [V, snippets] is conceptually an AP grid at every pixel, so the two approaches are the same family.

4.3 What the existing tools do

ToolGlobal alignmentLocal methodNotes
AutoStakkert!3Disc centroid or surface correlation [M]AP grid, per-AP correlation; local (AP) quality; sub-pixel; drizzle 1.5x and 3x [M]Closed source, freeware [M]; no official algorithm doc retrieved.
RegistaxAlignment on a chosen reference region; "optimise" stage [M]Limited local alignmentLargely superseded for alignment; still used for wavelets [M].
PlanetarySystemStackerRank all frames by global quality, then align globally on a high-structure patch using the chosen method (README) [V]AP mesh with brightness/contrast/structure rejection; per-AP ranking; multi-level correlation: phase 1 on a half-resolution box with a wide search, phase 2 on full resolution with a small search around the phase-1 result, optional sub-pixel quadratic fit; alternates: "Subpixel", "CrossCorrelation", "RadialSearch", "SteepestDescent" [V code]Python 3, OpenCV, numpy, Qt5; GPLv3 per the source header [V]. Patch accumulation is in a separate module (not read).
Siril (multipoint)Planet mode: brightness centroid; Surface mode: cross-correlation on an auto-picked patch; reference built from best 5 per cent of frames [V]AP spacing default 2.25 x the correlation half-width (54 px at the default half-width of 24); search width 14 px; zero-mean option; two-pass refinement against a sharper local reference; robust local smoothing across neighbouring APs (default radius 2.5 grid steps); patch engine with raised-cosine blending and brightness matching, and an experimental warp engine that interpolates AP shifts to a single smooth field and resamples each frame once [V]GPLv3 [M]. Output scale 1x, 1.5x, 2x, 3x; 32-bit float accumulation [V].
ImPPGPhase correlation or solar limb stabilisation, sub-pixel option [V]None (alignment of sequences, not lucky imaging)GPLv3, C++, OpenGL [V].

Two design lessons are worth copying from Siril: (1) the refinement pass, where the best frames at each AP are stacked into a sharper local reference and shifts are re-measured against it (the first reference is blurred, which biases shifts toward the mean); (2) robust smoothing of the shift field, which cuts measurement noise while preserving genuine warp.

4.4 Destretching in professional solar work

"Destretching" is the solar term for the same operation: a local correlation scheme determines displacements against a reference, then frames are interpolated to match. It was introduced for removal of image distortion (November 1986) and extended to granule tracking by November & Simon (1988), ApJ 333, 427, whose abstract reports that the time-averaged localised cross-correlation gives displacement not biased by atmospheric seeing (as summarised in arXiv:0707.1994 and the search results) [V, secondary]. Practical reports use multiple passes with shrinking windows (one workflow used four passes) to correct different spatial scales [V, snippet]. Speckle and MFBD pipelines at SST (SSTRED) and GREGOR do the equivalent inside their sub-field processing, followed by mosaicking of the reconstructed patches (SSTRED, arXiv:1804.03030) [V, snippet].

4.5 Recommendation

ComponentVerdict
Global translation by windowed, high-pass, band-limited phase correlation, f32 FFT (or the PSS multilevel NCC alternative)MVP. Provide the limb-fit / centroid mode for full-disc frames where a limb is present.
AP grid sized in arcseconds, with brightness, contrast and structure rejection; NCC search ±S px; quadratic sub-pixel fitMVP.
Multi-pass: coarse (half-resolution) then fine; refinement against a locally stacked referenceMVP+ (small cost, large benefit; Siril default).
Smoothing of the AP shift field by robust local fitMVP+.
Dense warp resampling (destretch) rather than patch pastingLater (needs a GPU gather kernel, bicubic or Lanczos resampling; avoids patch seams). Build patch blending first with Hann windows.
DFT-upsampled sub-pixel (Guizar-Sicairos)Later, only if the quadratic fit shows pixel-locking bias in synthetic tests (peak-locking is a known failure of parabolic fits [M]).
Optical flowNot worth it for MVP.

GPU compute estimate (mine): 5000 frames x 400 APs x (32 x 32 template) x (17 x 17 shifts) is about 5.9e11 multiply-adds, i.e. well under a second of ideal arithmetic on a consumer GPU and a few seconds in practice. The AP stage is therefore cheap; I/O and memory bandwidth dominate.


5. Stacking

5.1 Estimators

After registration, each output pixel (or AP patch) is combined from N selected samples:

  • Mean: optimal (minimum variance, unbiased) under independent Gaussian noise and identical expectation. Preserves the linear photometry that solar users want. Noise falls as 1/sqrt(N). This is the default in PSS and Siril's multipoint stacking (the patch engine averages best frames) [V].
  • Median: robust to outliers but 25 per cent less efficient than the mean for Gaussian noise (asymptotic variance factor π/2 [M]), and the median of resampled, sharpened frames loses the highest frequencies slightly more than the mean. Rarely used for lucky imaging.
  • Sigma clipping / winsorising: reject samples more than kσ from a robust centre. Helpful against cosmic-ray-like events (rare in short ground exposures), hot pixels not removed by darks, and frames corrupted by drifting clouds or bird transits. Caution: on a registered stack whose samples are meant to differ at the finest scales (seeing-induced differences carry residual signal), clipping at low k can erode genuine bright and dark detail, creating a slight loss of contrast. A conservative k (3 to 4) and iteration limit are standard [M].
  • Quality-weighted mean: weights w_i derived from rank or metric; Siril fades frames near the cutoff and PSS keeps a fixed top-N per AP [V]. A weight w ∝ (quality)^p, or a smooth taper from 1 to 0 over the last 10 to 20 per cent of the selected set, avoids the hard threshold's discontinuity as seeing changes.

5.2 How many frames

Stacking N independent frames reduces noise by sqrt(N), but the gain from seeing selection is in the signal (sharper frames) and N falls as the cut tightens. Amateur practice stacks the best 10 to 50 per cent depending on seeing and frame count [M]; the AutoStakkert 50 per cent line is a widely used cutoff, per the community guidance cited in Section 3 [V, snippet]. Helia's UI should show the quality-versus-rank curve and set a default from the knee, but allow per-AP override.

5.3 Drizzle

Fruchter & Hook (2002), "Drizzle: A Method for the Linear Reconstruction of Undersampled Images", PASP 114, 144 [V]: input pixels are shrunk to "drops" (parameter pixfrac) and mapped onto a finer output grid with geometric transformation, preserving photometry and resolution and avoiding further convolution, with per-pixel weights. It was developed for dithered HST data.

When it helps: only if the data are undersampled (q = lambda F / (2p) < 1 in Section 2.2) and the shifts are diverse in the sub-pixel sense. Lucky imaging satisfies the second condition for free, because seeing jitter provides random sub-pixel dither. For oversampled or Nyquist-sampled solar data drizzle gains nothing but costs noise correlation (adjacent output pixels share input pixels) and holes at small pixfrac with few frames. Siril offers 1x, 1.5x, 2x, 3x output scale, with 32-bit accumulation [V]; PSS has a drizzle factor and sizes its stacking buffer accordingly [V code]. AutoStakkert offers drizzle at 1.5x and 3x [M].

Implementation hint: design the accumulator as "splat each (frame, AP) sample with a position and a weight into a float buffer plus a weight buffer, then divide". Then drizzle is only a different splat kernel (drop footprint) and scale factor, and "mean" with bilinear or Lanczos resampling is another. This makes the MVP drizzle stub a real option rather than a rewrite.

5.4 Recommendation

MethodVerdict
Quality-weighted mean with soft cutoffMVP default.
Optional sigma clip (k 3 to 4, 1 to 2 iterations), applied to the registered, normalised samplesMVP, off by default.
MedianLater, as an option only.
Drizzle (variable pixfrac, scale 1.5x to 3x)Later; enable only when q < 1; keep the splat design now.

6. Image restoration at professional observatories

6.1 Speckle interferometry

Labeyrie (1970) [M] showed that averaging the power spectrum of many short exposures preserves high-frequency information (the "speckle transfer function", STF) that long exposures lose; this recovers amplitudes but not phases. To recover phases:

  • Knox-Thompson (Knox & Thompson 1974, ApJ 193, L45, "Recovery of images from atmospherically degraded short-exposure photographs") [V citation via search] uses the cross-spectrum <I(k) I*(k+Δ)> for small shifts Δ to propagate phase differences.
  • Speckle masking / bispectrum (Weigelt 1977, Opt. Commun.; Lohmann, Weigelt & Wirnitzer 1983, Appl. Opt.) [V citation via search] uses the triple correlation / bispectrum <I(k1) I(k2) I*(k1+k2)>, which is insensitive to the random atmospheric phase and gives absolute phase information, at much higher computational and memory cost.
  • Solar adaptation: von der Lühe (1993), "Speckle imaging of solar small-scale structure. I. Methods", A&A 268, 374, and Paper II (A&A 281, 889, 1994) [V citation via search] adapted speckle masking to the extended Sun, working on sub-fields within the isoplanatic patch; there is no calibration star on the Sun, so the STF must be modelled from an estimated r0 (see the 2003 DOT discussion referenced at robrutten.nl, [V snippet]).
  • KISIP: Wöger & von der Lühe (2008), "KISIP: a software package for speckle interferometry of adaptive optics corrected solar data", SPIE 7019, 70191E; and Wöger, von der Lühe & Reardon (2008), "Speckle interferometry with adaptive optics corrected solar data", A&A 488, 375 [V citations; abstract content via search]. KISIP uses Fourier phase reconstruction by an extended Knox-Thompson or a triple-correlation scheme, splits images into sub-frames sized to the isoplanatic patch (so it parallelises), and models the AO system with wavefront-sensor information. The A&A paper compares DST ground-based reconstructions with Hinode/SOT and concludes that near real-time reconstruction with high photometric accuracy is possible [V].

Evidence of achievement: speckle reconstruction of GREGOR HiFI bursts yields nearly diffraction-limited images (Denker et al. 2018 [V]). A cautionary data point: Uitenbroek, Tritschler & Rimmele compared the rms contrast of speckle-reconstructed G-band images with simulations and found 14.1 per cent for the best reconstructions versus 21.5 per cent for a simulated quiet Sun, a considerable discrepancy (summarised in the search for HiFI metrics) [V, snippet; the primary paper is "The discrepancy in G-band contrast: where is the quiet Sun?", arXiv:0704.3637]. So even professional restorations do not recover full contrast, partly because of residual PSF mismatch and partly because of telescope PSF.

6.2 Multi-Frame Blind Deconvolution and MOMFBD

MFBD estimates the object and the per-frame PSFs jointly by maximising a likelihood, with PSFs parametrised by wavefront expansion coefficients (Zernike or Karhunen-Loève modes) so that every PSF is physically constrained (pupil, diffraction, positivity). Phase diversity (Gonsalves 1982 [M]; Löfdahl & Scharmer 1994 for solar use, cited in arXiv:2505.10639 [V, snippet]) adds a deliberately defocused channel to remove the object-PSF ambiguity. Multi-Object MFBD (van Noort, Rouppe van der Voort & Löfdahl 2005, Solar Physics 228, 191, "Solar image restoration by use of multi-frame blind de-convolution with multiple objects and phase diversity") [M for volume and page] generalises to several simultaneous objects (wavelengths or polarisation states) that share the same wavefront.

Löfdahl (2002), "Multi-frame blind deconvolution with linear equality constraints", SPIE [V], formulates phase-diverse speckle as MFBD plus linear equality constraints on the wavefront parameters, so one code handles several acquisition schemes. Key facts from recent literature [V, snippets]:

  • Restoration is performed in sub-fields of roughly 4 to 5 arcsec (arXiv:2205.13650), because the space-invariance assumption holds only inside an isoplanatic patch.
  • The method is used with CRISP and CHROMIS data from the SST through the SSTRED pipeline (arXiv:1804.03030).
  • In low-SNR regimes the joint point estimate can be ill-posed and needs many images (Marginal MFBD, arXiv:2605.11980), which motivates marginalised formulations.
  • A modern open-source implementation is torchmfbd, a flexible MOMFBD code in PyTorch (arXiv:2505.10639) [V, snippet]. It is the best available reference for how to structure a GPU MFBD, including how to handle objects and modes.

Compute cost. Per iteration, each frame in a sub-field needs forward and backward transforms (about 4 FFTs) plus pointwise work. For a 64 x 64 sub-field the FFT is about 5 N log2 N = 5 x 4096 x 12 ≈ 0.25 MFLOP, so roughly 1 MFLOP per frame per iteration; 64 frames x 200 iterations ≈ 13 GFLOP per sub-field. A 1500 x 1500 px ROI with 32 px sub-field stride has ~2,200 overlapping sub-fields: about 30 TFLOP in total. On a consumer GPU sustaining 1 to 3 TFLOP/s (realistic with small FFTs and memory-bound steps) this is tens of seconds to a few minutes (my estimate; real optimisers also need line searches and the parameter gradient, so allow a factor of two to five). Memory is modest. In summary MFBD is feasible on a consumer GPU, but not trivial: it needs a batched small-FFT library (wgpu does not ship one, so Helia would write radix-2/4 kernels or use shared-memory Stockham FFTs; I did not find a mature wgpu FFT crate and did not verify this [unverified]) and an L-BFGS or conjugate-gradient driver.

6.3 What adaptive optics adds

AO removes low-order aberrations in real time, increasing the fraction of good frames and relaxing the burden on post-processing; but daytime AO corrects only inside the isoplanatic patch (Section 1.4), so post-facto reconstruction is still used (A&A 2017 statement above) [V snippet]. The DST uses a 97-actuator, 76-sub-aperture correlating Shack-Hartmann system and tested multi-conjugate AO using solar structure as guide targets (ao4elt abstract) [V, snippet]; BBSO runs three deformable mirrors over a 35 arcsec wavefront-sensing field (BBSO AFOSR 2017) [V, snippet]. Amateurs do not have AO on solar telescopes of significance (a few commercial tip-tilt systems exist; I did not verify any), so Helia should treat the problem as post-facto.

6.4 Feasibility for an amateur tool on a consumer GPU, and what amateurs have achieved

I looked for documented amateur speckle masking or MFBD on solar video and found none (the search returned only professional literature). Consequently:

  • Treat the simplified versions as research: a per-sub-field MFBD with a low number of Zernike modes (say tip, tilt, defocus, astigmatism, coma, spherical: about 10 to 20 modes, using the known telescope pupil including central obstruction) and a Poisson or Gaussian likelihood is the most realistic route, mirroring torchmfbd and SSTRED in miniature.
  • A simplified speckle path is possible via Knox-Thompson (cross-spectrum, O(N frames x shifts) per sub-field) with an STF model from an r0 estimate. Bispectrum/speckle masking is far more memory and compute hungry, and in the solar setting also needs the STF model; not justified for an MVP.
  • Amateurs' realised gain from lucky imaging, stacking and wavelet or Lucy-Richardson sharpening is well established; beyond that point I have no verified evidence. The prudent claim for Helia is: "MFBD may add the equivalent of a larger effective aperture for the best sub-fields, as it does professionally, but proof must come from Helia's own synthetic and real tests."

Failure modes (all professional pipelines): ill-posed with too few frames or low SNR; PSF mis-modelling creates ringing and spurious fine structure; sub-field boundary artefacts (mitigated by overlap and apodisation); sensitivity to flat-field errors; high cost of tuning; granulation contrast mismatch with simulations (Uitenbroek et al.).

6.5 Recommendation

MethodVerdict
Speckle masking / bispectrumNot worth it for Helia.
Knox-Thompson speckleNot worth it unless the MFBD path fails; it is dominated by MFBD in flexibility.
Phase diversityNot applicable to amateur video (no diverse channel); possible later with a deliberate defocus-pair capture, but low demand.
Per-sub-field MFBD with a Zernike-parametrised PSFResearch milestone (later): build on CPU reference first, then GPU; validate on synthetic frozen-turbulence data with known truth; ship behind an experimental flag.
Shift-and-add with local lucky selectionMVP: this is the practical equivalent that delivers most of the gain for typical amateur apertures.

7. Post-processing

7.1 Wavelets (à trous / Registax-style)

The à trous ("with holes") algorithm is an undecimated wavelet transform using a B3 spline scaling function; in 1D the smoothing mask is (1, 4, 6, 4, 1)/16 and in 2D a 5 x 5 mask, with dilation by inserting holes at each scale; reconstruction is the plain sum of the wavelet planes and the residual (arXiv:astro-ph/0202107; arXiv:astro-ph/0407053) [V, snippets]. The standard reference is Starck, Murtagh & Bijaoui (1998), Image Processing and Data Analysis: The Multiscale Approach, CUP [V via ImageJ plugin references]. The transform is redundant and noise variance differs per scale (arXiv:astro-ph/0303246) [V, snippet]. Registax-style sharpening multiplies each wavelet plane by a user gain and optionally applies noise thresholding ("denoise"); the exact Registax layer definitions I could not verify [M].

Properties for Helia: each scale is a separable 5-tap convolution applied with stride 2^j, which maps trivially to compute shaders; there is no ringing from the transform itself, but boosting fine scales boosts noise, so a per-scale threshold derived from the MAD of the finest scale is needed. It is a linear unsharp-mask family tool: it cannot recover frequencies the PSF has removed entirely, only reweight them.

7.2 Richardson-Lucy (RL) and regularisation

RL (Richardson 1972, JOSA; Lucy 1974, AJ) [M] is the iteration x_{k+1} = x_k · [(y / (H x_k)) ⊛ H^T]; it converges to the maximum-likelihood solution for Poisson noise, preserves flux and non-negativity, but amplifies noise and produces ringing as iterations grow (acceptable solutions "at the cost of hundreds or thousands of iterations" for low noise, per the ADASS summary in the search [V, snippet]; RL-type methods did better on stars than on planet-like extended objects in a 1998 comparison [V, snippet]). Regularised variants:

  • Total-variation RL: Dey, Blanc-Féraud, Zimmer et al. (2006), "Richardson-Lucy algorithm with total variation regularization for 3D confocal microscope deconvolution", Microsc. Res. Tech. 69, 260 (PMID 16586486) [V]: a TV penalty suppresses oscillations while preserving edges; an ImageJ operator exists, with a regularisation factor of 0.002 used in tutorials [V, snippet]. Parameter selection remains manual (Laasmaa et al. 2011 propose an automatic estimate [V, snippet]).
  • Damped RL (White 1994) [M]: ignores updates in pixels where residual is consistent with noise.
  • Early stopping: the simplest regulariser.

Wiener filtering applies H*/(|H|^2 + NSR) in the Fourier domain; fast and linear but yields ringing at sharp edges and requires an NSR estimate. Fine for a quick preview.

Blind deconvolution on a single stacked image is under-determined (Section 6.2); for the stacked solar image it is reasonable to use a non-blind RL with a PSF modelled as (diffraction MTF of the telescope x residual seeing/registration blur x pixel/aperture smoothing). The stack's effective PSF is a Gaussian-like blur of width tied to the residual seeing; ImPPG's choice of a Gaussian kernel with user sigma is a good-enough approximation (ImPPG [V]).

7.3 ImPPG's approach

Pipeline: brightness normalisation, Lucy-Richardson with Gaussian kernel (adjustable sigma and iteration count, plus an experimental option to reduce ringing around bright areas), unsharp masking (several masks, adaptive mode that varies strength with brightness), tone curve; 32-bit float arithmetic throughout; OpenGL GPU back end that is "often several times faster" than CPU and gives a live preview; batch and Lua scripting; GPLv3; version 2.1.0 dated 2025-11-18 (README) [V]. The live-preview architecture is the major usability feature Helia should match or improve on.

7.4 Limb, ringing and edge artefacts

The solar limb is a step from bright disc to dark sky; unconstrained RL/wavelet sharpening produces bright and dark rings around it and, for H-alpha, around prominences. Mitigations:

  1. Process disc and off-limb separately (see Section 8.2) or use a mask-weighted RL that excludes the sky region from the data term.
  2. Extend the disc by reflecting or by extrapolating the limb-darkening profile before deconvolution (boundary handling), and crop afterwards.
  3. Use TV or damped RL and cap iterations.
  4. Apply the sharpening step to a limb-darkening-flattened image (Section 8.3); this reduces the large-scale gradient that otherwise drives ringing.
  5. Wavelet noise thresholds on the finest scales; the 'ringing reduction' options in ImPPG are the baseline to compare against [V].

7.5 Noise handling

After a stack of N frames the noise is approximately Poisson-Gaussian; Anscombe or generalised Anscombe variance stabilisation followed by Gaussian denoising (wavelet thresholding, BM3D [M]) is the classical route. For a scientific tool the safest default is denoising only via the wavelet threshold tied to a measured noise level, with an explicit user switch, and no non-linear patch denoiser by default.

7.6 Recommendation

StageVerdict
Unsharp mask, tone curve, brightness normalisationMVP (trivial, GPU).
à trous wavelet sharpening with per-scale gains and noise thresholdMVP.
Non-blind RL with Gaussian (and Airy-plus-Gaussian) PSF, damping and iteration capMVP+, real-time on GPU.
TV-regularised RLLater.
WienerLater; optional.
Blind single-image deconvolutionNot worth it.
Limb-aware masked processingMVP+ (needed for solar).

8. Solar-specific workflow

8.1 Wavelength regimes

RegimeTargetPractical notes
White light (continuum, ND or Herschel wedge, 500 to 600 nm)granulation, sunspots, faculaehigh photon flux, short exposures easy; granulation contrast a few per cent; limb darkening strong.
H-alpha (656.3 nm, narrowband etalon)chromosphere, filaments, prominences, plagephoton-limited, etalon sweet spot and rings (Section 2.3); thin field-dependent bandpass; chromospheric structure is high contrast but soft.
Ca II K (393.4 nm)plage, networkr0 at 393 nm is about 0.54 of its value at 656 nm by the lambda^(6/5) scaling (my arithmetic), so seeing is worse; diffraction limit is better; need UV-sensitive sensor and glass choices.

8.2 Prominences versus disc: exposure and compositing

H-alpha disc and off-limb prominences differ in brightness by more than an order of magnitude [M]. Standard amateur practice: take separate captures at different exposure (or process separate stacks with different tone curves), then blend with a limb mask. Helia should (a) allow two exposure sets per scene, (b) detect the limb by circle fit, (c) provide a feathered radial mask (width in arcsec) and (d) stack and sharpen each separately before compositing in linear float, then apply a single tone mapping. Processing in linear light before tone curves is essential to avoid halos.

8.3 Limb-darkening correction

For white light, Neckel & Labs (1994), "The solar radiation between 3300 and 12500 A" (Solar Physics 153, 91): they measured the centre-to-limb variation at 30 continuum wavelengths from 303 to 1099 nm (observations at Kitt Peak, 1986 to 1987), fitted by fifth-order polynomials in mu = cos(theta), with data within 7 arcsec of the limb excluded because of seeing, which corresponds to mu of about 0.12 (summarised in arXiv search results) [V, snippet; coefficient tables not retrieved]. Later work found about 0.4 per cent accuracy and possible over-correction for scattered light [V, snippet]. For H-alpha and Ca II K there is no clean closed form for the user's instrument (etalon bandpass and filter profile change the centre-to-limb behaviour), so Helia should fit an empirical radial profile to the image (azimuthally median-averaged, spline) and divide by it, with the fit mask excluding active regions.

8.4 Colourisation and inversion

There is no standard "true colour" for monochrome H-alpha; conventions are orange or red gradients applied to a mono image, or inverted greyscale (dark disc prominences become bright) used to show prominences and filaments. Helia should keep the processed result as scientific mono float and apply colour as a LUT in an export step, with the LUT declared in metadata, so the process remains reversible and scientific data stay uncoloured.

8.5 Derotation and animation

The Sun rotates differentially; the Snodgrass & Ulrich (1990, ApJ 351, 309) sidereal rate is approximately 14.713 - 2.396 sin^2(φ) - 1.787 sin^4(φ) degrees per day [M, confirm coefficients]. My arithmetic for the equator at disc centre: 14.713 deg/day is 0.01022 deg/min or 1.78e-4 rad/min, and times a solar radius of about 960 arcsec that is roughly 0.17 arcsec per minute. A 10 minute capture therefore moves features by about 1.7 arcsec at disc centre (more of the disc's apparent motion is foreshortened near the limb), comparable to the seeing, so rotation matters only for captures of several minutes at high sampling. Helia should therefore stack bursts of at most a few minutes unless it derotates by projecting to heliographic coordinates. Derotation of mosaic panels and time-lapse is a standard operation in amateur tools (WinJUPOS for planets and Sun, which is closed source and Windows-only [M]). This needs the position angle (P), B0, L0 from ephemeris (can use an SPICE or low-precision formulae; a mature Rust ephemeris crate was not verified [unverified]).

Time-lapse: align frames to a limb-fit circle, apply a consistent tone map, and optionally derotate; export through ffmpeg. Keep this later.

8.6 Full-disc mosaics

A full disc at fine sampling needs panels. A pipeline: capture overlapping panels (20 to 30 per cent), stack each, flatten each radially, estimate pairwise translations in the overlaps (phase correlation; includes small rotation/scale from lens distortions as a refinement), solve a global least-squares for panel positions, blend with feathering or multi-band (Burt-Adelson [M]) in linear space. Alternatives to Microsoft ICE (which I recall as discontinued by Microsoft [M, unverified]) are Hugin with Enblend/Enfuse (GPL, cross-platform), PTGui (commercial), and Siril's own mosaic features. Solar disc mosaics have an easy geometry: the limb provides global constraints, so a limb-circle prior can regularise panel positions. Evolution of the Sun between panels (minutes) is the main inconsistency, which argues for fast panel acquisition and a consistent seeing selection per panel.


9. Machine learning

9.1 Published solar restoration networks

  • Enhance, Díaz Baso & Asensio Ramos (2018), "Enhancing SDO/HMI images using deep learning", A&A 614, A5 [V]: two fully convolutional residual networks (about 450k parameters; a 40k lite version) for continuum and magnetograms, trained on MURaM simulations degraded by an HMI PSF, to deconvolve and 2x super-resolve. Granulation contrast rose from ≈3.7 to ≈7 per cent on real data compared to degraded Hinode; 400 x 400 images at about 100 ms on GPU. The authors state the result "is a clear indication that Enhance is introducing prior information not present in the data": the residual after re-convolving the output with the PSF is about 4 per cent compared with under 1 per cent for RL; performance is poorer near the limb; training used one simulation. This is a space-based, known-PSF problem; it does not transfer to variable seeing.
  • Real-time multiframe blind deconvolution, Asensio Ramos, de la Cruz Rodríguez & Pastor Yabar (2018), "Real-time, multiframe, blind deconvolution of solar images", A&A 620, A73 [V]: an encoder-decoder with a fixed 7-frame input (3.15M parameters) and a recurrent variant (4.02M) trained on 80,000 patches of 88 x 88 pixels using MOMFBD reconstructions of SST/CRISP data as targets; inference under 5 ms per 1k x 1k image on a Titan X; outputs resemble MOMFBD but are slightly less sharp for fine Ca II 8542 fibrils; shuffling inputs yields different artefacts and can create spurious polarisation signals; one telescope, one instrument, one day; hallucination not directly tested [V]. The training target is itself an estimate, so the network inherits MOMFBD's biases.
  • Neural blind deconvolution, Schirninger, Jarolim, Veronig, Rempel & Wöger (2026), "Neural blind deconvolution to reconstruct high-resolution ground-based solar observations", arXiv:2603.05033 [V abstract]: a physics-informed coordinate network (implicit neural representation) jointly fitting object and PSFs from a burst, trained per scene; tested on synthetic MURaM data and real GREGOR and DKIST bursts; claims small-scale features beyond state-of-the-art speckle and MFBD; the abstract implies spatially varying PSFs are future work. The "unprecedented" claim is the authors' own and I did not read the full comparison.

9.2 Hallucination and the scientific stance

A prior-trained network fills in plausible detail. For a scientific tool the risks are: (1) detail that looks like granulation but is invented (Enhance's own admission above); (2) systematic flux and contrast changes; (3) generalisation failure under unseen seeing, wavelength or instrument; (4) false features near the limb. Mitigations: never overwrite the data; output as a separate layer with a visible badge and metadata; compute the data-consistency residual (re-blur the output with the estimated PSF and compare to the input stack; large residual means the network added information); require input-shuffling consistency (as the RT-MFBD authors did, averaging 20 shufflings halved noise variance [V]); and test with injected features absent from the input to see whether the model hallucinates them (Section 12).

9.3 Runtime options: ONNX Runtime and Coral Edge TPU

  • Coral Edge TPU: full acceleration needs 8-bit quantised tensors with fixed shapes known at compile time; tensors should be 1D, 2D or 3D; only supported ops are accelerated, and the compiler cannot partition more than once, so after the first unsupported op everything runs on the CPU (Coral model requirements) [V]. A reported example: UpSampling2D unsupported, replaced by a bilinear resize op [V, snippet]. A frame-quality classifier (small CNN on 64 x 64 patches, int8) fits well; restoration networks with upsampling, 4D tensors, large inputs and wide dynamic range (solar contrast of a few per cent over 12 bits) fit badly because 8-bit weights and activations lose small-contrast information.
  • ONNX Runtime: supports CPU, CUDA and DirectML providers and a WebGPU execution provider [M, check current status]; in Rust it is used through the ort crate, and there are pure-Rust alternatives such as tract and burn (which has a wgpu backend) [M, unverified]. The cleanest design for Helia is to keep the core pipeline model-free, and add an optional plug-in that runs an ONNX model on patches, so the main wgpu pipeline remains deterministic.

9.4 Frame-quality classifiers

A learned frame-quality scorer can use the synthetic harness (Section 12) for supervision, with the known per-frame Strehl or sharpness as label. Risk is low because it only reorders frames and never writes pixel values, so it is the safest ML use. Evidence that it beats a band-limited Fourier metric: none found. Treat as a later experiment.

9.5 Recommendation

UseVerdict
Learned frame-quality scorer (small CNN, ONNX)Later, experimental.
Learned denoiser on the final stackLater, optional, clearly labelled.
Learned deblurring/super-resolution of the stackNot for MVP. If added, only as a separate output with data-consistency residual and a hallucination test.
Neural MFBD (RT-MFBD style)Not worth it for Helia: needs MOMFBD training targets and calibrated instrument data.
Coral Edge TPUNot worth it, except possibly for a tiny classifier.

10. Existing tools comparison

Licence and platform data not verified in this session are tagged [M]. Where I only know a tool from background knowledge I say so.

ToolLicencePlatformsStrengthsGaps Helia can fill
AutoStakkert!3Freeware, closed source [M]Windows (works under Wine) [M]Fast, robust AP-based stacking, local AP quality, noise-robust gradient metrics, drizzle, de-rotation of planets [M]; the de facto standardClosed; Windows-only; limited solar-specific tools (no limb-aware processing, no flats built in for solar rings); no transparent algorithm description
Registax 6Freeware, closed source [M]Windows [M]Wavelets sharpening; long-establishedOld alignment, no modern multipoint; Windows only
PlanetarySystemStackerGPLv3 (source header) [V]Python 3 with Qt5; Windows, Linux, macOS [V partial]Open, multilevel correlation, AP rejection, per-AP ranking, optional sharpening, drizzle option [V]Pure-Python/OpenCV speed; planetary focus; GUI heavy; no solar-specific limb handling
Siril (multipoint)GPLv3 [M]Windows, Linux, macOSAP engine with refinement, smoothing, warp engine (experimental), SER support, scripting, 32-bit pipeline [V]Primarily deep-sky workflow; multipoint is new; no solar flat/limb tools; CPU-oriented; no deconvolution tuned for solar [M]
ImPPGGPLv3 [V]Windows, Linux, macOS (OpenGL) [V partial]Interactive GPU RL + unsharp + tone curve; sequence alignment by phase correlation or solar limb; batch; Lua scripting [V]Not a stacker; Gaussian-only PSF; limited limb-ringing control
AstroSurfaceFreeware [M]Windows [M]All-in-one: stack, wavelets, RL [M]Closed source; unverified details
FireCaptureProprietary, free (aggregator listing) [V, secondary]Windows, macOS, Linux, Raspberry Pi [V, secondary]Capture (with SER), planetary and solar ROI, pluginsCapture only
SharpCapMostly free, Pro paid (about £14 per year per FAQ) [V]WindowsCapture, live flats and live stacking [V]Capture-side; Windows only
KISIP / SSTRED / MOMFBDProfessional; availability per observatory; MOMFBD code released by Löfdahl [M]; torchmfbd open (PyTorch) [V]Linux / IDL / C++ / PythonSpeckle and MFBD at professional qualityNot packaged for amateurs; no GUI; calibration-heavy

Gaps in the open landscape that Helia can address, by priority:

  1. Solar-specific stack: limb-aware alignment and flats, limb-darkening and radial flattening, and prominence compositing as first-class features.
  2. GPU throughput for the AP stage and live parameter preview (only ImPPG currently uses a GPU, and only for sharpening).
  3. Transparent, reproducible algorithms with deterministic CPU reference (PSS has it in Python, but slowly).
  4. Metrics and diagnostics: quality-versus-time plots, per-AP stack-depth maps, shift-field maps (none of the free tools expose these well).
  5. Optional research-grade restoration (per-sub-field MFBD) behind a flag.

11.1 Ordered stages

MVP (milestone M1)

#StageMethodReason
1ReadSER (v2/v3, defensive header and endianness), FITS cube; timestampsThe native astro capture format; timestamps support seeing diagnostics and later derotation.
2CalibrateMaster dark (optional) and flat (solar-aware: allow flats captured at the same focus); hot-pixel map; convert to f32Dust and etalon rings are fixed in the sensor frame while the image moves; calibration prevents stacking a fixed pattern into detail.
3Frame quality (global)Band-limited gradient energy with noise-floor subtraction and brightness normalisation; Laplacian std as an alternativeCheap, GPU-trivial, mirrors AS!3/PSS/Siril; only for reference choice and coarse cut.
4Global alignmentWindowed, high-pass phase correlation (FFT), sub-pixel via quadratic fit; limb-fit option for full-disc framesThe tip/tilt dominates the variance (Section 1.2); phase correlation is contrast-independent; limb stabilisation exists in ImPPG.
5AP gridSpacing and box in arcsec tied to isoplanatic scale (default 5 to 10 arcsec box); rejection by brightness, contrast, structure; 50 per cent overlapSection 1.4; same rejection logic as PSS and Siril.
6Local quality and selectionPer-AP metric as stage 3 at patch scale, rank frames, soft cutoffThe isoplanatic argument; fade at the cutoff as Siril does.
7Local shiftsNCC search ±S px, coarse-to-fine, quadratic sub-pixelCheap on GPU (Section 4.5), PSS and Siril precedent.
8StackQuality-weighted mean of resampled patches, Hann windows, background fill from global stack; optional sigma clip off by default; f32 accumulationLinear photometry; no seams (Siril patch engine).
9Basic sharpenUnsharp mask, à trous wavelets with per-scale gain and noise threshold; export f32 FITS/16-bit TIFFImmediate usefulness; compatible with ImPPG for RL.
10DiagnosticsQuality curve, AP validity map, shift-field plot, stack-depth mapFills a gap in tooling; essential for debugging.

MVP+ (M2)

#StageMethodReason
11Two-pass AP refinementStack best frames per AP, re-measure shifts against sharper local reference; robust smoothing of the shift fieldRemoves reference bias; Siril default.
12Non-blind RLGaussian + Airy-based PSF, damping, iteration cap, live previewImPPG parity; real-time on GPU.
13Limb-aware processingRadial flat, limb circle fit, masked deconvolution, ringing guardSolar-specific ringing control.
14Quality metric upgradeBand-limited Fourier power with measured noise floor, selected by synthetic testsTests the hypothesis in Section 3.2.

Later (M3 and beyond)

#StageMethodReason
15Dense warp / destretchSmooth displacement field, resample each frame once (bicubic/Lanczos)Removes patch seams; Siril's experimental warp engine shows feasibility.
16DrizzleSplat kernel with pixfrac, enabled when q < 1Only for undersampled data.
17MosaicPhase-correlation panel registration, limb prior, multi-band blendFull-disc imaging.
18Derotation / time-lapseHeliographic projection using P, B0, L0 and differential rotationLong captures and animations.
19TV-RL and WienerRegularised optionsQuality and control.
20Per-sub-field MFBD (experimental)Zernike-parametrised PSFs, L-BFGS, batched small FFTsResearch path; follow torchmfbd/SSTRED structure.
21ML plug-insONNX patch models for denoise and frame-quality; labelled outputsOptional, hallucination-guarded.

11.2 Changes to the current MVP design

The current design is: CPU reference + wgpu backend, SER/FITS input, quality ranking, phase-correlation global alignment, AP-grid multipoint alignment, sigma-clipped stacking, drizzle stub. Findings:

  1. Keep the CPU-reference plus wgpu approach, SER/FITS input, and the AP grid. They match the practice of every tool reviewed.
  2. Change quality ranking from a single global score to global plus per-AP local ranking, with a soft taper and brightness normalisation. A single global rank is inadequate on the Sun (Section 1.4). Add noise-floor subtraction to the metric.
  3. Add calibration (flats, darks, hot pixels) to the MVP. Solar data are dominated by dust and etalon fringes; without flats, de-warping amplifies fixed patterns. This is not in the list above.
  4. Define AP size in arcsec, not pixels, from the user's optics, and warn about sampling. Add a sampling-ratio calculator.
  5. Phase correlation needs windowing, high-pass whitening and a limb-fit fallback; do not rely on a naive FFT phase correlation of raw frames. Plan for it to fail on featureless ROIs; return a confidence value.
  6. Make sigma clipping optional and off by default; default to quality-weighted mean. In seeing-selected stacks the clip can erode fine contrast (Section 5.1). Retain it as an outlier guard.
  7. Re-shape the drizzle stub into a general "splat accumulator" so that bilinear resampling, Lanczos resampling and drizzle are different kernels of the same accumulator. Activate drizzle only when the sampling ratio indicates undersampling.
  8. Add the refinement pass and shift-field smoothing as soon as the single pass works (Siril's default behaviour; small cost).
  9. Add a minimal sharpening stage (unsharp and à trous) to the MVP, plus f32 FITS/TIFF output, so that users can complete a workflow without leaving Helia; defer RL to M2 unless trivial.
  10. FFT strategy: wgpu has no FFT library; avoid depending on FFT for the AP stage (use direct NCC with small search windows) and write one radix-2/4 Stockham FFT for the global step and for later Fourier metrics. Take care with f32 accumulation (no f64 in baseline WGSL [M]); use pairwise or Kahan summation in the stack.
  11. Determinism: CPU and GPU results will differ at f32 rounding; define tolerances (Section 12) and test both.

12. Test methods

12.1 Synthetic data with known truth

Build a generator in the repository (Rust or Python tooling) producing sequences where every quantity is known:

  1. Truth object: (a) a granulation-like field made by filtering random noise to a power-law spectrum with Voronoi cells (cheap); (b) a radiative-MHD snapshot if available (MURaM is used in [Díaz Baso 2018] and [Schirninger 2026]; I did not verify whether public snapshots can be redistributed) [V for use, unverified for availability]; (c) a limb disc with a Neckel-Labs profile and sharp dark sky for ringing tests; (d) a bar target and Siemens star for MTF.
  2. Turbulence: phase screens by the FFT method with a Kolmogorov or von Kármán spectrum (Φ(κ) ∝ r0^(-5/3) κ^(-11/3) [M]) with chosen r0 (3 to 10 cm), frozen flow (wind translates the screen between frames), and several layers or a ground-layer screen with height so that the PSF varies across the field: anisoplanatism is simulated by shifting the screen by an amount proportional to field angle [M]. The truth tilt field is recorded for every frame.
  3. Imaging: PSF = |FT(pupil · exp(i φ))|^2 with a pupil including central obstruction if desired; convolve (space-variant by patches with overlap) with the truth object; resample to a camera pixel grid at a chosen q (undersampled, Nyquist, oversampled); add Poisson and read noise; quantise to 8/12/16 bits; apply global jitter.
  4. Instrument defects: dust shadows, Newton-ring-like fringes, hot pixels, brightness drift, vignetting, so that calibration and robust rejection have a truth.
  5. Export as SER with timestamps so the whole reading path is tested.

12.2 Per-stage measurements

StageMeasurePass criterion (initial proposal, tune on data)
ReaderRound-trip bit-exactness against the generator's stored frames; fuzz of malformed headers0 differences; no panic on malformed input
CalibrationResidual correlation of the output with the injected dust/fringe patternAt least 20 dB reduction
Quality metricSpearman rank correlation between metric and truth per-frame (and per-AP) Strehl or sharpness, across r0 and noise levels; also fraction of the truth-best 10 per cent captured by the metric's top 10 per centReport for each metric; choose the highest and most noise-stable
Global alignmentRMS error vs truth tilt after removing the field meanBelow 0.1 px at SNR where visible features exist; reports low confidence at featureless ROIs
AP shiftsRMS error of the recovered shift field vs the truth tilt field; failure rateBelow 0.15 px; failure below 2 per cent
StackPSNR/SSIM vs the truth object (convolved with the diffraction-limited PSF, which is the best possible outcome), azimuthally averaged power-spectrum ratio, granulation rms contrast vs truth, flux conservationMonotonic improvement with N and selection; contrast ratio approaching diffraction-limited value; flux error below 0.5 per cent
Sigma clipBias in mean and contrast with and without clipping on clean dataContrast loss below 1 per cent at default k
DrizzleResolution gain (MTF at the Nyquist of the original grid) for q < 1; noise correlationGain only when undersampled; documented noise penalty
Sharpening/RLMTF recovery; ringing amplitude at the synthetic limb and at bar targets (overshoot percentage); noise gain; reproducibility of fluxOvershoot below a stated threshold (e.g. 2 per cent); no negative pixels
MosaicSeam residual and position error vs truth shiftsBelow 0.3 px; no visible seams
DerotationFeature position error after derotation vs the synthetic rotationBelow 0.5 px
MFBD (if built)Recovery of injected Zernike coefficients; improvement over the stack; failure on mismatched PSFCoefficient error below 20 per cent of the injected value
ML (if built)Injected-feature test: add a feature to the truth that is absent from inputs; confirm the model does not create it; data-consistency residualNo hallucinated feature; residual reported with every ML output

12.3 CPU versus GPU parity and performance

  • Run each kernel on the CPU reference and wgpu on identical inputs; compare with a stated tolerance (for example max absolute difference of 1e-4 relative for f32 stacks; ranks may swap near ties, so compare rank-correlation not equality).
  • Record throughput (frames/s per stage) and peak memory; a regression test with a fixed 5000-frame synthetic capture.
  • Run tests on multiple GPUs and the software (llvmpipe/WARP) adapter to expose backend-specific bugs [M].

12.4 Real captures

  1. Self-consistency: split-half stacks (odd and even frames, or two independent selections) and Fourier Ring Correlation between them as a resolution estimate with no ground truth (FRC; Saxton & Baumeister 1982, van Heel 2005) [M]; the resolution at which FRC drops below 0.143 or 0.5 gives a defensible, comparable figure, and the stability of FRC is a test of the whole pipeline.
  2. Cross-tool comparison: stack the same SER with Helia, AutoStakkert!, PSS and Siril on identical selection fractions; compare granulation rms contrast, power spectra, and subjective detail; publish SER files and settings.
  3. Cross-instrument comparison: compare against co-temporal Hinode/SOT or SDO/HMI continuum images for white-light active regions (as the Enhance and speckle papers did with Hinode [V]); after reprojection and PSF-matching the correlation coefficient and contrast spectra can be computed. Data availability and licensing of such products should be confirmed before building tests on them [unverified].
  4. Public professional data: GREGOR HiFI and DKIST bursts have been used in published reconstructions (Schirninger 2026); whether bursts are open for redistribution in a test suite I have not verified.
  5. Regression gallery: a small set of real SER captures (white light, H-alpha, Ca II K) of varied seeing, with recorded settings, as golden outputs checked in as hashes plus tolerance, reviewed visually by a person before any golden update.
  6. Seeing diagnostics: plot the quality metric against time per capture; compare the autocorrelation time with the 50 Hz+ guidance (Section 1.5) and check that the metric tracks visible blur on hand-picked frames.

13. Open questions and unverified items

  • Daytime r0 and coherence-time tables (Rimmele & Marino 2011) not retrieved; numbers in Section 1 are order-of-magnitude.
  • No solar frame-metric comparison with ground truth found (Section 3.2); Denker et al. (2018) full text not read; MFGS authorship and definition are [M].
  • No amateur speckle or MFBD results found (Section 6.4).
  • Licence and platform data for AutoStakkert!, Registax and AstroSurface unverified; Siril's GPLv3 licence is from background knowledge.
  • Whether a mature wgpu FFT crate, Rust ephemeris crate, or ONNX WebGPU execution provider is production-ready was not checked.
  • Uitenbroek et al. G-band contrast numbers come from a search snippet and should be confirmed in the paper before quoting.
  • Snodgrass & Ulrich coefficients, Neckel-Labs coefficients, Noll 87 per cent tilt figure, the Fried-1978 constants, and Registax's internal wavelet definition should be re-checked at the source.

14. Reference list (with check status)

Primary and near-primary sources that were opened or surfaced in this review [V]:

  • Asensio Ramos, de la Cruz Rodríguez, Pastor Yabar (2018), Real-time, multiframe, blind deconvolution of solar images, A&A 620, A73. https://ar5iv.labs.arxiv.org/html/1806.07150
  • Díaz Baso & Asensio Ramos (2018), Enhancing SDO/HMI images using deep learning, A&A 614, A5. https://ar5iv.labs.arxiv.org/html/1706.02933
  • Schirninger, Jarolim, Veronig, Rempel, Wöger (2026), Neural blind deconvolution to reconstruct high-resolution ground-based solar observations. https://arxiv.org/abs/2603.05033
  • Denker et al. (2018), Image Quality in High-resolution and High-cadence Solar Imaging, Solar Physics. https://arxiv.org/abs/1802.00760
  • Law, Mackay, Baldwin (2006), Lucky imaging: high angular resolution imaging in the visible from the ground, A&A 446, 739. https://arxiv.org/abs/astro-ph/0507299
  • Fried (1978), Probability of getting a lucky short-exposure image through turbulence, JOSA 68, 1651. https://doi.org/10.1364/JOSA.68.001651
  • Fruchter & Hook (2002), Drizzle, PASP 114, 144. https://arxiv.org/abs/astro-ph/9808087
  • Guizar-Sicairos, Thurman, Fienup (2008), Efficient subpixel image registration algorithms, Opt. Lett. 33, 156. https://labsites.rochester.edu/fienup/wp-content/uploads/2019/07/MGS_OL08_EffRegistration.pdf
  • Löfdahl (2002), Multi-frame blind deconvolution with linear equality constraints. https://arxiv.org/abs/physics/0209004
  • Wöger, von der Lühe, Reardon (2008), Speckle interferometry with adaptive optics corrected solar data, A&A 488, 375. https://www.aanda.org/10.1051/0004-6361:200809894
  • Rimmele & Marino (2011), Solar Adaptive Optics, Living Rev. Sol. Phys. 8, 2. https://doi.org/10.12942/lrsp-2011-2
  • Dey, Blanc-Féraud, Zimmer et al. (2006), Richardson-Lucy with total variation regularisation, Microsc. Res. Tech. 69, 260 (PMID 16586486).
  • Torchmfbd (2025). https://arxiv.org/pdf/2505.10639 ; SSTRED (2018). https://arxiv.org/pdf/1804.03030 ; MFBD with uncorrected high-order modes (2022, authors not verified). https://arxiv.org/pdf/2205.13650; Marginal MFBD (2026). https://arxiv.org/pdf/2605.11980
  • Tools: PlanetarySystemStacker https://github.com/Rolf-Hempel/PlanetarySystemStacker ; Siril multipoint https://siril.readthedocs.io/en/latest/preprocessing/multipoint.html ; ImPPG https://github.com/GreatAttractor/imppg ; AutoStakkert! https://www.autostakkert.com ; SER format https://grischa-hahn.homepage.t-online.de/astro/ser/index.htm ; Coral https://www.coral.ai/docs/edgetpu/models-intro

Classical references cited from background knowledge [M]: Fried (1966); Noll (1976); Labeyrie (1970); Knox & Thompson (1974); Weigelt (1977); Lohmann, Weigelt & Wirnitzer (1983); von der Lühe (1993, 1994); van Noort, Rouppe van der Voort & Löfdahl (2005); Gonsalves (1982); Löfdahl & Scharmer (1994); November & Simon (1988); Neckel & Labs (1994); Snodgrass & Ulrich (1990); Richardson (1972); Lucy (1974); White (1994); Kuglin & Hines (1975); Starck, Murtagh & Bijaoui (1998); Saxton & Baumeister (1982); van Heel (2005).

Helia: state of the art in solar imaging methods, refresh of October 2026

Status: research refresh, 2026-10-18, for tracking issue laurence/helia #15 ("include all the most up to date processes"). Companion to docs/research/imaging-methods.md (dated 2026-10-17), which this file does not replace. Audience: Helia designers (GPL-3.0, pure Rust, wgpu).

0. How to read this document

Tags:

  • [V] verified this session: the source page, abstract, changelog, code listing or PDF text was opened and read (WebFetch or a local PDF extraction).
  • [U] unverified: seen only as a web-search snippet or a secondary summary, or stated from background knowledge. Treat as "probably right, confirm before relying on it".

Scope limits, stated plainly. WebFetch summarises pages with a small model, so numbers quoted from fetched pages are as reported by that summary; anything that drives a design decision should be re-read at source before it is acted on. I found no 2023 to 2026 paper that compares TV-L1 or learned optical flow against AP-grid destretching on solar seeing data, no solar-specific diffusion-model restoration paper, and no GPU speckle paper newer than 2020 outside the PyTorch MFBD work. Where a search found nothing, that is said in the section.

Dates: today is 2026-10-18. "Current" means as of the sources dated below.


1. Professional pipelines and their latest methods

1.1 DKIST (VBI, VTF, data centre)

  • VBI reconstruction uses a burst of 80 frames at 30 Hz (2.667 s); with mechanism moves each reconstructed image takes 3.4 s; the raw frames are discarded after reconstruction, so Level 0 is never available for speckle-reconstructed VBI data. The NSO page says reconstruction was then done in Boulder rather than at the telescope, and that reconstructed images are re-ingested for Level 1 conversion to SPEC-0214. Field of view per image is limited by the 4k x 4k detector to 45 x 45 arcsec (blue) or 69 x 69 arcsec (red), covered by tiling (NSO VBI page) [V]. The page is an older Confluence version and may lag current practice.
  • Beard, Wöger & Ferayorni (2020), "Real-time speckle image processing with the DKIST", SPIE 11452, 114521X: a real-time speckle pipeline for the VBI first-light instrument, C++ implementation, first-light reconstruction shown (SPIE) [U: only the opening of the abstract was visible via search; the page itself returned empty]. The earlier plan was CUDA acceleration (Wöger & Ferayorni 2012, SPIE 8451, 84511C) [U, snippet].
  • The algorithm family is KISIP-style speckle masking in isoplanatic sub-fields (see imaging-methods.md section 6.1). The KIS VTF data-pipeline prototype, delivered to the DKIST Data Center in December 2021, "uses the GPU-integrated speckle code developed by Wöger et al." for broadband channels and Keller & von der Lühe (1992, A&A 261, 321) speckle interferometry for narrowband data (Leibniz-KIS) [V]. No algorithm internals, GPU model or benchmark figures are given there.
  • Level 1 DKIST FITS conforms to FITS 4 with DKIST header keys; a VBI Level 1 file is a single wideband image with a single timestamp, and each dataset has an ASDF metadata file (DKIST data centre docs) [U, snippet]. The header dictionary is SPEC-0214 (draft seen: Jan 2019), validated by the dkist-header-validator package [U].

I could not find a public DKIST statement that MFBD is part of the VBI production path; the published production path is speckle. The only DKIST MFBD result I found is research (Schirninger et al. 2026, section 2).

1.2 SST (SSTRED, MOMFBD)

  • SSTRED handles CRISP and CHROMIS. The MOMFBD step was reworked through a fork called REDUX; metadata follow SOLARNET recommendations; arXiv revisions run to v4 (August 2021) and the journal version is A&A 2021 (arXiv:1804.03030) [V via search summary; journal volume not confirmed].
  • Searches for 2023 to 2024 SSTRED or REDUX GPU work found nothing. The SST remains the reference implementation of MOMFBD with sub-fields of about 4 to 5 arcsec (Löfdahl & Hillberg 2022, below) [V].

1.3 GREGOR (sTools, HiFI+)

  • sTools is IDL-based, Creative Commons licensed, delivers reduced and image-reconstructed HiFI and GFPI data, ranks HiFI frames with MFGS (typically 500 per burst, best 100 restored), and has interfaces to KISIP and MOMFBD (arXiv:1701.01670) [U, snippet].
  • HiFI+ (2023, JATIS) offers six bands (Ca II H 396.8 nm, G-band 430.7, blue continuum 450.6, narrow and broad H-alpha 656.3, TiO 705.8 nm); every channel can be restored by speckle masking or MFBD (IAC listing) [U, snippet]. I found no sTools 2023 to 2024 changelog.

1.4 BBSO GST and NVST

  • GST uses post-facto speckle reconstruction for diffraction-limited imaging; deep-learning work around it uses GST as training truth for HMI and GONG super-resolution, not as a replacement for the GST speckle step (A&A 2023 SDGAN, DeepHMI/SDGAN listing; GONG to GST, arXiv:2510.06281) [U, snippets].
  • NVST: parallel CPU/MPI speckle (2015 to 2017); the 2017 paper planned a GPU port (arXiv:1701.00581) [U, snippet]. NVST also produced the MFGS metric (section 5).

1.5 GPU speckle and GPU MFBD

WorkWhatSourceTag
Zheng et al. 2018CUDA speckle masking, 9 to 10 x speed-up for a 256 x 256 sub-imageKorea Science[U]
Xuan et al. 2020CUDA speckle, 2304 x 1984 TiO image in about 70 s, up to 7 x faster than CPUJ. Atmos. Environ. Opt. 15(2), 90[U]
Asensio Ramos, Díaz Baso, Kuckein, Esteban Pozuelo, Löfdahl 2025torchmfbd: MOMFBD in PyTorch, MAP estimate, spatially variant PSFs, wavefront or data-driven PSF bases (experimental NMF), smoothness and sparsity regularisation, patch processing, apodisation and destretching tools, GPU acceleration, A&A 703, A269, MIT licence, pip install torchmfbdarXiv:2505.10639, repo[V]
Asensio Ramos 2026Marginal MOMFBD estimator: adds a log-determinant term to the merit function; regularises, keeps noise out of high-order aberrations, makes object-PSD hyperparameters auto-tunable; integrated into torchmfbdarXiv:2605.11980[V]
Löfdahl & Hillberg 2022"Statistical diversity": add random Kolmogorov-statistics high-order aberration tails (scaled by r0) to MFBD; improves contrast and power-spectrum accuracy on noise-free synthetics, same run time; real-data tests still needed. A&A 668, A129arXiv:2205.13650[V]

No timings are published in the torchmfbd abstract (it says only "significantly reduces computation time") [V]. For Helia this means torchmfbd is the practical reference oracle for any Rust MFBD: run it on the same synthetic bursts and compare recovered Zernike coefficients, rather than trusting a new implementation unaided.


2. Machine learning for solar restoration, 2023 to 2026

WorkIdeaTraining signalHallucination risk (my assessment)Optional step?
Asensio Ramos, Esteban Pozuelo, Kuckein (2023), "Accelerating Multiframe Blind Deconvolution via Deep Learning", Sol. Phys. (arXiv:2306.12078) [V]A CNN and an algorithm-unrolled model (fixed few gradient-descent steps, small networks correcting each step) that output wavefront coefficients and a restored image; orders of magnitude faster than MOMFBD per patchUnsupervised: loss is the image-formation (data-consistency) model on observed CRISP, CHROMIS and HiFI data; trained models, code and data releasedLowest of the set: the forward model is in the loss, so output must re-blur to the input. Residual risk: wavefront prior and generalisation to unseen seeingYes, later, as a PSF-estimator or initialiser feeding a classical non-blind step, with the residual reported
Asensio Ramos (2024), spatially variant convolution emulator, A&A 688, A88 (arXiv:2405.09864) [V]NN emulates space-variant blur as the MFBD forward model, avoiding patch mosaickingSynthetic convolutionsLow (forward model only) but PSF-field accuracy unproven on my readingResearch watch
Schirninger, Jarolim, Veronig, Kuckein (2025), "Deep learning image burst stacking...", A&A (arXiv:2506.04781) [V]Network turns a 100-frame burst into one image in real time (about 0.5 s per burst [U, snippet])Unpaired image-to-image translation, speckle reconstructions as the "good" domainHighest of the learned-restoration set: generative translation, "better perceptual quality than speckle" is a perceptual claim, not fidelity. The abstract does not discuss hallucination, names no telescope and links no code [V]. Note: arXiv page lists "A&A 693, January 2025" yet a June 2025 submission; check the bibliographic recordNo for default; at most a labelled comparison output
Schirninger, Jarolim, Veronig, Rempel, Wöger (2026), "Neural blind deconvolution...", (arXiv:2603.05033) [V]Coordinate network (implicit neural representation) jointly fits object and PSF parameters per burst; tested on MURaM synthetics, GREGOR and DKIST bursts; spatially varying PSFs left as future workPer-scene physics-informed optimisation, no pretrainingMedium-low in principle (data-consistent by construction) but the abstract has no hallucination discussion and no quantitative comparison; code not linked on the abstract page. A related repository Schirni/neural-MFBD (fork of aasensio/neural-MFBD) exists with unstated licence (repo) [V]Watch; the closest thing to "neural fields for seeing"
Spagnoletti et al. (2024), diffusion posterior sampling for astronomical deconvolution (arXiv:2411.19158) [V]Score-based prior trained on cosmological simulations plus likelihood; proposes a metric flagging prior-driven featuresSimulationsHigh by nature (strong learned prior), though this paper is the only one found that explicitly quantifies prior-driven features. Not solarNo
Space-to-ground super-resolution (diffusion, GAN, Real-ESRGAN) of HMI or GONG using GST as truth, 2022 to 2025 [U, snippets]Enhance low-resolution full-disc productsGST imagesHigh; these invent detail to a learned target and are not restoration of a burstNo, out of scope for Helia input
General turbulence-mitigation video networks (TMT, Tran. Comput. Imaging 2024; DATUM, CVPR 2024; DeTurb, ACCV 2024) [U, snippets]Terrestrial scenes with simulated turbulenceSyntheticUnknown on solar granulation; trained on a different statistics regimeNo
Jarolim et al. (2020), full-disc image quality assessment with GANs, A&A (arXiv:2008.12030) [U, snippet]Unsupervised whole-disc quality classifier for Kanzelhöhe H-alphaGood-quality frames onlyLow (only ranks)Relevant to #8 as a frame-level scorer

Honest assessment:

  1. The two papers that keep the physical forward model inside the training or inference loop (Asensio Ramos et al. 2023, Schirninger et al. 2026) are the only ones I would consider defensible in a scientific tool. Everything trained on "speckle result as target" or on simulations with a hard prior inherits that target's biases, which was already the finding for 2018 networks (imaging-methods.md section 9.1).
  2. No paper I found tests for hallucination with injected features absent from the input, which is the test Helia's synthetic harness can do. That is both a gap and an opportunity: Helia can publish the test.
  3. Suitability as optional, clearly labelled steps: acceptable if (a) the original stack is always written first, (b) the ML output is a separate layer with a visible badge and FITS provenance (PRSTEPn='ML-...', see section 6), (c) a data-consistency residual and an input-shuffle consistency map accompany it, (d) the model is a plug-in outside the core pipeline. Frame-quality scoring (#8) is the safest ML use because it never writes pixels.

3. Amateur and open tools, state in October 2026

ToolVersion and dateLicence, platformWhat matters for HeliaSource
AutoStakkert!44.0.1 beta 2023-11-11 to 4.0.13 2025-02-19 (4.0.11, 2024-01-21 is "reasonably stable"). Adds data browser, automated surface anchor placement, automated quality estimation settings, improved surface tracking for large jumps ("Improved Tracking"), H-alpha AP placement fix for colour cameras, AVX2 line-artefact fix, RGB align/derotation fixes. Stacking about 2 to 3 x faster than AS!3 per a beta tester. Change notes never mention "solar", flats or GPUFreeware (forum: "stayed free"), Windows-native Delphi (Delphi 12 update, FastMM5)Auto anchor and auto quality settings are the AS!4 differentiators Helia lacks. Closed source: no algorithm docdownload, change notes [V]; speed claim and "free" [U]
Siril1.4.4 stable, 2026-06-17; 1.4.0 stable 2025-12-05. No 1.5 or release candidate listed on siril.org. 1.4 headline features are deep-sky (new drizzle, astrometric mosaics, SIP distortion, FITS keyword management, SPCC)GPLv3 [U]; Win/Linux/macOSMulti-point registration and stacking "was introduced in Siril 1.5" (documentation) and is not in any released Siril. Docs describe: Planet (centroid) and Surface (detail) modes, reference = best 5 per cent, grid pitch 2.25 x half-width, search 14 px, global search 34, refine-against-first-pass default on, shift smoothing radius 2.5, zero-mean option, Min brightness/contrast/structure AP rejection, 1x/1.5x/2x/3x scale, patch engine default and experimental warp engine, default demosaic LMMSE, mpp commands. Reworked with input from PSS's authorsiril.org, downloads, 1.4.0, multipoint docs [V]
PlanetarySystemStackerRelease list: V0.5.0 (first) to V0.9.8 (Windows installer, fixes PyInstaller problem); V0.8.31 added stack-N-or-percent; V0.8.18 added RGB-align postprocessing. The release page shows days and months only, no years. Debian/Ubuntu still package a 2023-09 snapshot (0.8.32)GPLv3 (header, from existing review), PythonNothing solar-specific added; maintained at a slow cadencereleases [V for the list, years unknown]
ImPPG2.1.0, "18 Nov" (year not shown; existing review says 2025)GPLv3, C++/OpenGLScripting (Lua), multiple unsharp masks, RGB, adaptive unsharp, solar limb alignment, dering; still the live-preview benchmarkreleases [V; year U]
RegiStax6.1.0.8 (May 2011) is final; page states "This marks the end of development for RegiStax"Freeware, WindowsDead. Its wavelet sharpening lives on in waveSharp (1.0 beta Dec 2023, 2.0 Dec 2024, 3.0 2025-12-09, hosted on Codeberg) and AS!4 now "sends to waveSharp"astronomie.be [V]
LuckyStackWorker5.2.0 (2024-06-23); a search report says 7.0.0 in Jan 2026MIT, Win/macOS; author Wilco KasteleijnPost-stack automation: per-object profiles recognised from FireCapture/AS file names, bulk processing, adaptive sharpening and denoising (Savitzky-Golay 2D, sigma). A workflow model, not an algorithm oneScopeTrader [V for 5.2.0]; 7.0.0 [U]
SolHAT (successor to solar_ha_processing)No releases listed, 39 commits, 2 starsMIT, Rust, CLI; Kevin M. Gill (kmgill)Closest prior art to Helia: H-alpha lucky imaging replacing PIPP and AutoStakkert; flat and dark correction, glitch-frame detection, quality filtering, centre-of-mass centring, crop, parallactic-angle derotation for alt-az mounts, drizzle 1.0 to 3.0 x, optional prominence light set input, user hot-pixel TOML map; no GPU, no multi-point, no limb-darkening feature listed. The older solar_ha_processing repo is declared replacedsolhat, old repo [V]
JSol'Ex (astro4j)5.5.2 (date not shown; 5.6.0-SNAPSHOT in development)Apache-2.0, Java 25, Win/Linux/macOS; Cédric ChampeauFor Sol'Ex spectroheliograph SER, not video lucky imaging, but its feature list is the best open model of solar output handling: limb darkening, colourisation, prominences, mosaic, animation, FITS/SOLARNET metadata, ImageMath, Doppler, autocrop. A 2025 spectroheliogram atlas and a 2026 G-band prominence paper both used it (arXiv:2507.13025, arXiv:2604.11650) [U, snippets]astro4j, docs [V for the feature list]; Sol'Ex only [U]
INTIPython Sol'Ex pipeline, V. Desnoux / C. Buil[U]Predecessor of JSol'Ex for spectroheliography[U]
PIPP, SER PlayerNo successor found. SER Player (free-astro) is a viewer on Flathub[U]PIPP's job (crop, centre, reject frames, calibrate) is what Helia's input stage should replace[U]
AstroSurfaceCould not locate any page or version[U]Existing review rows remain [U]none
WinJUPOSPlanetary derotation tools; no solar full-disc workflow foundClosed, Windows [U]Solar derotation is not a WinJUPOS strength; AS!4 does RGB-channel field derotation itself[U]

Features those tools have that Helia lacks, ranked by impact on a solar user:

  1. Flat/dark/hot-pixel calibration (SolHAT, PSS, SharpCap): #3.
  2. Automatic anchor/AP placement and auto quality settings (AS!4); AP rejection thresholds (Siril brightness/contrast/structure): #16.
  3. Refinement against a first-pass stack, shift smoothing, zero-mean NCC (Siril 1.5 docs): #4.
  4. Alt-az field-rotation (parallactic) correction (SolHAT, AS): not in any issue.
  5. Prominence light-set compositing (SolHAT input, JSol'Ex): not in any issue.
  6. Limb-darkening flattening, colourisation, autocrop, mosaic, animation (JSol'Ex): not in any issue.
  7. RL/unsharp/tone-curve with live preview, scripting (ImPPG): #5.
  8. SOLARNET/FITS metadata (JSol'Ex, SSTRED, DKIST): not in any issue.
  9. Drizzle, warp engine (SolHAT, PSS, AS, Siril): #6.
  10. Demosaic variety (Siril default LMMSE; Helia has bilinear and MHC): low priority for mono H-alpha.

4. Destretching and flow-based registration

  • Destretching is local correlation against a reference then interpolation (November & Simon 1988); it is the same family as LCT/FLCT and as the AP grid [V via search summary of arXiv:0707.1994]. torchmfbd ships destretching and apodisation tools as first-class utilities [V], which confirms it is still the professional pre-step.
  • Method comparison on solar data: Chae & Sakurai (2008) tested LCT, DAVE and NAVE on simulated and Hinode data: NAVE best at sub-pixel, super-pixel and non-uniform motions, LCT the fastest and weakest on non-uniform motion, NAVE the slowest (summarised in search result) [U, secondary]. This concerns velocity measurement, not seeing correction. LCT/feature tracking was found unreliable below about 2500 km and 30 min by Rieutord et al. (2001) [U, secondary]. A 2025 preprint applies Fourier LCT to MURaM (arXiv:2511.19048) [U].
  • Turbulence-distortion registration outside solar: Zhu & Milanfar (B-spline non-rigid registration to a temporal-average reference, temporal regression, then blind deconvolution; UCSC ICCP 2011) [U, snippet]. Shimizu et al. (CVPR 2008) added a stabilisation term for textureless regions, which Zhu & Milanfar criticised as bias [U, snippet]. A 2026 Gaussian-splatting turbulence mitigation paper (GSTurb, arXiv:2602.22800) again uses a multi-frame average as the tilt-free reference [U, snippet].
  • TV-L1, learned flow: I found no 2022 to 2026 solar comparison. TV-L1 is available as a GPU algorithm in OpenCV [U, background]. Granulation is low contrast and evolves, which violates brightness constancy in a way AP correlation (with a small window and a short time span) tolerates better. Learned flow (RAFT-style) would be a trained black box on a task with no ground truth.
  • Comparison with AP grids: the practical difference is smoothness. Siril's experimental warp engine already interpolates AP shifts into one smooth field and resamples each frame once, and its docs say it "may be slightly crisper" but slower [V]. That is the pragmatic version of dense flow: use the AP grid as the sparse estimator and make the dense field by robust smoothing, rather than running a separate flow solver per frame.

Recommendation: keep AP-grid correlation as the estimator; add robust shift-field smoothing and a single-resample warp output (later). Run TV-L1 only as an experiment inside the synthetic harness, scoring recovered shift field RMS against the known truth tilt field; no current evidence supports spending effort before that result exists.


5. Frame and patch quality metrics

Correction to the existing review: comparison studies do exist.

  • Popowicz, Radlak, Bernacki, Orlov (2017), "Review of image quality measures for solar imaging" (arXiv:1709.09458) [V abstract]: reviews 36 methods from 40 years; ground truth is Hinode/SOT images degraded with a Random Wave Vector turbulence simulation (active regions and granulation, many D/r0 values). Three recommended: Helmli and Scherer's Mean (lowest computational cost, usable in virtually any conditions), MFGS (best at good seeing, D/r0 below 4), DCT Energy Ratio (best for strongly blurred images). This is the closest thing to the head-to-head truth comparison the old review said did not exist, and it uses simulated seeing with known truth.
  • Kamlah, Verma, Diercke, Denker (2021), "Wavelength dependence of image quality metrics and seeing parameters and their relation to adaptive optics performance", Solar Physics (arXiv:2011.04346) [V abstract]: MFGS, Median Filter Laplacian Similarity (MFLS), Helmli-Scherer mean, granular rms contrast and differential image motion compared against r0 on HiFI/VTT quiet-Sun data (July and August 2016); UMAP used to characterise seeing; per-wavelength dependence and AO field dependence. No numerical ranking in the abstract.
  • Deng et al. (2015), MFGS definition: a 3 x 3 median-filtered copy of the frame, gradient images of both, a similarity ratio giving a score between 0 and 1 that falls monotonically with quality and correlates linearly with granular rms contrast; tested on NVST and Hinode/SOT (NJIT record, arXiv:1701.05300) [V]. Denker et al. (2018) moved to a Scharr-operator gradient magnitude [U, snippet].
  • No 2023 to 2026 solar frame-metric comparison found. The ML route to quality is Jarolim et al. (2020) whole-disc classifier and Gao et al. (Sensors, Dec 2023) PSF-to-Airy-similarity network for night-time extended-target AO frame selection (PMC10781174) [U, snippets].

What this changes for Helia: the old review's hypothesis (band-limited Fourier power with noise floor) is partly supported (DCT energy ratio is a transform-domain energy measure that wins in poor seeing). Candidate set for the harness: gradient energy, Laplacian std (built), Helmli-Scherer mean, MFGS, DCT-ER, band-limited Fourier power with noise floor. Expect different winners by D/r0, so the metric (or a blend) should depend on estimated seeing. All of Popowicz's findings are on simulated Hinode truth with a particular turbulence generator [V], so re-check on Helia's own harness before fixing a default.

MFGS successors: MFLS (Kamlah et al.) and the Scharr-gradient variant are the only ones found [V/U as above].


6. Full-disc processing and metadata

6.1 Limb darkening

  • Empirical radial profile is the established route for H-alpha and Ca K full-disc images: convert to polar coordinates, take the median in the azimuthal direction (median rather than mean because plages and filaments are high contrast), and divide by the reconstructed 2D profile (Kodaikanal H-alpha, arXiv:1608.03390; arXiv:1110.3894 citing Denker et al. 1999) [U, snippets]. A polynomial fit to the average centre-to-limb variation (5th order) is the alternative (arXiv:0905.3055) [U].
  • Filter transmission is a separate term: for Lyot-filter He I (ChroTel) a symmetric limb-darkening function alone leaves filter-transmission artefacts, fitted with Zernike polynomials (arXiv:1812.04404) [U]. This maps onto the etalon "sweet spot" in the old review: model the instrument gradient and the physical CLV separately when possible.
  • Per-site signature: NSO/NISP H-alpha Carrington maps build radial mean profiles per GONG site and model them with Legendre polynomials because each site has a unique limb-darkening signature (arXiv:2608.13812, an unusually recent ID, verify) [U].
  • White light: Neckel & Labs (1994) fifth-order polynomials in mu at 30 continuum wavelengths (303 to 1099 nm), data within 7 arcsec of the limb excluded (mu about 0.12); later work gives about 0.4 per cent accuracy and possible scattered-light over-correction (e.g. arXiv:2310.05652) [U, secondary]. A related Neckel (2003/2005) Solar Physics series uses the same coefficients [U].

6.2 Compositing and colour

  • Prominence plus disc: SolHAT takes an optional prominence light set; amateur practice is separate exposures blended through a limb mask (existing review 8.2). No standard exists beyond practice [U]. Recommend linear-float compositing with a feathered limb mask, single tone map afterward.
  • Colour conventions: no formal convention found. Amateur practice is greyscale, pseudo-colour, partly inverted and inverted pseudo-colour variants per filter; H-alpha is commonly rendered red-orange; no source for a Ca K false colour (Stargazers Lounge 2023, S&T gallery) [U]. Keep the data mono float and apply colour as a declared LUT at export (as the old review said); JSol'Ex does "colourisation" as a named feature [V].

6.3 Mosaicking

Amateur full-disc H-alpha mosaics are typically 13 to 20 panes: AS!3 per pane, ImPPG sharpen, then MS-ICE, Autopano Giga, Affinity Photo or GIMP (example posts) [U, snippets]. I could not confirm that Microsoft ICE is discontinued. Siril 1.4 has astrometric alignment for mosaics with little overlap and WCS-SIP distortion correction, but for deep-sky plate solving (siril.org 1.4.0) [V]. JSol'Ex lists "mosaic" as a feature [V].

6.4 Derotation

  • Two different operations must not be conflated: field rotation of alt-az mounts (parallactic angle from time and site; SolHAT does it, AS offers alt-az derotation [V: AS 2013 beta note via the AS blog; SolHAT README]) and solar rotation (differential, needs P, B0, L0). The old review covers only the second.
  • Differential rotation coefficients: the form 14.713 - 2.396 sin^2(phi) - 1.787 sin^4(phi) degrees per day is cited as the accepted average set with signs negative on B and C (EPFL concept page) [U]; another paper quotes the Snodgrass & Ulrich (1990) magnitudes as 14.713, 2.293, 1.787 (arXiv:2204.09014) [U]. The middle coefficient is therefore disputed in my sources (2.396 or 2.293); check the paper before coding.
  • WinJUPOS derotation workflows found are planetary (Jupiter/Mars). No solar full-disc workflow found [U].

6.5 Metadata: SOLARNET recommendations

SOLARNET Metadata Recommendations for Solar Observations, Haugan & Fredvik, arXiv:2011.12139, v4 revised 2024-03-18 (document header "Version 2.2 - 18. March 2024"; the Read the Docs site additionally lists releases 3.0.0 on 2025-02-04, 3.1.0 on 2025-06-25 and 3.2.3 undated) (arXiv, docs) [V]. Points read from the text:

  • Every fully or partially compliant observation HDU must carry EXTNAME, SOLARNET, OBS_HDU, DATE-BEG (SOLARNET = 1 fully, 0.5 partially; OBS_HDU = 1) [V].
  • WCS: use HPLN-TAN and HPLT-TAN for solar coordinates (Thompson 2006), plus WAVE, UTC, STOKES where relevant. For plate scale derived from the apparent solar radius, report RSUN_REF (Thompson 2010 section 8) [V].
  • Ground-based observatories must report OBSGEO-X, -Y, -Z (GPS is an acceptable proxy for ITRF). DSUN_OBS, HGLN_OBS, HGLT_OBS are required for deep-space missions and optional for others [V].
  • Time: DATE-BEG mandatory, DATE-AVG optional; exposure in XPOSURE not EXPTIME; NSUMEXP and TEXPOSUR for summed frames [V].
  • Provenance: PRSTEPn (comma-separated list for inseparable steps, in order, e.g. FLATFIELDING, CALIBRATION, DISTORTION-CORRECTION) with optional PRPROCn, PRPVERn, PRMODEn, PRPARAn, PRREFn, PRLOGn, PRENVn and library keywords PRLIBna/PRVERna; the worked example uses PRSTEP1='MOMFBD' [V].
  • Also CAMERA, DETECTOR, FILTER, XPOSURE etc. as instrument keywords [V].

For Helia the mapping is direct: SER trailer timestamps give DATE-BEG/DATE-AVG/DATE-END; frame count and selection fraction give NSUMEXP; each pipeline stage (calibration, AP alignment, weighted stack, wavelet or RL sharpening, any ML step) is one PRSTEPn. Helioprojective WCS needs a limb fit (centre, radius in pixels) plus an assumed or ephemeris solar angular radius to set CDELT in arcsec; mark the WCS "approximate" unless an ephemeris solar radius and observer position are used. Computing the ephemeris itself is not required to write valid headers.


7. Calibration best practice for solar narrowband

  • Flats from displaced Sun images (Kuhn, Lin & Loranz 1991, PASP 103, 1097): record many images of the disc at different pointings so the disc falls on different pixels; solve gain tables iteratively; the Rome PSPT group reports relative-gain accuracy better than 1e-4 (OA Roma, Kanzelhöhe, arXiv:2111.03176) [U, snippets]. Toussaint, Harvey & Toussaint improved convergence with over-relaxation and fractional-pixel shifts; a 2020 Solar Physics KLL variant with small offsets and stitching reports better than 0.1 rms across the field (Springer) [U]. A 2025 paper extracts the flat from real-time observation data by correcting an average flat with KLL, because pointing and column fixed-pattern noise of CMOS detectors change during observing and leave residuals (arXiv:2503.15094) [U, snippet].
  • Column FPN: a two-way (vertical guided) filter estimates and removes column fixed-pattern noise from solar images (NJIT record) [U, snippet; year not confirmed].
  • Gain table model: corrected = gain x (raw + offset), gain and offset row, column or pixel dependent (camera vendor FFC documentation) [U]; sCMOS pixel-gain noise is multiplicative and scales with illumination [U, one microscopy source]. Hence a dark/bias frame matched to gain and exposure, plus a flat taken at the science focus.
  • Etalon fringes and Newton rings: shift with focus and tilt, so flats must be taken at science focus (existing review 2.3, still unverified beyond amateur sources); the displaced-Sun method naturally reproduces the fringe pattern because it is taken through the same optical path [U inference, mine].
  • I found no published per-pixel gain-table study specific to a high-frame-rate amateur or professional sCMOS solar camera, nor a dark-frame versus flat-table comparison for it.

Design implication: #3 should offer (a) master dark, (b) master flat by median of defocused or displaced-disc frames with the disc masked, (c) hot-pixel map, (d) column-FPN removal option, (e) later, a KLL-style gain-table solve from a displaced-disc SER.


8. Method matrix

Columns: maturity P production / R research / E experimental. Compute cost: low / med / high (per full capture on a consumer GPU). GPU fit: good / fair / poor. In Helia: built / planned #N / missing. Recommendation: MVP+ / next / later / skip.

MethodCategorySourceMaturityComputeGPU fitIn Helia now?Recommendation and reason
Dark, flat, hot-pixel calibrationCalibrationSolHAT, PSS, existing review 2.3 [V]Plowgoodplanned #3MVP+: fixed dust and fringes are sharpened by the warp otherwise
Column FPN removalCalibrationNJIT [U]Plowgoodmissingnext: cheap, CMOS-specific
Displaced-Sun gain table (KLL)CalibrationKuhn et al. 1991 [U]Pmedgoodmissinglater: best flat, needs a special capture
Limb-fit / centroid global alignmentRegistrationSiril Planet mode, SolHAT, ImPPG [V]PlowgoodmissingMVP+: full-disc frames lack a patch to correlate
Phase-correlation global shiftRegistrationexisting review 4.2Plowgoodbuiltkeep; return a confidence value
AP-grid multi-point NCCRegistrationPSS, Siril docs [V]Pmedgoodbuiltkeep
Two-pass AP refinement vs local stackRegistrationSiril 1.5 docs: default on, 5 per cent of frames clamped 8 to 32 [V]Pmedgoodplanned #4MVP+: removes reference bias
Robust shift-field smoothingRegistrationSiril docs, radius 2.5 grid steps [V]Plowgoodmissing (part of #4)MVP+: cheap, cuts measurement noise
Zero-mean NCC optionRegistrationSiril docs [V] (about doubles failures on low-contrast boxes)Plowgoodmissingnext: option, default off
AP rejection (brightness, contrast, structure, limb mask, timestamp burn-in)RegistrationSiril docs, #16 [V]Plowgoodplanned #16MVP+: avoids bad AP shifts at the limb
Automatic anchor and quality-parameter selectionUsabilityAS!4 4.0.1 [V]Plowgoodmissingnext: key AS!4 convenience
Gradient and Laplacian frame/AP qualityQualityPSS, Siril [V]Plowgoodbuiltkeep
Helmli-Scherer mean, MFGS, DCT energy ratioQualityPopowicz et al. 2017 [V]Plowgoodmissingnext: choose by D/r0 in the harness
Noise-floor band-limited Fourier powerQualityimaging-methods.md 3.2 (hypothesis)Rmedgood (FFT)missingnext: test in the harness; evaluate wgsl-fft
Learned frame-quality scorerQuality/MLJarolim 2020 [U], Gao 2023 [U]Rlowgoodplanned #8later: safe use of ML, no pixel output
Quality-weighted mean stack, soft cutoffStackingSiril, PSS [V]Plowgoodbuilt (weighted mean)keep
Sigma clipStackingexisting review 5.1Plowgoodbuiltkeep, off by default
Drizzle / splat accumulatorStackingFruchter & Hook 2002; SolHAT 1x to 3x; Siril 1x to 3x [V]Pmedgoodplanned #6later: only when sampling q < 1; design as splat now
Dense warp, single resampleStackingSiril warp engine (experimental) [V]Emedgoodmissinglater: removes patch seams
TV-L1 or learned optical flowRegistrationnone solar foundEhighfairmissingskip: test in harness first
B-spline non-rigid registrationRegistrationZhu & Milanfar [U]Rhighpoormissingskip
Alt-az field rotation (parallactic)GeometrySolHAT, AS [V]Plowgoodmissingnext: needs SER timestamps and site
Solar differential derotationGeometryexisting review 8.5; coefficients disputed [U]Plowgoodmissinglater: only for long captures and mosaics
Unsharp mask, tone curveSharpeningImPPG [V]Plowgoodplanned #5MVP+
À trous wavelets with noise thresholdSharpeningStarck et al.; waveSharp 3.0 [V for waveSharp]Plowgoodplanned #5MVP+
Non-blind Richardson-Lucy, Gaussian + AirySharpeningImPPG 2.1.0 [V]Pmedgoodplanned #5MVP+: live preview
TV or damped RL, WienerSharpeningDey et al. 2006 (existing)Pmedgoodmissinglater
Limb-aware masked deconvolutionSharpeningexisting review 7.4PlowgoodmissingMVP+ with RL: avoids limb ringing
Radial limb-darkening flattening (azimuthal median)Full discKodaikanal H-alpha [U]PlowgoodmissingMVP+: needed by sharpening and compositing
Neckel & Labs CLV polynomialsFull disc[U]Plowgoodmissinglater: white light only
Prominence and disc HDR compositingFull discSolHAT, JSol'Ex [V]Plowgoodmissingnext
Colourisation LUT at exportFull discJSol'Ex [V]Plowgoodmissingnext
Mosaic panel registration and blendFull discamateur practice [U]Pmedfairmissinglater
SOLARNET FITS header, PRSTEPn provenance, HPLN-TAN WCSMetadataHaugan & Fredvik, v2.2 [V]Plown/amissingMVP+: cheap, costly to retrofit
Time-lapse and animationOutputJSol'Ex [V]Plowgoodmissinglater
Per-sub-field MFBD, Zernike PSFsRestorationtorchmfbd [V], Löfdahl & Hillberg 2022 [V]Rhighfair (small FFTs)planned #7later: validate vs torchmfbd; include statistical high-order tails and marginal term
Speckle masking, Knox-ThompsonRestorationKISIP, DKIST VBI [V/U]P (professional)highfairmissingskip: dominated by MFBD for Helia's inputs
Phase diversityRestorationexisting reviewP (professional)highfairmissingskip: needs a second channel
Unsupervised/unrolled MFBD networkMLAsensio Ramos et al. 2023 [V]Elow at inferencegoodmissinglater: optional PSF estimator, labelled
Burst-to-image translation networkMLSchirninger et al. 2025 [V]Elowgoodmissingskip: perceptual training target
Neural-field blind deconvolutionMLSchirninger et al. 2026 [V]Ehigh (per scene)fairmissingskip now; watch
Diffusion posterior samplingMLSpagnoletti 2024 [V]Ehighfairmissingskip: strong prior, not solar
Fourier ring correlation for resolutionDiagnosticsexisting review 12.4 [U]Plowgoodmissingnext: harness metric
GPU FFTInfrastructurewgsl-fft 0.5.1, MIT, Stockham radix-4/2 plus Bluestein, batched, released 2026-09-03; 2D and f32 precision not stated [V]En/agoodmissingnext: spike before writing own FFT

9. Proposed milestones

M1 is the built MVP (SER/AVI/FITS input, bilinear and MHC debayer, frame and per-AP quality, phase correlation, AP grid, weighted mean, optional sigma clip).

M2: trustworthy stacking inputs and outputs. #3 calibration (dark, flat at science focus, hot pixels, column FPN option); #16 AP masking (limb mask, burn-in timestamp, brightness/contrast/structure rejection); #4 two-pass refinement plus shift-field smoothing; limb-fit global alignment with confidence; FITS output with SOLARNET minimum (EXTNAME, SOLARNET, OBS_HDU, DATE-BEG, XPOSURE, NSUMEXP, OBSGEO-X/Y/Z, PRSTEPn). Why first: every later step is judged on this base; flats and AP masking fix the largest known solar failure modes; the metadata is cheap now and painful to retrofit.

M3: look and measure. #5 sharpening (unsharp, à trous with noise threshold, non-blind RL with Gaussian + Airy and a limb-aware mask, live preview); radial limb-darkening flattening; synthetic-harness metric bake-off (gradient, Laplacian, Helmli-Scherer, MFGS, DCT-ER, Fourier band power) and quality-versus-rank diagnostics; FRC; spike wgsl-fft. Why: gives users a complete workflow and gives designers the evidence for the quality default.

M4: solar presentation. Prominence/disc HDR compositing, colourisation LUT export, helioprojective WCS from the limb fit, alt-az field-rotation correction using SER timestamps, autocrop, auto anchor/AP parameters, displaced-Sun gain-table flats. Why: these are the features JSol'Ex and SolHAT users already have and are missing from every issue.

M5: geometry and sampling. #6 drizzle on a splat accumulator (enabled when q < 1), dense warp single-resample engine, panel mosaicking, solar derotation, time-lapse. Why: need M2 diagnostics to show benefit and M4 geometry to be correct.

M6: research track. #7 per-sub-field MFBD validated against torchmfbd on harness bursts, with Löfdahl & Hillberg statistical tails and the marginal term; #8 learned quality scorer; optional unsupervised PSF-estimator plug-in; all labelled experimental. Why: highest cost, least certain gain for amateur apertures; the harness and FFT groundwork come from M3.


10. "Keep it current" checklist (re-check at each release)

  1. Siril: siril.org/download for a 1.5 release or candidate, and the multipoint documentation page (the feature is documented but unreleased as of 2026-10-18).
  2. AutoStakkert!: autostakkert.com download page and change-notes page for 4.0.x or 5.x, especially solar, flats or GPU entries.
  3. PlanetarySystemStacker, ImPPG, waveSharp (Codeberg), LuckyStackWorker, JSol'Ex/astro4j, SolHAT: GitHub/Codeberg releases pages (years are not shown on some; check the tags).
  4. torchmfbd repo and A&A listing; arXiv astro-ph.IM and astro-ph.SR for "multi-frame blind deconvolution", "solar speckle", "burst" (search each of Asensio Ramos, Löfdahl, Schirninger, Wöger, Jarolim as authors).
  5. Metrics: Solar Physics and A&A for "image quality" with Denker, Popowicz, Kamlah, Deng as authors; arXiv 1709.09458 follow-ups.
  6. Professional pipelines: NSO DKIST data-centre docs (VBI page, SPEC-0214, dkist-header-validator), KIS VTF news, AIP GREGOR/sTools pages, SSTRED/REDUX repository.
  7. Metadata: SOLARNET Read the Docs release list and arXiv:2011.12139 version; FITS standard releases; astropy/sunpy WCS changes affecting HPLN-TAN.
  8. Infrastructure: crates.io pages for wgsl-fft, gpu-fft, wgpu, ort, burn, tract.
  9. ML: arXiv for "hallucination" or "uncertainty" in solar restoration; any paper using injected-feature tests.
  10. Conferences: SPIE Astronomical Telescopes + Instrumentation, SPD/AAS, SOLARNET and DKIST data-user workshops.

11. Corrections to imaging-methods.md

  1. Section 3.2 and preamble: "no peer-reviewed head-to-head comparison of frame-quality metrics on solar data" is wrong. Popowicz et al. (2017, arXiv:1709.09458) compared 36 methods against simulated-truth Hinode images and recommend Helmli-Scherer mean, MFGS (D/r0 < 4) and DCT energy ratio; Kamlah et al. (2021, arXiv:2011.04346) related MFGS, MFLS, Helmli-Scherer, rms contrast and differential image motion to r0 on HiFI data [V].
  2. Sections 4.3 and 10: Siril multipoint is listed as a Siril feature. The documentation states it "was introduced in Siril 1.5". siril.org lists 1.4.4 (2026-06-17) as latest stable and no 1.5 build or RC. The row should read "Siril 1.5 (documented, unreleased)". The detailed defaults the old review quoted (2.25 x half-width, search 14, smoothing 2.5, best 5 per cent reference, scales 1/1.5/2/3, patch and experimental warp engines) all match the current docs [V]. New detail: default demosaic LMMSE (earlier default RCD), mpp/register_mpp/stack_mpp commands.
  3. Section 3.1, MFGS: "original definition and authorship not verified". MFGS is Deng et al. (Solar Physics 2015; arXiv:1701.05300, authors Deng, Zhang, Wang, Ji, Wang, Liu, Xiang, Jin, Cao): 3 x 3 median filter, gradient comparison, 0 to 1 score, linear with granular rms contrast, tested on NVST and Hinode/SOT [V]. The old table row's "gradient similarity" is right; later variants use a Scharr gradient magnitude [U].
  4. Sections 4.3, 10: AutoStakkert!3 only. AutoStakkert!4 exists: 4.0.1 (2023-11-11) to 4.0.13 (2025-02-19), beta status for 4.0.13, with automated surface-anchor placement and automated quality estimation [V]. "Closed source, freeware, Windows [M]" stays [U]; it is Delphi-built Windows software [V from change notes].
  5. Section 10: Registax. Development ended: final version 6.1.0.8 (May 2011), stated on the official page. Wavelet sharpening continues in waveSharp (3.0 on 2025-12-09) [V].
  6. Section 10: missing tools. Add SolHAT (Rust, MIT, closest prior art), JSol'Ex (Apache-2.0, solar output features), LuckyStackWorker (MIT), waveSharp [V]. The claim that "only ImPPG uses a GPU" remains true for the tools checked; SolHAT lists no GPU use [V].
  7. Section 6.2, torchmfbd: bibliography. It is Asensio Ramos, Díaz Baso, Kuckein, Esteban Pozuelo & Löfdahl (2025), A&A 703, A269, MIT licence; the old text says only "arXiv:2505.10639". The "Marginal MFBD" reference is Asensio Ramos (2026), single author, submitted 2026-05-12 [V]. The "MFBD with uncorrected high-order modes (2022, authors not verified)" reference is Löfdahl & Hillberg (2022), A&A 668, A129 [V].
  8. Section 6.2 and 11.2, wgpu FFT: "did not find a mature wgpu FFT crate". wgsl-fft 0.5.1 (MIT, Stockham radix-4/2, Bluestein, batched, released 2026-09-03) exists; gpu-fft (CubeCL, radix-2) also exists. Maturity, 2D transforms and f32 accuracy are unverified [V for existence, U for quality].
  9. Section 9.1, Schirninger et al. (2026): authors and arXiv ID confirmed; the abstract says synthetic MURaM, GREGOR and DKIST tests and that spatially varying PSFs are future work [V]. New 2025 and 2023 items are missing from section 9 (table in section 2 above): Schirninger et al. (2025), Asensio Ramos et al. (2023), Asensio Ramos (2024).
  10. Section 8.5, differential rotation coefficients [M, confirm coefficients]. Sources disagree on the middle coefficient: 2.396 (accepted-average set with negative signs) versus 2.293 (quoted for Snodgrass & Ulrich 1990) [U]. The 0.17 arcsec per minute arithmetic at the equator and disc centre is reproduced by 14.713 deg/day x 960 arcsec [V, my arithmetic].
  11. Section 1.2, Noll 87 per cent. Supported by the Noll residual coefficients Delta_1 = 1.0299 and Delta_3 = 0.134: (1.0299 - 0.134) / 1.0299 = 0.87 (coefficients from a secondary paper, arXiv:1901.08438) [U for the primary table].
  12. Section 1.3, Fried 1978 constants. P = 5.6 exp[-0.1557 (D/r0)^2] is quoted in arXiv:0906.3041, with worked values about 1 in 9 at D/r0 = 5, 1 in 50 at 6, 1 in 3800 at 8; still only one secondary source, check Fried [U]. Note that the old text's "D/r0 = 6 gives rare good frames" is consistent with 1 in 50.
  13. Section 8.3, Neckel & Labs. Primary details (30 wavelengths, 303 to 1099 nm, fifth-order polynomials in mu, 7 arcsec limb exclusion, about 0.4 per cent accuracy with possible scattered-light over-correction) corroborated by several secondary papers; coefficient tables still not retrieved [U].
  14. Section 7.3, ImPPG 2.1.0 "dated 2025-11-18". The releases page shows "18 Nov" without a year. Keep the date but tag [U] on the year [V for the version].
  15. Sections 2.3 and 8.6: Kuhn-Lin-Loranz displaced-Sun flats, SOLARNET and derotation by alt-az field rotation are absent. Added in sections 6 and 7 above. The old review's derotation section covers only solar rotation; alt-az field rotation (SolHAT, AS) is a separate correction and a practical gap.
  16. Section 8.6, "Microsoft ICE discontinued [M, unverified]". Still unverified; amateur posts show MS-ICE in current use for H-alpha mosaics [U].
  17. Section 9.3, ONNX Runtime WebGPU provider, ort, tract, burn [M]. Not re-checked; remain [U].
  18. Section 1.5, Denker et al. (2018) "50 Hz" and section 6.1 KISIP references were not re-checked this session; the DKIST VBI burst (80 frames at 30 Hz) is new context [V].

12. Reference list for this refresh

Verified (opened this session):

  • Asensio Ramos et al. (2025), torchmfbd, A&A 703, A269. https://arxiv.org/abs/2505.10639 ; https://github.com/aasensio/torchmfbd
  • Asensio Ramos (2026), Marginal MOMFBD. https://arxiv.org/abs/2605.11980
  • Löfdahl & Hillberg (2022), MFBD with statistical inclusion of uncorrected high-order modes, A&A 668, A129. https://arxiv.org/abs/2205.13650
  • Asensio Ramos, Esteban Pozuelo, Kuckein (2023), Accelerating MFBD via deep learning, Sol. Phys. https://arxiv.org/abs/2306.12078
  • Asensio Ramos (2024), spatially variant convolution emulator, A&A 688, A88. https://arxiv.org/abs/2405.09864
  • Schirninger et al. (2025), Deep learning image burst stacking. https://arxiv.org/abs/2506.04781
  • Schirninger et al. (2026), Neural blind deconvolution. https://arxiv.org/abs/2603.05033
  • Spagnoletti et al. (2024), diffusion deconvolution. https://arxiv.org/abs/2411.19158
  • Popowicz et al. (2017), Review of image quality measures for solar imaging. https://arxiv.org/abs/1709.09458
  • Kamlah et al. (2021), Wavelength dependence of image quality metrics. https://arxiv.org/abs/2011.04346
  • Deng et al., Objective image-quality assessment by MFGS. https://arxiv.org/abs/1701.05300 ; https://digitalcommons.njit.edu/fac_pubs/7006
  • Haugan & Fredvik, SOLARNET Metadata Recommendations (v4, 2024-03-18). https://arxiv.org/abs/2011.12139 ; https://solarnet-metadata.readthedocs.io/en/stable/
  • NSO VBI page. https://nso.atlassian.net/wiki/spaces/PDOI/pages/3534716929 ; Leibniz-KIS VTF pipeline. https://www.leibniz-kis.de/en/news/article/vtf-data-pipeline-prototype/
  • Siril: https://siril.org/ ; https://siril.org/download/ ; https://siril.org/download/2025-12-05-siril-1-4-0/ ; https://siril.readthedocs.io/en/latest/preprocessing/multipoint.html
  • AutoStakkert!: https://www.autostakkert.com/wp/download/ ; https://www.autostakkert.com/wp/change-notes/
  • PSS releases https://github.com/Rolf-Hempel/PlanetarySystemStacker/releases ; ImPPG releases https://github.com/GreatAttractor/imppg/releases ; Registax/waveSharp https://www.astronomie.be/registax ; LuckyStackWorker article https://scopetrader.com/luckystackworker-automates-solar-image-processing/
  • SolHAT https://github.com/kmgill/solhat ; solar_ha_processing https://github.com/kmgill/solar_ha_processing ; JSol'Ex https://github.com/melix/astro4j ; https://melix.github.io/astro4j/latest/
  • wgsl-fft https://docs.rs/wgsl-fft

Unverified (search snippets or secondary): all items tagged [U] above, notably Zheng 2018 and Xuan 2020 CUDA speckle, Beard et al. 2020 SPIE abstract body, sTools and HiFI+ details, DKIST SPEC-0214 contents, Jarolim 2020, Gao 2023, TMT/DATUM/DeTurb, Chae & Sakurai 2008, Zhu & Milanfar, Kuhn et al. 1991 and follow-ups, Kodaikanal/ChroTel/NISP limb-darkening papers, Snodgrass & Ulrich coefficients, JSol'Ex/INTI details beyond the feature list, amateur colour conventions.

Roadmap

This is the plan, in the order the project intends to build it. It follows the milestones proposed in the October 2026 state of the art review, and each item links to its issue, where the discussion and the current status live. Nothing below is available until its issue is closed and this page says so; dates are deliberately not given. Issue titles are as of 18 October 2026.

The overall plan and every decision so far are in issue #1. This documentation is tracked in issue #14.

M1: the stacking pipeline (built)

SER, AVI and FITS input (and MP4, MOV, MKV and WebM through the optional ffmpeg), bilinear and Malvar-He-Cutler demosaicing, frame and per-point quality, phase-correlation global alignment, an alignment point grid, quality-weighted stacking with optional sigma clipping, TIFF and FITS output, the command line, the desktop app and the API. Everything in the User guide describes this milestone. Since the first release it also has guided steps, work notices and colour presets for the solar lines (the colour part of #23; see Colourising).

M2: trustworthy stacking inputs and outputs

Fixes the largest known solar failure modes and makes the files carry the metadata they should.

IssueTitle
#3Dark, flat and hot-pixel calibration (built)
#4Two-pass alignment-point refinement and shift-field smoothing
#16Real-data bugs: APs on timestamp and sky, quality vs rank mismatch (built)
#18Limb-fit global alignment with confidence (built)
#19SOLARNET-compliant FITS output (built)

M3: look and measure

A complete workflow, and the evidence for choosing the default quality metric.

IssueTitle
#5Sharpening and adjustments: unsharp mask, a-trous wavelets, Richardson-Lucy, levels, curves, crop, rotate, resize (built: see Sharpening and adjustments)
#20Radial limb-darkening flattening
#21Quality-metric bake-off and default by seeing
#22Spike: wgsl-fft for GPU phase correlation

M4: solar presentation

Features that users of other solar tools already expect.

IssueTitle
#23Prominence/disc HDR compositing and colour LUT export (the colour LUT part is built: see Colourising; HDR compositing is not)
#24Alt-az field-rotation correction from SER timestamps
#25Autocrop and automatic AP parameters

M5: geometry and sampling

IssueTitle
#6Drizzle via the splat accumulator
#26Mosaics, solar derotation and time-lapse

M6: research track

All experimental, and labelled that way when it arrives.

IssueTitle
#7Experimental MFBD restoration
#8ML frame-quality scorer (ONNX Runtime; Coral experimental)

Licence and notices

Copyright (C) 2026 Laurence Horrocks-Barlow.

Helia is free software under the GNU General Public Licence, version 3 or (at your option) any later version, with the additional terms below. The Helia logo is not part of that licence. Back to the main site: 12hobbies.studio.

Additional terms under section 7 of the GNU General Public License version 3

Helia is free software under the GNU General Public License, version 3 or (at your option) any later version. The licence text is in LICENSE and is unmodified.

As permitted by section 7(b) and 7(c) of that licence, the following additional terms apply to Helia and to every work based on it:

  1. The author attribution "Copyright (C) 2026 Laurence Horrocks-Barlow" and the dedication of Helia, which forms part of that author attribution, must be preserved, unaltered, in the source code notices and in the Appropriate Legal Notices displayed by the program (its About page and its version output).

  2. Modified versions must be marked as changed from the original, and must not be presented as the original work or as made or endorsed by Laurence Horrocks-Barlow.

The author attribution

Copyright (C) 2026 Laurence Horrocks-Barlow

The dedication

Helia is dedicated to my daughter, the light of my life. Her middle name is Helia, because she has an energetic spark. She has not stayed still since the day she was born, which suits software whose whole job is catching the sharp moments in something that never stops moving.

What this means in practice

  • Keep the SPDX header and the line that points here at the top of every source file. You may add your own copyright line for work you contribute; you may not remove or change the line above.
  • In a modified version, the About page and the output of --version must still show the copyright line, the dedication, the no-warranty notice, the licence and a notice of these additional terms. Add your own notices beside them, not in place of them.
  • If you distribute a modified version, mark it as changed from the original (for example in the About page, the version output and the release notes). Do not present it as the original work, and do not say or imply that Laurence Horrocks-Barlow made or endorses it.
  • The Helia logo is not covered by the GPL and is all rights reserved, see LOGO-LICENCE.md. A modified version or fork must replace it.
  • Contributions are accepted under GPL-3.0-or-later with these additional terms.

These are additional terms permitted by section 7(b) and 7(c) of the GPL. They are not further restrictions within the meaning of the licence.

Helia logo licence

Copyright (C) 2026 Laurence Horrocks-Barlow. All rights reserved.

The Helia source code is free software under the GPL-3.0-or-later with the additional terms in ADDITIONAL-TERMS.md. The Helia logo is not part of that. The logo, and every image derived from it, is not covered by the GPL. In the REUSE sense it is LicenseRef-Helia-Logo (the text is in LICENSES/LicenseRef-Helia-Logo.txt).

What is covered

Everything under assets/branding/, and the copies of it elsewhere in the repository :

FileWhat it is
helia-logo.pngThe original artwork, unmodified.
helia-logo.svgThe vector artwork, unmodified.
helia-logo-transparent.pngThe vector logo without its background.
icons/helia-<size>.pngSquare icons, 16 to 1024 pixels.
helia.ico, helia.icnsThe Windows and macOS icons.

cargo xtask branding rebuilds everything except the two originals from helia-logo.svg. The originals are identified by their SHA-256:

33accd205d49891ee85f39f7c401ed0e76fbfcf9ef2777cc8a7985f7437b2812  helia-logo.png
cc3f7be591846a5e882144ec816ee575783bc18be82fb70f6360ff738982c721  helia-logo.svg

What you may do

  • Use the logo, unmodified, in unmodified builds of Helia, such as the releases published by the author.
  • Use the unmodified logo to refer to the Helia project: an article, a review, a link to the project page.

Anything else needs the author's written permission.

Forks and modified versions

If you distribute a modified version of Helia, or a fork, you must replace the logo and every image derived from it with your own, so that your version is not mistaken for the original. This goes with section 2 of ADDITIONAL-TERMS.md (GPLv3 section 7(c)): a modified version must not be presented as the original work or as made or endorsed by Laurence Horrocks-Barlow. The places to change are the README, the book, the About page and the window icon in crates/helia-gui, the Windows icon in crates/helia-gui/build.rs and the Linux icons and desktop file in packaging/.