Skip to content

Commit

Permalink
integrate scipy convex hull (#12)
Browse files Browse the repository at this point in the history
* integrate scipy convex hull

* test

* add concave_hull as suggested by @ChrisBarker-NOAA

* lint code

* fix doc
  • Loading branch information
district10 authored Mar 2, 2023
1 parent dd77193 commit c3b1e19
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 34 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ repos:

# Sort your imports in a standard form
- repo: https://github.com/PyCQA/isort
rev: 5.10.1
rev: 5.11.5
hooks:
- id: isort

Expand Down
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ set(CMAKE_CXX_STANDARD 14)
set(PYBIND11_CPP_STANDARD -std=c++14)

add_subdirectory(pybind11)
pybind11_add_module(concave_hull src/main.cpp)
pybind11_add_module(pybind11_concave_hull src/main.cpp)

# EXAMPLE_VERSION_INFO is defined by setup.py and passed into the C++ code as a
# define (VERSION_INFO) here.
target_compile_definitions(concave_hull
target_compile_definitions(pybind11_concave_hull
PRIVATE VERSION_INFO=${EXAMPLE_VERSION_INFO})
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ python_sdist:
$(PYTHON) setup.py sdist
# tar -tvf dist/concave_hull-*.tar.gz
python_test:
$(PYTHON) -c 'from concave_hull import rdp; print(rdp([[1, 1], [2, 2], [3, 3], [4, 4]]))'
$(PYTHON) test.py
pytest --capture=tee-sys tests

# conda create -y -n py36 python=3.6
# conda create -y -n py37 python=3.7
Expand Down
25 changes: 18 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,26 @@ pip install git+https://github.com/cubao/concave_hull.git
Signature:

```python
# get concave hull indexes
concave_hull_indexes(
points: numpy.ndarray[numpy.float64[m, 2]],
points: Union[numpy.ndarray, List, Tuple],
*,
convex_hull_indexes: numpy.ndarray[numpy.int32[m, 1]],
concavity: float = 2.0,
length_threshold: float = 0.0,
convex_hull_indexes: numpy.ndarray[numpy.int32[m, 1]] = None, # will use integrated scipy ConvexHull if None
) -> numpy.ndarray[numpy.int32[m, 1]]

# get concave hull points
concave_hull(
points: Union[numpy.ndarray, List, Tuple],
*args, *kwargs # will proxy all to covcave_hull_indexes
) -> Union[numpy.ndarray, List, Tuple]
```

- `concavity` is a relative measure of concavity. 1 results in a relatively
detailed shape, Infinity results in a convex hull. You can use values lower
than 1, but they can produce pretty crazy shapes.
- `lengthThreshold`: when a segment length is under this threshold, it stops
- `length_threshold`: when a segment length is under this threshold, it stops
being considered for further detalization. Higher values result in simpler
shapes.

Expand All @@ -62,7 +69,7 @@ import matplotlib.pyplot as plt
import numpy as np
from scipy.spatial import ConvexHull

from concave_hull import concave_hull_indexes
from concave_hull import concave_hull, concave_hull_indexes

points = []
c = np.array([250, 250])
Expand All @@ -85,20 +92,24 @@ for simplex in convex_hull.simplices:

idxes = concave_hull_indexes(
points[:, :2],
convex_hull_indexes=convex_hull.vertices.astype(np.int32),
length_threshold=50,
# for concave_hull>=0.0.3
# it's not necessary to provide convex_hull_indexes
# convex_hull_indexes=convex_hull.vertices.astype(np.int32),
)
# you can get coordinates by `points[idxes]`
assert np.all(points[idxes] == concave_hull(points, length_threshold=50))

for f, t in zip(idxes[:-1], idxes[1:]): # noqa
seg = points[[f, t]]
plt.plot(seg[:, 0], seg[:, 1], "r-", alpha=0.5)
# plt.savefig('hull.png')
plt.show()
```

## Tests

```
python3 test.py
python3 tests/test_basic.py
make python_install
make python_test
```
50 changes: 50 additions & 0 deletions concave_hull/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from typing import List, Tuple, Union

import numpy as np
from pybind11_concave_hull import __version__ # noqa
from pybind11_concave_hull import ( # noqa
concave_hull_indexes as concave_hull_indexes_impl,
)
from scipy.spatial import ConvexHull


def concave_hull_indexes(
points: Union[np.ndarray, List, Tuple],
*,
concavity: float = 2.0,
length_threshold: float = 0.0,
convex_hull_indexes: np.ndarray = None,
):
"""
Get concave hull indexes of points.
- `points` is an array of [x, y, [z]] points (can be numpy.ndarray, list, or tuple).
- `concavity` is a relative measure of concavity. 1 results in a relatively
detailed shape, Infinity results in a convex hull. You can use values lower
than 1, but they can produce pretty crazy shapes.
- `length_threshold`: when a segment length is under this threshold, it stops
being considered for further detalization. Higher values result in simpler
shapes.
See original document here: https://github.com/mapbox/concaveman
"""
points = np.asarray(points, dtype=np.float64)
points = points[:, :2]
if convex_hull_indexes is None:
convex_hull = ConvexHull(points)
convex_hull_indexes = convex_hull.vertices.astype(np.int32)
return concave_hull_indexes_impl(
points,
convex_hull_indexes=convex_hull_indexes,
concavity=concavity,
length_threshold=length_threshold,
)


def concave_hull(points: Union[np.ndarray, List, Tuple], *args, **kwargs):
indexes = concave_hull_indexes(points, *args, **kwargs)
return (
points[indexes]
if isinstance(points, np.ndarray)
else [points[i] for i in indexes]
)
7 changes: 4 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import subprocess
import sys

from setuptools import Extension, setup
from setuptools import Extension, find_packages, setup
from setuptools.command.build_ext import build_ext

# Convert distutils Windows platform specifiers to CMake -A arguments
Expand Down Expand Up @@ -122,16 +122,17 @@ def build_extension(self, ext):
# logic and declaration, and simpler if you include description/version in a file.
setup(
name="concave_hull",
version="0.0.2",
version="0.0.3",
author="tzx",
author_email="[email protected]",
url="https://github.com/cubao/concave_hull",
description="A very fast 2D concave hull algorithm",
long_description=open("README.md", encoding="utf-8").read(),
long_description_content_type="text/markdown",
packages=find_packages(),
ext_modules=[CMakeExtension("concave_hull")],
cmdclass={"build_ext": CMakeBuild},
zip_safe=False,
install_requires=["numpy"],
install_requires=["numpy", "scipy"],
extras_require={"test": ["pytest>=6.0"]},
)
2 changes: 1 addition & 1 deletion src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Eigen::VectorXi concave_hull_indexes(
namespace py = pybind11;
using namespace pybind11::literals;

PYBIND11_MODULE(concave_hull, m)
PYBIND11_MODULE(pybind11_concave_hull, m)
{
m.doc() = R"pbdoc(
A very fast 2D concave hull algorithm
Expand Down
8 changes: 6 additions & 2 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import numpy as np
from scipy.spatial import ConvexHull

from concave_hull import concave_hull_indexes
from concave_hull import concave_hull, concave_hull_indexes

points = []
c = np.array([250, 250])
Expand All @@ -25,12 +25,16 @@

idxes = concave_hull_indexes(
points[:, :2],
convex_hull_indexes=convex_hull.vertices.astype(np.int32),
length_threshold=50,
# for concave_hull>=0.0.3
# it's not necessary to provide convex_hull_indexes
# convex_hull_indexes=convex_hull.vertices.astype(np.int32),
)
# you can get coordinates by `points[idxes]`
assert np.all(points[idxes] == concave_hull(points, length_threshold=50))

for f, t in zip(idxes[:-1], idxes[1:]): # noqa
seg = points[[f, t]]
plt.plot(seg[:, 0], seg[:, 1], "r-", alpha=0.5)
# plt.savefig('hull.png')
plt.show()
82 changes: 65 additions & 17 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import numpy as np

from concave_hull import concave_hull_indexes
from concave_hull import concave_hull, concave_hull_indexes


# see ../test.py for testing data
Expand All @@ -22,11 +22,8 @@ def __convex_hull_indexes():
return [208, 138, 83, 49, 19, 7, 0, 8, 34, 66, 166, 183, 198, 204]


def test_concave_hull():
points = __all_points()
convex_hull = __convex_hull_indexes()

expected = [
def __concave_hull_indexes():
return [
205,
206,
208,
Expand Down Expand Up @@ -84,14 +81,11 @@ def test_concave_hull():
198,
199,
204,
]
idxes = concave_hull_indexes(
points,
convex_hull_indexes=convex_hull,
)
assert np.all(idxes == expected)
] # noqa


expected = [
def __concave_hull_indexes_thresh50():
return [
208,
207,
203,
Expand All @@ -114,23 +108,77 @@ def test_concave_hull():
49,
19,
7,
4,
5,
2,
1,
0,
8,
34,
66,
84,
102,
120,
139,
166,
183,
198,
204,
]


def __test_concave_hull(points):
convex_hull = __convex_hull_indexes()
idxes = concave_hull_indexes(
points,
convex_hull_indexes=convex_hull,
)
assert np.all(idxes == __concave_hull_indexes())

idxes = concave_hull_indexes(points) # integrated convex hull
assert np.all(idxes == __concave_hull_indexes())

idxes = concave_hull_indexes(
points,
convex_hull_indexes=convex_hull,
length_threshold=50,
)
assert np.all(idxes == expected)
assert np.all(idxes == __concave_hull_indexes_thresh50())
idxes = concave_hull_indexes(points, length_threshold=50)
assert np.all(idxes == __concave_hull_indexes_thresh50())


def test_concave_hull_np_array():
points = __all_points()
__test_concave_hull(points)
# Nx3
points = np.c_[points, np.zeros(len(points))]
__test_concave_hull(points)


def test_concave_hull_list_tuple():
points = __all_points()
__test_concave_hull(points.tolist())
__test_concave_hull(tuple(points.tolist()))
# Nx3
points = np.c_[points, np.zeros(len(points))]
__test_concave_hull(points.tolist())
__test_concave_hull(tuple(points.tolist()))


def test_concave_hull_api():
all_points = __all_points()
all_points = np.c_[all_points, np.random.random(len(all_points))]
assert all_points.shape == (209, 3)

hull_points = concave_hull(all_points)
assert isinstance(hull_points, np.ndarray)
assert hull_points.shape == (57, 3)

indexes = concave_hull_indexes(all_points)
assert np.all(all_points[indexes] == hull_points)

hull_points = concave_hull(all_points.tolist())
assert isinstance(hull_points, list)
assert len(hull_points) == 57

hull_points = concave_hull(tuple(all_points.tolist()))
assert isinstance(hull_points, list)
assert len(hull_points) == 57

0 comments on commit c3b1e19

Please sign in to comment.