Skip to content
This repository has been archived by the owner on Dec 30, 2023. It is now read-only.

Commit

Permalink
Merge pull request #31 from larsmans/io
Browse files Browse the repository at this point in the history
New Pythonic pcl.{load,save} functions; PLY format support
  • Loading branch information
nzjrs committed Jul 7, 2014
2 parents 3832971 + bc0622b commit 126b4cf
Show file tree
Hide file tree
Showing 12 changed files with 182 additions and 102 deletions.
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
*.pcd

pcl.cpp
pcl.html
pcl/_pcl.cpp
pcl/_pcl.html

*.py[co]
*.so
Expand Down
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
pcl.so: pcl.pyx setup.py pcl_defs.pxd minipcl.cpp
pcl/_pcl.so: pcl/_pcl.pyx setup.py pcl/pcl_defs.pxd pcl/minipcl.cpp
python setup.py build_ext --inplace

test: pcl.so tests/test.py
test: pcl/_pcl.so tests/test.py
nosetests -s

clean:
rm -rf build
rm -f pcl.cpp pcl.so
rm -f pcl/_pcl.cpp pcl/_pcl.so

doc: pcl.so conf.py readme.rst
sphinx-build -b singlehtml -d build/doctrees . build/html
Expand Down
47 changes: 47 additions & 0 deletions pcl/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# XXX do a more specific import!
from ._pcl import *


def load(path, format=None):
"""Load pointcloud from path.
Currently supports PCD and PLY files.
Format should be "pcd", "ply", or None to infer from the pathname.
"""
format = _infer_format(path, format)
p = PointCloud()
try:
loader = getattr(p, "_from_%s_file" % format)
except AttributeError:
raise ValueError("unknown file format %s" % format)
if loader(path):
raise IOError("error while loading pointcloud from %r (format=%r)"
% (path, format))
return p


def save(cloud, path, format=None, binary=False):
"""Save pointcloud to file.
Format should be "pcd", "ply", or None to infer from the pathname.
"""
format = _infer_format(path, format)
try:
dumper = getattr(cloud, "_to_%s_file" % format)
except AttributeError:
raise ValueError("unknown file format %s" % format)
if dumper(path, binary):
raise IOError("error while saving pointcloud to %r (format=%r)"
% (path, format))


def _infer_format(path, format):
if format is not None:
return format.lower()

for candidate in ["pcd", "ply"]:
if path.endswith("." + candidate):
return candidate

raise ValueError("Could not determine file format from pathname %s" % path)
137 changes: 84 additions & 53 deletions pcl.pyx → pcl/_pcl.pyx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
#cython: embedsignature=True

from collections import Sequence
import numbers
import numpy as np

cimport numpy as cnp

cimport pcl_defs as cpp

cimport cython
from cython.operator import dereference as deref
from libcpp.string cimport string
from libcpp cimport bool
Expand Down Expand Up @@ -117,12 +120,31 @@ cdef class SegmentationNormal:
mpcl_sacnormal_set_axis(deref(self.me),ax,ay,az)

cdef class PointCloud:
"""
Represents a class of points, supporting the PointXYZ type.
"""Represents a cloud of points in 3-d space.
A point cloud can be initialized from either a NumPy ndarray of shape
(n_points, 3), from a list of triples, or from an integer n to create an
"empty" cloud of n points.
To load a point cloud from disk, use pcl.load.
"""
cdef cpp.PointCloud[cpp.PointXYZ] *thisptr
def __cinit__(self):

def __cinit__(self, init=None):
self.thisptr = new cpp.PointCloud[cpp.PointXYZ]()

if init is None:
return
elif isinstance(init, (numbers.Integral, np.integer)):
self.resize(init)
elif isinstance(init, np.ndarray):
self.from_array(init)
elif isinstance(init, Sequence):
self.from_list(init)
else:
raise TypeError("Can't initialize a PointCloud from a %s"
% type(init))

def __dealloc__(self):
del self.thisptr
property width:
Expand All @@ -138,6 +160,7 @@ cdef class PointCloud:
""" property containing whether the cloud is dense or not """
def __get__(self): return self.thisptr.is_dense

@cython.boundscheck(False)
def from_array(self, cnp.ndarray[cnp.float32_t, ndim=2] arr not None):
"""
Fill this object from a 2D numpy array (float32)
Expand All @@ -149,43 +172,43 @@ cdef class PointCloud:
self.thisptr.width = npts
self.thisptr.height = 1

