feat: 接口1/6 发际线贴近头顶时改为修正而非弃用置null
顶庭(头顶→发际线)<0.7cm 时不再返回 null: - 上庭 = 0.95×中庭 反推发际线;若越过头顶则顶庭固定 0.7cm(发际线钉在头顶下方) - hairline_source 新增 "corrected";to_response 始终输出完整四庭 - 标注图始终正常画发际线横线与庭段 - 接口6 三庭逻辑同步,去 discarded 分支 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -401,36 +401,22 @@ def _run_face_measure_data(image, variant="v1"):
|
|||||||
logger.warning("头发/耳朵分割失败,回退方案A:%s", seg_e)
|
logger.warning("头发/耳朵分割失败,回退方案A:%s", seg_e)
|
||||||
|
|
||||||
result = measure_face(landmarks, hair_mask, w, h, head_pose=head_pose)
|
result = measure_face(landmarks, hair_mask, w, h, head_pose=head_pose)
|
||||||
discarded = result.hairline_discarded
|
|
||||||
data = result.to_response()
|
data = result.to_response()
|
||||||
vd = result.vertical
|
vd = result.vertical
|
||||||
if variant == "v6":
|
if variant == "v6":
|
||||||
if discarded:
|
# 接口6 是三庭:去掉顶庭相关字段(top_court_cm / ratios.top_court),
|
||||||
# 发际线弃用:接口6 的上庭也依赖发际线,一并置 null;只保留中/下庭。
|
# 比例分母按上/中/下三庭。发际线贴近头顶时已在 measure 修正,hairline 正常返回。
|
||||||
base_px = vd["middle_court_px"] + vd["lower_court_px"]
|
base_px = vd["upper_court_px"] + vd["middle_court_px"] + vd["lower_court_px"]
|
||||||
data["four_courts"]["upper_court_cm"] = None
|
data["four_courts"]["ratios"] = {
|
||||||
data["four_courts"]["ratios"] = {
|
"upper_court": round(vd["upper_court_px"] / base_px, 3),
|
||||||
"upper_court": None,
|
"middle_court": round(vd["middle_court_px"] / base_px, 3),
|
||||||
"middle_court": round(vd["middle_court_px"] / base_px, 3),
|
"lower_court": round(vd["lower_court_px"] / base_px, 3),
|
||||||
"lower_court": round(vd["lower_court_px"] / base_px, 3),
|
}
|
||||||
}
|
data["four_courts"].pop("top_court_cm", None)
|
||||||
data["four_courts"].pop("top_court_cm", None)
|
data["face_total_height_cm"] = round(
|
||||||
data["face_total_height_cm"] = round(
|
result.upper_cm + result.middle_cm + result.lower_cm, 2)
|
||||||
result.middle_cm + result.lower_cm, 2)
|
# 注:landmarks.hair_top 保留返回(供前端/下游定位头顶),但顶庭数值、
|
||||||
data["landmarks"]["hairline"] = None
|
# 占比、标注图仍按三庭处理,显示效果不变。
|
||||||
else:
|
|
||||||
base_px = vd["upper_court_px"] + vd["middle_court_px"] + vd["lower_court_px"]
|
|
||||||
# 接口6 是三庭:去掉顶庭相关字段(top_court_cm / ratios.top_court / landmarks.hair_top)
|
|
||||||
data["four_courts"]["ratios"] = {
|
|
||||||
"upper_court": round(vd["upper_court_px"] / base_px, 3),
|
|
||||||
"middle_court": round(vd["middle_court_px"] / base_px, 3),
|
|
||||||
"lower_court": round(vd["lower_court_px"] / base_px, 3),
|
|
||||||
}
|
|
||||||
data["four_courts"].pop("top_court_cm", None)
|
|
||||||
data["face_total_height_cm"] = round(
|
|
||||||
result.upper_cm + result.middle_cm + result.lower_cm, 2)
|
|
||||||
# 注:landmarks.hair_top 保留返回(供前端/下游定位头顶),但顶庭数值、
|
|
||||||
# 占比、标注图仍按三庭处理,显示效果不变。
|
|
||||||
|
|
||||||
# 七眼段宽度(cm)。eye1=左耳外段 eye2=左脸颊 eye3=左眼 eye4=两眼间距 eye5=右眼 eye6=右脸颊 eye7=右耳外段。
|
# 七眼段宽度(cm)。eye1=左耳外段 eye2=左脸颊 eye3=左眼 eye4=两眼间距 eye5=右眼 eye6=右脸颊 eye7=右耳外段。
|
||||||
# eye2~eye6(5段)只用内部分点,接口1/6 共用;eye1/eye7 需耳朵分割端线,仅接口1 有。
|
# eye2~eye6(5段)只用内部分点,接口1/6 共用;eye1/eye7 需耳朵分割端线,仅接口1 有。
|
||||||
@@ -446,10 +432,9 @@ def _run_face_measure_data(image, variant="v1"):
|
|||||||
None if (a is None or b is None) else round((b - a) / pc, 2))
|
None if (a is None or b is None) else round((b - a) / pc, 2))
|
||||||
if variant != "v6":
|
if variant != "v6":
|
||||||
# 接口1 额外算 eye1/eye7(左/右耳外段),需耳朵分割端线。
|
# 接口1 额外算 eye1/eye7(左/右耳外段),需耳朵分割端线。
|
||||||
# 竖向范围:发际线弃用时用眉心做上界(hair_top 不可靠),否则用头顶。
|
# 竖向范围上界用头顶(hair_top 始终可用)。
|
||||||
from face_analysis.annotation import _ear_edges_from_mask
|
from face_analysis.annotation import _ear_edges_from_mask
|
||||||
top_y = (vd["brow_center"][1] if result.hairline_discarded
|
top_y = vd["hair_top"][1]
|
||||||
else vd["hair_top"][1])
|
|
||||||
head_l, head_r = _ear_edges_from_mask(
|
head_l, head_r = _ear_edges_from_mask(
|
||||||
ear_mask, hair_mask,
|
ear_mask, hair_mask,
|
||||||
top_y, vd["chin_tip"][1],
|
top_y, vd["chin_tip"][1],
|
||||||
@@ -709,11 +694,11 @@ def _run_face_measure_data_debug(image):
|
|||||||
_draw_text_cv2(vis_hl, f"hairline_y={hairline_y} (source={result.hairline_source})",
|
_draw_text_cv2(vis_hl, f"hairline_y={hairline_y} (source={result.hairline_source})",
|
||||||
(hairline_y, w - int(s * 0.5)), color=(0, 200, 255),
|
(hairline_y, w - int(s * 0.5)), color=(0, 200, 255),
|
||||||
scale=max(0.4, s * 0.0015))
|
scale=max(0.4, s * 0.0015))
|
||||||
# 发际线弃用提示:顶庭 < 0.7cm 视为贴近头顶、不可靠
|
# 发际线修正提示:检测到的顶庭 < 0.7cm 视为贴近头顶、已按 0.95×中庭修正
|
||||||
if result.hairline_discarded:
|
if getattr(result, "hairline_corrected", False):
|
||||||
gap_cm = result.top_cm
|
gap_cm = result.detected_top_cm
|
||||||
_draw_text_cv2(vis_hl,
|
_draw_text_cv2(vis_hl,
|
||||||
f"⚠️ 发际线离头顶仅 {gap_cm:.2f}cm (<0.7cm),已弃用",
|
f"⚠️ 发际线离头顶仅 {gap_cm:.2f}cm (<0.7cm),已按 0.95×中庭修正",
|
||||||
(10, 10), color=(40, 40, 255), scale=max(0.5, s * 0.0022))
|
(10, 10), color=(40, 40, 255), scale=max(0.5, s * 0.0022))
|
||||||
put("hairline", vis_hl)
|
put("hairline", vis_hl)
|
||||||
|
|
||||||
@@ -829,8 +814,8 @@ def _run_face_measure_data_debug(image):
|
|||||||
None if (a is None or b is None) else round((b - a) / pc, 2))
|
None if (a is None or b is None) else round((b - a) / pc, 2))
|
||||||
from face_analysis.annotation import _ear_edges_from_mask as _eef
|
from face_analysis.annotation import _ear_edges_from_mask as _eef
|
||||||
lcx, rcx = epts["left_cheek"][0], epts["right_cheek"][0]
|
lcx, rcx = epts["left_cheek"][0], epts["right_cheek"][0]
|
||||||
# 弃用时用眉心做上界(与 _run_face_measure_data 一致)
|
# 竖向范围上界用头顶(hair_top 始终可用)
|
||||||
top_y = v["brow_center"][1] if result.hairline_discarded else v["hair_top"][1]
|
top_y = v["hair_top"][1]
|
||||||
head_l, head_r = _eef(ear_mask, hair_mask, top_y, v["chin_tip"][1],
|
head_l, head_r = _eef(ear_mask, hair_mask, top_y, v["chin_tip"][1],
|
||||||
lcx, rcx, (lcx + rcx) / 2)
|
lcx, rcx, (lcx + rcx) / 2)
|
||||||
data["seven_eyes"]["eye1"] = None if head_l is None else round((lcx - head_l) / pc, 2)
|
data["seven_eyes"]["eye1"] = None if head_l is None else round((lcx - head_l) / pc, 2)
|
||||||
|
|||||||
+1
-1
@@ -446,7 +446,7 @@
|
|||||||
| four_courts | object | 四庭数据(顶/上/中/下庭 cm + 占比 ratios),结构同接口1 |
|
| four_courts | object | 四庭数据(顶/上/中/下庭 cm + 占比 ratios),结构同接口1 |
|
||||||
| seven_eyes | object | 七眼数据(眼宽/脸宽/两眼间距 cm + 占比 ratios + eye1~eye7 从左到右 7 段宽度),结构同接口1 |
|
| seven_eyes | object | 七眼数据(眼宽/脸宽/两眼间距 cm + 占比 ratios + eye1~eye7 从左到右 7 段宽度),结构同接口1 |
|
||||||
| landmarks | object | 5 个纵向关键点像素坐标(hair_top/hairline/brow_center/nose_bottom/chin_tip),结构同接口1 |
|
| landmarks | object | 5 个纵向关键点像素坐标(hair_top/hairline/brow_center/nose_bottom/chin_tip),结构同接口1 |
|
||||||
| hairline_source | string | 发际线来源:`segmentation`(真实分割)/ `estimated`(比例估算) |
|
| hairline_source | string | 发际线来源:`segmentation`(真实分割)/ `estimated`(比例估算)/ `corrected`(发际线贴近头顶、分割不可靠,已按 0.95×中庭反推修正) |
|
||||||
| head_pose | object | 头部姿态角度(yaw/pitch/roll,单位:度) |
|
| head_pose | object | 头部姿态角度(yaw/pitch/roll,单位:度) |
|
||||||
| left_position | object | MediaPipe 21 号关键点坐标(左脸定位点),原图像素:`{ "x": int, "y": int }` |
|
| left_position | object | MediaPipe 21 号关键点坐标(左脸定位点),原图像素:`{ "x": int, "y": int }` |
|
||||||
| right_position | object | MediaPipe 251 号关键点坐标(右脸定位点,与 21 号镜像),原图像素:`{ "x": int, "y": int }` |
|
| right_position | object | MediaPipe 251 号关键点坐标(右脸定位点,与 21 号镜像),原图像素:`{ "x": int, "y": int }` |
|
||||||
|
|||||||
@@ -212,11 +212,9 @@ def create_annotated_image(image_bgr, measure_result, ear_mask=None, hair_mask=N
|
|||||||
|
|
||||||
buf = np.zeros((h, w, 4), dtype=np.uint8)
|
buf = np.zeros((h, w, 4), dtype=np.uint8)
|
||||||
|
|
||||||
# 发际线弃用(hairline_discarded):保留头顶横线,去掉发际线横线,
|
# 横线:v1 = 头顶/发际线/眉心/鼻翼下缘/下巴尖;v6 = 发际线/眉心/鼻翼下缘/下巴尖。
|
||||||
# 也不标顶/上庭(缺发际线作边界,算不出)。横线 = 头顶/眉心/鼻翼下缘/下巴尖。
|
# (发际线贴近头顶时已在 MeasureResult 修正 vertical,此处始终正常画发际线。)
|
||||||
if getattr(measure_result, "hairline_discarded", False):
|
if variant == "v6":
|
||||||
order = ["hair_top", "brow_center", "nose_bottom", "chin_tip"]
|
|
||||||
elif variant == "v6":
|
|
||||||
order = ["hairline", "brow_center", "nose_bottom", "chin_tip"]
|
order = ["hairline", "brow_center", "nose_bottom", "chin_tip"]
|
||||||
else:
|
else:
|
||||||
order = ["hair_top", "hairline", "brow_center", "nose_bottom", "chin_tip"]
|
order = ["hair_top", "hairline", "brow_center", "nose_bottom", "chin_tip"]
|
||||||
@@ -243,7 +241,7 @@ def create_annotated_image(image_bgr, measure_result, ear_mask=None, hair_mask=N
|
|||||||
face_cx = (fx0 + fx1) / 2
|
face_cx = (fx0 + fx1) / 2
|
||||||
over = max(6, round(s * 0.030)) # 线超出包围盒的长度(参考图风格)
|
over = max(6, round(s * 0.030)) # 线超出包围盒的长度(参考图风格)
|
||||||
face_half = (fx1 - fx0) / 2 + over # 横线超出最外侧竖线一点
|
face_half = (fx1 - fx0) / 2 + over # 横线超出最外侧竖线一点
|
||||||
# 竖线纵向范围:v6 = 发际线→下巴尖(不超出);v1(含发际线弃用)= 头顶→下巴尖并两端超出一点
|
# 竖线纵向范围:v6 = 发际线→下巴尖(不超出);v1 = 头顶→下巴尖并两端超出一点
|
||||||
v_top = fy0 if variant == "v6" else fy0 - over
|
v_top = fy0 if variant == "v6" else fy0 - over
|
||||||
v_bot = fy1 if variant == "v6" else fy1 + over
|
v_bot = fy1 if variant == "v6" else fy1 + over
|
||||||
|
|
||||||
@@ -281,14 +279,8 @@ def create_annotated_image(image_bgr, measure_result, ear_mask=None, hair_mask=N
|
|||||||
draw.text((x, ys[i]), text, fill=LINE_COLOR, font=font, anchor="lm")
|
draw.text((x, ys[i]), text, fill=LINE_COLOR, font=font, anchor="lm")
|
||||||
|
|
||||||
# --- 3b. 左侧四庭:名 + 数值两行(无 cm)+ 竖向虚线双箭头 ---
|
# --- 3b. 左侧四庭:名 + 数值两行(无 cm)+ 竖向虚线双箭头 ---
|
||||||
# court_start:庭段在 order 里的起始索引。发际线弃用时 order 首位是头顶(无下界发际线,
|
# court_start:庭段在 order 里的起始索引。v6 为三庭(无顶庭),其余四庭。
|
||||||
# 顶/上庭不标),中庭从眉心开始 → 跳过 order[0]。
|
if variant == "v6":
|
||||||
if getattr(measure_result, "hairline_discarded", False):
|
|
||||||
court_cm = [measure_result.middle_cm, measure_result.lower_cm]
|
|
||||||
court_name = ["中庭", "下庭"]
|
|
||||||
n_court = 2
|
|
||||||
court_start = 1
|
|
||||||
elif variant == "v6":
|
|
||||||
court_cm = [measure_result.upper_cm, measure_result.middle_cm, measure_result.lower_cm]
|
court_cm = [measure_result.upper_cm, measure_result.middle_cm, measure_result.lower_cm]
|
||||||
court_name = ["上庭", "中庭", "下庭"]
|
court_name = ["上庭", "中庭", "下庭"]
|
||||||
n_court = 3
|
n_court = 3
|
||||||
|
|||||||
+61
-82
@@ -153,10 +153,12 @@ def pt_or_none(vertical, name):
|
|||||||
class MeasureResult:
|
class MeasureResult:
|
||||||
"""测量结果,提供 to_response() 输出与接口文档同构的 data 字段。"""
|
"""测量结果,提供 to_response() 输出与接口文档同构的 data 字段。"""
|
||||||
|
|
||||||
# 发际线弃用阈值:发际线离头顶(顶庭)< 此值时判定分割不可靠,弃用发际线。
|
# 发际线修正阈值/固定值:发际线离头顶(顶庭)< 此值时判定分割不可靠,需修正发际线。
|
||||||
# hairline 与 hair_top 几乎重合(如稀疏头发中轴漏检只剩一小撮),说明发际线
|
# hairline 与 hair_top 几乎重合(如稀疏头发中轴漏检只剩一小撮)→ 上庭改取
|
||||||
# 定位无意义 → 顶/上庭置 null、标注图不画头顶/发际线。
|
# 0.95×中庭反推发际线;若反推后越过头顶,则顶庭固定为 0.7cm(发际线钉在头顶下方
|
||||||
HAIRLINE_DISCARD_TOP_CM = 0.7
|
# 0.7cm),上庭取眉心→发际线剩余距离。修正后始终返回完整四庭,不再置 null。
|
||||||
|
HAIRLINE_CORRECT_TOP_CM = 0.7
|
||||||
|
UPPER_TO_MIDDLE_RATIO = 0.95 # 贴近头顶时,上庭 = 中庭 × 此比例
|
||||||
|
|
||||||
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):
|
landmarks=None, image_width=None, image_height=None):
|
||||||
@@ -170,19 +172,32 @@ class MeasureResult:
|
|||||||
self.w = image_width
|
self.w = image_width
|
||||||
self.h = image_height
|
self.h = image_height
|
||||||
|
|
||||||
# 各庭厘米
|
# 发际线修正:顶庭(头顶→发际线)< 阈值视为发际线贴近头顶、不可靠。
|
||||||
|
# 改写:上庭 = 0.95×中庭 → 由眉心反推发际线 y;若发际线越过头顶
|
||||||
|
# (hairline_y < hair_top_y),顶庭固定为 0.7cm(发际线钉在头顶下方 0.7cm)。
|
||||||
|
# 回写 vertical,使 to_response/标注图/v6 复用全部拿到修正后的发际线。
|
||||||
|
self.detected_top_cm = vertical["top_court_px"] / px_per_cm
|
||||||
|
self.hairline_corrected = self.detected_top_cm < self.HAIRLINE_CORRECT_TOP_CM
|
||||||
|
if self.hairline_corrected:
|
||||||
|
brow_x, brow_y = vertical["brow_center"]
|
||||||
|
hair_top_y = vertical["hair_top"][1]
|
||||||
|
new_upper_px = vertical["middle_court_px"] * self.UPPER_TO_MIDDLE_RATIO
|
||||||
|
hairline_y = brow_y - new_upper_px
|
||||||
|
if hairline_y < hair_top_y: # 反推发际线越过头顶 → 顶庭固定 0.7cm
|
||||||
|
hairline_y = hair_top_y + self.HAIRLINE_CORRECT_TOP_CM * px_per_cm
|
||||||
|
new_upper_px = brow_y - hairline_y
|
||||||
|
vertical["hairline"] = (brow_x, float(hairline_y))
|
||||||
|
vertical["upper_court_px"] = new_upper_px
|
||||||
|
vertical["top_court_px"] = hairline_y - hair_top_y
|
||||||
|
self.hairline_source = "corrected"
|
||||||
|
|
||||||
|
# 各庭厘米(修正后)
|
||||||
self.top_cm = vertical["top_court_px"] / px_per_cm
|
self.top_cm = vertical["top_court_px"] / px_per_cm
|
||||||
self.upper_cm = vertical["upper_court_px"] / px_per_cm
|
self.upper_cm = vertical["upper_court_px"] / px_per_cm
|
||||||
self.middle_cm = vertical["middle_court_px"] / px_per_cm
|
self.middle_cm = vertical["middle_court_px"] / px_per_cm
|
||||||
self.lower_cm = vertical["lower_court_px"] / px_per_cm
|
self.lower_cm = vertical["lower_court_px"] / px_per_cm
|
||||||
# 发际线弃用判定:顶庭(头顶→发际线)过小视为发际线贴近头顶、不可靠。
|
self.face_total_cm = (self.top_cm + self.upper_cm
|
||||||
# 弃用时 hairline_source 改为 "discarded",face_total 只算中庭+下庭。
|
+ self.middle_cm + self.lower_cm)
|
||||||
self.hairline_discarded = self.top_cm < self.HAIRLINE_DISCARD_TOP_CM
|
|
||||||
if self.hairline_discarded:
|
|
||||||
self.hairline_source = "discarded"
|
|
||||||
self.face_total_cm = self.middle_cm + self.lower_cm
|
|
||||||
else:
|
|
||||||
self.face_total_cm = self.top_cm + self.upper_cm + self.middle_cm + self.lower_cm
|
|
||||||
|
|
||||||
# 七眼厘米
|
# 七眼厘米
|
||||||
self.eye_width_cm = eyes["eye_width_px"] / px_per_cm
|
self.eye_width_cm = eyes["eye_width_px"] / px_per_cm
|
||||||
@@ -190,77 +205,41 @@ class MeasureResult:
|
|||||||
self.inter_eye_cm = eyes["inter_eye_distance_px"] / px_per_cm
|
self.inter_eye_cm = eyes["inter_eye_distance_px"] / px_per_cm
|
||||||
|
|
||||||
def to_response(self):
|
def to_response(self):
|
||||||
# 发际线弃用:顶/上庭相关字段置 null(保留键),ratio 分母只算中下庭;
|
# 四庭完整输出(发际线贴近头顶时已在 __init__ 修正 vertical,无 null 分支)。
|
||||||
# landmarks.hair_top/hairline 置 null。否则按四庭正常输出。
|
total_px = (self.vertical["top_court_px"] + self.vertical["upper_court_px"]
|
||||||
if self.hairline_discarded:
|
+ self.vertical["middle_court_px"] + self.vertical["lower_court_px"])
|
||||||
base_px = (self.vertical["middle_court_px"] + self.vertical["lower_court_px"])
|
data = {
|
||||||
data = {
|
"face_total_height_cm": round(self.face_total_cm, 2),
|
||||||
"face_total_height_cm": round(self.face_total_cm, 2),
|
"four_courts": {
|
||||||
"four_courts": {
|
"top_court_cm": round(self.top_cm, 2),
|
||||||
"top_court_cm": None,
|
"upper_court_cm": round(self.upper_cm, 2),
|
||||||
"upper_court_cm": None,
|
"middle_court_cm": round(self.middle_cm, 2),
|
||||||
"middle_court_cm": round(self.middle_cm, 2),
|
"lower_court_cm": round(self.lower_cm, 2),
|
||||||
"lower_court_cm": round(self.lower_cm, 2),
|
"ratios": {
|
||||||
"ratios": {
|
"top_court": round(self.vertical["top_court_px"] / total_px, 3),
|
||||||
"top_court": None,
|
"upper_court": round(self.vertical["upper_court_px"] / total_px, 3),
|
||||||
"upper_court": None,
|
"middle_court": round(self.vertical["middle_court_px"] / total_px, 3),
|
||||||
"middle_court": round(self.vertical["middle_court_px"] / base_px, 3),
|
"lower_court": round(self.vertical["lower_court_px"] / total_px, 3),
|
||||||
"lower_court": round(self.vertical["lower_court_px"] / base_px, 3),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"seven_eyes": {
|
},
|
||||||
"eye_width_cm": round(self.eye_width_cm, 2),
|
"seven_eyes": {
|
||||||
"face_width_cm": round(self.face_width_cm, 2),
|
"eye_width_cm": round(self.eye_width_cm, 2),
|
||||||
"inter_eye_distance_cm": round(self.inter_eye_cm, 2),
|
"face_width_cm": round(self.face_width_cm, 2),
|
||||||
"ratios": {
|
"inter_eye_distance_cm": round(self.inter_eye_cm, 2),
|
||||||
"eye_width": round(self.eyes["eye_width_px"] / self.eyes["face_width_px"], 3),
|
"ratios": {
|
||||||
"inter_eye_distance": round(self.eyes["inter_eye_distance_px"] / self.eyes["face_width_px"], 3),
|
"eye_width": round(self.eyes["eye_width_px"] / self.eyes["face_width_px"], 3),
|
||||||
},
|
"inter_eye_distance": round(self.eyes["inter_eye_distance_px"] / self.eyes["face_width_px"], 3),
|
||||||
},
|
},
|
||||||
"landmarks": {
|
},
|
||||||
"hair_top": None,
|
"landmarks": {
|
||||||
"hairline": None,
|
"hair_top": pt_or_none(self.vertical, "hair_top"),
|
||||||
"brow_center": pt_or_none(self.vertical, "brow_center"),
|
"hairline": pt_or_none(self.vertical, "hairline"),
|
||||||
"nose_bottom": pt_or_none(self.vertical, "nose_bottom"),
|
"brow_center": pt_or_none(self.vertical, "brow_center"),
|
||||||
"chin_tip": pt_or_none(self.vertical, "chin_tip"),
|
"nose_bottom": pt_or_none(self.vertical, "nose_bottom"),
|
||||||
},
|
"chin_tip": pt_or_none(self.vertical, "chin_tip"),
|
||||||
"hairline_source": self.hairline_source,
|
},
|
||||||
}
|
"hairline_source": self.hairline_source,
|
||||||
else:
|
}
|
||||||
total_px = (self.vertical["top_court_px"] + self.vertical["upper_court_px"]
|
|
||||||
+ self.vertical["middle_court_px"] + self.vertical["lower_court_px"])
|
|
||||||
data = {
|
|
||||||
"face_total_height_cm": round(self.face_total_cm, 2),
|
|
||||||
"four_courts": {
|
|
||||||
"top_court_cm": round(self.top_cm, 2),
|
|
||||||
"upper_court_cm": round(self.upper_cm, 2),
|
|
||||||
"middle_court_cm": round(self.middle_cm, 2),
|
|
||||||
"lower_court_cm": round(self.lower_cm, 2),
|
|
||||||
"ratios": {
|
|
||||||
"top_court": round(self.vertical["top_court_px"] / total_px, 3),
|
|
||||||
"upper_court": round(self.vertical["upper_court_px"] / total_px, 3),
|
|
||||||
"middle_court": round(self.vertical["middle_court_px"] / total_px, 3),
|
|
||||||
"lower_court": round(self.vertical["lower_court_px"] / total_px, 3),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"seven_eyes": {
|
|
||||||
"eye_width_cm": round(self.eye_width_cm, 2),
|
|
||||||
"face_width_cm": round(self.face_width_cm, 2),
|
|
||||||
"inter_eye_distance_cm": round(self.inter_eye_cm, 2),
|
|
||||||
"ratios": {
|
|
||||||
"eye_width": round(self.eyes["eye_width_px"] / self.eyes["face_width_px"], 3),
|
|
||||||
"inter_eye_distance": round(self.eyes["inter_eye_distance_px"] / self.eyes["face_width_px"], 3),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"landmarks": {
|
|
||||||
"hair_top": pt_or_none(self.vertical, "hair_top"),
|
|
||||||
"hairline": pt_or_none(self.vertical, "hairline"),
|
|
||||||
"brow_center": pt_or_none(self.vertical, "brow_center"),
|
|
||||||
"nose_bottom": pt_or_none(self.vertical, "nose_bottom"),
|
|
||||||
"chin_tip": pt_or_none(self.vertical, "chin_tip"),
|
|
||||||
},
|
|
||||||
"hairline_source": self.hairline_source,
|
|
||||||
}
|
|
||||||
# left/right_position:mediapipe 21/251 号定位点(原图像素,与 landmarks 同坐标系)。
|
# left/right_position:mediapipe 21/251 号定位点(原图像素,与 landmarks 同坐标系)。
|
||||||
# landmarks 缺省(如测试直构 MeasureResult)时不输出,保持向后兼容。
|
# landmarks 缺省(如测试直构 MeasureResult)时不输出,保持向后兼容。
|
||||||
if self.landmarks is not None and self.w and self.h:
|
if self.landmarks is not None and self.w and self.h:
|
||||||
|
|||||||
+2
-2
@@ -173,7 +173,7 @@ def test_hairline_gen_female(client, monkeypatch):
|
|||||||
assert set([f"eye{i}" for i in range(1, 8)]).issubset(fm["seven_eyes"].keys())
|
assert set([f"eye{i}" for i in range(1, 8)]).issubset(fm["seven_eyes"].keys())
|
||||||
assert set(["hair_top", "hairline", "brow_center",
|
assert set(["hair_top", "hairline", "brow_center",
|
||||||
"nose_bottom", "chin_tip"]).issubset(fm["landmarks"].keys())
|
"nose_bottom", "chin_tip"]).issubset(fm["landmarks"].keys())
|
||||||
assert fm["hairline_source"] in ("segmentation", "estimated")
|
assert fm["hairline_source"] in ("segmentation", "estimated", "corrected")
|
||||||
assert set(["yaw", "pitch", "roll"]).issubset(fm["head_pose"].keys())
|
assert set(["yaw", "pitch", "roll"]).issubset(fm["head_pose"].keys())
|
||||||
|
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ def test_success_structure(client):
|
|||||||
assert data["seven_eyes"]["eye3"] is not None
|
assert data["seven_eyes"]["eye3"] is not None
|
||||||
assert data["seven_eyes"]["eye4"] is not None
|
assert data["seven_eyes"]["eye4"] is not None
|
||||||
assert data["seven_eyes"]["eye5"] is not None
|
assert data["seven_eyes"]["eye5"] is not None
|
||||||
assert data["hairline_source"] in ("segmentation", "estimated")
|
assert data["hairline_source"] in ("segmentation", "estimated", "corrected")
|
||||||
# base64 解码为合法 PNG(非 URL)
|
# base64 解码为合法 PNG(非 URL)
|
||||||
assert "annotated_image_url" not in data
|
assert "annotated_image_url" not in data
|
||||||
png = base64.b64decode(data["annotated_image_base64"])
|
png = base64.b64decode(data["annotated_image_base64"])
|
||||||
|
|||||||
Reference in New Issue
Block a user