diff options
| author | yum <yum.food.vr@gmail.com> | 2026-03-30 18:43:30 -0700 |
|---|---|---|
| committer | yum <yum.food.vr@gmail.com> | 2026-03-30 18:43:34 -0700 |
| commit | c7c1f7733eac8d28faed999ee0dc4796d16a6ade (patch) | |
| tree | 7b67125cef30f104ac7fefbafeb7eca82de4f5b4 /Scripts/gaussianize | |
| parent | 46265149d719c0ebb61b0b72d9884f8bb5a76f4b (diff) | |
Add heightmaps to burley tiling
Also add --homo flag to gaussianize. This "homogenizes" the input by
removing low frequency energy from the DFT of the image luma.
Diffstat (limited to 'Scripts/gaussianize')
| -rwxr-xr-x | Scripts/gaussianize | 61 |
1 files changed, 61 insertions, 0 deletions
diff --git a/Scripts/gaussianize b/Scripts/gaussianize index 2ca915d..620e05d 100755 --- a/Scripts/gaussianize +++ b/Scripts/gaussianize @@ -72,6 +72,58 @@ def save_image(image: np.ndarray, output_path: Path): raise ValueError(f"Unsupported output format '{output_path.suffix}'. Use .exr or .png.") +def rgb_to_ycrcb(image: np.ndarray) -> np.ndarray: + """Convert RGB in [0, 1] to full-range YCrCb in [0, 1].""" + rgb = np.clip(image, 0.0, 1.0) + r = rgb[:, :, 0] + g = rgb[:, :, 1] + b = rgb[:, :, 2] + + y = 0.299 * r + 0.587 * g + 0.114 * b + cb = 0.5 + (b - y) * 0.564 + cr = 0.5 + (r - y) * 0.713 + return np.stack((y, cr, cb), axis=-1) + + +def ycrcb_to_rgb(image: np.ndarray) -> np.ndarray: + """Convert full-range YCrCb in [0, 1] back to RGB in [0, 1].""" + ycrcb = np.asarray(image, dtype=np.float64) + y = ycrcb[:, :, 0] + cr = ycrcb[:, :, 1] - 0.5 + cb = ycrcb[:, :, 2] - 0.5 + + r = y + 1.402 * cr + g = y - 0.714136 * cr - 0.344136 * cb + b = y + 1.772 * cb + return np.clip(np.stack((r, g, b), axis=-1), 0.0, 1.0) + + +def subtract_low_frequencies(channel: np.ndarray, cutoff_cycles: float = 8.0) -> np.ndarray: + """Subtract a smooth periodic low-frequency component from a scalar image.""" + if cutoff_cycles <= 0.0: + raise ValueError(f"cutoff_cycles must be > 0, got {cutoff_cycles}") + + channel = np.asarray(channel, dtype=np.float64) + height, width = channel.shape + + fy = np.fft.fftfreq(height) + fx = np.fft.fftfreq(width) + radius = np.sqrt((fy[:, np.newaxis] * height) ** 2 + (fx[np.newaxis, :] * width) ** 2) + + low_mask = np.exp(-((radius / cutoff_cycles) ** 2)) + low_pass = np.fft.ifft2(np.fft.fft2(channel) * low_mask).real + + # Remove only the spatial trend and keep the original average luminance. + return channel - low_pass + float(np.mean(low_pass)) + + +def homogenize_luminance(image: np.ndarray, cutoff_cycles: float = 8.0) -> np.ndarray: + """Reduce very-low-frequency luminance variation while preserving chroma.""" + ycrcb = rgb_to_ycrcb(image) + ycrcb[:, :, 0] = subtract_low_frequencies(ycrcb[:, :, 0], cutoff_cycles=cutoff_cycles) + return ycrcb_to_rgb(ycrcb) + + class TruncatedGaussian: """Truncated Gaussian distribution for histogram transformation.""" @@ -450,6 +502,11 @@ def main(): action="store_true", help="Print detailed progress information" ) + parser.add_argument( + "--homo", + action="store_true", + help="Homogenize luminance before gaussianizing by removing very-low-frequency Y in YCrCb space" + ) args = parser.parse_args() @@ -467,6 +524,10 @@ def main(): image = load_image(args.input) quantization_step = 0.0 if args.input.suffix.lower() == ".exr" else (1.0 / 255.0) + if args.homo: + print("Homogenizing luminance in YCrCb space...") + image = homogenize_luminance(image) + # Gaussianize the texture print("Applying per-channel Gaussianization...") gaussianized, inverse_luts = gaussianize_texture( |