cdef cpp.PointXYZ *p
for i in range(npts):
self.thisptr.at(i).x = arr[i,0]
self.thisptr.at(i).y = arr[i,1]
self.thisptr.at(i).z = arr[i,2]
p = &self.thisptr.at(i)
p.x, p.y, p.z = arr[i, 0], arr[i, 1], arr[i, 2]

@cython.boundscheck(False)
def to_array(self):
"""
Return this object as a 2D numpy array (float32)
"""
cdef float x,y,z
cdef cnp.npy_intp n = self.thisptr.size()
cdef cnp.ndarray[float, ndim=2] result = np.empty([n,3], dtype=np.float32)
cdef cnp.ndarray[cnp.float32_t, ndim=2, mode="c"] result
cdef cpp.PointXYZ *p

result = np.empty((n, 3), dtype=np.float32)

for i in range(n):
x = self.thisptr.at(i).x
y = self.thisptr.at(i).y
z = self.thisptr.at(i).z
result[i,0] = x
result[i,1] = y
result[i,2] = z
p = &self.thisptr.at(i)
result[i, 0] = p.x
result[i, 1] = p.y
result[i, 2] = p.z
return result

def from_list(self, _list):
"""
Fill this pointcloud from a list of 3-tuples
"""
assert len(_list)
assert len(_list[0]) == 3

cdef Py_ssize_t npts = len(_list)
cdef cpp.PointXYZ *p

self.resize(npts)
self.thisptr.width = npts
self.thisptr.height = 1
for i,l in enumerate(_list):
self.thisptr.at(i).x = l[0]
self.thisptr.at(i).y = l[1]
self.thisptr.at(i).z = l[2]
p = &self.thisptr.at(i)
p.x, p.y, p.z = l

def to_list(self):
"""
Expand All @@ -200,46 +223,54 @@ cdef class PointCloud:
"""
Return a point (3-tuple) at the given row/column
"""
#grr.... the following doesnt compile to valid
#cython.. so just take the perf hit
#cdef PointXYZ &p = self.thisptr.at(x,y)
cdef x = self.thisptr.at(row,col).x
cdef y = self.thisptr.at(row,col).y
cdef z = self.thisptr.at(row,col).z
return x,y,z
cdef cpp.PointXYZ *p = &self.thisptr.at(row, col)
return p.x, p.y, p.z

def __getitem__(self, cnp.npy_intp idx):
cdef x = self.thisptr.at(idx).x
cdef y = self.thisptr.at(idx).y
cdef z = self.thisptr.at(idx).z
return x,y,z
cdef cpp.PointXYZ *p = &self.thisptr.at(idx)
return p.x, p.y, p.z

def from_file(self, char *f):
"""
Fill this pointcloud from a file (a local path).
Only pcd files supported currently.
Deprecated; use pcl.load instead.
"""
return self._from_pcd_file(f)

def _from_pcd_file(self, const char *s):
cdef int error = 0
with nogil:
ok = cpp.loadPCDFile(string(s), deref(self.thisptr))
return error

def _from_ply_file(self, const char *s):
cdef int ok = 0
cdef string s = string(f)
if f.endswith(".pcd"):
ok = cpp.loadPCDFile(s, deref(self.thisptr))
else:
raise ValueError("Incorrect file extension (must be .pcd)")
return ok
with nogil:
error = cpp.loadPLYFile(string(s), deref(self.thisptr))
return error

def to_file(self, char *f, bool ascii=True):
"""
Save this pointcloud to a local file.
Only saving to binary or ascii pcd is supported
def to_file(self, const char *fname, bool ascii=True):
"""Save pointcloud to a file in PCD format.
Deprecated: use pcl.save instead.
"""
cdef bool binary = not ascii
cdef int ok = 0
return self._to_pcd_file(fname, not ascii)

def _to_pcd_file(self, const char *f, bool binary=False):
cdef int error = 0
cdef string s = string(f)
if f.endswith(".pcd"):
ok = cpp.savePCDFile(s, deref(self.thisptr), binary)
else:
raise ValueError("Incorrect file extension (must be .pcd)")
return ok
with nogil:
error = cpp.savePCDFile(s, deref(self.thisptr), binary)
return error

def _to_ply_file(self, const char *f, bool binary=False):
cdef int error = 0
cdef string s = string(f)
with nogil:
error = cpp.savePLYFile(s, deref(self.thisptr), binary)
return error

