V 2.0.0 - Rasterix - new dithering

This commit is contained in:
DESKTOP-TVBJISQ\Primere
2026-03-24 18:41:10 +01:00
parent ad6bcc50fa
commit 5051565d2b
+29 -25
View File
@@ -2,7 +2,6 @@ import numpy as np
from PIL import Image
from numpy.lib.stride_tricks import sliding_window_view
def _adaptive_dither_amplitude(scale: float, adaptive: bool, max_val: float) -> float:
"""
Return dither amplitude in output-code units (LSB of 8-bit domain).
@@ -102,28 +101,15 @@ def _normalize_gaps_legacy(
max_val: float = 255.0,
) -> np.ndarray:
"""
Anti-comb filter — TPDF gap dithering.
Anti-comb filter — TPDF gap dithering (moved from img_levels_auto).
Fills quantization gaps from non-integer stretch scale factors.
At 16-bit the gaps are far smaller (1/65535 vs 1/255) and largely
invisible, but dithering is still applied for completeness.
Amplitude formula is ratio-based so it works at any bit depth:
amplitude = max(1.0, (scale / 1.275) ^ 2.2) × (max_val / 255)
Args:
stretched : 2D float32 array
scale : stretch scale factor
rng_gap : np.random.Generator
max_val : 255.0 for 8-bit, 65535.0 for 16-bit
Returns:
Float32 array with gaps filled
"""
print('------------ 5 -------------------')
amplitude = max(1.0, (scale / 1.275) ** 2.2) * (max_val / 255.0)
half = amplitude / 2.0
noise = (rng_gap.uniform(-half, half, stretched.shape).astype(np.float32) +
rng_gap.uniform(-half, half, stretched.shape).astype(np.float32))
noise = (rng_gap.uniform(-half, half, stretched.shape).astype(np.float32) + rng_gap.uniform(-half, half, stretched.shape).astype(np.float32))
return np.clip(stretched + noise, 0.0, max_val)
@@ -143,16 +129,34 @@ def img_dithering(
if not (1 <= peak_width <= 10):
raise ValueError(f"peak_width must be 1–10, got {peak_width}")
print('------------ 2 -------------------')
arr_8f = np.array(image.convert("RGB"), dtype=np.float32)
max_val = 65535.0 if high_precision else 255.0
scale_factor = max_val / 255.0
arr = arr_8f * scale_factor if high_precision else arr_8f
# 1) Legacy anti-comb (must run first when enabled and scale is known)
if normalize_gaps_legacy and len(stretched_gaps_spike) > 0:
for ch in range(3):
# arr = _normalize_gaps_legacy(arr, float(scale), rng_gap, max_val)
arr[:, :, ch] = _normalize_gaps_legacy(stretched_gaps_spike[:, :, ch], scale_spike[:, :, ch], rng_gap_spike[:, :, ch], max_val)
# 1) Legacy anti-comb (TPDF gap dither from auto-levels stretch)
# - If lists were provided by img_levels_auto → use exact per-channel scale + RNG
# - Otherwise (auto-levels was off) → fallback to a useful non-specific scale
# estimated from the current image tonal span (exactly as you requested).
if normalize_gaps_legacy:
print('------------ 3 -------------------')
if len(stretched_gaps_spike) > 0 and len(scale_spike) > 0 and len(rng_gap_spike) > 0:
print('------------ 4a -------------------')
# Auto-levels was used → use its exact stretch parameters
for ch in range(3):
scale = scale_spike[ch]
rng_gap = rng_gap_spike[ch]
# Apply to the current image (post-gamma if auto_gamma was enabled).
# This is the cleanest logical placement now that the function lives here.
arr[:, :, ch] = _normalize_gaps_legacy(arr[:, :, ch], scale, rng_gap, max_val)
else:
print('------------ 4b -------------------')
# Auto-levels was OFF → run with useful non-specific parameters
local_scale = _estimate_global_scale(arr, max_val)
for ch in range(3):
rng_gap = np.random.default_rng(ch) # same seeding style as levels_auto
arr[:, :, ch] = _normalize_gaps_legacy(arr[:, :, ch], local_scale, rng_gap, max_val)
# 2) Mid-peak smoothing
if normalize_midpeaks:
@@ -166,7 +170,7 @@ def img_dithering(
else:
quant_input = arr
if dither_quantization:
local_scale = float(scale) if scale is not None else _estimate_global_scale(quant_input, max_val)
local_scale = _estimate_global_scale(quant_input, max_val)
amp = _adaptive_dither_amplitude(local_scale, adaptive_dither_strength, max_val)
quant_input = quant_input + _tpdf_noise(quant_input.shape, amp)
quantized = np.clip(np.rint(quant_input), 0, max_val)
@@ -174,4 +178,4 @@ def img_dithering(
out_8f = quantized * (255.0 / max_val) if high_precision else quantized
out_8 = np.clip(np.rint(out_8f), 0, 255).astype(np.uint8)
return Image.fromarray(out_8, mode="RGB")
return Image.fromarray(out_8, mode="RGB")