Skip to content

Commit

Permalink
Merge pull request #30 from cemsbv/27-add-class-convexhull3d
Browse files Browse the repository at this point in the history
Add class ConvexHull3D
  • Loading branch information
PabloVasconez authored Nov 15, 2023
2 parents f57fa28 + e977ae2 commit 532d434
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 0 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ module = [
"plxscripting.*",
"skspatial.*",
"numpy.*",
"scipy.*",
"matplotlib.*",
"requests.*",
"nuclei.*",
Expand Down
110 changes: 110 additions & 0 deletions src/plxcontroller/geometry_3d/convex_hull_3d.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from __future__ import annotations

from typing import List, Sequence, Tuple

import numpy as np
from scipy import spatial

from plxcontroller.geometry_3d.bounding_box_3d import BoundingBox3D
from plxcontroller.geometry_3d.point_3d import Point3D


class ConvexHull3D:
"""
A class with information about a convex hull in the 3D space.
"""

def __init__(self, points: Sequence[Point3D | Tuple[float, float, float]]):
"""Initializes a ConvexHull3D instance.
Parameters
----------
points : List[Point3D | Tuple[float, float, float]]
a list of the points from which the convex hull will be created.
Raises
------
TypeError
if points of its (sub)items are not of the expected type.
ValueError
- if length of points is not >= 4.
- if item of points is tuple but its length of any tuple is not exactly 3.
- if a convex hull cannot be computed from the given points.
"""
# Validate input
# points
if not isinstance(points, list):
raise TypeError(
f"Unexpected type for points. Expected list, but got: {type(points)}"
)
# point items and subtimes
for i, point in enumerate(points):
if not isinstance(point, (Point3D, tuple)):
raise TypeError(
f"Unexpected type for item of points at index {i}. Expected Point3D or tuple, but got: {type(point)}"
)
if isinstance(point, tuple):
if not len(point) == 3:
raise ValueError(
f"Unexpected tuple length for item of points at index {i}. Expected tuple length = 3, but got: {len(point)}"
)
if not all(isinstance(a, (float, int)) for a in point):
raise TypeError(
f"Unexpected type for tuple items of point at index {i}. Expected float, but got: {tuple(isinstance(a, float) for a in point)}"
)

# Validate length of list
if not len(points) >= 4:
raise ValueError(
f"Unexpected length of points. Expected length >= 4, but got: {len(points)}"
)

# Cast tuples to points
casted_points: List[Point3D] = []
for point in points:
if isinstance(point, Point3D):
casted_points.append(point)
else:
casted_points.append(Point3D(x=point[0], y=point[1], z=point[2]))

# Compute the convex hull of all the points
try:
convex_hull = spatial.ConvexHull(
points=np.vstack([point.coordinates for point in casted_points])
)
except spatial.QhullError as e:
raise ValueError(
"Cannot create ConvexHull3D from given points, the followin error "
+ f"was raised when computing the convex hull: {e}"
)

# Store the points and the indices of a
self._points = casted_points
self._vertex_indices = list(convex_hull.vertices)

@property
def points(self) -> List[Point3D]:
"""Returns all the points of the convex hull."""
return self._points

@property
def vertex_indices(self) -> List[int]:
"""Returns the indices of the vertices of the convex hull."""
return self._vertex_indices

@property
def vertices(self) -> List[Point3D]:
"""Returns the vertices of the convex hull as points."""
return [self._points[i] for i in self._vertex_indices]

@property
def bounding_box(self) -> BoundingBox3D:
"""Returns the bounding box of the convex hull."""
return BoundingBox3D(
x_min=min([vertex.x for vertex in self.vertices]),
y_min=min([vertex.y for vertex in self.vertices]),
z_min=min([vertex.z for vertex in self.vertices]),
x_max=max([vertex.x for vertex in self.vertices]),
y_max=max([vertex.y for vertex in self.vertices]),
z_max=max([vertex.z for vertex in self.vertices]),
)
16 changes: 16 additions & 0 deletions src/plxcontroller/geometry_3d/operations_3d.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from __future__ import annotations

from typing import List

import numpy as np
from scipy import spatial
from skspatial.objects import Plane as ScikitSpatialPlane

from plxcontroller.geometry_3d.point_3d import Point3D
Expand Down Expand Up @@ -65,3 +68,16 @@ def project_vertically_point_onto_polygon_3d(
z = point.z

return Point3D(x=point.x, y=point.y, z=z)


def get_convex_hull_vertices_3d(points: List[Point3D]) -> List[Point3D]:
convex_hull = spatial.ConvexHull(
points=np.vstack([point.coordinates for point in points])
)

vertices = []
for vertix_index in convex_hull.vertices:
vertix = convex_hull.points[vertix_index]
vertices.append(Point3D(x=vertix[0], y=vertix[1], z=vertix[2]))

return vertices
57 changes: 57 additions & 0 deletions tests/test_geometry_3d/test_convex_hull_3d.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import pytest

from plxcontroller.geometry_3d.convex_hull_3d import ConvexHull3D
from plxcontroller.geometry_3d.point_3d import Point3D


def test_convex_hull_3d() -> None:
"""
Tests the methods of the class ConvexHull3D.
"""

# Assert invalid input
with pytest.raises(TypeError, match="Expected list"):
ConvexHull3D(points="invalid input")

with pytest.raises(TypeError, match="Expected Point3D or tuple"):
ConvexHull3D(points=["invalid input"])

with pytest.raises(ValueError, match="Expected tuple length = 3"):
ConvexHull3D(points=[(1.0, 2.0)])

with pytest.raises(TypeError, match="Expected float"):
ConvexHull3D(points=[(1.0, 2.0, "invalid input")])

with pytest.raises(ValueError, match="Expected length >= 4"):
ConvexHull3D(points=[])

with pytest.raises(
ValueError, match="Cannot create ConvexHull3D from given points"
):
ConvexHull3D(
points=[
Point3D(-5.0, -5.0, -5.0),
Point3D(5.0, -5.0, -5.0),
Point3D(5.0, 5.0, -5.0),
Point3D(-5.0, 5.0, -5.0),
]
)

# Assert instance is correctly created with valid input
points = [
Point3D(-5.0, -5.0, -5.0),
Point3D(5.0, -5.0, -5.0),
Point3D(5.0, 5.0, -5.0),
Point3D(-5.0, 5.0, -5.0),
Point3D(-5.0, -5.0, 5.0),
Point3D(5.0, -5.0, 5.0),
Point3D(5.0, 5.0, 5.0),
Point3D(-5.0, 5.0, 5.0),
Point3D(0.0, 0.0, 0.0),
]

convex_hull = ConvexHull3D(points=points)

assert convex_hull.points == points
assert convex_hull.vertex_indices == list(range(0, 8))
assert convex_hull.vertices == points[:-1]

0 comments on commit 532d434

Please sign in to comment.