# HURA (Hexagonal Uniformly Redundant Arrays) are used for aperture masks and imaging, and encoding. # check it out https://ntrs.nasa.gov/citations/19850026627 # by Surn (Charles Fettinger) 4/5/2025 from PIL import Image import math import gradio as gr from tempfile import NamedTemporaryFile from transformers.models.deprecated.vit_hybrid import image_processing_vit_hybrid import utils.constants as constants import utils.color_utils as color_utils class HuraConfig: """Configuration for Hexagonal Uniformly Redundant Array pattern generation.""" def __init__(self): # Core parameters self.v = 139 # Prime number parameter (affects pattern complexity) self.r = 42 # Pattern frequency parameter self.version = "0.2.2" # Pattern generation constants self.hex_ratio = 0.5773503 # sqrt(3)/3 self.pattern_scale = 21.0 # Controls pattern frequency self.vignette_inner = 0.97 self.vignette_outer = 1.01 # Colors self.default_colors = [ (255, 0, 0), # Red (0, 255, 0), # Green (0, 0, 255) # Blue ] # Prime number calculation self.prime_range_start = 1 self.prime_range_end = 5001 self._primes_cache = None # Will be lazily loaded def get_v(self): """Get the current V parameter value.""" return self.v def set_v(self, value): """Set the V parameter value.""" if not isinstance(value, (int, float)) or value < 1: raise ValueError(f"V value must be a positive float, got {value}") self.v = value def get_r(self): """Get the current R parameter value.""" return self.r def set_r(self, value): """Set the R parameter value.""" if not isinstance(value, (int, float)) or value < 1: raise ValueError(f"R value must be a positive float, got {value}") self.r = value def get_primes(self): """Get or calculate the list of primes in the configured range.""" if self._primes_cache is None: self._primes_cache = get_primes_in_range(self.prime_range_start, self.prime_range_end) return self._primes_cache def find_nearest_prime(self, value): """Find the nearest prime number to the given value.""" return min(self.get_primes(), key=lambda x: abs(x - value)) def reset_colors(self): """Reset default colors to original values.""" self.default_colors = [ (255, 0, 0), # Red (0, 255, 0), # Green (0, 0, 255) # Blue ] return self.default_colors # Initialize the HuraConfig instance config = HuraConfig() # For backwards compatibility - consider deprecating these __version__ = config.version _V = config.v _R = config.r def get_v(): return config.get_v() def set_v(val): config.set_v(val) def get_r(): return config.get_r() def set_r(val): config.set_r(val) state_colors = [] def smoothstep(edge0, edge1, x): """ Smoothstep function for vignette effect. Smoothly interpolate between edge0 and edge1 based on x. """ if edge0 == edge1: return 0.0 if x < edge0 else 1.0 t = min(max((x - edge0) / (edge1 - edge0), 0.0), 1.0) return t * t * (3 - 2 * t) # Define the hexagon function to compute coordinates def hexagon(p): """ Compute hexagon coordinates and metrics for point p. Args: p (tuple): Normalized point (x,y) in [-aspect,aspect] � [-1,1] range Returns: tuple: (hex_x, hex_y, edge_distance, center_distance) - hex_x, hex_y: Integer coordinates of the hexagon cell - edge_distance: Distance to nearest edge (0-1) - center_distance: Distance to cell center (0-1) """ # Transform to hexagonal coordinate system q = (p[0] * 2.0 * config.hex_ratio, p[1] + p[0] * config.hex_ratio) pi = (math.floor(q[0]), math.floor(q[1])) pf = (q[0] - pi[0], q[1] - pi[1]) mod_val = (pi[0] + pi[1]) % 3.0 # renamed from v ca = 1.0 if mod_val >= 1.0 else 0.0 cb = 1.0 if mod_val >= 2.0 else 0.0 ma = (1.0 if pf[1] >= pf[0] else 0.0, 1.0 if pf[0] >= pf[1] else 0.0) temp = ( 1.0 - pf[1] + ca * (pf[0] + pf[1] - 1.0) + cb * (pf[1] - 2.0 * pf[0]), 1.0 - pf[0] + ca * (pf[0] + pf[1] - 1.0) + cb * (pf[0] - 2.0 * pf[1]) ) e = ma[0] * temp[0] + ma[1] * temp[1] p2_x = (q[0] + math.floor(0.5 + p[1] / 1.5)) * 0.5 + 0.5 p2_y = (4.0 * p[1] / 3.0) * 0.5 + 0.5 fract_p2 = (p2_x - math.floor(p2_x), p2_y - math.floor(p2_y)) f = math.sqrt((fract_p2[0] - 0.5)**2 + ((fract_p2[1] - 0.5) * 0.85)**2) h_xy = (pi[0] + ca - cb * ma[0], pi[1] + ca - cb * ma[1]) return (h_xy[0], h_xy[1], e, f) # important note: this is not a true hexagonal pattern, but a hexagonal grid def ura(p): """ Generate binary pattern value based on Uniformly Redundant Array algorithm. Args: p (tuple): Hexagon coordinates (x,y) Returns: float: 1.0 for pattern, 0.0 for background future consideration.. add animation #ifdef INCREMENT_R float l = mod(p.y + floor(time*1.5)*p.x, v); #else float l = mod(p.y + r*p.x, v); """ r = get_r() v = get_v() l = math.fmod(abs(p[1]) + r * abs(p[0]), v) rz = 1.0 for i in range(1, int(v/2) + 1): if math.isclose(math.fmod(i * i, v), l, abs_tol=1e-6): rz = 0.0 break return rz # Generate the image with colorful_hexagonal pattern def generate_image_color(width, height, colors=None): """Generate an RGB image with a colorful hexagonal pattern.""" img = Image.new('RGB', (width, height)) if colors is None or colors == []: colors = config.default_colors r = config.get_r() v = config.get_v() aspect = width / height for j in range(height): for i in range(width): # Normalize pixel coordinates to [0, 1] q_x = i / width q_y = j / height # Transform to centered coordinates with aspect ratio p_x = (q_x * 2.0 - 1.0) * aspect p_y = q_y * 2.0 - 1.0 p = (p_x, p_y) # Scale coordinates for pattern frequency h = hexagon((p[0] * config.pattern_scale, p[1] * config.pattern_scale)) h_xy = (int(h[0]), int(h[1])) # Assign color based on hexagon coordinates rz = math.fmod(abs(h_xy[0]) + r * abs(h_xy[1]),v) color_index = int(rz % len(colors)) col = colors[color_index] # Apply vignette effect q = (q_x * 2.0 - 1.0, q_y * 2.0 - 1.0) vignette = smoothstep(config.vignette_outer, config.vignette_inner, max(abs(q[0]), abs(q[1]))) col = tuple(int(c * vignette) for c in col) # Set the pixel color img.putpixel((i, j), col) return img def generate_image_grayscale(width, height): img = Image.new('RGB', (width, height)) aspect = width / height for j in range(height): for i in range(width): q_x = i / width q_y = j / height p_x = (q_x * 2.0 - 1.0) * aspect p_y = q_y * 2.0 - 1.0 p = (p_x, p_y) h = hexagon((p[0] * config.pattern_scale, p[1] * config.pattern_scale)) rz = ura(h[:2]) smooth = smoothstep(-0.2, 0.13, h[2]) if rz > 0.5: col = smooth else: col = 1.0 - smooth q = (q_x * 2.0 - 1.0, q_y * 2.0 - 1.0) vignette = smoothstep(config.vignette_outer, config.vignette_inner, max(abs(q[0]), abs(q[1]))) col *= vignette color = int(abs(col) * 255) img.putpixel((i, j), (color, color, color)) return img def get_primes_in_range(start: int, end: int) -> list: """ Return a list of prime numbers between start and end (inclusive). Uses the Sieve of Eratosthenes for efficiency. Parameters: start (int): The starting number of the range. end (int): The ending number of the range. Returns: list: A list of prime numbers between start and end. """ if end < 2: return [] sieve = [True] * (end + 1) sieve[0] = sieve[1] = False for i in range(2, int(end ** 0.5) + 1): if sieve[i]: for j in range(i * i, end + 1, i): sieve[j] = False return [i for i in range(start, end + 1) if sieve[i]] def find_nearest_prime(value): """Find the closest prime number to the given value.""" return config.find_nearest_prime(value) def generate_pattern_background(pattern_type="color", width=1024, height=768, v_value=_V, r_value=_R, colors=None): # Generate a hexagonal pattern image with the given parameters. # Do not pass gr.State values here # Set the parameters set_v(v_value) set_r(r_value) print(f"Generating pattern with V: {v_value}, R: {r_value}, Colors: {colors}") color_count = 3 if pattern_type == "color": if colors is None: img = generate_image_color(width, height) else: img = generate_image_color(width, height, colors) color_count = len(colors) else: # grayscale img = generate_image_grayscale(width, height) color_count = 1 # Save to temporary file and return path with NamedTemporaryFile(delete=False,prefix=f"hura_{str(color_count)}_v{str(v_value)}_r{str(r_value)}_", suffix=".png") as tmp: img.save(tmp.name, format="PNG") constants.temp_files.append(tmp.name) return tmp.name def create_color_swatch_html(colors): """Create HTML for displaying color swatches""" swatches = ''.join( f'<div style="width: 50px; height: 50px; background-color: rgb{c}; ' f'border: 1px solid #ccc;"></div>' for c in colors ) return f'<div style="display: flex; gap: 10px;">{swatches}</div>' def _add_color(color, color_list): if color is None: return color_list, color_list, "" # Convert hex color to RGB rgb_color = color_utils.hex_to_rgb(color) color_list = color_list + [rgb_color] # Create HTML to display color swatches html = create_color_swatch_html(color_list) return color_list, html def _init_colors(): """Initialize the color swatches HTML display based on config colors""" updated_list = list(config.default_colors) # Rebuild the HTML swatches from the updated list html = create_color_swatch_html(updated_list) return html def reset_colors(): """Reset the color list to the default colors.""" colors = config.reset_colors() html = create_color_swatch_html(colors) return colors, html def _generate_pattern_from_state(pt, width, height, v_val, r_val, colors_list): # colors_list is automatically the raw value from the gr.State input return generate_pattern_background(pt, width, height, v_val, r_val, colors_list) def render() -> dict: """ Renders a colorful or grayscale hexagonal pattern creation interface Returns: dict: A dictionary containing: - target_image (gr.Image): The generated pattern image component - run_generate_hex_pattern (function): Function to generate a pattern with given dimensions - set_height_width_hura_image (function): Function to update the slider values - width_slider (gr.Slider): The width slider component - height_slider (gr.Slider): The height slider component """ # Initialize state global state_colors state_colors = gr.State(config.default_colors) init_colors_html = _init_colors() target_image = gr.Image(label="Generated Pattern", type="filepath") with gr.Row(): pattern_type = gr.Radio( label="Pattern Type", choices=["color", "grayscale"], value="grayscale", type="value" ) with gr.Column(): with gr.Row(): width_slider = gr.Slider(minimum=256, maximum=2560, value=1024, label="Width", step=8) height_slider = gr.Slider(minimum=256, maximum=2560, value=768, label="Height", step=8) v_value_slider = gr.Slider(minimum=config.prime_range_start, maximum=config.prime_range_end, value=config.v, label="V Value (Prime Number)", step=1) r_value_slider = gr.Slider(minimum=1, maximum=100, value=config.r, label="R Value") show_borders_chbox = gr.Checkbox(label="Show Borders", value=True) with gr.Row(visible=False) as color_row: color_picker = gr.ColorPicker(label="Pick a Color") add_button = gr.Button("Add Color") with gr.Column(): color_display = gr.HTML(label="Color Swatches", value=init_colors_html) with gr.Row(): delete_colors_button = gr.Button("Delete Colors") reset_colors_button = gr.Button("Reset Colors") with gr.Row(): generate_button = gr.Button("Generate Pattern") def run_generate_hex_pattern(width: int, height: int) -> str: """ Generate a colored hexagonal pattern image with the given width and height. Uses default V and R values and the default color palette. Returns: str: The filepath of the generated image. """ global state_colors width_slider.value=width height_slider.value=height gr.update() # Use the current _V, _R, and default_colors filepath = generate_pattern_background( pattern_type="color", width=width, height=height, v_value=get_v(), r_value=get_r(), colors=state_colors.value ) return filepath pattern_type.change( fn=lambda x: gr.update(visible=(x == "color")), inputs=pattern_type, outputs=color_row ) add_button.click( fn=_add_color, inputs=[color_picker, state_colors], outputs=[state_colors, color_display] ) delete_colors_button.click( fn=lambda x: ([], "<div>Add Colors</div>"), inputs=[], outputs=[state_colors, color_display] ) reset_colors_button.click( fn=reset_colors, inputs=[], outputs=[state_colors,color_display] ) generate_button.click( fn=_generate_pattern_from_state, inputs=[pattern_type, width_slider, height_slider, v_value_slider, r_value_slider, state_colors], outputs=target_image, scroll_to_output=True ) v_value_slider.input( lambda x: config.find_nearest_prime(x), inputs=v_value_slider, outputs=v_value_slider ) v_value_slider.release( lambda x: config.find_nearest_prime(x), inputs=v_value_slider, outputs=v_value_slider, queue=False ) return { "target_image": target_image, "run_generate_hex_pattern": run_generate_hex_pattern, "width_slider": width_slider, "height_slider": height_slider }