There's a cool solution for this: https://stackoverflow.com/questions/32892932/create-the-oriented-bounding-box-obb-with-python-and-numpy
import numpy as np
from shapely.geometry.base import BaseGeometry
from shapely.geometry import Point, Polygon, MultiPolygon, GeometryCollection
def pca_eigenvectors(pts: np.ndarray) -> np.ndarray:
"""
Returns the principal axes of a set of points.
Method is essentially running a PCA on the points.
Parameters
----------
pts : array_like
"""
ca = np.cov(pts, y=None, rowvar=False, bias=True)
val, vect = np.linalg.eig(ca)
return np.transpose(vect)
def oriented_bounding_box(pts: np.ndarray) -> np.ndarray:
"""
Returns the oriented bounding box width set of points.
Based on [Create the Oriented Bounding-box (OBB) with Python and NumPy](https://stackoverflow.com/questions/32892932/create-the-oriented-bounding-box-obb-with-python-and-numpy).
Parameters
----------
pts : array_like
"""
tvect = pca_eigenvectors(pts)
rot_matrix = np.linalg.inv(tvect)
rot_arr = np.dot(pts, rot_matrix)
mina = np.min(rot_arr, axis=0)
maxa = np.max(rot_arr, axis=0)
diff = (maxa - mina) * 0.5
center = mina + diff
half_w, half_h = diff
corners = np.array([
center + [-half_w, -half_h],
center + [half_w, -half_h],
center + [half_w, half_h],
center + [-half_w, half_h],
])
return np.dot(corners, tvect)
def polygon_from_obb(obb: np.ndarray) -> Polygon:
"""
Returns the oriented bounding box width set of points.
Parameters
----------
obb : array_like
"""
obb = np.vstack((obb, obb[0]))
return Polygon(obb)
This essentially performs a mini Principal Component Analysis to pull out the two perpendicular axes best describing the points, which can then be used to draw. It's all implemented with numpy for the most part so performant for most use cases.
I've pulled that example's implementation and added some utilities for working with shapely and geopandas here: https://github.com/raphaellaude/geo-obb/blob/main/geoobb/obb.py
See example usage: https://github.com/raphaellaude/geo-obb/blob/main/examples/parcel_obbs.ipynb