ophi is a lightweight Rust tool for analyzing and sorting FITS light frames before stacking.
The basic idea is simple: point it at a folder of light frames, let it measure each frame, and then review the generated reports or let it copy/move the files into accept, review, and reject folders.
It is meant to be a practical pre-stacking quality check, not a replacement for visual inspection or proper calibration. It works well on my own ASI2600MM data, but it still needs testing on more cameras, filters, focal lengths, sky conditions, and FITS variants.
This project is early and should be treated as a testing build.
Some features are more experimental than others:
| Feature | Status | Notes |
|---|---|---|
| CPU analysis and sorting | primary path | This is the baseline path I use for real testing. |
| JSON and HTML reports | primary path | Useful for inspecting the actual metric values behind each verdict. |
| Target profiles | experimental | Each profile now sets real score weights and a structure tolerance, but the values are reasoned rather than measured and still need validation on more targets. |
| GPU acceleration | experimental | Currently often slower than CPU. Only selected stages are offloaded, each frame must be converted/uploaded, partial results are read back, and many important metrics still run on CPU. |
| GUI | experimental | Useful for basic runs, but the CLI is still the main interface for repeatable testing. |
Everything below is data I shot myself, so it covers one mount, one site and two cameras. That is the main limit of the testing so far, not the frame counts.
| Data | Frames | Notes |
|---|---|---|
| ASI2600MM mono, LRGB and Ha, 130 mm | 529 | Five filters in one session, scored per filter. |
| ASI2600MC Pro colour, 179 mm | 115 | One night, sky brightened and the target sank from 63 to 52 degrees. |
| ASI2600MC colour, 282 mm | 15 | Galaxy field, mostly a check that the CFA path behaves. |
The colour frames are the more interesting test. A one-shot-colour sensor is handled by equalising the four Bayer positions in place rather than binning to 2x2 superpixels, so the native sampling is kept. At 179 mm that is 4.33 arcseconds per pixel, and a fixed FWHM limit in arcseconds is meaningless at that scale: the built-in default of 4 arcseconds is smaller than one pixel and would have rejected every frame. The adaptive limit derived from the session came out at 15.78 arcseconds and rejected five frames measuring 3.9 to 5.0 pixels against a median of 2.75. That is the behaviour I wanted, but it is one night, not a proof.
Conditions changed a lot across that night. The second half had 60 percent more background and 38 percent more noise, which raised the detection threshold and cut the star count from 22673 to 13958. Median scores stayed at 0.926 and 0.922, so the session-relative scoring did not punish the later frames for a brighter sky. The star counts still look alarming side by side, and I would rather people know that than discover it themselves.
Speed, from my system:
- 348 ASI2600MM light frames on NVMe: about one minute
- 529 frames, 27 GB, over 2.5 GbE from a two-disk NAS: 4 minutes, which is what the network and the disks allow rather than anything ophi decides
- a re-run of an unchanged folder: under a second, because measurements are cached per file
What has not been tested: any camera that is not an ASI2600, any pixel scale outside 2.7 to 4.4 arcseconds per pixel, and the 2x2 superpixel path, which both colour data sets avoided by equalising instead.
I am especially interested in real-world feedback about:
- number of frames processed
- camera and image size
- filters used
- processing time
- whether
accept/review/rejectdecisions seemed reasonable - obviously misclassified frames
- crashes or FITS files it could not read
Criticism is more useful than compliments at this stage, especially examples where the classification is wrong.
Builds for Windows, Linux and macOS are on the releases page. There is no installer. The archive holds the program and one library next to it, and both have to stay in the same folder; ophi will not start without the library.
On Windows, unpack the .zip and you have ophi.exe and zfits_c.dll.
Double-click ophi.exe for the graphical interface, or run it from a terminal
with a folder as its argument for the command line one.
On Linux and macOS, unpack the .tar.gz and mark the binary executable with
chmod +x ophi. The library beside it is libzfits_c.so or libzfits_c.dylib.
The first time you run it you will get a blue dialog saying "Windows protected your PC". Click More info, then Run anyway.
This happens because the program is not code-signed, here's why: A signing certificate costs money every year, and Microsoft's cheap alternative is not available to individual developers in Germany, which is where I am. So the warning is not going away soon.
What you can check instead: every release lists a checksums.sha256 file. On
Windows, Get-FileHash ophi-....zip -Algorithm SHA256 should match the line
for your archive. That tells you the file you downloaded is the file the build
produced, which is the part a signature would otherwise confirm. The build
itself runs in the open, in GitHub Actions, from the tag it says it came from.
macOS is stricter and will refuse outright the first time. Right-click the
binary and choose Open, or run
xattr -d com.apple.quarantine ophi once.
If any of this is a problem for you, building from source is a few commands and is described below.
- Reads
.fit,.fits, and.ftslight frames through thezfitsC ABI. - Measures star count, FWHM, HFD, ellipticity, eccentricity, roundness, and sharpness.
- Measures background level, background noise, background uniformity, gradients, vignetting, fog, faint haze texture, and stripe-like haze.
- Attempts to distinguish cloud/fog structure from real astrophysical structure such as dust lanes, dark nebulae, nebulosity, or IFN.
- Detects possible cosmic rays, trails, tracking drift, and obstruction/dew-like star-density defects.
- Measures spatial PSF variation across the field, including PSF uniformity, tilt, and curvature.
- Computes a composite quality score from session-relative robust statistics.
- Groups and scores frames by imaging session and normalized FITS
FILTERvalue where possible. - Supports target profiles:
auto,galaxy,nebula,dark-nebula,cluster,widefield, andhigh-res. - Sorts frames into
accept,review, andrejectfolders, with optional filter subfolders. - Writes a full JSON report, a flagged-only JSON report, and an interactive HTML report.
- Can copy files by default, move files with
--move, or analyze only with--dry-run. - Has a bounded streaming pipeline so it does not load the entire dataset into memory.
- Supports optional experimental GPU acceleration for selected analysis stages through
wgpu. - Supports an optional experimental GUI build through
egui/eframe.
Requires Rust 2024 edition and the zfits C ABI library.
cargo build --releaseWith GPU support:
cargo build --release --features gpuWith the GUI:
cargo build --release --features guiWith both:
cargo build --release --features "gpu gui"ophi links against zfits, a separate FITS reader with a C ABI. Build it first (requires Zig 0.16):
git clone https://codeberg.org/chrischtel/zfits
cd zfits
zig build ffi -Doptimize=ReleaseFastThe build script finds it automatically if the checkout sits next to ophi:
parent/
ophi/
zfits/
Anywhere else, point at it explicitly:
$env:ZFITS_DIR = "C:\path\to\zfits"
cargo build --releaseIf zfits cannot be found the build stops with the locations it searched and the commands to fix it, rather than failing later with a linker error about a missing symbol.
The shared library is copied next to the built executables, and on Linux and
macOS an rpath is set so they find it — a cargo run works without setting
LD_LIBRARY_PATH.
ophi <INPUT_DIR> [OPTIONS]Example:
ophi "D:\Astro\Lights\M31" --recursiveDry run on the first 50 files:
ophi "D:\Astro\Lights\M31" --recursive --dry-run --limit 50Move files instead of copying:
ophi "D:\Astro\Lights\M31" --moveUse a target profile:
ophi "D:\Astro\Lights\LDN1235" --profile dark-nebulaUse GPU acceleration, if built with --features gpu:
ophi "D:\Astro\Lights\M31" --gpuRun the GUI, if built with --features gui:
ophi --guiWhen built with GUI support, running ophi without an input directory also opens the GUI.
| Option | Default | Description |
|---|---|---|
input_dir |
required for CLI | Folder containing FITS light frames. Omit when using the GUI. |
-o, --output-dir <DIR> |
<input_dir>/ophi_sorted |
Output folder for sorted files and reports. |
-r, --recursive |
off | Search subdirectories for FITS files. |
--dry-run |
off | Analyze only: writes the reports but never copies or moves a file. |
--move |
off | Move files instead of copying them. |
--gpu |
off | Use GPU compute shaders when built with --features gpu. |
--profile <PROFILE> |
auto |
Scoring profile: auto, galaxy, nebula, dark-nebula, cluster, widefield, high-res. |
-l, --limit <N> |
none | Analyze only the first N discovered files. |
-v, --verbose |
off | Enable detailed logging. |
--min-stars <N> |
50 |
Minimum detected star count before a strong penalty is applied. |
--max-fwhm <PX> |
6.0 |
Maximum acceptable median FWHM in pixels, with adaptive relaxation for sessions whose baseline is naturally higher. |
--max-roundness <VALUE> |
0.3 |
Maximum allowed deviation from round stars. |
--min-sharpness <VALUE> |
100.0 |
Minimum Laplacian-variance sharpness. Only consulted when no pixel scale is available. |
--max-clipped <PCT> |
5.0 |
Maximum near-saturated pixel percentage. |
--max-fwhm-arcsec <ARCSEC> |
4.0 |
Maximum median FWHM in arcseconds. Used whenever the pixel scale can be derived from the header. |
--model <FILE> |
none | Fitted rejection model from train_model. Its calibrated probability and precision-tuned bands then decide verdicts. |
Review margin is not exposed as a CLI flag; its default is review-margin = 0.10.
--max-fwhm is a fallback. FWHM in pixels is not comparable between setups —
6 px on a 135 mm lens and 6 px on a 2000 mm SCT describe completely different
stars — so whenever XPIXSZ + FOCALLEN (or a recorded SECPIX/PIXSCALE)
are present, ophi works in arcseconds and --max-fwhm-arcsec applies instead.
Filter subfolders are created when the FITS FILTER keyword is available. Common aliases are normalized, for example Ha, H-alpha, and halpha become H; OIII and O3 become O; lum, luminance, and clear become L.
Recursive discovery skips existing ophi_sorted, accept, review, and reject folders so old sorted output is not processed again.
ophi_report.json contains one entry per successfully analyzed frame.
ophi_flagged.json contains only review and reject frames, so it is easier to inspect questionable subs.
ophi_report.html is currently a placeholder page. The interactive report is
being rewritten; use ophi_report.json for the full per-frame values in the
meantime.
Each JSON frame report includes:
| Field | Meaning |
|---|---|
filename |
FITS filename. |
filter |
Normalized FITS filter value, if present. |
focal_length_mm |
FITS FOCALLEN, if present. |
verdict |
ACCEPT, REVIEW, or REJECT. |
score |
Final composite score after penalties, clamped to 0.0..1.0. |
stack_weight |
Relative weight for noise-optimal integration, normalised so a typical frame sits near 1.0. 0.0 for rejected frames. |
reject_probability |
Calibrated probability that integrating the frame would hurt the stack. Present only when --model was supplied. |
fwhm_arcsec |
Median FWHM in arcseconds — the portable seeing measure. null when no pixel scale could be derived. |
pixel_scale_arcsec |
Arcsec per pixel of the analysed image, including CFA binning. |
is_cfa / bayer_pattern |
Whether the frame was an undebayered colour mosaic, and its layout. |
airmass / altitude_deg |
Atmosphere traversed at mid-exposure, when the header allows it to be determined. |
base_score |
Weighted score before penalties. |
penalty |
Total penalty from thresholds and detected defects. |
subscores |
Per-metric normalized subscores used in the weighted score. |
reasons |
Human-readable reasons for warnings, review, or rejection. |
The full metric fields are listed in the next section.
| Metric | Field | Direction | Notes |
|---|---|---|---|
| Star count | stars |
higher is usually better | Number of detected stars from matched-filter local maxima. Low count can indicate clouds, fog, poor focus, narrowband sparsity, or obstruction. |
| FWHM | fwhm |
lower is better | Flux-weighted median full width at half maximum of detected stars, in pixels. |
| HFD | hfd |
lower is better | Flux-weighted median half-flux diameter, in pixels. More model-independent than FWHM. |
| Ellipticity | ellipticity |
lower is better | 1 - b/a, where 0 is round and 1 is line-like. |
| Eccentricity | eccentricity |
lower is better | sqrt(1 - b^2/a^2), more sensitive to elongation than ellipticity. |
| Roundness | roundness |
higher is better | Minor/major axis ratio, retained for compatibility with threshold checks. |
| Star sharpness | internal aggregate | higher is better | Median per-star sharpness ratio. |
| Theta consistency | internal aggregate | lower depends on cause | Circular spread of elongation angles. Consistent angles can indicate tracking drift or trails. |
| Metric | Field | Direction | Notes |
|---|---|---|---|
| Laplacian sharpness | sharpness |
higher is better | Variance of a discrete Laplacian response. Used as an absolute sharpness check. |
| Wavelet blur metric | blur_metric |
higher is better | Fine-scale energy ratio from an a-trous B3-spline wavelet decomposition. Lower values indicate blur. |
| Noise estimate | noise |
lower is better | MAD-based noise estimate from a subsampled central region. |
| Star SNR | star_snr |
higher is better | Median per-star signal-to-noise, from each star's flux against its own shot noise and the sky within its aperture. |
| Transparency | transparency_zenith |
higher is better | Median flux of the brightest stars per second, corrected to the zenith for airmass. Falls when cloud or extinction attenuates the field. |
| Deep stars | deep_stars |
higher is better | Stars detected above 10 sigma — a depth proxy. |
| Background SNR | background_snr |
diagnostic only | Sky brightness over noise. This rises under moonlight and light pollution, so it is not a quality measure. Reported because sky brightness is useful to know. |
| Clipped pixels | clipped_pct |
lower is better | Percentage of pixels within 98 percent of the frame maximum. |
| Metric | Field | Direction | Notes |
|---|---|---|---|
| Background uniformity | bg_uniformity |
lower is better | Coefficient of variation of background cells in an 8x8 grid. |
| Relative gradient | gradient_relative |
lower is better | Plane-fit gradient strength divided by background level. |
| Vignetting | vignetting |
lower is better | Corner darkening estimated from a radial brightness model. |
| Fog indicator | fog_indicator |
lower is better | Fraction of background grid cells more than 3 sigma above the global median. |
| Fog texture | fog_texture |
lower is better | Variance of normalized (P75 - median) across a 16x16 grid. Detects faint partial haze. |
| Stripe fog | stripe_fog |
lower is better | Peak normalized deviation in detrended row/column background profiles. Detects band-like haze. |
| Dark structure score | dark_structure_score |
informational | Coherent low-frequency dark structure, such as dust lanes or dark nebulae. |
| Nebulosity score | nebulosity_score |
informational | Coherent low-frequency bright structure, such as nebulosity or IFN. |
| Structure confidence | structure_confidence |
higher means more likely real structure | Used to reduce false cloud penalties on dusty or nebulous fields. |
| Cloud likelihood | cloud_likelihood |
lower is better | Raw combined cloud/fog estimate. Deliberately not discounted for coherent structure — that allowance is a profile decision applied during scoring. |
| Metric | Field | Direction | Notes |
|---|---|---|---|
| Cosmic rays | cosmic_rays |
lower is better | L.A.Cosmic-style Laplacian detections with star masking and density-aware thresholds. |
| Trail score | trail_score |
lower is better | Score from elongated-source ratio and angular consistency. |
| Obstruction score | obstruction_score |
lower is better | Star-density variation cross-checked against local background to reduce false positives on dark nebulae. |
| Metric | Field | Direction | Notes |
|---|---|---|---|
| PSF uniformity | psf_uniformity |
higher is better | 1 - sigma(FWHM) / mean(FWHM) across field grid cells. |
| Tilt | tilt |
lower is better | Plane-fit FWHM gradient across the field. |
| Curvature | curvature |
lower is better | Center-vs-edge FWHM curvature estimate. |
ophi scores frames relative to the current dataset, not only against fixed universal thresholds. This is intentional: different filters, focal lengths, sampling rates, and sky conditions can produce very different absolute values.
The base score is a weighted average of robust session-relative metric subscores:
The weights depend on the target profile, because which axis matters is a property of the target. Fine structure in a galaxy or cluster is destroyed by a soft frame and cannot be recovered; faint nebulosity is limited by integration time and barely notices half an arcsecond of seeing.
| Metric | auto | galaxy | cluster | high-res | nebula | dark-nebula | widefield |
|---|---|---|---|---|---|---|---|
| FWHM (seeing) | 0.28 | 0.32 | 0.34 | 0.36 | 0.20 | 0.20 | 0.20 |
| Ellipticity | 0.14 | 0.18 | 0.20 | 0.20 | 0.10 | 0.10 | 0.16 |
| Star SNR | 0.14 | 0.12 | 0.10 | 0.10 | 0.18 | 0.16 | 0.14 |
| Star count | 0.12 | 0.08 | 0.10 | 0.08 | 0.12 | 0.10 | 0.14 |
| Transparency | 0.12 | 0.08 | 0.06 | 0.06 | 0.22 | 0.24 | 0.16 |
| Wavelet blur | 0.10 | 0.12 | 0.10 | 0.12 | 0.08 | 0.08 | 0.08 |
| PSF uniformity | 0.05 | 0.08 | 0.08 | 0.06 | 0.04 | 0.04 | 0.06 |
| Background uniformity | 0.05 | 0.02 | 0.02 | 0.02 | 0.06 | 0.08 | 0.06 |
Each column sums to 1.0. widefield deliberately de-emphasises FWHM: an
undersampled field puts every star at one or two pixels whatever the seeing
did, so FWHM discriminates poorly between frames there and transparency
carries more information.
Relative quality lives entirely in the base score above, which degrades smoothly. Penalties carry only absolute defects — things that are wrong no matter how the rest of the session looked — and each is charged according to how much of the damage actually reaches the final image:
Permanent — baked into the pixels, charged in full:
- FWHM above the adaptive seeing limit
- elongated stars
- very low star count
- saturation/clipping
- fog/cloud likelihood, fog texture, stripe haze
- obstruction/dew-like defects
- wrong FITS
OBJECTrelative to the session majority
Removed by calibration and background extraction — charged at 20%:
- strong gradients
- vignetting
Removed by pixel rejection during integration — charged at 10% once the group has at least 10 frames, in full below that:
- cosmic rays
- satellite/aircraft trails
Low signal is deliberately not a penalty. With noise-optimal weighting the
total stacked SNR2 is the sum of the per-frame contributions, so a
dim frame adds little but essentially never subtracts — discarding it just
loses integration time. It is down-weighted through stack_weight instead.
Final score:
score = clamp(base_score - penalty, 0.0, 1.0)
Default verdict behavior:
| Verdict | Meaning |
|---|---|
ACCEPT |
No meaningful quality reasons were found, or the score meets the active profile's accept threshold. |
REVIEW |
Borderline frame. It may still be useful, but should be checked manually. |
REJECT |
Strong defects or low final score. |
The default reject cutoff is effectively 0.40, because review_margin defaults to 0.10 and the internal reject boundary starts from 0.50.
Profiles do two things: they set the weights of the composite score, and they set how strict ophi is about star shape and background structure.
The weights are the important half. Scaling penalties alone — which is all the profiles used to do — cannot express a target preference, because penalties only fire on defects and two clean frames scored identically under every profile.
| Profile | Priority | Structure tolerance | Use case |
|---|---|---|---|
auto |
balanced | 0.65 | General default. Tightens star-shape policy at FOCALLEN >= 400 mm. |
galaxy |
detail | 0.45 | Galaxies, including dusty ones. Sharpness first; dust lanes partly excused. |
cluster |
detail | 0.25 | Star clusters. Strictest star shape, and background structure is treated as suspicious. |
high-res |
detail | 0.30 | Long focal length, well sampled. Strictest resolution priority. |
nebula |
depth | 0.80 | Emission and reflection nebulae. Nebulosity is expected, not a defect. |
dark-nebula |
depth | 0.92 | Dust lanes, dark nebulae, IFN. Coherent structure is the subject. |
widefield |
depth | 0.60 | Short focal length. FWHM is a weak discriminator when undersampled. |
Priority is the trade the profile takes. Detail-first profiles weight
resolution and star shape heavily and reject soft frames; depth-first profiles
weight transparency and signal, and keep soft frames because integration time
is the binding constraint. A frame measured at 5" seeing scores 0.39 under
high-res and 0.55 under dark-nebula — the same measurement, a different
verdict, which is the point of having profiles at all.
Structure tolerance is how far coherent large-scale structure excuses an
apparent cloud signal, from 0 (no excuse) to 1 (fully excused). Dust lanes,
dark nebulae and thin cloud are indistinguishable to any single-frame
measurement — all three are large, soft, low-frequency brightness variation.
The difference is whether you expected it, which is exactly what choosing a
profile declares. On dark-nebula structure is almost never read as cloud; on
cluster, where a field has no business showing large-scale structure, it
mostly is. Cloud with no coherent structure is judged identically under every
profile.
When running auto, if ophi measures strong coherent background structure it
reports the observation and lists the profiles that would interpret it, rather
than guessing. It deliberately recommends nothing: a high
dark_structure_score fires on any dusty field including galaxies, and
dark-nebula is the wrong answer for a galaxy — that profile relaxes
background scoring where a galaxy wants stricter star shape.
After scoring, ophi may print a Suggestions section with one or both of the following tips.
Borderline FWHM frames: If any REVIEW frames were flagged only because their FWHM is marginally over the threshold (less than 5% over), they are listed by name with their reason. These frames passed every other check and the PSF degradation is small enough that they may not be visible to the eye. They are worth inspecting before discarding.
Coherent structure notice: If the session average dark_structure_score or
nebulosity_score is above 0.40 and the profile is auto, ophi reports what it
measured and lists the profiles that would interpret it differently. It names no
single recommendation, because one background metric cannot identify the target.
The pipeline discovers FITS files, loads them through I/O workers, analyzes frames, scores the session, then copies or moves files.
Important implementation details:
- Pixel data is dropped after each frame is analyzed.
- Frames are passed through bounded channels to avoid loading the whole dataset into memory.
- Default I/O worker count is available CPU parallelism capped at 8.
- Default in-flight frame buffer is
2 * io_workers, capped at 16. - CPU analysis uses Rayon for parallel work.
- File copies/moves are done after scoring and can run concurrently.
Optional environment variables:
| Variable | Meaning |
|---|---|
OPHI_IO_WORKERS |
Override number of FITS loading workers. |
OPHI_PIPELINE_BUFFER |
Override maximum number of in-flight frames. |
RUST_LOG |
Override logging level. -v sets debug logging. |
ZFITS_DIR |
Path to the zfits checkout used by the build script. |
When built with --features gpu and run with --gpu, ophi uses wgpu compute shaders for selected analysis work. If no suitable GPU is available, it falls back to CPU.
The GPU path is experimental. At the moment it can be slower than the CPU path.
The main reasons are:
- FITS pixels are loaded as
f64, then converted tof32before GPU upload. - Pixel buffers are uploaded per frame.
- Some GPU passes require readback to the CPU, which forces synchronization.
- Star candidates are found on the GPU, but individual star measurement and aggregate metrics still happen on the CPU.
- Median/MAD-heavy work is still CPU-side, including noise estimation and much of the robust statistics.
- Cloud/background structure, artifacts, wavelets, spatial PSF analysis, scoring, sorting, and reporting still run on CPU.
- The CPU path is already heavily parallelized with Rayon, so partial GPU offload has to beat a fairly fast baseline.
For now, use the CPU path for serious testing and use --gpu only if you want to help benchmark or improve the GPU implementation.
When built with --features gui, ophi includes a small desktop GUI using egui / eframe.
The GUI is experimental and not fully finished. The CLI is still the better path when you want repeatable test runs, exact flags, and easier comparison between datasets.
The GUI supports:
- selecting input and output folders
- drag-and-drop folder selection
- recursive search
- copy or move mode
- dry run
- GPU toggle when compiled with GPU support
- progress display
- result summary
- simple result table with verdict filtering
The CLI remains the primary interface for repeatable testing.
ophi reads these FITS header values when present:
| Header | Use |
|---|---|
FILTER |
Filter grouping and normalized filter subfolders. |
DATE-OBS |
Session grouping. Gaps over 30 minutes are treated as a new session. |
OBJECT |
Detects frames whose object differs from the session majority. |
FOCALLEN |
Pixel scale, and tightens star-shape policy for longer focal lengths. |
XPIXSZ |
Pixel scale, with FOCALLEN. Taken as the pitch after binning, per the usual SBIG convention. |
SECPIX1 / PIXSCALE / SCALE |
Arcsec per pixel, preferred over deriving it when present. |
BAYERPAT |
Marks an undebayered colour frame, which is then analysed as 2×2 superpixels. |
EXPTIME |
Normalises transparency so different exposure lengths compare. |
AIRMASS, or OBJCTALT |
Extinction correction, preferred over reconstructing it. |
OBJCTRA / OBJCTDEC, SITELAT / SITELONG |
Airmass reconstruction when it was not recorded directly. |
CCD-TEMP |
Loaded, currently available from the frame layer. |
Undebayered colour frames are detected via BAYERPAT and the four mosaic
positions are equalised — shifted to a common background and rescaled to a
common response — before anything is measured. Every pixel stays at its native
coordinate, so a colour sensor is measured on the same angular grid as a mono
one.
This matters more than it sounds. On a colour mosaic, neighbouring pixels belong to different colour channels, so every measurement that compares a pixel to its neighbour — PSF second moments, the Laplacian, wavelet detail planes, MAD noise — reads the Bayer checkerboard instead of the sky.
Against a synthetic field of known width (true FWHM 2.92″):
| measured FWHM | error | stars found | |
|---|---|---|---|
| Mono sensor (reference) | 2.93″ | 0.3% | 390 |
| Colour, raw mosaic | 20.65″ | 606% | 299 |
| Colour, 2×2 superpixels | 3.11″ | 6.4% | 374 |
| Colour, equalised | 2.93″ | 0.3% | 390 |
Laplacian sharpness on the raw mosaic came out roughly 600× too high, which
meant the --min-sharpness check could never fire on a colour frame.
Superpixel binning remains as a fallback when a frame is too blank to yield a level for all four positions; it halves the sampling, which costs a few percent on FWHM. The equalisation rescales each position by its own sky level as a stand-in for its sensitivity, so the correction is only partial when the sensor bias offset is large next to the sky — the offset removal, which is what fixes the background, is exact either way.
There are no labels shipped with ophi, so its accuracy on your data is unmeasured until you measure it. Two tools help.
rank_sweep ranks frames by any reported metric, stacks the best K for a
sweep of K using Siril, and reports where stack quality
stopped improving. The frames past that point are the ones that were not worth
integrating — objective labels from your own data, with no human judgement in
the loop:
rank_sweep --report ophi_sorted\ophi_report.json --frames "D:\Astro\Lights\M31" --out sweepFrames are hard-linked into each working directory where the filesystem allows
it, so a sweep does not duplicate the dataset. Use --dry-run to inspect the
generated Siril scripts first, and --rank-by fwhm_arcsec (or stack_weight,
star_snr) to see which metric orders your frames best.
train_model fits a logistic model from those labels and picks verdict
thresholds that hit a precision target on held-out frames:
train_model --report ophi_sorted\ophi_report.json --labels labels.csv --out model.json
ophi "D:\Astro\Lights\M31" --model model.jsonlabels.csv needs a filename column and a hurts_stack column (1/0).
The model reports ACCEPT precision, REJECT precision and the REVIEW rate. Read them together: sending every frame to REVIEW would score perfect precision while being useless. When the precision target cannot be met, the bands stay conservative and more frames land in REVIEW rather than being decided wrongly.
Note that three-way agreement with a human has a ceiling well below 100%, because experienced imagers disagree on borderline subs. Precision on the confident classes, with REVIEW absorbing the ambiguity, is the reachable and more useful target.
- The thresholds are still empirical.
- The tool has mainly been tested on my own camera/filter setup.
- It analyzes light frames only; it is not a calibration pipeline.
- It does not know whether a frame is scientifically useful in context. A
reviewframe may still improve a stack. - Very unusual FITS files may fail depending on what
zfitssupports. - Dark nebulae, IFN, gradients, reflection nebulosity, clouds, and flat-field problems can look similar numerically. The structure detection helps, but it will not be perfect.
If you test it, the most useful report would include:
Frames processed:
Camera:
Image size:
Filters:
Storage type:
CPU:
GPU used:
Processing time:
Accepted / Review / Rejected:
Did the decisions seem reasonable:
Examples of wrong decisions:
Crashes or unreadable FITS files:
Notes:
GPL-3.0-or-later. See LICENSE.
You may use, study, change and share ophi. If you distribute a modified version, it has to stay under the same licence, so the people you give it to keep the freedoms you had.