diff --git a/README.md b/README.md index dee09b6..055658a 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/ext/readme_imgs/from_polygon.png b/ext/readme_imgs/from_polygon.png new file mode 100644 index 0000000..75001e4 Binary files /dev/null and b/ext/readme_imgs/from_polygon.png differ diff --git a/ext/readme_imgs/sample2.png b/ext/readme_imgs/sample2.png deleted file mode 100644 index 7a912be..0000000 Binary files a/ext/readme_imgs/sample2.png and /dev/null differ diff --git a/ext/readme_imgs/sample3.png b/ext/readme_imgs/sample3.png deleted file mode 100644 index be1b018..0000000 Binary files a/ext/readme_imgs/sample3.png and /dev/null differ diff --git a/ext/readme_imgs/sample4.png b/ext/readme_imgs/sample4.png deleted file mode 100644 index c907763..0000000 Binary files a/ext/readme_imgs/sample4.png and /dev/null differ diff --git a/ext/readme_imgs/sample5.png b/ext/readme_imgs/sample5.png deleted file mode 100644 index 9c74fae..0000000 Binary files a/ext/readme_imgs/sample5.png and /dev/null differ diff --git a/largestinteriorrectangle/__init__.py b/largestinteriorrectangle/__init__.py index a19dcaa..a1a8c9a 100644 --- a/largestinteriorrectangle/__init__.py +++ b/largestinteriorrectangle/__init__.py @@ -1,3 +1,3 @@ -from .lir import lir +from .lir import lir, pt1, pt2 -__version__ = "0.1.1" +__version__ = "0.2.0" diff --git a/largestinteriorrectangle/lir.py b/largestinteriorrectangle/lir.py index 6b38ddb..1ca30b0 100644 --- a/largestinteriorrectangle/lir.py +++ b/largestinteriorrectangle/lir.py @@ -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) diff --git a/largestinteriorrectangle/lir_within_polygon.py b/largestinteriorrectangle/lir_within_polygon.py new file mode 100644 index 0000000..a029f03 --- /dev/null +++ b/largestinteriorrectangle/lir_within_polygon.py @@ -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.') diff --git a/tests/context.py b/tests/context.py index 86ca1d7..9bf15ce 100644 --- a/tests/context.py +++ b/tests/context.py @@ -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 \ No newline at end of file +from largestinteriorrectangle import lir, pt1, pt2, lir_basis, lir_within_contour, lir_within_polygon diff --git a/tests/test_lir.py b/tests/test_lir.py index 4ff891c..753b3a7 100644 --- a/tests/test_lir.py +++ b/tests/test_lir.py @@ -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() diff --git a/tests/test_lir_basis.py b/tests/test_lir_basis.py new file mode 100644 index 0000000..41aea8c --- /dev/null +++ b/tests/test_lir_basis.py @@ -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() diff --git a/tests/test_lir_within_contour.py b/tests/test_lir_within_contour.py index 7dbf3da..b4e4632 100644 --- a/tests/test_lir_within_contour.py +++ b/tests/test_lir_within_contour.py @@ -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, :] diff --git a/tests/test_lir_within_polygon.py b/tests/test_lir_within_polygon.py new file mode 100644 index 0000000..8aa0895 --- /dev/null +++ b/tests/test_lir_within_polygon.py @@ -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()