Skip to content

Commit

Permalink
support polygon coordinate list (#19)
Browse files Browse the repository at this point in the history
lukasalexanderweber authored Dec 10, 2022

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 3488058 commit 414f2c4
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
@@ -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!

@@ -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.
Binary file added ext/readme_imgs/from_polygon.png

Unable to render rich display

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
@@ -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
@@ -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()
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()
9 changes: 3 additions & 6 deletions tests/test_lir_within_contour.py
Original file line number Diff line number Diff line change
@@ -26,8 +26,7 @@ def test_grid(self):

grid = np.uint8(grid * 255)

contours, _ = \
cv.findContours(grid, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
contours, _ = cv.findContours(grid, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
contour = contours[0][:, 0, :]

grid = grid > 0
@@ -39,8 +38,7 @@ def test_grid(self):
def test_img(self):
grid = cv.imread(os.path.join(TEST_DIR, "testdata", "mask.png"), 0)

contours, _ = \
cv.findContours(grid, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
contours, _ = cv.findContours(grid, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
contour = contours[0][:, 0, :]

grid = grid > 0
@@ -56,8 +54,7 @@ def test_multiple_shapes(self):
grid = cv.imread(os.path.join(TEST_DIR, "testdata", "two_shapes.png"),
0)

contours, _ = \
cv.findContours(grid, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
contours, _ = cv.findContours(grid, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
contour1 = contours[0][:, 0, :]
contour2 = contours[1][:, 0, :]

79 changes: 79 additions & 0 deletions tests/test_lir_within_polygon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import unittest
import os

import numpy as np
import cv2 as cv

from .context import lir_within_polygon as lir

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


class TestLIRwithinPolygon(unittest.TestCase):

def test_create_mask_from_polygon(self):
polygon = np.array([[
[10,10],
[150,10],
[100,100],
[-40,100]]
], dtype=np.int32)

origin, mask = lir.create_mask_from_polygon(polygon)

self.assertEqual(origin, (-40, 10))
self.assertEqual(mask.shape, (91, 191))
self.assertEqual(np.count_nonzero(mask == 255), 12831)

def test_polygon(self):
polygon = np.array([[
[10,10],
[150,10],
[100,100],
[-40,100]]
], dtype=np.int32 )

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

def test_polygon2(self):
polygon = np.array([[
[9,-7],
[12,-6],
[8,3],
[10,6],
[12,7],
[1,9],
[-8,7],
[-6,6],
[-4,6],
[-6,2],
[-6,0],
[-7,-5],
[-2,-7],
[1,-3],
[5,-7],
[8,-4],
]], dtype=np.int32 )

rect = lir.largest_interior_rectangle(polygon)
np.testing.assert_array_equal(rect, np.array([-5, -3, 14, 12]))


def test_img(self):
grid = cv.imread(os.path.join(TEST_DIR, "testdata", "two_shapes.png"), 0)

contours, _ = \
cv.findContours(grid, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
polygon = np.array([contours[0][:, 0, :]])

rect = lir.largest_interior_rectangle(polygon)
np.testing.assert_array_equal(rect, np.array([162, 62, 43, 44]))


def starttest():
unittest.main()


if __name__ == "__main__":
starttest()

0 comments on commit 414f2c4

Please sign in to comment.