Skip to content

Commit

Permalink
Merge pull request #264 from daavoo/add-open3d-integration
Browse files Browse the repository at this point in the history
Add open3d integration
  • Loading branch information
daavoo authored Oct 4, 2019
2 parents b72ea00 + 5264f8b commit ebea260
Show file tree
Hide file tree
Showing 9 changed files with 276 additions and 54 deletions.
56 changes: 56 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,59 @@ With the following concise code:
new_cloud = cloud.get_sample("voxelgrid_nearest", voxelgrid_id=voxelgrid_id, as_PyntCloud=True)
new_cloud.to_file("out_file.npz")
Integration with other libraries
================================

pyntcloud offers seamless integration with other 3D processing libraries.

You can create / convert PyntCloud instances from / to many 3D processing libraries using the `from_instance` / `to_instance` methods:

- `Open3D <https://www.open3d.org>`_

.. code-block:: python
import open3d as o3d
from pyntcloud import PyntCloud
# FROM Open3D
original_triangle_mesh = o3d.io.read_triangle_mesh("diamond.ply")
cloud = PyntCloud.from_instance("open3d", original_triangle_mesh)
assert cloud.mesh is not None
assert np.allclose(cloud.mesh.values, original_triangle_mesh.triangles)
assert np.allclose(cloud.xyz, original_triangle_mesh.vertices)
assert {'red', 'green', 'blue'}.issubset(cloud.points.columns)
assert np.allclose(cloud.points[['red', 'green', 'blue']].values / 255., original_triangle_mesh.vertex_colors)
assert {'nx', 'ny', 'nz'}.issubset(cloud.points.columns)
assert np.allclose(cloud.points[['nx', 'ny', 'nz']].values, original_triangle_mesh.vertex_normals)
# TO Open3D
cloud = PyntCloud.from_file("diamond.ply")
converted_triangle_mesh = cloud.to_instance("open3d", mesh=True) # mesh=True by default
assert isinstance(converted_triangle_mesh, o3d.geometry.TriangleMesh)
assert np.allclose(cloud.xyz, converted_triangle_mesh.vertices)
assert np.allclose(cloud.mesh.values, converted_triangle_mesh.triangles)
- `PyVista <https://docs.pyvista.org>`_

.. code-block:: python
import pyvista as pv
from pyntcloud import PyntCloud
# FROM PyVista
original_point_cloud = pv.read("diamond.ply")
cloud = PyntCloud.from_instance("pyvista", original_point_cloud)
assert np.allclose(cloud.xyz, original_point_cloud.points)
assert {'red', 'green', 'blue'}.issubset(cloud.points.columns)
assert np.allclose(cloud.points[['red', 'green', 'blue']].values, original_point_cloud.point_arrays["RGB"])
assert {'nx', 'ny', 'nz'}.issubset(cloud.points.columns)
assert np.allclose(cloud.points[['nx', 'ny', 'nz']].values, original_point_cloud.point_arrays["Normals"])
# TO PyVista
cloud = PyntCloud.from_file("diamond.ply")
converted_triangle_mesh = cloud.to_instance("open3d", mesh=True)
assert isinstance(converted_triangle_mesh, pv.PolyData)
assert np.allclose(cloud.xyz, converted_triangle_mesh.points)
assert np.allclose(cloud.mesh.values, converted_triangle_mesh.faces[:, 1:])
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@
# built documents.
#
# The short X.Y version.
version = '0.0.1'
version = '0.0.2'
# The full version, including alpha/beta/rc tags.
release = '0.0.1'
release = '0.0.2'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
4 changes: 2 additions & 2 deletions pyntcloud/core_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ def from_file(cls, filename, **kwargs):

@classmethod
def from_instance(cls, library, instance, **kwargs):
"""Extract data from file and construct a PyntCloud with it.
"""Convert library's instance to PyntCloud intstance.
Parameters
----------
Expand Down Expand Up @@ -182,7 +182,7 @@ def to_file(self, filename, also_save=None, **kwargs):
TO_FILE[ext](**kwargs)

