summaryrefslogtreecommitdiffstats
path: root/Third_Party/generate_recursive_dithering.py
blob: c7f92480772d6fd6f4d61d3e95aba713c5aeb7cb (plain)
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()