113 lines
4.3 KiB
Python
113 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import numpy as np
|
|
import scipy.ndimage as ndimage
|
|
|
|
WIDTH = 300
|
|
SECTION_HEIGHT = 200
|
|
|
|
def dither2_diffuse(signal, diffusion_vector, factor=1):
|
|
dithered = np.copy(signal)
|
|
for i in range(len(dithered)):
|
|
dithered[i] = 0 if dithered[i] < 0.5 else 1
|
|
err = signal[i] - dithered[i]
|
|
num_neighbors = len(dithered) - i - 1
|
|
for j in range(min(num_neighbors, len(diffusion_vector))):
|
|
dithered[i+j+1] += err * diffusion_vector[j] * factor
|
|
return np.int8(dithered)
|
|
|
|
def dither2_noise(signal, make_noise):
|
|
noise = make_noise(len(signal))
|
|
return np.int8(signal + noise)
|
|
|
|
def white_noise(length):
|
|
return np.random.default_rng().random(length)
|
|
|
|
def blue_noise(initial_one_fraction=0.1, stdev=1.5):
|
|
def gaussian(arr):
|
|
return np.fft.ifft(ndimage.fourier_gaussian(np.fft.fft(arr), stdev))
|
|
|
|
def find_largest_void_idx(arr):
|
|
return np.argmax((1-arr) * (1-gaussian(arr)))
|
|
|
|
def find_tightest_cluster_idx(arr):
|
|
return np.argmax(arr * gaussian(arr))
|
|
|
|
def make(length):
|
|
if initial_one_fraction >= 0.5:
|
|
raise ValueError("initial_one_fraction must be less than 0.5")
|
|
|
|
# Initialize the binary pattern with white noise.
|
|
rng = np.random.default_rng()
|
|
ones = max(1, int(length * initial_one_fraction))
|
|
initial_binary_pattern = np.append(np.ones(ones), np.zeros(length-ones))
|
|
rng.shuffle(initial_binary_pattern)
|
|
|
|
# Break up clusters until the binary pattern is uniform enough.
|
|
converged = False
|
|
while (not converged):
|
|
# The minority pixels are ones.
|
|
tightest_cluster_idx = find_tightest_cluster_idx(initial_binary_pattern)
|
|
initial_binary_pattern[tightest_cluster_idx] = 0
|
|
largest_void_idx = find_largest_void_idx(initial_binary_pattern)
|
|
if tightest_cluster_idx == largest_void_idx:
|
|
initial_binary_pattern[tightest_cluster_idx] = 1
|
|
converged = True
|
|
else:
|
|
initial_binary_pattern[largest_void_idx] = 1
|
|
|
|
dither_arr = np.zeros(length)
|
|
|
|
# Phase I
|
|
prototype_binary_pattern = np.copy(initial_binary_pattern)
|
|
for rank in range(ones-1, -1, -1):
|
|
tightest_cluster_idx = find_tightest_cluster_idx(prototype_binary_pattern)
|
|
prototype_binary_pattern[tightest_cluster_idx] = 0
|
|
dither_arr[tightest_cluster_idx] = rank
|
|
|
|
# Phase II
|
|
np.copyto(prototype_binary_pattern, initial_binary_pattern)
|
|
rank = ones
|
|
for rank in range(ones, int((length+1)/2)):
|
|
largest_void_idx = find_largest_void_idx(prototype_binary_pattern)
|
|
prototype_binary_pattern[largest_void_idx] = 1
|
|
dither_arr[largest_void_idx] = rank
|
|
|
|
# Phase III
|
|
# Zeros are the minority pixel now, so invert the pattern to find
|
|
# clusters of them.
|
|
prototype_binary_pattern = 1-prototype_binary_pattern
|
|
for rank in range(int((length+1)/2), length):
|
|
tightest_cluster_idx = find_tightest_cluster_idx(prototype_binary_pattern)
|
|
prototype_binary_pattern[tightest_cluster_idx] = 0
|
|
dither_arr[tightest_cluster_idx] = rank
|
|
|
|
# Normalize to [0, length)
|
|
return dither_arr/length
|
|
return make
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import sys
|
|
stdout = open(sys.__stdout__.fileno(),
|
|
mode=sys.__stdout__.mode,
|
|
buffering=1,
|
|
encoding=sys.__stdout__.encoding,
|
|
errors=sys.__stdout__.errors,
|
|
newline='\n',
|
|
closefd=False)
|
|
|
|
gradient = np.arange(0, 1, 1/(WIDTH-1))
|
|
diffusion_vector = np.ones(7)
|
|
|
|
diffused = dither2_diffuse(gradient, diffusion_vector, 1/len(diffusion_vector))
|
|
white_noised = dither2_noise(gradient, white_noise)
|
|
blue_noised = dither2_noise(gradient, blue_noise(stdev=1.9))
|
|
|
|
sections = [diffused, white_noised, blue_noised]
|
|
print("P1", file=stdout)
|
|
print(WIDTH, SECTION_HEIGHT * len(sections), file=stdout)
|
|
for section in sections:
|
|
for _ in range(SECTION_HEIGHT):
|
|
print(*section, 1, file=stdout)
|