Skip to content

Repository files navigation

MoonStack

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.

Eclipse sequence

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:

Before and after

What it does

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

Install

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.

Run

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.

Outputs

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

The parts worth stealing

  • 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.

Claude Code skill

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/

License

MIT.

About

One-click lunar eclipse RAW stacking for any camera (Canon/Nikon/Sony/Fuji/DNG...): frame rejection, phase grouping, HDR-aware stacking, sliver rescue, PixInsight/Photoshop automation, phone composites, plus a Claude Code skill

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages