Skip to content

Commit df33942

Browse files
add python side for o3d meshes
1 parent f4dbff7 commit df33942

File tree

2 files changed

+169
-0
lines changed

2 files changed

+169
-0
lines changed

python/helios/scene.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
detect_separator,
1010
is_xml_loaded,
1111
_validate_points_array_and_get_indices,
12+
_as_array,
13+
_validate_same_shape,
1214
)
1315

1416
from helios.validation import (
@@ -438,6 +440,153 @@ def from_numpy_array(
438440

439441
return cls._from_cpp(_cpp_part)
440442

443+
@classmethod
444+
def _compose_from_o3d_triangle_mesh(
445+
cls,
446+
geometry,
447+
*,
448+
up_axis: Literal["y", "z"] = "z",
449+
):
450+
if up_axis not in ("y", "z"):
451+
raise ValueError("`up_axis` must be either 'y' or 'z'.")
452+
453+
vertices = _as_array(
454+
geometry.vertices, dtype=np.float64, name="Open3D mesh vertices"
455+
)
456+
triangles = _as_array(
457+
geometry.triangles, dtype=np.int32, name="Open3D mesh triangles"
458+
)
459+
460+
if vertices.shape[0] == 0:
461+
raise ValueError("Open3D mesh is empty.")
462+
if triangles.shape[0] == 0:
463+
raise ValueError("Open3D triangle mesh has no triangles.")
464+
465+
normals = None
466+
if geometry.has_vertex_normals():
467+
normals = _as_array(
468+
geometry.vertex_normals,
469+
dtype=np.float64,
470+
name="Open3D mesh vertex normals",
471+
)
472+
normals = _validate_same_shape(
473+
normals, "vertices", vertices, "Open3D mesh vertex normals"
474+
)
475+
476+
colors = None
477+
if geometry.has_vertex_colors():
478+
colors = _as_array(
479+
geometry.vertex_colors,
480+
dtype=np.float64,
481+
name="Open3D mesh vertex colors",
482+
)
483+
colors = _validate_same_shape(
484+
colors, "vertices", vertices, "Open3D mesh vertex colors"
485+
)
486+
487+
_cpp_part = _helios.read_open3d_mesh_scene_part(
488+
vertices,
489+
triangles,
490+
normals,
491+
colors,
492+
up_axis,
493+
)
494+
495+
return cls._from_cpp(_cpp_part)
496+
497+
@classmethod
498+
def _compose_from_o3d_pointcloud(
499+
cls,
500+
geometry,
501+
*,
502+
voxel_size: PositiveFloat,
503+
max_color_value: NonNegativeFloat = 0.0,
504+
default_normal: R3Vector = np.array(
505+
[np.finfo(np.float64).max] * 3, dtype=np.float64
506+
),
507+
sparse: bool = True,
508+
estimate_normals: bool = False,
509+
snap_neighbor_normal: bool = False,
510+
):
511+
points = _as_array(
512+
geometry.points, dtype=np.float64, name="Open3D point cloud points"
513+
)
514+
normals = (
515+
_as_array(
516+
geometry.normals, dtype=np.float64, name="Open3D point cloud normals"
517+
)
518+
if geometry.has_normals()
519+
else None
520+
)
521+
colors = (
522+
_as_array(
523+
geometry.colors, dtype=np.float64, name="Open3D point cloud colors"
524+
)
525+
if geometry.has_colors()
526+
else None
527+
)
528+
529+
if points.shape[0] == 0:
530+
raise ValueError("Open3D point cloud is empty.")
531+
532+
combined_array = [points]
533+
column_count = 3
534+
if normals is not None:
535+
if normals.shape != points.shape:
536+
raise ValueError(
537+
"The number of normals must match the number of points."
538+
)
539+
combined_array.append(normals)
540+
normals_indices = [column_count, column_count + 1, column_count + 2]
541+
column_count += 3
542+
543+
if colors is not None:
544+
if colors.shape != points.shape:
545+
raise ValueError(
546+
"The number of colors must match the number of points."
547+
)
548+
combined_array.append(colors)
549+
rgb_file_columns = [column_count, column_count + 1, column_count + 2]
550+
column_count += 3
551+
552+
effective_max_color_value = (
553+
1.0 if colors is not None and max_color_value == 0.0 else max_color_value
554+
)
555+
556+
result_array = np.hstack(combined_array)
557+
558+
return cls.from_numpy_array(
559+
points=result_array,
560+
voxel_size=voxel_size,
561+
normals_file_columns=normals_indices if normals is not None else None,
562+
rgb_file_columns=rgb_file_columns if colors is not None else None,
563+
max_color_value=effective_max_color_value,
564+
default_normal=default_normal,
565+
sparse=sparse,
566+
estimate_normals=estimate_normals,
567+
snap_neighbor_normal=snap_neighbor_normal,
568+
)
569+
570+
@classonlymethod
571+
@validate_call
572+
def from_open3d(cls, geometry, **kwargs):
573+
"""Load the scene part from an Open3D geometry."""
574+
try:
575+
import open3d
576+
except ImportError:
577+
raise ImportError(
578+
"Open3D is required for `ScenePart.from_open3d`, but can't be installed via conda. Install it with `pip install open3d`."
579+
)
580+
if isinstance(geometry, open3d.geometry.TriangleMesh):
581+
return cls._compose_from_o3d_triangle_mesh(geometry, **kwargs)
582+
if isinstance(geometry, open3d.geometry.PointCloud):
583+
return cls._compose_from_o3d_pointcloud(geometry, **kwargs)
584+
585+
raise TypeError(
586+
"Unsupported geometry type for `ScenePart.from_o3d`. "
587+
f"Expected open3d.geometry.TriangleMesh or open3d.geometry.PointCloud, got {type(geometry)}."
588+
)
589+
441590
@validate_call
442591
def _apply_material_to_all_primitives(self, material: Material):
443592
"""Apply a material to all primitives in the scene part."""

python/helios/utils.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,26 @@ def _validate_points_array_and_get_indices(
405405
return normals_indices, rgb_indices
406406

407407

408+
def _as_array(value, *, dtype, shape_second_dim: int = 3, name: str) -> np.ndarray:
409+
arr = np.asarray(value, dtype=dtype)
410+
if arr.ndim != 2 or arr.shape[1] != shape_second_dim:
411+
raise ValueError(
412+
f"{name} must have shape (N, {shape_second_dim}). Got {arr.shape}."
413+
)
414+
return np.ascontiguousarray(arr, dtype=dtype)
415+
416+
417+
def _validate_same_shape(
418+
arr: np.ndarray, ref_name: str, ref: np.ndarray, name: str
419+
) -> None:
420+
if arr.shape != ref.shape:
421+
raise ValueError(
422+
f"{name} must have the same shape as {ref_name}. "
423+
f"Got {name}={arr.shape}, {ref_name}={ref.shape}."
424+
)
425+
return np.ascontiguousarray(arr, dtype=arr.dtype)
426+
427+
408428
def is_finalized(obj) -> bool:
409429
"""Return True if the Scene was finalized."""
410430
return getattr(obj, "_is_finalized", False)

0 commit comments

Comments
 (0)