import numpy as np
import open3d as o3d
from bbox_utils.utils import in_google_colab
[docs]class PointCloud:
def __init__(self, point_cloud, *args, **kwargs):
"""Create a point cloud.
Args:
point_cloud (obj): a valid Open3D point cloud
"""
self.in_colab = in_google_colab()
# Used when running tests to disable GUI
self.display_gui = True
if PointCloud.validate_point_cloud(point_cloud):
self.point_cloud = point_cloud
else:
raise TypeError("PointCloud received invalid point cloud")
[docs] @classmethod
def validate_point_cloud(cls, point_cloud):
"""Validates the point cloud
Args:
image (obj): image to validate
Returns:
bool: whether the image is valid.
"""
return isinstance(point_cloud, o3d.cpu.pybind.geometry.PointCloud)
[docs] @classmethod
def load_from_file(cls, file_path, *args, **kwargs):
"""Loads a point cloud from a file
Args:
file_path (str): the path to the file
"""
pcd = o3d.io.read_point_cloud(file_path)
return PointCloud(pcd)
[docs] def display_bboxes(self, bboxes, colors="#ff0000", size=2, *args, **kwargs):
"""Display a list of bounding boxes
Args:
bboxes (list(BoundingBox)): a list of bounding boxes
color (str or list(str)): a valid Plotly color:
The 'color' property is a color and may be specified as:
- A hex string (e.g. '#ff0000')
- An rgb/rgba string (e.g. 'rgb(255,0,0)')
- An hsl/hsla string (e.g. 'hsl(0,100%,50%)')
- An hsv/hsva string (e.g. 'hsv(0,100%,100%)')
- A named CSS color:
aliceblue, antiquewhite, aqua, aquamarine, azure,
beige, bisque, black, blanchedalmond, blue,
blueviolet, brown, burlywood, cadetblue,
chartreuse, chocolate, coral, cornflowerblue,
cornsilk, crimson, cyan, darkblue, darkcyan,
darkgoldenrod, darkgray, darkgrey, darkgreen,
darkkhaki, darkmagenta, darkolivegreen, darkorange,
darkorchid, darkred, darksalmon, darkseagreen,
darkslateblue, darkslategray, darkslategrey,
darkturquoise, darkviolet, deeppink, deepskyblue,
dimgray, dimgrey, dodgerblue, firebrick,
floralwhite, forestgreen, fuchsia, gainsboro,
ghostwhite, gold, goldenrod, gray, grey, green,
greenyellow, honeydew, hotpink, indianred, indigo,
ivory, khaki, lavender, lavenderblush, lawngreen,
lemonchiffon, lightblue, lightcoral, lightcyan,
lightgoldenrodyellow, lightgray, lightgrey,
lightgreen, lightpink, lightsalmon, lightseagreen,
lightskyblue, lightslategray, lightslategrey,
lightsteelblue, lightyellow, lime, limegreen,
linen, magenta, maroon, mediumaquamarine,
mediumblue, mediumorchid, mediumpurple,
mediumseagreen, mediumslateblue, mediumspringgreen,
mediumturquoise, mediumvioletred, midnightblue,
mintcream, mistyrose, moccasin, navajowhite, navy,
oldlace, olive, olivedrab, orange, orangered,
orchid, palegoldenrod, palegreen, paleturquoise,
palevioletred, papayawhip, peachpuff, peru, pink,
plum, powderblue, purple, red, rosybrown,
royalblue, rebeccapurple, saddlebrown, salmon,
sandybrown, seagreen, seashell, sienna, silver,
skyblue, slateblue, slategray, slategrey, snow,
springgreen, steelblue, tan, teal, thistle, tomato,
turquoise, violet, wheat, white, whitesmoke,
yellow, yellowgreen
- A number that will be interpreted as a color
according to mesh3d.colorscale
"""
# Lazy import to allow someone to use PointCloud without installing Plotly
import plotly.express as px
import plotly.graph_objects as go
points = self.points
x, y, z = points[:, 0], points[:, 1], points[:, 2]
# Specify the size of the points (this needs to be a 1D array the same
# length as x, y, and z)
point_size = np.full(x.shape, size)
# Plot it
fig = px.scatter_3d(x=x, y=y, z=z, size=point_size, opacity=1)
# The PCD scatter
data = [go.Scatter3d(x=x, y=y, z=z, mode="markers", marker=dict(size=size))]
# Create a list of colors if colors is just a single string
if isinstance(colors, str):
colors = [colors for i in range(0, len(bboxes))]
# Add any annotations
for idx, bbox in enumerate(bboxes):
# Get corners and triangle vertices
corners, triangle_vertices = bbox.p, bbox.triangle_vertices
data.append(
go.Mesh3d(
x=corners[:, 0],
y=corners[:, 1],
z=corners[:, 2],
i=triangle_vertices[0],
j=triangle_vertices[1],
k=triangle_vertices[2],
opacity=0.6,
color=colors[idx],
flatshading=True,
)
)
fig = go.Figure(data=data)
if self.display_gui: # pragma: no cover
fig.show()
return fig
[docs] def display_bbox(self, bbox, color="#ff0000", size=2, *args, **kwargs):
"""Display a single bounding box
Args:
bbox (BoundingBox): a single bounding box
color (string, optional): a valid Plotly color. Defaults to '#ff0000'
Returns:
Figure: a Plotly figure
"""
return self.display_bboxes([bbox], [color], size)
[docs] def display(self, size=2):
return self.display_bboxes([], colors=[], size=2)
@property
def points(self):
"""Get a np.ndarray representation of the point cloud.
Returns:
np.ndarray: the point cloud's points
"""
return np.asarray(self.point_cloud.points)
@property
def number_of_points(self):
"""The number of points within a point cloud.
Returns:
int: the number of points within a point cloud.
"""
return len(self.points)
[docs] def crop(self, bbox):
"""Extract a point cloud from a 3D bounding box.
Source: https://stackoverflow.com/a/65350251/6942666
Args:
bbox (BoundingBox3D): a 3D bounding box
Returns:
PointCloud: a new point cloud with just the points within
the bounding box.
"""
# Convert the corners array to have type float64
bounding_polygon = bbox.p.astype("float64")
# Create a SelectionPolygonVolume
vol = o3d.visualization.SelectionPolygonVolume()
# You need to specify what axis to orient the polygon to.
# I choose the "Y" axis. I made the max value the maximum Y of
# the polygon vertices and the min value the minimum Y of the
# polygon vertices.
vol.orthogonal_axis = "Y"
vol.axis_max = np.max(bounding_polygon[:, 1])
vol.axis_min = np.min(bounding_polygon[:, 1])
# Set all the Y values to 0 (they aren't needed since we specified what they
# should be using just vol.axis_max and vol.axis_min).
bounding_polygon[:, 1] = 0
# Convert the np.array to a Vector3dVector
vol.bounding_polygon = o3d.utility.Vector3dVector(bounding_polygon)
# Crop the point cloud using the Vector3dVector
cropped_pcd = vol.crop_point_cloud(self.point_cloud)
return PointCloud(cropped_pcd)