Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ply-compatible float64 types #323

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions pyntcloud/core_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def from_instance(cls, library, instance, **kwargs):
else:
return cls(**FROM_INSTANCE[library](instance, **kwargs))

def to_file(self, filename, also_save=None, **kwargs):
def to_file(self, filename, also_save=None, force_float32: bool = True, **kwargs):
"""Save PyntCloud data to file.

Parameters
Expand All @@ -166,9 +166,18 @@ def to_file(self, filename, also_save=None, **kwargs):
Names of the attributes that will be extracted from the PyntCloud
to be saved in addition to points. Usually also_save=["mesh"]

force_float32: bool, optional
Default: True
Float64 coordinates were painful to export and re-import in others softwares.
The author decided to force converting the coordinates columns into float32 by default.
See https://github.com/daavoo/pyntcloud/issues/146#issuecomment-369179245
You can now disable this behaviour by setting force_float32=False.

kwargs: only usable in some formats
"""
convert_columns_dtype(self.points, np.float64, np.float32)
if force_float32:
convert_columns_dtype(self.points, np.float64, np.float32)

ext = filename.split(".")[-1].upper()
if ext not in TO_FILE:
raise ValueError(
Expand Down
7 changes: 5 additions & 2 deletions pyntcloud/geometry/models/sphere.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,11 @@ def create_sphere(center=[0, 0, 0], radius=1, n_points=100):
np_axis = round(np.sqrt(n_points - 2), 0) + 1

index = np.arange(0, np.square(np_axis) + 2, 1)
sphere = pd.DataFrame(np.zeros([np.size(index, 0), 3]),
index=index, columns=['x', 'y', 'z'])
sphere = pd.DataFrame(
np.zeros([np.size(index, 0), 3]),
index=index,
columns=['x', 'y', 'z']
)

zmin = center[2] - radius
zmax = center[2] + radius
Expand Down
2 changes: 1 addition & 1 deletion pyntcloud/io/pcd.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def read_pcd(filename):
# TODO what to use as second argument? if buf is None
# (compressed > uncompressed)
# should we read buf as raw binary?
#buf = lzf.decompress(compressed_data, uncompressed_size)
# buf = lzf.decompress(compressed_data, uncompressed_size)
if len(buf) != uncompressed_size:
raise Exception('Error decompressing data')
# the data is stored field-by-field
Expand Down
77 changes: 72 additions & 5 deletions pyntcloud/io/ply.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ def read_ply(filename, allow_bool=False):
data["points"][col] = data["points"][col].astype(
dtypes["vertex"][n][1])

if mesh_size :
if mesh_size:
top = count + points_size

names = np.array([x[0] for x in dtypes["face"]])
Expand Down Expand Up @@ -248,16 +248,83 @@ def describe_element(name, df):
-------
element: list[str]
"""
property_formats = {'f': 'float', 'u': 'uchar', 'i': 'int', 'b': 'bool'}
# map between numpy built-in types and supported ply File Structure types
# see numpy built-in types: https://numpy.org/devdocs/reference/arrays.scalars.html#built-in-scalar-types
# see ply File Structure: http://paulbourke.net/dataformats/ply/
_NotPlyCompatible = "not implemented in ply file structure"
property_formats = {
"b": "char",
"h": "short",
"i": "int",
"l": "double",
"q": _NotPlyCompatible,
"B": "uchar",
"H": "ushort",
"I": "uint",
"L": _NotPlyCompatible,
"Q": _NotPlyCompatible,
"e": _NotPlyCompatible,
"f": "float",
"d": "double",
"g": _NotPlyCompatible,
"F": _NotPlyCompatible,
"D": _NotPlyCompatible,
"G": _NotPlyCompatible,
"?": _NotPlyCompatible,
"M": _NotPlyCompatible,
"m": _NotPlyCompatible,
"O": _NotPlyCompatible,
"S": _NotPlyCompatible,
"U": _NotPlyCompatible,
"V": _NotPlyCompatible,
"p": _NotPlyCompatible,
"P": _NotPlyCompatible,
}
# backward compatibility with https://github.com/daavoo/pyntcloud/pull/321
property_formats["?"] = "bool"

element = ['element ' + name + ' ' + str(len(df))]

if name == 'face':
element.append("property list uchar int vertex_indices")

else:
for i in range(len(df.columns)):
# get first letter of dtype to infer format
f = property_formats[str(df.dtypes[i])[0]]
element.append('property ' + f + ' ' + df.columns.values[i])
column_name = df.columns.values[i]
column_dtype = df.dtypes[i]

f = property_formats[column_dtype.char]
if f == _NotPlyCompatible:
potential_error = TypeError(
f"Property '{column_name}' (dtype: {column_dtype.name}) is {_NotPlyCompatible}"
)

# try downcasting column
column = df[column_name]

downcasted_column = None
if pd.api.types.is_float_dtype(column):
downcasted_column = pd.to_numeric(column, downcast="float")
elif pd.api.types.is_signed_integer_dtype(column):
downcasted_column = pd.to_numeric(column, downcast="signed")
elif pd.api.types.is_unsigned_integer_dtype(column):
downcasted_column = pd.to_numeric(column, downcast="unsigned")

if downcasted_column is None:
# column cannot be downcasted
raise potential_error

downcasted_f = property_formats[downcasted_column.dtype.char]
if downcasted_f == _NotPlyCompatible:
# even downcasted, column is still not ply compatible
raise potential_error

# propagate downcasted column dtype into original dataframe column
# used to keep coherency between .ply headers and binary content
df[column_name] = column.astype(downcasted_column.dtype)

f = downcasted_f

element.append('property ' + f + ' ' + column_name)

return element
4 changes: 2 additions & 2 deletions pyntcloud/plot/matplotlib_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@

def set_proper_aspect_ratio(ax):
extents = np.array([getattr(ax, 'get_{}lim'.format(dim))() for dim in 'xyz'])
sz = extents[:,1] - extents[:,0]
sz = extents[:, 1] - extents[:, 0]
centers = np.mean(extents, axis=1)
maxsize = max(abs(sz))
r = maxsize/2
r = maxsize / 2
for ctr, dim in zip(centers, 'xyz'):
getattr(ax, 'set_{}lim'.format(dim))(ctr - r, ctr + r)

Expand Down
2 changes: 1 addition & 1 deletion pyntcloud/plot/pyvista_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,4 @@ def plot_with_pyvista(cloud, **kwargs):
return plotter.show(use_panel=kwargs.pop("use_panel", None),
title=kwargs.pop("title", None),
screenshot=kwargs.pop("screenshot", False),
cpos=kwargs.pop("cpos", None) )
cpos=kwargs.pop("cpos", None))
2 changes: 1 addition & 1 deletion pyntcloud/plot/voxelgrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def plot_voxelgrid_with_pythreejs(voxel_centers,

centroid, camera_position = get_centroid_and_camera_position(voxel_centers)
camera = pythreejs.PerspectiveCamera(fov=90,
aspect=width/height,
aspect=width / height,
position=camera_position,
up=[0, 0, 1])
mesh = get_voxelgrid_pythreejs(voxel_centers, voxel_colors)
Expand Down
2 changes: 1 addition & 1 deletion pyntcloud/samplers/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def compute(self):

# (n, 1) the 1 is for broadcasting
u = np.random.uniform(low=0., high=1., size=(self.n, 1))
v = np.random.uniform(low=0., high=1-u, size=(self.n, 1))
v = np.random.uniform(low=0., high=1 - u, size=(self.n, 1))

result = pd.DataFrame()

Expand Down
6 changes: 3 additions & 3 deletions pyntcloud/samplers/points.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def cal_distance(self, point, solution_set):
distance_sum = np.zeros(len(point))

for pt in solution_set:
distance_sum += np.diag(np.dot((point[:, :3]-pt[:3]), self.d_metric@(point[:, :3]-pt[:3]).T))
distance_sum += np.diag(np.dot((point[:, :3] - pt[:3]), self.d_metric @ (point[:, :3] - pt[:3]).T))
return distance_sum

def compute(self):
Expand All @@ -70,13 +70,13 @@ def compute(self):
# the sampled points set as the return
select_idx = np.random.randint(low=0, high=len(self.points))
# to remain the shape as (1, n) instead of (n, )
solution_set = remaining_points[select_idx: select_idx+1]
solution_set = remaining_points[select_idx: select_idx + 1]
remaining_points = np.delete(remaining_points, select_idx, 0)

for _ in range(self.n - 1):
distance_sum = self.cal_distance(remaining_points, solution_set)
select_idx = np.argmax(distance_sum)
solution_set = np.concatenate([solution_set, remaining_points[select_idx:select_idx+1]], axis=0)
solution_set = np.concatenate([solution_set, remaining_points[select_idx:select_idx + 1]], axis=0)
remaining_points = np.delete(remaining_points, select_idx, 0)

return pd.DataFrame(solution_set, columns=self.points.columns)
2 changes: 1 addition & 1 deletion pyntcloud/structures/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def get_and_set(self, pyntcloud):

@classmethod
def extract_info(cls, pyntcloud):
"""ABC API"""
"""ABC API"""
info = {
"points": pyntcloud.xyz,
}
Expand Down
2 changes: 1 addition & 1 deletion pyntcloud/structures/voxelgrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def compute(self):
# -1 so index are 0-based; clip for edge cases
self.voxel_x = np.clip(np.searchsorted(self.segments[0], self._points[:, 0]) - 1, 0, self.x_y_z[0])
self.voxel_y = np.clip(np.searchsorted(self.segments[1], self._points[:, 1]) - 1, 0, self.x_y_z[1])
self.voxel_z = np.clip(np.searchsorted(self.segments[2], self._points[:, 2]) - 1, 0, self.x_y_z[2])
self.voxel_z = np.clip(np.searchsorted(self.segments[2], self._points[:, 2]) - 1, 0, self.x_y_z[2])
self.voxel_n = np.ravel_multi_index([self.voxel_x, self.voxel_y, self.voxel_z], self.x_y_z)

# compute center of each voxel
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"pandas",
],
extras_require={
'LAS': ["pylas", "lazrs"],
'LAS': ["pylas", "lazrs"],
'PLOT': ["ipython", "matplotlib", "pyvista"],
'NUMBA': ["numba"]
},
Expand Down
6 changes: 3 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ def data_path():
@pytest.fixture()
def xyz():
return np.array([
[0. , 0. , 0. ],
[0.1, 0.1, 0.1],
[0. , 0. , 0. ], # noqa: E202,E203
[0.1, 0.1, 0.1], # noqa: E202,E203
[0.2, 0.2, 0.2],
[0.5, 0.5, 0.5],
[0.9, 0.9, 0.9],
[1. , 1. , 1. ]], dtype=np.float32)
[1. , 1. , 1. ]], dtype=np.float32) # noqa: E202,E203


@pytest.fixture()
Expand Down
1 change: 0 additions & 1 deletion tests/integration/filters/test_kdtree_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,3 @@ def test_SOR_expected_results(pyntcloud_with_kdtree_and_kdtree_id, k, z_max, exp
z_max=z_max
)
assert_array_equal(result, expected_result)

1 change: 0 additions & 1 deletion tests/integration/filters/test_xyz_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,3 @@ def test_BBOX_expected_results(simple_pyntcloud, bounding_box, expected_result):
**bounding_box
)
assert_array_equal(result, expected_result)

1 change: 1 addition & 0 deletions tests/integration/io/test_from_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def test_obj_issue_226(data_path):

assert "w" in cloud.points.columns


def test_obj_issue_vn(data_path):
"""
Fix type issue in pyntcloud/io/obj.py.
Expand Down
10 changes: 5 additions & 5 deletions tests/integration/io/test_from_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
try:
import pyvista as pv
SKIP_PYVISTA = False
except:
except: # noqa: E722
pv = None
SKIP_PYVISTA = True

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

Expand All @@ -25,7 +25,7 @@ def test_pyvista_conversion(data_path):
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"])
assert np.allclose(cloud.points[['nx', 'ny', 'nz']].values, original_point_cloud.point_arrays["Normals"])
assert cloud.mesh is not None


Expand Down Expand Up @@ -64,7 +64,7 @@ def test_open3d_point_cloud(data_path):
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))
assert np.allclose(cloud.points[['nx', 'ny', 'nz']].values, np.asarray(point_cloud.normals))


