diff --git a/.gitignore b/.gitignore index 563e905..f3314aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +.DS_Store +concaveman.egg-info +build/ + # Prerequisites *.d diff --git a/demos/demos.py b/demos/demos.py new file mode 100755 index 0000000..62bf0ee --- /dev/null +++ b/demos/demos.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python + +""" +demos / checks for concaveman +""" + +import numpy as np + +import matplotlib.pyplot as plt + +from concaveman import ConcaveHull + +def circular_points(N): + """ + create a bunch of random points in a circle + """ + # create a bunch of random points + points_in_circle = [] + + while len(points_in_circle) < N: + n = max(100, N // 2) +# print(f"picking {n} random points") + points = (np.random.random((n, 2)) - 0.5) * 100 + + # check which ones are in the circle + points_in_circle.extend(points[np.hypot(points[:,0], points[:,1]) <= 50.0]) + # print(f"there are now {len(points_in_circle)} in the circle") + return np.array(points_in_circle[:N]) + +# get some points +points = circular_points(100) + +# compute the hulls +hull = ConcaveHull(points, concavity=1.0) +hull_1 = hull.cc_hull + +hull.concavity = 3.0 +hull.compute() +hull_2 = hull.cc_hull + + +hull.concavity = np.inf +hull.compute() +hull_inf = hull.cc_hull + + +fig, ax = plt.subplots() +ax.plot(points[:, 0], points[:, 1], '.') +ax.plot(hull_1[:, 0], hull_1[:, 1], label="alpha=1.0") +ax.plot(hull_2[:, 0], hull_2[:, 1], label="alpha=3.0") +ax.plot(hull_inf[:, 0], hull_inf[:, 1], label="alpha=Inf") +ax.legend() + +ax.set_xlim(-80, 80) +ax.set_ylim(-80, 80) +ax.set_aspect('equal', 'box') + +plt.show() + diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..7bece54 --- /dev/null +++ b/setup.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python + +from setuptools import setup, Extension + +from Cython.Build import cythonize + +import os +import numpy as np # For the include directory. + +from pathlib import Path + +rootpath = Path(__file__).parent.resolve() + +long_description = open(rootpath / 'README.md').read() + +include_dirs = [np.get_include(), + str(rootpath / 'src' / 'cpp')] + +# # Need the stdint header for Windows (VS2008). +# if sys.platform.startswith('win') and sys.version_info.major <= 2: +# include_dirs.append(os.path.join('rootpath', 'src', 'win_headers')) + + +ext_modules = cythonize([Extension("concaveman.build_hull", + ["src/concaveman/build_hull.pyx", + "src/cpp/concaveman.cpp"], + include_dirs=include_dirs, + language="c++", + # this work on the mac, should work in Linux + # for Windows: who knows? + extra_compile_args=['-std=c++11'], + )]) + + +def extract_version(): + fname = os.path.join(rootpath, 'src', 'concaveman', '__init__.py') + with open(fname) as f: + for line in f: + if (line.startswith('__version__')): + version = line.split('=')[1].strip().strip('"') + break + else: + raise ValueError("Couldn't find __version__ in %s" % fname) + return version + +setup( + name="concaveman", + version=extract_version(), + description="Python wrappers around a C++ concave hull Implementation", + long_description=long_description, + author="s.adaszewski, Christopher H. Barker", + author_email="Chris.Barker@noaa.gov", + url="https://github.com/NOAA-ORR-ERD", + license="BSD", + # keywords = "", + ext_modules=ext_modules, + packages=["concaveman", "concaveman/tests"], + package_dir={'': 'src'}, + install_requires=['numpy', 'scipy'], + setup_requires=['cython>0.29'], + tests_require=['pytest'], + classifiers=[ + "Development Status :: 4 - Beta", + "License :: Public Domain", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Operating System :: OS Independent", + "Programming Language :: C++", + "Programming Language :: Cython", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: Utilities", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Visualization", + ], +) diff --git a/src/concaveman/__init__.py b/src/concaveman/__init__.py new file mode 100644 index 0000000..fd51ea1 --- /dev/null +++ b/src/concaveman/__init__.py @@ -0,0 +1,16 @@ + +__version__ = "0.1.0" + +import numpy as np + +from .concaveman import ConcaveHull + + +# an example square with a couple interior points +EXAMPLE_SQUARE = np.array(((5.0, 10.0), + (5.0, 20.0), + (10.0, 20.0), + (10.0, 10.0), + (6.0, 13.0), + (8.0, 18.0), + )) diff --git a/src/concaveman/build_hull.pyx b/src/concaveman/build_hull.pyx new file mode 100644 index 0000000..981ecaa --- /dev/null +++ b/src/concaveman/build_hull.pyx @@ -0,0 +1,106 @@ +# cython: language_level=3 + +""" +wrapper of concaveman + +NOTE: after working on this for a while, I realized that concaveman.cpp was +mostly a wrapper that was set up to call the templates from C, with +regular C arrays. + +But we should be able to call the Templates directly from Cython, so save that +translation step. + +but other than some wasted data copying, this works. + +NOTE: I removed the function pointer for the free() call, as this code is now using that + malloc'ed pointer to build the returning numpy array -- so should be freed by + python when the numpy array is deleted. +""" + +import numpy as np +cimport numpy as cnp + +from libc.stdlib cimport free +from libc.string cimport memcpy + + +cdef extern from "pyconcaveman.h": + + void pyconcaveman2d(double *points_c, + size_t num_points, + int *hull_points_c, + size_t num_hull_points, + double concavity, + double lengthThreshold, + double **concave_points_c, + size_t *num_concave_points, + ) + +cpdef concave_hull(cnp.float64_t[:,:] points, + int[:] hull, + double concavity=2.0, + double length_threshold=0.0): + """ + compute the concave hull of a bunch of points + + :param points: The points to make the hull out of. + :type points: Nx2 array of float64 type + + :param hull: The convex hull of the points + :type points: 1D array of int32 + + :param concavity=2.0: concavity parameter: large means smoother + :type concavity: python float + + :param length_threshold: + :type length_threshold: python float + """ + + if points.shape[1] != 2: + raise ValueError('points must be an Nx2 array') + + # now a memoryview, so any isn't helpful here. + # should this check be at a higher level? + # if np.any(hull >= len(points)) or np.any(hull < 0): + # raise ValueError('hull indices out of bounds') + + cdef cnp.float64_t* p_concave_points = NULL + cdef size_t num_concave_points = 0 + + # print("in cython: about to call pyconcaveman2d") + + pyconcaveman2d(&points[0, 0], + len(points), + &hull[0], + len(hull), + concavity, + length_threshold, + &p_concave_points, + &num_concave_points, + ) + + # print("cpp concave hull returned") + # print("num concave points:", num_concave_points) + + # create an array to return + cdef cnp.ndarray[cnp.float64_t, ndim=2, mode="c"] arr_concave_points + arr_concave_points = np.zeros((num_concave_points, 2), dtype=np.float64) + + # loping through element by element + # cdef cnp.float64_t* temp = &arr_concave_points[0, 0] + # for i in range(num_concave_points * 2): + # temp[i] = p_concave_points[i] + + # copy the data to the ndarray's memory block directly + memcpy(&arr_concave_points[0, 0], + p_concave_points, + sizeof(cnp.float64_t) * num_concave_points * 2) + + # print('in cython again: concave_points:\n', arr_concave_points) + + # As we are compiling the C++ code all together, the + # plain free() should work. + # on OS-X, it doesn't crash nor leak memory. + free(p_concave_points) + + return arr_concave_points diff --git a/src/concaveman/concaveman.py b/src/concaveman/concaveman.py new file mode 100644 index 0000000..e6f0737 --- /dev/null +++ b/src/concaveman/concaveman.py @@ -0,0 +1,100 @@ +import numpy as np +import scipy.spatial + +from .build_hull import concave_hull + + +class ConcaveHull(): + """ + class modeled after the scipy.spatial.ConvexHull implementation + + One of these days, we could add area and other computations. + """ + + def __init__(self, points, hull=None, concavity=2.0, length_threshold=0.0): + + self.points = points + + if hull is not None: + if len(hull.shape) != 1: + raise ValueError('hull must be a 1-D array') + + if np.any(hull >= len(points)) or np.any(hull < 0): + raise ValueError('hull indices out of bounds') + self.cvx_hull = hull + self.cvx_hull = scipy.spatial.ConvexHull(points).vertices + + self.concavity = concavity + self.length_threshold = length_threshold + + self.compute() + + @property + def concavity(self): + return self._concavity + @concavity.setter + def concavity(self, value): + self._concavity = value + self._cc_hull = None + + @property + def length_threshold(self): + return self._length_threshold + @length_threshold.setter + def length_threshold(self, value): + self._length_threshold = value + self._cc_hull = None + + @property + def points(self): + return self._points + @points.setter + def points(self, points): + points = np.asarray(points, dtype=np.float64) + if len(points.shape) != 2 or points.shape[1] != 2: + raise ValueError('points must be an Nx2 array') + self._points = points + self.cvx_hull = None + self._cc_hull = None + + @property + def cc_hull(self): + if self._cc_hull is None: + self.compute() + return self._cc_hull + + + def compute(self): + """ + compute the concave hull -- should be called after changing any parameters + """ + if self.cvx_hull is None: + self.cvx_hull = scipy.spatial.ConvexHull(self.points).vertices + + self._cc_hull = concave_hull(self.points, + self.cvx_hull, + self.concavity, + self.length_threshold) + + + + + +# def concaveman2d(points, hull=None, concavity=2.0, length_threshold=0.0): +# points = np.asrray(points, dytpe=np.float64) + +# if len(points.shape) != 2 or points.shape[0] != 2: +# raise ValueError('points must be an Nx2 array') + +# if hull is not None: +# if len(hull.shape) != 1: +# raise ValueError('hull must be a 1-D array') + +# if np.any(hull >= len(points)) or np.any(hull < 0): +# raise ValueError('hull indices out of bounds') +# else: +# # compute the convex hull +# # compute the ConvexHull +# hull = scipy.spatial.ConvexHull(points).vertices + +# return concave_hull(points, hull, concavity, length_threshold) diff --git a/src/concaveman/tests/__init__.py b/src/concaveman/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/concaveman/tests/test_build_hull.py b/src/concaveman/tests/test_build_hull.py new file mode 100644 index 0000000..b453569 --- /dev/null +++ b/src/concaveman/tests/test_build_hull.py @@ -0,0 +1,139 @@ +""" +test_build_hull.py +""" +import sys +import numpy as np +import scipy.spatial +from concaveman.build_hull import concave_hull +from concaveman import ConcaveHull, EXAMPLE_SQUARE + + +def test_build_hull(): + """ + a simple call of the function + """ + points = np.array([[0, 0], [.25, .15], [1, 0], [1, 1]], dtype=np.float64) + hull = np.array([0, 2, 3], dtype=np.int32) + + print("about to call concave_hull") + cvx_hull = concave_hull(points, hull) + print("concave_hull returned") + print(cvx_hull) + + assert np.alltrue(cvx_hull == [[0.25, 0.15], + [0., 0.], + [1., 0.], + [1., 1.], + ]) + + +def test_ConcaveHull(): + """ + test with defaults + """ + points = EXAMPLE_SQUARE + hull = ConcaveHull(EXAMPLE_SQUARE, concavity=2.0, length_threshold=0.0) + + print(hull.cvx_hull) + + # convex hull should be correct + assert sorted(hull.cvx_hull) == [0, 1, 2, 3] + + print(hull.cc_hull) + + + # same results as first got -- correct? + assert hull.cc_hull.tolist() == [[5., 20.], + [6., 13.], + [5., 10.], + [10., 10.], + [10., 20.], + ] + +def test_ConcaveHull_inf_concavity(): + """ + test with defaults + """ + points = EXAMPLE_SQUARE + hull = ConcaveHull(EXAMPLE_SQUARE, + concavity=float("inf"), + length_threshold=0.0) + + print(hull.cvx_hull) + + # convex hull should be correct + assert sorted(hull.cvx_hull) == [0, 1, 2, 3] + + print(hull.cc_hull) + + + # should be the outer square + assert hull.cc_hull.tolist() == [[5., 20.], + [5., 10.], + [10., 10.], + [10., 20.], + ] + +def test_ConcaveHull_small_concavity(): + """ + test with defaults + """ + points = EXAMPLE_SQUARE + hull = ConcaveHull(EXAMPLE_SQUARE, + concavity= -5.0, + length_threshold=0.0) + + print(points) + print(hull.cvx_hull) + + # convex hull should be correct + assert sorted(hull.cvx_hull) == [0, 1, 2, 3] + + print(hull.cc_hull) + + + # should be the outer square + assert hull.cc_hull.tolist() == [[5., 20.], + [5., 10.], + [10., 10.], + [10., 20.], + ] + + +def check_memory(): + """ + not a real test, but something to run to make sure that there isn't a memory leak + + runs an infinite loop, so you can see if the memory use is growing. + + I ran this for a good while, and memory use was rock-steady :-) + + """ + # create a whole bunch of points + N = 100000 + + while True: + print(f"computing a hull of {N} points") + points = np.random.random((N, 2)) * 1000 + + # compute the ConvexHull + cvx_hull = scipy.spatial.ConvexHull(points) + + print(f"convex hull has {len(cvx_hull.vertices)} points") + # print(cvx_hull.vertices) + + # call concave_hull + cc_hull = concave_hull(points, cvx_hull.vertices) + print(f"concave hull has {len(cc_hull)} points") + + # print(cc_hull) + + +if __name__ == "__main__": + # test_simple() + + check_memory() + + + + diff --git a/src/cpp/concaveman.cpp b/src/cpp/concaveman.cpp new file mode 100644 index 0000000..e468248 --- /dev/null +++ b/src/cpp/concaveman.cpp @@ -0,0 +1,78 @@ +#if 0 +g++ -std=c++11 -shared concaveman.cpp -o libconcaveman.so +exit 0 +#endif + +// +// Author: Stanislaw Adaszewski, 2019 +// + +#include "concaveman.h" + +// extern "C" { +// void pyconcaveman2d(double *points_c, size_t num_points, +// int *hull_points_c, size_t num_hull_points, +// double concavity, double lengthThreshold, +// double **concave_points_c, size_t *num_concave_points, +// void(**p_free)(void*)); +// } + +void pyconcaveman2d(double *points_c, size_t num_points, + int *hull_points_c, size_t num_hull_points, + double concavity, double lengthThreshold, + double **p_concave_points_c, + size_t *p_num_concave_points) +// void(**p_free)(void*)) + { + + // std::cout << "pyconcaveman2d(), concavity: " << concavity << + // " lengthThreshold: " << lengthThreshold << std::endl; + + typedef double T; + typedef std::array point_type; + + std::vector points(num_points); + for (auto i = 0; i < num_points; i++) { + points[i] = { points_c[i << 1], points_c[(i << 1) + 1] }; + } + + // std::cout << "points:" << std::endl; + // for (auto &p : points) + // std::cout << p[0] << " " << p[1] << std::endl; + + // std::cout << "num points: " << num_points << std::endl; + + std::vector hull(num_hull_points); + for (auto i = 0; i < num_hull_points; i++) { + hull[i] = hull_points_c[i]; + } + + // std::cout << "hull:" << std::endl; + // for (auto &i : hull) + // std::cout << i << std::endl; + + // std::cout << "num hull points: " << num_hull_points << std::endl; + + auto concave_points = concaveman(points, hull, concavity, lengthThreshold); + + // std::cout << "concave_points:" << std::endl; + // for (auto &p : concave_points) + // std::cout << p[0] << " " << p[1] << std::endl; + + // std::cout << "number of cchull points: " << concave_points.size() << std::endl; + + // std::cout << "about to copy points" << std::endl; + + double *concave_points_c = *p_concave_points_c = (double*) malloc(sizeof(double) * 2 * concave_points.size()); + for (auto i = 0; i < concave_points.size(); i++) { + concave_points_c[i << 1] = concave_points[i][0]; + concave_points_c[(i << 1) + 1] = concave_points[i][1]; + } + + // std::cout << "about to return from cpp function" << std::endl; + + *p_num_concave_points = concave_points.size(); + // free is called in the Cython wrapper + // *p_free = free; +} + diff --git a/src/main/cpp/concaveman.h b/src/cpp/concaveman.h similarity index 99% rename from src/main/cpp/concaveman.h rename to src/cpp/concaveman.h index 1a45bf5..930cb42 100644 --- a/src/main/cpp/concaveman.h +++ b/src/cpp/concaveman.h @@ -24,6 +24,7 @@ //#define DEBUG // uncomment to dump debug info to screen //#define DEBUG_2 // uncomment to dump second-level debug info to screen + template std::unique_ptr make_unique(Args&&... args) { return std::unique_ptr(new T(std::forward(args)...)); @@ -457,12 +458,12 @@ template class CircularList { } ~CircularList() { -#ifdef DEBUG +#ifdef DEBUG std::cout << "~CircularList()" << std::endl; #endif auto node = m_last; while (true) { -#ifdef DEBUG +#ifdef DEBUG // std::cout << (i++) << std::endl; #endif auto tmp = node; diff --git a/src/cpp/pyconcaveman.h b/src/cpp/pyconcaveman.h new file mode 100644 index 0000000..72b7909 --- /dev/null +++ b/src/cpp/pyconcaveman.h @@ -0,0 +1,68 @@ +#if 0 +g++ -std=c++11 -shared concaveman.cpp -o libconcaveman.so +exit 0 +#endif + +// +// Author: Stanislaw Adaszewski, 2019 +// + +#include "concaveman.h" + +// extern "C" { +// void pyconcaveman2d(double *points_c, size_t num_points, +// int *hull_points_c, size_t num_hull_points, +// double concavity, double lengthThreshold, +// double **concave_points_c, size_t *num_concave_points, +// void(**p_free)(void*)); +// } + +void pyconcaveman2d(double *points_c, size_t num_points, + int *hull_points_c, size_t num_hull_points, + double concavity, double lengthThreshold, + double **p_concave_points_c, + size_t *p_num_concave_points); + + // void(**p_free)(void*)); + +// { + +// std::cout << "pyconcaveman2d(), concavity: " << concavity << +// " lengthThreshold: " << lengthThreshold << std::endl; + +// typedef double T; +// typedef std::array point_type; + +// std::vector points(num_points); +// for (auto i = 0; i < num_points; i++) { +// points[i] = { points_c[i << 1], points_c[(i << 1) + 1] }; +// } + +// std::cout << "points:" << std::endl; +// for (auto &p : points) +// std::cout << p[0] << " " << p[1] << std::endl; + +// std::vector hull(num_hull_points); +// for (auto i = 0; i < num_hull_points; i++) { +// hull[i] = hull_points_c[i]; +// } + +// std::cout << "hull:" << std::endl; +// for (auto &i : hull) +// std::cout << i << std::endl; + +// auto concave_points = concaveman(points, hull, concavity, lengthThreshold); + +// std::cout << "concave_points:" << std::endl; +// for (auto &p : concave_points) +// std::cout << p[0] << " " << p[1] << std::endl; + +// double *concave_points_c = *p_concave_points_c = (double*) malloc(sizeof(double) * 2 * concave_points.size()); +// for (auto i = 0; i < concave_points.size(); i++) { +// concave_points_c[i << 1] = concave_points[i][0]; +// concave_points_c[(i << 1) + 1] = concave_points[i][1]; +// } + +// *p_num_concave_points = concave_points.size(); +// *p_free = free; +// } diff --git a/src/main/python/concaveman.py b/src/cpp/python_cffi/concaveman.py similarity index 100% rename from src/main/python/concaveman.py rename to src/cpp/python_cffi/concaveman.py diff --git a/src/main/python/demo.py b/src/cpp/python_cffi/demo.py similarity index 100% rename from src/main/python/demo.py rename to src/cpp/python_cffi/demo.py diff --git a/src/main/cpp/concaveman.cpp b/src/main/cpp/concaveman.cpp deleted file mode 100644 index 10798bc..0000000 --- a/src/main/cpp/concaveman.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#if 0 -g++ -std=c++11 -shared concaveman.cpp -o libconcaveman.so -exit 0 -#endif - -// -// Author: Stanislaw Adaszewski, 2019 -// - -#include "concaveman.h" - -extern "C" { - void pyconcaveman2d(double *points_c, size_t num_points, - int *hull_points_c, size_t num_hull_points, - double concavity, double lengthThreshold, - double **concave_points_c, size_t *num_concave_points, - void(**p_free)(void*)); -} - -void pyconcaveman2d(double *points_c, size_t num_points, - int *hull_points_c, size_t num_hull_points, - double concavity, double lengthThreshold, - double **p_concave_points_c, - size_t *p_num_concave_points, - void(**p_free)(void*)) { - - std::cout << "pyconcaveman2d(), concavity: " << concavity << - " lengthThreshold: " << lengthThreshold << std::endl; - - typedef double T; - typedef std::array point_type; - - std::vector points(num_points); - for (auto i = 0; i < num_points; i++) { - points[i] = { points_c[i << 1], points_c[(i << 1) + 1] }; - } - - std::cout << "points:" << std::endl; - for (auto &p : points) - std::cout << p[0] << " " << p[1] << std::endl; - - std::vector hull(num_hull_points); - for (auto i = 0; i < num_hull_points; i++) { - hull[i] = hull_points_c[i]; - } - - std::cout << "hull:" << std::endl; - for (auto &i : hull) - std::cout << i << std::endl; - - auto concave_points = concaveman(points, hull, concavity, lengthThreshold); - - std::cout << "concave_points:" << std::endl; - for (auto &p : concave_points) - std::cout << p[0] << " " << p[1] << std::endl; - - double *concave_points_c = *p_concave_points_c = (double*) malloc(sizeof(double) * 2 * concave_points.size()); - for (auto i = 0; i < concave_points.size(); i++) { - concave_points_c[i << 1] = concave_points[i][0]; - concave_points_c[(i << 1) + 1] = concave_points[i][1]; - } - - *p_num_concave_points = concave_points.size(); - *p_free = free; -}