103 changes: 103 additions & 0 deletions pdal/PyMesh.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/******************************************************************************
* Copyright (c) 2021, Runette Software Ltd (www.runette.co.uk)
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following
* conditions are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided
* with the distribution.
* * Neither the name of Hobu, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
* OF SUCH DAMAGE.
****************************************************************************/

#pragma once


#include <pdal/PointView.hpp>
#include <numpy/ndarraytypes.h>

#include <utility>

namespace pdal
{
namespace python
{
class MeshIter;

class PDAL_DLL Mesh
{
public:
using Shape = std::array<size_t, 3>;

bool hasMesh;

Mesh();

~Mesh();

void update(PointViewPtr view);
PyArrayObject *getPythonArray() const;

bool rowMajor() const;
Shape shape() const;
MeshIter& iterator();


private:
inline PyObject* buildNumpyDescription(PointViewPtr view) const;

PyArrayObject* m_mesh;

Mesh& operator=(Mesh const& rhs);
bool m_rowMajor;
Shape m_shape {};
std::vector<std::unique_ptr<MeshIter>> m_iterators;
};

class MeshIter
{
public:
MeshIter(const MeshIter&) = delete;
MeshIter() = delete;

MeshIter(Mesh& mesh);
~MeshIter();

MeshIter& operator++();
operator bool () const;
char *operator * () const;

private:
NpyIter *m_iter;
NpyIter_IterNextFunc *m_iterNext;
char **m_data;
npy_intp *m_size;
npy_intp *m_stride;
bool m_done;
};

} // namespace python
} // namespace pdal

19 changes: 19 additions & 0 deletions pdal/PyPipeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
#include <pdal/pdal_features.hpp>

#include "PyArray.hpp"
#include "PyMesh.hpp"

namespace pdal
{
Expand Down Expand Up @@ -169,6 +170,24 @@ std::vector<Array *> Pipeline::getArrays() const
return output;
}

std::vector<Mesh *> Pipeline::getMeshes() const
{
std::vector<Mesh *> output;

if (!m_executor->executed())
throw python_error("call execute() before fetching the mesh");

const PointViewSet& pvset = m_executor->getManagerConst().views();

for (auto i: pvset)
{
Mesh *mesh = new python::Mesh;
mesh->update(i);
output.push_back(mesh);
}
return output;
}

} // namespace python
} // namespace pdal

3 changes: 2 additions & 1 deletion pdal/PyPipeline.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ namespace python
{

class Array;
class Mesh;

class python_error : public std::runtime_error
{
Expand Down Expand Up @@ -84,7 +85,7 @@ class Pipeline
return m_executor->getLog();
}
std::vector<pdal::python::Array *> getArrays() const;

std::vector<pdal::python::Mesh *> getMeshes() const;
void setLogLevel(int level);
int getLogLevel() const;

Expand Down
21 changes: 21 additions & 0 deletions pdal/libpdalpython.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ cdef extern from "PyArray.hpp" namespace "pdal::python":
Array(np.ndarray) except +
void *getPythonArray() except+

cdef extern from "PyMesh.hpp" namespace "pdal::python":
cdef cppclass Mesh:
Mesh(np.ndarray) except +
void *getPythonArray() except +

cdef extern from "PyPipeline.hpp" namespace "pdal::python":
cdef cppclass Pipeline:
Pipeline(const char* ) except +
Expand All @@ -52,6 +57,7 @@ cdef extern from "PyPipeline.hpp" namespace "pdal::python":
string getSchema() except +
string getLog() except +
vector[Array*] getArrays() except +
vector[Mesh*] getMeshes() except +
int getLogLevel()
void setLogLevel(int)

Expand Down Expand Up @@ -152,6 +158,21 @@ cdef class PyPipeline:
inc(it)
return output

property meshes:

def __get__(self):
v = self.thisptr.getMeshes()
output = []
cdef vector[Mesh *].iterator it = v.begin()
cdef Mesh* m
while it != v.end():
ptr = deref(it)
m = ptr#.get()
o = m.getPythonArray()
output.append(<object>o)
del ptr
inc(it)
return output

def execute(self):
if not self.thisptr:
Expand Down
23 changes: 23 additions & 0 deletions pdal/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,26 @@ def validate(self):
def get_arrays(self):
return self.p.arrays
arrays = property(get_arrays)

def get_meshes(self):
return self.p.meshes
meshes = property(get_meshes)

def get_meshio(self, idx: int):
try:
from meshio import Mesh
except ModuleNotFoundError:
raise RuntimeError(
"The get_meshio function can only be used if you have installed meshio. Try pip install meshio")
array = self.arrays[idx]
mesh = self.meshes[idx]
if len(mesh) == 0:
return None
return Mesh(
np.stack((array['X'], array['Y'], array['Z']), 1),
[(
"triangle",
np.stack((mesh['A'], mesh['B'], mesh['C']), 1)
)
]
)
5 changes: 5 additions & 0 deletions test/data/mesh.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
"test/data/1.2-with-color.las",
{"type": "filters.splitter", "length": 1000},
{"type": "filters.delaunay"}
]
27 changes: 27 additions & 0 deletions test/test_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,33 @@ def test_fetch_dimensions(self):
self.assertLess(len(dims), 100)
self.assertGreater(len(dims), 71)

class TestMesh(PDALTest):
@unittest.skipUnless(os.path.exists(os.path.join(DATADIRECTORY, 'sort.json')),
"missing test data")
def test_no_execute(self):
"""Does fetching meshes without executing throw an exception"""
json = self.fetch_json('sort.json')
r = pdal.Pipeline(json)
with self.assertRaises(RuntimeError):
r.meshes

@unittest.skipUnless(os.path.exists(os.path.join(DATADIRECTORY, 'mesh.json')),
"missing test data")
def test_mesh(self):
"""Can we fetch PDAL face data as a numpy array"""
json = self.fetch_json('mesh.json')
r = pdal.Pipeline(json)
r.validate()
points = r.execute()
self.assertEqual(points, 1065)
meshes = r.meshes
self.assertEqual(len(meshes), 24)

m = meshes[0]
self.assertEqual(str(m.dtype), "[('A', '<u4'), ('B', '<u4'), ('C', '<u4')]")
self.assertEqual(len(m),134)
self.assertEqual(m[0][0], 29)

def test_suite():
return unittest.TestSuite(
[TestPipeline()])
Expand Down