def test_homography_warper(self, batch_size, device, dtype): # generate input data height, width = 128, 64 eye_size = 3 # identity 3x3 patch_src = torch.ones(batch_size, 1, height, width, device=device, dtype=dtype) # create base homography dst_homo_src = utils.create_eye_batch(batch_size, eye_size, device=device, dtype=dtype) # instantiate warper warper = kornia.geometry.transform.HomographyWarper(height, width, align_corners=True) for _ in range(self.num_tests): # generate homography noise homo_delta = torch.rand_like(dst_homo_src) * 0.3 dst_homo_src_i = dst_homo_src + homo_delta # transform the points from dst to ref patch_dst = warper(patch_src, dst_homo_src_i) patch_dst_to_src = warper(patch_dst, _torch_inverse_cast(dst_homo_src_i)) # same transform precomputing the grid warper.precompute_warp_grid(_torch_inverse_cast(dst_homo_src_i)) patch_dst_to_src_precomputed = warper(patch_dst) assert_close(patch_dst_to_src_precomputed, patch_dst_to_src, atol=1e-4, rtol=1e-4) # projected should be equal as initial error = utils.compute_patch_error(patch_src, patch_dst_to_src, height, width) assert error.item() < self.threshold # check functional api patch_dst_to_src_functional = kornia.geometry.transform.homography_warp( patch_dst, _torch_inverse_cast(dst_homo_src_i), (height, width), align_corners=True) assert_close(patch_dst_to_src, patch_dst_to_src_functional, atol=1e-4, rtol=1e-4)
def normalize_homography3d( dst_pix_trans_src_pix: torch.Tensor, dsize_src: Tuple[int, int, int], dsize_dst: Tuple[int, int, int] ) -> torch.Tensor: r"""Normalize a given homography in pixels to [-1, 1]. Args: dst_pix_trans_src_pix: homography/ies from source to destination to be normalized. :math:`(B, 4, 4)` dsize_src: size of the source image (depth, height, width). dsize_src: size of the destination image (depth, height, width). Returns: the normalized homography. Shape: Output: :math:`(B, 4, 4)` """ if not isinstance(dst_pix_trans_src_pix, torch.Tensor): raise TypeError(f"Input type is not a torch.Tensor. Got {type(dst_pix_trans_src_pix)}") if not (len(dst_pix_trans_src_pix.shape) == 3 or dst_pix_trans_src_pix.shape[-2:] == (4, 4)): raise ValueError(f"Input dst_pix_trans_src_pix must be a Bx3x3 tensor. Got {dst_pix_trans_src_pix.shape}") # source and destination sizes src_d, src_h, src_w = dsize_src dst_d, dst_h, dst_w = dsize_dst # compute the transformation pixel/norm for src/dst src_norm_trans_src_pix: torch.Tensor = normal_transform_pixel3d(src_d, src_h, src_w).to(dst_pix_trans_src_pix) src_pix_trans_src_norm = _torch_inverse_cast(src_norm_trans_src_pix) dst_norm_trans_dst_pix: torch.Tensor = normal_transform_pixel3d(dst_d, dst_h, dst_w).to(dst_pix_trans_src_pix) # compute chain transformations dst_norm_trans_src_norm: torch.Tensor = dst_norm_trans_dst_pix @ (dst_pix_trans_src_pix @ src_pix_trans_src_norm) return dst_norm_trans_src_norm
def denormalize_homography( dst_pix_trans_src_pix: torch.Tensor, dsize_src: Tuple[int, int], dsize_dst: Tuple[int, int] ) -> torch.Tensor: r"""De-normalize a given homography in pixels from [-1, 1] to actual height and width. Args: dst_pix_trans_src_pix: homography/ies from source to destination to be denormalized. :math:`(B, 3, 3)` dsize_src: size of the source image (height, width). dsize_dst: size of the destination image (height, width). Returns: the denormalized homography of shape :math:`(B, 3, 3)`. """ if not isinstance(dst_pix_trans_src_pix, torch.Tensor): raise TypeError(f"Input type is not a torch.Tensor. Got {type(dst_pix_trans_src_pix)}") if not (len(dst_pix_trans_src_pix.shape) == 3 or dst_pix_trans_src_pix.shape[-2:] == (3, 3)): raise ValueError(f"Input dst_pix_trans_src_pix must be a Bx3x3 tensor. Got {dst_pix_trans_src_pix.shape}") # source and destination sizes src_h, src_w = dsize_src dst_h, dst_w = dsize_dst # compute the transformation pixel/norm for src/dst src_norm_trans_src_pix: torch.Tensor = normal_transform_pixel(src_h, src_w).to(dst_pix_trans_src_pix) dst_norm_trans_dst_pix: torch.Tensor = normal_transform_pixel(dst_h, dst_w).to(dst_pix_trans_src_pix) dst_denorm_trans_dst_pix = _torch_inverse_cast(dst_norm_trans_dst_pix) # compute chain transformations dst_norm_trans_src_norm: torch.Tensor = dst_denorm_trans_dst_pix @ (dst_pix_trans_src_pix @ src_norm_trans_src_pix) return dst_norm_trans_src_norm
def normalize_homography( dst_pix_trans_src_pix: torch.Tensor, dsize_src: Tuple[int, int], dsize_dst: Tuple[int, int] ) -> torch.Tensor: r"""Normalize a given homography in pixels to [-1, 1]. Args: dst_pix_trans_src_pix (torch.Tensor): homography/ies from source to destination to be normalized. :math:`(B, 3, 3)` dsize_src (tuple): size of the source image (height, width). dsize_dst (tuple): size of the destination image (height, width). Returns: torch.Tensor: the normalized homography of shape :math:`(B, 3, 3)`. """ check_is_tensor(dst_pix_trans_src_pix) if not (len(dst_pix_trans_src_pix.shape) == 3 or dst_pix_trans_src_pix.shape[-2:] == (3, 3)): raise ValueError( "Input dst_pix_trans_src_pix must be a Bx3x3 tensor. Got {}".format(dst_pix_trans_src_pix.shape) ) # source and destination sizes src_h, src_w = dsize_src dst_h, dst_w = dsize_dst # compute the transformation pixel/norm for src/dst src_norm_trans_src_pix: torch.Tensor = normal_transform_pixel(src_h, src_w).to(dst_pix_trans_src_pix) src_pix_trans_src_norm = _torch_inverse_cast(src_norm_trans_src_pix) dst_norm_trans_dst_pix: torch.Tensor = normal_transform_pixel(dst_h, dst_w).to(dst_pix_trans_src_pix) # compute chain transformations dst_norm_trans_src_norm: torch.Tensor = dst_norm_trans_dst_pix @ (dst_pix_trans_src_pix @ src_pix_trans_src_norm) return dst_norm_trans_src_norm
def warp_affine3d( src: torch.Tensor, M: torch.Tensor, dsize: Tuple[int, int, int], flags: str = 'bilinear', padding_mode: str = 'zeros', align_corners: bool = True, ) -> torch.Tensor: r"""Apply a projective transformation a to 3d tensor. .. warning:: This API signature it is experimental and might suffer some changes in the future. Args: src : input tensor of shape :math:`(B, C, D, H, W)`. M: projective transformation matrix of shape :math:`(B, 3, 4)`. dsize: size of the output image (depth, height, width). mode: interpolation mode to calculate output values ``'bilinear'`` | ``'nearest'``. padding_mode: padding mode for outside grid values ``'zeros'`` | ``'border'`` | ``'reflection'``. align_corners : mode for grid_generation. Returns: torch.Tensor: the warped 3d tensor with shape :math:`(B, C, D, H, W)`. .. note:: This function is often used in conjunction with :func:`get_perspective_transform3d`. """ if len(src.shape) != 5: raise AssertionError(src.shape) if not (len(M.shape) == 3 and M.shape[-2:] == (3, 4)): raise AssertionError(M.shape) if len(dsize) != 3: raise AssertionError(dsize) B, C, D, H, W = src.size() size_src: Tuple[int, int, int] = (D, H, W) size_out: Tuple[int, int, int] = dsize M_4x4 = convert_affinematrix_to_homography3d(M) # Bx4x4 # we need to normalize the transformation since grid sample needs -1/1 coordinates dst_norm_trans_src_norm: torch.Tensor = normalize_homography3d( M_4x4, size_src, size_out) # Bx4x4 src_norm_trans_dst_norm = _torch_inverse_cast(dst_norm_trans_src_norm) P_norm: torch.Tensor = src_norm_trans_dst_norm[:, :3] # Bx3x4 # compute meshgrid and apply to input dsize_out: List[int] = [B, C] + list(size_out) grid = torch.nn.functional.affine_grid(P_norm, dsize_out, align_corners=align_corners) return torch.nn.functional.grid_sample(src, grid, align_corners=align_corners, mode=flags, padding_mode=padding_mode)
def test_values(self, device, dtype): x = torch.tensor([[4.0, 7.0], [2.0, 6.0]], device=device, dtype=dtype) y_expected = torch.tensor([[0.6, -0.7], [-0.2, 0.4]], device=device, dtype=dtype) y = _torch_inverse_cast(x) assert_close(y, y_expected)
def get_projective_transform(center: torch.Tensor, angles: torch.Tensor, scales: torch.Tensor) -> torch.Tensor: r"""Calculate the projection matrix for a 3D rotation. .. warning:: This API signature it is experimental and might suffer some changes in the future. The function computes the projection matrix given the center and angles per axis. Args: center: center of the rotation (x,y,z) in the source with shape :math:`(B, 3)`. angles: angle axis vector containing the rotation angles in degrees in the form of (rx, ry, rz) with shape :math:`(B, 3)`. Internally it calls Rodrigues to compute the rotation matrix from axis-angle. scales: scale factor for x-y-z-directions with shape :math:`(B, 3)`. Returns: the projection matrix of 3D rotation with shape :math:`(B, 3, 4)`. .. note:: This function is often used in conjunction with :func:`warp_affine3d`. """ if not (len(center.shape) == 2 and center.shape[-1] == 3): raise AssertionError(center.shape) if not (len(angles.shape) == 2 and angles.shape[-1] == 3): raise AssertionError(angles.shape) if center.device != angles.device: raise AssertionError(center.device, angles.device) if center.dtype != angles.dtype: raise AssertionError(center.dtype, angles.dtype) # create rotation matrix angle_axis_rad: torch.Tensor = K.deg2rad(angles) rmat: torch.Tensor = K.angle_axis_to_rotation_matrix( angle_axis_rad) # Bx3x3 scaling_matrix: torch.Tensor = K.eye_like(3, rmat) scaling_matrix = scaling_matrix * scales.unsqueeze(dim=1) rmat = rmat @ scaling_matrix.to(rmat) # define matrix to move forth and back to origin from_origin_mat = torch.eye(4)[None].repeat(rmat.shape[0], 1, 1).type_as(center) # Bx4x4 from_origin_mat[..., :3, -1] += center to_origin_mat = from_origin_mat.clone() to_origin_mat = _torch_inverse_cast(from_origin_mat) # append translation with zeros proj_mat = projection_from_Rt(rmat, torch.zeros_like(center)[..., None]) # Bx3x4 # chain 4x4 transforms proj_mat = convert_affinematrix_to_homography3d(proj_mat) # Bx4x4 proj_mat = from_origin_mat @ proj_mat @ to_origin_mat return proj_mat[..., :3, :] # Bx3x4
def compute_inverse_transformation(self, transform: torch.Tensor): """Compute the inverse transform of given transformation matrices.""" return _torch_inverse_cast(transform)
def test_smoke(self, device, dtype, input_shape): x = torch.rand(input_shape, device=device, dtype=dtype) y = _torch_inverse_cast(x) assert y.shape == x.shape
def warp_perspective( src: torch.Tensor, M: torch.Tensor, dsize: Tuple[int, int], mode: str = 'bilinear', padding_mode: str = 'zeros', align_corners: Optional[bool] = None, ) -> torch.Tensor: r"""Applies a perspective transformation to an image. .. image:: https://kornia-tutorials.readthedocs.io/en/latest/_images/warp_perspective_10_2.png The function warp_perspective transforms the source image using the specified matrix: .. math:: \text{dst} (x, y) = \text{src} \left( \frac{M^{-1}_{11} x + M^{-1}_{12} y + M^{-1}_{13}}{M^{-1}_{31} x + M^{-1}_{32} y + M^{-1}_{33}} , \frac{M^{-1}_{21} x + M^{-1}_{22} y + M^{-1}_{23}}{M^{-1}_{31} x + M^{-1}_{32} y + M^{-1}_{33}} \right ) Args: src: input image with shape :math:`(B, C, H, W)`. M: transformation matrix with shape :math:`(B, 3, 3)`. dsize: size of the output image (height, width). mode: interpolation mode to calculate output values ``'bilinear'`` | ``'nearest'``. padding_mode: padding mode for outside grid values ``'zeros'`` | ``'border'`` | ``'reflection'``. align_corners(bool, optional): interpolation flag. Returns: the warped input image :math:`(B, C, H, W)`. Example: >>> img = torch.rand(1, 4, 5, 6) >>> H = torch.eye(3)[None] >>> out = warp_perspective(img, H, (4, 2), align_corners=True) >>> print(out.shape) torch.Size([1, 4, 4, 2]) .. note:: This function is often used in conjuntion with :func:`get_perspective_transform`. .. note:: See a working example `here <https://kornia-tutorials.readthedocs.io/en/ latest/warp_perspective.html>`_. """ if not isinstance(src, torch.Tensor): raise TypeError("Input src type is not a torch.Tensor. Got {}".format( type(src))) if not isinstance(M, torch.Tensor): raise TypeError("Input M type is not a torch.Tensor. Got {}".format( type(M))) if not len(src.shape) == 4: raise ValueError("Input src must be a BxCxHxW tensor. Got {}".format( src.shape)) if not (len(M.shape) == 3 and M.shape[-2:] == (3, 3)): raise ValueError("Input M must be a Bx3x3 tensor. Got {}".format( M.shape)) # TODO: remove the statement below in kornia v0.6 if align_corners is None: message: str = ( "The align_corners default value has been changed. By default now is set True " "in order to match cv2.warpPerspective. In case you want to keep your previous " "behaviour set it to False. This warning will disappear in kornia > v0.6." ) warnings.warn(message) # set default value for align corners align_corners = True B, C, H, W = src.size() h_out, w_out = dsize # we normalize the 3x3 transformation matrix and convert to 3x4 dst_norm_trans_src_norm: torch.Tensor = normalize_homography( M, (H, W), (h_out, w_out)) # Bx3x3 src_norm_trans_dst_norm = _torch_inverse_cast( dst_norm_trans_src_norm) # Bx3x3 # this piece of code substitutes F.affine_grid since it does not support 3x3 grid = (create_meshgrid(h_out, w_out, normalized_coordinates=True, device=src.device).to(src.dtype).repeat( B, 1, 1, 1)) grid = transform_points(src_norm_trans_dst_norm[:, None, None], grid) return F.grid_sample(src, grid, align_corners=align_corners, mode=mode, padding_mode=padding_mode)
def warp_affine( src: torch.Tensor, M: torch.Tensor, dsize: Tuple[int, int], mode: str = 'bilinear', padding_mode: str = 'zeros', align_corners: Optional[bool] = None, ) -> torch.Tensor: r"""Applies an affine transformation to a tensor. .. image:: _static/img/warp_affine.png The function warp_affine transforms the source tensor using the specified matrix: .. math:: \text{dst}(x, y) = \text{src} \left( M_{11} x + M_{12} y + M_{13} , M_{21} x + M_{22} y + M_{23} \right ) Args: src: input tensor of shape :math:`(B, C, H, W)`. M: affine transformation of shape :math:`(B, 2, 3)`. dsize: size of the output image (height, width). mode: interpolation mode to calculate output values ``'bilinear'`` | ``'nearest'``. padding_mode (str): padding mode for outside grid values ``'zeros'`` | ``'border'`` | ``'reflection'``. align_corners : mode for grid_generation. Returns: the warped tensor with shape :math:`(B, C, H, W)`. Example: >>> img = torch.rand(1, 4, 5, 6) >>> A = torch.eye(2, 3)[None] >>> out = warp_affine(img, A, (4, 2), align_corners=True) >>> print(out.shape) torch.Size([1, 4, 4, 2]) .. note:: This function is often used in conjuntion with :func:`get_rotation_matrix2d`, :func:`get_shear_matrix2d`, :func:`get_affine_matrix2d`, :func:`invert_affine_transform`. .. note:: See a working example `here <https://kornia.readthedocs.io/en/latest/ tutorials/warp_affine.html>`__. """ if not isinstance(src, torch.Tensor): raise TypeError("Input src type is not a torch.Tensor. Got {}".format( type(src))) if not isinstance(M, torch.Tensor): raise TypeError("Input M type is not a torch.Tensor. Got {}".format( type(M))) if not len(src.shape) == 4: raise ValueError("Input src must be a BxCxHxW tensor. Got {}".format( src.shape)) if not (len(M.shape) == 3 or M.shape[-2:] == (2, 3)): raise ValueError("Input M must be a Bx2x3 tensor. Got {}".format( M.shape)) # TODO: remove the statement below in kornia v0.6 if align_corners is None: message: str = ( "The align_corners default value has been changed. By default now is set True " "in order to match cv2.warpAffine. In case you want to keep your previous " "behaviour set it to False. This warning will disappear in kornia > v0.6." ) warnings.warn(message) # set default value for align corners align_corners = True B, C, H, W = src.size() # we generate a 3x3 transformation matrix from 2x3 affine M_3x3: torch.Tensor = convert_affinematrix_to_homography(M) dst_norm_trans_src_norm: torch.Tensor = normalize_homography( M_3x3, (H, W), dsize) # src_norm_trans_dst_norm = torch.inverse(dst_norm_trans_src_norm) src_norm_trans_dst_norm = _torch_inverse_cast(dst_norm_trans_src_norm) grid = F.affine_grid(src_norm_trans_dst_norm[:, :2, :], [B, C, dsize[0], dsize[1]], align_corners=align_corners) return F.grid_sample(src, grid, align_corners=align_corners, mode=mode, padding_mode=padding_mode)
def warp_affine( src: torch.Tensor, M: torch.Tensor, dsize: Tuple[int, int], mode: str = 'bilinear', padding_mode: str = 'zeros', align_corners: bool = True, ) -> torch.Tensor: r"""Apply an affine transformation to a tensor. .. image:: _static/img/warp_affine.png The function warp_affine transforms the source tensor using the specified matrix: .. math:: \text{dst}(x, y) = \text{src} \left( M_{11} x + M_{12} y + M_{13} , M_{21} x + M_{22} y + M_{23} \right ) Args: src: input tensor of shape :math:`(B, C, H, W)`. M: affine transformation of shape :math:`(B, 2, 3)`. dsize: size of the output image (height, width). mode: interpolation mode to calculate output values ``'bilinear'`` | ``'nearest'``. padding_mode (str): padding mode for outside grid values ``'zeros'`` | ``'border'`` | ``'reflection'``. align_corners : mode for grid_generation. Returns: the warped tensor with shape :math:`(B, C, H, W)`. .. note:: This function is often used in conjunction with :func:`get_rotation_matrix2d`, :func:`get_shear_matrix2d`, :func:`get_affine_matrix2d`, :func:`invert_affine_transform`. .. note:: See a working example `here <https://kornia-tutorials.readthedocs.io/en/latest/ rotate_affine.html>`__. Example: >>> img = torch.rand(1, 4, 5, 6) >>> A = torch.eye(2, 3)[None] >>> out = warp_affine(img, A, (4, 2), align_corners=True) >>> print(out.shape) torch.Size([1, 4, 4, 2]) """ if not isinstance(src, torch.Tensor): raise TypeError(f"Input src type is not a torch.Tensor. Got {type(src)}") if not isinstance(M, torch.Tensor): raise TypeError(f"Input M type is not a torch.Tensor. Got {type(M)}") if not len(src.shape) == 4: raise ValueError(f"Input src must be a BxCxHxW tensor. Got {src.shape}") if not (len(M.shape) == 3 or M.shape[-2:] == (2, 3)): raise ValueError(f"Input M must be a Bx2x3 tensor. Got {M.shape}") B, C, H, W = src.size() # we generate a 3x3 transformation matrix from 2x3 affine M_3x3: torch.Tensor = convert_affinematrix_to_homography(M) dst_norm_trans_src_norm: torch.Tensor = normalize_homography(M_3x3, (H, W), dsize) # src_norm_trans_dst_norm = torch.inverse(dst_norm_trans_src_norm) src_norm_trans_dst_norm = _torch_inverse_cast(dst_norm_trans_src_norm) grid = F.affine_grid(src_norm_trans_dst_norm[:, :2, :], [B, C, dsize[0], dsize[1]], align_corners=align_corners) return F.grid_sample(src, grid, align_corners=align_corners, mode=mode, padding_mode=padding_mode)
def warp_affine3d( src: torch.Tensor, M: torch.Tensor, dsize: Tuple[int, int, int], flags: str = 'bilinear', padding_mode: str = 'zeros', align_corners: Optional[bool] = None, ) -> torch.Tensor: r"""Applies a projective transformation a to 3d tensor. .. warning:: This API signature it is experimental and might suffer some changes in the future. Args: src : input tensor of shape :math:`(B, C, D, H, W)`. M: projective transformation matrix of shape :math:`(B, 3, 4)`. dsize: size of the output image (depth, height, width). mode: interpolation mode to calculate output values ``'bilinear'`` | ``'nearest'``. padding_mode: padding mode for outside grid values ``'zeros'`` | ``'border'`` | ``'reflection'``. align_corners : mode for grid_generation. Returns: torch.Tensor: the warped 3d tensor with shape :math:`(B, C, D, H, W)`. .. note:: This function is often used in conjuntion with :func:`get_perspective_transform3d`. """ assert len(src.shape) == 5, src.shape assert len(M.shape) == 3 and M.shape[-2:] == (3, 4), M.shape assert len(dsize) == 3, dsize B, C, D, H, W = src.size() # TODO: remove the statement below in kornia v0.6 if align_corners is None: message: str = ( "The align_corners default value has been changed. By default now is set True " "in order to match cv2.warpAffine. In case you want to keep your previous " "behaviour set it to False. This warning will disappear in kornia > v0.6." ) warnings.warn(message) # set default value for align corners align_corners = True size_src: Tuple[int, int, int] = (D, H, W) size_out: Tuple[int, int, int] = dsize M_4x4 = convert_affinematrix_to_homography3d(M) # Bx4x4 # we need to normalize the transformation since grid sample needs -1/1 coordinates dst_norm_trans_src_norm: torch.Tensor = normalize_homography3d( M_4x4, size_src, size_out) # Bx4x4 src_norm_trans_dst_norm = _torch_inverse_cast(dst_norm_trans_src_norm) P_norm: torch.Tensor = src_norm_trans_dst_norm[:, :3] # Bx3x4 # compute meshgrid and apply to input dsize_out: List[int] = [B, C] + list(size_out) grid = torch.nn.functional.affine_grid(P_norm, dsize_out, align_corners=align_corners) return torch.nn.functional.grid_sample(src, grid, align_corners=align_corners, mode=flags, padding_mode=padding_mode)
def _get_inverse_transformation(cls, transform: torch.Tensor) -> torch.Tensor: return _torch_inverse_cast(transform)