diff --git a/cv_analysis/utils/image_operations.py b/cv_analysis/utils/image_operations.py index 58b3ffe..93813fc 100644 --- a/cv_analysis/utils/image_operations.py +++ b/cv_analysis/utils/image_operations.py @@ -2,7 +2,10 @@ from typing import Tuple import cv2 as cv import numpy as np -from PIL import ImageOps +from PIL import ImageOps, Image +from loguru import logger + +from cv_analysis.utils.conversion import normalize_image_format_to_pil Color = Tuple[int, int, int] @@ -49,3 +52,64 @@ def color_shift_array(image: np.ndarray, color: Color): assert colored.shape == image.shape return colored + + +def superimpose( + base_image: Image, + image_to_superimpose: Image, + crop_to_content=True, + pad=True, +) -> Image: + """Superimposes an image with transparency onto another image. + + Args: + base_image: The page image. + image_to_superimpose: The texture image. + crop_to_content: If True, the texture will be cropped to content (i.e. the bounding box of all non-transparent + parts of the texture image). + pad: If True, the texture will be padded to the size of the page. + + Returns: + Image where the texture is superimposed onto the page. + """ + base_image = normalize_image_format_to_pil(base_image) + image_to_superimpose = normalize_image_format_to_pil(image_to_superimpose) + + if crop_to_content: + image_to_superimpose = image_to_superimpose.crop(image_to_superimpose.getbbox()) + + if base_image.size != image_to_superimpose.size: + logger.trace(f"Size of page and texture do not match: {base_image.size} != {image_to_superimpose.size}") + if pad: + logger.trace(f"Padding texture before pasting to fit size {base_image.size}") + image_to_superimpose = pad_image_to_size(image_to_superimpose, base_image.size) + else: + logger.trace(f"Resizing texture before pasting to fit size {base_image.size}") + image_to_superimpose = image_to_superimpose.resize(base_image.size) + + assert base_image.size == image_to_superimpose.size + assert image_to_superimpose.mode == "RGBA" + + base_image.paste(image_to_superimpose, (0, 0), image_to_superimpose) + return base_image + + +def pad_image_to_size(image: Image, size: Tuple[int, int]) -> Image: + """Pads an image to a given size.""" + if image.size == size: + return image + + if image.size[0] > size[0] or image.size[1] > size[1]: + raise ValueError(f"Image size {image.size} is larger than target size {size}.") + + padded = Image.new(image.mode, size, color=255) + + pasting_coords = compute_pasting_coordinates(image, padded) + assert image.mode == "RGBA" + padded.paste(image, pasting_coords) + return padded + + +def compute_pasting_coordinates(smaller: Image, larger: Image.Image): + """Computes the coordinates for centrally pasting a smaller image onto a larger image.""" + return abs(larger.width - smaller.width) // 2, abs(larger.height - smaller.height) // 2 diff --git a/test/fixtures/page_generation/page.py b/test/fixtures/page_generation/page.py index ab8c21a..65a2d52 100644 --- a/test/fixtures/page_generation/page.py +++ b/test/fixtures/page_generation/page.py @@ -23,7 +23,7 @@ from matplotlib.colors import ListedColormap from cv_analysis.utils import star, rconj, conj from cv_analysis.utils.conversion import normalize_image_format_to_array, normalize_image_format_to_pil -from cv_analysis.utils.image_operations import blur, sharpen, overlay +from cv_analysis.utils.image_operations import blur, sharpen, overlay, superimpose, compute_pasting_coordinates from cv_analysis.utils.merging import merge_related_rectangles from cv_analysis.utils.postprocessing import remove_overlapping, remove_included from cv_analysis.utils.spacial import area @@ -185,7 +185,7 @@ def normalize_image_function(func): @pytest.fixture def texture(tinted_blank_page, base_texture): - texture = superimpose_texture_with_transparency(base_texture, tinted_blank_page) + texture = superimpose(base_texture, tinted_blank_page) return texture @@ -215,74 +215,6 @@ def size(dpi, orientation): return size -def superimpose_texture_with_transparency( - page: Image, - texture: Image, - crop_to_content=True, - pad=True, -) -> Image: - """Superimposes a noise image with transparency onto a page image. - - TODO: Rename page and texture to something more generic. - - Args: - page: The page image. - texture: The texture image. - crop_to_content: If True, the texture will be cropped to content (i.e. the bounding box of all non-transparent - parts of the texture image). - pad: If True, the texture will be padded to the size of the page. - - Returns: - Image where the texture is superimposed onto the page. - """ - page = normalize_image_format_to_pil(page) - texture = normalize_image_format_to_pil(texture) - - if crop_to_content: - texture = texture.crop(texture.getbbox()) - - if page.size != texture.size: - logger.trace(f"Size of page and texture do not match: {page.size} != {texture.size}") - if pad: - logger.trace(f"Padding texture before pasting to fit size {page.size}") - texture = pad_image_to_size(texture, page.size) - else: - logger.trace(f"Resizing texture before pasting to fit size {page.size}") - texture = texture.resize(page.size) - - assert page.size == texture.size - assert texture.mode == "RGBA" - - page.paste(texture, (0, 0), texture) - return page - - -def pad_image_to_size(image: Image, size: Tuple[int, int]) -> Image: - """Pads an image to a given size.""" - if image.size == size: - return image - - if image.size[0] > size[0] or image.size[1] > size[1]: - raise ValueError(f"Image size {image.size} is larger than target size {size}.") - - padded = Image.new(image.mode, size, color=255) - - pasting_coords = compute_pasting_coordinates(image, padded) - assert image.mode == "RGBA" - padded.paste(image, pasting_coords) - return padded - - -def compute_pasting_coordinates(smaller: Image, larger: Image.Image): - """Computes the coordinates for centrally pasting a smaller image onto a larger image.""" - return abs(larger.width - smaller.width) // 2, abs(larger.height - smaller.height) // 2 - - -def to_array(image: Image) -> np.ndarray: - """Converts a PIL image to a numpy array.""" - return np.array(image).astype(np.float32) - - class ContentGenerator: def __init__(self): self.constrain_layouts = True @@ -613,9 +545,9 @@ class RecursiveRandomTable(RandomContentRectangle): c2.draw_top_border(width=1) c2.draw_bottom_border(width=1) - c = superimpose_texture_with_transparency(c1.content, c2.content) + c = superimpose(c1.content, c2.content) - self.content = superimpose_texture_with_transparency(c, self.content) + self.content = superimpose(c, self.content) yield self @@ -716,7 +648,7 @@ class Cell(ContentRectangle): def fill(self, color=None): color = color or self.background_color image = Image.new("RGBA", (self.width, self.height), color=color) - self.content = superimpose_texture_with_transparency(image, self.content) + self.content = superimpose(image, self.content) return self @@ -1027,7 +959,7 @@ class RandomPlot(RandomContentRectangle): image = dump_plt_to_image(rectangle) assert image.mode == "RGBA" - self.content = image if not self.content else superimpose_texture_with_transparency(self.content, image) + self.content = image if not self.content else superimpose(self.content, image) def maybe(): @@ -1219,7 +1151,7 @@ class TextBlock(ContentRectangle): return self.__put_content(image) def __put_content(self, image: Image.Image): - self.content = image if not self.content else superimpose_texture_with_transparency(self.content, image) + self.content = image if not self.content else superimpose(self.content, image) assert self.content.mode == "RGBA" return self @@ -1375,11 +1307,6 @@ def drop_small_boxes(boxes: Iterable[Rectangle], page_width, page_height, min_pe return lremove(small, boxes) -def draw_boxes(page: Image, boxes: Iterable[Rectangle]): - # page = draw_rectangles(page, boxes, filled=False, annotate=True) - show_image(page, backend="pil") - - @pytest.fixture def page_with_opaque_content( blank_page, @@ -1427,6 +1354,15 @@ def page_with_translucent_content( texture = random_flip(texture) texture = texture_fn(texture) + page = blend_by_multiply(page_content, texture) + + return page, boxes + + +def blend_by_multiply(page_content, texture): + def to_array(image: Image) -> np.ndarray: + return np.array(image).astype(np.float32) + texture.putalpha(255) page_content.putalpha(255) factor = 1.2 @@ -1443,8 +1379,7 @@ def page_with_translucent_content( ), opacity=1, ).astype(np.uint8) - - return page, boxes + return page @pytest.fixture @@ -1459,3 +1394,10 @@ def page_with_content( draw_boxes(page, boxes) return page + + +def draw_boxes(page: Image, boxes: Iterable[Rectangle]): + from cv_analysis.utils.drawing import draw_rectangles + + page = draw_rectangles(page, boxes, filled=False, annotate=True) + show_image(page, backend="pil")