Spaces:
				
			
			
	
			
			
					
		Running
		
	
	
	
			
			
	
	
	
	
		
		
					
		Running
		
	| from gradio_client import Client, handle_file | |
| from pathlib import Path | |
| import gradio as gr | |
| import numpy as np | |
| from sklearn.cluster import KMeans | |
| import trimesh | |
| import plotly.graph_objects as go | |
| import plotly.express as px | |
| import polars as pl | |
| from scipy.spatial import cKDTree | |
| from constants import BLOCK_SIZES, LEGO_COLORS_RGB | |
| def get_client() -> Client: | |
| return Client("TencentARC/InstantMesh") # TODO: enable global client. | |
| def generate_mesh(img: Path | str, seed: int = 42) -> str: | |
| """Takes a img (path or bytes) and returns a str (path) to the generated .obj-file""" | |
| client = get_client() | |
| result = client.predict( | |
| input_image=handle_file(img), do_remove_background=True, api_name="/preprocess" | |
| ) | |
| result = client.predict( | |
| input_image=handle_file(result), | |
| sample_steps=75, | |
| sample_seed=seed, | |
| api_name="/generate_mvs", | |
| ) | |
| result = client.predict(api_name="/make3d") | |
| obj_file = result[0] | |
| return obj_file | |
| # ---- STEP 4: SELECT VOXEL SIZE ---- | |
| def voxelize(mesh_path: str | Path, resolution: str): | |
| resolution = {"Small (16)": 16, "Medium (32)": 32, "Large (64)": 64}[resolution] | |
| mesh = trimesh.load(mesh_path) | |
| bounds = mesh.bounds | |
| voxel_size = (bounds[1] - bounds[0]).max() / resolution # pitch | |
| voxels = mesh.voxelized(pitch=voxel_size) | |
| colors = tree_knearest_colors(1, mesh, voxels) # one is faster and good enough. | |
| mesh_state = {"voxels": voxels, "mesh": mesh, "colors": colors} | |
| return mesh_state | |
| def build_scene(mesh, voxels): | |
| """Writes trimesh scene to .obj file""" | |
| voxels_mesh = voxels.as_boxes().apply_translation((1.5, 0, 0)) | |
| scene = trimesh.Scene([mesh, voxels_mesh]) | |
| scene.export("scene.obj") | |
| return "scene.obj" | |
| # ---- STEP 5: VISUALIZE VOXELS ---- | |
| def quantize_colors(colors, k: int = 16): | |
| """ | |
| quantize colors by fitting into 16 unique colors. | |
| """ | |
| original_colors = np.array(colors)[:, :3] | |
| kmeans = KMeans(n_clusters=k, random_state=42) | |
| kmeans.fit(original_colors) | |
| # Get the representative colors | |
| representative_colors = kmeans.cluster_centers_.astype(int) | |
| # Transform the original colors to representative colors | |
| transformed_colors = representative_colors[kmeans.labels_] | |
| return transformed_colors | |
| def lego_colors(colors): | |
| """ | |
| quantize colors by fitting into 16 unique colors. | |
| """ | |
| original_colors = np.array(colors)[:, :3] | |
| # Use scipy cdist to calculate euclidean distance between original and LEGO_C.. | |
| from scipy.spatial.distance import cdist | |
| distances = cdist(original_colors, LEGO_COLORS_RGB, metric="sqeuclidean") | |
| distances = np.sqrt(distances) | |
| closest = np.argmin(distances, axis=1) | |
| return LEGO_COLORS_RGB[closest] | |
| def pl_color_to_str(): | |
| color_arr = pl.col("color").arr | |
| return pl.format( | |
| "rgb({},{},{})", color_arr.get(0), color_arr.get(1), color_arr.get(2) | |
| ) | |
| def visualize_voxels(mesh_state): | |
| # Step 1: Extract Colors | |
| # colors = tree_knearest_colors(5, mesh_state["mesh"], mesh_state["voxels"]) | |
| # Step 2: Lego'ify Colors | |
| colors = mesh_state["colors"] | |
| # colors = quantize_colors(colors) | |
| # Step 3: Visualize | |
| voxels = mesh_state["voxels"] | |
| # Convert occupied_voxel_indices to a Polars DataFrame (if not already done) | |
| df = pl.from_numpy(voxels.sparse_indices, schema=["x", "z", "y"]) | |
| df = df.with_columns(color=pl.Series(colors)).with_columns( | |
| color_str=pl_color_to_str() | |
| ) | |
| return ( | |
| px.scatter_3d( | |
| df, | |
| x="x", | |
| y="y", | |
| z="z", | |
| color="color_str", | |
| color_discrete_map="identity", | |
| symbol=["square"] * len(df), | |
| symbol_map="identity", | |
| ), | |
| df, | |
| ) | |
| def tree_knearest_colors(k: int, mesh, voxels): | |
| tree = cKDTree(mesh.vertices) | |
| distances, vertex_indices = tree.query(voxels.points, k=k) | |
| if k == 1: | |
| return mesh.visual.vertex_colors[vertex_indices] | |
| voxel_colors = [] | |
| for nearest_indices in vertex_indices: | |
| neighbor_colors = mesh.visual.vertex_colors[nearest_indices] | |
| average_color = np.mean(neighbor_colors, axis=0).astype(np.uint8) | |
| voxel_colors.append(average_color) | |
| return voxel_colors | |
| # ---- STEP 6: ADJUST BRIGHTNESS ---- | |
| # def adjust_brightness(image, brightness): | |
| # adjusted_image = cv2.convertScaleAbs(image, alpha=brightness) | |
| # return adjusted_image | |
| # ---- STEP 8: LEGO BUILD ANIMATION ---- | |
| def merge_into_bricks(grouped_df: pl.DataFrame, BLOCK_SIZES) -> pl.DataFrame: | |
| color_str = grouped_df[0, "color_str"] | |
| z_val = grouped_df[0, "z"] | |
| xy_grid = np.zeros( | |
| (grouped_df["x"].max() + 1, grouped_df["y"].max() + 1), dtype=bool | |
| ) | |
| xy_grid[grouped_df["x"], grouped_df["y"]] = 1 | |
| out_rows = [] | |
| grouped_df = grouped_df.sort(by=["x", "y"]) | |
| coords = {(x, y) for x, y in grouped_df[["x", "y"]].to_numpy()} | |
| while coords: | |
| (x0, y0) = coords.pop() | |
| coords.add((x0, y0)) # reinsert until placed | |
| placed = False | |
| for width, height in BLOCK_SIZES: | |
| if x0 + width > xy_grid.shape[0] or y0 + height > xy_grid.shape[1]: | |
| continue | |
| if np.all(xy_grid[x0 : x0 + width, y0 : y0 + height] == 1): | |
| place_block(x0, y0, width, height, coords) | |
| xy_grid[x0 : x0 + width, y0 : y0 + height] = 0 # remove from xygrid | |
| out_rows.append((color_str, z_val, x0, y0, width, height)) | |
| placed = True | |
| break | |
| if not placed: | |
| # fallback to 1x1 | |
| coords.remove((x0, y0)) | |
| out_rows.append((color_str, z_val, x0, y0, 1, 1)) | |
| return pl.DataFrame( | |
| { | |
| "color_str": [row[0] for row in out_rows], | |
| "z": [row[1] for row in out_rows], | |
| "x": [row[2] for row in out_rows], | |
| "y": [row[3] for row in out_rows], | |
| "width": [row[4] for row in out_rows], | |
| "height": [row[5] for row in out_rows], | |
| } | |
| ) | |
| def can_place_block(x0, y0, w, h, coords): | |
| for xx in range(x0, x0 + w): | |
| for yy in range(y0, y0 + h): | |
| if (xx, yy) not in coords: | |
| return False | |
| return True | |
| def place_block(x0, y0, w, h, coords): | |
| for xx in range(x0, x0 + w): | |
| for yy in range(y0, y0 + h): | |
| coords.remove((xx, yy)) | |
| # Function to generate vertices for a rectangular prism (brick) | |
| def create_brick(x, y, z, width, height, depth=1, color="gray"): | |
| return go.Mesh3d( | |
| x=[x, x + width, x + width, x, x, x + width, x + width, x], # X-coordinates | |
| y=[y, y, y + height, y + height, y, y, y + height, y + height], # Y-coordinates | |
| z=[z, z, z, z, z + depth, z + depth, z + depth, z + depth], # Z-coordinates | |
| color=color, | |
| alphahull=-1, | |
| i=[7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2], | |
| j=[3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3], | |
| k=[0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6], | |
| name=f"Z={z}", | |
| ) | |
| def get_range(series: pl.Series) -> tuple[int, int]: | |
| return series.min(), series.max() | |
| def animate_lego_build(df_state): | |
| # Colors already merged. | |
| df: pl.DataFrame = df_state | |
| df = df.with_columns(color=quantize_colors(df["color"])).with_columns( | |
| color_str=pl_color_to_str() | |
| ) | |
| # Quantize Colors... Need to split string and use.. | |
| merged_df = df.group_by("color_str", "z").map_groups( | |
| lambda grp: merge_into_bricks(grp, BLOCK_SIZES) | |
| ) | |
| fig = go.Figure() | |
| fig.update_layout( | |
| scene=dict( | |
| xaxis=dict(range=get_range(df["x"]), autorange=False), | |
| yaxis=dict(range=get_range(df["y"]), autorange=False), | |
| zaxis=dict(range=get_range(df["z"]), autorange=False), | |
| ) | |
| ) | |
| # Add each brick to the plot | |
| for z in merged_df["z"].unique().sort(): | |
| for row in merged_df.filter(pl.col("z") == z).iter_rows(named=True): | |
| fig.add_trace( | |
| create_brick( | |
| x=row["x"], | |
| y=row["y"], | |
| z=row["z"], | |
| width=row["width"], | |
| height=row["height"], | |
| color=row["color_str"], | |
| ) | |
| ) | |
| # frame_jpgs.append(f"frame_z_{z}.jpg") | |
| # if not Path(frame_jpgs[-1]).exists(): | |
| # fig.write_image(frame_jpgs[-1]) | |
| return fig # , frame_jpgs | |
| # ---- GRADIO UI ---- | |
| with gr.Blocks() as demo: | |
| gr.Markdown("# 🧱 **Image 2 Lego Builder** 🧱") | |
| # Step 1: Upload Image and Build Mesh | |
| with gr.Column(variant="compact"): | |
| with gr.Row(): | |
| image_input = gr.Image( | |
| type="filepath", height="250px", label="Upload an Image" | |
| ) | |
| with gr.Column(variant="compact"): | |
| seed = gr.Number(label="Seed", value=42) | |
| # Potentially add color options. | |
| voxel_size_selector = gr.Dropdown( | |
| ["Small (16)", "Medium (32)", "Large (64)"], | |
| value="Medium (32)", | |
| label="Select Voxel Size", | |
| ) | |
| with gr.Row(): | |
| build_button = gr.Button("Generate Mesh") | |
| voxelize_button = gr.Button("Generate Voxels") | |
| # Visualizations... | |
| # Mesh | Voxel Color | Voxel Lego Bricks+Color | |
| with gr.Row(): | |
| mesh_info_display = gr.Model3D( | |
| label="Mesh Visualization", height="250px", value="mesh.obj" | |
| ) | |
| voxel_color_display = gr.Plot(label="Colorized Voxels") | |
| voxel_bricks = gr.Plot(label="Lego Bricks") | |
| brick_animation = gr.Gallery(label="Build Animation") | |
| mesh_state = gr.State(value={}) | |
| build_button.click( | |
| generate_mesh, inputs=[image_input, seed], outputs=mesh_info_display | |
| ) | |
| # Step 4: Select Voxel Size | |
| voxelize_button.click( | |
| voxelize, | |
| inputs=[mesh_info_display, voxel_size_selector], | |
| outputs=[mesh_state], | |
| ) | |
| df_state = gr.State() | |
| mesh_state.change( | |
| visualize_voxels, | |
| inputs=[mesh_state], | |
| outputs=[voxel_color_display, df_state], | |
| ) | |
| df_state.change(animate_lego_build, inputs=[df_state], outputs=[voxel_bricks]) | |
| def anim_pltly(df): | |
| df = df.with_columns(color=quantize_colors(df["color"])).with_columns( | |
| color_str=pl_color_to_str() | |
| ) | |
| # Quantize Colors... Need to split string and use.. | |
| merged_df = df.group_by("color_str", "z").map_groups( | |
| lambda grp: merge_into_bricks(grp, BLOCK_SIZES) | |
| ) | |
| fig = go.Figure() | |
| fig.update_layout( | |
| scene=dict( | |
| xaxis=dict(range=get_range(df["x"]), autorange=False), | |
| yaxis=dict(range=get_range(df["y"]), autorange=False), | |
| zaxis=dict(range=get_range(df["z"]), autorange=False), | |
| ) | |
| ) | |
| frame_jpgs = [] | |
| # Add each brick to the plot | |
| for z in merged_df["z"].unique().sort(): | |
| for row in merged_df.filter(pl.col("z") == z).iter_rows(named=True): | |
| fig.add_trace( | |
| create_brick( | |
| x=row["x"], | |
| y=row["y"], | |
| z=row["z"], | |
| width=row["width"], | |
| height=row["height"], | |
| color=row["color_str"], | |
| ) | |
| ) | |
| frame_jpgs.append(f"frame_z_{z}.jpg") | |
| if not Path(frame_jpgs[-1]).exists(): | |
| fig.write_image(frame_jpgs[-1]) | |
| return frame_jpgs | |
| # TODO: add to generate layer-by-layer | |
| # df_state.change(anim_pltly, inputs=[df_state], outputs=[brick_animation]) | |
| # Launch the app | |
| demo.launch(share=True, debug=True) | |