import pytest import numpy as np import math from h07_image_rotation_utils import ImageRotationUtils @pytest.fixture def rot_utils(): return ImageRotationUtils() class TestImageRotationUtils: def test_normalize_angle(self, rot_utils): """Verifies angle normalization to the standard 0-360 degree range.""" assert rot_utils.normalize_angle(360.0) == 0.0 assert rot_utils.normalize_angle(405.0) == 45.0 assert rot_utils.normalize_angle(-90.0) == 270.0 assert rot_utils.normalize_angle(-450.0) == 270.0 assert rot_utils.normalize_angle(45.0) == 45.0 def test_rotate_image(self, rot_utils): """Verifies basic image rotation using OpenCV affine transformations.""" img = np.zeros((10, 10), dtype=np.uint8) img[0, 0] = 255 # 0 degree rotation should return an exact copy rot_0 = rot_utils.rotate_image(img, 0.0) np.testing.assert_array_equal(rot_0, img) # Standard rotation should preserve dimensions rotated = rot_utils.rotate_image(img, 90.0, center=(4.5, 4.5)) assert rotated.shape == (10, 10) def test_kabsch_zero_rotation(self, rot_utils): """Verifies identical point clouds return exactly 0.0 degrees.""" points = np.array([[0, 0], [10, 0], [0, 10], [10, 10]], dtype=np.float64) angle = rot_utils.calculate_rotation_from_points(points, points) assert np.isclose(angle, 0.0) def test_kabsch_known_rotation(self, rot_utils): """Verifies correct angle extraction for standard geometric rotations (90 and 180 deg).""" src_points = np.array([[10, 10], [20, 10], [10, 20], [20, 20]], dtype=np.float64) # 90 degrees Counter-Clockwise mapping: (x, y) -> (-y, x) dst_points_90 = np.array([[-10, 10], [-10, 20], [-20, 10], [-20, 20]], dtype=np.float64) angle_90 = rot_utils.calculate_rotation_from_points(src_points, dst_points_90) assert np.isclose(angle_90, 90.0) # 180 degrees mapping: (x, y) -> (-x, -y) dst_points_180 = np.array([[-10, -10], [-20, -10], [-10, -20], [-20, -20]], dtype=np.float64) angle_180 = rot_utils.calculate_rotation_from_points(src_points, dst_points_180) assert np.isclose(angle_180, 180.0) def test_kabsch_with_translation(self, rot_utils): """Verifies SVD strictly isolates rotation and ignores translation via mean centering.""" src_points = np.array([[0, 0], [10, 0], [0, 10]], dtype=np.float64) # Manually apply a 45 degree rotation AND a massive [100, 50] translation angle_rad = math.radians(45.0) cos_a, sin_a = math.cos(angle_rad), math.sin(angle_rad) R = np.array([[cos_a, -sin_a], [sin_a, cos_a]]) dst_points = np.dot(src_points, R.T) + np.array([100.0, 50.0]) angle = rot_utils.calculate_rotation_from_points(src_points, dst_points) assert np.isclose(angle, 45.0) def test_kabsch_insufficient_points(self, rot_utils): """Verifies safety return when less than 2 points are provided.""" assert rot_utils.calculate_rotation_from_points(np.array([[0, 0]]), np.array([[10, 10]])) == 0.0