|
9 | 9 | detect_separator, |
10 | 10 | is_xml_loaded, |
11 | 11 | _validate_points_array_and_get_indices, |
| 12 | + _as_array, |
| 13 | + _validate_same_shape, |
12 | 14 | ) |
13 | 15 |
|
14 | 16 | from helios.validation import ( |
@@ -438,6 +440,153 @@ def from_numpy_array( |
438 | 440 |
|
439 | 441 | return cls._from_cpp(_cpp_part) |
440 | 442 |
|
| 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 | + |
441 | 590 | @validate_call |
442 | 591 | def _apply_material_to_all_primitives(self, material: Material): |
443 | 592 | """Apply a material to all primitives in the scene part.""" |
|
0 commit comments