@pytest.mark.skipif(SKIP_OPEN3D, reason="Requires Open3D")
Expand All @@ -80,4 +80,4 @@ def test_open3d_triangle_mesh(data_path):
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)
assert np.allclose(cloud.points[['nx', 'ny', 'nz']].values, triangle_mesh.vertex_normals)
1 change: 1 addition & 0 deletions tests/integration/io/test_to_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def test_to_file(tmpdir, diamond, extension, color, mesh, comments):
if comments:
assert written_file.comments == ["PyntCloud is cool"]


def test_to_bin_raises_ValueError_if_invalid_kwargs(tmpdir, diamond):
with pytest.raises(ValueError):
diamond.to_file(str(tmpdir.join("written.bin")), also_save=["mesh"])
Expand Down
6 changes: 3 additions & 3 deletions tests/integration/io/test_to_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
try:
import pyvista as pv
SKIP_PYVISTA = False
except:
except: # noqa: E722
pv = None
SKIP_PYVISTA = True

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

Expand Down Expand Up @@ -41,4 +41,4 @@ def test_open3d_triangle_mesh_conversion(data_path):
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)
assert np.allclose(cloud.mesh.values, triangle_mesh.triangles)
1 change: 0 additions & 1 deletion tests/integration/samplers/test_mesh_samplers.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,3 @@ def test_mesh_random_sampling_sampled_points_bounds(diamond, n):