def to_instance(self, library, **kwargs):
"""Save PyntCloud data to file.
"""Convert PyntCloud's instance to library's instance.
Parameters
----------
Expand Down
7 changes: 5 additions & 2 deletions pyntcloud/io/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from pyntcloud.io.open3d import from_open3d, to_open3d
from pyntcloud.io.pyvista import from_pyvista, to_pyvista
from .ascii import read_ascii, write_ascii
from .bin import read_bin, write_bin
Expand All @@ -23,7 +24,8 @@
"XYZ": read_ascii,
}
FROM_INSTANCE = {
"PYVISTA": from_pyvista
"PYVISTA": from_pyvista,
"OPEN3D": from_open3d
}

TO_FILE = {
Expand All @@ -38,5 +40,6 @@
"XYZ": write_ascii,
}
TO_INSTANCE = {
"PYVISTA": to_pyvista
"PYVISTA": to_pyvista,
"OPEN3D": to_open3d
}
85 changes: 85 additions & 0 deletions pyntcloud/io/open3d.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import numpy as np
import pandas as pd


def from_open3d(o3d_data, **kwargs):
"""Create a PyntCloud instance from Open3D's PointCloud/TriangleMesh instance"""
try:
import open3d as o3d
except ImportError:
raise ImportError("Open3D must be installed. Try `pip install open3d`")

if not isinstance(o3d_data, (o3d.geometry.PointCloud, o3d.geometry.TriangleMesh)):
raise TypeError(f"Type {type(o3d_data)} not supported for conversion."
f"Expected {o3d.geometry.PointCloud} or {o3d.geometry.TriangleMesh}")

mesh = None
if isinstance(o3d_data, o3d.geometry.TriangleMesh):
mesh = pd.DataFrame(data=np.asarray(o3d_data.triangles),
columns=['v1', 'v2', 'v3'])

points = pd.DataFrame(data=np.asarray(o3d_data.vertices),
columns=["x", "y", "z"])

if o3d_data.vertex_colors:
colors = (np.asarray(o3d_data.vertex_colors) * 255).astype(np.uint8)
points["red"] = colors[:, 0]
points["green"] = colors[:, 1]
points["blue"] = colors[:, 2]

if o3d_data.vertex_normals:
normals = np.asarray(o3d_data.vertex_normals)
points["nx"] = normals[:, 0]
points["ny"] = normals[:, 1]
points["nz"] = normals[:, 2]

elif isinstance(o3d_data, o3d.geometry.PointCloud):
points = pd.DataFrame(data=np.asarray(o3d_data.points),
columns=["x", "y", "z"])

if o3d_data.colors:
colors = (np.asarray(o3d_data.colors) * 255).astype(np.uint8)
points["red"] = colors[:, 0]
points["green"] = colors[:, 1]
points["blue"] = colors[:, 2]

if o3d_data.normals:
normals = np.asarray(o3d_data.normals)
points["nx"] = normals[:, 0]
points["ny"] = normals[:, 1]
points["nz"] = normals[:, 2]

return {
"points": points,
"mesh": mesh
}


def to_open3d(cloud,
mesh=True,
colors=True,
normals=True,
**kwargs):
"""Convert PyntCloud's instance `cloud` to Open3D's PointCloud/TriangleMesh instance"""
try:
import open3d as o3d
except ImportError:
raise ImportError("Open3D must be installed. Try `pip install open3d`")

if mesh and cloud.mesh is not None:
triangle_mesh = o3d.geometry.TriangleMesh()
triangle_mesh.triangles = o3d.utility.Vector3iVector(cloud.mesh[["v1", "v2", "v3"]].values)
triangle_mesh.vertices = o3d.utility.Vector3dVector(cloud.xyz)
if colors and {'red', 'green', 'blue'}.issubset(cloud.points.columns):
triangle_mesh.vertex_colors = o3d.utility.Vector3dVector(cloud.points[['red', 'green', 'blue']].values)
if normals and {'nx', 'ny', 'nz'}.issubset(cloud.points.columns):
triangle_mesh.vertex_normals = o3d.utility.Vector3dVector(cloud.points[['nx', 'ny', 'nz']].values)
return triangle_mesh
else:
point_cloud = o3d.geometry.PointCloud()
point_cloud.points = o3d.utility.Vector3dVector(cloud.xyz)
if colors and {'red', 'green', 'blue'}.issubset(cloud.points.columns):
point_cloud.colors = o3d.utility.Vector3dVector(cloud.points[['red', 'green', 'blue']].values)
if normals and {'nx', 'ny', 'nz'}.issubset(cloud.points.columns):
point_cloud.normals = o3d.utility.Vector3dVector(cloud.points[['nx', 'ny', 'nz']].values)
return point_cloud
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ matplotlib
numba
pytest
pyvista
open3d
82 changes: 82 additions & 0 deletions tests/integration/io/test_from_instance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import pytest
import numpy as np
from pyntcloud import PyntCloud

