Files
hair/tests/test_hairline_height.py
T
xsl 5b2b73f5d5 feat: 新增52号发际线接口并限制GPU worker来源IP
保留5号历史三档实现,新增中下庭高度定位、扶正裁切、矩形叠加与30案例对比页。

worker仅允许本机和配置白名单访问;网关不增加IP限制。空图片URL字段改写限定为52号,保持旧接口兼容。同步接口、裁切与部署文档。

验证:113项测试通过;1项历史数值基线失败,在未修改的a3fddbb版本复现。浏览器验证5/52及30案例对比页通过。
2026-11-04 10:20:29 +08:00

92 lines
4.0 KiB
Python

"""Court-based design placement, including preview/inpaint consistency."""
import cv2
import numpy as np
import pytest
from hairline.height_rule import (
height_target, curve_center, place_curve, inpaint_from_curve,
)
def _landmarks(width=320, height=400):
lm = np.zeros((468, 3), np.float32)
for index, xy in {9: (160, 180), 94: (160, 260), 152: (160, 320),
10: (160, 140), 21: (70, 170), 251: (250, 170)}.items():
lm[index, :2] = np.array(xy) / [width, height]
return lm
def test_target_uses_mean_courts_without_old_estimation_ratio():
rule = height_target(_landmarks(), 320, 400)
assert rule["middle_height_px"] == pytest.approx(80, abs=1e-4)
assert rule["lower_height_px"] == pytest.approx(60, abs=1e-4)
assert rule["target_upper_height_px"] == pytest.approx(70, abs=1e-4)
assert rule["target_center"]["y"] == pytest.approx(110, abs=1e-4)
def test_reference_uses_unrounded_measurement_and_scale():
ref = {"brow_center": (161.4, 181.3), "nose_bottom": (160.0, 261.9),
"chin_tip": (160.0, 321.5), "px_per_cm": 20}
rule = height_target(_landmarks(), 320, 400, ref)
assert rule["target_upper_height_px"] == pytest.approx(70.1)
assert rule["target_center"]["y"] == pytest.approx(111.2)
assert rule["target_upper_height_cm"] == pytest.approx(3.505)
assert rule["measurement_source"] == "face_measure"
@pytest.mark.parametrize("scale", [1, 2])
@pytest.mark.parametrize("center_y", [90, 125, 150])
def test_all_shapes_align_to_same_height_at_multiple_scales(scale, center_y):
h, w = 400 * scale, 320 * scale
lm = _landmarks()
overlay = np.zeros((h, w, 4), np.float32)
# A non-horizontal design, preserving a relative 20px arc after movement.
pts = np.array([[70, center_y + 20], [160, center_y], [250, center_y + 20]]) * scale
cv2.polylines(overlay, [pts.astype(np.int32)], False, (255, 255, 255, 255), 3 * scale)
original = overlay.copy()
target = height_target(lm, w, h)["target_center"]
moved, placement = place_curve(overlay, target, lm)
assert abs(curve_center(moved, target["x"])["y"] - target["y"]) < .2
assert placement["target_error_px"] < .2
# Shape's relative heights stay constant after translation.
before_delta = curve_center(original, 100 * scale)["y"] - curve_center(original, 160 * scale)["y"]
after_delta = curve_center(moved, 100 * scale)["y"] - curve_center(moved, 160 * scale)["y"]
assert after_delta == pytest.approx(before_delta, abs=.1)
np.testing.assert_array_equal(overlay, original)
def test_rolled_face_preserves_curve_angle():
lm = _landmarks()
lm[10, 0] -= .08
overlay = np.zeros((400, 320, 4), np.float32)
cv2.line(overlay, (60, 95), (260, 135), (255, 255, 255, 255), 3)
moved, _ = place_curve(overlay, {"x": 160, "y": 150}, lm)
original_slope = curve_center(overlay, 200)["y"] - curve_center(overlay, 120)["y"]
moved_slope = curve_center(moved, 200)["y"] - curve_center(moved, 120)["y"]
assert moved_slope == pytest.approx(original_slope, abs=.2)
def test_inpaint_mask_follows_new_line_not_old_black_template():
lm = _landmarks()
photo = np.full((400, 320, 3), 200, np.uint8)
parsing = np.ones((400, 320), np.int32)
overlay = np.zeros((400, 320, 4), np.float32)
cv2.line(overlay, (20, 140), (300, 140), (255, 255, 255, 255), 3)
moved, _ = place_curve(overlay, {"x": 160, "y": 100}, lm)
marked, mask = inpaint_from_curve(photo, lm, parsing, moved)
assert marked[100, 160, 0] == 0
assert marked[140, 160, 0] == 200
assert mask[90, 160] > 240
assert mask[130, 160] < 10
def test_out_of_frame_target_is_reported_instead_of_silent_clamping():
ref = {"brow_center": (160, 50), "nose_bottom": (160, 200), "chin_tip": (160, 350)}
with pytest.raises(ValueError, match="超出图片"):
height_target(_landmarks(), 320, 400, ref)
def test_missing_midline_curve_is_reported():
with pytest.raises(ValueError, match="有效曲线"):
place_curve(np.zeros((400, 320, 4), np.float32), {"x": 160, "y": 100}, _landmarks())