| """
|
| Perspective Transformation Module
|
| ==================================
|
|
|
| Implements perspective transformation for converting camera view coordinates
|
| to real-world top-down coordinates, essential for accurate speed calculation.
|
|
|
| Authors:
|
| - Abhay Gupta (0205CC221005)
|
| - Aditi Lakhera (0205CC221011)
|
| - Balraj Patel (0205CC221049)
|
| - Bhumika Patel (0205CC221050)
|
|
|
| Mathematical Background:
|
| Perspective transformation uses a 3x3 homography matrix to map points
|
| from one plane to another. This is crucial for converting pixel coordinates
|
| to real-world measurements.
|
| """
|
|
|
| import cv2
|
| import numpy as np
|
| from typing import Tuple
|
| import logging
|
|
|
| logger = logging.getLogger(__name__)
|
|
|
|
|
| class PerspectiveTransformer:
|
| """
|
| Handles perspective transformation between camera view and real-world coordinates.
|
|
|
| This class computes and applies homography transformations to convert
|
| image coordinates to a top-down view with real-world measurements.
|
| """
|
|
|
| def __init__(
|
| self,
|
| source_points: np.ndarray,
|
| target_points: np.ndarray
|
| ):
|
| """
|
| Initialize the perspective transformer.
|
|
|
| Args:
|
| source_points: 4 points in source image (camera view)
|
| Shape: (4, 2) with [[x1,y1], [x2,y2], [x3,y3], [x4,y4]]
|
| target_points: 4 corresponding points in target space (real-world)
|
| Shape: (4, 2) with same format
|
|
|
| Raises:
|
| ValueError: If points are invalid or transformation cannot be computed
|
| """
|
|
|
| self._validate_points(source_points, "source")
|
| self._validate_points(target_points, "target")
|
|
|
|
|
| self.source_pts = source_points.astype(np.float32)
|
| self.target_pts = target_points.astype(np.float32)
|
|
|
|
|
| self.matrix = self._compute_transformation_matrix()
|
|
|
|
|
| self.inverse_matrix = self._compute_inverse_matrix()
|
|
|
| logger.info("Perspective transformer initialized successfully")
|
| logger.debug(f"Source points:\n{self.source_pts}")
|
| logger.debug(f"Target points:\n{self.target_pts}")
|
|
|
| def _validate_points(self, points: np.ndarray, name: str) -> None:
|
| """
|
| Validate point array format and values.
|
|
|
| Args:
|
| points: Points array to validate
|
| name: Name for error messages
|
|
|
| Raises:
|
| ValueError: If points are invalid
|
| """
|
| if not isinstance(points, np.ndarray):
|
| raise ValueError(f"{name} points must be a numpy array")
|
|
|
| if points.shape != (4, 2):
|
| raise ValueError(
|
| f"{name} points must have shape (4, 2), got {points.shape}"
|
| )
|
|
|
| if not np.isfinite(points).all():
|
| raise ValueError(f"{name} points contain invalid values (NaN or Inf)")
|
|
|
| def _compute_transformation_matrix(self) -> np.ndarray:
|
| """
|
| Compute the perspective transformation matrix.
|
|
|
| Returns:
|
| 3x3 homography matrix
|
|
|
| Raises:
|
| ValueError: If transformation cannot be computed
|
| """
|
| try:
|
| matrix = cv2.getPerspectiveTransform(
|
| self.source_pts,
|
| self.target_pts
|
| )
|
|
|
|
|
| if matrix is None or not np.isfinite(matrix).all():
|
| raise ValueError("Invalid transformation matrix computed")
|
|
|
| logger.debug(f"Transformation matrix:\n{matrix}")
|
| return matrix
|
|
|
| except cv2.error as e:
|
| raise ValueError(f"Failed to compute perspective transform: {e}")
|
|
|
| def _compute_inverse_matrix(self) -> np.ndarray:
|
| """
|
| Compute the inverse transformation matrix.
|
|
|
| Returns:
|
| 3x3 inverse homography matrix
|
| """
|
| try:
|
| inverse = cv2.getPerspectiveTransform(
|
| self.target_pts,
|
| self.source_pts
|
| )
|
| return inverse
|
| except Exception as e:
|
| logger.warning(f"Could not compute inverse matrix: {e}")
|
| return None
|
|
|
| def apply_transformation(self, points: np.ndarray) -> np.ndarray:
|
| """
|
| Transform points from source to target coordinate system.
|
|
|
| Args:
|
| points: Array of points to transform
|
| Shape: (N, 2) where N is number of points
|
|
|
| Returns:
|
| Transformed points in target coordinate system
|
| Shape: (N, 2)
|
|
|
| Raises:
|
| ValueError: If points have invalid shape
|
| """
|
|
|
| if points.size == 0:
|
| return points
|
|
|
|
|
| if len(points.shape) != 2 or points.shape[1] != 2:
|
| raise ValueError(
|
| f"Points must have shape (N, 2), got {points.shape}"
|
| )
|
|
|
| try:
|
|
|
| points_reshaped = points.reshape(-1, 1, 2).astype(np.float32)
|
|
|
|
|
| transformed = cv2.perspectiveTransform(
|
| points_reshaped,
|
| self.matrix
|
| )
|
|
|
|
|
| result = transformed.reshape(-1, 2)
|
|
|
| return result
|
|
|
| except Exception as e:
|
| logger.error(f"Error applying transformation: {e}")
|
| raise ValueError(f"Transformation failed: {e}")
|
|
|
| def apply_inverse_transformation(self, points: np.ndarray) -> np.ndarray:
|
| """
|
| Transform points from target back to source coordinate system.
|
|
|
| Args:
|
| points: Array of points in target coordinates
|
| Shape: (N, 2)
|
|
|
| Returns:
|
| Points in source coordinate system
|
| Shape: (N, 2)
|
|
|
| Raises:
|
| ValueError: If inverse matrix not available or transformation fails
|
| """
|
| if self.inverse_matrix is None:
|
| raise ValueError("Inverse transformation matrix not available")
|
|
|
| if points.size == 0:
|
| return points
|
|
|
| try:
|
| points_reshaped = points.reshape(-1, 1, 2).astype(np.float32)
|
| transformed = cv2.perspectiveTransform(
|
| points_reshaped,
|
| self.inverse_matrix
|
| )
|
| return transformed.reshape(-1, 2)
|
|
|
| except Exception as e:
|
| logger.error(f"Error applying inverse transformation: {e}")
|
| raise ValueError(f"Inverse transformation failed: {e}")
|
|
|
| def transform_single_point(self, x: float, y: float) -> Tuple[float, float]:
|
| """
|
| Transform a single point (convenience method).
|
|
|
| Args:
|
| x: X coordinate in source system
|
| y: Y coordinate in source system
|
|
|
| Returns:
|
| Tuple of (x, y) in target system
|
| """
|
| point = np.array([[x, y]], dtype=np.float32)
|
| transformed = self.apply_transformation(point)
|
| return tuple(transformed[0])
|
|
|
| def get_transformation_matrix(self) -> np.ndarray:
|
| """
|
| Get the transformation matrix.
|
|
|
| Returns:
|
| 3x3 homography matrix
|
| """
|
| return self.matrix.copy()
|
|
|
| def get_scale_factors(self) -> Tuple[float, float]:
|
| """
|
| Estimate scale factors in x and y directions.
|
|
|
| Returns:
|
| Tuple of (scale_x, scale_y) representing pixels per meter
|
| """
|
|
|
| source_width = np.linalg.norm(self.source_pts[1] - self.source_pts[0])
|
| source_height = np.linalg.norm(self.source_pts[3] - self.source_pts[0])
|
|
|
| target_width = np.linalg.norm(self.target_pts[1] - self.target_pts[0])
|
| target_height = np.linalg.norm(self.target_pts[3] - self.target_pts[0])
|
|
|
| scale_x = source_width / target_width if target_width > 0 else 1.0
|
| scale_y = source_height / target_height if target_height > 0 else 1.0
|
|
|
| return scale_x, scale_y
|
|
|