File size: 2,685 Bytes
acb9f1e
 
 
 
 
 
a068458
 
acb9f1e
 
 
 
 
 
 
 
d5e1b6d
acb9f1e
 
 
 
 
d5e1b6d
acb9f1e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import cv2
import numpy as np
from PIL import Image


def pil_to_bgr(pil_img: Image.Image) -> np.ndarray:
    """Convert PIL image to OpenCV BGR. Handles RGB, RGBA, palette, grayscale."""
    pil_img = pil_img.convert("RGB")  # always normalise to 3-channel RGB
    return cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)


def bgr_to_pil(bgr_img: np.ndarray) -> Image.Image:
    """Convert OpenCV BGR numpy array to PIL RGB image."""
    return Image.fromarray(cv2.cvtColor(bgr_img, cv2.COLOR_BGR2RGB))


def resize_to_max(image: np.ndarray, max_size: int = 2048) -> np.ndarray:
    """Downscale image so its longest side does not exceed max_size."""
    h, w = image.shape[:2]
    if max(h, w) <= max_size:
        return image
    scale = max_size / max(h, w)
    return cv2.resize(image, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_LANCZOS4)


def apply_color_correction(source: np.ndarray, target: np.ndarray, mask: np.ndarray) -> np.ndarray:
    """
    Shift source pixel statistics (mean/std per LAB channel) inside the masked
    region to match those of the target, improving blending realism.
    """
    source_lab = cv2.cvtColor(source, cv2.COLOR_BGR2LAB).astype(float)
    target_lab = cv2.cvtColor(target, cv2.COLOR_BGR2LAB).astype(float)

    mask_bool = mask > 128

    for ch in range(3):
        src_vals = source_lab[:, :, ch][mask_bool]
        tgt_vals = target_lab[:, :, ch][mask_bool]

        src_mean, src_std = src_vals.mean(), src_vals.std()
        tgt_mean, tgt_std = tgt_vals.mean(), tgt_vals.std()

        if src_std > 1e-6:
            source_lab[:, :, ch][mask_bool] = (
                (src_vals - src_mean) * (tgt_std / src_std) + tgt_mean
            )

    source_lab = np.clip(source_lab, 0, 255).astype(np.uint8)
    return cv2.cvtColor(source_lab, cv2.COLOR_LAB2BGR)


def feather_mask(mask: np.ndarray, blur_radius: int = 15) -> np.ndarray:
    """Apply Gaussian blur to soften mask edges."""
    if blur_radius % 2 == 0:
        blur_radius += 1
    return cv2.GaussianBlur(mask, (blur_radius, blur_radius), 0)


def alpha_blend(foreground: np.ndarray, background: np.ndarray, mask: np.ndarray) -> np.ndarray:
    """
    Blend foreground onto background using a soft mask.

    Args:
        foreground: BGR image (same size as background).
        background: BGR image.
        mask: Single-channel uint8 mask (0-255).

    Returns:
        Blended BGR image.
    """
    alpha = mask.astype(float) / 255.0
    alpha_3ch = np.stack([alpha] * 3, axis=-1)
    blended = (foreground.astype(float) * alpha_3ch +
               background.astype(float) * (1.0 - alpha_3ch))
    return np.clip(blended, 0, 255).astype(np.uint8)