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