V 2.0.0 - Rasterix - new dithering
This commit is contained in:
+5
-8
@@ -2250,11 +2250,11 @@ class PrimereRasterix:
|
||||
"skip_if_no_clip": ("BOOLEAN", {"default": False, "label_off": "Offset all values", "label_on": "Skip if no clips"}),
|
||||
|
||||
"normalize_gaps": ("BOOLEAN", {"default": False, "label_on": "Anti-comb filter: ON", "label_off": "Anti-comb filter: OFF"}),
|
||||
"dither_quantization": ("BOOLEAN", {"default": False, "label_off": "Dither quantization OFF", "label_on": "Dither quantization ON"}),
|
||||
"adaptive_dither_strength": ("BOOLEAN", {"default": False, "label_off": "Keep dither strength", "label_on": "Increase dither strength"}),
|
||||
"error_diffusion": ("BOOLEAN", {"default": False, "label_off": "Ignore endpoint offset", "label_on": "Apply endpoint offset"}),
|
||||
"normalize_midpeaks": ("BOOLEAN", {"default": False, "label_on": "Anti-spike filter: ON", "label_off": "Anti-spike filter: OFF"}),
|
||||
"peak_width": ("INT", {"default": 3, "min": 1, "max": 10, "step": 1}),
|
||||
"dither_quantization": ("BOOLEAN", {"default": False, "label_off": "Dither quantization OFF", "label_on": "Dither quantization ON"}),
|
||||
"adaptive_dither_strength": ("BOOLEAN", {"default": False, "label_off": "Keep dither strength", "label_on": "Increase dither strength"}),
|
||||
"error_diffusion": ("BOOLEAN", {"default": False, "label_off": "Error diffusion OFF", "label_on": "Error diffusion ON"}),
|
||||
|
||||
"use_ai_detection_bypasser": ("BOOLEAN", {"default": False, "label_off": "AI detection bypass off", "label_on": "AI detection bypass on"}),
|
||||
"adb_freq_strength": ("FLOAT", {"default": 0.019, "min": 0.0, "max": 0.1, "step": 0.001}),
|
||||
@@ -2279,12 +2279,9 @@ class PrimereRasterix:
|
||||
|
||||
rasterix_json_path = os.path.join(PRIMERE_ROOT, 'front_end', 'rasterix.json')
|
||||
rasterix_data = utility.json2tuple(rasterix_json_path) or {}
|
||||
stretched_gaps_spike = []
|
||||
scale_spike = []
|
||||
rng_gap_spike = []
|
||||
|
||||
if auto_normalize:
|
||||
pil_img = img_levels_auto.img_levels_auto(image=pil_img, auto_normalize=auto_normalize, threshold=auto_levels_threshold, auto_gamma=auto_gamma, gamma_target=gamma_target, precision=precision)
|
||||
pil_img = img_levels_auto.img_levels_auto(image=pil_img, auto_normalize=auto_normalize, threshold=auto_levels_threshold, normalize_gaps=normalize_gaps, normalize_midpeaks=False, peak_width=peak_width, auto_gamma=auto_gamma, gamma_target=gamma_target, precision=precision)
|
||||
|
||||
if use_white_balance and (wb_temperature != 6500 or wb_tint != 0):
|
||||
pil_img = img_white_balance.img_white_balance(image=pil_img, temperature=wb_temperature, tint=wb_tint)
|
||||
@@ -2325,7 +2322,7 @@ class PrimereRasterix:
|
||||
pil_img = img_levels_compress.img_levels_compress(image=pil_img, black_offset=black_offset, white_offset=white_offset, skip_if_no_clip=skip_if_no_clip, high_precision=precision)
|
||||
|
||||
if dither_quantization or error_diffusion or normalize_midpeaks:
|
||||
pil_img = img_dithering.img_dithering(image=pil_img, normalize_gaps_legacy=normalize_gaps, stretched_gaps_spike=stretched_gaps_spike, scale_spike=scale_spike, rng_gap_spike=rng_gap_spike, dither_quantization=dither_quantization, adaptive_dither_strength=adaptive_dither_strength, error_diffusion=error_diffusion, normalize_midpeaks=normalize_midpeaks, peak_width=peak_width, high_precision=precision)
|
||||
pil_img = img_dithering.img_dithering(image=pil_img, dither_quantization=dither_quantization, adaptive_dither_strength=adaptive_dither_strength, error_diffusion=error_diffusion, normalize_midpeaks=normalize_midpeaks, peak_width=peak_width, high_precision=precision)
|
||||
|
||||
if use_ai_detection_bypasser:
|
||||
pil_img = isgen_detect_ext_full.bypass_ai_detector(image=pil_img, freq_strength=adb_freq_strength, variance_strength=adb_variance_strength, unsharp_percent=adb_unsharp_percent, jpeg_cycles=adb_jpeg_cycles)
|
||||
|
||||
@@ -2,6 +2,7 @@ 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).
|
||||
@@ -94,31 +95,8 @@ def _normalize_midpeaks_channel(
|
||||
return np.where(qualify_mask, np.clip(result + noise, 0.0, max_val), result)
|
||||
|
||||
|
||||
def _normalize_gaps_legacy(
|
||||
stretched: np.ndarray,
|
||||
scale: float,
|
||||
rng_gap: np.random.Generator,
|
||||
max_val: float = 255.0,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Anti-comb filter — TPDF gap dithering (moved from img_levels_auto).
|
||||
|
||||
Fills quantization gaps from non-integer stretch scale factors.
|
||||
"""
|
||||
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))
|
||||
|
||||
return np.clip(stretched + noise, 0.0, max_val)
|
||||
|
||||
|
||||
def img_dithering(
|
||||
image: Image.Image,
|
||||
normalize_gaps_legacy: bool = False,
|
||||
stretched_gaps_spike: list = [],
|
||||
scale_spike: list = [],
|
||||
rng_gap_spike: list = [],
|
||||
dither_quantization: bool = True,
|
||||
adaptive_dither_strength: bool = True,
|
||||
error_diffusion: bool = False,
|
||||
@@ -126,56 +104,45 @@ def img_dithering(
|
||||
peak_width: int = 3,
|
||||
high_precision: bool = False,
|
||||
) -> Image.Image:
|
||||
"""
|
||||
Standalone quantization dither stage for post-processing.
|
||||
|
||||
Args:
|
||||
image: PIL Image (RGB)
|
||||
dither_quantization: Apply TPDF dither before rounding.
|
||||
adaptive_dither_strength: Adapt dither amount to current tonal span.
|
||||
error_diffusion: Use Floyd-Steinberg quantization path.
|
||||
high_precision: False = process in 8-bit domain (0..255),
|
||||
True = process in 16-bit domain (0..65535),
|
||||
then convert back to 8-bit RGB output.
|
||||
normalize_midpeaks: Alternative/extra anti-spike smoothing near
|
||||
histogram gaps before final quantization.
|
||||
peak_width: 1..10 neighborhood used by normalize_midpeaks.
|
||||
"""
|
||||
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 (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:
|
||||
for ch in range(3):
|
||||
rng = np.random.default_rng(100 + ch)
|
||||
arr[:, :, ch] = _normalize_midpeaks_channel(arr[:, :, ch], peak_width, max_val, rng)
|
||||
|
||||
# 3) Quantization path
|
||||
if error_diffusion:
|
||||
quantized = _floyd_steinberg_quantize(arr, max_val)
|
||||
else:
|
||||
quant_input = arr
|
||||
if dither_quantization:
|
||||
local_scale = _estimate_global_scale(quant_input, max_val)
|
||||
amp = _adaptive_dither_amplitude(local_scale, adaptive_dither_strength, max_val)
|
||||
scale = _estimate_global_scale(quant_input, max_val)
|
||||
amp = _adaptive_dither_amplitude(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)
|
||||
|
||||
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")
|
||||
|
||||
@@ -130,6 +130,73 @@ def levels_edge_spread(
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def levels_normalize_midpeaks(
|
||||
stretched: np.ndarray,
|
||||
peak_width: int,
|
||||
rng_spike: np.random.Generator,
|
||||
max_val: float = 255.0,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Anti-spike filter — smooths histogram bins near quantization gaps.
|
||||
|
||||
A bin qualifies as a peak if it has at least one zero bin within
|
||||
peak_width positions. Targeted TPDF dithering is applied to qualifying
|
||||
pixels using a single pre-generated noise field (not per-bin), making
|
||||
the operation O(1) in the number of qualifying bins.
|
||||
|
||||
Args:
|
||||
stretched : 2D float32 array after edge spread
|
||||
peak_width : 1–10, distance from a gap that qualifies a bin
|
||||
rng_spike : np.random.Generator (independent from gap dithering)
|
||||
max_val : 255.0 for 8-bit, 65535.0 for 16-bit
|
||||
|
||||
Returns:
|
||||
Float32 array with peak bins redistributed
|
||||
"""
|
||||
n_bins = int(max_val) + 1
|
||||
result = stretched.copy()
|
||||
s_int = np.clip(np.round(result).astype(np.int64), 0, int(max_val))
|
||||
s_hist = np.bincount(s_int.ravel(), minlength=n_bins).astype(np.float64)
|
||||
|
||||
# Mid-range: exclude edge-spread zones (bins 0–edge and max-edge–max)
|
||||
edge_bins = int(EDGE_SPREAD_RATIO * max_val) + 1
|
||||
lo = edge_bins
|
||||
hi = n_bins - edge_bins
|
||||
|
||||
gap_bins = set(int(b) for b in range(lo, hi) if s_hist[b] == 0)
|
||||
if not gap_bins:
|
||||
return result
|
||||
|
||||
amp = (peak_width / 2.0) * (max_val / 255.0) # scale amplitude with bit depth
|
||||
half = amp / 2.0
|
||||
|
||||
# Generate noise once for the full channel
|
||||
noise = (rng_spike.uniform(-half, half, result.shape).astype(np.float32) +
|
||||
rng_spike.uniform(-half, half, result.shape).astype(np.float32))
|
||||
|
||||
# Vectorized near-gap detection via sliding window
|
||||
gap_arr = np.zeros(n_bins, dtype=bool)
|
||||
for g in gap_bins:
|
||||
gap_arr[g] = True
|
||||
|
||||
from numpy.lib.stride_tricks import sliding_window_view
|
||||
pad = peak_width
|
||||
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) # shape (n_bins,)
|
||||
|
||||
# Build mask: all pixels in qualifying non-gap bins
|
||||
qualify_mask = np.zeros(result.shape, dtype=bool)
|
||||
for b in range(lo, hi):
|
||||
if s_hist[b] == 0 or not near_gap[b]:
|
||||
continue
|
||||
qualify_mask |= (s_int == b)
|
||||
|
||||
result = np.where(qualify_mask, np.clip(result + noise, 0.0, max_val), result)
|
||||
return result
|
||||
|
||||
|
||||
def levels_normalize_gaps(
|
||||
stretched: np.ndarray,
|
||||
scale: float,
|
||||
@@ -157,8 +224,7 @@ def levels_normalize_gaps(
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
@@ -208,12 +274,12 @@ def levels_auto_gamma(
|
||||
|
||||
def img_levels_auto(
|
||||
image: Image.Image,
|
||||
auto_normalize: bool = False,
|
||||
auto_normalize: bool = True,
|
||||
threshold: float = 2.0,
|
||||
# normalize_gaps: bool = False,
|
||||
# normalize_midpeaks: bool = False,
|
||||
# peak_width: int = 3,
|
||||
auto_gamma: bool = False,
|
||||
normalize_gaps: bool = True,
|
||||
normalize_midpeaks: bool = False,
|
||||
peak_width: int = 3,
|
||||
auto_gamma: bool = True,
|
||||
gamma_target: float = 128.0,
|
||||
precision: bool = False,
|
||||
) -> Image.Image:
|
||||
@@ -273,9 +339,6 @@ def img_levels_auto(
|
||||
6. levels_auto_gamma — gamma correction (if auto_gamma)
|
||||
"""
|
||||
img = image.convert("RGB")
|
||||
stretched_gaps_spike = []
|
||||
scale_spike = []
|
||||
rng_gap_spike = []
|
||||
|
||||
if not auto_normalize:
|
||||
return img
|
||||
@@ -284,8 +347,8 @@ def img_levels_auto(
|
||||
raise ValueError(f"threshold must be 0.0–100.0, got {threshold}")
|
||||
if not (0.0 <= gamma_target <= 255.0):
|
||||
raise ValueError(f"gamma_target must be 0–255, got {gamma_target}")
|
||||
# if not (1 <= peak_width <= 10):
|
||||
# raise ValueError(f"peak_width must be 1–10, got {peak_width}")
|
||||
if not (1 <= peak_width <= 10):
|
||||
raise ValueError(f"peak_width must be 1–10, got {peak_width}")
|
||||
|
||||
# ── Bit depth configuration ───────────────────────────────────────────────
|
||||
max_val = 65535.0 if precision else 255.0
|
||||
@@ -302,7 +365,7 @@ def img_levels_auto(
|
||||
|
||||
for ch in range(3):
|
||||
rng_gap = np.random.default_rng(ch)
|
||||
# rng_spike = np.random.default_rng(ch + 100)
|
||||
rng_spike = np.random.default_rng(ch + 100)
|
||||
|
||||
channel = arr[:, :, ch]
|
||||
|
||||
@@ -316,15 +379,12 @@ def img_levels_auto(
|
||||
stretched = levels_edge_spread(channel, stretched, black_point, white_point, max_val)
|
||||
|
||||
# 4. Peak smoothing (before gap dithering)
|
||||
# if normalize_midpeaks:
|
||||
# stretched = levels_normalize_midpeaks(stretched, peak_width, rng_spike, max_val)
|
||||
if normalize_midpeaks:
|
||||
stretched = levels_normalize_midpeaks(stretched, peak_width, rng_spike, max_val)
|
||||
|
||||
# 5. Gap dithering
|
||||
''' if normalize_gaps:
|
||||
if normalize_gaps:
|
||||
stretched = levels_normalize_gaps(stretched, scale, rng_gap, max_val)
|
||||
stretched_gaps_spike.append(stretched)
|
||||
scale_spike.append(scale)
|
||||
rng_gap_spike.append(rng_gap) '''
|
||||
|
||||
# 6. Auto gamma
|
||||
if auto_gamma:
|
||||
|
||||
Reference in New Issue
Block a user