156 lines
5.5 KiB
Python
Executable File
156 lines
5.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import numpy as np
|
|
from PIL import Image
|
|
import argparse
|
|
from pathlib import Path
|
|
|
|
def gaussian_kernel(size, sigma):
|
|
x = np.arange(-size // 2 + 1, size // 2 + 1)
|
|
y = np.arange(-size // 2 + 1, size // 2 + 1)
|
|
xx, yy = np.meshgrid(x, y)
|
|
kernel = np.exp(-(xx**2 + yy**2) / (2 * sigma**2))
|
|
return kernel / kernel.sum()
|
|
|
|
def apply_periodic_filter(binary_pattern, kernel):
|
|
from scipy.signal import fftconvolve
|
|
return fftconvolve(binary_pattern, kernel, mode='same')
|
|
|
|
def generate_blue_noise_void_cluster(width, height, sigma=1.5, iterations=None):
|
|
if iterations is None:
|
|
iterations = width * height
|
|
|
|
size = width * height
|
|
pattern = np.zeros((height, width), dtype=np.float32)
|
|
|
|
kernel_size = int(6 * sigma)
|
|
if kernel_size % 2 == 0:
|
|
kernel_size += 1
|
|
kernel = gaussian_kernel(kernel_size, sigma)
|
|
|
|
initial_pattern = np.random.rand(height, width)
|
|
|
|
print(f"Generating {width}x{height} blue noise texture...")
|
|
print(f"Kernel size: {kernel_size}x{kernel_size}, sigma: {sigma}")
|
|
|
|
dither_array = np.zeros(size, dtype=np.int32)
|
|
binary_pattern = np.zeros((height, width), dtype=np.float32)
|
|
|
|
for i in range(size):
|
|
if i % (size // 10) == 0:
|
|
print(f"Progress: {i}/{size} ({100*i//size}%)")
|
|
|
|
filtered = apply_periodic_filter(binary_pattern, kernel)
|
|
|
|
if i < size // 2:
|
|
initial_energy = initial_pattern + filtered
|
|
coords = np.unravel_index(np.argmax(initial_energy), initial_energy.shape)
|
|
else:
|
|
coords = np.unravel_index(np.argmin(filtered), filtered.shape)
|
|
|
|
dither_array[i] = coords[0] * width + coords[1]
|
|
binary_pattern[coords[0], coords[1]] = 1.0
|
|
|
|
print("Converting to threshold map...")
|
|
|
|
threshold_map = np.zeros((height, width), dtype=np.float32)
|
|
for rank, pos in enumerate(dither_array):
|
|
y = pos // width
|
|
x = pos % width
|
|
threshold_map[y, x] = rank / size
|
|
|
|
print("Done!")
|
|
return threshold_map
|
|
|
|
def generate_blue_noise_simulated_annealing(width, height, iterations=10000):
|
|
print(f"Generating {width}x{height} blue noise using simulated annealing...")
|
|
|
|
pattern = np.random.rand(height, width)
|
|
|
|
def energy(pattern):
|
|
fft = np.fft.fft2(pattern)
|
|
power = np.abs(fft) ** 2
|
|
|
|
h, w = pattern.shape
|
|
cy, cx = h // 2, w // 2
|
|
y, x = np.ogrid[:h, :w]
|
|
dist = np.sqrt((x - cx)**2 + (y - cy)**2)
|
|
|
|
low_freq_mask = dist < min(h, w) * 0.1
|
|
low_freq_energy = np.sum(power * low_freq_mask)
|
|
|
|
return low_freq_energy
|
|
|
|
current_energy = energy(pattern)
|
|
temperature = 1.0
|
|
cooling_rate = 0.9995
|
|
|
|
for i in range(iterations):
|
|
if i % (iterations // 10) == 0:
|
|
print(f"Iteration {i}/{iterations}, Energy: {current_energy:.2f}, Temp: {temperature:.4f}")
|
|
|
|
y1, x1 = np.random.randint(0, height), np.random.randint(0, width)
|
|
y2, x2 = np.random.randint(0, height), np.random.randint(0, width)
|
|
|
|
pattern[y1, x1], pattern[y2, x2] = pattern[y2, x2], pattern[y1, x1]
|
|
|
|
new_energy = energy(pattern)
|
|
delta_energy = new_energy - current_energy
|
|
|
|
if delta_energy < 0 or np.random.rand() < np.exp(-delta_energy / temperature):
|
|
current_energy = new_energy
|
|
else:
|
|
pattern[y1, x1], pattern[y2, x2] = pattern[y2, x2], pattern[y1, x1]
|
|
|
|
temperature *= cooling_rate
|
|
|
|
print("Done!")
|
|
return pattern
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='Generate blue noise texture for dithering')
|
|
parser.add_argument('--width', type=int, default=128, help='Texture width (default: 128)')
|
|
parser.add_argument('--height', type=int, default=128, help='Texture height (default: 128)')
|
|
parser.add_argument('--method', choices=['void_cluster', 'annealing'], default='void_cluster',
|
|
help='Generation method (default: void_cluster)')
|
|
parser.add_argument('--sigma', type=float, default=1.5,
|
|
help='Gaussian kernel sigma for void_cluster method (default: 1.5)')
|
|
parser.add_argument('--iterations', type=int, default=None,
|
|
help='Number of iterations (optional)')
|
|
parser.add_argument('--output', type=str, default='../blue_noise.png',
|
|
help='Output file path (default: ../blue_noise.png)')
|
|
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
from scipy.signal import fftconvolve
|
|
except ImportError:
|
|
print("Error: scipy is required for this script.")
|
|
print("Install it with: pip install scipy")
|
|
return
|
|
|
|
if args.method == 'void_cluster':
|
|
noise = generate_blue_noise_void_cluster(args.width, args.height, args.sigma, args.iterations)
|
|
else:
|
|
noise = generate_blue_noise_simulated_annealing(args.width, args.height,
|
|
args.iterations or 10000)
|
|
|
|
noise_normalized = ((noise - noise.min()) / (noise.max() - noise.min()) * 255).astype(np.uint8)
|
|
|
|
img = Image.fromarray(noise_normalized, mode='L')
|
|
|
|
output_path = Path(__file__).parent / args.output
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
img.save(output_path)
|
|
|
|
print(f"\nBlue noise texture saved to: {output_path}")
|
|
print(f"Size: {args.width}x{args.height}")
|
|
print(f"Method: {args.method}")
|
|
|
|
fft = np.fft.fft2(noise)
|
|
power_spectrum = np.abs(np.fft.fftshift(fft)) ** 2
|
|
print(f"Power spectrum range: {power_spectrum.min():.2e} - {power_spectrum.max():.2e}")
|
|
|
|
if __name__ == '__main__':
|
|
main()
|