"""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())