保留5号历史三档实现,新增中下庭高度定位、扶正裁切、矩形叠加与30案例对比页。 worker仅允许本机和配置白名单访问;网关不增加IP限制。空图片URL字段改写限定为52号,保持旧接口兼容。同步接口、裁切与部署文档。 验证:113项测试通过;1项历史数值基线失败,在未修改的a3fddbb版本复现。浏览器验证5/52及30案例对比页通过。
92 lines
4.0 KiB
Python
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())
|