Skip to content

Commit

Permalink
support polygon coordinate list (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasalexanderweber authored Dec 10, 2022
1 parent 3488058 commit 414f2c4
Show file tree
Hide file tree
Showing 14 changed files with 338 additions and 83 deletions.
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Fast Largest Interior Rectangle calculation within a binary grid.

![sample1](https://github.com/lukasalexanderweber/lir/blob/main/ext/readme_imgs/sample1.png?raw=true) ![sample2](https://github.com/lukasalexanderweber/lir/blob/main/ext/readme_imgs/sample2.png?raw=true) ![sample4](https://github.com/lukasalexanderweber/lir/blob/main/ext/readme_imgs/sample5.png?raw=true)
![sample1](https://github.com/lukasalexanderweber/lir/blob/main/ext/readme_imgs/sample1.png?raw=true)

:rocket: Through [Numba](https://github.com/numba/numba) the Python code is compiled to machine code for execution at native machine code speed!

Expand Down Expand Up @@ -53,6 +53,32 @@ then calculate the rectangle.
lir.lir(grid, contour) # array([2, 2, 4, 7])
```

You can also calculate the lir from a list of polygon coordinates.

```python
import numpy as np
import cv2 as cv
import largestinteriorrectangle as lir

polygon = np.array([[[20, 15], [210, 10], [220, 100], [100, 150], [20, 100]]], np.int32)
rectangle = lir.lir(polygon)

img = np.zeros((160, 240, 3), dtype="uint8")

cv.polylines(img, [polygon], True, (0, 0, 255), 1)
cv.rectangle(img, lir.pt1(rectangle), lir.pt2(rectangle), (255, 0, 0), 1)

cv.imshow('lir', img)
cv.waitKey(0)
cv.destroyAllWindows()
```

![from_polygon](https://github.com/lukasalexanderweber/lir/blob/main/ext/readme_imgs/from_polygon.png?raw=true)

In the background, a grid is created with `cv.fillPoly` (OpenCV is needed as optional dependency), on which the contour is computed and the lir based on contour is used.

See also my [answer in this SO question](https://stackoverflow.com/questions/70362355/finding-largest-inscribed-rectangle-in-polygon/74736411#74736411).

## Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Expand Down
Binary file added ext/readme_imgs/from_polygon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed ext/readme_imgs/sample2.png
Binary file not shown.
Binary file removed ext/readme_imgs/sample3.png
Binary file not shown.
Binary file removed ext/readme_imgs/sample4.png
Binary file not shown.
Binary file removed ext/readme_imgs/sample5.png
Binary file not shown.
4 changes: 2 additions & 2 deletions largestinteriorrectangle/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .lir import lir
from .lir import lir, pt1, pt2

__version__ = "0.1.1"
__version__ = "0.2.0"
36 changes: 28 additions & 8 deletions largestinteriorrectangle/lir.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,39 @@
from .lir_basis import largest_interior_rectangle as lir_basis
from .lir_within_contour import largest_interior_rectangle \
as lir_within_contour
from .lir_within_polygon import largest_interior_rectangle \
as lir_within_polygon


def lir(grid, contour=None):
def lir(data, contour=None):
"""
Returns the Largest Interior Rectangle of a binary grid.
:param grid: 2D ndarray containing data with `bool` type.
:param contour: (optional) 2D ndarray with shape (n, 2) containing
xy values of a specific contour where the rectangle could start
(in all directions).
Computes the Largest Interior Rectangle.
:param data: Can be
1. a 2D ndarray with shape (n, m) of type boolean. The lir is found within all True cells
2. a 3D ndarray with shape (1, n, 2) with integer xy coordinates of a polygon in which the lir should be found
:param contour: (optional) 2D ndarray with shape (n, 2) containing xy values of a specific contour where the rectangle could start (in all directions). Only needed for case 1.
:return: 1D ndarray with lir specification: x, y, width, height
:rtype: ndarray
"""
if len(data.shape) == 3:
return lir_within_polygon(data)
if contour is None:
return lir_basis(grid)
return lir_basis(data)
else:
return lir_within_contour(grid, contour)
return lir_within_contour(data, contour)


def pt1(lir):
"""
Helper function to compute pt1 of OpenCVs rectangle() from a lir
"""
assert lir.shape == (4,)
return (lir[0], lir[1])


def pt2(lir):
"""
Helper function to compute pt2 of OpenCVs rectangle() from a lir
"""
assert lir.shape == (4,)
return (lir[0] + lir[2] - 1, lir[1] + lir[3] - 1)
42 changes: 42 additions & 0 deletions largestinteriorrectangle/lir_within_polygon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import numpy as np

from .lir_within_contour import largest_interior_rectangle as lir_contour

cv = None # as an optional dependency opencv will only be imported if needed


def largest_interior_rectangle(polygon):
check_for_opencv()
origin, mask = create_mask_from_polygon(polygon)
contours, _ = cv.findContours(mask, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
contour = contours[0][:, 0, :]
mask = mask > 0
lir = lir_contour(mask, contour)
lir = lir.astype(np.int32)
lir[0:2] = lir[0:2] + origin
return lir


def create_mask_from_polygon(polygon):
assert polygon.shape[0] == 1
assert polygon.shape[1] > 2
assert polygon.shape[2] == 2
check_for_opencv()
bbox = cv.boundingRect(polygon)
mask = np.zeros([bbox[3], bbox[2]], dtype=np.uint8)
zero_centered_x = polygon[:, :, 0] - bbox[0]
zero_centered_y = polygon[:, :, 1] - bbox[1]
polygon = np.dstack((zero_centered_x, zero_centered_y))
cv.fillPoly(mask, polygon, 255)
origin = bbox[0:2]
return origin, mask


def check_for_opencv():
global cv
if cv is None:
try:
import cv2
cv = cv2
except Exception:
raise ImportError('Missing optional dependency \'opencv-python\' to compute lir based on polygon. Use pip or conda to install it.')
2 changes: 1 addition & 1 deletion tests/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

from largestinteriorrectangle import lir_basis, lir_within_contour
from largestinteriorrectangle import lir, pt1, pt2, lir_basis, lir_within_contour, lir_within_polygon
135 changes: 70 additions & 65 deletions tests/test_lir.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,81 +2,86 @@
import os

import numpy as np
import cv2 as cv

from .context import lir_basis as lir
from .context import lir, pt1, pt2

TEST_DIR = os.path.abspath(os.path.dirname(__file__))

GRID = np.array([[0, 0, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 1, 1, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 0, 0, 0],
[1, 1, 0, 0, 0, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 0]], "bool")


class TestLIR(unittest.TestCase):

def test_lir(self):

grid = np.array([[0, 0, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 1, 1, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 0, 0, 0],
[1, 1, 0, 0, 0, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 0]])
grid = grid > 0

h = lir.horizontal_adjacency(grid)
v = lir.vertical_adjacency(grid)
span_map = lir.span_map(grid, h, v)
rect = lir.biggest_span_in_span_map(span_map)
rect2 = lir.largest_interior_rectangle(grid)
def test_lir_polygon(self):
polygon = np.array([[
[10,10],
[150,10],
[100,100],
[-40,100]]
], dtype=np.int32 )

rect = lir(polygon)
np.testing.assert_array_equal(rect, np.array([10, 10, 91, 91]))


def test_lir_binary_mask(self):

rect = lir(GRID)
np.testing.assert_array_equal(rect, np.array([2, 2, 4, 7]))

def test_lir_binary_mask_with_contour(self):
contour = np.array([[2, 0],
[2, 1],
[2, 2],
[2, 3],
[2, 4],
[1, 5],
[2, 6],
[2, 7],
[1, 8],
[0, 8],
[0, 9],
[1, 9],
[2, 8],
[3, 8],
[4, 8],
[5, 9],
[6, 9],
[7, 9],
[8, 9],
[7, 9],
[6, 9],
[5, 8],
[5, 7],
[5, 6],
[6, 5],
[7, 4],
[7, 3],
[6, 2],
[5, 1],
[4, 1],
[3, 2],
[2, 1]], dtype=np.int32)


rect = lir(GRID, contour)
np.testing.assert_array_equal(rect, np.array([2, 2, 4, 7]))
np.testing.assert_array_equal(rect, rect2)

def test_spans(self):
grid = np.array([[1, 1, 1],
[1, 1, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 1, 1]])
grid = grid > 0

h = lir.horizontal_adjacency(grid)
v = lir.vertical_adjacency(grid)
v_vector = lir.v_vector(v, 0, 0)
h_vector = lir.h_vector(h, 0, 0)
spans = lir.spans(h_vector, v_vector)

np.testing.assert_array_equal(v_vector, np.array([6, 2, 1]))
np.testing.assert_array_equal(h_vector, np.array([3, 2, 1]))
np.testing.assert_array_equal(spans, np.array([[3, 1],
[2, 2],
[1, 6]]))

def test_vector_size(self):
t0 = np.array([1, 1, 1, 1], dtype=np.uint32)
t1 = np.array([1, 1, 1, 0], dtype=np.uint32)
t2 = np.array([1, 1, 0, 1, 1, 0], dtype=np.uint32)
t3 = np.array([0, 0, 0, 0], dtype=np.uint32)
t4 = np.array([0, 1, 1, 1], dtype=np.uint32)
t5 = np.array([], dtype=np.uint32)

self.assertEqual(lir.predict_vector_size(t0), 4)
self.assertEqual(lir.predict_vector_size(t1), 3)
self.assertEqual(lir.predict_vector_size(t2), 2)
self.assertEqual(lir.predict_vector_size(t3), 0)
self.assertEqual(lir.predict_vector_size(t4), 0)
self.assertEqual(lir.predict_vector_size(t5), 0)

def test_img(self):
grid = cv.imread(os.path.join(TEST_DIR, "testdata", "mask.png"), 0)
grid = grid > 0
rect = lir.largest_interior_rectangle(grid)
np.testing.assert_array_equal(rect, np.array([4, 20, 834, 213]))

def test_rectangle_pts(self):
rect = np.array([10, 10, 91, 91])
self.assertEqual(pt1(rect), (10, 10))
self.assertEqual(pt2(rect), (100, 100))


def starttest():
unittest.main()
Expand Down
86 changes: 86 additions & 0 deletions tests/test_lir_basis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import unittest
import os

import numpy as np
import cv2 as cv

from .context import lir_basis as lir

TEST_DIR = os.path.abspath(os.path.dirname(__file__))


class TestLIRbasis(unittest.TestCase):

def test_lir(self):

grid = np.array([[0, 0, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 1, 1, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 0, 0, 0],
[1, 1, 0, 0, 0, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 0]])
grid = grid > 0

h = lir.horizontal_adjacency(grid)
v = lir.vertical_adjacency(grid)
span_map = lir.span_map(grid, h, v)
rect = lir.biggest_span_in_span_map(span_map)
rect2 = lir.largest_interior_rectangle(grid)

np.testing.assert_array_equal(rect, np.array([2, 2, 4, 7]))
np.testing.assert_array_equal(rect, rect2)

def test_spans(self):
grid = np.array([[1, 1, 1],
[1, 1, 0],
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
[1, 1, 1]])
grid = grid > 0

h = lir.horizontal_adjacency(grid)
v = lir.vertical_adjacency(grid)
v_vector = lir.v_vector(v, 0, 0)
h_vector = lir.h_vector(h, 0, 0)
spans = lir.spans(h_vector, v_vector)

np.testing.assert_array_equal(v_vector, np.array([6, 2, 1]))
np.testing.assert_array_equal(h_vector, np.array([3, 2, 1]))
np.testing.assert_array_equal(spans, np.array([[3, 1],
[2, 2],
[1, 6]]))

def test_vector_size(self):
t0 = np.array([1, 1, 1, 1], dtype=np.uint32)
t1 = np.array([1, 1, 1, 0], dtype=np.uint32)
t2 = np.array([1, 1, 0, 1, 1, 0], dtype=np.uint32)
t3 = np.array([0, 0, 0, 0], dtype=np.uint32)
t4 = np.array([0, 1, 1, 1], dtype=np.uint32)
t5 = np.array([], dtype=np.uint32)

self.assertEqual(lir.predict_vector_size(t0), 4)
self.assertEqual(lir.predict_vector_size(t1), 3)
self.assertEqual(lir.predict_vector_size(t2), 2)
self.assertEqual(lir.predict_vector_size(t3), 0)
self.assertEqual(lir.predict_vector_size(t4), 0)
self.assertEqual(lir.predict_vector_size(t5), 0)

def test_img(self):
grid = cv.imread(os.path.join(TEST_DIR, "testdata", "mask.png"), 0)
grid = grid > 0
rect = lir.largest_interior_rectangle(grid)
np.testing.assert_array_equal(rect, np.array([4, 20, 834, 213]))


def starttest():
unittest.main()


if __name__ == "__main__":
starttest()
Loading

0 comments on commit 414f2c4

Please sign in to comment.