Spaces:
Running on Zero
Running on Zero
File size: 12,967 Bytes
2dd4628 73fb7a2 b246c3b 73fb7a2 2dd4628 73fb7a2 2dd4628 73fb7a2 2dd4628 472020a 2dd4628 472020a 2dd4628 472020a 2dd4628 472020a 2dd4628 472020a 2dd4628 472020a 2dd4628 b246c3b 472020a 2dd4628 472020a b246c3b 2dd4628 472020a b246c3b 2dd4628 472020a 2dd4628 b246c3b 472020a 2dd4628 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 | from typing import Optional, Tuple
import numpy as np
import trimesh
from PIL import Image
from instruct_particulate.utils.articulation_utils import plucker_to_axis_point
LINK_COLOR_HEX = (
"#fca5a5",
"#fdba74",
"#fde047",
"#86efac",
"#67e8f9",
"#93c5fd",
"#c4b5fd",
"#f0abfc",
"#f9a8d4",
"#a7f3d0",
"#fcd34d",
"#bfdbfe",
"#ddd6fe",
"#fecaca",
"#bbf7d0",
"#bae6fd",
)
def _hex_to_rgb(color: str) -> tuple[int, int, int]:
color = color.removeprefix("#")
return tuple(int(color[index : index + 2], 16) for index in range(0, 6, 2))
COLORS = tuple(_hex_to_rgb(color) for color in LINK_COLOR_HEX)
ARROW_COLOR_REVOLUTE = (255, 0, 0)
ARROW_COLOR_PRISMATIC = (255, 255, 0)
def create_textured_mesh_parts(mesh_parts, part_ids=None, colors=COLORS, tex_res=256):
# Create a texture map with evenly distributed color blocks
# Use a horizontal strip layout: texture height = tex_res, width = num_parts * tex_res
part_ids = list(range(len(mesh_parts))) if part_ids is None else list(part_ids)
if len(part_ids) != len(mesh_parts):
raise ValueError(
f"part_ids must align with mesh_parts, got {len(part_ids)} ids for {len(mesh_parts)} meshes"
)
texture_height = block_width = tex_res
texture_width = len(mesh_parts) * block_width
texture_array = np.zeros((texture_height, texture_width, 3), dtype=np.uint8)
for i, part_id in enumerate(part_ids):
color_rgb = colors[int(part_id) % len(colors)][:3]
x_start = i * block_width
x_end = (i + 1) * block_width
texture_array[:, x_start:x_end] = color_rgb
texture = Image.fromarray(texture_array)
mesh_parts_colored = []
for i, mesh_part in enumerate(mesh_parts):
# Create UV coordinates specifically for this part
# All faces in this part should point to the same color block
u_center = (i + 0.5) * block_width / texture_width
v_center = 0.5
# Create UV coordinates for all vertices in this submesh
num_part_vertices = len(mesh_part.vertices)
part_uv_coords = np.full((num_part_vertices, 2), [u_center, v_center], dtype=np.float32)
mesh_part.visual = trimesh.visual.TextureVisuals(uv=part_uv_coords, image=texture)
mesh_parts_colored.append(mesh_part)
return mesh_parts_colored
def apply_color_with_texture(mesh: trimesh.Trimesh, color: Tuple, tex_res: int = 16) -> trimesh.Trimesh:
"""
Apply a solid color to a mesh using UV texture coordinates instead of face colors.
This ensures compatibility with Blender and other tools that don't support face colors.
Args:
mesh: The mesh to apply color to
color: Color as tuple (R, G, B) with values 0-1 or (R, G, B, A) with values 0-255
tex_res: Resolution of the texture (default: 16x16)
Returns:
mesh: The mesh with texture applied
"""
# Normalize color to 0-255 range
if len(color) >= 3:
if all(c <= 1.0 for c in color[:3]):
# Color is in 0-1 range, convert to 0-255
color_rgb = tuple(int(c * 255) for c in color[:3])
else:
# Color is already in 0-255 range
color_rgb = tuple(int(c) for c in color[:3])
else:
raise ValueError("Color must have at least 3 components (R, G, B)")
# Create a solid color texture
texture_array = np.full((tex_res, tex_res, 3), color_rgb, dtype=np.uint8)
texture = Image.fromarray(texture_array)
# Create UV coordinates (all pointing to center of texture)
num_vertices = len(mesh.vertices)
uv_coords = np.full((num_vertices, 2), 0.5, dtype=np.float32)
# Apply texture to mesh
mesh.visual = trimesh.visual.TextureVisuals(uv=uv_coords, image=texture)
return mesh
def create_ring(center, normal, major_radius=0.04, minor_radius=0.006, color=(255, 0, 0), segments=32, tube_segments=16):
"""
Create a 3D ring (torus) perpendicular to a given direction.
Args:
center: The center position of the ring (3D point)
normal: The normal direction of the ring plane (will be normalized)
major_radius: The radius of the ring from center to tube center
minor_radius: The radius of the tube itself (ring width)
color: RGB color tuple (can be 0-1 or 0-255 range)
segments: Number of segments around the ring
tube_segments: Number of segments around the tube cross-section
Returns:
trimesh.Trimesh: The ring mesh
"""
center = np.array(center)
normal = np.array(normal)
normal = normal / np.linalg.norm(normal)
# Find two perpendicular vectors to the normal
if abs(normal[2]) < 0.9:
v1 = np.cross(normal, np.array([0, 0, 1]))
else:
v1 = np.cross(normal, np.array([1, 0, 0]))
v1 = v1 / np.linalg.norm(v1)
v2 = np.cross(normal, v1)
v2 = v2 / np.linalg.norm(v2)
# Generate torus vertices
vertices = []
for i in range(segments):
theta = 2 * np.pi * i / segments
# Point on the major circle
circle_point = center + major_radius * (np.cos(theta) * v1 + np.sin(theta) * v2)
# Direction from center to this point on the major circle
radial_dir = np.cos(theta) * v1 + np.sin(theta) * v2
for j in range(tube_segments):
phi = 2 * np.pi * j / tube_segments
# Point on the tube cross-section
tube_offset = minor_radius * (np.cos(phi) * radial_dir + np.sin(phi) * normal)
vertex = circle_point + tube_offset
vertices.append(vertex)
vertices = np.array(vertices)
# Generate faces
faces = []
for i in range(segments):
for j in range(tube_segments):
# Current vertex indices
v0 = i * tube_segments + j
v1 = i * tube_segments + (j + 1) % tube_segments
v2 = ((i + 1) % segments) * tube_segments + (j + 1) % tube_segments
v3 = ((i + 1) % segments) * tube_segments + j
# Create two triangles for this quad
faces.append([v0, v1, v2])
faces.append([v0, v2, v3])
faces = np.array(faces)
# Create mesh with color using UV texture (compatible with Blender)
ring_mesh = trimesh.Trimesh(vertices=vertices, faces=faces, process=False)
ring_mesh = apply_color_with_texture(ring_mesh, color)
return ring_mesh
def create_arrow(
start_point: np.ndarray,
end_point: np.ndarray,
color=(1, 0, 0, 1),
radius: float = 0.03,
radius_tip: float = 0.05
) -> trimesh.Trimesh:
"""
Build a 3-D arrow (cylinder + cone) going from `start_point` to `end_point`.
"""
direction = end_point - start_point
length = np.linalg.norm(direction)
if length == 0:
raise ValueError("start_point and end_point must be different.")
# Unit vector in arrow direction
v_dir = direction / length
# Heuristic: tip is 10 % of length but never longer than 0.07 m
tip_h = min(0.1 * length, 0.04)
body_h = length - tip_h
if body_h <= 0: # extremely short arrow fallback
tip_h = 0.5 * length
body_h = length - tip_h
# Cylinder (body) -- origin on z, height along +z
cyl = trimesh.creation.cylinder(radius=radius, height=body_h, sections=32)
cyl.apply_translation([0, 0, body_h / 2]) # base sits at z = 0
# Cone (tip) -- base at z = 0, apex at z = +tip_h
cone = trimesh.creation.cone(radius=radius_tip, height=tip_h, sections=32)
cone.apply_translation([0, 0, body_h]) # base starts where cylinder ends
# Rotate both meshes from +Z to desired direction
R = trimesh.geometry.align_vectors([0, 0, 1], v_dir)
cyl.apply_transform(R)
cone.apply_transform(R)
# Translate so tail is at start_point
cyl.apply_translation(start_point)
cone.apply_translation(start_point)
cyl = apply_color_with_texture(cyl, color)
cone = apply_color_with_texture(cone, color)
return trimesh.util.concatenate([cyl, cone])
def get_3D_arrow_on_points(
direction: np.ndarray,
points: np.ndarray,
fixed_point: Optional[np.ndarray] = None,
extension: float = 0.05,
min_extension: float = 0.1,
) -> Tuple[float, float]:
"""
Build a 3-D arrow (cylinder + cone) that encloses `points` along `direction`.
"""
# ββ normalise direction ββββββββββββββββββββββββββββββββββββββββββββββββ
direction = np.asarray(direction, dtype=float)
if np.linalg.norm(direction) == 0:
raise ValueError("`direction` must be a non-zero vector.")
d_hat = direction / np.linalg.norm(direction)
# ββ validate points βββββββββββββββββββββββββββββββββββββββββββββββββββ
points = np.asarray(points, dtype=float)
if points.ndim != 2 or points.shape[1] != 3:
raise ValueError("`points` must be of shape (N, 3).")
# ββ choose reference point on axis ββββββββββββββββββββββββββββββββββββ
P0 = (
np.asarray(fixed_point, dtype=float)
if fixed_point is not None
else points.mean(axis=0)
)
# ββ project points onto axis to find extents ββββββββββββββββββββββββββ
scalars = np.dot(points - P0, d_hat)
if scalars.shape[0] > 0:
padding = max(extension * (scalars.max() - scalars.min()), min_extension)
s_min = scalars.min() - padding
s_max = scalars.max() + padding
else:
s_min = -min_extension
s_max = min_extension
start_pt = P0 + s_min * d_hat
end_pt = P0 + s_max * d_hat
return start_pt, end_pt
def _mesh_parts_max_extent(mesh_parts) -> float:
vertices = [
np.asarray(mesh_part.vertices, dtype=np.float64)
for mesh_part in mesh_parts
if len(mesh_part.vertices) > 0
]
if not vertices:
return 1.0
points = np.concatenate(vertices, axis=0)
max_extent = float(np.ptp(points, axis=0).max())
return max(max_extent, 1e-6)
def create_motion_axis_meshes(
mesh_parts,
unique_part_ids: np.ndarray,
is_part_revolute: np.ndarray,
is_part_prismatic: np.ndarray,
revolute_plucker: np.ndarray,
prismatic_axis: np.ndarray,
):
"""Create arrow/ring meshes visualizing predicted joint motion."""
axes = []
visual_scale = _mesh_parts_max_extent(mesh_parts)
arrow_radius = 0.01 * visual_scale
arrow_tip_radius = 0.018 * visual_scale
ring_major_radius = 0.03 * visual_scale
ring_minor_radius = 0.006 * visual_scale
min_axis_extension = 0.1 * visual_scale
for mesh_part, part_id in zip(mesh_parts, unique_part_ids, strict=True):
if is_part_revolute[part_id]:
axis_direction, axis_point = plucker_to_axis_point(revolute_plucker[part_id])
arrow_start, arrow_end = get_3D_arrow_on_points(
axis_direction,
mesh_part.vertices,
fixed_point=axis_point,
extension=0.2,
min_extension=min_axis_extension,
)
axes.append(
create_arrow(
arrow_start,
arrow_end,
color=ARROW_COLOR_REVOLUTE,
radius=arrow_radius,
radius_tip=arrow_tip_radius,
)
)
arrow_direction = arrow_end - arrow_start
axes.append(
create_ring(
arrow_start,
arrow_direction,
major_radius=ring_major_radius,
minor_radius=ring_minor_radius,
color=ARROW_COLOR_REVOLUTE,
)
)
axes.append(
create_ring(
arrow_end,
arrow_direction,
major_radius=ring_major_radius,
minor_radius=ring_minor_radius,
color=ARROW_COLOR_REVOLUTE,
)
)
elif is_part_prismatic[part_id]:
arrow_start, arrow_end = get_3D_arrow_on_points(
prismatic_axis[part_id],
mesh_part.vertices,
extension=0.2,
min_extension=min_axis_extension,
)
axes.append(
create_arrow(
arrow_start,
arrow_end,
color=ARROW_COLOR_PRISMATIC,
radius=arrow_radius,
radius_tip=arrow_tip_radius,
)
)
return axes
|