From 85569b74de1e72bdfb8d901df2af8d1e4c0b5c2e Mon Sep 17 00:00:00 2001 From: Martin Fleischmann Date: Tue, 30 Jan 2024 10:11:12 +0100 Subject: [PATCH 1/3] REF: reimplement mrr on top of shapely --- pointpats/centrography.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/pointpats/centrography.py b/pointpats/centrography.py index 0c0e4fe..546dd85 100644 --- a/pointpats/centrography.py +++ b/pointpats/centrography.py @@ -31,12 +31,14 @@ import numpy as np import warnings import copy +import math from math import pi as PI from scipy.spatial import ConvexHull from libpysal.cg import get_angle_between, Ray, is_clockwise from scipy.spatial import distance as dist from scipy.optimize import minimize +import shapely not_clockwise = lambda x: not is_clockwise(x) @@ -102,15 +104,21 @@ def minimum_rotated_rectangle(points, return_angle=False): """ points = np.asarray(points) - try: - from cv2 import minAreaRect, boxPoints - except (ImportError, ModuleNotFoundError): - raise ModuleNotFoundError("OpenCV2 is required to use this function. Please install it with `pip install opencv-contrib-python`") - ((x, y), (w, h), angle) = rot_rect = minAreaRect(points.astype(np.float32)) - out_points = boxPoints(rot_rect) + out_points = shapely.get_coordinates( + shapely.minimum_rotated_rectangle(shapely.multipoints(points)) + )[:-1] if return_angle: - return (out_points, angle) - return out_points + angle = ( + math.degrees( + math.atan2( + out_points[1][1] - out_points[0][1], + out_points[1][0] - out_points[0][0], + ) + ) + % 90 + ) + return (out_points[::-1], angle) + return out_points[::-1] def mbr(points): From 1c4c46e014d5aa4ff940026b13914e1c86702f00 Mon Sep 17 00:00:00 2001 From: Martin Fleischmann Date: Tue, 30 Jan 2024 10:13:29 +0100 Subject: [PATCH 2/3] remove opencv dependency --- ci/envs/310-latest.yaml | 2 -- ci/envs/311-dev.yaml | 2 -- ci/envs/311-latest.yaml | 2 -- ci/envs/38-minimal.yaml | 4 ---- ci/envs/39-latest.yaml | 2 -- docs/conf.py | 1 - pointpats/centrography.py | 34 +++++++++++++++++++++++----------- 7 files changed, 23 insertions(+), 24 deletions(-) diff --git a/ci/envs/310-latest.yaml b/ci/envs/310-latest.yaml index 4221397..dbfe900 100644 --- a/ci/envs/310-latest.yaml +++ b/ci/envs/310-latest.yaml @@ -8,7 +8,6 @@ dependencies: - pandas - matplotlib - libpysal - - py-opencv # tests - scikit-learn - shapely @@ -19,5 +18,4 @@ dependencies: - codecov - pip - pip: - - opencv-contrib-python - KDEpy diff --git a/ci/envs/311-dev.yaml b/ci/envs/311-dev.yaml index 89d2e31..a61bb8a 100644 --- a/ci/envs/311-dev.yaml +++ b/ci/envs/311-dev.yaml @@ -9,7 +9,6 @@ dependencies: - libpysal - mapclassify - folium - - py-opencv # tests - shapely - pyproj @@ -20,7 +19,6 @@ dependencies: - pip - pip: - --pre --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple --extra-index-url https://pypi.org/simple - - opencv-contrib-python - git+https://github.com/geopandas/geopandas.git - git+https://github.com/pysal/libpysal.git - scipy diff --git a/ci/envs/311-latest.yaml b/ci/envs/311-latest.yaml index be53100..31bd1de 100644 --- a/ci/envs/311-latest.yaml +++ b/ci/envs/311-latest.yaml @@ -8,7 +8,6 @@ dependencies: - pandas - matplotlib - libpysal - - py-opencv # tests - scikit-learn - shapely @@ -19,7 +18,6 @@ dependencies: - codecov - pip - pip: - - opencv-contrib-python - KDEpy # for docs build action (this env only) - nbsphinx diff --git a/ci/envs/38-minimal.yaml b/ci/envs/38-minimal.yaml index 8155c41..f405fdb 100644 --- a/ci/envs/38-minimal.yaml +++ b/ci/envs/38-minimal.yaml @@ -8,7 +8,6 @@ dependencies: - pandas=1.3 - matplotlib=3.4 - libpysal=4.5 - - py-opencv=4.6 # tests - scikit-learn==1.2 - shapely @@ -16,6 +15,3 @@ dependencies: - pytest - pytest-cov - codecov - - pip - - pip: - - opencv-contrib-python==4.6.0.66 diff --git a/ci/envs/39-latest.yaml b/ci/envs/39-latest.yaml index 18551a9..619afd3 100644 --- a/ci/envs/39-latest.yaml +++ b/ci/envs/39-latest.yaml @@ -8,7 +8,6 @@ dependencies: - pandas - matplotlib - libpysal - - py-opencv # tests - scikit-learn - shapely @@ -19,5 +18,4 @@ dependencies: - codecov - pip - pip: - - opencv-contrib-python - KDEpy diff --git a/docs/conf.py b/docs/conf.py index 639b296..835f286 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -287,7 +287,6 @@ def setup(app): 'libpysal': ('https://pysal.org/libpysal/', None), 'pandas': ('https://pandas.pydata.org/pandas-docs/stable/', None), 'matplotlib':("https://matplotlib.org/", None), - 'opencv-contrib-python':("https://docs.opencv.org/3.4/index.html", None), 'KDEpy':("https://kdepy.readthedocs.io/en/latest/", None), 'statsmodels':("https://www.statsmodels.org/stable/", None), } diff --git a/pointpats/centrography.py b/pointpats/centrography.py index 546dd85..e51b1a4 100644 --- a/pointpats/centrography.py +++ b/pointpats/centrography.py @@ -83,8 +83,7 @@ def minimum_rotated_rectangle(points, return_angle=False): Compute the minimum rotated rectangle for an input point set. This is the smallest enclosing rectangle (possibly rotated) - for the input point set. It is computed using OpenCV2, so - if that is not available, then this function will fail. + for the input point set. It is computed using Shapely. Parameters ---------- @@ -94,7 +93,6 @@ def minimum_rotated_rectangle(points, return_angle=False): return_angle : bool whether to return the angle (in degrees) of the angle between the horizontal axis of the rectanle and the first side (i.e. length). - Computed directly from cv2.minAreaRect. Returns ------- @@ -386,8 +384,22 @@ def _skyum_lists(points): removed = [] i = 0 while True: - angles = [_angle(_prec(p, points), p, _succ(p, points),) for p in points] - circles = [_circle(_prec(p, points), p, _succ(p, points),) for p in points] + angles = [ + _angle( + _prec(p, points), + p, + _succ(p, points), + ) + for p in points + ] + circles = [ + _circle( + _prec(p, points), + p, + _succ(p, points), + ) + for p in points + ] radii = [c[0] for c in circles] lexord = np.lexsort((radii, angles)) # confusing as hell defaults... lexmax = lexord[-1] @@ -514,14 +526,14 @@ def _circle(p, q, r, dmetric=_euclidean_distance): else: D = 2 * (px * (qy - ry) + qx * (ry - py) + rx * (py - qy)) center_x = ( - (px ** 2 + py ** 2) * (qy - ry) - + (qx ** 2 + qy ** 2) * (ry - py) - + (rx ** 2 + ry ** 2) * (py - qy) + (px**2 + py**2) * (qy - ry) + + (qx**2 + qy**2) * (ry - py) + + (rx**2 + ry**2) * (py - qy) ) / float(D) center_y = ( - (px ** 2 + py ** 2) * (rx - qx) - + (qx ** 2 + qy ** 2) * (px - rx) - + (rx ** 2 + ry ** 2) * (qx - px) + (px**2 + py**2) * (rx - qx) + + (qx**2 + qy**2) * (px - rx) + + (rx**2 + ry**2) * (qx - px) ) / float(D) radius = _euclidean_distance(center_x, center_y, px, py) return radius, center_x, center_y From 339435e1367af61b45a938a239791912f70896ae Mon Sep 17 00:00:00 2001 From: Martin Fleischmann Date: Tue, 30 Jan 2024 10:28:06 +0100 Subject: [PATCH 3/3] skip for old GEOS --- pointpats/tests/test_centrography.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pointpats/tests/test_centrography.py b/pointpats/tests/test_centrography.py index b7c786e..3f920e2 100644 --- a/pointpats/tests/test_centrography.py +++ b/pointpats/tests/test_centrography.py @@ -1,6 +1,8 @@ # TODO: skyum, dtot, weighted_mean_center, manhattan_median import unittest import numpy as np +import shapely +import pytest from ..centrography import * @@ -26,6 +28,10 @@ def setUp(self): ] ) + @pytest.mark.skipif( + shapely.geos_version < (3, 12, 0), + reason="Requires GEOS 3.12.0 to use correct algorithm" + ) def test_centrography_mar(self): mrr = minimum_rotated_rectangle(self.points) known = np.array(