| import cv2 |
| import numpy as np |
|
|
| def _read_png_rgba(path): |
| png = cv2.imread(path, cv2.IMREAD_UNCHANGED) |
| if png is None or png.shape[2] != 4: |
| raise ValueError("Hairstyle PNG must be RGBA with transparency.") |
| return png |
|
|
| def auto_align(png_rgba, mask, landmarks=None): |
| """Scale & position the hairstyle PNG to cover the mask area, using landmarks if available.""" |
| mh, mw = mask.shape[:2] |
| ys, xs = np.where(mask > 0) |
| if len(xs) == 0 or len(ys) == 0: |
| return cv2.resize(png_rgba, (mw, mh)) |
|
|
| x0, x1 = xs.min(), xs.max() |
| y0, y1 = ys.min(), ys.max() |
| tw, th = int((x1 - x0) * 1.2), int((y1 - y0) * 1.1) |
| tw = max(1, min(tw, mw)) |
| th = max(1, min(th, mh)) |
| aligned = cv2.resize(png_rgba, (tw, th)) |
|
|
| canvas = np.zeros((mh, mw, 4), dtype=np.uint8) |
|
|
| |
| if landmarks and "forehead_anchor" in landmarks: |
| fx, fy = landmarks["forehead_anchor"] |
| y = max(0, fy - int(0.8 * th)) |
| x = max(0, fx - int(tw / 2)) |
| else: |
| y = max(0, y0 - int(0.25 * th)) |
| x = max(0, x0 - int(0.05 * tw)) |
|
|
| y2 = min(mh, y + th) |
| x2 = min(mw, x + tw) |
| crop_h, crop_w = y2 - y, x2 - x |
| canvas[y:y2, x:x2] = aligned[:crop_h, :crop_w] |
| return canvas |
|
|
| def _alpha_blend(base_bgr, overlay_rgba): |
| bgr = base_bgr.copy() |
| alpha = overlay_rgba[:, :, 3:4] / 255.0 |
| rgb = overlay_rgba[:, :, :3] |
| bgr = (alpha * rgb + (1 - alpha) * bgr).astype(np.uint8) |
| return bgr |
|
|
| def apply_hairstyle(img_bgr, style_path, mask, landmarks=None): |
| png = _read_png_rgba(style_path) |
| aligned = auto_align(png, mask, landmarks) |
| out = _alpha_blend(img_bgr, aligned) |
| return out |