circle / app.py
kaitongg's picture
Update app.py
df1ef4f verified
import gradio as gr
import matplotlib.pyplot as plt
import numpy as np
import random
# --- Configure Matplotlib ---
plt.style.use('ggplot')
plt.rcParams['figure.figsize'] = [10, 10]
# --- Core Data Structure: Circle Class ---
class Circle:
"""Represents a circle in the drawing and its state"""
def __init__(self, x, y, radius, color):
self.x = x
self.y = y
self.radius = radius
self.color = color
def __repr__(self):
return f"Circle(Center:({self.x:.2f}, {self.y:.2f}), R:{self.radius}, Color:{self.color})"
# --- Shape Grammar Core Function ---
def run_shape_grammar(N_iterations):
"""
Runs the shape grammar iterative process N times and yields the state after each iteration.
"""
# 1. Initial Condition (Start)
circles = [Circle(0, 0, 2, 'blue')]
lines = [] # Stores all line segments for plotting
current_iteration = 0
print(f"--- Starting Shape Grammar: Target Iterations N = {N_iterations} ---")
# Yield initial state before any iterations
yield circles, lines
while current_iteration < N_iterations:
# Filter out non-red circles
active_circles = [c for c in circles if c.color != 'red']
# 2. Stop Condition Check
if not active_circles:
print(f"\n✅ Stop: All circles have turned red. Total iterations: {current_iteration}")
break
# 3. Randomly select an original circle (Pick a random circle)
C_original = random.choice(active_circles)
# 4. Determine Direction and Line Segment (Direction and Line)
# Random angle (multiple of 45°)
angle_deg = random.choice([0, 45, 90, 135, 180, 225, 270, 315])
angle_rad = np.deg2rad(angle_deg)
# Random line length
L_length = random.choice([6, 8, 10])
# Randomly choose starting point (center or tangent point)
start_from_center = random.choice([True, False])
if start_from_center:
# Option A: Start from the center
P_start_x, P_start_y = C_original.x, C_original.y
else:
# Option B: Start from a tangent point on the circumference
# The tangent point is the center translated by R in the opposite direction of 'angle_rad'
P_start_x = C_original.x + C_original.radius * np.cos(angle_rad + np.pi)
P_start_y = C_original.y + C_original.radius * np.sin(angle_rad + np.pi)
# Calculate the end point of the line segment
P_end_x = P_start_x + L_length * np.cos(angle_rad)
P_end_y = P_start_y + L_length * np.sin(angle_rad)
# Store the line segment
lines.append(((P_start_x, P_start_y), (P_end_x, P_end_y)))
# 5. Create New Circle (New Circle)
# Random radius and color
R_new = random.choice([1, 2, 3, 4])
# Color selection: Kept uniformly random for faster generation
colors = ['blue'] * 9 + ['green'] * 9 + ['red'] * 2 # 9:9:2 比例
Color_new = random.choice(colors)
# Random placement position
placement_option = random.choice(['tangential_left', 'tangential_right', 'centered'])
if placement_option == 'centered':
# Option C: Centered at the end point of the line segment
C_new_x, C_new_y = P_end_x, P_end_y
else:
# Option A/B: Tangential to the line segment
# Normal direction: Angle plus/minus 90 degrees
# Tangential to the line (L) left or right
normal_angle = angle_rad + (np.pi/2 if placement_option == 'tangential_left' else -np.pi/2)
# The center of the new circle is located at the end point P_end, moved by R_new distance along the normal direction
C_new_x = P_end_x + R_new * np.cos(normal_angle)
# Corrected the calculation for C_new_y - it should use P_end_y
C_new_y = P_end_y + R_new * np.sin(normal_angle)
C_new = Circle(C_new_x, C_new_y, R_new, Color_new)
circles.append(C_new)
# 6. Color Updates (Color Updates) - Modifying Green's mortality rate
if C_original.color == 'blue':
C_original.color = 'green'
elif C_original.color == 'green':
# 只有 50% 的概率从 Green 变为 Red,另 50% 保持 Green
if random.random() < 0.5:
C_original.color = 'red'
# 否则 C_original.color 保持 'green'
current_iteration += 1
print(f"Iteration {current_iteration}/{N_iterations}: Original circle turned {C_original.color}, New circle {C_new.color}, Total circles: {len(circles)}")
# Yield the current state
yield circles, lines
if current_iteration == N_iterations:
print(f"\n✅ Stop: Maximum number of iterations N = {N_iterations} reached.")
# --- Plotting Function ---
def plot_grammar_result(circles, lines):
"""
Plots the generated shape using Matplotlib and returns the figure.
"""
fig, ax = plt.subplots()
# Plot all circles
for c in circles:
# Plot using matplotlib.patches.Circle
circle_patch = plt.Circle((c.x, c.y), c.radius,
color=c.color,
alpha=0.6, # Transparency
fill=True,
linewidth=1,
edgecolor='black')
ax.add_patch(circle_patch)
# Plot all line segments
for (start, end) in lines:
ax.plot([start[0], end[0]], [start[1], end[1]],
color='gray',
linestyle='-',
linewidth=0.5,
zorder=-1) # Place below circles
# Set axes and limits
if circles:
all_x = [c.x for c in circles]
all_y = [c.y for c in circles]
# Automatically calculate boundaries to ensure all circles are visible
x_min, x_max = min(all_x), max(all_x)
y_min, y_max = min(all_y), max(all_y)
padding = 10 # Additional padding
# Add a check to prevent errors if min/max are the same (e.g., only one circle at 0,0)
if x_min == x_max:
x_min -= padding
x_max += padding
if y_min == y_max:
y_min -= padding
y_max += padding
ax.set_xlim(x_min - padding, x_max + padding)
ax.set_ylim(y_min - padding, y_max + padding)
ax.set_aspect('equal', adjustable='box') # Maintain equal aspect ratio
ax.set_title(f"Shape Grammar Generation Result (Total {len(circles)} circles)")
ax.set_xlabel("X Coordinate")
ax.set_ylabel("Y Coordinate")
# Don't call plt.show() here, return the figure object
return fig
# Store the generated plots
generated_plots = []
def run_shape_grammar_and_generate_plots(num_iterations):
"""
Runs the shape grammar and generates plots for each iteration.
Returns a list of Matplotlib figure objects.
"""
global generated_plots
generated_plots = [] # Clear previous plots
print(f"run_shape_grammar_and_generate_plots called with {num_iterations} iterations")
# Ensure the input is a positive integer
if num_iterations is None or num_iterations <= 0:
num_iterations = 50 # Default to 50 if invalid
# Run the generation process and iterate through yielded results
for i, (circles, lines) in enumerate(run_shape_grammar(int(num_iterations))):
# Plot the result for the current step and get the figure object
fig = plot_grammar_result(circles, lines)
generated_plots.append(fig)
# Close the figure to free up memory
plt.close(fig)
print(f"Generated {len(generated_plots)} plots.")
return generated_plots
def display_iteration(iteration_index):
"""
Displays the plot for a specific iteration based on the slider value.
"""
global generated_plots
print(f"display_iteration called with index {iteration_index}")
if generated_plots and 0 <= iteration_index < len(generated_plots):
print(f"Displaying plot for iteration {iteration_index}")
return generated_plots[int(iteration_index)] # Ensure index is integer
else:
print("No plot available or index out of range.")
# Return an empty plot or a placeholder if no plots are available or index is out of range
fig, ax = plt.subplots()
ax.text(0.5, 0.5, "No plot available", horizontalalignment='center', verticalalignment='center')
ax.set_title("Error")
return fig
# Create the Gradio interface
with gr.Blocks() as demo:
gr.Markdown("## Shape Grammar Generator with Iteration Slider")
gr.Markdown("Generate abstract shapes using a simple shape grammar and explore each step of the process.")
with gr.Row():
num_iterations_input = gr.Number(label="Number of Iterations", value=50, precision=0)
generate_button = gr.Button("Generate")
# Output area for the plots
plot_output = gr.Plot()
# Slider to control the displayed iteration
iteration_slider = gr.Slider(minimum=0, maximum=0, step=1, label="Iteration")
# Link the generate button to the function that runs the grammar and generates plots
# Update the slider's maximum value after generation
generate_button.click(
fn=run_shape_grammar_and_generate_plots,
inputs=num_iterations_input,
outputs=None # We don't directly output here, we just generate and store plots
).then(
fn=lambda: gr.update(maximum=len(generated_plots)-1, value=0), # Update slider max and reset to 0
inputs=None, # No direct input needed, accessing global variable
outputs=iteration_slider
).then(
fn=lambda: generated_plots[0] if generated_plots else None, # Display the first plot initially
inputs=None, # No direct input needed, accessing global variable
outputs=plot_output
)
# Link the slider to the function that displays the selected iteration's plot
iteration_slider.change(
fn=display_iteration,
inputs=iteration_slider,
outputs=plot_output
)
# Launch the interface for Hugging Face Spaces
if __name__ == "__main__":
# Modified to include share=True for Colab/hosted environments
demo.launch(share=True)