From cebba301bb8f43b6c1b20f2caba3aec467c37705 Mon Sep 17 00:00:00 2001 From: thetalkingdrum <254868117+thetalkingdrum@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:48:05 +0100 Subject: [PATCH] fix(fade): suggest Remove Stain on dim scans and mild stains The suggestion missed heavily stained frames whose white sat just over 0.1 D under full scale, and mild stains under the 0.2 D gap. The white may now sit 0.2 D under full scale, and blue must be at least 0.08 D below both red and green. A smaller gap from the brightest channel alone would also flag red casts, where blue sits just under red. Survey of 138 frames: all 10 new flags are in the stained set; none in the Toppenhaug or FADED_SLIDES folders; no frame lost its flag. --- negpy/features/process/fade.py | 12 ++++++------ tests/test_fade_white_anchor.py | 8 ++++++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/negpy/features/process/fade.py b/negpy/features/process/fade.py index ab05af34b..0b578bfb7 100644 --- a/negpy/features/process/fade.py +++ b/negpy/features/process/fade.py @@ -38,9 +38,9 @@ #: Percentile of the analysis grid that Remove Stain reads as each channel's white. WHITE_PCT = 99.9 #: To suggest Remove Stain, the brightest channel's white is within this density of full scale. -STAIN_FULL_SCALE_TOLERANCE = 0.1 -#: To suggest Remove Stain, blue is the darkest channel at the white, by at least this density. -STAIN_BLUE_MIN = 0.2 +STAIN_FULL_SCALE_TOLERANCE = 0.2 +#: To suggest Remove Stain, blue's white is at least this density below both red's and green's. +STAIN_BLUE_MIN = 0.08 #: Remove Stain puts the white at least this log density below full scale, so clear areas do not clip. FADE_WHITE_HEADROOM = 0.1 @@ -167,9 +167,9 @@ def white_anchored_grid(grid: ImageBuffer, white: tuple[float, float, float]) -> def stain_suggested(white: tuple[float, float, float]) -> bool: """Whether the white reads as a yellow stain on clear film: the brightest channel near full - scale, and blue clearly the darkest. A dim scan with no white object never qualifies.""" - brightest = max(white) - return brightest >= -STAIN_FULL_SCALE_TOLERANCE and white[2] <= min(white[0], white[1]) and brightest - white[2] >= STAIN_BLUE_MIN + scale, and blue below both red and green. A red cast leaves blue near green, and a dim scan + with no white object never qualifies.""" + return max(white) >= -STAIN_FULL_SCALE_TOLERANCE and min(white[0], white[1]) - white[2] >= STAIN_BLUE_MIN class EstimateMethod(StrEnum): diff --git a/tests/test_fade_white_anchor.py b/tests/test_fade_white_anchor.py index b8d88b30b..7843b0c97 100644 --- a/tests/test_fade_white_anchor.py +++ b/tests/test_fade_white_anchor.py @@ -249,10 +249,14 @@ def test_the_anchored_estimate_reads_the_true_survivals_through_a_stain(): [ ((-0.05, -0.18, -0.73), True), # NORAD 106-25: bright, heavy yellow stain ((-0.04, -0.16, -0.28), True), # NORAD 106-5: blue below red by more than the minimum + ((-0.14, -0.12, -0.49), True), # a heavy stain on a scan placed under full scale + ((-0.01, -0.04, -0.16), True), # a mild stain on a bright white ((-0.76, -1.05, -1.05), False), # SilverFast at gamma 2.2: no white near full scale ((-2.9, -3.4, -3.25), False), # a scan with no white object ((-0.25, -0.12, -0.01), False), # bluish whites: a snow or sky frame - ((0.0, -0.05, -0.15), False), # a stain under the minimum + ((0.0, -0.05, -0.10), False), # a stain under the minimum + ((-0.09, -0.02, -0.13), False), # a red cast: blue barely below red + ((-0.25, -0.30, -0.60), False), # a stain on a white too far under full scale ((0.0, -0.4, -0.3), False), # green, not blue, is darkest ], ) @@ -261,7 +265,7 @@ def test_stain_suggestion_rule(white, expected): def test_stain_suggestion_thresholds_are_the_documented_ones(): - assert (STAIN_FULL_SCALE_TOLERANCE, STAIN_BLUE_MIN) == (0.1, 0.2) + assert (STAIN_FULL_SCALE_TOLERANCE, STAIN_BLUE_MIN) == (0.2, 0.08) @unittest.skipUnless(GPUDevice.get().is_available, "GPU not available")