feat(测量): 接口1/6 返回与标注图一致的竖线坐标
前端需要按标注图重绘竖线,坐标与画线共用同一套几何。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+38
-1
@@ -461,7 +461,7 @@ def test_success_structure(client):
|
||||
data = body["data"]
|
||||
# 业务字段对齐文档
|
||||
assert set(["face_total_height_cm", "four_courts", "seven_eyes",
|
||||
"landmarks", "hairline_source", "head_pose",
|
||||
"landmarks", "hairline_source", "head_pose", "vertical_lines",
|
||||
"annotated_image_base64"]).issubset(data.keys())
|
||||
assert set(["top_court_cm", "upper_court_cm", "middle_court_cm",
|
||||
"lower_court_cm", "ratios"]).issubset(data["four_courts"].keys())
|
||||
@@ -478,3 +478,40 @@ def test_success_structure(client):
|
||||
assert "annotated_image_url" not in data
|
||||
png = base64.b64decode(data["annotated_image_base64"])
|
||||
assert png[:8] == b"\x89PNG\r\n\x1a\n"
|
||||
_assert_vertical_lines(data, "v1")
|
||||
|
||||
|
||||
_INNER_VLINES = [
|
||||
"left_cheek", "left_eye_outer", "left_eye_inner",
|
||||
"right_eye_inner", "right_eye_outer", "right_cheek",
|
||||
]
|
||||
|
||||
|
||||
def _assert_vertical_lines(data, variant):
|
||||
vl = data["vertical_lines"]
|
||||
assert isinstance(vl["y_top"], int) and isinstance(vl["y_bottom"], int)
|
||||
assert vl["y_top"] < vl["y_bottom"]
|
||||
names = [line["name"] for line in vl["lines"]]
|
||||
xs = [line["x"] for line in vl["lines"]]
|
||||
assert xs == sorted(xs)
|
||||
assert all(isinstance(x, int) for x in xs)
|
||||
if variant == "v6":
|
||||
assert names == _INNER_VLINES
|
||||
assert vl["y_top"] == data["landmarks"]["hairline"]["y"]
|
||||
assert vl["y_bottom"] == data["landmarks"]["chin_tip"]["y"]
|
||||
assert "eye1" not in data["seven_eyes"]
|
||||
assert "eye7" not in data["seven_eyes"]
|
||||
return
|
||||
assert set(_INNER_VLINES).issubset(names)
|
||||
assert names[0] in ("left_head_edge", "left_cheek")
|
||||
assert names[-1] in ("right_head_edge", "right_cheek")
|
||||
by_name = {line["name"]: line["x"] for line in vl["lines"]}
|
||||
assert ("left_head_edge" in by_name) == (data["seven_eyes"]["eye1"] is not None)
|
||||
assert ("right_head_edge" in by_name) == (data["seven_eyes"]["eye7"] is not None)
|
||||
|
||||
|
||||
def test_measure_v2_vertical_lines_have_no_head_edges(client):
|
||||
files = {"image_file": ("frontal.jpg", open(fixture("frontal.jpg"), "rb"), "application/octet-stream")}
|
||||
body = client.post("/api/v1/face/measure-v2", headers=H, files=files).json()
|
||||
assert body["code"] == 0, body
|
||||
_assert_vertical_lines(body["data"], "v6")
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
"""竖线坐标:与标注图同一套几何,接口1/6 的返回字段由此而来。"""
|
||||
import numpy as np
|
||||
|
||||
from face_analysis.measure import MeasureResult
|
||||
from face_analysis.annotation import vertical_line_geometry
|
||||
|
||||
|
||||
def _result():
|
||||
"""已知像素坐标的测量结果。顶庭 2cm,不会触发发际线修正。"""
|
||||
vertical = {
|
||||
"hair_top": (500.0, 100.0),
|
||||
"hairline": (500.0, 200.2),
|
||||
"brow_center": (500.0, 350.0),
|
||||
"nose_bottom": (500.0, 500.0),
|
||||
"chin_tip": (500.0, 700.4),
|
||||
"top_court_px": 100.0,
|
||||
"upper_court_px": 150.0,
|
||||
"middle_court_px": 150.0,
|
||||
"lower_court_px": 200.0,
|
||||
}
|
||||
eyes = {
|
||||
"eye_width_px": 80.0,
|
||||
"face_width_px": 400.0,
|
||||
"inter_eye_distance_px": 90.0,
|
||||
"points": {
|
||||
"left_cheek": (300.4, 400.0),
|
||||
"left_outer": (360.6, 400.0),
|
||||
"left_inner": (440.2, 400.0),
|
||||
"right_inner": (530.8, 400.0),
|
||||
"right_outer": (610.4, 400.0),
|
||||
"right_cheek": (700.7, 400.0),
|
||||
},
|
||||
}
|
||||
return MeasureResult(vertical, eyes, 50.0, "estimated", None)
|
||||
|
||||
|
||||
_INNER = [
|
||||
"left_cheek", "left_eye_outer", "left_eye_inner",
|
||||
"right_eye_inner", "right_eye_outer", "right_cheek",
|
||||
]
|
||||
|
||||
|
||||
def test_v1_inner_lines_and_extended_span():
|
||||
geom = vertical_line_geometry(_result(), 1000, 800, variant="v1")
|
||||
# 短边 800,伸出 max(6, round(800*0.03)) = 24
|
||||
assert geom["y_top"] == 76
|
||||
assert geom["y_bottom"] == 724
|
||||
assert [line["name"] for line in geom["lines"]] == _INNER
|
||||
assert [line["x"] for line in geom["lines"]] == [300, 361, 440, 531, 610, 701]
|
||||
|
||||
|
||||
def test_v6_span_is_hairline_to_chin_without_head_edges():
|
||||
ear = np.zeros((800, 1000), dtype=np.uint8)
|
||||
ear[120:680, 250] = 255
|
||||
ear[120:680, 760] = 255
|
||||
geom = vertical_line_geometry(
|
||||
_result(), 1000, 800, ear_mask=ear, hair_mask=None, variant="v6")
|
||||
assert geom["y_top"] == 200
|
||||
assert geom["y_bottom"] == 700
|
||||
assert [line["name"] for line in geom["lines"]] == _INNER
|
||||
|
||||
|
||||
def test_v1_includes_ear_edges_left_to_right():
|
||||
ear = np.zeros((800, 1000), dtype=np.uint8)
|
||||
ear[120:680, 250] = 255
|
||||
ear[120:680, 760] = 255
|
||||
geom = vertical_line_geometry(
|
||||
_result(), 1000, 800, ear_mask=ear, hair_mask=None, variant="v1")
|
||||
names = [line["name"] for line in geom["lines"]]
|
||||
xs = [line["x"] for line in geom["lines"]]
|
||||
assert names == ["left_head_edge", *_INNER, "right_head_edge"]
|
||||
assert xs[0] == 250 and xs[-1] == 760
|
||||
assert xs == sorted(xs)
|
||||
|
||||
|
||||
def test_missing_ear_omits_that_edge():
|
||||
ear = np.zeros((800, 1000), dtype=np.uint8)
|
||||
ear[120:680, 250] = 255
|
||||
geom = vertical_line_geometry(
|
||||
_result(), 1000, 800, ear_mask=ear, hair_mask=None, variant="v1")
|
||||
names = [line["name"] for line in geom["lines"]]
|
||||
assert names[0] == "left_head_edge"
|
||||
assert "right_head_edge" not in names
|
||||
Reference in New Issue
Block a user