import numpy as np import math import cv2 from typing import Optional, Tuple from abc import ABC, abstractmethod class IImageRotationUtils(ABC): @abstractmethod def rotate_image(self, image: np.ndarray, angle: float, center: Optional[Tuple[int, int]] = None) -> np.ndarray: pass @abstractmethod def calculate_rotation_from_points(self, src_points: np.ndarray, dst_points: np.ndarray) -> float: pass @abstractmethod def normalize_angle(self, angle: float) -> float: pass @abstractmethod def compute_rotation_matrix(self, angle: float, center: Tuple[int, int]) -> np.ndarray: pass class ImageRotationUtils(IImageRotationUtils): """H07: Image rotation operations, angle calculations from point shifts.""" def rotate_image(self, image: np.ndarray, angle: float, center: Optional[Tuple[int, int]] = None) -> np.ndarray: h, w = image.shape[:2] if center is None: center = (w // 2, h // 2) return cv2.warpAffine(image, self.compute_rotation_matrix(angle, center), (w, h)) def calculate_rotation_from_points(self, src_points: np.ndarray, dst_points: np.ndarray) -> float: if len(src_points) == 0 or len(dst_points) == 0: return 0.0 sc, dc = np.mean(src_points, axis=0), np.mean(dst_points, axis=0) angles = [] for s, d in zip(src_points - sc, dst_points - dc): if np.linalg.norm(s) > 1e-3 and np.linalg.norm(d) > 1e-3: angles.append(math.atan2(d[1], d[0]) - math.atan2(s[1], s[0])) if not angles: return 0.0 return self.normalize_angle(math.degrees(np.mean(np.unwrap(angles)))) def normalize_angle(self, angle: float) -> float: return angle % 360.0 def compute_rotation_matrix(self, angle: float, center: Tuple[int, int]) -> np.ndarray: return cv2.getRotationMatrix2D(center, -angle, 1.0)