OpenCV provides:
getRotationMatrix2D
to get a 2x3 transformation matrix (rotation, scale, shift) defined by center
, angle
and scale
getAffineTransform
to get a 2x3 transformation matrix (rotation, scale, shift, sheer) defined by three pairs of points.I'd like to get a transformation matrix with rotation, scale, and shift (i.e. no sheer) from two pairs of points.
Here is my current implementation, which works, but it's way too complex for my taste:
from typing import Tuple, List
import cv2
import numpy as np
import numpy.typing
def _third_triangle_point(p1: Tuple[float, float], p2: Tuple[float, float]) -> Tuple[float, float]:
"""Calculate the third point of an isosceles right-angled triangle."""
p1_arr = np.array(p1, dtype=np.float32)
p2_arr = np.array(p2, dtype=np.float32)
diff = p2_arr - p1_arr
perpendicular = np.array((diff[1], -diff[0]), dtype=np.float32)
result = p1_arr + perpendicular
return result[0], result[1]
def _stack_points(points: List[Tuple[float, float]]) -> np.typing.NDArray[np.float32]:
return np.vstack([np.array(p, dtype=np.float32) for p in points])
def get_transformation_between_two_point_pairs(
src: Tuple[Tuple[float, float], Tuple[float, float]],
dst: Tuple[Tuple[float, float], Tuple[float, float]]
) -> np.typing.NDArray[np.float32]:
# cv2.getAffineTransform takes three point pairs.
# It supports rotation, translation, scaling, and shearing.
# We don't need the shearing,
# so we invent a third point with a stable relation to the given two.
return cv2.getAffineTransform( # type: ignore
_stack_points([src[0], src[1], _third_triangle_point(src[0], src[1])]),
_stack_points([dst[0], dst[1], _third_triangle_point(dst[0], dst[1])])
)
print(get_transformation_between_two_point_pairs(((10, 10), (17, 23)), ((30, 30), (70, 30))))
[[ 1.28440367 2.3853211 -6.69724771]
[-2.3853211 1.28440367 41.00917431]]
Is there a simpler way to achieve the same result?
The solution is simple, one can just use estimateAffinePartial2D
:
from typing import Tuple
import cv2
import numpy as np
import numpy.typing
def get_transformation_between_two_point_pairs(
src: Tuple[Tuple[float, float], Tuple[float, float]],
dst: Tuple[Tuple[float, float], Tuple[float, float]]
) -> np.typing.NDArray[np.float32]:
return cv2.estimateAffinePartial2D(np.array([src[0], src[1]]), np.array([dst[0], dst[1]]))[0] # type: ignore
print(get_transformation_between_two_point_pairs(((10, 10), (17, 23)), ((30, 30), (70, 30))))
[[ 1.28440367 2.3853211 -6.69724771]
[-2.3853211 1.28440367 41.00917431]]