From 3b38de00d616b6f87d754e9262ec7f7c802c8b47 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Mon, 24 Aug 2020 16:21:47 -0700 Subject: [PATCH 1/9] new structure for python/cython module --- .gitignore | 2 ++ src/concaveman/__init__.py | 0 src/{main => }/cpp/concaveman.cpp | 0 src/{main => }/cpp/concaveman.h | 0 src/{main/python => cpp/python_cffi}/concaveman.py | 0 src/{main/python => cpp/python_cffi}/demo.py | 0 6 files changed, 2 insertions(+) create mode 100644 src/concaveman/__init__.py rename src/{main => }/cpp/concaveman.cpp (100%) rename src/{main => }/cpp/concaveman.h (100%) rename src/{main/python => cpp/python_cffi}/concaveman.py (100%) rename src/{main/python => cpp/python_cffi}/demo.py (100%) diff --git a/.gitignore b/.gitignore index 563e905..ea1e12f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.DS_Store + # Prerequisites *.d diff --git a/src/concaveman/__init__.py b/src/concaveman/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/main/cpp/concaveman.cpp b/src/cpp/concaveman.cpp similarity index 100% rename from src/main/cpp/concaveman.cpp rename to src/cpp/concaveman.cpp diff --git a/src/main/cpp/concaveman.h b/src/cpp/concaveman.h similarity index 100% rename from src/main/cpp/concaveman.h rename to src/cpp/concaveman.h 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 From 539f10d2013f82eaf37ba004470d045a1c761302 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Mon, 24 Aug 2020 18:19:18 -0700 Subject: [PATCH 2/9] got cython to run -- but many compilng errors --- setup.py | 71 +++++++++++++++ src/concaveman/__init__.py | 4 + src/concaveman/build_hull.pyx | 109 ++++++++++++++++++++++++ src/concaveman/concaveman.py | 52 +++++++++++ src/concaveman/concaveman.pyx | 0 src/concaveman/tests/__init__.py | 0 src/concaveman/tests/test_build_hull.py | 18 ++++ 7 files changed, 254 insertions(+) create mode 100755 setup.py create mode 100644 src/concaveman/build_hull.pyx create mode 100644 src/concaveman/concaveman.py create mode 100644 src/concaveman/concaveman.pyx create mode 100644 src/concaveman/tests/__init__.py create mode 100644 src/concaveman/tests/test_build_hull.py diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..b443ff8 --- /dev/null +++ b/setup.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python + +from setuptools import setup, Extension + +from Cython.Build import cythonize + +import os +import sys +import numpy as np # For the include directory. + +rootpath = os.path.abspath(os.path.dirname(__file__)) +long_description = open(os.path.join(rootpath, 'README.md')).read() + +include_dirs = [np.get_include(), + os.path.join(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 = [Extension("concaveman.concaveman", + ["src/concaveman/build_hull.pyx", + "src/cpp/concaveman.cpp"], + include_dirs=include_dirs, + language="c++", + language_level='3', + )] + + +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=cythonize(ext_modules), + packages=["src/concaveman", "src/concaveman/tests"], + install_requires=['numpy', 'scipy'], + setup_requires=['cython>0.29', 'setuptools'], + 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 index e69de29..d00cdf0 100644 --- a/src/concaveman/__init__.py +++ b/src/concaveman/__init__.py @@ -0,0 +1,4 @@ + +__version__ == "0.1.0" + + diff --git a/src/concaveman/build_hull.pyx b/src/concaveman/build_hull.pyx new file mode 100644 index 0000000..170c0e9 --- /dev/null +++ b/src/concaveman/build_hull.pyx @@ -0,0 +1,109 @@ + +import numpy as np +cimport numpy as cnp + +# from libc.stdlib cimport malloc, free + +cdef extern from "concaveman.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, + void(**p_free)(void*), + ) + +# defining the function pointer type +ctypedef void (**f_type)(void*) + +# def concave_hull(cnp.ndarray[double, ndim=2, mode="c"] points, +# cnp.ndarray[cnp.int32_t, ndim=2, mode="c"] hull, +# double concavity=2.0, +# double length_threshold=0.0): +def 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 + """ + + # points = np.array(points).astype(np.double) + # hull = np.array(hull).astype(np.int32) + + if points.shape[1] != 2: + raise ValueError('points must be an Nx2 array') + + # 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') + + cdef double** p_concave_points + cdef size_t p_num_concave_points + cdef f_type p_free + + # p_concave_points_c = _ffi.new('double**') + # p_num_concave_points = _ffi.new('size_t*') + # p_free = _ffi.new('void(**)(void*)') + + + # points_c = _ffi.cast('double*', points.ctypes.data) + # hull_c = _ffi.cast('int*', hull.ctypes.data) + pyconcaveman2d(&points[0,0], + len(points), + &hull[0], + len(hull), + concavity, + length_threshold, + p_concave_points, + &p_num_concave_points, + p_free) + + # _lib.pyconcaveman2d(points_c, len(points), + # hull_c, len(hull), + # concavity, lengthThreshold, + # p_concave_points_c, p_num_concave_points, + # p_free) + + num_concave_points = p_num_concave_points + concave_points_c = p_concave_points + + # buffer = _ffi.buffer(concave_points_c, 8 * 2 * num_concave_points) + + # concave_points = np.frombuffer(buffer, dtype=np.double) + # concave_points = concave_points.reshape((num_concave_points, 2)) + # concave_points = concave_points.copy() + + cdef cnp.ndarray[cnp.float64_t, ndim=2, mode="c"] concave_points + concave_points = np.empty((p_num_concave_points, 2), dtype=np.float64) + cdef unsigned int i + for i in range(len(concave_points)): + concave_points[i, 0] = p_concave_points[i][0] + concave_points[i, 1] = p_concave_points[i][1] + + print('concave_points:', concave_points) + + + + # fixme: need to make sure this isn't a memory leak! + # p_free[0](concave_points_c) + + return concave_points diff --git a/src/concaveman/concaveman.py b/src/concaveman/concaveman.py new file mode 100644 index 0000000..398c725 --- /dev/null +++ b/src/concaveman/concaveman.py @@ -0,0 +1,52 @@ +import cffi +import numpy as np + + +_ffi = cffi.FFI() +_ffi.cdef('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*));') +_lib = _ffi.dlopen('/Users/sadaszewski/Documents/workspace/concaveman-cpp/src/main/cpp/libconcaveman.so') + + +def concaveman2d(points, hull, concavity=2.0, lengthThreshold=0.0): + points = np.array(points).astype(np.double) + hull = np.array(hull).astype(np.int32) + + if len(points.shape) != 2: + raise ValueError('points must be a 2-D array') + + 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') + + p_concave_points_c = _ffi.new('double**') + p_num_concave_points = _ffi.new('size_t*') + p_free = _ffi.new('void(**)(void*)') + + points_c = _ffi.cast('double*', points.ctypes.data) + hull_c = _ffi.cast('int*', hull.ctypes.data) + _lib.pyconcaveman2d(points_c, len(points), + hull_c, len(hull), + concavity, lengthThreshold, + p_concave_points_c, p_num_concave_points, + p_free) + + num_concave_points = p_num_concave_points[0] + concave_points_c = p_concave_points_c[0] + + buffer = _ffi.buffer(concave_points_c, 8 * 2 * num_concave_points) + + concave_points = np.frombuffer(buffer, dtype=np.double) + concave_points = concave_points.reshape((num_concave_points, 2)) + concave_points = concave_points.copy() + + print('concave_points:', concave_points) + + p_free[0](concave_points_c) + + return concave_points + + +if __name__ == '__main__': + concaveman2d([[0, 0], [.25, .15], [1, 0], [1, 1]], [0, 2, 3]) diff --git a/src/concaveman/concaveman.pyx b/src/concaveman/concaveman.pyx new file mode 100644 index 0000000..e69de29 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..cb201b3 --- /dev/null +++ b/src/concaveman/tests/test_build_hull.py @@ -0,0 +1,18 @@ +""" +test_build_hull.py +""" +import numpy as np +from concaveman.build_hull import build_hull + + +def test_simple(): + """ + 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) + + cvx_hull = build_hull(points, hull) + + print(cvx_hull) + From cd4e4af03989eea01df7da1cad68a209a33cdef6 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Mon, 24 Aug 2020 22:53:37 -0700 Subject: [PATCH 3/9] getting closer, but still won't build :-( --- setup.py | 17 ++++++++++++----- src/concaveman/__init__.py | 2 +- src/concaveman/build_hull.pyx | 2 ++ src/concaveman/concaveman.pyx | 0 src/cpp/concaveman.h | 13 +++++++++++-- 5 files changed, 26 insertions(+), 8 deletions(-) delete mode 100644 src/concaveman/concaveman.pyx diff --git a/setup.py b/setup.py index b443ff8..5983e04 100755 --- a/setup.py +++ b/setup.py @@ -8,11 +8,14 @@ import sys import numpy as np # For the include directory. -rootpath = os.path.abspath(os.path.dirname(__file__)) -long_description = open(os.path.join(rootpath, 'README.md')).read() +from pathlib import Path + +rootpath = Path(__file__).parent.resolve() + +long_description = open(rootpath / 'README.md').read() include_dirs = [np.get_include(), - os.path.join(rootpath, 'src', 'cpp')] + str(rootpath / 'src' / 'cpp')] # # Need the stdint header for Windows (VS2008). # if sys.platform.startswith('win') and sys.version_info.major <= 2: @@ -24,7 +27,9 @@ "src/cpp/concaveman.cpp"], include_dirs=include_dirs, language="c++", - language_level='3', + # this work on the mac, should work in Linux + # for Windows: who knows? + extra_compile_args=['-std=c++11'], )] @@ -39,6 +44,8 @@ def extract_version(): raise ValueError("Couldn't find __version__ in %s" % fname) return version +print("building version: ", extract_version()) + setup( name="concaveman", version=extract_version(), @@ -52,7 +59,7 @@ def extract_version(): ext_modules=cythonize(ext_modules), packages=["src/concaveman", "src/concaveman/tests"], install_requires=['numpy', 'scipy'], - setup_requires=['cython>0.29', 'setuptools'], + setup_requires=['cython>0.29'], tests_require=['pytest'], classifiers=[ "Development Status :: 4 - Beta", diff --git a/src/concaveman/__init__.py b/src/concaveman/__init__.py index d00cdf0..62c6387 100644 --- a/src/concaveman/__init__.py +++ b/src/concaveman/__init__.py @@ -1,4 +1,4 @@ -__version__ == "0.1.0" +__version__ = "0.1.0" diff --git a/src/concaveman/build_hull.pyx b/src/concaveman/build_hull.pyx index 170c0e9..7e823af 100644 --- a/src/concaveman/build_hull.pyx +++ b/src/concaveman/build_hull.pyx @@ -1,4 +1,6 @@ +#cython: language_level=3 + import numpy as np cimport numpy as cnp diff --git a/src/concaveman/concaveman.pyx b/src/concaveman/concaveman.pyx deleted file mode 100644 index e69de29..0000000 diff --git a/src/cpp/concaveman.h b/src/cpp/concaveman.h index 1a45bf5..00ed29d 100644 --- a/src/cpp/concaveman.h +++ b/src/cpp/concaveman.h @@ -24,6 +24,15 @@ //#define DEBUG // uncomment to dump debug info to screen //#define DEBUG_2 // uncomment to dump second-level debug info to screen +// declared here so the Cython can build +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*)) + + template std::unique_ptr make_unique(Args&&... args) { return std::unique_ptr(new T(std::forward(args)...)); @@ -457,12 +466,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; From 63535950f0c93ed6da8b0c87794bba50189a9ece Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Wed, 26 Aug 2020 16:43:21 -0700 Subject: [PATCH 4/9] got it all to build, but it segfaults :-( --- .gitignore | 2 + setup.py | 8 +-- src/concaveman/build_hull.pyx | 76 ++++++++++++------------- src/concaveman/tests/test_build_hull.py | 10 +++- src/cpp/concaveman.cpp | 33 ++++++----- src/cpp/concaveman.h | 8 --- src/cpp/pyconcaveman.h | 68 ++++++++++++++++++++++ 7 files changed, 135 insertions(+), 70 deletions(-) create mode 100644 src/cpp/pyconcaveman.h diff --git a/.gitignore b/.gitignore index ea1e12f..f3314aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .DS_Store +concaveman.egg-info +build/ # Prerequisites *.d diff --git a/setup.py b/setup.py index 5983e04..1c66788 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,6 @@ from Cython.Build import cythonize import os -import sys import numpy as np # For the include directory. from pathlib import Path @@ -22,7 +21,7 @@ # include_dirs.append(os.path.join('rootpath', 'src', 'win_headers')) -ext_modules = [Extension("concaveman.concaveman", +ext_modules = [Extension("concaveman.build_hull", ["src/concaveman/build_hull.pyx", "src/cpp/concaveman.cpp"], include_dirs=include_dirs, @@ -44,8 +43,6 @@ def extract_version(): raise ValueError("Couldn't find __version__ in %s" % fname) return version -print("building version: ", extract_version()) - setup( name="concaveman", version=extract_version(), @@ -57,7 +54,8 @@ def extract_version(): license="BSD", # keywords = "", ext_modules=cythonize(ext_modules), - packages=["src/concaveman", "src/concaveman/tests"], + packages=["concaveman", "concaveman/tests"], + package_dir={'': 'src'}, install_requires=['numpy', 'scipy'], setup_requires=['cython>0.29'], tests_require=['pytest'], diff --git a/src/concaveman/build_hull.pyx b/src/concaveman/build_hull.pyx index 7e823af..0e2997b 100644 --- a/src/concaveman/build_hull.pyx +++ b/src/concaveman/build_hull.pyx @@ -1,12 +1,24 @@ +""" +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 tranlsation step. + +Though maybe getting it to stop segfaulting first would be a good step +""" + #cython: language_level=3 import numpy as np cimport numpy as cnp -# from libc.stdlib cimport malloc, free +from libc.stdlib cimport free -cdef extern from "concaveman.h": +cdef extern from "pyconcaveman.h": void pyconcaveman2d(double *points_c, size_t num_points, @@ -16,20 +28,21 @@ cdef extern from "concaveman.h": double lengthThreshold, double **concave_points_c, size_t *num_concave_points, - void(**p_free)(void*), + # void(**p_free)(void*), ) # defining the function pointer type -ctypedef void (**f_type)(void*) +# ctypedef void (**f_type)(void*) # def concave_hull(cnp.ndarray[double, ndim=2, mode="c"] points, # cnp.ndarray[cnp.int32_t, ndim=2, mode="c"] hull, # double concavity=2.0, # double length_threshold=0.0): -def concave_hull(cnp.float64_t [:,:] points, - int [:] hull, - double concavity=2.0, - double length_threshold=0.0): + +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 @@ -58,54 +71,35 @@ def concave_hull(cnp.float64_t [:,:] points, # if np.any(hull >= len(points)) or np.any(hull < 0): # raise ValueError('hull indices out of bounds') - cdef double** p_concave_points - cdef size_t p_num_concave_points - cdef f_type p_free - - # p_concave_points_c = _ffi.new('double**') - # p_num_concave_points = _ffi.new('size_t*') - # p_free = _ffi.new('void(**)(void*)') - + cdef double** p_concave_points = NULL + cdef size_t num_concave_points = 0 + # cdef f_type p_free = NULL - # points_c = _ffi.cast('double*', points.ctypes.data) - # hull_c = _ffi.cast('int*', hull.ctypes.data) - pyconcaveman2d(&points[0,0], + print("in cython: about to call pyconcaveman2d") + pyconcaveman2d(&points[0, 0], len(points), &hull[0], len(hull), concavity, length_threshold, p_concave_points, - &p_num_concave_points, - p_free) - - # _lib.pyconcaveman2d(points_c, len(points), - # hull_c, len(hull), - # concavity, lengthThreshold, - # p_concave_points_c, p_num_concave_points, - # p_free) - - num_concave_points = p_num_concave_points - concave_points_c = p_concave_points - - # buffer = _ffi.buffer(concave_points_c, 8 * 2 * num_concave_points) - - # concave_points = np.frombuffer(buffer, dtype=np.double) - # concave_points = concave_points.reshape((num_concave_points, 2)) - # concave_points = concave_points.copy() + &num_concave_points, + # p_free). # should't need this, as we're c omiling with same lib. + ) cdef cnp.ndarray[cnp.float64_t, ndim=2, mode="c"] concave_points - concave_points = np.empty((p_num_concave_points, 2), dtype=np.float64) + concave_points = np.empty((num_concave_points, 2), dtype=np.float64) cdef unsigned int i for i in range(len(concave_points)): concave_points[i, 0] = p_concave_points[i][0] concave_points[i, 1] = p_concave_points[i][1] - print('concave_points:', concave_points) - - + print('in cython: concave_points:', concave_points) # fixme: need to make sure this isn't a memory leak! - # p_free[0](concave_points_c) + # as we are compiling the C++ code all together, the + # plan free() should work. I think. + free(p_concave_points) return concave_points + diff --git a/src/concaveman/tests/test_build_hull.py b/src/concaveman/tests/test_build_hull.py index cb201b3..0c8ca58 100644 --- a/src/concaveman/tests/test_build_hull.py +++ b/src/concaveman/tests/test_build_hull.py @@ -2,7 +2,7 @@ test_build_hull.py """ import numpy as np -from concaveman.build_hull import build_hull +from concaveman.build_hull import concave_hull def test_simple(): @@ -12,7 +12,13 @@ def test_simple(): points = np.array([[0, 0], [.25, .15], [1, 0], [1, 1]], dtype=np.float64) hull = np.array([0, 2, 3], dtype=np.int32) - cvx_hull = build_hull(points, hull) + print("about to call concave_hull") + cvx_hull = concave_hull(points, hull) + print("concave_hull returned") + sys.stdout.flush() print(cvx_hull) +if __name__ == "__main__": + test_simple() + diff --git a/src/cpp/concaveman.cpp b/src/cpp/concaveman.cpp index 10798bc..498694d 100644 --- a/src/cpp/concaveman.cpp +++ b/src/cpp/concaveman.cpp @@ -9,20 +9,21 @@ exit 0 #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*)); -} +// 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*)) { + size_t *p_num_concave_points) +// void(**p_free)(void*)) + { std::cout << "pyconcaveman2d(), concavity: " << concavity << " lengthThreshold: " << lengthThreshold << std::endl; @@ -54,12 +55,16 @@ void pyconcaveman2d(double *points_c, size_t num_points, for (auto &p : concave_points) std::cout << p[0] << " " << p[1] << 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]; - } + // 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(); - *p_free = free; + // *p_free = free; } diff --git a/src/cpp/concaveman.h b/src/cpp/concaveman.h index 00ed29d..930cb42 100644 --- a/src/cpp/concaveman.h +++ b/src/cpp/concaveman.h @@ -24,14 +24,6 @@ //#define DEBUG // uncomment to dump debug info to screen //#define DEBUG_2 // uncomment to dump second-level debug info to screen -// declared here so the Cython can build -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*)) - template std::unique_ptr make_unique(Args&&... args) { 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; +// } From d8fe278fbfab1f5c8e6dfa53e6313d7a11c0b1e3 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Thu, 27 Aug 2020 12:10:28 -0700 Subject: [PATCH 5/9] got it to work -- no more segfaults! Still needs cleaning up. --- setup.py | 20 ++++++------- src/concaveman/build_hull.pyx | 39 +++++++++++++++---------- src/concaveman/tests/test_build_hull.py | 1 + src/cpp/concaveman.cpp | 16 +++++++--- 4 files changed, 47 insertions(+), 29 deletions(-) diff --git a/setup.py b/setup.py index 1c66788..7bece54 100755 --- a/setup.py +++ b/setup.py @@ -21,15 +21,15 @@ # include_dirs.append(os.path.join('rootpath', 'src', 'win_headers')) -ext_modules = [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'], - )] +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(): @@ -53,7 +53,7 @@ def extract_version(): url="https://github.com/NOAA-ORR-ERD", license="BSD", # keywords = "", - ext_modules=cythonize(ext_modules), + ext_modules=ext_modules, packages=["concaveman", "concaveman/tests"], package_dir={'': 'src'}, install_requires=['numpy', 'scipy'], diff --git a/src/concaveman/build_hull.pyx b/src/concaveman/build_hull.pyx index 0e2997b..00a60e8 100644 --- a/src/concaveman/build_hull.pyx +++ b/src/concaveman/build_hull.pyx @@ -1,3 +1,5 @@ +# cython: language_level=3 + """ wrapper of concaveman @@ -10,9 +12,6 @@ But we should be able to call the Templates directly from Cython, so save that t Though maybe getting it to stop segfaulting first would be a good step """ - -#cython: language_level=3 - import numpy as np cimport numpy as cnp @@ -71,35 +70,45 @@ cpdef concave_hull(cnp.float64_t[:,:] points, # if np.any(hull >= len(points)) or np.any(hull < 0): # raise ValueError('hull indices out of bounds') - cdef double** p_concave_points = NULL - cdef size_t num_concave_points = 0 + cdef double* p_concave_points = NULL + cdef size_t[1] num_concave_points + num_concave_points[0] = 2 # cdef f_type p_free = NULL + print("num concave points:", 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, + &p_concave_points, + num_concave_points, # p_free). # should't need this, as we're c omiling with same lib. ) - cdef cnp.ndarray[cnp.float64_t, ndim=2, mode="c"] concave_points - concave_points = np.empty((num_concave_points, 2), dtype=np.float64) + print("cpp concave hull returned") + print("num concave points:", num_concave_points[0]) + + #cdef cnp.float64_t[:, :] concave_points_mview = p_concave_points + cdef cnp.ndarray[cnp.float64_t, ndim=2, mode="c"] arr_concave_points + arr_concave_points = np.zeros((num_concave_points[0], 2), dtype=np.float64) cdef unsigned int i - for i in range(len(concave_points)): - concave_points[i, 0] = p_concave_points[i][0] - concave_points[i, 1] = p_concave_points[i][1] + print(p_concave_points[0]) + for i in range(num_concave_points[0]): + # arr_concave_points[i, 0] = p_concave_points[i] + # arr_concave_points[i, 1] = p_concave_points[i] + arr_concave_points[i, 0] = p_concave_points[i * 2] + arr_concave_points[i, 1] = p_concave_points[i * 2 + 1] - print('in cython: concave_points:', concave_points) + print('in cython again: concave_points:', arr_concave_points) # fixme: need to make sure this isn't a memory leak! # as we are compiling the C++ code all together, the # plan free() should work. I think. - free(p_concave_points) + # free(p_concave_points) - return concave_points + return arr_concave_points diff --git a/src/concaveman/tests/test_build_hull.py b/src/concaveman/tests/test_build_hull.py index 0c8ca58..50c3bb3 100644 --- a/src/concaveman/tests/test_build_hull.py +++ b/src/concaveman/tests/test_build_hull.py @@ -1,6 +1,7 @@ """ test_build_hull.py """ +import sys import numpy as np from concaveman.build_hull import concave_hull diff --git a/src/cpp/concaveman.cpp b/src/cpp/concaveman.cpp index 498694d..f572e53 100644 --- a/src/cpp/concaveman.cpp +++ b/src/cpp/concaveman.cpp @@ -40,6 +40,8 @@ void pyconcaveman2d(double *points_c, size_t num_points, 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]; @@ -49,22 +51,28 @@ void pyconcaveman2d(double *points_c, size_t num_points, 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]; - // } + //double *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(); // *p_free = free; } + From b103e3841abccd035faddeed365bf5f3f4ac6eee Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Thu, 27 Aug 2020 14:31:02 -0700 Subject: [PATCH 6/9] oops -- put the free() call back in again! that would have been a big memory leak --- src/concaveman/build_hull.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/concaveman/build_hull.pyx b/src/concaveman/build_hull.pyx index 00a60e8..21c9740 100644 --- a/src/concaveman/build_hull.pyx +++ b/src/concaveman/build_hull.pyx @@ -108,7 +108,7 @@ cpdef concave_hull(cnp.float64_t[:,:] points, # fixme: need to make sure this isn't a memory leak! # as we are compiling the C++ code all together, the # plan free() should work. I think. - # free(p_concave_points) + free(p_concave_points) return arr_concave_points From c1c0b7d4b7019d64852ba1c746c486d87d8136e5 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Thu, 27 Aug 2020 15:15:52 -0700 Subject: [PATCH 7/9] everything working, and extranious prints and stdout calls removed. --- src/concaveman/build_hull.pyx | 58 +++++++++---------------- src/concaveman/tests/test_build_hull.py | 9 +++- src/cpp/concaveman.cpp | 34 +++++++-------- 3 files changed, 45 insertions(+), 56 deletions(-) diff --git a/src/concaveman/build_hull.pyx b/src/concaveman/build_hull.pyx index 21c9740..d8ebbc1 100644 --- a/src/concaveman/build_hull.pyx +++ b/src/concaveman/build_hull.pyx @@ -7,9 +7,14 @@ 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 tranlsation step. +But we should be able to call the Templates directly from Cython, so save that +translation step. -Though maybe getting it to stop segfaulting first would be a good 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 @@ -27,17 +32,8 @@ cdef extern from "pyconcaveman.h": double lengthThreshold, double **concave_points_c, size_t *num_concave_points, - # void(**p_free)(void*), ) -# defining the function pointer type -# ctypedef void (**f_type)(void*) - -# def concave_hull(cnp.ndarray[double, ndim=2, mode="c"] points, -# cnp.ndarray[cnp.int32_t, ndim=2, mode="c"] hull, -# double concavity=2.0, -# double length_threshold=0.0): - cpdef concave_hull(cnp.float64_t[:,:] points, int[:] hull, double concavity=2.0, @@ -58,25 +54,18 @@ cpdef concave_hull(cnp.float64_t[:,:] points, :type length_threshold: python float """ - # points = np.array(points).astype(np.double) - # hull = np.array(hull).astype(np.int32) - if points.shape[1] != 2: raise ValueError('points must be an Nx2 array') - # if len(hull.shape) != 1: - # raise ValueError('hull must be a 1-D 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 double* p_concave_points = NULL - cdef size_t[1] num_concave_points - num_concave_points[0] = 2 - # cdef f_type p_free = NULL + cdef size_t num_concave_points = 0 - print("num concave points:", num_concave_points[0]) - print("in cython: about to call pyconcaveman2d") + # print("in cython: about to call pyconcaveman2d") pyconcaveman2d(&points[0, 0], len(points), @@ -85,25 +74,21 @@ cpdef concave_hull(cnp.float64_t[:,:] points, concavity, length_threshold, &p_concave_points, - num_concave_points, - # p_free). # should't need this, as we're c omiling with same lib. + &num_concave_points, ) - print("cpp concave hull returned") - print("num concave points:", num_concave_points[0]) + # print("cpp concave hull returned") + # print("num concave points:", num_concave_points) - #cdef cnp.float64_t[:, :] concave_points_mview = p_concave_points cdef cnp.ndarray[cnp.float64_t, ndim=2, mode="c"] arr_concave_points - arr_concave_points = np.zeros((num_concave_points[0], 2), dtype=np.float64) - cdef unsigned int i - print(p_concave_points[0]) - for i in range(num_concave_points[0]): - # arr_concave_points[i, 0] = p_concave_points[i] - # arr_concave_points[i, 1] = p_concave_points[i] - arr_concave_points[i, 0] = p_concave_points[i * 2] - arr_concave_points[i, 1] = p_concave_points[i * 2 + 1] + arr_concave_points = np.zeros((num_concave_points, 2), dtype=np.float64) + + # could we use a memcopy here? + cdef double* temp = &arr_concave_points[0, 0] + for i in range(num_concave_points * 2): + temp[i] = p_concave_points[i] - print('in cython again: concave_points:', arr_concave_points) + # print('in cython again: concave_points:\n', arr_concave_points) # fixme: need to make sure this isn't a memory leak! # as we are compiling the C++ code all together, the @@ -111,4 +96,3 @@ cpdef concave_hull(cnp.float64_t[:,:] points, free(p_concave_points) return arr_concave_points - diff --git a/src/concaveman/tests/test_build_hull.py b/src/concaveman/tests/test_build_hull.py index 50c3bb3..802d53e 100644 --- a/src/concaveman/tests/test_build_hull.py +++ b/src/concaveman/tests/test_build_hull.py @@ -16,10 +16,15 @@ def test_simple(): print("about to call concave_hull") cvx_hull = concave_hull(points, hull) print("concave_hull returned") - - sys.stdout.flush() print(cvx_hull) + assert np.alltrue(cvx_hull == [[0.25, 0.15], + [0., 0.], + [1., 0.], + [1., 1.], + ]) + + if __name__ == "__main__": test_simple() diff --git a/src/cpp/concaveman.cpp b/src/cpp/concaveman.cpp index f572e53..e468248 100644 --- a/src/cpp/concaveman.cpp +++ b/src/cpp/concaveman.cpp @@ -25,8 +25,8 @@ void pyconcaveman2d(double *points_c, size_t num_points, // void(**p_free)(void*)) { - std::cout << "pyconcaveman2d(), concavity: " << concavity << - " lengthThreshold: " << lengthThreshold << std::endl; + // std::cout << "pyconcaveman2d(), concavity: " << concavity << + // " lengthThreshold: " << lengthThreshold << std::endl; typedef double T; typedef std::array point_type; @@ -36,43 +36,43 @@ void pyconcaveman2d(double *points_c, size_t num_points, 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 << "points:" << std::endl; + // for (auto &p : points) + // std::cout << p[0] << " " << p[1] << std::endl; - std::cout << "num points: " << num_points << 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 << "hull:" << std::endl; + // for (auto &i : hull) + // std::cout << i << std::endl; - std::cout << "num hull points: " << num_hull_points << 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 << "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 << "number of cchull points: " << concave_points.size() << std::endl; - std::cout << "about to copy points" << 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()); - //double *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; + // 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; } From 517b1c2bac0ec184801897af27e582e2f5fd7911 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Thu, 27 Aug 2020 15:27:56 -0700 Subject: [PATCH 8/9] updated to use memcpy to copy data to numpy array. still one extra data copy but not horrible --- src/concaveman/build_hull.pyx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/concaveman/build_hull.pyx b/src/concaveman/build_hull.pyx index d8ebbc1..b8e896a 100644 --- a/src/concaveman/build_hull.pyx +++ b/src/concaveman/build_hull.pyx @@ -21,6 +21,8 @@ import numpy as np cimport numpy as cnp from libc.stdlib cimport free +from libc.string cimport memcpy + cdef extern from "pyconcaveman.h": @@ -62,7 +64,7 @@ cpdef concave_hull(cnp.float64_t[:,:] points, # if np.any(hull >= len(points)) or np.any(hull < 0): # raise ValueError('hull indices out of bounds') - cdef double* p_concave_points = NULL + cdef cnp.float64_t* p_concave_points = NULL cdef size_t num_concave_points = 0 # print("in cython: about to call pyconcaveman2d") @@ -80,13 +82,19 @@ cpdef concave_hull(cnp.float64_t[:,:] 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) - # could we use a memcopy here? - cdef double* temp = &arr_concave_points[0, 0] - for i in range(num_concave_points * 2): - temp[i] = p_concave_points[i] + # 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) From 572abebb62cc1e6d76f62aa55890934d65e48589 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Fri, 24 Feb 2023 14:54:25 -0800 Subject: [PATCH 9/9] Seems to be working, and bit more of a demo. --- demos/demos.py | 59 ++++++++++++ src/concaveman/__init__.py | 12 +++ src/concaveman/build_hull.pyx | 6 +- src/concaveman/concaveman.py | 116 +++++++++++++++++------- src/concaveman/tests/test_build_hull.py | 113 ++++++++++++++++++++++- 5 files changed, 267 insertions(+), 39 deletions(-) create mode 100755 demos/demos.py 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/src/concaveman/__init__.py b/src/concaveman/__init__.py index 62c6387..fd51ea1 100644 --- a/src/concaveman/__init__.py +++ b/src/concaveman/__init__.py @@ -1,4 +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 index b8e896a..981ecaa 100644 --- a/src/concaveman/build_hull.pyx +++ b/src/concaveman/build_hull.pyx @@ -98,9 +98,9 @@ cpdef concave_hull(cnp.float64_t[:,:] points, # print('in cython again: concave_points:\n', arr_concave_points) - # fixme: need to make sure this isn't a memory leak! - # as we are compiling the C++ code all together, the - # plan free() should work. I think. + # 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 index 398c725..e6f0737 100644 --- a/src/concaveman/concaveman.py +++ b/src/concaveman/concaveman.py @@ -1,52 +1,100 @@ -import cffi import numpy as np +import scipy.spatial +from .build_hull import concave_hull -_ffi = cffi.FFI() -_ffi.cdef('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*));') -_lib = _ffi.dlopen('/Users/sadaszewski/Documents/workspace/concaveman-cpp/src/main/cpp/libconcaveman.so') +class ConcaveHull(): + """ + class modeled after the scipy.spatial.ConvexHull implementation -def concaveman2d(points, hull, concavity=2.0, lengthThreshold=0.0): - points = np.array(points).astype(np.double) - hull = np.array(hull).astype(np.int32) + One of these days, we could add area and other computations. + """ - if len(points.shape) != 2: - raise ValueError('points must be a 2-D array') + def __init__(self, points, hull=None, concavity=2.0, length_threshold=0.0): - if len(hull.shape) != 1: - raise ValueError('hull must be a 1-D array') + self.points = points - if np.any(hull >= len(points)) or np.any(hull < 0): - raise ValueError('hull indices out of bounds') + if hull is not None: + if len(hull.shape) != 1: + raise ValueError('hull must be a 1-D array') - p_concave_points_c = _ffi.new('double**') - p_num_concave_points = _ffi.new('size_t*') - p_free = _ffi.new('void(**)(void*)') + 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 - points_c = _ffi.cast('double*', points.ctypes.data) - hull_c = _ffi.cast('int*', hull.ctypes.data) - _lib.pyconcaveman2d(points_c, len(points), - hull_c, len(hull), - concavity, lengthThreshold, - p_concave_points_c, p_num_concave_points, - p_free) + self.concavity = concavity + self.length_threshold = length_threshold - num_concave_points = p_num_concave_points[0] - concave_points_c = p_concave_points_c[0] + self.compute() - buffer = _ffi.buffer(concave_points_c, 8 * 2 * num_concave_points) + @property + def concavity(self): + return self._concavity + @concavity.setter + def concavity(self, value): + self._concavity = value + self._cc_hull = None - concave_points = np.frombuffer(buffer, dtype=np.double) - concave_points = concave_points.reshape((num_concave_points, 2)) - concave_points = concave_points.copy() + @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 - print('concave_points:', concave_points) + @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 - p_free[0](concave_points_c) + @property + def cc_hull(self): + if self._cc_hull is None: + self.compute() + return self._cc_hull - return concave_points + 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 -if __name__ == '__main__': - concaveman2d([[0, 0], [.25, .15], [1, 0], [1, 1]], [0, 2, 3]) + 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/test_build_hull.py b/src/concaveman/tests/test_build_hull.py index 802d53e..b453569 100644 --- a/src/concaveman/tests/test_build_hull.py +++ b/src/concaveman/tests/test_build_hull.py @@ -3,10 +3,12 @@ """ import sys import numpy as np +import scipy.spatial from concaveman.build_hull import concave_hull +from concaveman import ConcaveHull, EXAMPLE_SQUARE -def test_simple(): +def test_build_hull(): """ a simple call of the function """ @@ -25,6 +27,113 @@ def test_simple(): ]) +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() + # test_simple() + + check_memory() + + +