76 lines
2.8 KiB
Python
76 lines
2.8 KiB
Python
"""
|
||
解析 face_picture_3dmax.obj,得到顶点 UV 和三角形索引。
|
||
|
||
OBJ 约定:v 顶点 / vt UV / vn 法线,f 三元组 v/vt/vn 索引(1-based)。
|
||
本服务只需要每个顶点的 UV 和三角形顶点索引——3D 顶点位置在 SDK 里也只是
|
||
占位(运行时被 MediaPipe landmark 覆盖),这里同样无需读 v。
|
||
|
||
★ 与 vulkan/FaceApp.cpp::LoadOBJ 一致,UV 的 V 分量取 (1 - v),把 OBJ 的
|
||
bottom-up 约定翻成 top-down,与图像(OpenCV/Pillow)一致。
|
||
"""
|
||
|
||
from dataclasses import dataclass
|
||
from pathlib import Path
|
||
|
||
import numpy as np
|
||
|
||
|
||
@dataclass
|
||
class FaceMesh:
|
||
# (468, 2) float32, 每个顶点的 UV,V 已翻转,∈ [0,1]²
|
||
uvs: np.ndarray
|
||
# (852, 3) int32, 每个三角形的 3 个顶点索引(0-based,指向 uvs 第一维)
|
||
triangles: np.ndarray
|
||
|
||
|
||
def load_face_mesh(obj_path: str | Path) -> FaceMesh:
|
||
obj_path = Path(obj_path)
|
||
text = obj_path.read_text(encoding="utf-8", errors="ignore")
|
||
|
||
pos_count = 0
|
||
uv_list: list[tuple[float, float]] = []
|
||
# OBJ 的 face 用 v/vt/vn,三个索引可能不同;但本工程的 obj 三者一一对齐
|
||
# (pos_index == uv_index == normal_index),所以我们只看 v 索引来索引 UV
|
||
# 列表也成立。这里仍然显式校验以防换 obj 时出错。
|
||
triangles: list[tuple[int, int, int]] = []
|
||
|
||
for raw in text.splitlines():
|
||
line = raw.strip()
|
||
if not line or line.startswith("#"):
|
||
continue
|
||
parts = line.split()
|
||
tag = parts[0]
|
||
|
||
if tag == "v":
|
||
pos_count += 1
|
||
elif tag == "vt":
|
||
u = float(parts[1])
|
||
v = float(parts[2])
|
||
uv_list.append((u, 1.0 - v)) # V 翻转
|
||
elif tag == "f":
|
||
if len(parts) != 4:
|
||
raise ValueError(f"only triangle faces supported, got: {line}")
|
||
tri: list[int] = []
|
||
for token in parts[1:]:
|
||
# 形如 "v/vt/vn" 或 "v//vn" 或 "v"
|
||
seg = token.split("/")
|
||
v_idx = int(seg[0]) - 1 # OBJ 1-based → 0-based
|
||
vt_idx = int(seg[1]) - 1 if len(seg) > 1 and seg[1] else v_idx
|
||
if v_idx != vt_idx:
|
||
raise ValueError(
|
||
f"this loader assumes pos_idx == uv_idx, "
|
||
f"got v={v_idx + 1} vt={vt_idx + 1} in line: {line}"
|
||
)
|
||
tri.append(v_idx)
|
||
triangles.append((tri[0], tri[1], tri[2]))
|
||
|
||
if pos_count != len(uv_list):
|
||
raise ValueError(
|
||
f"vertex count {pos_count} != uv count {len(uv_list)}; "
|
||
"obj loader assumes 1:1 correspondence"
|
||
)
|
||
|
||
uvs = np.asarray(uv_list, dtype=np.float32)
|
||
tris = np.asarray(triangles, dtype=np.int32)
|
||
return FaceMesh(uvs=uvs, triangles=tris)
|