241 lines
8.1 KiB
Python
241 lines
8.1 KiB
Python
#
|
|
# 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
|