54 lines
1.5 KiB
Python
54 lines
1.5 KiB
Python
|
|
"""Basic retouching tools: healing / inpainting and skin softening."""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
from typing import Tuple
|
|||
|
|
|
|||
|
|
import numpy as np
|
|||
|
|
from PIL import Image
|
|||
|
|
|
|||
|
|
|
|||
|
|
def heal_spot(
|
|||
|
|
img: Image.Image,
|
|||
|
|
center: Tuple[int, int],
|
|||
|
|
radius: int,
|
|||
|
|
) -> Image.Image:
|
|||
|
|
"""Remove blemish at *center* using OpenCV Telea inpainting."""
|
|||
|
|
import cv2
|
|||
|
|
|
|||
|
|
arr = np.asarray(img.convert("RGB"))
|
|||
|
|
bgr = cv2.cvtColor(arr, cv2.COLOR_RGB2BGR)
|
|||
|
|
|
|||
|
|
mask = np.zeros(arr.shape[:2], dtype=np.uint8)
|
|||
|
|
cv2.circle(mask, center, radius, 255, -1)
|
|||
|
|
|
|||
|
|
result = cv2.inpaint(bgr, mask, inpaintRadius=radius * 2, flags=cv2.INPAINT_TELEA)
|
|||
|
|
result = cv2.cvtColor(result, cv2.COLOR_BGR2RGB)
|
|||
|
|
return Image.fromarray(result)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def soften_skin(
|
|||
|
|
rgb_float01: np.ndarray,
|
|||
|
|
mask: np.ndarray,
|
|||
|
|
strength: float,
|
|||
|
|
) -> np.ndarray:
|
|||
|
|
"""Edge-preserving skin softening (bilateral filter) within *mask* area.
|
|||
|
|
|
|||
|
|
*strength*: 0..100. 0 = no effect.
|
|||
|
|
*mask*: float01 (H, W) – typically the person/face mask.
|
|||
|
|
"""
|
|||
|
|
if strength <= 0:
|
|||
|
|
return rgb_float01
|
|||
|
|
import cv2
|
|||
|
|
|
|||
|
|
img8 = np.clip(rgb_float01 * 255, 0, 255).astype(np.uint8)
|
|||
|
|
|
|||
|
|
d = max(3, int(strength / 10))
|
|||
|
|
sigma_color = 30 + strength * 0.7
|
|||
|
|
sigma_space = 30 + strength * 0.7
|
|||
|
|
smoothed = cv2.bilateralFilter(img8, d, sigma_color, sigma_space)
|
|||
|
|
|
|||
|
|
smoothed_f = smoothed.astype(np.float32) / 255.0
|
|||
|
|
alpha = np.clip(mask * min(strength / 100.0, 1.0), 0.0, 1.0)[..., None]
|
|||
|
|
result = rgb_float01 * (1.0 - alpha) + smoothed_f * alpha
|
|||
|
|
return np.clip(result, 0.0, 1.0).astype(np.float32)
|