Stabilise, clean and assemble solar timelapses.
Takes a timelapse video of the Sun and gives back a stable one. Any source will do — smart telescope, DSLR sequence, a phone behind a filter — as long as the solar disc is visible and roughly the same size throughout.
Useful when the assembled video is all you have: frame export was not enabled, or the originals are long gone. The Sun drifts around the frame, auto-exposure flickers between shots, and the occasional frame is black or torn.
Left: the source video. Right: after processing.
There are excellent tools around this space, and they solve adjacent problems: PIPP for planetary pre-processing, Siril for registration and stacking, PSS for lucky-imaging stacks. They are worth using for what they are built for.
This one covers a different chain, end to end and in one pass: video in, stabilised video out — extract, centre, screen, level, re-encode — and optionally assemble a finished piece with title cards, still photographs, a phase poster and music.
The point is not that it does more. It is that the whole chain is scriptable, so changing one decision re-derives everything downstream: lengthen a transition and the soundtrack re-fits itself, switch to a vertical crop and the cards and posters follow.
pip install -r requirements.txt
# 1. stabilise: extract frames, centre the disc, screen and level
python -m solar_timelapse.cli eclipse.mp4 --size 1080x1920 --diagnostic
# 2. optional: assemble a finished piece
python -m solar_timelapse.montage \
--frames frames/processed --audio track.mp3 --photos photos/ \
--date "12 · 08 · 2026" --headline "PARTIAL SOLAR ECLIPSE" --magnitude "94%" \
--place "MATALASCAÑAS · SPAIN" --author "YOUR NAME" \
--clock --clock-start 19:41:36 --match-cut --mosaicFrames are kept on disk in frames/raw/ (untouched) and frames/processed/ (centred and
levelled), so you can reprocess them in Siril, PixInsight or anything else. Starting from
frames/processed/ saves you the registration step: they are already aligned to sub-pixel.
Already have individual frames rather than a video? Encode them first, at a quality high enough that the round trip costs nothing visible:
ffmpeg -framerate 30 -i frame_%05d.png -c:v libx264 -crf 10 -pix_fmt yuv420p source.mp4The brightness centroid does not work during a partial eclipse. As the crescent thins, the centroid slides towards it and the disc wobbles more than in the original footage.
Instead the solar limb is fitted, a circle whose radius stays constant across the whole sequence. For each point on the lit contour, the gradient normal votes for a centre at distance R. Solar limb points all vote for the same spot; lunar limb points scatter.
These cost real time to find, and they are the reason this repository might save you some.
The Moon's apparent radius is almost identical to the Sun's. That is why eclipses can be total — and it means the lunar limb is another arc of radius ≈R. Selecting fit inliers by distance alone lets those points in and biases the centre. What separates them is orientation: on the solar limb the gradient normal points at the centre; on the lunar limb it does not.
Smoothing the trajectory makes it worse. It looks like the obvious optimisation. It is not: the five-to-seven pixel wobble between shots is real mount motion, and the detections were tracking it correctly. Smoothing the correction stops compensating for it and it comes back as visible jitter.
"Cannot locate the Sun" is not the same as "the frame is broken." At sunset the disc dissolves into horizon haze and becomes unmeasurable, but those images are fine — and they are the hardest ones to capture. Conflating the two destroys exactly the end of the event.
A relative brightness threshold normalises itself away. Measuring the disc level with a threshold relative to the frame's own peak saw only a 1.3× swing where the real one was 2.6×, so levelling corrected less than half the flicker. The culprit is clipping at 255: once the disc saturates it loses limb darkening and stops scaling with exposure. The fix is to define the measurement region geometrically, from the detected disc.
What you see moving may not be the Sun. Veil and vignetting are fixed to the camera.
Pin the Sun and they slide by several pixels per shot. Against a black sky that is
invisible; once the Sun drops into haze the background lights up and the whole scene appears
to sway. --subtract-background removes it, at the cost of the sunset glow.
ffmpeg's fade filters chain. A fade=t=out leaves the entire rest of the stream
black, so chaining several in/out pairs blacks out everything after the first fade-out.
Segment fades are applied to the frames instead.
Two verification methods that are invalid here, in case you reach for them:
- Phase correlation between consecutive frames measures apparent content motion. The crescent's shape keeps changing, so it carries the same flaw as the centroid.
- The minimum enclosing circle stops matching the limb as soon as the crescent is thin, returning a radius close to R with a displaced centre — so it does not even self-validate.
- Extract every frame to PNG, streamed through ffmpeg pipes.
- Detect the solar centre by limb fitting, seeded with the previous frame's position (the accumulator can show two nearly tied peaks, and without a tie-breaker noise picks one, alternating frame to frame — which reads as jitter).
- Screen in two passes: statistics first (black frames, area drops), then temporal novelty on the already centred and levelled frames, against a local baseline because the real rate of change is not constant.
- Track: measured positions as-is, gaps interpolated, suspicious fits overridden only when they deviate and fit worse than their neighbourhood.
- Level exposure and colour per channel towards a rolling median, which flattens the auto-exposure sawtooth while leaving the slow sunset trend intact.
- Render with sub-pixel translation, square or vertical crop, and edge extension rather than black fill.
--diagnostic renders a video with the fitted circle drawn on every frame. Given how many
plausible-looking measurements turned out to be wrong, this is the check worth trusting.
montage.py adds an opening card, the timelapse with an optional real-time clock, a
cross-dissolved still sequence, the phase poster and credits, with music trimmed to fit.
| Phase mosaic | Cover poster |
|---|---|
![]() |
![]() |
Two touches worth knowing about:
--match-cutbrings the first photograph in with its Sun exactly where the timelapse left it, and the same size, then opens out to its natural framing. The same Sun seen by two instruments.--mosaiccloses with the phase poster, assembling one phase at a time, then dissolving into the credits.
Music trimming: pass --audio-end to say where in the track the music should stop. By
default it ends where the track ends, so the only splice is at the start, hidden under the
opening fade, and the finish resolves on its own.
- Python 3.10 or newer.
- Fonts are looked up per platform; override with
SOLAR_TIMELAPSE_FONT. - ffmpeg comes bundled via
imageio-ffmpeg; no system install needed. --coverembeds an image as MP4 cover art, without re-encoding. Support varies by player and platform; some build their own preview from the first frames instead.
python tests/test_detect.py
python tests/test_pipeline.pyThe detector is validated against synthetic discs and crescents with a known centre, including a Moon of exactly the same radius walked across the disc. The pipeline tests cover trajectory, screening, levelling, recentring and frame-count arithmetic.
Built for the partial solar eclipse of 12 August 2026, shot from Matalascañas (Huelva, Spain) with a ZWO Seestar S50 and a Nikon D90.
MIT licensed.



