1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
|
import numpy as np
from PIL import Image, ImageDraw
import argparse
import math
def create_circle_image(res, radius, center=None):
"""Create a binary image with a white circle on black background."""
if center is None:
center = (res/2, res/2)
img = Image.new('L', (res, res), 0)
draw = ImageDraw.Draw(img)
# Convert radius and center to pixels
r_px = int(radius * res)
cx, cy = int(center[0]), int(center[1])
# Draw white circle (255)
draw.ellipse([cx-r_px, cy-r_px, cx+r_px, cy+r_px], fill=255)
# Handle edge case 1: circle is on left edge
if cx == 0:
cx2 = res
draw.ellipse([cx2-r_px, cy-r_px, cx2+r_px, cy+r_px], fill=255)
# Handle edge case 2: circle is on top edge
if cy == 0:
cy2 = res
draw.ellipse([cx-r_px, cy2-r_px, cx+r_px, cy2+r_px], fill=255)
# Handle edge case 3: circle is on top left corner
if cx == 0 and cy == 0:
cx2 = res
cy2 = res
draw.ellipse([cx2-r_px, cy2-r_px, cx2+r_px, cy2+r_px], fill=255)
return img
def get_bayer_location(nth, res):
"""
Returns the normalized location (x, y) for the nth dot in a recursive Bayer dithering pattern.
The pattern is constructed by writing nth in base-4 and mapping each digit as follows:
0 -> (0, 0)
1 -> (1, 1)
2 -> (1, 0)
3 -> (0, 1)
Each digit is weighted by successive powers of 1/2 so that:
x = sum_{i=0}^{k-1} (digit_x(i)) / 2^(i+1)
y = sum_{i=0}^{k-1} (digit_y(i)) / 2^(i+1)
The parameter `res` is expected to be a power-of-two (and in practice an even number); we let k = (res+1)//2.
For example:
- For res == 2, k = 1, and the function returns one of: (0,0), (0.5,0.5), (0.5,0), or (0,0.5).
- For res == 4, k = 2, and the function returns positions on a 4×4 grid.
- For res == 8, k = 4, and the function returns positions on a 16×16 grid.
If nth is greater than or equal to 4^k then an error is raised.
"""
k = (res + 1) // 2 # determine the number of base-4 digits to use
if nth >= 4 ** k:
raise ValueError(f"nth value {nth} too large for given res {res} (max is {4**k - 1}).")
x, y = 0.0, 0.0
# Process each base-4 digit (least significant first)
for i in range(k):
digit = nth % 4
nth //= 4
weight = 1 / (2 ** (i + 1))
if digit == 0:
dx, dy = 0, 0
elif digit == 1:
dx, dy = 1, 1
elif digit == 2:
dx, dy = 1, 0
elif digit == 3:
dx, dy = 0, 1
else:
raise ValueError("Unexpected digit encountered while converting number to base-4")
x += dx * weight
y += dy * weight
return (x, y)
def main():
parser = argparse.ArgumentParser(description="Generate dot and circle images.")
parser.add_argument("--res", type=int, default=128, help="Resolution of the images.")
args = parser.parse_args()
res = args.res
initial_area = 0.1
bayer_res = 16
total_layers = bayer_res * bayer_res
# Calculate number of digits needed for zero padding
num_digits = len(str(total_layers))
for layer in range(1, total_layers + 1):
# Calculate radius for this layer (split area among n circles)
radius = math.sqrt(initial_area/layer) * res
# Create the combined image for this layer
layer_img = Image.new('L', (res, res), 0)
# Place n circles according to Bayer pattern
for n in range(layer):
# Get normalized coordinates from Bayer pattern
x, y = get_bayer_location(n, bayer_res)
# Convert to pixel coordinates
center = (x * res, y * res)
# Create and add circle
circle = create_circle_image(res, radius/res, center=center)
layer_img = Image.fromarray(np.maximum(np.array(layer_img), np.array(circle)))
# Save the layer with zero-padded number
layer_img.save(f"dots_L{layer:0{num_digits}d}.png")
if __name__ == "__main__":
main()
|