assert all(sample[["x", "y", "z"]].values.max(0) <= diamond.xyz.max(0))
assert all(sample[["x", "y", "z"]].values.min(0) >= diamond.xyz.min(0))

2 changes: 1 addition & 1 deletion tests/integration/samplers/test_voxelgrid_samplers.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def test_voxelgrid_highest_expected_values(simple_pyntcloud, size_x, expected_n,
voxelgrid_id = simple_pyntcloud.add_structure(
"voxelgrid",
size_x=size_x)
sample = simple_pyntcloud.get_sample(
sample = simple_pyntcloud.get_sample(
"voxelgrid_highest",
voxelgrid_id=voxelgrid_id)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import numpy as np


@pytest.mark.parametrize("scalar_field_name", [
"anisotropy",
"planarity"
Expand Down Expand Up @@ -46,6 +47,3 @@ def test_eigen_sum_values(pyntcloud_and_eigenvalues):
ev=ev)
scalar_field_values = cloud.points[scalar_field].values
assert all(scalar_field_values > 0)



Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,3 @@ def test_normal_scalar_fields_bounds(pyntcloud_with_rgb_and_normals, scalar_fiel
scalar_field_values = pyntcloud_with_rgb_and_normals.points[scalar_field]
assert all(scalar_field_values >= min_val)
assert all(scalar_field_values <= max_val)

1 change: 0 additions & 1 deletion tests/integration/scalar_fields/test_xyz_scalar_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,3 @@ def test_cylindrical_coords_bounds(pyntcloud_with_rgb_and_normals):
degrees=False)
assert all(pyntcloud_with_rgb_and_normals.points["angular_cylindrical"] >= - (np.pi / 2))
assert all(pyntcloud_with_rgb_and_normals.points["angular_cylindrical"] <= (np.pi * 1.5))

3 changes: 2 additions & 1 deletion tests/unit/filters/test_kdtree_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
StatisticalOutlierRemovalFilter
)


@pytest.mark.parametrize("kdtree_id", [
"FOO",
"K",
Expand Down Expand Up @@ -75,4 +76,4 @@ def test_SORFilter_expected_results(pyntcloud_with_kdtree_and_kdtree_id, k, z_ma
filter.extract_info()
result = filter.compute()

assert_array_equal(result, expected_result)
assert_array_equal(result, expected_result)
Loading