Skip to content
Snippets Groups Projects
Commit 22abe1f8 authored by Manon Blanco's avatar Manon Blanco Committed by Yoann Schneider
Browse files

Implement `resized_images` method

parent 70be9bf6
No related branches found
No related tags found
1 merge request!579Implement `resized_images` method
Pipeline #187237 passed
......@@ -3,12 +3,15 @@ Helper methods to download and open IIIF images, and manage polygons.
"""
import re
import tempfile
from collections import namedtuple
from collections.abc import Generator, Iterator
from io import BytesIO
from math import ceil
from pathlib import Path
from typing import TYPE_CHECKING
import humanize
import requests
from PIL import Image
from shapely.affinity import rotate, scale, translate
......@@ -40,6 +43,8 @@ IIIF_URL = re.compile(r"\w+:\/{2}.+\/.+\/.+\/.+\/(?P<size>.+)\/!?\d+\/\w+\.\w+")
IIIF_FULL = "full"
# Maximum size available
IIIF_MAX = "max"
# Ratio to resize image
IMAGE_RATIO = [1, 0.9, 0.85, 0.80, 0.75, 0.70, 0.60, 0.50, 0.40, 0.30]
def open_image(
......@@ -149,6 +154,64 @@ def upload_image(image: Image, url: str) -> requests.Response:
return resp
def resized_images(
element: "Element",
max_pixels: int | None = None,
max_bytes: int | None = None,
) -> Iterator[Generator[tempfile.NamedTemporaryFile, None, None]]:
"""
Build resized images according to the pixel and byte limits.
:param element: Element whose image needs to be resized.
:param max_pixels: Maximum pixel size of the resized images.
:param max_bytes: Maximum byte size of the resized images.
:returns: An iterator of the temporary file of the resized image.
"""
_, _, element_width, element_height = polygon_bounding_box(element.polygon)
logger.info(f"This element's image sizes are ({element_width} x {element_height}).")
if max_pixels and max(element_width, element_height) > max_pixels:
logger.warning(
f"Maximum image input size supported is ({max_pixels} x {max_pixels})."
)
logger.warning("The image will be resized.")
element_pixel, param = (
(element_width, "max_width")
if element_width > element_height
else (element_height, "max_height")
)
for resized_pixel in sorted(
set(
min(round(ratio * element_pixel), max_pixels or element_pixel)
for ratio in IMAGE_RATIO
),
reverse=True,
):
with element.open_image_tempfile(**{param: resized_pixel}) as image:
pillow_image = Image.open(image)
if (
pillow_image.width != element_width
or pillow_image.height != element_height
):
logger.warning(
f"The image was resized to ({pillow_image.width} x {pillow_image.height})."
)
# The image is still too large
image_size = Path(image.name).stat().st_size
if max_bytes and image_size > max_bytes:
logger.warning(f"The image size is {humanize.naturalsize(image_size)}.")
logger.warning(
f"Maximum image input size supported is {humanize.naturalsize(max_bytes)}."
)
logger.warning("The image will be resized.")
continue
yield image
def polygon_bounding_box(polygon: list[list[int | float]]) -> BoundingBox:
"""
Compute the rectangle bounding box of a polygon.
......
......@@ -8,6 +8,7 @@ version = "0.4.0b3"
description = "Base Worker to easily build Arkindex ML workflows"
license = { file = "LICENSE" }
dependencies = [
"humanize==4.9.0",
"peewee~=3.17",
"Pillow==10.4.0",
"python-gnupg==0.5.2",
......
tests/data/AD026_6M_00505_0001_0373.jpg

8.6 MiB

import logging
import math
import unittest
import uuid
......@@ -18,21 +19,51 @@ from arkindex_worker.image import (
download_tiles,
open_image,
polygon_bounding_box,
resized_images,
revert_orientation,
trim_polygon,
upload_image,
)
from arkindex_worker.models import Element
from tests import FIXTURES_DIR
FIXTURES = Path(__file__).absolute().parent / "data"
TILE = FIXTURES / "test_image.jpg"
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"
TILE = FIXTURES_DIR / "test_image.jpg"
FULL_IMAGE = FIXTURES_DIR / "tiled_image.jpg"
ROTATED_IMAGE = FIXTURES_DIR / "rotated_image.jpg"
MIRRORED_IMAGE = FIXTURES_DIR / "mirrored_image.jpg"
ROTATED_MIRRORED_IMAGE = FIXTURES_DIR / "rotated_mirrored_image.jpg"
TEST_IMAGE = {"width": 800, "height": 300}
@pytest.fixture()
def mock_page():
class Page(Element):
def open_image(
self,
*args,
max_width: int | None = None,
max_height: int | None = None,
use_full_image: bool | None = False,
**kwargs,
) -> Image.Image:
# Image from Socface (https://socface.site.ined.fr/) project (AD026)
image = Image.open(FIXTURES_DIR / "AD026_6M_00505_0001_0373.jpg")
image.thumbnail(size=(max_width or image.width, max_height or image.height))
return image
return Page(
id="page_id",
name="1",
zone={
"polygon": [[0, 0], [1000, 0], [1000, 3000], [0, 3000], [0, 0]],
"image": {"width": 1000, "height": 3000},
},
rotation_angle=0,
mirrored=False,
)
def _root_mean_square(img_a, img_b):
"""
Get the root-mean-square difference between two images for fuzzy matching
......@@ -582,3 +613,187 @@ def test_upload_image(responses):
assert len(responses.calls) == 1
assert list(map(attrgetter("request.url"), responses.calls)) == [dest_url]
@pytest.mark.parametrize(
("max_pixels", "max_bytes", "expected_sizes", "expected_logs"),
[
# No limits
(
None,
None,
[
(1992, 3000),
(1793, 2700),
(1694, 2550),
(1594, 2400),
(1494, 2250),
(1395, 2100),
(1195, 1800),
(996, 1500),
(797, 1200),
(598, 900),
],
[
(logging.WARNING, "The image was resized to (1992 x 3000)."),
(logging.WARNING, "The image was resized to (1793 x 2700)."),
(logging.WARNING, "The image was resized to (1694 x 2550)."),
(logging.WARNING, "The image was resized to (1594 x 2400)."),
(logging.WARNING, "The image was resized to (1494 x 2250)."),
(logging.WARNING, "The image was resized to (1395 x 2100)."),
(logging.WARNING, "The image was resized to (1195 x 1800)."),
(logging.WARNING, "The image was resized to (996 x 1500)."),
(logging.WARNING, "The image was resized to (797 x 1200)."),
(logging.WARNING, "The image was resized to (598 x 900)."),
],
),
# Image already under the limits
(
10000,
4000000, # 4MB
[
(1992, 3000),
(1793, 2700),
(1694, 2550),
(1594, 2400),
(1494, 2250),
(1395, 2100),
(1195, 1800),
(996, 1500),
(797, 1200),
(598, 900),
],
[
(logging.WARNING, "The image was resized to (1992 x 3000)."),
(logging.WARNING, "The image was resized to (1793 x 2700)."),
(logging.WARNING, "The image was resized to (1694 x 2550)."),
(logging.WARNING, "The image was resized to (1594 x 2400)."),
(logging.WARNING, "The image was resized to (1494 x 2250)."),
(logging.WARNING, "The image was resized to (1395 x 2100)."),
(logging.WARNING, "The image was resized to (1195 x 1800)."),
(logging.WARNING, "The image was resized to (996 x 1500)."),
(logging.WARNING, "The image was resized to (797 x 1200)."),
(logging.WARNING, "The image was resized to (598 x 900)."),
],
),
# Image above the limits
(
None,
100000, # 100kB
[(598, 900)],
[
(logging.WARNING, "The image was resized to (1992 x 3000)."),
(logging.WARNING, "The image size is 773.4 kB."),
(logging.WARNING, "Maximum image input size supported is 100.0 kB."),
(logging.WARNING, "The image will be resized."),
(logging.WARNING, "The image was resized to (1793 x 2700)."),
(logging.WARNING, "The image size is 616.0 kB."),
(logging.WARNING, "Maximum image input size supported is 100.0 kB."),
(logging.WARNING, "The image will be resized."),
(logging.WARNING, "The image was resized to (1694 x 2550)."),
(logging.WARNING, "The image size is 546.4 kB."),
(logging.WARNING, "Maximum image input size supported is 100.0 kB."),
(logging.WARNING, "The image will be resized."),
(logging.WARNING, "The image was resized to (1594 x 2400)."),
(logging.WARNING, "The image size is 479.4 kB."),
(logging.WARNING, "Maximum image input size supported is 100.0 kB."),
(logging.WARNING, "The image will be resized."),
(logging.WARNING, "The image was resized to (1494 x 2250)."),
(logging.WARNING, "The image size is 416.1 kB."),
(logging.WARNING, "Maximum image input size supported is 100.0 kB."),
(logging.WARNING, "The image will be resized."),
(logging.WARNING, "The image was resized to (1395 x 2100)."),
(logging.WARNING, "The image size is 360.5 kB."),
(logging.WARNING, "Maximum image input size supported is 100.0 kB."),
(logging.WARNING, "The image will be resized."),
(logging.WARNING, "The image was resized to (1195 x 1800)."),
(logging.WARNING, "The image size is 258.6 kB."),
(logging.WARNING, "Maximum image input size supported is 100.0 kB."),
(logging.WARNING, "The image will be resized."),
(logging.WARNING, "The image was resized to (996 x 1500)."),
(logging.WARNING, "The image size is 179.0 kB."),
(logging.WARNING, "Maximum image input size supported is 100.0 kB."),
(logging.WARNING, "The image will be resized."),
(logging.WARNING, "The image was resized to (797 x 1200)."),
(logging.WARNING, "The image size is 115.7 kB."),
(logging.WARNING, "Maximum image input size supported is 100.0 kB."),
(logging.WARNING, "The image will be resized."),
(logging.WARNING, "The image was resized to (598 x 900)."),
],
),
# Image above the limits
(
2000,
None,
[(1328, 2000), (1195, 1800), (996, 1500), (797, 1200), (598, 900)],
[
(
logging.WARNING,
"Maximum image input size supported is (2000 x 2000).",
),
(logging.WARNING, "The image will be resized."),
(logging.WARNING, "The image was resized to (1328 x 2000)."),
(logging.WARNING, "The image was resized to (1195 x 1800)."),
(logging.WARNING, "The image was resized to (996 x 1500)."),
(logging.WARNING, "The image was resized to (797 x 1200)."),
(logging.WARNING, "The image was resized to (598 x 900)."),
],
),
# Image above the limits
(
2000,
100000, # 100kB
[(598, 900)],
[
(
logging.WARNING,
"Maximum image input size supported is (2000 x 2000).",
),
(logging.WARNING, "The image will be resized."),
(logging.WARNING, "The image was resized to (1328 x 2000)."),
(logging.WARNING, "The image size is 325.5 kB."),
(logging.WARNING, "Maximum image input size supported is 100.0 kB."),
(logging.WARNING, "The image will be resized."),
(logging.WARNING, "The image was resized to (1195 x 1800)."),
(logging.WARNING, "The image size is 258.6 kB."),
(logging.WARNING, "Maximum image input size supported is 100.0 kB."),
(logging.WARNING, "The image will be resized."),
(logging.WARNING, "The image was resized to (996 x 1500)."),
(logging.WARNING, "The image size is 179.0 kB."),
(logging.WARNING, "Maximum image input size supported is 100.0 kB."),
(logging.WARNING, "The image will be resized."),
(logging.WARNING, "The image was resized to (797 x 1200)."),
(logging.WARNING, "The image size is 115.7 kB."),
(logging.WARNING, "Maximum image input size supported is 100.0 kB."),
(logging.WARNING, "The image will be resized."),
(logging.WARNING, "The image was resized to (598 x 900)."),
],
),
# Image always above the limits
(
50,
50, # 50B
[],
[
(logging.WARNING, "Maximum image input size supported is (50 x 50)."),
(logging.WARNING, "The image will be resized."),
(logging.WARNING, "The image was resized to (33 x 50)."),
(logging.WARNING, "The image size is 1.0 kB."),
(logging.WARNING, "Maximum image input size supported is 50 Bytes."),
(logging.WARNING, "The image will be resized."),
],
),
],
)
def test_resized_images(
max_pixels, max_bytes, expected_sizes, expected_logs, mock_page, caplog
):
caplog.set_level(logging.WARNING)
assert [
Image.open(image).size
for image in resized_images(mock_page, max_pixels, max_bytes)
] == expected_sizes
assert [
(record.levelno, record.message) for record in caplog.records
] == expected_logs
from pathlib import Path
import pytest
from arkindex_worker.utils import (
......@@ -9,9 +7,9 @@ from arkindex_worker.utils import (
extract_tar_zst_archive,
parse_source_id,
)
from tests import FIXTURES_DIR
FIXTURES = Path(__file__).absolute().parent / "data"
ARCHIVE = FIXTURES / "archive.tar.zst"
ARCHIVE = FIXTURES_DIR / "archive.tar.zst"
@pytest.mark.parametrize(
......
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