Skip to content
Snippets Groups Projects
Commit a2ef1d92 authored by ml bonhomme's avatar ml bonhomme :bee: Committed by Bastien Abadie
Browse files

add helper to trim polygons so that they fit inside their image

parent e557fdf1
No related branches found
No related tags found
1 merge request!159add helper to trim polygons so that they fit inside their image
Pipeline #79140 passed
......@@ -181,3 +181,49 @@ def download_tiles(url):
)
return full_image
def trim_polygon(polygon, image_width: int, image_height: int):
"""
This method takes as input:
- a polygon: a list or tuple of points
- image_width, image_height: an image's dimensions
and outputs a new polygon, whose points are all located within the image.
If some of the polygon's points are not inside the image, the polygon gets trimmed,
which means that some points can disappear or their coordinates be modified.
"""
assert isinstance(
polygon, (list, tuple)
), "Input polygon must be a valid list or tuple of points."
assert all(
isinstance(point, (list, tuple)) for point in polygon
), "Polygon points must be tuples or lists."
assert all(
len(point) == 2 for point in polygon
), "Polygon points must be tuples or lists of 2 elements."
assert all(
isinstance(point[0], int) and isinstance(point[1], int) for point in polygon
), "Polygon point coordinates must be integers."
assert any(
point[0] <= image_width and point[1] <= image_height for point in polygon
), "This polygon is entirely outside the image's bounds."
trimmed_polygon = [
[
min(image_width, max(0, x)),
min(image_height, max(0, y)),
]
for x, y in polygon
]
updated_polygon = []
for point in trimmed_polygon:
if point not in updated_polygon:
updated_polygon.append(point)
# Add back the matching last point, if it was present in the original polygon
if polygon[-1] == polygon[0]:
updated_polygon.append(updated_polygon[0])
return updated_polygon
# -*- coding: utf-8 -*-
import math
import unittest
from io import BytesIO
from pathlib import Path
import pytest
from PIL import Image, ImageChops, ImageOps
from arkindex_worker.image import download_tiles, open_image
from arkindex_worker.image import download_tiles, open_image, trim_polygon
FIXTURES = Path(__file__).absolute().parent / "data"
TILE = FIXTURES / "test_image.jpg"
......@@ -14,6 +15,7 @@ FULL_IMAGE = FIXTURES / "tiled_image.jpg"
ROTATED_IMAGE = FIXTURES / "rotated_image.jpg"
MIRRORED_IMAGE = FIXTURES / "mirrored_image.jpg"
ROTATED_MIRRORED_IMAGE = FIXTURES / "rotated_mirrored_image.jpg"
TEST_IMAGE = {"width": 800, "height": 300}
def _root_mean_square(img_a, img_b):
......@@ -146,3 +148,211 @@ def test_open_image_rotate_mirror(rotation_angle, mirrored, expected_path):
actual = open_image(str(TILE), rotation_angle=rotation_angle, mirrored=mirrored)
actual.save(f"/tmp/{rotation_angle}_{mirrored}.jpg")
assert _root_mean_square(expected, actual) <= 15.0
class TestTrimPolygon(unittest.TestCase):
def test_trim_polygon_partially_outside_image(self):
bad_polygon = [
[99, 200],
[197, 224],
[120, 251],
[232, 350],
[312, 364],
[325, 310],
[318, 295],
[296, 260],
[352, 259],
[106, 210],
[197, 206],
[99, 200],
]
expected_polygon = [
[99, 200],
[197, 224],
[120, 251],
[232, 300],
[312, 300],
[325, 300],
[318, 295],
[296, 260],
[352, 259],
[106, 210],
[197, 206],
[99, 200],
]
assert (
trim_polygon(bad_polygon, TEST_IMAGE["width"], TEST_IMAGE["height"])
== expected_polygon
)
def test_trim_polygon_good_polygon(self):
good_polygon = (
(12, 56),
(29, 60),
(35, 61),
(42, 59),
(58, 57),
(65, 61),
(72, 57),
(12, 56),
)
expected_polygon = [
[12, 56],
[29, 60],
[35, 61],
[42, 59],
[58, 57],
[65, 61],
[72, 57],
[12, 56],
]
assert (
trim_polygon(good_polygon, TEST_IMAGE["width"], TEST_IMAGE["height"])
== expected_polygon
)
def test_trim_polygon_invalid_polygon(self):
"""
An assertion error is raised the polygon input isn't a list or tuple
"""
bad_polygon = {
"polygon": [
[99, 200],
[25, 224],
[0, 0],
[0, 300],
[102, 300],
[260, 300],
[288, 295],
[296, 260],
[352, 259],
[106, 210],
[197, 206],
[99, 208],
]
}
with self.assertRaises(
AssertionError, msg="Input polygon must be a valid list or tuple of points."
):
trim_polygon(bad_polygon, TEST_IMAGE["width"], TEST_IMAGE["height"])
def test_trim_polygon_negative_coordinates(self):
"""
Negative coordinates are ignored and replaced by 0 with no error being thrown
"""
bad_polygon = [
[99, 200],
[25, 224],
[-8, -52],
[-12, 350],
[102, 364],
[260, 310],
[288, 295],
[296, 260],
[352, 259],
[106, 210],
[197, 206],
[99, 200],
]
expected_polygon = [
[99, 200],
[25, 224],
[0, 0],
[0, 300],
[102, 300],
[260, 300],
[288, 295],
[296, 260],
[352, 259],
[106, 210],
[197, 206],
[99, 200],
]
assert (
trim_polygon(bad_polygon, TEST_IMAGE["width"], TEST_IMAGE["height"])
== expected_polygon
)
def test_trim_polygon_outside_image_error(self):
"""
An assertion error is raised when none of the polygon's points are inside the image
"""
bad_polygon = [
[999, 200],
[1097, 224],
[1020, 251],
[1232, 350],
[1312, 364],
[1325, 310],
[1318, 295],
[1296, 260],
[1352, 259],
[1006, 210],
[997, 206],
[999, 200],
]
with self.assertRaises(
AssertionError, msg="This polygon is entirely outside the image's bounds."
):
trim_polygon(bad_polygon, TEST_IMAGE["width"], TEST_IMAGE["height"])
def test_trim_polygon_float_coordinates(self):
"""
An assertion error is raised when point coordinates are not integers
"""
bad_polygon = [
[9.9, 200],
[25, 224],
[0, 0],
[0, 300],
[102, 300],
[260, 300],
[288, 295],
[296, 260],
[352, 259],
[106, 210],
[197, 206],
[99, 20.8],
]
with self.assertRaises(
AssertionError, msg="Polygon point coordinates must be integers."
):
trim_polygon(bad_polygon, TEST_IMAGE["width"], TEST_IMAGE["height"])
def test_trim_polygon_invalid_points_1(self):
"""
An assertion error is raised when point coordinates are not lists or tuples
"""
bad_polygon = [
[12, 56],
[29, 60],
[35, 61],
"[42, 59]",
[58, 57],
[65, 61],
[72, 57],
[12, 56],
]
with self.assertRaises(
AssertionError, msg="Polygon points must be tuples or lists."
):
trim_polygon(bad_polygon, TEST_IMAGE["width"], TEST_IMAGE["height"])
def test_trim_polygon_invalid_points_2(self):
"""
An assertion error is raised when point coordinates are not lists or tuples of length 2
"""
bad_polygon = [
[12, 56],
[29, 60, 3],
[35, 61],
[42, 59],
[58, 57],
[65, 61],
[72, 57],
[12, 56],
]
with self.assertRaises(
AssertionError, msg="Polygon points must be tuples or lists of 2 elements."
):
trim_polygon(bad_polygon, TEST_IMAGE["width"], TEST_IMAGE["height"])
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment