Coming soon. Code and documentation are being prepared for public release. Star/watch this repo to get notified.
A browser-controlled pipeline for producing annotated tracking animations of any solar-system object from plate-solved astrophotography frames. Point it at calibrated XISF data from PixInsight and a JPL Horizons ephemeris file — it handles WCS-based tracking, photometric normalization, optional AI denoising (NoiseXTerminator), zoom animation, and broadcast-quality H.264 encoding with a real-time data overlay.
First built for the Artemis II / Orion MPCV "Integrity" observation campaign at the Southern Carina Observatory (Villarrica, Chile). Works with any target supported by JPL Horizons: spacecraft, comets, asteroids, planets, moons.
The system takes calibrated, plate-solved XISF frames from PixInsight's WBPP pipeline, computes per-frame spacecraft pixel positions via WCS + ephemeris, and renders a tracked MP4 with a real-time data overlay showing RA/Dec, range, phase angle, sky motion, and more.
- WCS-based spacecraft tracking — converts JPL Horizons RA/Dec to pixel coordinates using each frame's astrometric solution (PixInsight ImageSolver)
- Edge clamping — when the spacecraft approaches the source-image boundary, the crop window pins to the edge and the spacecraft drifts naturally out of frame
- Track smoothing — configurable rolling-average window to suppress single-frame WCS jitter
- Ephemeris extrapolation — linearly extrapolates past the Horizons file boundary so the track doesn't freeze if the ephemeris is a few minutes short
- Photometric normalization — robust median + MAD matching per frame against a reference, removing atmospheric transparency drift and background pedestal changes. Eliminates the "jumpy brightness" problem in time-series animations
- Auto-locked stretch — normalization automatically pins the display stretch to the reference frame's percentile bounds
- Temporal smoothing — optional rolling-median over the normalization coefficients to damp single-frame anomalies (satellite trails, meteors)
- Headless PixInsight batch denoising — runs NXT on pre-cropped tracking windows via PJSR (
PixInsight.exe -n --automation-mode -r=... --force-exit) - Tile stitching — groups N crops into composite images with padding to amortize NXT's per-call TensorFlow overhead (~4s/call down to ~0.3s/tile effective)
- Chunked execution — restarts PixInsight every N composites to avoid CUDA state degradation that causes hangs after ~30 sequential calls
- Stall watchdog — kills hung PI processes after configurable timeout; the next chunk picks up where it left off
- Progress tracking — PJSR writes sidecar files that Python polls for live UI progress bars
- Dynamic source-window sizing — animate between zoom levels across the timeline (e.g., start wide at 0.5x showing more sky, zoom to 1:1 over 50 frames)
- Easing functions — linear, ease-in (cubic), ease-out (cubic), ease-in-out (smoothstep)
- NXT-compatible — precrop tiles are sized to the maximum zoom-out so denoised tiles work at any zoom level without re-processing
- Data panel — real-time ephemeris display: UTC time, ICRF RA/Dec, range (km + AU), range rate, solar elongation, phase angle, sky motion, frame counter, progress bar
- Configurable panel placement — 9 anchor positions (corners, edges, center) with pixel offsets, adjustable width and padding
- Pointer types — crosshair reticle, arrow (fixed angle or auto-along-motion), or none
- Trajectory line — historical spacecraft path drawn as a polyline in the current crop's coordinate space, with configurable color, width, length (full path or last N frames), and end gap
- Arrow/trajectory gap — configurable pixel offset so indicators don't obscure the spacecraft
- Off-screen clamping — when the spacecraft exits the visible crop, the arrow snaps to the nearest frame edge and points toward the actual position
- Watermark/credit — configurable text, size, and color at the bottom of each frame
- ffmpeg encoding — pipes raw BGR frames to ffmpeg via stdin for high-quality H.264/HEVC output (replaces OpenCV's soft mp4v codec)
- Encoder options — libx264 (CRF, highest quality), h264_nvenc (GPU-accelerated), hevc_nvenc (HEVC, smaller files)
- Output resolution — decoupled from crop size; text and overlays are drawn natively at the higher resolution for crisp rendering
- Parallel rendering — ProcessPoolExecutor distributes per-frame work across CPU cores; the main thread collects results in order for the sequential video writer
- Crop-first optimization — stretching runs on the ~360K-pixel tracking window instead of the full 61M-pixel source frame (~170x reduction)
- Tab-based sidebar — Source, Frame, Image, Annotation, Output (persisted via localStorage)
- Live preview — single-frame render to PNG for instant parameter tuning
- Dark astronomical theme — designed for observatory use; low-glare deep-blue palette
XISF frames (PixInsight WBPP)
|
v
[Scan] ── load WCS + timestamps from each file
|
v
[Ephemeris] ── parse JPL Horizons .txt, interpolate/extrapolate to frame midpoints
|
v
[Track] ── compute_craft_pixel(WCS, RA/Dec) per frame, smooth, fill gaps
|
v
[Normalize] ── (optional) compute (a, b) per frame to match reference median/MAD
|
v
[NXT Denoise] ── (optional) precrop -> stitch composites -> PixInsight headless -> unstitch
|
v
[Render] ── per frame (parallel):
| load XISF (or precropped tile)
| apply normalization
| crop to tracking window (variable size via zoom keyframes)
| stretch to 8-bit display
| resize to output resolution
| draw annotations (panel, pointer, trajectory, watermark)
|
v
[Encode] ── pipe BGR frames to ffmpeg (libx264/NVENC) -> MP4
| File | Purpose |
|---|---|
artemis_annotator_v2.py |
Core pipeline: ephemeris parsing, WCS tracking, display stretch, frame annotation renderer |
server.py |
FastAPI web server, global state, all API endpoints, render orchestration |
render_worker.py |
Isolated frame renderer for ProcessPoolExecutor workers (3 render paths: precropped, crop-first, legacy) |
normalize_frames.py |
Photometric normalization via robust statistics (median/MAD) with parallel batch computation |
nxt_pipeline.py |
NoiseXTerminator preprocessing: parallel precrop + headless PixInsight invocation with chunking and stall detection |
stitch_pipeline.py |
Tile stitching/unstitching for NXT composite processing with JSON manifests |
video_encoder.py |
ffmpeg-backed video writer (libx264, h264_nvenc, hevc_nvenc) with cv2.VideoWriter-compatible API |
zoom_keyframes.py |
Keyframe interpolation with easing functions for dynamic zoom animation |
tools/pi_nxt_batch.js |
PixInsight PJSR script for headless batch NoiseXTerminator processing |
index.html |
Single-page browser UI with dark theme, tab navigation, and live preview |
- Python 3.10+
- ffmpeg on PATH (for H.264/HEVC encoding; falls back to OpenCV mp4v if missing)
- PixInsight with NoiseXTerminator (optional, for NXT denoising only)
- NVIDIA GPU + CUDA (optional, for NXT GPU acceleration and NVENC encoding)
numpy
opencv-python
astropy
Pillow
xisf
fastapi
uvicorn
pydantic
- XISF frames — calibrated, plate-solved mono frames from PixInsight WBPP with astrometric solution in XISF properties (
PCL:AstrometricSolution:*) - Ephemeris file — JPL Horizons text export with columns
1,2,3,4,5,9,10,18,19,20,23,24,25,47(see Ephemeris Setup)
# Install dependencies
pip install numpy opencv-python astropy Pillow xisf fastapi uvicorn pydantic
# Start the server
python server.py
# Open in browser
# http://localhost:8642- Source tab — set the frames directory (folder of XISF files) and ephemeris file path, click SCAN FRAMES
- Frame tab — adjust crop size (source extraction window) and output resolution (final video pixel size)
- Image tab — lock the stretch, run normalization, optionally prepare NXT denoising
- Annotation tab — configure the pointer, trajectory, panel placement, and watermark
- Output tab — set FPS, encoder, quality, click RENDER MP4
Generate from JPL Horizons:
| Setting | Value |
|---|---|
| Ephemeris Type | OBSERVER |
| Target Body | Your target (e.g., Orion MPCV for Artemis II) |
| Observer Location | Your observatory coordinates |
| Time Span | Must cover the full observation window (check frame timestamps) |
| Step Size | 1-2 minutes recommended |
| Table Quantities | 1,2,3,4,5,9,10,18,19,20,23,24,25,47 |
Important: If the ephemeris doesn't cover the full frame time range, the system will extrapolate linearly from the last two records. A warning is shown during scan. For best accuracy, always extend the ephemeris end time past the last frame by at least 5 minutes.
1 Astrometric RA & DEC 18 Heliocentric ecliptic lon. & lat.
2 Apparent RA & DEC 19 Heliocentric range & range-rate
3 Rates; RA & DEC 20 Observer range & range-rate
4 Apparent AZ & EL 23 Sun-Observer-Target ELONG angle
5 Rates; AZ & EL 24 Sun-Target-Observer ~PHASE angle
9 Visual mag. & Surface Brght 25 Target-Observer-Moon angle / Illum%
10 Illuminated fraction 47 Sky motion: rate & angles
| Method | Path | Description |
|---|---|---|
POST |
/api/scan |
Start scanning XISF frames + loading ephemeris |
GET |
/api/scan/status |
Poll scan progress and results |
GET |
/api/frame-info/{index} |
Get metadata for a specific frame |
| Method | Path | Description |
|---|---|---|
POST |
/api/preview |
Render a single frame as PNG |
POST |
/api/render |
Start parallel MP4 render |
GET |
/api/render/status |
Poll render progress |
GET |
/api/download |
Download the rendered MP4 |
| Method | Path | Description |
|---|---|---|
POST |
/api/lock-stretch |
Lock stretch to a reference frame's percentiles |
POST |
/api/unlock-stretch |
Revert to per-frame auto-stretch |
GET |
/api/stretch-status |
Check lock state |
| Method | Path | Description |
|---|---|---|
POST |
/api/normalize |
Compute per-frame (a, b) coefficients + auto-lock stretch |
POST |
/api/unnormalize |
Discard normalization |
GET |
/api/normalize-status |
Poll normalization progress |
| Method | Path | Description |
|---|---|---|
POST |
/api/nxt/prepare |
Start 4-stage NXT pipeline (precrop, stitch, denoise, unstitch) |
POST |
/api/nxt/enable |
Toggle denoised frame source |
POST |
/api/nxt/clear |
Forget prepared state |
GET |
/api/nxt/status |
Poll NXT progress |
| Method | Path | Description |
|---|---|---|
GET |
/api/zoom-keyframes |
Get keyframes + interpolated curve |
POST |
/api/zoom-keyframes |
Set keyframe list |
DELETE |
/api/zoom-keyframes |
Clear all keyframes |
| Method | Path | Description |
|---|---|---|
GET |
/api/debug/render-coords |
Track values, crop bounds, projected reticle position for a frame |
GET |
/api/debug/frame-stats |
Raw + normalized pixel statistics for a frame |
Benchmarked on i9-13900K (24c/32t), RTX 4090 (24 GB), 64 GB RAM:
| Operation | Time | Notes |
|---|---|---|
| Frame scan (164 frames) | ~30 s | WCS + metadata extraction |
| Normalization | ~10 s | 8 parallel workers, 1M-sample stride per frame |
| NXT prepare (164 frames) | ~3-4 min | Tile-stitched composites, chunked PI invocations |
| Render 164 frames (600px) | ~15-30 s | 8 workers, crop-first optimization |
| Render 164 frames (1200px) | ~30-60 s | Larger output, text drawn at native resolution |
Originally developed during the Artemis II mission observation on 2026-04-10 at the Southern Carina Observatory. Built iteratively over one session:
- Frame normalization — solved atmospheric transparency flicker using robust median/MAD matching
- GPU-accelerated render — crop-first + ProcessPoolExecutor gave ~15x speedup over the original sequential loop
- NoiseXTerminator integration — headless PixInsight pipeline with tile stitching to amortize per-call overhead
- Zoom keyframes — dynamic source-window animation with easing, composable with NXT denoising
- ffmpeg encoding — replaced OpenCV's mp4v codec for dramatically crisper text overlays
- Arrow + trajectory overlays — configurable pointer and historical path visualization
- Tab-based UI — organized growing control surface into 5 logical groups
- Ephemeris extrapolation — fixed frozen indicators when Horizons file was shorter than the observation
MIT License. See LICENSE for details.
fluxa | Southern Carina Observatory | Chile