{ "cells": [ { "cell_type": "markdown", "metadata": { "id": "_vSR56u0u74i" }, "source": [ "# 3D File to Lego\n", "\n", "First create 3D file using something like InstantMesh. Then we turn the 3D file into Lego build." ] }, { "cell_type": "markdown", "metadata": { "id": "M-2cYolwvEcp" }, "source": [ "## Install Dependencies" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true, "id": "cuPmdwpq0txu" }, "outputs": [], "source": [ "%%capture\n", "!pip install trimesh rtree PyQt5 colormath # xformers==0.0.22.post7 rembg" ] }, { "cell_type": "markdown", "metadata": { "id": "IuNdIvDsvIgz" }, "source": [ "## Load a 3D object\n", "\n", "And display our mesh" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 521 }, "id": "kyIn7pDq2h30", "outputId": "98ab2ba7-eda8-471c-ee93-6eaba7b053a8" }, "outputs": [], "source": [ "import trimesh\n", "import numpy as np\n", "\n", "mesh = trimesh.load('doll.obj')\n", "mesh.show()" ] }, { "cell_type": "markdown", "metadata": { "id": "HE58Fe0xvOkD" }, "source": [ "## Voxelize\n", "\n", "We can then voxelize using a custom resolution." ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "id": "i5el0q3H2xLr" }, "outputs": [], "source": [ "def voxelize(mesh, resolution: int = 64):\n", " bounds = mesh.bounds\n", " voxel_size = (bounds[1] - bounds[0]).max() / 64 # pitch\n", "\n", " return mesh.voxelized(pitch=voxel_size)\n", "\n", "def display_scene(mesh, voxels):\n", " voxels_mesh = voxels.as_boxes().apply_translation((1.5,0,0))\n", " scene = trimesh.Scene([mesh, voxels_mesh])\n", "\n", " return scene.show()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 521 }, "id": "hYzOb6Oq4z48", "outputId": "3a84d86a-7184-44a3-a993-be3851e02e5c" }, "outputs": [], "source": [ "voxels = voxelize(mesh)\n", "\n", "display_scene(mesh, voxels)" ] }, { "cell_type": "markdown", "metadata": { "id": "5UOYrWRpvdaS" }, "source": [ "## Voxelize with Color\n", "\n", "We need to colorize voxels by fetching the N nearest colors and taking the mean." ] }, { "cell_type": "code", "execution_count": 127, "metadata": { "id": "eOW-K4PVDOlv" }, "outputs": [], "source": [ "import plotly.graph_objects as go\n", "import plotly.express as px\n", "import polars as pl\n", "\n", "def display_voxels_px(voxels, colors):\n", " # Convert occupied_voxel_indices to a Polars DataFrame (if not already done)\n", " df = pl.from_numpy(voxels.sparse_indices, schema=[\"x\", \"y\", \"z\"])\n", " df = df.with_columns(color=pl.Series(colors))\n", " px.scatter_3d(df, x=\"x\", y=\"y\", z=\"z\", color=\"color\",\n", " color_discrete_map=\"identity\", symbol=[\"square\"]*len(df), symbol_map=\"identity\"\n", ").show()" ] }, { "cell_type": "code", "execution_count": 128, "metadata": { "id": "Iyz1dRYbHPg7" }, "outputs": [], "source": [ "from scipy.spatial import cKDTree\n", "import numpy as np\n", "\n", "def tree_knearest_colors(k: int, mesh, voxels):\n", " tree = cKDTree(mesh.vertices)\n", " distances, vertex_indices = tree.query(voxels.points, k=k)\n", " voxel_colors = []\n", "\n", " for nearest_indices in vertex_indices:\n", " neighbor_colors = mesh.visual.vertex_colors[nearest_indices]\n", " average_color = np.mean(neighbor_colors, axis=0).astype(np.uint8)\n", " voxel_colors.append(average_color)\n", "\n", " return voxel_colors\n", "\n", "def tree_knearest_color_mesh(k: int, mesh, voxels):\n", " tree = cKDTree(mesh.vertices)\n", " distances, vertex_indices = tree.query(voxels.points, k=k)\n", " voxel_colors = []\n", "\n", " for nearest_indices in vertex_indices:\n", " neighbor_colors = mesh.visual.vertex_colors[nearest_indices]\n", " average_color = np.mean(neighbor_colors, axis=0).astype(np.uint8)\n", " voxel_colors.append(average_color)\n", "\n", " # 2. Create a (X, Y, Z, 4) color matrix\n", " color_matrix = np.zeros(voxels.shape + (4,), dtype=np.uint8) # Initialize with default color (e.g., transparent black)\n", " color_matrix[voxels.sparse_indices[:, 0], voxels.sparse_indices[:, 1], voxels.sparse_indices[:, 2]] = voxel_colors\n", "\n", " # 3. Create a VoxelMesh using as_boxes() with the color matrix\n", " voxel_mesh = voxels.as_boxes(colors=color_matrix)\n", " return voxel_mesh" ] }, { "cell_type": "markdown", "metadata": { "id": "DTphj_klvtv7" }, "source": [ "### Display using scatter3d" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 542 }, "id": "a9dn8T6Xeq-i", "outputId": "6264392d-fd82-454a-80a3-18810e746e57" }, "outputs": [], "source": [ "colors = tree_knearest_colors(5, mesh, voxels)\n", "display_voxels_px(voxels, [f\"rgb{c[0],c[1],c[2]}\" for c in colors])" ] }, { "cell_type": "markdown", "metadata": { "id": "s8Ta34iYvzTY" }, "source": [ "### Display using Blocks in Plotly" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 542 }, "id": "B_1Z2bXIt5TP", "outputId": "ca19a34d-98a6-4741-eebc-f490b16dd56d" }, "outputs": [], "source": [ "import plotly.graph_objects as go\n", "import numpy as np\n", "\n", "# Assuming you have 'voxel_grid', 'colors_blocks', and 'voxels' from your previous code\n", "\n", "# Create Mesh3d traces for each occupied voxel\n", "mesh_data = []\n", "for i in range(voxels.sparse_indices.shape[0]):\n", " x, y, z = voxels.sparse_indices[i]\n", " color = colors[i] # Get color from colors_blocks\n", " vertices = np.array([\n", " [x, y, z], [x + 1, y, z], [x + 1, y + 1, z], [x, y + 1, z],\n", " [x, y, z + 1], [x + 1, y, z + 1], [x + 1, y + 1, z + 1], [x, y + 1, z + 1]\n", " ])\n", " faces = np.array([\n", " [0, 1, 2], [0, 2, 3], # Bottom face\n", " [4, 5, 6], [4, 6, 7], # Top face\n", " [0, 1, 5], [0, 5, 4], # Front face\n", " [2, 3, 7], [2, 7, 6], # Back face\n", " [0, 3, 7], [0, 7, 4], # Left face\n", " [1, 2, 6], [1, 6, 5] # Right face\n", " ])\n", " mesh_data.append(go.Mesh3d(\n", " x=vertices[:, 0],\n", " y=vertices[:, 1],\n", " z=vertices[:, 2],\n", " i=faces[:, 0],\n", " j=faces[:, 1],\n", " k=faces[:, 2],\n", " color=f'rgb({color[0]}, {color[1]}, {color[2]})', # Convert to rgb string\n", " flatshading=True\n", " ))\n", "\n", "# Create Plotly figure\n", "fig = go.Figure(data=mesh_data)\n", "fig.show(renderer=\"colab\")" ] }, { "cell_type": "markdown", "metadata": { "id": "Jxvh2XwuwFgP" }, "source": [ "## Routing Algorithm 'Merge Blocks'\n", "\n", "We need to merge blocks into LEGO pieces. Similar colors by threshold merges into uniblock. Prefer large?" ] }, { "cell_type": "code", "execution_count": 131, "metadata": { "id": "PimIkgBqs1fJ" }, "outputs": [], "source": [ "LEGO_COLORS_RGB = np.asarray([\n", " (239, 239, 239), # White\n", " (165, 165, 165), # Light Bluish Gray\n", " (155, 155, 155), # Light Gray\n", " (109, 110, 109), # Dark Bluish Gray\n", " (88, 88, 88), # Dark Gray\n", " (48, 48, 48), # Black\n", " (196, 40, 28), # Red\n", " (214, 0, 0), # Bright Red\n", " (128, 0, 0), # Dark Red\n", " (0, 85, 191), # Blue\n", " (0, 51, 204), # Bright Blue\n", " (0, 32, 96), # Dark Blue\n", " (35, 122, 33), # Green\n", " (0, 153, 0), # Bright Green\n", " (0, 77, 0), # Dark Green\n", " (247, 205, 24), # Yellow\n", " (255, 204, 0), # Bright Yellow\n", " (255, 153, 0), # Dark Yellow\n", " (255, 102, 0), # Orange\n", " (255, 128, 0), # Bright Orange\n", " (124, 72, 36), # Brown\n", " (160, 96, 53), # Light Brown\n", " (215, 194, 149), # Tan\n", " (144, 118, 72), # Dark Tan\n", " (167, 205, 36), # Lime\n", " (242, 176, 61), # Bright Light Orange\n", " (247, 234, 142), # Bright Light Yellow\n", " (115, 150, 200), # Medium Blue\n", " (65, 165, 222), # Medium Azure\n", " (137, 200, 240), # Light Azure\n", " (144, 31, 118), # Magenta\n", " (255, 153, 204), # Pink\n", " (255, 189, 216) # Light Pink\n", "])" ] }, { "cell_type": "code", "execution_count": 194, "metadata": { "id": "DqA4M69ZwPAi" }, "outputs": [], "source": [ "from scipy.spatial.distance import cdist\n", "from sklearn.cluster import KMeans\n", "from scipy.spatial import cKDTree\n", "from colormath.color_objects import sRGBColor, LabColor\n", "from colormath.color_conversions import convert_color\n", "from colormath.color_diff import delta_e_cie1976\n", "\n", "def map_colors_to_lego(model_colors, lego_palette):\n", " \"\"\"\n", " cdist is optimized broadcast OP.\n", " \"\"\"\n", " distances = cdist(model_colors, lego_palette, 'euclidean') # Calculate Euclidean distances\n", " closest_indices = np.argmin(distances, axis=1) # Find indices of minimum distances\n", " return lego_palette[closest_indices]\n", "\n", "\n", "def convert_colors_max_diff(original_colors, predefined_colors):\n", " \"\"\"\n", " Converts colors by minimizing the maximum channel difference.\n", "\n", " Args:\n", " original_colors: A NumPy array of shape (N, 3) representing original RGB colors.\n", " predefined_colors: A NumPy array of shape (M, 3) representing predefined RGB colors.\n", "\n", " Returns:\n", " A NumPy array of shape (N, 3) representing converted RGB colors.\n", " \"\"\"\n", " diffs = np.abs(original_colors[:, np.newaxis, :] - predefined_colors)\n", " max_diffs = np.max(diffs, axis=2)\n", " indices = np.argmin(max_diffs, axis=1)\n", "\n", " converted_colors = predefined_colors[indices]\n", "\n", " return converted_colors\n", "\n", "def quantize_colors(model_colors, k: int = 16):\n", " \"\"\"\n", " quantize colors by fitting into 16 unique colors.\n", " \"\"\"\n", " original_colors = np.array(colors)[:,:3]\n", "\n", " kmeans = KMeans(n_clusters=k, random_state=42)\n", " kmeans.fit(original_colors)\n", "\n", " # Get the representative colors\n", " representative_colors = kmeans.cluster_centers_.astype(int)\n", "\n", " # Transform the original colors to representative colors\n", " transformed_colors = representative_colors[kmeans.labels_]\n", " return transformed_colors\n", "\n", "\n", "def map_color_cie(model_colors, lego_palette):\n", " original_lab = np.array([convert_color(sRGBColor(*rgb, is_upscaled=True), LabColor).get_value_tuple()\n", " for rgb in model_colors])\n", " predefined_lab = np.array([convert_color(sRGBColor(*rgb, is_upscaled=True), LabColor).get_value_tuple()\n", " for rgb in lego_palette])\n", "\n", " original_lab = original_lab[:, np.newaxis, :] # Reshape for broadcasting\n", " predefined_lab = predefined_lab[np.newaxis, :, :] # Reshape for broadcasting\n", " delta_e = np.sqrt(np.sum((original_lab - predefined_lab)**2, axis=2))\n", "\n", " # Find closest predefined color for each original color\n", " indices = np.argmin(delta_e, axis=1)\n", "\n", " # Transform colors\n", " transformed_colors = lego_palette[indices]\n", "\n", " return transformed_colors\n", "\n", "\n", "def normalize_value_to_mid(rgb, target_v=0.7):\n", " import colorsys\n", " r, g, b, _ = rgb\n", " # Scale to [0..1]\n", " rr, gg, bb = (r/255, g/255, b/255)\n", " h, s, v = colorsys.rgb_to_hsv(rr, gg, bb)\n", " # Force to target_v\n", " rr2, gg2, bb2 = colorsys.hsv_to_rgb(h, s, target_v)\n", " # Scale back to [0..255]\n", " return (int(rr2*255), int(gg2*255), int(bb2*255))\n", "\n", "\n", "def lab_color_tfm(colors: np.ndarray, lego_palette: np.ndarray) -> np.ndarray:\n", " from skimage import color\n", "\n", " scaled = rgb_array.astype(np.float32) / 255.0\n", "\n", " # 2) Reshape to (N,1,3) so that rgb2lab sees it as an image of height=N, width=1, channels=3\n", " reshaped = scaled.reshape((-1, 1, 3))\n", "\n", " # 3) Convert to Lab\n", " lab_reshaped = rgb2lab(reshaped) # shape: (N,1,3)\n", "\n", " # 4) Reshape back to (N,3)\n", " lab_array = lab_reshaped.reshape((-1, 3))\n", "\n", "def find_nearest_lego_colors_lab_weighted(\n", " lab_colors: np.ndarray,\n", " lego_palette_lab: np.ndarray,\n", " lego_palette_names: list,\n", " lightness_weight: float = 0.2\n", ") -> np.ndarray:\n", " \"\"\"\n", " Find the nearest LEGO color in Lab space for each input Lab color,\n", " reducing the influence of Lightness (L) in the distance calculation.\n", "\n", " Args:\n", " lab_colors (np.ndarray): (N,3) array of Lab colors (input colors).\n", " lego_palette_lab (np.ndarray): (M,3) array of Lab colors (LEGO palette colors).\n", " lego_palette_names (list): List of M names corresponding to the LEGO colors.\n", " lightness_weight (float): Weight for the L (lightness) component in the distance calculation.\n", "\n", " Returns:\n", " np.ndarray: (N,) array of LEGO color names corresponding to the closest match for each input color.\n", " \"\"\"\n", " # Expand lab_colors to (N,1,3) and lego_palette_lab to (1,M,3)\n", " lab_colors_exp = lab_colors[:, np.newaxis, :] # (N,1,3)\n", " lego_palette_exp = lego_palette_lab[np.newaxis, :, :] # (1,M,3)\n", "\n", " # Apply weights: Scale L component\n", " lab_colors_exp[:, :, 0] *= lightness_weight\n", " lego_palette_exp[:, :, 0] *= lightness_weight\n", "\n", " # Compute weighted Euclidean distance in Lab space (L2 Norm) across the last axis\n", " distances = np.linalg.norm(lab_colors_exp - lego_palette_exp, axis=2) # (N,M)\n", "\n", " # Find the index of the minimum distance for each row\n", " closest_indices = np.argmin(distances, axis=1) # (N,)\n", "\n", " # Map indices to LEGO color names\n", " closest_colors = np.array([lego_palette_names[i] for i in closest_indices])\n", "\n", " return closest_colors\n", "\n", "\n", "# Should I merge colors first or colors and bits at the same time?\n", "def to_df(voxels, colors) -> pl.DataFrame:\n", " df = pl.from_numpy(voxels.sparse_indices, schema=[\"x\", \"z\", \"y\"])\n", " df = df.with_columns(color=pl.Series(colors))\n", " return df\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 443 }, "id": "whURTjlRbog1", "outputId": "2e575ee3-08be-4443-f034-4a02ea9901e8" }, "outputs": [], "source": [ "df" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "R37iNUS_FOgd", "outputId": "e01e300c-8f66-4d7e-a626-82f5ba4c0481" }, "outputs": [], "source": [ "np.unique(np.asarray(mid_colors), axis=0).shape,np.unique(np.asarray(colors)[:,:3], axis=0).shape" ] }, { "cell_type": "code", "execution_count": 197, "metadata": { "id": "QtaCdCb20tUQ" }, "outputs": [], "source": [ "# This merges color while walking along neighbors. It's suboptimal in that it might override a previous group with a new neighbour. Should include visited.\n", "if False:\n", " import pandas as pd\n", " from scipy.spatial.distance import cdist\n", " def get_neighbors(x, y, z):\n", " \"\"\"6-connected neighbors in 3D.\"\"\"\n", " return [\n", " (x+1, y, z), (x-1, y, z),\n", " (x, y+1, z), (x, y-1, z),\n", " (x, y, z+1), (x, y, z-1)\n", " ]\n", "\n", " def coord_to_idx(df: pl.DataFrame) -> dict[tuple[int,int,int], int]:\n", " return dict(zip(zip(df[\"x\"], df[\"y\"], df[\"z\"]), range(len(df))))\n", "\n", " df = df.drop(\"r\", \"b\", \"g\", strict=False).with_columns(colors=pl.Series(colors))\n", " coord_to_idx = coord_to_idx(df)\n", " groups = {}\n", " df = df.with_columns(group=pl.arange(pl.len()))\n", " color_np = df[\"color\"].to_numpy()\n", " color_diff = cdist(color_np, color_np, 'euclidean') # indexed diff\n", "\n", " for idx in range(len(df)):\n", " neighbors = get_neighbors(df[idx, \"x\"], df[idx, \"y\"], df[idx, \"z\"])\n", " for neighbor in neighbors:\n", " if neighbor in coord_to_idx:\n", " neighbor_idx = coord_to_idx[neighbor]\n", " if neighbor_idx < idx:\n", " continue\n", " if (color_diff[idx, neighbor_idx] < 50):\n", " df[neighbor_idx, \"group\"] = df[idx, \"group\"] # bad mutation...\n", "\n", " color_list = pl.col(\"color\").arr\n", " df_group_color = df.with_columns(r=color_list.get(0), b=color_list.get(1), g=color_list.get(2)).group_by(\"group\").agg(pl.mean(\"r\", \"g\", \"b\"))\n", " df = df.join(df_group_color, on=\"group\")\n", " df.head()\n", " df = df.with_columns(color_rgb=pl.concat_str(\"r\", \"b\", \"g\", separator=\",\")).with_columns(\n", " color_rgb = \"rgb(\"+pl.col(\"color_rgb\")+\")\"\n", " )\n", " display_voxels_px(voxels, df[\"color_rgb\"])" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 542 }, "id": "EzdZxLoREyMc", "outputId": "46a654f0-436d-455c-ee81-dd0f8c9a20b0" }, "outputs": [], "source": [ "mid_colors = [normalize_value_to_mid(x, 0.8) for x in colors]\n", "display_voxels_px(voxels, [f\"rgb{c[0],c[1],c[2]}\" for c in mid_colors])" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 542 }, "id": "31jo_asa2uW8", "outputId": "4173b76f-9a8e-4091-8769-6f9adcb36ae7" }, "outputs": [], "source": [ "# map_colors_to_lego, map_color_cie, quantize_colors, convert_colors_max_diff\n", "color_lego = map_color_cie(np.asarray(mid_colors)[:,:3], np.asarray(LEGO_COLORS_RGB))\n", "display_voxels_px(voxels, [f\"rgb{c[0],c[1],c[2]}\" for c in color_lego])" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 542 }, "id": "VgXRAunt5dKW", "outputId": "e6b06c30-8683-4173-a4e4-5f9ec3facdfe" }, "outputs": [], "source": [ "color_lego = quantize_colors(np.asarray(mid_colors)[:,:3], k=16)\n", "display_voxels_px(voxels, [f\"rgb{c[0],c[1],c[2]}\" for c in color_lego])" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 542 }, "id": "t1QvgKbC3lsL", "outputId": "7558cb5c-de8b-4bb5-f0b2-f8685c2ad3b4" }, "outputs": [], "source": [ "display_voxels_px(voxels, [f\"rgb{c[0],c[1],c[2]}\" for c in colors])" ] }, { "cell_type": "code", "execution_count": 202, "metadata": { "id": "aEgErcK3lCxq" }, "outputs": [], "source": [ "df = to_df(voxels, color_lego)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 443 }, "id": "kAGJZc_9f-3e", "outputId": "bd950ea5-6832-4813-97f1-2ccd1b741685" }, "outputs": [], "source": [ "df" ] }, { "cell_type": "code", "execution_count": 204, "metadata": { "id": "Meq7cqqhiis1" }, "outputs": [], "source": [ "BLOCK_SIZES = np.asarray([\n", " [1,1],[1,2],[1,3],[1,4],[1,6],[1,8],\n", " [2,2],[2,3],[2,4],[2,6],[2,8]\n", "])\n", "coords = {(x,y,z) for x,y,z in df.select(\"x\", \"y\", \"z\").to_numpy()}" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "VMqkFCJdg-NZ", "outputId": "f5725408-f8ad-49ed-e2dc-9f05d44d720a" }, "outputs": [], "source": [ "# TODO: make sure user flips figure to stand on z = 0, z being height axis.\n", "\n", "def get_xy_neighbors(x, y, z):\n", " return [(x-1,y,z), (x+1,y,z), (x, y-1,z), (x,y+1,z)]\n", "\n", "y_group_1 = df.filter((pl.col(\"z\") == 16) & (pl.col(\"color\") == [44, 94, 130]))\n", "group_coords = {(x,y,z) for x,y,z in y_group_1[[\"x\", \"y\", \"z\"]].to_numpy()}\n", "\n", "for row in range(len(y_group_1)):\n", " for neighbor in get_xy_neighbors(y_group_1[row, \"x\"], y_group_1[row, \"y\"], y_group_1[row, \"z\"]):\n", " if neighbor in group_coords:\n", " \"found\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "_ZuNj25io0OK" }, "outputs": [], "source": [ "def merge_into_bricks(grouped_df: pl.DataFrame) -> pl.DataFrame:\n", " color_str = grouped_df[0,\"color_str\"]\n", " z_val = grouped_df[0, \"z\"]\n", "\n", " xy_grid = np.zeros((grouped_df[\"x\"].max() + 1, grouped_df[\"y\"].max() +1), dtype=bool)\n", " xy_grid[grouped_df[\"x\"], grouped_df[\"y\"]] = 1\n", " out_rows = []\n", " grouped_df = grouped_df.sort(by=[\"x\", \"y\"])\n", " coords = {(x,y) for x,y in grouped_df[[\"x\", \"y\"]].to_numpy()}\n", "\n", " while coords:\n", " (x0, y0) = coords.pop()\n", " coords.add((x0, y0)) # reinsert until placed\n", "\n", " placed = False\n", " for (width, height) in BLOCK_SIZES:\n", " if x0+width > xy_grid.shape[0] or y0+height > xy_grid.shape[1]:\n", " continue\n", " if np.all(xy_grid[x0:x0+width, y0:y0+height] == 1):\n", " place_block(x0, y0, width, height, coords)\n", " out_rows.append((color_str, z_val, x0, y0, width, height))\n", " placed = True\n", " break\n", "\n", " if not placed:\n", " # fallback to 1x1\n", " coords.remove((x0, y0))\n", " out_rows.append((color_str, z_val, x0, y0, 1, 1))\n", "\n", " return pl.DataFrame(\n", " {\n", " \"color_str\": [row[0] for row in out_rows],\n", " \"z\": [row[1] for row in out_rows],\n", " \"x\": [row[2] for row in out_rows],\n", " \"y\": [row[3] for row in out_rows],\n", " \"width\": [row[4] for row in out_rows],\n", " \"height\": [row[5] for row in out_rows],\n", " }\n", " )\n", "\n", "def can_place_block(x0, y0, w, h, coords):\n", " for xx in range(x0, x0 + w):\n", " for yy in range(y0, y0 + h):\n", " if (xx, yy) not in coords:\n", " return False\n", " return True\n", "\n", "def place_block(x0, y0, w, h, coords):\n", " for xx in range(x0, x0 + w):\n", " for yy in range(y0, y0 + h):\n", " coords.remove((xx, yy))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "edX0wBA7_O-g" }, "outputs": [], "source": [ "def merge_cells_recursive(df_group: pl.DataFrame, coords: set[tuple[int, int, int]]) -> pl.DataFrame:\n", " # Merge by xy_grid as above.\n", "\n", " pass\n", "\n", "\n", "def merge_cells(df_group: pl.DataFrame, coords: set[tuple[int, int, int]]) -> pl.DataFrame:\n", " # df [x, y, z, x2, y2, z2, color] (merged)\n", " # df [x, y, z, color] (unmerged)\n", " pass\n", "\n", "BLOCK_SIZES = [\n", " [1,1],[1,2],[1,3],[1,4],[1,6],[1,8],\n", " [2,1],[3,1],[4,1],[6,1],[8,1],\n", " [2,2],[2,3],[2,4],[2,6],[2,8],\n", " [3,2],[4,2],[6,2],[8,2]\n", "]\n", "# Sort array by area, largest first.\n", "BLOCK_SIZES.sort(key=lambda x: x[0]*x[1], reverse=True)\n", "\n", "coords = {(x,y,z) for x,y,z in df.select(\"x\", \"y\", \"z\").to_numpy()}\n", "# Colors already merged.\n", "df = df.with_columns(color_str = pl.col(\"color\").cast(pl.List(pl.String)).list.join(\"_\"))\n", "merge_into_bricks(df.filter(pl.col(\"z\") == 17))\n", "(df\n", " .group_by(\"color_str\", \"z\")\n", " .map_groups(merge_into_bricks)\n", " .select(w_h=pl.struct(\"width\", \"height\").value_counts()).unnest(\"w_h\")\n", ")\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "id": "8BUgE9SYv3Y2" }, "source": [ "## Utils" ] }, { "cell_type": "markdown", "metadata": { "id": "pSnD2PwLv4wV" }, "source": [ "### Enhance Brightness, Gamma and Saturation (color)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 521 }, "collapsed": true, "id": "DulCp6to6adh", "outputId": "6b64e715-80b0-43a2-ed74-c171e5664431" }, "outputs": [], "source": [ "def enhance_mesh_colors_vectorized(mesh, saturation_boost=1.5, brightness_factor=1.2, gamma=1.8):\n", " \"\"\"\n", " Enhances the saturation, brightness, and optionally applies gamma correction to mesh colors (vectorized).\n", "\n", " Args:\n", " mesh: The trimesh mesh object.\n", " saturation_boost: Factor to boost saturation ( > 1 increases saturation).\n", " brightness_factor: Factor to adjust brightness ( > 1 increases brightness).\n", " gamma: Gamma value for gamma correction (typically between 1.8 and 2.2).\n", " \"\"\"\n", " # Convert RGB to HSV (vectorized)\n", " colors = mesh.visual.vertex_colors.astype(np.float32) / 255.0 # Normalize to 0-1\n", " hsv_colors = np.array([colorsys.rgb_to_hsv(r, g, b) for r, g, b, a in colors])\n", "\n", " # Boost saturation (vectorized)\n", " hsv_colors[:, 1] = np.minimum(hsv_colors[:, 1] * saturation_boost, 1.0)\n", "\n", " # Adjust brightness (vectorized)\n", " hsv_colors[:, 2] = np.minimum(hsv_colors[:, 2] * brightness_factor, 1.0)\n", "\n", " # Gamma correction (vectorized)\n", " hsv_colors[:, 2] = hsv_colors[:, 2]**(1/gamma)\n", "\n", " # Convert back to RGB (vectorized)\n", " rgb_colors = np.array([colorsys.hsv_to_rgb(h, s, v) for h, s, v in hsv_colors])\n", "\n", " # Add alpha channel back\n", " rgb_colors = np.concatenate((rgb_colors, colors[:, 3:]), axis=1)\n", "\n", " # Denormalize and set back to mesh\n", " mesh = mesh.copy()\n", " mesh.visual.vertex_colors = (rgb_colors * 255).astype(np.uint8)\n", " return mesh\n", "\n", "# ... (load mesh)\n", "\n", "# Enhance colors (vectorized)\n", "enhance_mesh_colors_vectorized(mesh, saturation_boost=1.8, brightness_factor=1.2, gamma=2.0).show()" ] }, { "cell_type": "markdown", "metadata": { "id": "AHpn3zunv8IC" }, "source": [ "### Show RGB" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "LYD6Bxf_jvd2" }, "outputs": [], "source": [ "from IPython.display import display, HTML\n", "def show_rgb(rgb_color):\n", " html_code = f'
'\n", "\n", " display(HTML(html_code))" ] }, { "cell_type": "markdown", "metadata": { "id": "DOFUBRglv9X6" }, "source": [ "### Validate Points similar" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "aQOHdarW-Dt-" }, "outputs": [], "source": [ "import plotly.graph_objects as go\n", "import numpy as np\n", "def validate_points_similar(index: int):\n", " \"\"\"N.B. Uses attributes available in notebook!\"\"\"\n", " # Create Plotly figure\n", " fig = go.Figure(data=[\n", " go.Mesh3d(\n", " x=mesh.vertices[:, 0],\n", " y=mesh.vertices[:, 1],\n", " z=mesh.vertices[:, 2],\n", " i=mesh.faces[:, 0],\n", " j=mesh.faces[:, 1],\n", " k=mesh.faces[:, 2],\n", " color='lightgray',\n", " opacity=0.8\n", " ),\n", " go.Scatter3d(\n", " x=mesh.vertices[vertex_indices[index:index+1], 0],\n", " y=mesh.vertices[vertex_indices[index:index+1], 1],\n", " z=mesh.vertices[vertex_indices[index:index+1], 2],\n", " mode='markers',\n", " marker=dict(size=5, color='red'),\n", " name='Mesh Vertex'\n", " ),\n", " go.Scatter3d(\n", " x=[voxels.points[index, 0]],\n", " y=[voxels.points[index, 1]],\n", " z=[voxels.points[index, 2]],\n", " mode='markers',\n", " marker=dict(size=5, color='blue'),\n", " name='Voxel Point'\n", " )\n", " ])\n", "\n", " # Set layout (axis labels, title)\n", " fig.update_layout(\n", " scene=dict(\n", " xaxis_title='X',\n", " yaxis_title='Y',\n", " zaxis_title='Z'\n", " ),\n", " title='Mesh with Two Corresponding Points'\n", " )\n", "\n", " # Enable interactive mode for Colab\n", " fig.show(renderer=\"colab\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 542 }, "id": "DBPOtFzAhtVJ", "outputId": "c8b39ebf-c8c6-4966-95ab-54fe3484a4ae" }, "outputs": [], "source": [ "validate_points_similar(200)" ] } ], "metadata": { "colab": { "collapsed_sections": [ "pSnD2PwLv4wV" ], "provenance": [] }, "kernelspec": { "display_name": "Python 3", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.1" } }, "nbformat": 4, "nbformat_minor": 0 }