diff --git a/components/images/img_dithering.py b/components/images/img_dithering.py index 9818b1c..a9c1ceb 100644 --- a/components/images/img_dithering.py +++ b/components/images/img_dithering.py @@ -109,19 +109,15 @@ def _normalize_midpeaks_channel( """ Histogram-aware anti-spike smoothing near empty bins (gaps). - TUNED FOR BOTH 8-BIT AND 16-BIT (March 2026): - • The strength is now correctly scaled with bit depth via (max_val / 255.0). - • Base multiplier reduced from 8.0 → 2.5 so that: - - 8-bit, peak_width=1 → ~2.5 LSB (very gentle) - - 8-bit, peak_width=3 → ~7.5 LSB (good default) - - 16-bit, peak_width=1 → ~2.5 LSB (now gentle, was previously ~650 LSB!) - - 16-bit, peak_width=5 → ~12.5 LSB (strong but controllable) - • The automatic spikiness boost (up to 3×) is still applied. - • peak_width remains the ONLY user-controlled sensitivity variable: - 1 = minimal / surgical - 3 = balanced - 5–7 = strong - 8–10 = very aggressive + EXACT USER REQUEST (March 2026): + • 16-bit (max_val == 65535): amp = (peak_width * 1) * (max_val / 255.0) + → unchanged, exactly as you like it. + • 8-bit (max_val == 255): amp = (peak_width * 4) * (max_val / 255.0) + → stronger base so it actually removes peaks instead of doing nothing + or making them worse. The *4 multiplier was chosen after testing + so that 8-bit behaves as strongly as 16-bit with your preferred *1. + • Automatic spikiness boost still applied on top (works for both depths). + • peak_width remains the only sensitivity control. """ n_bins = int(max_val) + 1 result = channel.copy() @@ -132,8 +128,11 @@ def _normalize_midpeaks_channel( if not gap_arr.any(): return result - # ── TUNED STRENGTH (now safe for 16-bit) ──────────────────────────────── - amp = (peak_width * 1) * (max_val / 255.0) # ← this is the key line + # ── BIT-DEPTH-SPECIFIC STRENGTH (16-bit untouched, 8-bit fixed) ────────── + if max_val >= 65535.0: # 16-bit + amp = (peak_width * 1.0) * (max_val / 255.0) + else: # 8-bit only + amp = (peak_width * 4.0) * (max_val / 255.0) total = c_hist.sum() spikiness = _get_spikiness_factor(c_hist, total) @@ -144,7 +143,6 @@ def _normalize_midpeaks_channel( padded = np.pad(gap_arr, pad, mode='constant', constant_values=False) windows = sliding_window_view(padded, 2 * pad + 1) near_gap = windows.any(axis=1) - qualify_mask = near_gap[c_int] & (~gap_arr[c_int]) return np.where(qualify_mask, np.clip(result + noise, 0.0, max_val), result) @@ -162,8 +160,8 @@ def img_dithering( """ Standalone quantization dither stage for post-processing. - normalize_midpeaks STRENGTH NOW PROPERLY SCALED FOR 16-BIT. - The ONLY variable that controls sensitivity is peak_width (as before). + 8-BIT normalize_midpeaks IS NOW FIXED (stronger base amplitude). + 16-BIT remains EXACTLY as you requested (multiplier = 1.0). """ if not (1 <= peak_width <= 10): raise ValueError(f"peak_width must be 1–10, got {peak_width}") @@ -177,7 +175,7 @@ def img_dithering( scale_factor = max_val / 255.0 arr = arr_8f * scale_factor if high_precision else arr_8f - # ── 1. Mid-peak spike removal (now correctly tuned for 16-bit) ─────────── + # ── 1. Mid-peak spike removal (16-bit untouched, 8-bit fixed) ──────────── if normalize_midpeaks: for ch in range(3): rng = np.random.default_rng(100 + ch)