def make_segmenter(self):
"""
Expand Down Expand Up @@ -608,13 +639,13 @@ cdef class OctreePointCloudSearch(OctreePointCloud):

def __dealloc__(self):
del self.me

"""
Search for all neighbors of query point that are within a given radius.
Returns: (k_indices, k_sqr_distances)
"""

def radius_search (self, point, double radius, unsigned int max_nn = 0):
"""
Search for all neighbors of query point that are within a given radius.
Returns: (k_indices, k_sqr_distances)
"""
cdef vector[int] k_indices
cdef vector[float] k_sqr_distances
if max_nn > 0:
Expand Down
File renamed without changes.
File renamed without changes.
26 changes: 17 additions & 9 deletions pcl_defs.pxd → pcl/pcl_defs.pxd
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from libc.stddef cimport size_t

from libcpp.vector cimport vector
from libcpp.string cimport string
from libcpp cimport bool
Expand All @@ -7,15 +9,15 @@ from vector cimport vector as vector2

cdef extern from "pcl/point_cloud.h" namespace "pcl":
cdef cppclass PointCloud[T]:
PointCloud()
PointCloud(int, int)
PointCloud() except +
PointCloud(unsigned int, unsigned int) except +
unsigned int width
unsigned int height
bool is_dense
void resize(int)
int size()
T& operator[](int)
T& at(int)
void resize(size_t) except +
size_t size()
T& operator[](size_t)
T& at(size_t)
T& at(int, int)
shared_ptr[PointCloud[T]] makeShared()

Expand Down Expand Up @@ -120,8 +122,15 @@ ctypedef PointIndices PointIndices_t
ctypedef shared_ptr[PointIndices] PointIndicesPtr_t

cdef extern from "pcl/io/pcd_io.h" namespace "pcl::io":
int loadPCDFile (string file_name, PointCloud[PointXYZ] cloud)
int savePCDFile (string file_name, PointCloud[PointXYZ] cloud, bool binary_mode)
int load(string file_name, PointCloud[PointXYZ] &cloud) nogil
int loadPCDFile(string file_name, PointCloud[PointXYZ] &cloud) nogil
int savePCDFile(string file_name, PointCloud[PointXYZ] &cloud,
bool binary_mode) nogil

cdef extern from "pcl/io/ply_io.h" namespace "pcl::io":
int loadPLYFile(string file_name, PointCloud[PointXYZ] &cloud) nogil
int savePLYFile(string file_name, PointCloud[PointXYZ] &cloud,
bool binary_mode) nogil

#http://dev.pointclouds.org/issues/624
#cdef extern from "pcl/io/ply_io.h" namespace "pcl::io":
Expand Down Expand Up @@ -200,4 +209,3 @@ cdef extern from "pcl/kdtree/kdtree_flann.h" namespace "pcl":
int, int, vector[int], vector[float])

ctypedef KdTreeFLANN[PointXYZ] KdTreeFLANN_t

File renamed without changes.
File renamed without changes.
10 changes: 4 additions & 6 deletions readme.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ for interacting with numpy. For example (from tests/test.py)
import pcl
import numpy as np
p = pcl.PointCloud()
p.from_array(np.array([[1,2,3],[3,4,5]], dtype=np.float32))
p = pcl.PointCloud(np.array([[1, 2, 3], [3, 4, 5]], dtype=np.float32))
seg = p.make_segmenter()
seg.set_model_type(pcl.SACMODEL_PLANE)
seg.set_method_type(pcl.SAC_RANSAC)
Expand All @@ -46,8 +45,7 @@ or, for smoothing
.. code-block:: python
import pcl
p = pcl.PointCloud()
p.from_file("C/table_scene_lms400.pcd")
p = pcl.load("C/table_scene_lms400.pcd")
fil = p.make_statistical_outlier_filter()
fil.set_mean_k (50)
fil.set_std_dev_mul_thresh (1.0)
Expand Down Expand Up @@ -76,8 +74,8 @@ and CentOS 6.5 with
A note about types
------------------

Point Cloud is a heavily templated API, and consequently mapping this into python
using Cython is challenging.
Point Cloud is a heavily templated API, and consequently mapping this into
Python using Cython is challenging.

It is written in Cython, and implements enough hard bits of the API
(from Cythons perspective, i.e the template/smart_ptr bits) to
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ def pkgconfig(flag):
author='John Stowers',
author_email='[email protected]',
license='BSD',
ext_modules=[Extension("pcl", ["pcl.pyx", "minipcl.cpp"],
packages=["pcl"],
ext_modules=[Extension("pcl._pcl", ["pcl/_pcl.pyx", "pcl/minipcl.cpp"],
language="c++", **ext_args)],
cmdclass={'build_ext': build_ext}
)
Loading

0 comments on commit 126b4cf

Please sign in to comment.