bilegentile's picture
Upload folder using huggingface_hub
c19ca42 verified
raw
history blame contribute delete
8.82 kB
from enum import Enum
import cv2
import numpy as np
from ..utils.utils import get_h_w_c
from .image_utils import as_target_channels, normalize, to_uint8
class BlendMode(Enum):
NORMAL = 0
DARKEN = 2
MULTIPLY = 1
COLOR_BURN = 5
LINEAR_BURN = 22
LIGHTEN = 3
SCREEN = 12
COLOR_DODGE = 6
ADD = 4
OVERLAY = 9
SOFT_LIGHT = 17
HARD_LIGHT = 18
VIVID_LIGHT = 19
LINEAR_LIGHT = 20
PIN_LIGHT = 21
REFLECT = 7
GLOW = 8
DIFFERENCE = 10
EXCLUSION = 16
NEGATION = 11
SUBTRACT = 14
DIVIDE = 15
XOR = 13
__normalized = {
BlendMode.NORMAL: True,
BlendMode.MULTIPLY: True,
BlendMode.DARKEN: True,
BlendMode.LIGHTEN: True,
BlendMode.ADD: False,
BlendMode.COLOR_BURN: False,
BlendMode.COLOR_DODGE: False,
BlendMode.REFLECT: False,
BlendMode.GLOW: False,
BlendMode.OVERLAY: True,
BlendMode.DIFFERENCE: True,
BlendMode.NEGATION: True,
BlendMode.SCREEN: True,
BlendMode.XOR: True,
BlendMode.SUBTRACT: False,
BlendMode.DIVIDE: False,
BlendMode.EXCLUSION: True,
BlendMode.SOFT_LIGHT: True,
BlendMode.HARD_LIGHT: True,
BlendMode.VIVID_LIGHT: False,
BlendMode.LINEAR_LIGHT: False,
BlendMode.PIN_LIGHT: True,
BlendMode.LINEAR_BURN: False,
}
def blend_mode_normalized(blend_mode: BlendMode) -> bool:
"""
Returns whether the given blend mode is guaranteed to produce normalized results (value between 0 and 1).
"""
return __normalized.get(blend_mode, False)
class ImageBlender:
"""Class for compositing images using different blending modes."""
def __init__(self):
self.modes = {
BlendMode.NORMAL: self.__normal,
BlendMode.MULTIPLY: self.__multiply,
BlendMode.DARKEN: self.__darken,
BlendMode.LIGHTEN: self.__lighten,
BlendMode.ADD: self.__add,
BlendMode.COLOR_BURN: self.__color_burn,
BlendMode.COLOR_DODGE: self.__color_dodge,
BlendMode.REFLECT: self.__reflect,
BlendMode.GLOW: self.__glow,
BlendMode.OVERLAY: self.__overlay,
BlendMode.DIFFERENCE: self.__difference,
BlendMode.NEGATION: self.__negation,
BlendMode.SCREEN: self.__screen,
BlendMode.XOR: self.__xor,
BlendMode.SUBTRACT: self.__subtract,
BlendMode.DIVIDE: self.__divide,
BlendMode.EXCLUSION: self.__exclusion,
BlendMode.SOFT_LIGHT: self.__soft_light,
BlendMode.HARD_LIGHT: self.__hard_light,
BlendMode.VIVID_LIGHT: self.__vivid_light,
BlendMode.LINEAR_LIGHT: self.__linear_light,
BlendMode.PIN_LIGHT: self.__pin_light,
BlendMode.LINEAR_BURN: self.__linear_burn,
}
def apply_blend(
self, a: np.ndarray, b: np.ndarray, blend_mode: BlendMode
) -> np.ndarray:
return self.modes[blend_mode](a, b)
def __normal(self, a: np.ndarray, _: np.ndarray) -> np.ndarray:
return a
def __multiply(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return a * b
def __darken(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return np.minimum(a, b)
def __lighten(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return np.maximum(a, b)
def __add(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return a + b
def __color_burn(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return np.where(
a == 0, 0, np.maximum(0, (1 - ((1 - b) / np.maximum(0.0001, a))))
)
def __color_dodge(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return np.where(a == 1, 1, np.minimum(1, b / np.maximum(0.0001, (1 - a))))
def __reflect(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return np.where(a == 1, 1, np.minimum(1, b * b / np.maximum(0.0001, 1 - a)))
def __glow(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return np.where(b == 1, 1, np.minimum(1, a * a / np.maximum(0.0001, 1 - b)))
def __overlay(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return np.where(b < 0.5, 2 * b * a, 1 - 2 * (1 - b) * (1 - a))
def __difference(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return np.asarray(cv2.absdiff(a, b))
def __negation(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return 1 - cv2.absdiff(1 - b, a) # type: ignore
def __screen(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return a + b - (a * b) # type: ignore
def __xor(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return normalize(
np.bitwise_xor(to_uint8(a, normalized=True), to_uint8(b, normalized=True))
)
def __subtract(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return b - a
def __divide(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return b / np.maximum(0.0001, a)
def __exclusion(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return a * (1 - b) + b * (1 - a)
def __soft_light(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
l = 2 * b * a + np.square(b) * (1 - 2 * a)
h = np.sqrt(b) * (2 * a - 1) + 2 * b * (1 - a)
return np.where(a <= 0.5, l, h)
def __hard_light(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return np.where(a <= 0.5, 2 * a * b, 1 - 2 * (1 - a) * (1 - b))
def __vivid_light(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return np.where(a <= 0.5, self.__color_burn(a, b), self.__color_dodge(a, b))
def __linear_light(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return b + 2 * a - 1
def __pin_light(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
x = 2 * a
y = x - 1
return np.where(b < y, y, np.where(b > x, x, b))
def __linear_burn(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return a + b - 1
def blend_images(overlay: np.ndarray, base: np.ndarray, blend_mode: BlendMode):
"""
Changes the given image to the background overlayed with the image.
The 2 given images must be the same size and their values must be between 0 and 1.
The returned image is guaranteed to have values between 0 and 1.
If the 2 given images have a different number of channels, then the returned image
will have maximum of the two.
Only grayscale, RGB, and RGBA images are supported.
"""
o_shape = get_h_w_c(overlay)
b_shape = get_h_w_c(base)
assert (
o_shape[:2] == b_shape[:2]
), "The overlay and the base image must have the same size"
def assert_sane(c: int, name: str):
sane = c in (1, 3, 4)
assert sane, f"The {name} has to be a grayscale, RGB, or RGBA image"
o_channels = o_shape[2]
b_channels = b_shape[2]
assert_sane(o_channels, "overlay layer")
assert_sane(b_channels, "base layer")
blender = ImageBlender()
target_c = max(o_channels, b_channels)
needs_clipping = not blend_mode_normalized(blend_mode)
if target_c == 4 and b_channels < 4:
base = as_target_channels(base, 3)
# The general algorithm below can be optimized because we know that b_a is 1
o_a = np.dstack((overlay[:, :, 3],) * 3)
o_rgb = overlay[:, :, :3]
blend_rgb = blender.apply_blend(o_rgb, base, blend_mode)
final_rgb = o_a * blend_rgb + (1 - o_a) * base # type: ignore
if needs_clipping:
final_rgb = np.clip(final_rgb, 0, 1)
return as_target_channels(final_rgb, 4)
overlay = as_target_channels(overlay, target_c)
base = as_target_channels(base, target_c)
if target_c in (1, 3):
# We don't need to do any alpha blending, so the images can blended directly
result = blender.apply_blend(overlay, base, blend_mode)
if needs_clipping:
result = np.clip(result, 0, 1)
return result
# do the alpha blending for RGBA
o_a = overlay[:, :, 3]
b_a = base[:, :, 3]
o_rgb = overlay[:, :, :3]
b_rgb = base[:, :, :3]
final_a = 1 - (1 - o_a) * (1 - b_a)
blend_strength = o_a * b_a
o_strength = o_a - blend_strength # type: ignore
b_strength = b_a - blend_strength # type: ignore
blend_rgb = blender.apply_blend(o_rgb, b_rgb, blend_mode)
final_rgb = (
(np.dstack((o_strength,) * 3) * o_rgb)
+ (np.dstack((b_strength,) * 3) * b_rgb)
+ (np.dstack((blend_strength,) * 3) * blend_rgb)
)
final_rgb /= np.maximum(np.dstack((final_a,) * 3), 0.0001) # type: ignore
final_rgb = np.clip(final_rgb, 0, 1)
result = np.concatenate([final_rgb, np.expand_dims(final_a, axis=2)], axis=2)
if needs_clipping:
result = np.clip(result, 0, 1)
return result