"""Framing must align real pixel coordinates without scaling or invented pixels.""" import io import cv2 import numpy as np from PIL import Image import pytest from hairline.framing import calibrate_reference, crop_fits, decode_portrait, normalize_portrait, transform_points PROFILE = {"version": "test", "reference_image_url": "/reference.png", "width_per_face_length": 2.1, "height_per_face_length": 2.8, "brow_x_ratio": .49, "brow_y_ratio": .34} def landmarks(w, h, brow, length=100, angle=0): lm = np.zeros((478, 3), np.float64) down = np.array([np.sin(np.deg2rad(angle)), np.cos(np.deg2rad(angle))]) lm[:, :2] = np.array(brow)/[w, h] lm[152, :2] = (np.array(brow)+down*length)/[w, h] lm[:, 2] = .01 return lm @pytest.mark.parametrize("angle", [-22, 0, 17]) def test_native_scale_upright_and_inverse_mapping(angle): photo = np.full((700, 600, 3), 127, np.uint8) lm = landmarks(600, 700, (300, 280), angle=angle) result, moved, metadata = normalize_portrait(photo, lm, PROFILE) h, w = result.shape[:2] assert metadata["standard_crop"] is True assert w/h == .75 xy = moved[[9, 152], :2]*[w, h] assert abs(xy[0, 0]-xy[1, 0]) < 1e-9 assert xy[1, 1]-xy[0, 1] == pytest.approx(100) assert xy[0, 0]/w == pytest.approx(PROFILE["brow_x_ratio"]) assert xy[0, 1]/h == pytest.approx(PROFILE["brow_y_ratio"]) matrix = np.array(metadata["source_to_processed"]) np.testing.assert_allclose(np.linalg.svd(matrix[:, :2])[1], [1, 1], atol=1e-10) restored = transform_points(moved[:, :2]*[w, h], np.array(metadata["processed_to_source"])) np.testing.assert_allclose(restored, lm[:, :2]*[600, 700], atol=1e-9) assert result.min() == 127 # no black triangles / padded background def test_insufficient_space_keeps_upright_original_size_when_no_rotation_needed(): photo = np.full((300, 230, 3), 127, np.uint8) result, moved, metadata = normalize_portrait(photo, landmarks(230, 300, (115, 80), 150), PROFILE) assert metadata["standard_crop"] is False assert metadata["skip_reason"] == "insufficient_space" assert result.shape == photo.shape np.testing.assert_array_equal(result, photo) assert abs(moved[9, 0]-moved[152, 0]) < 1e-10 def test_rotated_fallback_has_no_empty_corners(): photo = np.full((400, 300, 3), 127, np.uint8) result, moved, metadata = normalize_portrait(photo, landmarks(300, 400, (150, 130), 160, 12), PROFILE) assert metadata["standard_crop"] is False assert result.min() == 127 assert metadata["axis_x_error_px"] < 1e-10 assert crop_fits(np.array(metadata["source_to_processed"]), (300, 400), result.shape[1::-1]) @pytest.mark.parametrize("brow,angle", [((150, 330), 0), ((150, 330), 8), ((150, 70), -8)]) def test_tall_fallback_is_three_by_four_and_retains_face(brow, angle): photo = np.full((700, 300, 3), 127, np.uint8) lm = landmarks(300, 700, brow, length=200, angle=angle) image, moved, metadata = normalize_portrait(photo, lm, PROFILE) h, w = image.shape[:2] assert metadata["standard_crop"] is False assert metadata["aspect_crop"] is True assert metadata["status"] == "aspect_crop" assert w*4 == h*3 xy = moved[:, :2]*[w, h] assert (xy >= -1e-7).all() and (xy <= [w-1+1e-7, h-1+1e-7]).all() assert abs(xy[9, 0]-xy[152, 0]) < 1e-9 assert np.linalg.norm(xy[152]-xy[9]) == pytest.approx(200) assert image.min() == 127 assert crop_fits(np.array(metadata["source_to_processed"]), (300, 700), (w, h)) def test_tall_fallback_does_not_cut_face_that_cannot_fit_three_by_four(): photo = np.full((700, 300, 3), 127, np.uint8) image, moved, metadata = normalize_portrait(photo, landmarks(300, 700, (150, 100), length=450), PROFILE) assert metadata["aspect_crop"] is False assert metadata["status"] == "rotation_only" assert image.shape == photo.shape def test_reference_parameters_are_measured_after_straightening(): photo = np.full((400, 300, 3), 127, np.uint8) image, moved, profile = calibrate_reference(photo, landmarks(300, 400, (145, 140), angle=8)) h, w = image.shape[:2] assert w/h == .75 and w < 300 and h < 400 assert profile["brow_9"]["x"] == pytest.approx(profile["chin_152"]["x"], abs=1e-9) assert profile["brow_x_ratio"] == pytest.approx(moved[9, 0]) assert profile["brow_y_ratio"] == pytest.approx(moved[9, 1]) assert profile["reference_width_per_face_length"] == pytest.approx(w/100) assert profile["reference_height_per_face_length"] == pytest.approx(h/100) assert profile["width_per_face_length"] == 2.5 assert profile["height_per_face_length"] == pytest.approx(3/.9) assert image.min() == 127 def test_default_crop_makes_face_ten_percent_smaller_than_three_l(): photo = np.full((700, 600, 3), 127, np.uint8) result, moved, metadata = normalize_portrait(photo, landmarks(600, 700, (300, 280), length=120)) assert metadata["standard_crop"] is True assert result.shape[:2] == (400, 300) assert metadata["face_axis_length_px"] == pytest.approx(120) assert metadata["height_per_face_length"] == pytest.approx(3/.9) assert metadata["width_per_face_length"] == 2.5 old_height = 3*120 assert old_height/result.shape[0] == pytest.approx(.9) def test_phone_orientation_is_applied_before_landmarks(): image = Image.new("RGB", (120, 80), "white") exif = Image.Exif(); exif[274] = 6 buf = io.BytesIO(); image.save(buf, format="JPEG", exif=exif) assert decode_portrait(buf.getvalue()).shape[:2] == (120, 80)