OpenICE IR removal method - #724
Merged
Merged
Conversation
Ports openICE, a byte-exact reverse-engineering of Nikon Scan's Digital ICE, as a second IR reconstruction alongside the existing chain. Selected by a Method combo in the IR Removal panel; default is unchanged. Where the current fill averages fine structure away, ICE rebuilds it: in log density, each detail band is added back only where it beats the infrared's own contrast at that scale, so the IR decides what is picture and what is dust. Three departures from the original, all forced by real scans: - Its level constants are absolute and tuned to a Coolscan under Nikon Scan. They do not transfer — on the LS-9000 sample every pixel clears its clear-film gate and no dust reaches its ramp. Clear-film level, ramp and dust floor are measured per frame from the gate's own distribution. - Added a confidence write ramp. ICE rebuilds any pixel under full confidence and floors at the original, which half-wave-rectifies IR noise into one-sided brightening; that mottled 25% of a clean frame. Clean film now comes out bit-identical. - Dropped the LCG dither (the finest band already restores real grain, and it is the only serial dependency), and added a per-component route cap so the film rebate cannot eat the inpaint budget. The two methods share no code: openice.py imports nothing from logic.py and the only join is one branch in _ir_bake, so whichever loses can be deleted whole. Verified against the openICE C reference built from src_C: confidences and gate pyramid match to float32 noise, give-up map agrees on 99.95%. On both samples/dust/ scans OpenICE leaves clean film 100.00% untouched against the current path's 99.83–99.98%, for comparable dust removal.
Contributor
|
Wow! |
Two constants in the port had no counterpart in the C, and both cost small dust its repair. The write ramp scaled the output by confidence. ICE writes max(L3, acc) at full strength for every pixel under full confidence; the ramp looked like cheap insurance against mottling, but dust only reaches the weight floor once it is deep, so a shallow speck lost most of its lift — 81% at a 15-25% dip. The sigma-scaled margin already gates the noise the ramp was meant to stop: removing it leaves clean-film activity at 1e-4, well under the repair itself. The give-up dust floor was anchored to noise sigma, about ten times shallower than ICE's D(0.065*M). That fired the trigger on ordinary deep dust and routed it away from the reconstruction that should have repaired it. Carried over as the transmittance fraction ICE's constant encodes — a defect passing under 6.5% of clear film — measured against this frame's clear-film reference, so it transfers between scanners the way the absolute level does not. Small-speck coverage rises across the sample scans (a 15-25% IR dip goes from 26-31% repaired to 34-45%); the film-border transition is correspondingly more strongly rewritten, since the trigger no longer shields it. Also verified the rest of the port against src_C and found no further drift: pyramid supports and their 1/69, 1/21, 1/16 normalisation, the crosstalk regression and reference install, gate bias, band coefficients and stage gains, the give-up probe geometry, and the zero-pad/edge-replicate split.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ports openICE, a reverse-engineering of Nikon Scan's Digital ICE verified byte-exact against the original engine, as a second IR reconstruction alongside the existing one. Both are GPL-3.0, so no licensing friction.
Selected by a new Method combo at the top of the IR Removal panel. Default is unchanged — this ships as a bake-off, not a replacement.
Why
Where the current fill averages fine structure away and pastes borrowed grain over it, ICE rebuilds the picture: working in log density, each detail band is added back only where it beats the infrared's own contrast at that same scale, so the IR channel itself decides what is photograph and what is dust.
What had to change
The algorithm ports cleanly; its constants do not.
The level constants are absolute and tuned to a Coolscan under Nikon Scan. On the LS-9000 frame in
samples/dust/, every pixel clears ICE's clear-film gate (τ=8847) — so its crosstalk fit would treat dust as film — and its ramp saturates at a 15% IR loss the deepest dust here (−11%) never reaches. Clear-film level, ramp and dust floor are now measured per frame from the gate's own distribution. openICE's docs concede this for flatbeds; it also bites a genuine Coolscan driven by different software.Added a confidence write ramp, which ICE has no equivalent of. It rebuilds any pixel under full confidence and floors the result at the original, which half-wave-rectifies IR noise into one-sided brightening. That mottled 25% of a synthetic clean frame. With the ramp, clean film comes out bit-identical.
Dropped the LCG dither (the finest band already restores real grain, and it was the only serial dependency in an otherwise per-pixel algorithm) and added a per-component route cap — the film rebate sits above the dead floor on some scanners and arrived as one component covering 1.2% of a flatbed frame, blowing the inpaint budget and taking the real dust with it.
Isolation
openice.pyimports nothing fromlogic.py; the only join is one branch in_ir_bakeplus the config field and its sidebar row. Constants that look duplicated are duplicated deliberately. Whichever method loses can be deleted as one file and one branch — noted in CLAUDE.md so it survives contact with future refactors.Verification
src_C, 64-bit): confidences and gate pyramid match to float32 noise across all four scales; give-up map agrees on 99.95%. This caught a real omission — I had dropped ICE'sθgate bias as negligible and it showed up as a clean 1.0000 offset, so it went back in.samples/dust/scans: OpenICE leaves clean film 100.00% untouched (current path: 99.83–99.98%) with a 4.7× smaller mean change, for comparable dust removal.tests/test_ir_openice.py; the existing 60-test IR suite untouched.make allgreen (3010 passed).Note for a follow-up
The existing NegPy path logs
routed defects cover 2.2% of the frame — inpaint skippedon the flatbed sample for the same rebate reason fixed here. Left alone to keep the methods isolated, but it is a real bug in the default path.