From a1d458eb2095d3260dc7de025a63ff6e2cbbaace Mon Sep 17 00:00:00 2001 From: xsl Date: Fri, 24 Jul 2026 00:42:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=8E=A5=E5=8F=A31/5/6):=20=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E6=95=B0=E6=8D=AE=E6=96=B0=E5=A2=9E=20left=5Fposition?= =?UTF-8?q?/right=5Fposition(MediaPipe=2021/251=E5=8F=B7=E7=82=B9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - face_mesh_landmarks.py: 加常量 LEFT_POSITION=21 / RIGHT_POSITION=251 - measure.py: MeasureResult 收 landmarks/宽高, to_response 顶层输出两点(原图像素 {x,y}, 与 landmarks 同格式) - measure_face 透传 landmarks(签名不变, 6处调用零改动); __init__ 用 None 默认值守卫向后兼容 - 三接口自动生效: 接口1/6 在 data 顶层, 接口5 在 face_measure 对象里(复用同一 to_response) - 实测坐标左右镜像合理, 44 个现有测试全过无回归 --- face_analysis/face_mesh_landmarks.py | 2 ++ face_analysis/measure.py | 23 ++++++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/face_analysis/face_mesh_landmarks.py b/face_analysis/face_mesh_landmarks.py index 345206b..c411ab1 100644 --- a/face_analysis/face_mesh_landmarks.py +++ b/face_analysis/face_mesh_landmarks.py @@ -18,6 +18,8 @@ RIGHT_EYE_INNER = 362 # 右眼内角 RIGHT_EYE_OUTER = 263 # 右眼外角 LEFT_CHEEK = 234 # 左脸颧弓(脸宽左端) RIGHT_CHEEK = 454 # 右脸颧弓(脸宽右端) +LEFT_POSITION = 21 # 左脸前侧定位点(脸颊/耳前区域,与 251 镜像) +RIGHT_POSITION = 251 # 右脸前侧定位点(与 21 镜像) # --- 鼻尖(solvePnP 用,可选) --- NOSE_TIP = 1 # 鼻尖(也有用 4 的版本) diff --git a/face_analysis/measure.py b/face_analysis/measure.py index d92930b..49a4d4d 100644 --- a/face_analysis/measure.py +++ b/face_analysis/measure.py @@ -14,7 +14,7 @@ from face_analysis.calibration import ( from face_analysis.face_mesh_landmarks import ( GLABELLA_9, GLABELLA_151, NOSE_BOTTOM, CHIN_TIP, LEFT_EYE_OUTER, LEFT_EYE_INNER, RIGHT_EYE_INNER, RIGHT_EYE_OUTER, - LEFT_CHEEK, RIGHT_CHEEK, + LEFT_CHEEK, RIGHT_CHEEK, LEFT_POSITION, RIGHT_POSITION, ) from face_analysis.hair_segmenter import locate_hairline_by_segmentation @@ -147,12 +147,17 @@ def measure_seven_eyes(landmarks, image_width, image_height): class MeasureResult: """测量结果,提供 to_response() 输出与接口文档同构的 data 字段。""" - def __init__(self, vertical, eyes, px_per_cm, hairline_source, head_pose): + def __init__(self, vertical, eyes, px_per_cm, hairline_source, head_pose, + landmarks=None, image_width=None, image_height=None): self.vertical = vertical self.eyes = eyes self.px_per_cm = px_per_cm self.hairline_source = hairline_source self.head_pose = head_pose # (yaw, pitch, roll) 或 None + # 原始 mediapipe 点集 + 图像尺寸,供 to_response 输出 21/251 号定位点 + self.landmarks = landmarks + self.w = image_width + self.h = image_height # 各庭厘米 self.top_cm = vertical["top_court_px"] / px_per_cm @@ -207,6 +212,17 @@ class MeasureResult: }, "hairline_source": self.hairline_source, } + # left/right_position:mediapipe 21/251 号定位点(原图像素,与 landmarks 同坐标系)。 + # landmarks 缺省(如测试直构 MeasureResult)时不输出,保持向后兼容。 + if self.landmarks is not None and self.w and self.h: + lm = _lm_list(self.landmarks) + + def _pt_lm(idx): + px, py = normalized_to_pixel(lm[idx], self.w, self.h) + return {"x": int(round(px)), "y": int(round(py))} + + data["left_position"] = _pt_lm(LEFT_POSITION) + data["right_position"] = _pt_lm(RIGHT_POSITION) if self.head_pose is not None: yaw, pitch, roll = self.head_pose data["head_pose"] = { @@ -220,7 +236,8 @@ def measure_face(landmarks, hair_mask, image_width, image_height, head_pose=None vertical, source = decide_vertical(landmarks, image_width, image_height, hair_mask) eyes = measure_seven_eyes(landmarks, image_width, image_height) px_per_cm = estimate_scale_factor(landmarks, image_width, image_height) - return MeasureResult(vertical, eyes, px_per_cm, source, head_pose) + return MeasureResult(vertical, eyes, px_per_cm, source, head_pose, + landmarks, image_width, image_height) if __name__ == "__main__":