Spaces:
Running
on
Zero
Running
on
Zero
| # 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 | |
| } | |