# # Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # NVIDIA CORPORATION, its affiliates and licensors retain all intellectual # property and proprietary rights in and to this material, related # documentation and any modifications thereto. Any use, reproduction, # disclosure or distribution of this material and related documentation # without an express license agreement from NVIDIA CORPORATION or # its affiliates is strictly prohibited. # # Standard Library from enum import Enum from typing import List, Tuple # Third Party import numpy as np import torch import trimesh from trimesh.voxel.creation import voxelize # CuRobo from curobo.util.logger import log_warn class SphereFitType(Enum): """Supported sphere fit types are listed here. VOXEL_VOLUME_SAMPLE_SURFACE works best. See :ref:`attach_object_note` for more details. """ #: samples the surface of the mesh to approximate geometry SAMPLE_SURFACE = "sample_surface" #: voxelizes the volume and returns voxel positions that are intersecting with surface. VOXEL_SURFACE = "voxel_surface" #: voxelizes the volume and returns all ocupioed voxel positions. VOXEL_VOLUME = "voxel_volume" #: voxelizes the volume and returns voxel positions that are inside the surface of the geometry VOXEL_VOLUME_INSIDE = "voxel_volume_inside" #: voxelizes the volume and returns voxel positions that are inside the surface, #: along with surface sampled positions VOXEL_VOLUME_SAMPLE_SURFACE = "voxel_volume_sample_surface" def sample_even_fit_mesh(mesh: trimesh.Trimesh, n_spheres: int, sphere_radius: float): n_pts = trimesh.sample.sample_surface_even(mesh, n_spheres)[0] n_radius = [sphere_radius for _ in range(len(n_pts))] return n_pts, n_radius def get_voxel_pitch(mesh: trimesh.Trimesh, n_cubes: int): d = mesh.extents cube_volume = d[0] * d[1] * d[2] v = mesh.volume if v > 0: occupancy = 1.0 - ((cube_volume - v) / cube_volume) else: log_warn("sphere_fit: occupancy test failed, assuming cuboid volume") occupancy = 1.0 # given the extents, find the radius to get number of spheres: pitch = (occupancy * cube_volume / n_cubes) ** (1 / 3) return pitch def voxel_fit_surface_mesh( mesh: trimesh.Trimesh, n_spheres: int, sphere_radius: float, voxelize_method: str = "ray", ): pts, rad = get_voxelgrid_from_mesh(mesh, n_spheres, voxelize_method) if pts is None: return pts, rad pr = trimesh.proximity.ProximityQuery(mesh) if len(pts) <= n_spheres: surface_pts, _, _ = pr.on_surface(pts) n_radius = [sphere_radius for _ in range(len(surface_pts))] return surface_pts, n_radius # compute signed distance: dist = pr.signed_distance(pts) # calculate distance to boundary: dist = np.abs(dist - rad) # get the first n points closest to boundary: _, idx = torch.topk(torch.as_tensor(dist), k=n_spheres, largest=False) n_pts = pts[idx] n_radius = [sphere_radius for _ in range(len(n_pts))] return n_pts, n_radius def get_voxelgrid_from_mesh(mesh: trimesh.Trimesh, n_spheres: int, voxelize_method: str = "ray"): """Get voxel grid from mesh using :py:func:`trimesh.voxel.creation.voxelize`.""" pitch = get_voxel_pitch(mesh, n_spheres) radius = pitch / 2.0 try: voxel = voxelize(mesh, pitch, voxelize_method) voxel = voxel.fill("base") pts = voxel.points rad = np.ravel([radius for _ in range(len(pts))]) except: log_warn("voxelization failed") pts = rad = None return pts, rad def voxel_fit_mesh( mesh: trimesh.Trimesh, n_spheres: int, surface_sphere_radius: float, voxelize_method: str = "ray", ): pts, rad = get_voxelgrid_from_mesh(mesh, n_spheres, voxelize_method) if pts is None: return pts, rad # compute signed distance: pr = trimesh.proximity.ProximityQuery(mesh) dist = pr.signed_distance(pts) # calculate distance to boundary: dist = dist - rad # all negative values are outside the mesh: idx = dist < 0.0 surface_pts, _, _ = pr.on_surface(pts[idx]) pts[idx] = surface_pts rad[idx] = surface_sphere_radius if len(pts) > n_spheres: # remove some surface pts: inside_idx = dist >= 0.0 inside_pts = pts[inside_idx] if len(inside_pts) < n_spheres: new_pts = np.zeros((n_spheres, 3)) new_pts[: len(inside_pts)] = inside_pts new_radius = np.zeros(n_spheres) new_radius[: len(inside_pts)] = rad[inside_idx] new_pts[len(inside_pts) :] = surface_pts[: n_spheres - len(inside_pts)] new_radius[len(inside_pts) :] = surface_sphere_radius pts = new_pts rad = new_radius n_pts = pts n_radius = rad.tolist() return n_pts, n_radius def voxel_fit_volume_sample_surface_mesh( mesh: trimesh.Trimesh, n_spheres: int, surface_sphere_radius: float, voxelize_method: str = "ray", ): pts, rad = voxel_fit_volume_inside_mesh(mesh, 0.75 * n_spheres, voxelize_method) if pts is None: return pts, rad # compute surface points: if len(pts) >= n_spheres: return pts, rad sample_count = n_spheres - (len(pts)) surface_sample_pts, sample_radius = sample_even_fit_mesh( mesh, sample_count, surface_sphere_radius ) pts = np.concatenate([pts, surface_sample_pts]) rad = np.concatenate([rad, sample_radius]) return pts, rad def voxel_fit_volume_inside_mesh( mesh: trimesh.Trimesh, n_spheres: int, voxelize_method: str = "ray", ): pts, rad = get_voxelgrid_from_mesh(mesh, 2 * n_spheres, voxelize_method) if pts is None: return pts, rad # compute signed distance: pr = trimesh.proximity.ProximityQuery(mesh) dist = pr.signed_distance(pts) # calculate distance to boundary: dist = dist - rad # all negative values are outside the mesh: idx = dist > 0.0 n_pts = pts[idx] n_radius = rad[idx].tolist() return n_pts, n_radius def fit_spheres_to_mesh( mesh: trimesh.Trimesh, n_spheres: int, surface_sphere_radius: float = 0.01, fit_type: SphereFitType = SphereFitType.VOXEL_VOLUME_SAMPLE_SURFACE, voxelize_method: str = "ray", ) -> Tuple[np.ndarray, List[float]]: """Approximate a mesh with spheres. See :ref:`attach_object_note` for more details. Args: mesh: Input trimesh n_spheres: _description_ surface_sphere_radius: _description_. Defaults to 0.01. fit_type: _description_. Defaults to SphereFitType.VOXEL_VOLUME_SAMPLE_SURFACE. voxelize_method: _description_. Defaults to "ray". Returns: _description_ """ n_pts = n_radius = None if fit_type == SphereFitType.SAMPLE_SURFACE: n_pts, n_radius = sample_even_fit_mesh(mesh, n_spheres, surface_sphere_radius) elif fit_type == SphereFitType.VOXEL_SURFACE: n_pts, n_radius = voxel_fit_surface_mesh( mesh, n_spheres, surface_sphere_radius, voxelize_method ) elif fit_type == SphereFitType.VOXEL_VOLUME_INSIDE: n_pts, n_radius = voxel_fit_volume_inside_mesh(mesh, n_spheres, voxelize_method) elif fit_type == SphereFitType.VOXEL_VOLUME: n_pts, n_radius = voxel_fit_mesh(mesh, n_spheres, surface_sphere_radius, voxelize_method) elif fit_type == SphereFitType.VOXEL_VOLUME_SAMPLE_SURFACE: n_pts, n_radius = voxel_fit_volume_sample_surface_mesh( mesh, n_spheres, surface_sphere_radius, voxelize_method ) if n_pts is None or len(n_pts) < 1: log_warn("sphere_fit: falling back to sampling surface") n_pts, n_radius = sample_even_fit_mesh(mesh, n_spheres, surface_sphere_radius) if n_pts is not None and len(n_pts) > n_spheres: samples = torch.as_tensor(n_pts) dist = torch.linalg.norm(samples - torch.mean(samples, dim=-1).unsqueeze(1), dim=-1) _, knn_i = dist.topk(n_spheres, largest=True) n_pts = samples[knn_i].cpu().numpy() n_radius = np.ravel(n_radius)[knn_i.cpu().flatten().tolist()].tolist() return n_pts, n_radius