2023-01-16 13:35:59 +01:00

404 lines
11 KiB
Python

import random
from typing import Tuple, Union, Iterable, List
import albumentations as A
import cv2 as cv
import numpy as np
import pytest
from PIL import Image, ImageOps
from PIL.Image import Transpose
Image_t = Union[Image.Image, np.ndarray]
#
# transform = A.Compose(
# [
# # geometric transforms
# A.HorizontalFlip(p=0.2),
# A.RandomRotate90(p=0.2),
# A.VerticalFlip(p=0.2),
# # brightness and contrast transforms
# A.OneOf(
# [
# A.RandomGamma(p=0.5),
# A.RandomBrightnessContrast(p=0.5),
# ],
# p=0.5,
# ),
# # noise transforms
# A.SomeOf(
# [
# A.Emboss(p=0.05),
# A.ImageCompression(p=0.05),
# A.PixelDropout(p=0.05),
# ],
# p=0.5,
# n=2,
# ),
# # color transforms
# A.SomeOf(
# [
# A.ColorJitter(p=1),
# A.RGBShift(p=1, r_shift_limit=0.1, g_shift_limit=0.1, b_shift_limit=0.1),
# A.ChannelShuffle(p=1),
# ],
# p=0.5,
# n=3, # 3 => all
# ),
# # blurring and sharpening transforms
# A.OneOf(
# [
# A.GaussianBlur(p=0.05),
# A.MotionBlur(p=0.05, blur_limit=21),
# A.Sharpen(p=0.05),
# ],
# p=0.5,
# ),
# # environmental transforms
# A.OneOf(
# [
# A.RandomRain(p=0.2, rain_type="drizzle"),
# A.RandomFog(p=0.2, fog_coef_upper=0.4),
# A.RandomSnow(p=0.2),
# ],
# p=0.5,
# ),
# ],
# p=0.5,
# )
from funcy import juxt, compose, identity, lflatten
from cv_analysis.locations import TEST_PAGE_TEXTURES_DIR
# transform = A.Compose(
# [
# # brightness and contrast transforms
# A.OneOf(
# [
# A.RandomGamma(p=0.2),
# A.RandomBrightnessContrast(p=0.2, brightness_limit=0.05, contrast_limit=0.05),
# ],
# p=0.5,
# ),
# # color transforms
# A.SomeOf(
# [
# A.ColorJitter(p=1),
# A.RGBShift(p=1, r_shift_limit=0.3, g_shift_limit=0.3, b_shift_limit=0.3),
# A.ChannelShuffle(p=1),
# ],
# p=1.0,
# n=3, # 3 => all
# ),
# # # blurring and sharpening transforms
# # A.OneOf(
# # [
# # A.GaussianBlur(p=0.05),
# # A.MotionBlur(p=0.05, blur_limit=21),
# # A.Sharpen(p=0.05),
# # ],
# # p=0.0,
# # ),
# ]
# )
from cv_analysis.utils.display import show_image
from cv_analysis.utils.drawing import draw_rectangles
from cv_analysis.utils.rectangle import Rectangle
transform = A.Compose(
[
# A.ColorJitter(p=1),
]
)
Color = Tuple[int, int, int]
@pytest.fixture(
params=[
# "rough_grain",
# "plain",
# "digital",
"crumpled",
]
)
def base_texture(request, size):
texture = Image.open(TEST_PAGE_TEXTURES_DIR / (request.param + ".jpg"))
texture = texture.resize(size)
return texture
@pytest.fixture(
params=[
"portrait",
# "landscape",
]
)
def orientation(request):
return request.param
@pytest.fixture(
params=[
# 30,
100,
]
)
def dpi(request):
return request.param
@pytest.fixture(
params=[
# "brown",
"sepia",
# "gray",
# "white",
# "light_red",
# "light_blue",
]
)
def color_name(request):
return request.param
@pytest.fixture(
params=[
# "smooth",
# "coarse",
"neutral",
]
)
def texture_name(request):
return request.param
@pytest.fixture(
params=[
# 30,
70,
# 150,
]
)
def color_intensity(request):
return request.param
def random_flip(image):
if random.choice([True, False]):
image = image.transpose(Transpose.FLIP_LEFT_RIGHT)
if random.choice([True, False]):
image = image.transpose(Transpose.FLIP_TOP_BOTTOM)
return image
@pytest.fixture
def color(color_name):
return {
"brown": "#7d6c5b",
"sepia": "#b8af88",
"gray": "#9c9c9c",
"white": "#ffffff",
"light_red": "#d68c8b",
"light_blue": "#8bd6d6",
}[color_name]
@pytest.fixture
def texture_fn(texture_name, size):
if texture_name == "smooth":
fn = blur
elif texture_name == "coarse":
fn = compose(overlay, juxt(blur, sharpen))
else:
fn = identity
return normalize_image_function(fn)
def blur(image: np.ndarray):
return cv.blur(image, (3, 3))
def normalize_image_format_to_array(image: Image_t):
if isinstance(image, Image.Image):
return np.array(image)
return image
def normalize_image_format_to_pil(image: Image_t):
if isinstance(image, np.ndarray):
return Image.fromarray(image)
return image
def normalize_image_function(func):
def inner(image):
image = normalize_image_format_to_array(image)
image = func(image)
image = normalize_image_format_to_pil(image)
return image
return inner
def sharpen(image: np.ndarray):
return cv.filter2D(image, -1, np.array([[-1, -1, -1], [-1, 6, -1], [-1, -1, -1]]))
def overlay(images, mode=np.sum):
assert mode in [np.sum, np.max]
images = np.stack(list(images))
image = mode(images, axis=0)
image = (image / image.max() * 255).astype(np.uint8)
return image
@pytest.fixture
def texture(base_texture, color, color_intensity):
color_image = Image.new("RGBA", base_texture.size, color)
color_image.putalpha(color_intensity)
texture = superimpose_texture_with_transparency(base_texture, color_image)
return texture
def tint_image(src, color="#FFFFFF"):
src.load()
r, g, b, alpha = src.split()
gray = ImageOps.grayscale(src)
result = ImageOps.colorize(gray, (0, 0, 0), color)
result.putalpha(alpha)
return result
def color_shift_array(image: np.ndarray, color: Color):
"""Creates a 3-tensor from a 2-tensor by stacking the 2-tensor three times weighted by the color tuple."""
assert image.ndim == 3
assert image.shape[-1] == 3
assert isinstance(color, tuple)
assert max(color) <= 255
assert image.max() <= 255
color = np.array(color)
weights = color / color.sum() / 10
assert max(weights) <= 1
colored = (image * weights).astype(np.uint8)
assert colored.shape == image.shape
return colored
@pytest.fixture
def size(dpi, orientation):
if orientation == "portrait":
size = (8.5 * dpi, 11 * dpi)
elif orientation == "landscape":
size = (11 * dpi, 8.5 * dpi)
else:
raise ValueError(f"Unknown orientation: {orientation}")
size = tuple(map(int, size))
return size
def superimpose_texture_with_transparency(page: Image, texture: Image) -> Image:
"""Superimposes a noise image with transparency onto a page image."""
assert page.mode == "RGB"
assert texture.mode == "RGBA"
assert page.size == texture.size
page.paste(texture, (0, 0), texture)
return page
@pytest.fixture
def blank_page(texture, texture_fn) -> np.ndarray:
"""Creates a blank page with a given orientation and dpi."""
page = random_flip(texture)
page = texture_fn(page)
content_box_generator = ContentBoxGenerator()
boxes = content_box_generator(page)
content_box_generator.draw_boxes(page, boxes)
return page
class ContentBoxGenerator:
def __init__(self):
self.left_margin_percentage = 0.05
self.right_margin_percentage = 0.05
self.top_margin_percentage = 0.1
self.bottom_margin_percentage = 0.1
self.margin_percentage = 0.005
self.max_depth = 3
self.initial_recursion_probability = 1
self.recursion_probability_decay = 0.1
def __call__(self, page: Image.Image) -> List[Rectangle]:
left_margin = int(page.width * self.left_margin_percentage)
right_margin = int(page.width * self.right_margin_percentage)
top_margin = int(page.height * self.top_margin_percentage)
bottom_margin = int(page.height * self.bottom_margin_percentage)
box = Rectangle(left_margin, top_margin, page.width - right_margin, page.height - bottom_margin)
boxes = lflatten(self.generate_content_boxes(box))
boxes = self.drop_small_boxes(boxes, *page.size)
return boxes
def draw_boxes(self, page: Image, boxes: Iterable[Rectangle]):
image = draw_rectangles(page, boxes, filled=False, annotate=True)
show_image(image)
def generate_content_boxes(self, box: Rectangle, depth=0):
if depth >= self.max_depth:
yield box
else:
child_boxes = self.generate_random_child_boxes(box)
if self.recurse(depth):
yield from (self.generate_content_boxes(b, depth + 1) for b in child_boxes)
else:
yield child_boxes
def generate_random_child_boxes(self, box: Rectangle) -> Tuple[Rectangle, Rectangle]:
axis = random.choice(["x", "y"])
edge_anchor_point, edge_length = (box.x1, box.width) if axis == "x" else (box.y1, box.height)
split_coordinate = random.uniform(0.3, 0.7) * edge_length + edge_anchor_point
child_boxes = self.get_child_boxes(box, split_coordinate, axis)
return child_boxes
def get_child_boxes(self, box: Rectangle, split_coordinate, axis) -> Tuple[Rectangle, Rectangle]:
def low(p):
return p * (1 + self.margin_percentage)
def high(p):
return p * (1 - self.margin_percentage)
if axis == "x":
return (
Rectangle(low(box.x1), low(box.y1), high(split_coordinate), high(box.y2)),
Rectangle(low(split_coordinate), low(box.y1), high(box.x2), high(box.y2)),
)
else:
return (
Rectangle(low(box.x1), low(box.y1), high(box.x2), high(split_coordinate)),
Rectangle(low(box.x1), low(split_coordinate), high(box.x2), high(box.y2)),
)
def recurse(self, depth):
return random.random() <= self.recursion_probability(depth)
def recursion_probability(self, depth):
return self.initial_recursion_probability * (1 - self.recursion_probability_decay) ** depth
def drop_small_boxes(
self,
boxes: Iterable[Rectangle],
page_width,
page_height,
min_percentage=0.13,
) -> List[Rectangle]:
min_width = page_width * min_percentage
min_height = page_height * min_percentage
return [b for b in boxes if b.width > min_width and b.height > min_height]