try:
import pyvista as pv
SKIP_PYVISTA = False
except:
pv = None
SKIP_PYVISTA = True

try:
import open3d as o3d
SKIP_OPEN3D = False
except:
o3d = None
SKIP_OPEN3D = True


@pytest.mark.skipif(SKIP_PYVISTA, reason="Requires PyVista")
def test_pyvista_conversion(data_path):
original_point_cloud = pv.read(str(data_path / "diamond.ply"))
cloud = PyntCloud.from_instance("pyvista", original_point_cloud)
assert np.allclose(cloud.xyz, original_point_cloud.points)
assert {'red', 'green', 'blue'}.issubset(cloud.points.columns)
assert np.allclose(cloud.points[['red', 'green', 'blue']].values, original_point_cloud.point_arrays["RGB"])
assert {'nx', 'ny', 'nz'}.issubset(cloud.points.columns)
assert np.allclose(cloud.points[['nx', 'ny', 'nz']].values, original_point_cloud.point_arrays["Normals"])


@pytest.mark.skipif(SKIP_PYVISTA, reason="Requires PyVista")
def test_pyvista_normals_are_handled():
poly = pv.Sphere()
pc = PyntCloud.from_instance("pyvista", poly)
assert all(x in pc.points.columns for x in ["nx", "ny", "nz"])


@pytest.mark.skipif(SKIP_PYVISTA, reason="Requires PyVista")
def test_pyvista_multicomponent_scalars_are_splitted():
poly = pv.Sphere()
poly.point_arrays["foo"] = np.zeros_like(poly.points)
pc = PyntCloud.from_instance("pyvista", poly)
assert all(x in pc.points.columns for x in ["foo_0", "foo_1", "foo_2"])


@pytest.mark.skipif(SKIP_PYVISTA, reason="Requires PyVista")
def test_pyvista_rgb_is_handled():
""" Serves as regression test for old `in` behaviour that could cause a subtle bug
if poin_arrays contain a field with `name in "RGB"`
"""
poly = pv.Sphere()
poly.point_arrays["RG"] = np.zeros_like(poly.points)[:, :2]
pc = PyntCloud.from_instance("pyvista", poly)
assert all(x in pc.points.columns for x in ["RG_0", "RG_1"])


@pytest.mark.skipif(SKIP_OPEN3D, reason="Requires Open3D")
def test_open3d_point_cloud(data_path):
point_cloud = o3d.io.read_point_cloud(str(data_path.joinpath("diamond.ply")))
cloud = PyntCloud.from_instance("open3d", point_cloud)
assert np.allclose(cloud.xyz, np.asarray(point_cloud.points))
assert {'red', 'green', 'blue'}.issubset(cloud.points.columns)
assert np.allclose(cloud.points[['red', 'green', 'blue']].values / 255., np.asarray(point_cloud.colors))

assert {'nx', 'ny', 'nz'}.issubset(cloud.points.columns)
assert np.allclose(cloud.points[['nx', 'ny', 'nz']].values, np.asarray(point_cloud.normals))


@pytest.mark.skipif(SKIP_OPEN3D, reason="Requires Open3D")
def test_open3d_triangle_mesh(data_path):
triangle_mesh = o3d.io.read_triangle_mesh(str(data_path.joinpath("diamond.ply")))
cloud = PyntCloud.from_instance("open3d", triangle_mesh)
assert cloud.mesh is not None
assert np.allclose(cloud.mesh.values, triangle_mesh.triangles)

assert np.allclose(cloud.xyz, triangle_mesh.vertices)

assert {'red', 'green', 'blue'}.issubset(cloud.points.columns)
assert np.allclose(cloud.points[['red', 'green', 'blue']].values / 255., triangle_mesh.vertex_colors)

assert {'nx', 'ny', 'nz'}.issubset(cloud.points.columns)
assert np.allclose(cloud.points[['nx', 'ny', 'nz']].values, triangle_mesh.vertex_normals)
43 changes: 43 additions & 0 deletions tests/integration/io/test_to_instance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import pytest
import numpy as np
from pyntcloud import PyntCloud

