From 9b52e08a7f2400c7ee78e9b284ed0c4c876a7dd4 Mon Sep 17 00:00:00 2001 From: yum Date: Wed, 5 Feb 2025 23:14:13 -0800 Subject: Add 4x4 textures to ssfd --- Third_Party/gen_sdf | 37 +++---- Third_Party/generate_recursive_dithering.py | 152 +++++++++++++++++----------- 2 files changed, 110 insertions(+), 79 deletions(-) (limited to 'Third_Party') diff --git a/Third_Party/gen_sdf b/Third_Party/gen_sdf index 2f0e3a6..52ef487 100644 --- a/Third_Party/gen_sdf +++ b/Third_Party/gen_sdf @@ -25,28 +25,29 @@ def compute_sdf(img, scale_factor): def main(): parser = argparse.ArgumentParser(description='Generate SDF from black and white image') - parser.add_argument('input_image', help='Path to input image') + parser.add_argument('input_images', nargs='+', help='Path to input image(s)') parser.add_argument('--scale', type=float, default=1.0, help='Scale factor for distance (in texels)') args = parser.parse_args() - # Get input and output paths - input_path = args.input_image - filename, ext = os.path.splitext(input_path) - output_path = f"{filename}-sdf{ext}" - - # Read input image - img = cv2.imread(input_path, cv2.IMREAD_GRAYSCALE) - if img is None: - print(f"Error: Could not read image {input_path}") - return - - # Compute SDF with scale factor - sdf = compute_sdf(img, args.scale) - - # Save result - cv2.imwrite(output_path, sdf) - print(f"SDF generated and saved to {output_path}") + # Process each input image + for input_path in args.input_images: + # Get input and output paths + filename, ext = os.path.splitext(input_path) + output_path = f"{filename}-sdf{ext}" + + # Read input image + img = cv2.imread(input_path, cv2.IMREAD_GRAYSCALE) + if img is None: + print(f"Error: Could not read image {input_path}") + continue + + # Compute SDF with scale factor + sdf = compute_sdf(img, args.scale) + + # Save result + cv2.imwrite(output_path, sdf) + print(f"SDF generated and saved to {output_path}") if __name__ == "__main__": main() diff --git a/Third_Party/generate_recursive_dithering.py b/Third_Party/generate_recursive_dithering.py index 417647e..c7f9248 100644 --- a/Third_Party/generate_recursive_dithering.py +++ b/Third_Party/generate_recursive_dithering.py @@ -3,86 +3,116 @@ from PIL import Image, ImageDraw import argparse import math -from shift_image_to_corner import shift_to_corner - 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 - goal_area = 0.1 - # a = pi * r^2 - # r = sqrt(a / pi) - initial_r = math.sqrt(goal_area / math.pi) * res # Start with a circle that fills 1/4 of the image - - # Calculate areas and radii for each layer - initial_area = math.pi * (initial_r/res)**2 - - # Layer 1: Single circle in corner - r1 = math.sqrt(initial_area/1) * res # Calculate radius consistently with other layers - base_img = create_circle_image(res, r1/res) - corner_img = shift_to_corner(base_img) - corner_img.save("dots_L1.png") - - # Layer 2: Two circles, split area - r2 = math.sqrt(initial_area/2) * res # New radius for each circle - # Create corner circle - corner_img = shift_to_corner(create_circle_image(res, r2/res)) - # Create center circle - center_img = create_circle_image(res, r2/res) - # Combine images - layer2 = Image.fromarray(np.maximum(np.array(corner_img), np.array(center_img))) - layer2.save("dots_L2.png") - - # Layer 3: Three circles - r3 = math.sqrt(initial_area/3) * res - # Create corner and center circles as before - corner_img = shift_to_corner(create_circle_image(res, r3/res)) - center_img = create_circle_image(res, r3/res) - # Create top circle - top_img = shift_to_corner(create_circle_image(res, r3/res), shift_side=False) - # Combine images - layer3 = Image.fromarray(np.maximum.reduce([ - np.array(corner_img), - np.array(center_img), - np.array(top_img) - ])) - layer3.save("dots_L3.png") - - # Layer 4: Four circles - r4 = math.sqrt(initial_area/4) * res - # Create corner and center circles - corner_img = shift_to_corner(create_circle_image(res, r4/res)) - center_img = create_circle_image(res, r4/res) - # Create top and right circles - top_img = shift_to_corner(create_circle_image(res, r4/res), shift_side=False) - right_img = shift_to_corner(create_circle_image(res, r4/res), shift_up=False) - # Combine images + 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) - layer4 = Image.fromarray(np.maximum.reduce([ - np.array(corner_img), - np.array(center_img), - np.array(top_img), - np.array(right_img) - ])) - layer4.save("dots_L4.png") + 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() \ No newline at end of file + main() -- cgit v1.2.3