From 94ad95850ef1a635293b1a6296ee2ed89275e33f Mon Sep 17 00:00:00 2001 From: xsl Date: Sun, 14 Jun 2026 23:42:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=8E=A5=E5=8F=A32):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E7=94=9F=E5=8F=91=E5=90=8E=E5=9B=BE=E7=89=87(ComfyUI/Flux=20in?= =?UTF-8?q?paint)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在发际线预览基础上,每种发际线再出一张「植发3个月」生发图: - hairline/mask.py: headmark 5步法遮罩(额头上部区域∩SegFormer头部=ROI, 取发际线曲线以上闭合区域);用 hairline_texture_black 渲染黑线替代手绘检测; compose_comfy_rgba 合成 RGBA(alpha=255-mask, 透明=重绘区, 对齐 ComfyUI mask=1-alpha) - hairline/comfyui.py: ComfyUI 客户端(默认8182),/upload/image+/prompt(改节点26+随机seed) +轮询/history+/view 取回生发图 - hairline/render.py: 抽出 build_overlay_layer 供遮罩取曲线像素 - hairline/service.py: extract_context 一次出 landmarks/parse_map/502点; generate_grow_results 每种=预览+生发图(同步串行N张,单张ComfyUI失败则grown置空不拖垮整请求) - app.py: /hair/grow 返回 results[].grown_image_base64;重活放线程池避免卡事件循环 - add_hair.json 工作流 + hairline_texture_black/ 黑贴图入库 - 测试: test_mask.py(遮罩几何) + test_api mock ComfyUI 验 grown 字段,35 全绿 实测(5090): female 5张生发图同步约18s;预览/生发图人物五官服饰背景保持、黑线已清除。 Co-Authored-By: Claude Opus 4.8 --- add_hair.json | 450 ++++++++++++++++++++ app.py | 15 +- hairline/comfyui.py | 110 +++++ hairline/mask.py | 149 +++++++ hairline/render.py | 30 +- hairline/service.py | 75 +++- hairline_texture/man_ inverse_arc.png | Bin 5594 -> 5927 bytes hairline_texture_black/girl_ellipse.png | Bin 0 -> 3847 bytes hairline_texture_black/girl_flower.png | Bin 0 -> 4342 bytes hairline_texture_black/girl_heart.png | Bin 0 -> 3980 bytes hairline_texture_black/girl_straight.png | Bin 0 -> 3848 bytes hairline_texture_black/girl_wave.png | Bin 0 -> 4179 bytes hairline_texture_black/man_ inverse_arc.png | Bin 0 -> 4356 bytes hairline_texture_black/man_ellipse.png | Bin 0 -> 3863 bytes hairline_texture_black/man_m.png | Bin 0 -> 4092 bytes hairline_texture_black/man_straight.png | Bin 0 -> 3511 bytes tests/test_api.py | 12 +- tests/test_mask.py | 47 ++ 18 files changed, 859 insertions(+), 29 deletions(-) create mode 100644 add_hair.json create mode 100644 hairline/comfyui.py create mode 100644 hairline/mask.py create mode 100644 hairline_texture_black/girl_ellipse.png create mode 100644 hairline_texture_black/girl_flower.png create mode 100644 hairline_texture_black/girl_heart.png create mode 100644 hairline_texture_black/girl_straight.png create mode 100644 hairline_texture_black/girl_wave.png create mode 100644 hairline_texture_black/man_ inverse_arc.png create mode 100644 hairline_texture_black/man_ellipse.png create mode 100644 hairline_texture_black/man_m.png create mode 100644 hairline_texture_black/man_straight.png create mode 100644 tests/test_mask.py diff --git a/add_hair.json b/add_hair.json new file mode 100644 index 0000000..78e4b68 --- /dev/null +++ b/add_hair.json @@ -0,0 +1,450 @@ +{ + "1": { + "inputs": { + "scheduler": "simple", + "steps": 6, + "denoise": 1, + "model": [ + "2", + 0 + ] + }, + "class_type": "BasicScheduler", + "_meta": { + "title": "基本调度器" + } + }, + "2": { + "inputs": { + "max_shift": 1.15, + "base_shift": 0.5, + "width": [ + "14", + 0 + ], + "height": [ + "14", + 1 + ], + "model": [ + "16", + 0 + ] + }, + "class_type": "ModelSamplingFlux", + "_meta": { + "title": "采样算法(Flux)" + } + }, + "3": { + "inputs": { + "vae_name": "flux2-vae.safetensors" + }, + "class_type": "VAELoader", + "_meta": { + "title": "加载VAE" + } + }, + "5": { + "inputs": { + "conditioning": [ + "19", + 0 + ], + "latent": [ + "13", + 0 + ] + }, + "class_type": "ReferenceLatent", + "_meta": { + "title": "参考Latent" + } + }, + "6": { + "inputs": { + "noise_seed": 690815303279000 + }, + "class_type": "RandomNoise", + "_meta": { + "title": "随机噪波" + } + }, + "7": { + "inputs": { + "width": [ + "14", + 0 + ], + "height": [ + "14", + 1 + ], + "batch_size": 1 + }, + "class_type": "EmptySD3LatentImage", + "_meta": { + "title": "空Latent图像(SD3)" + } + }, + "8": { + "inputs": { + "sampler_name": "euler" + }, + "class_type": "KSamplerSelect", + "_meta": { + "title": "K采样器选择" + } + }, + "9": { + "inputs": { + "noise": [ + "6", + 0 + ], + "guider": [ + "20", + 0 + ], + "sampler": [ + "8", + 0 + ], + "sigmas": [ + "1", + 0 + ], + "latent_image": [ + "7", + 0 + ] + }, + "class_type": "SamplerCustomAdvanced", + "_meta": { + "title": "自定义采样器(高级)" + } + }, + "10": { + "inputs": { + "samples": [ + "9", + 0 + ], + "vae": [ + "3", + 0 + ] + }, + "class_type": "VAEDecode", + "_meta": { + "title": "VAE解码" + } + }, + "13": { + "inputs": { + "pixels": [ + "44", + 0 + ], + "vae": [ + "3", + 0 + ] + }, + "class_type": "VAEEncode", + "_meta": { + "title": "VAE编码" + } + }, + "14": { + "inputs": { + "image": [ + "44", + 0 + ] + }, + "class_type": "GetImageSize+", + "_meta": { + "title": "🔧 Get Image Size" + } + }, + "16": { + "inputs": { + "unet_name": "flux2.0/flux-2-klein-9b-fp8.safetensors", + "weight_dtype": "fp8_e4m3fn" + }, + "class_type": "UNETLoader", + "_meta": { + "title": "UNet加载器" + } + }, + "17": { + "inputs": { + "filename_prefix": "ComfyUI", + "images": [ + "62", + 0 + ] + }, + "class_type": "SaveImage", + "_meta": { + "title": "保存图像" + } + }, + "19": { + "inputs": { + "guidance": 1, + "conditioning": [ + "22", + 0 + ] + }, + "class_type": "FluxGuidance", + "_meta": { + "title": "Flux引导" + } + }, + "20": { + "inputs": { + "model": [ + "2", + 0 + ], + "conditioning": [ + "5", + 0 + ] + }, + "class_type": "BasicGuider", + "_meta": { + "title": "基本引导器" + } + }, + "22": { + "inputs": { + "text": [ + "60", + 0 + ], + "clip": [ + "61", + 0 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP文本编码" + } + }, + "26": { + "inputs": { + "image": "clipspace/clipspace-painted-masked-1780841075435.png [input]" + }, + "class_type": "LoadImage", + "_meta": { + "title": "加载图像" + } + }, + "31": { + "inputs": { + "image": [ + "26", + 0 + ] + }, + "class_type": "easy imageSize", + "_meta": { + "title": "图像尺寸" + } + }, + "32": { + "inputs": { + "aspect_ratio": "custom", + "proportional_width": [ + "31", + 0 + ], + "proportional_height": [ + "31", + 1 + ], + "fit": "letterbox", + "method": "lanczos", + "round_to_multiple": "8", + "scale_to_side": "None", + "scale_to_length": 1024, + "background_color": "#000000", + "image": [ + "26", + 0 + ], + "mask": [ + "37", + 0 + ] + }, + "class_type": "LayerUtility: ImageScaleByAspectRatio V2", + "_meta": { + "title": "LayerUtility: ImageScaleByAspectRatio V2" + } + }, + "33": { + "inputs": { + "masks": [ + "26", + 1 + ] + }, + "class_type": "Mask Fill Holes", + "_meta": { + "title": "遮罩填充漏洞" + } + }, + "36": { + "inputs": { + "masks": [ + "33", + 0 + ] + }, + "class_type": "Convert Masks to Images", + "_meta": { + "title": "遮罩到图像" + } + }, + "37": { + "inputs": { + "method": "intensity", + "image": [ + "39", + 0 + ] + }, + "class_type": "Image To Mask", + "_meta": { + "title": "图像到遮罩" + } + }, + "39": { + "inputs": { + "upscale_method": "nearest-exact", + "width": [ + "31", + 0 + ], + "height": [ + "31", + 1 + ], + "crop": "disabled", + "image": [ + "36", + 0 + ] + }, + "class_type": "ImageScale", + "_meta": { + "title": "缩放图像" + } + }, + "44": { + "inputs": { + "mask_opacity": 1, + "mask_color": "FFFF00", + "pass_through": true, + "image": [ + "32", + 0 + ], + "mask": [ + "32", + 1 + ] + }, + "class_type": "ImageAndMaskPreview", + "_meta": { + "title": "图像与遮罩预览" + } + }, + "45": { + "inputs": { + "images": [ + "44", + 0 + ] + }, + "class_type": "PreviewImage", + "_meta": { + "title": "预览图像" + } + }, + "53": { + "inputs": { + "rgthree_comparer": { + "images": [ + { + "name": "A", + "selected": true, + "url": "/api/view?filename=rgthree.compare._temp_nljpo_00001_.png&type=temp&subfolder=&rand=0.7003773747423834" + }, + { + "name": "B", + "selected": true, + "url": "/api/view?filename=rgthree.compare._temp_nljpo_00002_.png&type=temp&subfolder=&rand=0.10574778041280719" + } + ] + }, + "image_a": [ + "62", + 0 + ], + "image_b": [ + "26", + 0 + ] + }, + "class_type": "Image Comparer (rgthree)", + "_meta": { + "title": "图像对比" + } + }, + "60": { + "inputs": { + "text": "严格保留原图人物五官、面部肤色、神态、服饰、背景、原图光影与整体色调,画面除划线区域外全部细节保持原样不变;先清除画面内所有黑色标注划线,仅在原划线划定范围内生成植发术后 3 个月长发效果,新生头发生长边界刚好止于原划线位置,新发沿原划线轮廓边缘和外部原生发丝交错融合、自然无缝衔接,无生硬分界线;发色和自身原生黑发保持统一,发丝细腻写实、带有真实毛发纹理与自然光泽,发量均匀,符合植发 3 个月刚长出的轻微稀疏生长状态,毛发走向遵从头部原生头发生长规律,头发光影明暗和原图环境统一,整体写实自然,无贴片假发质感。" + }, + "class_type": "JjkText", + "_meta": { + "title": "Text" + } + }, + "61": { + "inputs": { + "clip_name": "qwen_3_8b_fp8mixed.safetensors", + "type": "flux2", + "device": "default" + }, + "class_type": "CLIPLoader", + "_meta": { + "title": "加载CLIP" + } + }, + "62": { + "inputs": { + "method": "mkl", + "strength": 1, + "multithread": true, + "image_ref": [ + "26", + 0 + ], + "image_target": [ + "10", + 0 + ] + }, + "class_type": "ColorMatch", + "_meta": { + "title": "Color Match" + } + } +} \ No newline at end of file diff --git a/app.py b/app.py index 1aab43b..6539a78 100644 --- a/app.py +++ b/app.py @@ -504,17 +504,22 @@ async def hair_grow( return err(1002, "人像分辨率过低") try: - from hairline.service import generate_previews + from fastapi.concurrency import run_in_threadpool + from hairline.service import generate_grow_results - previews = generate_previews(image, gender) # 无人脸 → None - if previews is None: + # 预览 + 生发(ComfyUI) 都是阻塞且较慢,放线程池避免卡住事件循环 + items = await run_in_threadpool(generate_grow_results, image, gender) + if items is None: return err(1001, "无法识别人像") results = [] - for p in previews: - ok_enc, png = cv2.imencode(".png", p["image_bgr"]) + for p in items: + _ok, png = cv2.imencode(".png", p["image_bgr"]) + grown_b64 = (base64.b64encode(p["grown_png"]).decode() + if p["grown_png"] else None) results.append({ "image_base64": base64.b64encode(png.tobytes()).decode(), + "grown_image_base64": grown_b64, "hairline_type": p["hairline_type"], "order": p["order"], }) diff --git a/hairline/comfyui.py b/hairline/comfyui.py new file mode 100644 index 0000000..e9422aa --- /dev/null +++ b/hairline/comfyui.py @@ -0,0 +1,110 @@ +"""ComfyUI 客户端:用 add_hair.json 工作流跑生发图(Flux-2 inpaint)。 + +worker 不跑 Flux,只把「划线图 + 遮罩」的 RGBA 上传到本机 ComfyUI(默认 8182), +替换工作流节点 26 的输入图、随机 seed,提交 /prompt,轮询 /history,取回 /view 输出。 +""" +from __future__ import annotations + +import copy +import json +import os +import random +import time +import uuid + +import httpx + +COMFYUI_URL = os.getenv("COMFYUI_URL", "http://127.0.0.1:8182").rstrip("/") +WORKFLOW_PATH = os.getenv( + "ADD_HAIR_WORKFLOW", + os.path.join(os.path.dirname(os.path.dirname(__file__)), "add_hair.json"), +) +COMFY_TIMEOUT = float(os.getenv("COMFYUI_TIMEOUT", "600")) # 单张出图最长等待(秒) + +_INPUT_NODE = "26" # LoadImage:外部输入图(含 alpha 遮罩) +_SEED_NODE = "6" # RandomNoise +_OUTPUT_NODE = "17" # SaveImage + +_workflow = None + + +def _load_workflow() -> dict: + global _workflow + if _workflow is None: + with open(WORKFLOW_PATH, encoding="utf-8") as f: + _workflow = json.load(f) + return _workflow + + +def run(rgba_png_bytes: bytes, timeout: float = COMFY_TIMEOUT) -> bytes: + """提交一次生发任务,返回输出 PNG 字节。失败抛异常。""" + client_id = uuid.uuid4().hex + with httpx.Client(base_url=COMFYUI_URL, timeout=30.0) as cli: + # 1. 上传输入图(含 alpha 遮罩)到 ComfyUI input 目录 + fname = f"hair_{client_id}.png" + r = cli.post("/upload/image", files={"image": (fname, rgba_png_bytes, "image/png")}, + data={"overwrite": "true", "type": "input"}) + r.raise_for_status() + up = r.json() + name = (up.get("subfolder") + "/" if up.get("subfolder") else "") + up["name"] + + # 2. 改工作流:节点26 输入图 + 随机 seed + wf = copy.deepcopy(_load_workflow()) + wf[_INPUT_NODE]["inputs"]["image"] = name + wf[_SEED_NODE]["inputs"]["noise_seed"] = random.randint(0, 2**63 - 1) + + # 3. 提交 + r = cli.post("/prompt", json={"prompt": wf, "client_id": client_id}) + r.raise_for_status() + prompt_id = r.json()["prompt_id"] + + # 4. 轮询 /history + deadline = time.time() + timeout + outputs = None + while time.time() < deadline: + hr = cli.get(f"/history/{prompt_id}") + hr.raise_for_status() + hist = hr.json() + if prompt_id in hist: + entry = hist[prompt_id] + status = entry.get("status", {}) + if status.get("status_str") == "error": + raise RuntimeError(f"ComfyUI 执行报错: {status}") + outputs = entry.get("outputs") + if outputs and _OUTPUT_NODE in outputs: + break + time.sleep(1.0) + if not outputs or _OUTPUT_NODE not in outputs: + raise TimeoutError(f"ComfyUI 出图超时({timeout}s) prompt_id={prompt_id}") + + # 5. 取回输出图 + imgs = outputs[_OUTPUT_NODE].get("images") or [] + if not imgs: + raise RuntimeError("ComfyUI 输出无图像") + info = imgs[0] + vr = cli.get("/view", params={"filename": info["filename"], + "subfolder": info.get("subfolder", ""), + "type": info.get("type", "output")}) + vr.raise_for_status() + return vr.content + + +def ping() -> bool: + """探测 ComfyUI 是否在线(/system_stats)。""" + try: + with httpx.Client(base_url=COMFYUI_URL, timeout=3.0) as cli: + return cli.get("/system_stats").status_code == 200 + except Exception: # noqa: BLE001 + return False + + +if __name__ == "__main__": + import sys + inp = sys.argv[1] if len(sys.argv) > 1 else "tests/output/comfy_input.png" + print("ComfyUI:", COMFYUI_URL, "online:", ping()) + with open(inp, "rb") as f: + png = run(f.read()) + out = "tests/output/grown.png" + with open(out, "wb") as f: + f.write(png) + print(f"生发图已存 {out}({len(png)} bytes)") diff --git a/hairline/mask.py b/hairline/mask.py new file mode 100644 index 0000000..f726437 --- /dev/null +++ b/hairline/mask.py @@ -0,0 +1,149 @@ +"""接口2 第二步:inpaint 遮罩 + 黑色发际线划线合成(参考 headmark 5步法)。 + +算法(用 hairline_texture_black 渲染黑线替代 headmark 的手绘检测): + ① 额头上部区域:MediaPipe 额头边界关键点连线,向上+两侧补到图像边缘填充 + ② 头部轮廓:SegFormer 头部类(hair∪skin∪…,排除 bg/neck/cloth) + ③ ROI = ① ∩ ② + ④ 渲染黑色发际线 → 烧进照片(marked) + 得到曲线像素 + ⑤ mask = ROI 中"发际线曲线以上",闭运算去洞 + 最大连通域 + 轻羽化 +合成 RGBA:RGB=marked,alpha=255×(1−mask)(透明=重绘区,对齐 ComfyUI mask=1−alpha)。 +""" +from __future__ import annotations + +import cv2 +import numpy as np +from PIL import Image + +from .render import load_ext_mesh, load_texture_rgba, render_hairline_overlay, build_overlay_layer + +# headmark 额头边界关键点(MediaPipe canonical 索引,左→右沿上额) +FOREHEAD_LANDMARKS = [21, 68, 104, 69, 108, 151, 337, 299, 333, 298, 251] +# SegFormer 头部类(含 skin..hat;排除 bg=0 / ear_r=15 / neck_l=16 / neck=17 / cloth=18) +_HEAD_CLASSES = list(range(1, 15)) + + +def forehead_upper_region(landmarks_mp: np.ndarray, w: int, h: int) -> np.ndarray: + """headmark step1:额头边界关键点以上的"上部区域"填充 mask(uint8 0/255)。""" + pts = [(int(landmarks_mp[i, 0] * w), int(landmarks_mp[i, 1] * h)) for i in FOREHEAD_LANDMARKS] + left_ext = (0, pts[0][1]) + right_ext = (w - 1, pts[-1][1]) + polygon = np.array([left_ext] + pts + [right_ext, (w - 1, 0), (0, 0)], dtype=np.int32) + m = np.zeros((h, w), np.uint8) + cv2.fillPoly(m, [polygon], 255) + return m + + +def head_silhouette(parse_map: np.ndarray) -> np.ndarray: + """headmark step2:SegFormer 头部轮廓 mask(uint8 0/255)。""" + return (np.isin(parse_map, _HEAD_CLASSES).astype(np.uint8) * 255) + + +def _curve_bottom_per_column(curve_mask: np.ndarray): + """每列发际线曲线的**最低**像素 y(线下沿),返回 (xs, ys) 仅含有曲线的列。""" + ys_idx, xs_idx = np.where(curve_mask > 0) + if xs_idx.size == 0: + return None, None + w = curve_mask.shape[1] + bottom = np.full(w, -1, np.int32) + np.maximum.at(bottom, xs_idx, ys_idx) + cols = np.where(bottom >= 0)[0] + return cols, bottom[cols] + + +def _above_curve_region(curve_mask: np.ndarray, h: int, w: int) -> np.ndarray: + """由发际线曲线得到"曲线以上"区域(uint8 0/255)。 + + 曲线 x 跨度内逐列插值出下沿 y_line(x),两侧按端点 y 水平延伸; + above = 所有 y ≤ y_line(x)。曲线缺失(极端)则返回全 1(交给 ROI 兜底)。 + """ + cols, ybot = _curve_bottom_per_column(curve_mask) + if cols is None: + return np.full((h, w), 255, np.uint8) + x0, x1 = int(cols.min()), int(cols.max()) + # 全列插值 y_line:[x0,x1] 内线性插值,两侧水平延伸 + yline = np.interp(np.arange(w), cols, ybot, + left=float(ybot[0]), right=float(ybot[-1])).astype(np.int32) + yy = np.arange(h)[:, None] # (h,1) + above = (yy <= yline[None, :]).astype(np.uint8) * 255 # (h,w) + return above + + +def _clean_mask(mask: np.ndarray, w: int) -> np.ndarray: + """闭运算去洞 + 取最大连通域填充 + 轻羽化。""" + k = max(3, (int(w * 0.015) | 1)) + kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (k, k)) + closed = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) + cnts, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + out = np.zeros_like(mask) + if cnts: + largest = max(cnts, key=cv2.contourArea) + cv2.drawContours(out, [largest], -1, 255, -1) + # 轻羽化(柔化边缘,利于扩散衔接) + out = cv2.GaussianBlur(out, (0, 0), sigmaX=max(1.0, w * 0.004)) + return out + + +def build_inpaint_mask(photo_bgr: np.ndarray, landmarks_mp: np.ndarray, + parse_map: np.ndarray, points502: np.ndarray, + black_texture_rgba: np.ndarray): + """返回 (marked_bgr 划线图, mask uint8 0..255 重绘区)。""" + h, w = photo_bgr.shape[:2] + uv, ext_faces = load_ext_mesh() + + # ④ 渲染黑线:marked = 烧进照片;curve_mask = 曲线像素 + marked = render_hairline_overlay(photo_bgr, points502, ext_faces, uv, black_texture_rgba) + overlay = build_overlay_layer(h, w, points502, ext_faces, uv, black_texture_rgba) + curve_mask = (overlay[:, :, 3] > 40).astype(np.uint8) * 255 + + # ①②③ ROI + upper = forehead_upper_region(landmarks_mp, w, h) + head = head_silhouette(parse_map) + roi = cv2.bitwise_and(upper, head) + + # ⑤ ROI ∩ 曲线以上 → 清理 + above = _above_curve_region(curve_mask, h, w) + mask = cv2.bitwise_and(roi, above) + mask = _clean_mask(mask, w) + return marked, mask + + +def compose_comfy_rgba(marked_bgr: np.ndarray, mask: np.ndarray) -> Image.Image: + """合成 ComfyUI LoadImage 用的 RGBA:RGB=划线图,alpha=255−mask(透明=重绘区)。""" + rgb = cv2.cvtColor(marked_bgr, cv2.COLOR_BGR2RGB) + alpha = (255 - mask).astype(np.uint8) + rgba = np.dstack([rgb, alpha]) + return Image.fromarray(rgba, mode="RGBA") + + +if __name__ == "__main__": + import sys, os + from .service import get_landmarker, get_parser + + path = sys.argv[1] if len(sys.argv) > 1 else "tests/fixtures/frontal.jpg" + tex_name = sys.argv[2] if len(sys.argv) > 2 else "girl_straight" + img = cv2.imread(path) + h, w = img.shape[:2] + rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + + from .hairline_2d import sample_hairline, smooth_hairline + from .lift_3d import lift_hairline_to_3d, build_middle_row, assemble_full + lm = get_landmarker().detect(rgb) + parse_map = get_parser().parse(rgb) + h2d, valid = sample_hairline(lm, parse_map); h2d = smooth_hairline(h2d, valid) + h3d = lift_hairline_to_3d(lm, h2d); mid = build_middle_row(lm, h3d) + pts = assemble_full(lm, mid, h3d) + + black = load_texture_rgba(f"hairline_texture_black/{tex_name}.png") + marked, mask = build_inpaint_mask(img, lm, parse_map, pts, black) + os.makedirs("tests/output", exist_ok=True) + cv2.imwrite("tests/output/mask_marked.png", marked) + cv2.imwrite("tests/output/mask_binary.png", mask) + # 三联可视化:划线图 / ROI / mask 叠加 + upper = forehead_upper_region(lm, w, h); head = head_silhouette(parse_map) + roi = cv2.bitwise_and(upper, head) + vis = marked.copy() + vis[roi > 0] = (vis[roi > 0] * 0.6 + np.array([0, 40, 0])).clip(0, 255).astype(np.uint8) + vis[mask > 128] = (vis[mask > 128] * 0.4 + np.array([0, 0, 150])).clip(0, 255).astype(np.uint8) + cv2.imwrite("tests/output/mask_vis.png", vis) + compose_comfy_rgba(marked, mask).save("tests/output/comfy_input.png") + print(f"saved mask_marked/mask_binary/mask_vis/comfy_input;mask 像素 {int((mask>128).sum())}") diff --git a/hairline/render.py b/hairline/render.py index 5ac8d7a..2939029 100644 --- a/hairline/render.py +++ b/hairline/render.py @@ -78,19 +78,30 @@ def render_hairline_overlay(photo_bgr: np.ndarray, points502_norm: (502, 3) 归一化坐标(x,y ∈ [0,1]),**MP 顺序**(extract_hairline 输出)。 """ H, W = photo_bgr.shape[:2] - TH, TW = texture_rgba.shape[:2] - pts_obj = mp_order_to_obj_order(points502_norm) # MP序 → OBJ序 - img_xy = pts_obj[:, :2] * np.array([W, H], dtype=np.float32) # (502,2) + overlay = build_overlay_layer(H, W, points502_norm, ext_faces, uv502, texture_rgba) + # alpha 合成(RGBA→BGR:贴图 RGB 顺序需反成 BGR) + a = overlay[:, :, 3:4] / 255.0 + rgb = overlay[:, :, :3][..., ::-1] # RGB→BGR + out = photo_bgr.astype(np.float32) * (1.0 - a) + rgb * a + return np.clip(out, 0, 255).astype(np.uint8) - overlay = np.zeros((H, W, 4), np.float32) # 累积曲线层 RGBA + +def build_overlay_layer(H, W, points502_norm, ext_faces, uv502, texture_rgba) -> np.ndarray: + """渲染发际线曲线层,返回 (H, W, 4) float32 RGBA(未合成到照片)。 + + 供渲染合成(render_hairline_overlay)与遮罩(mask.py 取 alpha=曲线像素)共用。 + """ + TH, TW = texture_rgba.shape[:2] + pts_obj = mp_order_to_obj_order(points502_norm) + img_xy = pts_obj[:, :2] * np.array([W, H], dtype=np.float32) + overlay = np.zeros((H, W, 4), np.float32) tex = texture_rgba.astype(np.float32) for (i, j, k) in ext_faces: dst = img_xy[[i, j, k]].astype(np.float32) # UV → 贴图像素;flipY:贴图 y = (1 - v_raw) * TH(与 head3d Three.js flipY=true 一致) src = np.array([[uv502[v][0] * TW, (1.0 - uv502[v][1]) * TH] for v in (i, j, k)], dtype=np.float32) - # 退化三角形(投影到一条线)跳过,避免 getAffineTransform 奇异 - if cv2.contourArea(dst.astype(np.int32)) < 1.0: + if cv2.contourArea(dst.astype(np.int32)) < 1.0: # 退化三角形跳过 continue M = cv2.getAffineTransform(src, dst) warped = cv2.warpAffine(tex, M, (W, H), flags=cv2.INTER_LINEAR, @@ -99,9 +110,4 @@ def render_hairline_overlay(photo_bgr: np.ndarray, cv2.fillConvexPoly(tri_mask, dst.astype(np.int32), 255) sel = tri_mask > 0 overlay[sel] = warped[sel] - - # alpha 合成(RGBA→BGR:贴图 RGB 顺序需反成 BGR) - a = overlay[:, :, 3:4] / 255.0 - rgb = overlay[:, :, :3][..., ::-1] # RGB→BGR - out = photo_bgr.astype(np.float32) * (1.0 - a) + rgb * a - return np.clip(out, 0, 255).astype(np.uint8) + return overlay diff --git a/hairline/service.py b/hairline/service.py index b66c6a5..1130749 100644 --- a/hairline/service.py +++ b/hairline/service.py @@ -11,13 +11,22 @@ import cv2 import numpy as np from . import constants as C +from . import comfyui from .face_landmarks import FaceLandmarker from .face_parsing import FaceParser from .hairline_2d import sample_hairline, smooth_hairline from .lift_3d import lift_hairline_to_3d, build_middle_row, assemble_full from .render import load_ext_mesh, load_texture_rgba, render_hairline_overlay +from .mask import build_inpaint_mask, compose_comfy_rgba -_TEXTURE_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "hairline_texture") +import io +import logging + +logger = logging.getLogger("hair.worker") + +_REPO = os.path.dirname(os.path.dirname(__file__)) +_TEXTURE_DIR = os.path.join(_REPO, "hairline_texture") +_BLACK_TEXTURE_DIR = os.path.join(_REPO, "hairline_texture_black") # ⚠️ 本 worker 是 RTX 5090(sm_120),torch 2.2.2(cu121) 只编到 sm_90,CUDA 跑算子会报 # "no kernel image"。SegFormer 默认走 CPU(~2.5s/张)。换 torch cu128 后可设 SEG_DEVICE=cuda。 @@ -73,40 +82,84 @@ def get_texture_map() -> dict: def extract_502(image_bgr: np.ndarray): """照片(BGR) → (points502 MP序, valid17)。无人脸返回 (None, None)。""" + ctx = extract_context(image_bgr) + if ctx is None: + return None, None + return ctx["points"], ctx["valid"] + + +def extract_context(image_bgr: np.ndarray): + """照片(BGR) → {landmarks, parse_map, points, valid}。无人脸返回 None。""" rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) landmarks = get_landmarker().detect(rgb) if landmarks is None: - return None, None + return None parse_map = get_parser().parse(rgb) hairline_2d, valid = sample_hairline(landmarks, parse_map) hairline_2d = smooth_hairline(hairline_2d, valid) hairline_3d = lift_hairline_to_3d(landmarks, hairline_2d) middle_3d = build_middle_row(landmarks, hairline_3d) points = assemble_full(landmarks, middle_3d, hairline_3d) - return points, valid + return {"landmarks": landmarks, "parse_map": parse_map, "points": points, "valid": valid} + + +def _black_texture_path(white_path: str) -> str: + """白贴图路径 → 同名黑贴图路径(hairline_texture_black/)。""" + return os.path.join(_BLACK_TEXTURE_DIR, os.path.basename(white_path)) def generate_previews(image_bgr: np.ndarray, gender: str): - """生成该性别全部发际线预览图。 + """生成该性别全部发际线预览图(仅预览,不生发)。 - Returns: list[dict],每项 {"hairline_type": key, "image_bgr": ndarray, "order": 1..N}。 - 无人脸返回 None。gender 必须是 male/female。 + Returns: list[dict] {"hairline_type", "image_bgr", "order"};无人脸返回 None。 """ if gender not in ("male", "female"): raise ValueError(f"gender 必须是 male/female,收到 {gender!r}") - points, _valid = extract_502(image_bgr) - if points is None: + ctx = extract_context(image_bgr) + if ctx is None: return None - uv, ext_faces = load_ext_mesh() results = [] for order, (key, path) in enumerate(get_texture_map()[gender], start=1): - tex = load_texture_rgba(path) - preview = render_hairline_overlay(image_bgr, points, ext_faces, uv, tex) + preview = render_hairline_overlay(image_bgr, ctx["points"], ext_faces, uv, + load_texture_rgba(path)) results.append({"hairline_type": key, "image_bgr": preview, "order": order}) return results +def generate_grow_results(image_bgr: np.ndarray, gender: str): + """该性别全部发际线:预览图(白线) + 生发图(ComfyUI)。同步、串行。 + + Returns: list[dict] {"hairline_type","order","image_bgr"(预览), "grown_png"(bytes 或 None)}。 + 无人脸返回 None。某张 ComfyUI 失败时该项 grown_png=None,不影响其余。 + """ + if gender not in ("male", "female"): + raise ValueError(f"gender 必须是 male/female,收到 {gender!r}") + ctx = extract_context(image_bgr) + if ctx is None: + return None + uv, ext_faces = load_ext_mesh() + results = [] + for order, (key, white_path) in enumerate(get_texture_map()[gender], start=1): + white = load_texture_rgba(white_path) + preview = render_hairline_overlay(image_bgr, ctx["points"], ext_faces, uv, white) + + grown_png = None + try: + black = load_texture_rgba(_black_texture_path(white_path)) + marked, mask = build_inpaint_mask( + image_bgr, ctx["landmarks"], ctx["parse_map"], ctx["points"], black) + buf = io.BytesIO() + compose_comfy_rgba(marked, mask).save(buf, format="PNG") + grown_png = comfyui.run(buf.getvalue()) + except Exception as e: # noqa: BLE001 单张失败不拖垮整请求 + logger.warning("接口2 生发图失败 type=%s:%s", key, e) + + results.append({"hairline_type": key, "order": order, + "image_bgr": preview, "grown_png": grown_png}) + return results + + if __name__ == "__main__": import sys g = sys.argv[2] if len(sys.argv) > 2 else "female" diff --git a/hairline_texture/man_ inverse_arc.png b/hairline_texture/man_ inverse_arc.png index 70ce13a72c48cccacdcb3c456dc875135b153afc..1212535bbe8759823c0517423194d351476bf7c4 100644 GIT binary patch delta 3988 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 literal 0 HcmV?d00001 diff --git a/hairline_texture_black/girl_flower.png b/hairline_texture_black/girl_flower.png new file mode 100644 index 0000000000000000000000000000000000000000..debae8f2a72367d1de6d03725c344c871ff206e6 GIT binary patch literal 4342 zcmeHL`BxIy7k?lo>Nwe|k%l#s+F07R?`TezSZ3PS)Et}01#zh~4JE=+b`)5Hw$t_bY2_gLKeh4sn&8D;f z7IDz)AOMeZ43|T7wfI}2+bN=7>{(($XxtfaF$NH6S%TBkShxkq*5^crn)gVLBfSE6$ZQ!HI_x9#{% zGja31HKEyKk5?S!)Xuckzet0Cix~s}|8mWV|9fdLZ9Z$YM|l9?Vxiso7ZOSWWJ1Q1 z@;}i)uM6Nz5$SK4U7`VvT3k}Tyw{m$48<+iBT$TPKZx0jjDAuuMR%y$Yg!YKMR~}c zDVdZ@>k%Smmu3Kq;z^@(e6g+;Ve0(IT@_*Cj&GU}t>`#tgntBZj*%s(+YS2a zQ2P%0$gjjF`uHM7aN$d#MxG&{j3z%^DBm&}UA*|3j5s|L$qqzwt^%sL;PMB{9I~a_ z;ha2v(2H7XbhRRiZJc<&&nrce&_ZV+r&ye3=2p^1`^!J0DJjb<0hAk683$5(!!e?A zea8(DuugFJ$B4M(15*{ASl4g&2vi- z^5-pr8uxTcOvYKV9cX1=;7hyQRgwz<1BqMcj8Dwp1~+_M3u@g;O>N1eqO-mWmK~}R z3astslKdwY`@O36{utIUl)<<4VZgL&hlA{s|rB#&iJ;vh7j&{yeIJ-5Hn9o+3NN)n1O%7TalTq z>1I$cASl)F|D2=QQqni~_0N1a?AqmunfOIsXs>?V?_eD)W=Hcg097lOqWH}lrLr6` z&RH@Sk6|&Yk(u3fc2uDi__b)aB9<7>kK#1!T$rt*Pjw%H;l(iUR-l+Ec%f-M9 ztZ1jvov(wKJYeyT-=IcbKaIHt;^1I(=gZCe$689Vse}}07@@BUr;|Oda5eIoUaiYw z(nrkXMH7uEtYCTg6>s_H2)ig{))(5bm1!n@ zE3AVyMheK9jpje+mG__qJeR!_$tAZBh*Tw-h5fVd@dnhr;%K(8JNRi`BpTnuxLP$O z3lhkiBCH z3sMx5ow`M_@(L}jdrzaY%zq+dJ3rOT8t#!cB)`zmrj0yK>r(xzX9{IW_3XavM&WOh zXD2lOIq&S({xH;EnvqvgMzV4$u4=ce3GU1pFK|IXICmYcWq)v-=-Dm{k3A2~rqj1p z)&%z?xEVk=_Zb&szYt=~89sqV#t=AY{2V2Zf3wTvsq}M{(moYCxDUcX67AJpZwiff zNjs(DC}jZ^-h_ivMN?5pUlMrxn-27>g@=?2kTsv!Y_P{}E*)Xe}PuY==`aXb5f~EKkYIB-=$Iek4 zWSq%Dn(|i@H^W?-+;EgUJ3|lF!+F0;&IT!JEhRfNBpY27>D<30b!m!$TJY~QvTIFr zcSpJ_!;4@Em|U?XNXI!TDb`exI7(Fp!>#YohH>vSnvk)8!gbJ%O@6&}yXhp3oXh0TN?AN6+?p^j45wADx)f zj&Fq)M|>`aFtrWN_9lS8xlW!A-*2nbF$?qWTQiqLaX|-dgWx*_iWXW`YCdXiqa+_9 ziKKYL_ZRqV!XO~{OM*3&(j^u-hWBx`nIC!#ZMYs<AL-9$_;ipo2O;CX0Wlf=ZBj-#YcSdechtyaB+Y@wcy|7DV;XBR_ClT#a3AcG; z4DE}6PZQXxG4X@hGGS(o9aY@lKO=YAi`Zub!H0IxrU+^WDTHB$bq0zk6DWEDy3014-R z5}NG=+VUU25appb7klcghKpewr$`^}VH?#FwFA>PqXz6ge5Rbe;$Km_;V1DP&oMRr6NR$Vu(D1UTyEeVB15B>ivb C;k$qU literal 0 HcmV?d00001 diff --git a/hairline_texture_black/girl_heart.png b/hairline_texture_black/girl_heart.png new file mode 100644 index 0000000000000000000000000000000000000000..cd481269a01684e40f73a8f5d592efbc93b3f52f GIT binary patch literal 3980 zcmeHK_g52G7JexZT8P-t5e>NZAQ8p@qmY1r*eHS%7^I4d!a{D`cqyF&DE${ozdGFkJ-}&wxcUNbm&JQ{O z0OZLNjvfF&)h-lhX{wDVZa}Iw+T;_zL;;|?VQoP`e&H5%5)$R%d<;}|8+}#>@DO_! zdjM*)b>%@C>R3P8$vfJUbUB(5bnOy2el;XKsHpZ~FaRjTNk{v0age$3%owA=t(*^r zt~!rX#`MXEBK>2Ce6!L+^C<5MxTiVF&SkIE8^kPtO#7I zl5)Kw*Er6U;SZ&iTe%)(`L(#>j57^ok;~qym9i{SMlxO5vNtHeO>A*=abd)*e6?x& zf(T_E@%H=#Z{-}vyr_AqtI)IC!xUjp#W}EX2Eg|Tz2s4>KQo6g$lX(W`p-~he3nos zOX^lE&l>aw^U8=Q@8DY*b^9FVNbx#~+HR^przhM-%D?U4!Y`XH7BDt0pCdQHfmhI% zW;y3b*Y=s(7YW+QjrA(MHb%7hyjPs<6K3|*-c37%36_0GvA~&7s>Pkk%+J=yo$5!c zV1^|q$4Of>x+ps!Od0dCXG!?6XNEfDKHS8o-otxm3cmE;$xcc_hcdCehuw24TKvTA zN?aUQa7VZsF!pg%l;>bc`H}Y~nvdM*^@+JrvC6hS_mDmv;I$xJj2dMf4~g;1K6ULp z9E4cudn=L|3+9z-CONXvfN6FSaDXV|s zm9%}B#%iTc(+3cRE&@exmr5pz8oL{=ha<3TPqc$vS3veitYMZb5Q&u4exIaIxYj0XFNSc8f%P5w*MdC?Z)^(Qw~ju2+FTKqP%LS4h)28MTMvx7m=u{p zMGY{E9jn6k_q~pt+>-a;*B|uvg1j;JpQ>$LA0RVh!)C(T7VOtm9R~?HJ7?5sz4iV;Ek3{RkdDkt>l` zZUFS#>a5D){TQOLWbmtrv67SvwE9aISk+x5X|bB#STz`H(o5QWxxLZ=*f+#iRI8K} zja{D!x1KtyEKPdvo;Lx|7b6iA7BVR(oH%+AiNC*&p#kN+36C0{r0l5sT$^{Jj(J`^a`NkOutl)*kHgY+fjr^E-o~}qAC?IAe1Sjc42pg zq^%f=f-Uk1FG8-AnQHr5K*&#S`vmY3D(K)D(o<{ z{wwwImVN(XPZQ4o$wZ*;u*(glcYCf;Z#te3`32Q)(fb!ll zu>}g+8HKr>0T~bFow2l?me6l3!5T?KOU*dGd~prL%!4RH2Pt~9!=7#@TpWPW+48{@s!E_qr433OHT}hThf_BI=e|${ zZ&HUABYmWNCY<{u37q{OpRIC8$>IA7pzifO1D!k+Sx+Kf z4HL>P1V96KuAw<1I-Z)TC{Z_RUdJ<%f6yW#?|6C70%FB`)&ZQEVXk2Y7QO~xAuleV zp@A19mlhG$CHNUhSVfuYwHECkGh9d|u)ba0Ij`EUrB)T9&s-?@xaNJM`r;dxSKqJy tEsN{#)&E}o|L^Ayut68=fV97;?{9u;N{+I(P!G%Cq?4;-)v>_j{{llc)*S!< literal 0 HcmV?d00001 diff --git a/hairline_texture_black/girl_straight.png b/hairline_texture_black/girl_straight.png new file mode 100644 index 0000000000000000000000000000000000000000..023d3663851139fdc89848c3df06ca42e98c3710 GIT binary patch literal 3848 zcmeHK`B&4&7N75zV8RX}oBL7(tq5)rOOt?=CsstN)`$`az%cn`cE9)~}HcinyhZ@t|QU**NxzxI-s&uf!*e`*T+Bk!x#N9|qb zT%L_7OW1)|oSGNXN%i%h<^F>V7`(#)@N8n727!2tA(MpwSo;Csy%GX|_5TIT9Y(to z9}35A(&YNyrXX#qj^NJFzaAq!8SYVE5qv$SQ=XsVv_x*2NJG|WKE$UTX@=W`Jqat9 zhh!)^M8W}4+Kk8=o7FRA7jU?*r;8&=T2B4HeHl*XOpeaq z6#+<2<59~G3RQ8qp#ate5lMOQ$_2VJ$rorsyw0?f2JC^at?eMWIC?2h_ z0Z$yuK8mGtI$6^72c<;*V~xZ&QAq-3SE1Ou*i6Jkj{UALI!Mx(#xV}<&1{#;<^UvCC98@fGC zy%K)j5Iw`25zBtOm|zMc&qrc*+>tDp<5=?U4Lz>0P`3tMGg^0dYrEs5D)H{~P486S z5d_E^DAyOzH?;;GS~3}#)W=Gw4RmT7Pl!5y6M&GH^hqcwwFoMkX>g~ANE6@n4xH~s#S!$O|kigdP8bPR`5cj^*${+ z=2*-QX^JuLPGu{bdJ*zzufqVBE<$VySAR=lc|n3mT#^ZFRIIH zkKS>w*iISOO7YBq2$}P;M4KLzgx{JbhP!a3yDj~WbN2h#^vup z6*sI%A5LQ;GtK#CGv*rla*Eqj14Z36I01q%pi-pL@YkhZzkq*V_re)ob;%caM3=Id zA)V^5=thyE?hiFF#j>9Wp;-}J-Q#!s5=ATbdQYr2ncTk~LLD@s^i`KxD>%aPIx7~> z854UMNcD}PbZMM$krs~i2^A}}iiHwN{9cTAJ%yw6GDE~Y!sIQmfRupJwmk!JL4@3s4RNM=i5rf?J_NkF&kVgwB;6_<6Pei(b*x#MyBq6JG{g*m|W~` zQXAg!QgFxwS#pZZQ5g+ybC<)M%b?yyTs}d5hu*IZ66~viS{5B#17f|&#VJkdxa9*T zB}d7{dA>Btm89KWs{ki>N#Aw_-ChF?cjseH5G%}y_q~B zNjYAK`Mr(Q{;|>!oZ2NNxg+vFd!~mFo?wRw zk}2M*_=40#?L#T?8)51nqFm}YHui@sr22hvTYhN^A1oe5$RwZDDyv{B}$d?>s$&xW`R^01-gRQ`O!0j0^K zXPqFT_^+)P6qC4ZEQ zo7x-6xkC0k5M>eiztyI7sAp8e!m@kJuJqA#U1&;AlMB_Lu@lb}f~Jy@AKQ!jG(eIan!tiDaknginbqr%`mukcTT%$ zj68Evj@wJEVd)c5D#8JtbsT<^TNTBAB#UMO0H?Rfd{+B~;ztI>|D}H1R{*TM1OVb~ zc(}j@n|WaVJm#;%d^OBBgZWmjDyXV;~@A4~X#k1E*X8?cMBlX~uN D>5iFe literal 0 HcmV?d00001 diff --git a/hairline_texture_black/girl_wave.png b/hairline_texture_black/girl_wave.png new file mode 100644 index 0000000000000000000000000000000000000000..4c2762fa041bfd4ac6dadf8d2729e627ddcfef2d GIT binary patch literal 4179 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 B{Br;R literal 0 HcmV?d00001 diff --git a/hairline_texture_black/man_straight.png b/hairline_texture_black/man_straight.png new file mode 100644 index 0000000000000000000000000000000000000000..632174e0509dd342cc3336e418431a4db266b725 GIT binary patch literal 3511 zcmeHK{Z|ub7QQnON%#;1kx#+(EK3IRo%`J9x$iS` z-aGfqr30}MHkPiI0ALdp8GZ-=C2mn5uOgOr`7iavLSaW9;Q&}UEL{kcS2z~2_i5*xi@Anz$2lRA%#T5tMm3H=Edin$UBpgn+E&SPiCduesnXH*kfB%_}+wk zL^bsD^?^Q@(w}-fO!Yx_f!6~4gDwXCz_UBU;^BFf|0sFo_<;n=C6)MGOF;f?{qKu^ zOO_yhn3rzPNu_I5)sTxY9JTd>H&x>PwO|(k5PI`RISIaB17H_PL>mx*VspZn{r?Rh z+boiwS)h(zRW@13W@JuWmpgT%oES|yt$jSRT-Lo)VX_JZb)|%7F)^yDpHtv`QI%eU zcw`T~dho{EU?{UF)XpymTc@qose5@JYmdEkUyavl2b>mag|<@|cUSn{q8WMFgK=Tu z%zWml*JOeF5U;v!G|f0$X&ane#@h8UZK}2K%)h)ohy3ruI}ZtHC;ggsh%L7ow`craNq)HA`}5K>`dZJ}B`wK#wFc(S_rvd#C|PkKg7!%p6X zGcR9kkE^L2TEuVZ?JWj3A?&-Q7mOqIqY{sz=QESS=%Nmb^U(tK9_hJ(QSZ~Gs(PL5 zewAWwV0NUR^!VX-Jq_zoyq$Qk4kJ(ewAiTAADZSDJbl99nW)1l&CR;W-9~1UvvZyH zPx35{$;9rZ56sr_iWsRK9mXERfiaTVwUoqz=UdiKs^)um2Ko!_j{90#PN!_FoFeeu znV^r`p^DM<7ipA;A+02(?Zsb|3c>@`-QX!5rVV~MX@d@yOL$c%-Ap2c2Gn-Fq3pe< zC0@tf;b%TI;}-$Jk0i04hO&)?hoL8eQN$Nw!Zt6m!24zI#Lg0hKfc?o`<V5p9 z?TGr5VfwonmAyGaN@paQju_*qeG;Oi(KQsuM%!t4SmXMNZ zvGMD|Bye|It*tTs)7G+_uwU8t+VmBQ%YHsM-WQ@tE%@tMKNJs}7AJk1ggpPoN4Ux_ z3Yd|ZyPv(LVNYZE`$HMQH=#iw3GPGLS5q2W=dM4Ft@MZkCZ-PKj?u7!zTS{axm&SO zvcT%p&=0u`xO_Upk(Cr&3W=}8%vLnNnp?1um5s7eYTOcs*Fk*6QBs3%w->DI(+`@O zP?nE(g2o=K-gLgDVNAN>1h$(ZAlZ1%o-=)_KZdX+I>l)5*f;N`iJfAqE|UO?b}AR? z6+c>H+m6aj#-}tGJ9gtfqWrHtR>Cw{h>|DM1&66OqQUCO4yE1*`1#iDN_yLIl;u+E z%4u~1;hco3<5Rzc#Qk#7IR*sx^;~IWEEBf6aZtc#^{V;2oa_e5L*Gqn;Muq4ueVSo#