try:
import pyvista as pv
SKIP_PYVISTA = False
except:
pv = None
SKIP_PYVISTA = True

try:
import open3d as o3d
SKIP_OPEN3D = False
except:
o3d = None
SKIP_OPEN3D = True


@pytest.mark.skipif(SKIP_PYVISTA, reason="Requires PyVista")
def test_pyvista_conversion(data_path):
cloud = PyntCloud.from_file(str(data_path.joinpath("diamond.ply")))
poly = cloud.to_instance("pyvista", mesh=True)
assert np.allclose(cloud.xyz, poly.points)
assert np.allclose(cloud.mesh.values, poly.faces[:, 1:])


@pytest.mark.skipif(SKIP_OPEN3D, reason="Requires Open3D")
def test_open3d_point_cloud_conversion(data_path):
cloud = PyntCloud.from_file(str(data_path.joinpath("diamond.ply")))
point_cloud = cloud.to_instance("open3d", mesh=False)
assert isinstance(point_cloud, o3d.geometry.PointCloud)
assert np.allclose(cloud.xyz, point_cloud.points)


@pytest.mark.skipif(SKIP_OPEN3D, reason="Requires Open3D")
def test_open3d_triangle_mesh_conversion(data_path):
cloud = PyntCloud.from_file(str(data_path.joinpath("diamond.ply")))
# mesh=True by default
triangle_mesh = cloud.to_instance("open3d")
assert isinstance(triangle_mesh, o3d.geometry.TriangleMesh)
assert np.allclose(cloud.xyz, triangle_mesh.vertices)
assert np.allclose(cloud.mesh.values, triangle_mesh.triangles)
48 changes: 0 additions & 48 deletions tests/integration/test_core_class.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import os
import pytest
import numpy as np
import pandas as pd
from shutil import rmtree
from pyntcloud import PyntCloud

try:
import pyvista as pv
SKIP_PYVISTA = False
except:
pv = None
SKIP_PYVISTA = True


def test_points():
"""PyntCloud.points.
Expand Down Expand Up @@ -99,43 +91,3 @@ def test_split_on(data_path):
assert len(output) == 8

rmtree("tmp_out")


@pytest.mark.skipif(SKIP_PYVISTA, reason="Requires PyVista")
def test_pyvista_conversion(data_path):
print(data_path)
print(data_path.joinpath("diamond.ply"))
cloud = PyntCloud.from_file(str(data_path.joinpath("diamond.ply")))
poly = cloud.to_instance("pyvista", mesh=True)
pc = PyntCloud.from_instance("pyvista", poly)
assert np.allclose(cloud.points[['x', 'y', 'z']].values, poly.points)
assert np.allclose(cloud.mesh.values, pc.mesh.values)
poly = pv.read(str(data_path.joinpath("diamond.ply")))
pc = PyntCloud.from_instance("pyvista", poly)
assert np.allclose(pc.points[['x', 'y', 'z']].values, poly.points)


@pytest.mark.skipif(SKIP_PYVISTA, reason="Requires PyVista")
def test_pyvista_normals_are_handled():
poly = pv.Sphere()
pc = PyntCloud.from_instance("pyvista", poly)
assert all(x in pc.points.columns for x in ["nx", "ny", "nz"])


@pytest.mark.skipif(SKIP_PYVISTA, reason="Requires PyVista")
def test_pyvista_multicomponent_scalars_are_splitted():
poly = pv.Sphere()
poly.point_arrays["foo"] = np.zeros_like(poly.points)
pc = PyntCloud.from_instance("pyvista", poly)
assert all(x in pc.points.columns for x in ["foo_0", "foo_1", "foo_2"])


@pytest.mark.skipif(SKIP_PYVISTA, reason="Requires PyVista")
def test_pyvista_RGB_is_handled():
""" Serves as regression test for old `in` behaviour that could cause a subtle bug
if poin_arrays contain a field with `name in "RGB"`
"""
poly = pv.Sphere()
poly.point_arrays["RG"] = np.zeros_like(poly.points)[:, :2]
pc = PyntCloud.from_instance("pyvista", poly)
assert all(x in pc.points.columns for x in ["RG_0", "RG_1"])

0 comments on commit ebea260

Please sign in to comment.