July 26, 2026 · 🔭 Astronomy 🖥️ Technology
The thought came to me in bed – a Sunday with somebody else's Siril code
It arrived just before falling asleep: Cyril Richard, one of the Siril developers, has written a script of his own that calibrates and stacks several nights. Pretty much what my own script does in its first half. Why had I never actually looked inside it?
The next morning – Sunday, Hannover, coffee – it was still night in Texas. My rig at Starfront was on Sh2-129, the Flying Bat Nebula, collecting Ha and OIII: the bat in one channel, in the other the prospect of the Squid, which shows itself almost exclusively in OIII. So there was nothing to process – the data was only just coming into existence, 8,500 kilometres further west.
A good morning for reading instead of building.
The single idea turned into three rounds: first Cyril's script, then everything in the official repository that has to do with colour, then the rest. Twenty-six foreign scripts.
It was never about copying anything, but about seeing how other people solve the same problems – and understanding why they solve them that way. Some of it I adopted, some of it deliberately not, and in one place somebody else's code taught me something about my own.
Round one: the script that solves the same problem
AMSP is the script from that night-time thought. The question I put to the analysis wasn't "what can I take", but: what does Cyril do differently – and what of that does he do better? The answer was more uncomfortable than I would have liked.
He groups by exposure time; I only warned. AMSP forms groups from target, filter and exposure. My script pooled all exposures of a filter and picked the dark based on one representative frame – so a filter holding both 120- and 300-second subs got a dark that fits only half of them. There was a warning, but a warning repairs nothing.
Now such a filter is calibrated in parts: each exposure with its own dark, after which the parts are merged again, before registration.
His filenames carry the recipe. AMSP assembles the name from the header of the loaded file – that way it cannot be mismatched. My mapping ran through an internal table that I had gone to some lengths to safeguard; his solution doesn't need that safeguard in the first place.
So M16_HA_fullframe.fit has become M16_HA_29x300s_G100_-10C_fullframe.fit.
The real gain is a safeguard I picked up along the way. After alignment, every master is asked which filter it itself carries in its header – before its name is written. If that contradicts the internal table, the channel is thrown out.
Because a channel under the wrong name is the one error in this chain that nothing downstream would notice: the image would come out finished, colour-calibrated and plausible – only with Ha where OIII belongs.
Two points I deliberately left alone. He corrects optical distortion during registration already; at my focal length the effect is probably below the noise.
And he calibrates per night on principle and merges only afterwards – which lets every light get the flat of its own night. That is the one genuine architectural difference between the two scripts. For a permanently mounted rig that nobody touches between nights it would be a large rebuild for an advantage I wouldn't have.
I adopted his rejection tiers wholesale. Which method of discarding outliers is the right one depends on how many frames are being stacked – what works for four images is not what works for three hundred. I had set those thresholds by gut feeling.
Cyril is one of Siril's developers. When he lays down in his script at which frame count you switch from percentile to sigma, then to Winsorized, GESDT and finally linear fit, he has reasons for it that I don't have. His thresholds are considered; mine were guessed.
The comparison showed differences in three of five bands: I had linear fit sitting in the middle, he has it right at the top; GESDT started at 50 frames for me, at 31 for him. So his tiers now sit in my script, with the source noted in the comment.
Conversely there are areas where my script is further along: it keeps finished calibration masters rather than rebuilding them on every run, and it writes a report about what actually happened.
And colour isn't a topic in AMSP at all – it ends at the aligned master, whereas for me palette, composition and colour calibration only start after that. That's not a shortcoming, it's a different purpose.
Round two: how other people do colour
If one foreign script yields that much – why only one? The official repository now holds around 65 Python scripts, and when it comes to assembling colour images the pickings are considerably richer than for preprocessing.
The most valuable find was a counter-position, not a recipe. Cyril Richard's PalettePicker.py carries an explicit design note: this version deliberately does not combine linear images, it expects channels that have already been stretched – otherwise the script would have had to stretch them itself, taking away the user's control over precisely the step that is a matter of taste.
My script does it the other way round, because colour calibration needs linear data. Not a contradiction, a different division of labour. But the note made me recompute my own Ha blending – and that's where it got interesting.
The formula behind it is a so-called screen blend, not plain addition. On linear data it looks like this:
| Red | Ha | Screen blend | Plain addition | Difference |
|---|---|---|---|---|
| 0.002 | 0.003 | 0.003497 | 0.003500 | 0.09 % |
| 0.02 | 0.03 | 0.034700 | 0.035000 | 0.86 % |
| 0.4 | 0.6 | 0.580000 | 0.700000 | 17.1 % |
Typical linear background and nebula values sit between 0.001 and 0.01 – that is, in the top two rows. There, my screen blend is arithmetically indistinguishable from plain addition; its actual character only shows at brightness levels that, in linear data, only star cores reach.
Not a bug – addition is a perfectly legitimate way to blend in Ha. But the slider does something other than its name suggests, and that belongs in the documentation.
The same arithmetic also explains why I can't adopt the popular dynamic palettes: their weighting factor depends on the product of two channel values, and in linear data that product sits at roughly 0.000006. The formula collapses before it weights anything at all.
Somebody else's comments are sometimes dearly bought experience. The same script doesn't hand its result to Siril through a file; it pushes the pixels across directly.
Next to it sits a warning that someone evidently learned the hard way: you must not load an existing mono image as a template and swap its data – Siril then stays in single-channel mode and the finished colour image sporadically comes out grey. You have to create a new image.
That single line of comment probably saved me half an evening of debugging. I adopted the direct route and removed two workarounds of my own.
Along with that came a few cheap additions: seven further narrowband assignments, two weighted mixes, a synthetic luminance for nights without an L filter – and a neat three-liner against the purple stars that SHO images tend to have.
One find I deliberately left alone, even though it would be the cleanest. Another script determines computationally how much Ha is already sitting in the red channel, rather than estimating it with a slider: it tries forty scaling factors, fits a smoothed curve through the noisy results and takes its vertex.
That is the correct answer to a question my slider merely sidesteps. It's on the list, but it's the biggest chunk.
Round three: across the whole pipeline
After that I widened the search to everything else my script does – calibration, registration, stacking, file handling. Twenty-six scripts, nineteen of them in areas I cover too.
The most important find concerns something my script had been estimating. Three foreign scripts use the same interface: Siril hands out its registration data in structured form. Per frame it holds full width at half maximum, roundness, quality, background level and star count.
That hits precisely the spot I had been writing my way around in the report for weeks: the frame count in the filename – the 29 in M16_HA_29x300s_G100_-10C – was an upper bound, which is why the report carried a less-than-or-equal sign in front of it. A phrasing that dresses up a gap instead of closing it.
Through the interface it becomes a measurement, and "roughly 29 frames integrated" turns into real per-channel numbers.
The second gain is invisible but matters more for durability: alignment quality was being read as text out of the Siril log – which works exactly until someone rewords a message.
An intermediate step that costs ten lines. Another script creates its working copies in three stages: symlink first, then hardlink, then a real copy. I only had two. The hardlink in between takes no additional storage and works where symlinks aren't permitted.
Above all it is more robust against exactly the incident that cost me an evening a while back: a cloud sync service had "repaired" Siril's symlinks. A hardlink isn't a reference that could be repaired, it is a second name for the same file.
Flats get checked, not just used. The most elegant idea came from a small analysis tool: divide two flats by each other and the result should ideally be a uniform surface. The spread of that result measures whether anything changed between the two exposures. A percentile view additionally says in which direction – dark outliers point to dust or vignetting.
My script has so far taken flats as it found them and pooled them across nights; the switch for that relies on my knowing whether anything was touched on the setup in between. Now the script does the arithmetic before it pools.
A fourth suggestion I left alone. One script attaches a file watcher to the working folder and deletes each intermediate generation the moment its successor appears – with a hundred subs that's several gigabytes of difference.
The watcher itself isn't going into my script: an additional dependency and a race with Siril's own writes. The deterministic equivalent is on the list.
With a good dozen scripts there was simply nothing to take: smart-telescope mosaics, comet star trails, satellite trail removal, several enhancement suites. They all work non-linearly or in areas my pipeline never touches. That is a result too – you just have to have looked once.
What I'm taking away
Somebody else's code is a checker you didn't build yourself. My own tests find what I thought of while writing. A script that solves the same problem differently exposes the assumptions I never questioned – such as a filter having only one exposure time.
The biggest gain was something I didn't adopt. The design note in a foreign colour script changed nothing about my architecture – it only made me recompute my own formula. Result: it computes correctly, but it is named for behaviour it doesn't have. Copying would never have told me that.
Comments in other people's code are experience for free. A single line of warning about a state in which Siril stays single-channel saved me half an evening of debugging. Somebody had already paid for that evening and written down what they learned.
A no is a result as well. A good dozen scripts had nothing for me, and four good ideas I deliberately didn't adopt – per-night calibration, distortion correction, the file watcher, computing the Ha content.
Those are now decisions with reasons instead of gaps I didn't know about.
By the end of the morning there wasn't a single feature I had come up with myself. In Texas the night was over by then, and the Sh2-129 data is waiting for a script that is a little better than it was the evening before – mostly because I spent the morning reading somebody else's source instead of my own.
I think I have now built the mono train script that is right for me. And that is exactly what is good about open source and the GPL 3: everything lies open, anyone can look inside, and out of that comes exchange and learning from one another.
Without other people's scripts this morning wouldn't have been possible – and of course my own script is available to anyone interested on GitHub.
🔭 What the script is actually for: Seven Filters, One Button → · All features: ImageMono Train in detail