One-click post-processing for a lunar eclipse (or any moon sequence): drop a folder of RAWs in, get one clean image per eclipse phase, a whole-night sequence, phone-format composites, layered Photoshop files and a report that shows which frames were used and why.
Works with any camera whose RAW LibRaw can decode — Canon (CR2/CR3), Nikon (NEF), Sony (ARW), Fuji (RAF, X-Trans included), Olympus/OM (ORF), Panasonic (RW2), Pentax (PEF), DNG and more — with any lens or telescope, tracked or untracked. The only camera-specific input is the sensor width (or pixel pitch); everything else comes from EXIF.
The heuristics were tuned on one real night: 96 frames from a Fuji X-T30 + 300 mm on a plain tripod, 96 % umbral coverage. That data set is where every rule in the code came from; the Chinese README and skills/lunar-eclipse-stacking/ record the reasons.
Before / after, all from the same untracked frames — a single camera JPG of the umbra vs. the stack, a 100 % crop of a partial phase, and a sunlit sliver that was clipped in every exposure vs. the rebuilt one:
analyze decode RAW (rawpy/LibRaw, linear, daylight WB), find the moon disk to sub-pixel accuracy,
score sharpness / clipping / trailing, cache a crop per frame
group split the night into phases from physics (sunlit fraction of the disk, exposure
regime, time window); exposure brackets become their own HDR group; reject soft,
trailed, shaken and over-clipped frames — but only against frames of similar exposure
stack chained sub-pixel alignment (phase correlation → coarse-to-fine ECC, limb-radius
correction for thin slivers, drift model for untracked mounts), photometric
normalisation in the sensor's linear range, HDR-aware noise-normalised sigma-clip
stack in radiance units (counts × N² / (t × ISO))
pixinsight BlurXTerminator + NoiseXTerminator headless (optional)
finish sunlit-surface white balance, lateral-CA channel alignment, local tone mapping of the
umbra, soft neutralisation of saturated limbs, rebuild of blown sunlit slivers from the
registered full-moon texture × a measured penumbral profile, composites, phone layouts
photoshop layered PSD per phase (stack + sharpen + curves/vibrance layers) and a sequence PSD (optional)
report self-contained HTML: every frame, its score and its fate
pip install -r requirements.txt # rawpy, opencv-python, scikit-image, tifffile, exifread, Pillow, numpy
Optional: PixInsight with RC-Astro BlurXTerminator/NoiseXTerminator, Adobe Photoshop 2025 (paths in moonstack/config.py; skipped when absent or with --no-pi --no-ps), and exiftool on PATH for RAW formats whose metadata exifread cannot parse (CR3) when no JPG sidecar exists.
python run.py # full pipeline, ~7 min for 100 × 26 MP frames
python run.py --from stack # resume from a stage
python run.py --stage finish # rerun one stage while tuning the look
python -m moonstack.share # shareable single-file page
Put your RAWs (with or without JPG sidecars) in Eclipse/ or set input_dir in config.json. The first stage, preflight, inspects the camera (sensor size from the EXIF crop factor, CFA type, bit depth), the optics (focal length → arcsec/px → moon size → crop size), the exposures (regimes, bracket ladders) and whether the mount was tracking (measured moon drift), then writes an adapted config.json and prints warnings for anything it could not determine:
[preflight] 96 x .RAF from X-T30 6246x4170 X-Trans 6x6 14-bit (JPG sidecars)
[preflight] sensor 24.0 mm / 3.842 um px <- EXIF 35mm-equivalent focal (450 mm -> crop 1.50x)
[preflight] focal 300 mm -> 2.64"/px, moon ~709 px, crop 1024 px
[preflight] regime +4 stops: 45 frames e.g. 0.25s/ISO1600/f6.3 ...
[preflight] bracket ladders: [12]
[preflight] drift 3.53 px/s (untracked would be 5.49) -> untracked
Settings you may still need to give it:
| key | when |
|---|---|
sensor_width_mm |
always — APS-C ≈ 23.5, full frame 36, Micro Four Thirds 17.3, 1" 13.2 (or give pixel_pitch_um instead) |
focal_mm |
telescopes and manual lenses that write no focal length to EXIF |
tracked: true |
equatorial mount — turns off the drift model and the trailing rejection |
These only seed the moon-radius prior (recalibrated from full-disk frames) and the motion-blur estimate, so ±10 % is fine. Non-Fuji bodies have not been run end-to-end yet — the decode, EXIF and geometry paths are generic, but if preflight reports something odd for your camera, please open an issue with its output.
output/final/NN_stage_HHMM.tif|jpg one per phase, 16-bit sRGB
output/final/00_composite.jpg the whole night in time order
output/final/phone_grid.png|jpg 6 phases, 2 per row, 1080×2340, 16-bit PNG (also phone_arc / phone_hero)
output/photoshop/*.psd layered, plus 00_eclipse_sequence.psd
output/report.html frames, scores, keep/reject reasons, alignment stats
output/frames.json, groups.json machine-readable state of every stage
- Radiance normalisation with the aperture term. Mixed-exposure stacks only work if every frame is on one scale; forgetting N² makes f/6.3 and f/13 frames differ by 4.3×.
- HDR without an HDR step. Mask clipped pixels, weight by photon SNR, sigma-clip with residuals normalised by each frame's own noise. Bracket ladders and mixed-exposure totality groups merge in the same code path.
- Registering thin crescents. ECC slides along a smooth arc. Position the sliver along the arc from a constant-velocity drift model and radially from its limb radius against the reference circle.
- Radial-ray limb fit with confidence. Works on the sunlit limb, the faint umbra/sky limb of a long exposure, and tells you when a frame is too thin to trust.
- Rebuilding a blown sliver honestly. Texture from the full-moon frame registered onto the disk (field rotation searched), brightness shape from a measured penumbral profile. Extrapolating the gradient never worked; a flat plateau looks fake at a glance.
skills/lunar-eclipse-stacking/SKILL.md packages the workflow, log-reading guide and pitfalls so an agent can run and tune this pipeline. Install with
claude plugin marketplace add mtang724/moonstack # or copy the folder into ~/.claude/skills/
MIT.



