- 从head3d复制发际线检测管线到 hairline/ 包:MediaPipe Tasks + SegFormer分割 + 17锚点射线检测 + 502点mesh(face_ext.obj)+UV - 复制模型:face_landmarker.task(3.7MB)、SegFormer config/preprocessor (model.safetensors 340MB 单独下载中) - 新增 docs/接口2-C端生发-技术实现方案.md:第一步=发际线曲线叠加预览图, 新增gender必填参数,按性别贴图数量输出(female5/male4),hairline_type英文key, 服务端cv2逐三角形warp渲染器(head3d只有浏览器端Three.js渲染) - 接口文档.md 接口2章节同步:gender参数、输出语义、错误码说明 - hairline_texture/ 9张发际线贴图入库
116 lines
4.1 KiB
Python
116 lines
4.1 KiB
Python
"""Main CLI: image -> JSON of 502 3D points consumable by the SDK.
|
|
|
|
Pipeline:
|
|
1. MediaPipe FaceMesh -> 468 normalized landmarks
|
|
2. Face parsing (HF SegFormer) -> per-pixel class map
|
|
3. Hairline curve detection + per-anchor ray casting -> 17 hairline 2D points
|
|
4. Lift 2D hairline points to 3D using anchor Z + curvature offset
|
|
5. Interpolate 17 middle-row 3D points
|
|
6. Concatenate into (502, 3) and emit JSON
|
|
|
|
Output JSON schema:
|
|
{
|
|
"image": {"width": W, "height": H, "path": "..."},
|
|
"n_total": 502,
|
|
"n_mp": 468,
|
|
"n_extension": 34,
|
|
"layout": ["mp[0..468)", "middle[468..485)", "hairline[485..502)"],
|
|
"points": [[x_norm, y_norm, z_relative], ...] # length 502
|
|
"valid_hairline": [true/false, ...] # length 17
|
|
}
|
|
|
|
Usage:
|
|
py -3 python/extract_hairline.py path/to/image.jpg
|
|
py -3 python/extract_hairline.py path/to/image.jpg --out data/out.json
|
|
"""
|
|
from __future__ import annotations
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
import numpy as np
|
|
|
|
if __package__ is None or __package__ == "":
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
from python import constants as C
|
|
from python.face_landmarks import FaceLandmarker
|
|
from python.face_parsing import FaceParser
|
|
from python.hairline_2d import sample_hairline, smooth_hairline
|
|
from python.lift_3d import lift_hairline_to_3d, build_middle_row, assemble_full
|
|
else:
|
|
from . import constants as C
|
|
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
|
|
|
|
|
|
def run(image_path: str, out_path: str | None = None, device: str | None = None) -> dict:
|
|
import cv2
|
|
bgr = cv2.imread(image_path)
|
|
if bgr is None:
|
|
raise FileNotFoundError(f"could not read image: {image_path}")
|
|
rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)
|
|
H, W = rgb.shape[:2]
|
|
|
|
t0 = time.perf_counter()
|
|
lmk = FaceLandmarker(static_image_mode=True)
|
|
landmarks = lmk.detect(rgb)
|
|
lmk.close()
|
|
if landmarks is None:
|
|
raise RuntimeError("no face detected")
|
|
t1 = time.perf_counter()
|
|
print(f" [time] mediapipe: {(t1 - t0)*1000:.0f} ms")
|
|
|
|
parser = FaceParser(device=device)
|
|
parse_map = parser.parse(rgb)
|
|
t2 = time.perf_counter()
|
|
print(f" [time] parsing: {(t2 - t1)*1000:.0f} ms")
|
|
|
|
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_full = assemble_full(landmarks, middle_3d, hairline_3d)
|
|
t3 = time.perf_counter()
|
|
print(f" [time] hairline: {(t3 - t2)*1000:.0f} ms")
|
|
|
|
record = {
|
|
"image": {"width": int(W), "height": int(H), "path": image_path},
|
|
"n_total": C.N_TOTAL,
|
|
"n_mp": C.N_MP,
|
|
"n_extension": C.N_EXT,
|
|
"layout": [
|
|
f"mp[0..{C.N_MP})",
|
|
f"middle[{C.MIDDLE_START}..{C.HAIRLINE_START})",
|
|
f"hairline[{C.HAIRLINE_START}..{C.N_TOTAL})",
|
|
],
|
|
"points": points_full.tolist(),
|
|
"valid_hairline": valid.tolist(),
|
|
}
|
|
|
|
if out_path is None:
|
|
base = os.path.splitext(os.path.basename(image_path))[0]
|
|
out_path = os.path.join("data", base + ".json")
|
|
|
|
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
|
|
with open(out_path, "w", encoding="utf-8") as f:
|
|
json.dump(record, f, ensure_ascii=False, indent=2)
|
|
print(f" wrote {out_path}")
|
|
return record
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser(description="Extract MediaPipe + hairline points from one image.")
|
|
ap.add_argument("image", help="path to input image (jpg/png)")
|
|
ap.add_argument("--out", default=None, help="output JSON path")
|
|
ap.add_argument("--device", default=None, help="torch device (cpu/cuda); auto if omitted")
|
|
args = ap.parse_args()
|
|
run(args.image, args.out, args.device)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|