234 lines
8.1 KiB
Python
234 lines
8.1 KiB
Python
#%%
|
|
import numpy as np
|
|
import cv2
|
|
|
|
def merge_intervals(intervals):
|
|
# Merge overlapping intervals.
|
|
if not intervals:
|
|
return []
|
|
intervals.sort(key=lambda x: x[0])
|
|
merged = [intervals[0]]
|
|
for current in intervals[1:]:
|
|
last = merged[-1]
|
|
if current[0] <= last[1]:
|
|
merged[-1] = (last[0], max(last[1], current[1]))
|
|
else:
|
|
merged.append(current)
|
|
return merged
|
|
|
|
def get_allowed_intervals(neighbor_values, delta):
|
|
# Compute forbidden intervals for this channel from neighbor values.
|
|
forbidden = []
|
|
for c in neighbor_values:
|
|
low = max(0, c - delta)
|
|
high = min(1, c + delta)
|
|
forbidden.append((low, high))
|
|
# Merge overlapping forbidden intervals.
|
|
merged = merge_intervals(forbidden)
|
|
allowed = []
|
|
# Allowed region before the first forbidden interval.
|
|
if merged:
|
|
if merged[0][0] > 0:
|
|
allowed.append((0, merged[0][0]))
|
|
# Allowed regions between forbidden intervals.
|
|
for i in range(1, len(merged)):
|
|
allowed.append((merged[i-1][1], merged[i][0]))
|
|
# Allowed region after the last forbidden interval.
|
|
if merged[-1][1] < 1:
|
|
allowed.append((merged[-1][1], 1))
|
|
else:
|
|
allowed.append((0, 1))
|
|
return allowed
|
|
|
|
def sample_from_allowed(allowed):
|
|
# Choose a random value from the allowed intervals with probability proportional to interval length.
|
|
lengths = [end - start for start, end in allowed]
|
|
total_length = sum(lengths)
|
|
if total_length <= 0:
|
|
return None
|
|
point = np.random.rand() * total_length
|
|
cumulative = 0
|
|
for (start, end), length in zip(allowed, lengths):
|
|
cumulative += length
|
|
if point <= cumulative:
|
|
return np.random.uniform(start, end)
|
|
return allowed[-1][0]
|
|
|
|
def sample_channel(neighbor_values, delta):
|
|
# Compute allowed intervals and sample a value.
|
|
allowed = get_allowed_intervals(neighbor_values, delta)
|
|
if not allowed:
|
|
return None
|
|
return sample_from_allowed(allowed)
|
|
|
|
def clip_allowed_to_min(allowed, min_value):
|
|
clipped = []
|
|
for start, end in allowed:
|
|
start = max(start, min_value)
|
|
if end > start:
|
|
clipped.append((start, end))
|
|
return clipped
|
|
|
|
|
|
def generate_valid_gray(neighbor_grays, r, black_threshold):
|
|
delta = np.sqrt(r)
|
|
min_gray = min_gray_normalized(black_threshold)
|
|
allowed = clip_allowed_to_min(get_allowed_intervals(neighbor_grays, delta), min_gray)
|
|
if not allowed:
|
|
return np.random.uniform(min_gray, 1.0)
|
|
sampled = sample_from_allowed(allowed)
|
|
if sampled is None:
|
|
return np.random.uniform(min_gray, 1.0)
|
|
return enforce_min_gray(sampled, black_threshold)
|
|
|
|
|
|
def rgb_to_gray(color):
|
|
# ITU-R BT.601 luma, color channels in [0, 1].
|
|
r, g, b = color
|
|
return 0.299 * r + 0.587 * g + 0.114 * b
|
|
|
|
|
|
def min_gray_normalized(black_threshold):
|
|
# Strictly above black_threshold in uint8 space.
|
|
return (float(black_threshold) + 1.0) / 255.0
|
|
|
|
|
|
def gray_to_rgb(gray):
|
|
gray = float(gray)
|
|
return np.array([gray, gray, gray], dtype=np.float32)
|
|
|
|
|
|
def enforce_min_gray(gray, black_threshold):
|
|
return max(float(gray), min_gray_normalized(black_threshold))
|
|
|
|
|
|
def generate_valid_color(neighbors, r):
|
|
"""
|
|
Generate a random color that satisfies, for each neighbor,
|
|
|R - R_neighbor| >= sqrt(r), and similarly for G and B.
|
|
neighbors: list of neighbor colors (each is an array of shape (3,))
|
|
r: threshold for squared difference.
|
|
"""
|
|
delta = np.sqrt(r)
|
|
color = np.zeros(3)
|
|
for channel in range(3):
|
|
neighbor_channel = [n[channel] for n in neighbors]
|
|
sampled = sample_channel(neighbor_channel, delta)
|
|
# If the allowed intervals are empty, use a fallback.
|
|
if sampled is None:
|
|
sampled = np.random.rand()
|
|
color[channel] = sampled
|
|
return color
|
|
|
|
def fill_pattern_remainder(pattern, h, w, d, black_threshold):
|
|
"""Fill pixels not covered by full dxd patches (image size may not be divisible by d)."""
|
|
min_gray = min_gray_normalized(black_threshold)
|
|
filled_h = (h // d) * d
|
|
filled_w = (w // d) * d
|
|
|
|
if filled_h < h and filled_w > 0:
|
|
remainder_h = h - filled_h
|
|
source = pattern[filled_h - d:filled_h, :filled_w]
|
|
pattern[filled_h:h, :filled_w] = np.tile(
|
|
source, ((remainder_h + d - 1) // d, 1)
|
|
)[:remainder_h, :]
|
|
|
|
if filled_w < w and filled_h > 0:
|
|
remainder_w = w - filled_w
|
|
source = pattern[:filled_h, filled_w - d:filled_w]
|
|
pattern[:filled_h, filled_w:w] = np.tile(source, (1, (remainder_w + d - 1) // d))[:, :remainder_w]
|
|
|
|
if filled_h < h and filled_w < w:
|
|
remainder_h = h - filled_h
|
|
remainder_w = w - filled_w
|
|
source = pattern[filled_h - d:filled_h, filled_w - d:filled_w]
|
|
pattern[filled_h:h, filled_w:w] = np.tile(
|
|
source,
|
|
((remainder_h + d - 1) // d, (remainder_w + d - 1) // d),
|
|
)[:remainder_h, :remainder_w]
|
|
|
|
pattern[:] = np.maximum(pattern, min_gray)
|
|
return pattern
|
|
|
|
|
|
def generate_color_pattern(h, w, d, r, black_threshold):
|
|
"""
|
|
Generate a dense grayscale pattern.
|
|
h, w: height and width of the output image in pixels.
|
|
d: patch size in pixels (each patch is d x d).
|
|
r: randomness regulation factor. For each neighbor the condition is:
|
|
(|channel_new - channel_neighbor|)^2 >= r.
|
|
black_threshold: minimum grayscale value in [0, 255]; patches cannot be black.
|
|
"""
|
|
pattern = np.zeros((h, w), dtype=np.float32)
|
|
num_patches_x = h // d
|
|
num_patches_y = w // d
|
|
min_gray = min_gray_normalized(black_threshold)
|
|
|
|
for i in range(num_patches_x):
|
|
for j in range(num_patches_y):
|
|
# Collect neighbor patch grays as RGB triples for the existing sampler.
|
|
neighbors = []
|
|
neighbor_grays = []
|
|
if i > 0:
|
|
patch_above = pattern[(i-1)*d:i*d, j*d:(j+1)*d]
|
|
gray_above = float(np.mean(patch_above))
|
|
neighbor_grays.append(gray_above)
|
|
neighbors.append(gray_to_rgb(gray_above))
|
|
if j > 0:
|
|
patch_left = pattern[i*d:(i+1)*d, (j-1)*d:j*d]
|
|
gray_left = float(np.mean(patch_left))
|
|
neighbor_grays.append(gray_left)
|
|
neighbors.append(gray_to_rgb(gray_left))
|
|
|
|
if len(neighbors) == 0:
|
|
# First patch: sample directly above the black threshold.
|
|
gray = np.random.uniform(min_gray, 1.0)
|
|
else:
|
|
new_color = generate_valid_color(neighbors, r)
|
|
gray = rgb_to_gray(new_color)
|
|
if gray < min_gray:
|
|
gray = generate_valid_gray(neighbor_grays, r, black_threshold)
|
|
else:
|
|
gray = enforce_min_gray(gray, black_threshold)
|
|
|
|
pattern[i*d:(i+1)*d, j*d:(j+1)*d] = gray
|
|
print('Patch ({}, {}) gray={:.3f}'.format(i, j, gray))
|
|
|
|
return fill_pattern_remainder(pattern, h, w, d, black_threshold)
|
|
|
|
def main():
|
|
# Parameters for the pattern.
|
|
# For example, using 100x100 pixels image with patches of 3x3 pixels.
|
|
# height = 2750
|
|
# width = 1950
|
|
height = 1340
|
|
width = 1360
|
|
patch_size = 12
|
|
black_threshold = 125
|
|
# Set r to a value that gives sufficient difference.
|
|
# Note: r is the threshold for squared difference per channel, and the condition is
|
|
# |c - c_neighbor| >= sqrt(r). Very high r may make it impossible to satisfy.
|
|
r = 1000
|
|
|
|
pattern = generate_color_pattern(height, width, patch_size, r, black_threshold)
|
|
# Scale to 0-255 for display and saving.
|
|
pattern_display = (pattern * 255).astype(np.uint8)
|
|
|
|
if pattern_display.min() <= black_threshold:
|
|
raise RuntimeError(
|
|
f"Generated pattern contains values <= black_threshold ({black_threshold}); "
|
|
f"min={int(pattern_display.min())}"
|
|
)
|
|
|
|
# Display the pattern with OpenCV.
|
|
cv2.imshow('Pattern', pattern_display)
|
|
cv2.waitKey(0)
|
|
cv2.destroyAllWindows()
|
|
|
|
# Save the pattern as an image file.
|
|
cv2.imwrite('gray_pattern.png', pattern_display)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|