Repository: kornia/kornia

Issue:
Add homography-from-line-correpsondences RANSAC
## 🚀 Feature

Add solver H from line correspondences. 

## Motivation

To have implemented  some nice use-case for SOLD2
## Pitch

<!-- A clear and concise description of what you want to happen. -->

## Alternatives

<!-- A clear and concise description of any alternative solutions or features you've considered, if any. -->

## Additional context

<!-- Add any other context or screenshots about the feature request here. -->

______________________________________________________________________

#### Consider also to contribute to Kornia universe projects :)

<sub>

- [**Tutorials**](https://github.com/kornia/tutorials): our repository containing the tutorials.

</sub>



Discussion:

Resolving patch:
diff --git a/kornia/geometry/homography.py b/kornia/geometry/homography.py
--- a/kornia/geometry/homography.py
+++ b/kornia/geometry/homography.py
@@ -3,18 +3,18 @@
 
 import torch
 
+from kornia.core import Tensor
+from kornia.testing import KORNIA_CHECK_SHAPE
 from kornia.utils import _extract_device_dtype, safe_inverse_with_mask
 
 from .conversions import convert_points_from_homogeneous, convert_points_to_homogeneous
 from .epipolar import normalize_points
 from .linalg import transform_points
 
-TupleTensor = Tuple[torch.Tensor, torch.Tensor]
+TupleTensor = Tuple[Tensor, Tensor]
 
 
-def oneway_transfer_error(
-    pts1: torch.Tensor, pts2: torch.Tensor, H: torch.Tensor, squared: bool = True, eps: float = 1e-8
-) -> torch.Tensor:
+def oneway_transfer_error(pts1: Tensor, pts2: Tensor, H: Tensor, squared: bool = True, eps: float = 1e-8) -> Tensor:
     r"""Return transfer error in image 2 for correspondences given the homography matrix.
 
     Args:
@@ -29,11 +29,7 @@ def oneway_transfer_error(
     Returns:
         the computed distance with shape :math:`(B, N)`.
     """
-    if not isinstance(H, torch.Tensor):
-        raise TypeError(f"H type is not a torch.Tensor. Got {type(H)}")
-
-    if (len(H.shape) != 3) or not H.shape[-2:] == (3, 3):
-        raise ValueError(f"H must be a (*, 3, 3) tensor. Got {H.shape}")
+    KORNIA_CHECK_SHAPE(H, ["B", "3", "3"])
 
     if pts1.size(-1) == 3:
         pts1 = convert_points_from_homogeneous(pts1)
@@ -43,16 +39,14 @@ def oneway_transfer_error(
 
     # From Hartley and Zisserman, Error in one image (4.6)
     # dist = \sum_{i} ( d(x', Hx)**2)
-    pts1_in_2: torch.Tensor = transform_points(H, pts1)
-    error_squared: torch.Tensor = (pts1_in_2 - pts2).pow(2).sum(dim=-1)
+    pts1_in_2: Tensor = transform_points(H, pts1)
+    error_squared: Tensor = (pts1_in_2 - pts2).pow(2).sum(dim=-1)
     if squared:
         return error_squared
     return (error_squared + eps).sqrt()
 
 
-def symmetric_transfer_error(
-    pts1: torch.Tensor, pts2: torch.Tensor, H: torch.Tensor, squared: bool = True, eps: float = 1e-8
-) -> torch.Tensor:
+def symmetric_transfer_error(pts1: Tensor, pts2: Tensor, H: Tensor, squared: bool = True, eps: float = 1e-8) -> Tensor:
     r"""Return Symmetric transfer error for correspondences given the homography matrix.
 
     Args:
@@ -67,12 +61,7 @@ def symmetric_transfer_error(
     Returns:
         the computed distance with shape :math:`(B, N)`.
     """
-    if not isinstance(H, torch.Tensor):
-        raise TypeError(f"H type is not a torch.Tensor. Got {type(H)}")
-
-    if (len(H.shape) != 3) or not H.shape[-2:] == (3, 3):
-        raise ValueError(f"H must be a (*, 3, 3) tensor. Got {H.shape}")
-
+    KORNIA_CHECK_SHAPE(H, ["B", "3", "3"])
     if pts1.size(-1) == 3:
         pts1 = convert_points_from_homogeneous(pts1)
 
@@ -84,18 +73,51 @@ def symmetric_transfer_error(
     # dist = \sum_{i} (d(x, H^-1 x')**2 + d(x', Hx)**2)
     H_inv, good_H = safe_inverse_with_mask(H)
 
-    there: torch.Tensor = oneway_transfer_error(pts1, pts2, H, True, eps)
-    back: torch.Tensor = oneway_transfer_error(pts2, pts1, H_inv, True, eps)
-    good_H_reshape: torch.Tensor = good_H.view(-1, 1).expand_as(there)
+    there: Tensor = oneway_transfer_error(pts1, pts2, H, True, eps)
+    back: Tensor = oneway_transfer_error(pts2, pts1, H_inv, True, eps)
+    good_H_reshape: Tensor = good_H.view(-1, 1).expand_as(there)
     out = (there + back) * good_H_reshape.to(there.dtype) + max_num * (~good_H_reshape).to(there.dtype)
     if squared:
         return out
     return (out + eps).sqrt()
 
 
-def find_homography_dlt(
-    points1: torch.Tensor, points2: torch.Tensor, weights: Optional[torch.Tensor] = None
-) -> torch.Tensor:
+def line_segment_transfer_error_one_way(ls1: Tensor, ls2: Tensor, H: Tensor, squared: bool = False) -> Tensor:
+    r"""Return transfer error in image 2 for line segment correspondences given the homography matrix. Line segment
+    end points are reprojected into image 2, and point-to-line error is calculted w.r.t. line, induced by line
+    segment in image 2. See :cite:`homolines2001` for details.
+
+    Args:
+        ls1: line segment correspondences from the left images with shape
+          (B, N, 2, 2).
+        ls2: line segment correspondences from the right images with shape
+          (B, N, 2, 2).
+        H: Homographies with shape :math:`(B, 3, 3)`.
+        squared: if True (default is False), the squared distance is returned.
+
+    Returns:
+        the computed distance with shape :math:`(B, N)`.
+    """
+    KORNIA_CHECK_SHAPE(H, ["B", "3", "3"])
+    KORNIA_CHECK_SHAPE(ls1, ["B", "N", "2", "2"])
+    KORNIA_CHECK_SHAPE(ls2, ["B", "N", "2", "2"])
+    B, N = ls1.shape[:2]
+    ps1, pe1 = torch.chunk(ls1, dim=2, chunks=2)
+    ps2, pe2 = torch.chunk(ls2, dim=2, chunks=2)
+    ps2_h = convert_points_to_homogeneous(ps2)
+    pe2_h = convert_points_to_homogeneous(pe2)
+    ln2 = ps2_h.cross(pe2_h, dim=3)
+    ps1_in2 = convert_points_to_homogeneous(transform_points(H, ps1))
+    pe1_in2 = convert_points_to_homogeneous(transform_points(H, pe1))
+    er_st1 = (ln2 @ ps1_in2.transpose(-2, -1)).view(B, N).abs()
+    er_end1 = (ln2 @ pe1_in2.transpose(-2, -1)).view(B, N).abs()
+    error = 0.5 * (er_st1 + er_end1)
+    if squared:
+        error = error**2
+    return error
+
+
+def find_homography_dlt(points1: Tensor, points2: Tensor, weights: Optional[Tensor] = None) -> Tensor:
     r"""Compute the homography matrix using the DLT formulation.
 
     The linear system is solved by using the Weighted Least Squares Solution for the 4 Points algorithm.
@@ -110,10 +132,10 @@ def find_homography_dlt(
     """
     if points1.shape != points2.shape:
         raise AssertionError(points1.shape)
-    if not (len(points1.shape) >= 1 and points1.shape[-1] == 2):
-        raise AssertionError(points1.shape)
     if points1.shape[1] < 4:
         raise AssertionError(points1.shape)
+    KORNIA_CHECK_SHAPE(points1, ["B", "N", "2"])
+    KORNIA_CHECK_SHAPE(points2, ["B", "N", "2"])
 
     device, dtype = _extract_device_dtype([points1, points2])
 
@@ -153,8 +175,8 @@ def find_homography_dlt(
 
 
 def find_homography_dlt_iterated(
-    points1: torch.Tensor, points2: torch.Tensor, weights: torch.Tensor, soft_inl_th: float = 3.0, n_iter: int = 5
-) -> torch.Tensor:
+    points1: Tensor, points2: Tensor, weights: Tensor, soft_inl_th: float = 3.0, n_iter: int = 5
+) -> Tensor:
     r"""Compute the homography matrix using the iteratively-reweighted least squares (IRWLS).
 
     The linear system is solved by using the Reweighted Least Squares Solution for the 4 Points algorithm.
@@ -170,17 +192,15 @@ def find_homography_dlt_iterated(
     Returns:
         the computed homography matrix with shape :math:`(B, 3, 3)`.
     """
-    '''Function, which finds homography via iteratively-reweighted
-    least squares ToDo: add citation'''
-    H: torch.Tensor = find_homography_dlt(points1, points2, weights)
+    H: Tensor = find_homography_dlt(points1, points2, weights)
     for _ in range(n_iter - 1):
-        errors: torch.Tensor = symmetric_transfer_error(points1, points2, H, False)
-        weights_new: torch.Tensor = torch.exp(-errors / (2.0 * (soft_inl_th**2)))
+        errors: Tensor = symmetric_transfer_error(points1, points2, H, False)
+        weights_new: Tensor = torch.exp(-errors / (2.0 * (soft_inl_th**2)))
         H = find_homography_dlt(points1, points2, weights_new)
     return H
 
 
-def sample_is_valid_for_homography(points1: torch.Tensor, points2: torch.Tensor) -> torch.Tensor:
+def sample_is_valid_for_homography(points1: Tensor, points2: Tensor) -> Tensor:
     """Function, which implements oriented constraint check from :cite:`Marquez-Neila2015`.
 
     Analogous to https://github.com/opencv/opencv/blob/4.x/modules/calib3d/src/usac/degeneracy.cpp#L88
@@ -194,10 +214,8 @@ def sample_is_valid_for_homography(points1: torch.Tensor, points2: torch.Tensor)
     """
     if points1.shape != points2.shape:
         raise AssertionError(points1.shape)
-    if not (len(points1.shape) >= 1 and points1.shape[-1] == 2):
-        raise AssertionError(points1.shape)
-    if points1.shape[1] != 4:
-        raise AssertionError(points1.shape)
+    KORNIA_CHECK_SHAPE(points1, ["B", "4", "2"])
+    KORNIA_CHECK_SHAPE(points2, ["B", "4", "2"])
     device = points1.device
     idx_perm = torch.tensor([[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]], dtype=torch.long, device=device)
     points_src_h = convert_points_to_homogeneous(points1)
@@ -213,3 +231,95 @@ def sample_is_valid_for_homography(points1: torch.Tensor, points2: torch.Tensor)
     ).sign()
     sample_is_valid = (left_sign == right_sign).view(-1, 4).min(dim=1)[0]
     return sample_is_valid
+
+
+def find_homography_lines_dlt(ls1: Tensor, ls2: Tensor, weights: Optional[Tensor] = None) -> Tensor:
+    r"""Compute the homography matrix using the DLT formulation for line correspondences.
+
+    See :cite:`homolines2001` for details.
+    The linear system is solved by using the Weighted Least Squares Solution for the 4 Line correspondences algorithm.
+    Args:
+        ls1: A set of line segments in the first image with a tensor shape :math:`(B, N, 2, 2)`.
+        ls2: A set of line segments in the second image with a tensor shape :math:`(B, N, 2, 2)`.
+        weights: Tensor containing the weights per point correspondence with a shape of :math:`(B, N)`.
+    Returns:
+        the computed homography matrix with shape :math:`(B, 3, 3)`.
+    """
+    if len(ls1.shape) == 3:
+        ls1 = ls1[None]
+    if len(ls2.shape) == 3:
+        ls2 = ls2[None]
+    KORNIA_CHECK_SHAPE(ls1, ["B", "N", "2", "2"])
+    KORNIA_CHECK_SHAPE(ls2, ["B", "N", "2", "2"])
+    BS, N = ls1.shape[:2]
+    device, dtype = _extract_device_dtype([ls1, ls2])
+
+    points1 = ls1.reshape(BS, 2 * N, 2)
+    points2 = ls2.reshape(BS, 2 * N, 2)
+
+    points1_norm, transform1 = normalize_points(points1)
+    points2_norm, transform2 = normalize_points(points2)
+    lst1, le1 = torch.chunk(points1_norm, dim=1, chunks=2)
+    lst2, le2 = torch.chunk(points2_norm, dim=1, chunks=2)
+
+    xs1, ys1 = torch.chunk(lst1, dim=-1, chunks=2)  # BxNx1
+    xs2, ys2 = torch.chunk(lst2, dim=-1, chunks=2)  # BxNx1
+    xe1, ye1 = torch.chunk(le1, dim=-1, chunks=2)  # BxNx1
+    xe2, ye2 = torch.chunk(le2, dim=-1, chunks=2)  # BxNx1
+
+    A = ys2 - ye2
+    B = xe2 - xs2
+    C = xs2 * ye2 - xe2 * ys2
+
+    eps: float = 1e-8
+
+    # http://diis.unizar.es/biblioteca/00/09/000902.pdf
+    ax = torch.cat([A * xs1, A * ys1, A, B * xs1, B * ys1, B, C * xs1, C * ys1, C], dim=-1)
+    ay = torch.cat([A * xe1, A * ye1, A, B * xe1, B * ye1, B, C * xe1, C * ye1, C], dim=-1)
+    A = torch.cat((ax, ay), dim=-1).reshape(ax.shape[0], -1, ax.shape[-1])
+
+    if weights is None:
+        # All points are equally important
+        A = A.transpose(-2, -1) @ A
+    else:
+        # We should use provided weights
+        if not ((len(weights.shape) == 2) and (weights.shape == ls1.shape[:2])):
+            raise AssertionError(weights.shape)
+        w_diag = torch.diag_embed(weights.unsqueeze(dim=-1).repeat(1, 1, 2).reshape(weights.shape[0], -1))
+        A = A.transpose(-2, -1) @ w_diag @ A
+
+    try:
+        _, _, V = torch.svd(A)
+    except RuntimeError:
+        warnings.warn('SVD did not converge', RuntimeWarning)
+        return torch.empty((points1_norm.size(0), 3, 3), device=device, dtype=dtype)
+
+    H = V[..., -1].view(-1, 3, 3)
+    H = transform2.inverse() @ (H @ transform1)
+    H_norm = H / (H[..., -1:, -1:] + eps)
+    return H_norm
+
+
+def find_homography_lines_dlt_iterated(
+    ls1: Tensor, ls2: Tensor, weights: Tensor, soft_inl_th: float = 4.0, n_iter: int = 5
+) -> Tensor:
+    r"""Compute the homography matrix using the iteratively-reweighted least squares (IRWLS) from line segments. The
+    linear system is solved by using the Reweighted Least Squares Solution for the 4 line segments algorithm.
+
+    Args:
+        ls1: A set of line segments in the first image with a tensor shape :math:`(B, N, 2, 2)`.
+        ls2: A set of line segments in the second image with a tensor shape :math:`(B, N, 2, 2)`.
+        weights: Tensor containing the weights per point correspondence with a shape of :math:`(B, N)`.
+          Used for the first iteration of the IRWLS.
+        soft_inl_th: Soft inlier threshold used for weight calculation.
+        n_iter: number of iterations.
+
+    Returns:
+        the computed homography matrix with shape :math:`(B, 3, 3)`.
+    """
+    H: Tensor = find_homography_lines_dlt(ls1, ls2, weights)
+    for _ in range(n_iter - 1):
+        errors: Tensor = line_segment_transfer_error_one_way(ls1, ls2, H, False)
+        weights_new: Tensor = torch.exp(-errors / (2.0 * (soft_inl_th**2)))
+        H = find_homography_lines_dlt(ls1, ls2, weights_new)
+    return H
diff --git a/kornia/geometry/ransac.py b/kornia/geometry/ransac.py
--- a/kornia/geometry/ransac.py
+++ b/kornia/geometry/ransac.py
@@ -5,13 +5,21 @@
 import torch
 import torch.nn as nn
 
+from kornia.core import Tensor
 from kornia.geometry import (
     find_fundamental,
     find_homography_dlt,
     find_homography_dlt_iterated,
+    find_homography_lines_dlt,
+    find_homography_lines_dlt_iterated,
     symmetrical_epipolar_distance,
 )
-from kornia.geometry.homography import oneway_transfer_error, sample_is_valid_for_homography
+from kornia.geometry.homography import (
+    line_segment_transfer_error_one_way,
+    oneway_transfer_error,
+    sample_is_valid_for_homography,
+)
+from kornia.testing import KORNIA_CHECK_SHAPE
 
 __all__ = ["RANSAC"]
 
@@ -30,7 +38,7 @@ class RANSAC(nn.Module):
         max_local_iterations: number of local optimization (polishing) iterations.
     """
 
-    supported_models = ['homography', 'fundamental']
+    supported_models = ['homography', 'fundamental', 'homography_from_linesegments']
 
     def __init__(
         self,
@@ -54,6 +62,11 @@ def __init__(
             self.minimal_solver = find_homography_dlt  # type: ignore
             self.polisher_solver = find_homography_dlt_iterated  # type: ignore
             self.minimal_sample_size = 4
+        elif model_type == 'homography_from_linesegments':
+            self.error_fn = line_segment_transfer_error_one_way  # type: ignore
+            self.minimal_solver = find_homography_lines_dlt  # type: ignore
+            self.polisher_solver = find_homography_lines_dlt_iterated  # type: ignore
+            self.minimal_sample_size = 4
         elif model_type == 'fundamental':
             self.error_fn = symmetrical_epipolar_distance  # type: ignore
             self.minimal_solver = find_fundamental  # type: ignore
@@ -66,11 +79,11 @@ def __init__(
 
     def sample(
         self, sample_size: int, pop_size: int, batch_size: int, device: torch.device = torch.device('cpu')
-    ) -> torch.Tensor:
+    ) -> Tensor:
         """Minimal sampler, but unlike traditional RANSAC we sample in batches to get benefit of the parallel
         processing, esp.
 
-        on GPU
+        on GPU.
         """
         rand = torch.rand(batch_size, pop_size, device=device)
         _, out = rand.topk(k=sample_size, dim=1)
@@ -84,20 +97,21 @@ def max_samples_by_conf(n_inl: int, num_tc: int, sample_size: int, conf: float)
             return 1.0
         return math.log(1.0 - conf) / math.log(1.0 - math.pow(n_inl / num_tc, sample_size))
 
-    def estimate_model_from_minsample(self, kp1: torch.Tensor, kp2: torch.Tensor) -> torch.Tensor:
+    def estimate_model_from_minsample(self, kp1: Tensor, kp2: Tensor) -> Tensor:
         batch_size, sample_size = kp1.shape[:2]
         H = self.minimal_solver(kp1, kp2, torch.ones(batch_size, sample_size, dtype=kp1.dtype, device=kp1.device))
         return H
 
-    def verify(
-        self, kp1: torch.Tensor, kp2: torch.Tensor, models: torch.Tensor, inl_th: float
-    ) -> Tuple[torch.Tensor, torch.Tensor, float]:
+    def verify(self, kp1: Tensor, kp2: Tensor, models: Tensor, inl_th: float) -> Tuple[Tensor, Tensor, float]:
         if len(kp1.shape) == 2:
             kp1 = kp1[None]
         if len(kp2.shape) == 2:
             kp2 = kp2[None]
         batch_size = models.shape[0]
-        errors = self.error_fn(kp1.expand(batch_size, -1, 2), kp2.expand(batch_size, -1, 2), models)
+        if self.model_type == 'homography_from_linesegments':
+            errors = self.error_fn(kp1.expand(batch_size, -1, 2, 2), kp2.expand(batch_size, -1, 2, 2), models)
+        else:
+            errors = self.error_fn(kp1.expand(batch_size, -1, 2), kp2.expand(batch_size, -1, 2), models)
         inl = errors <= inl_th
         models_score = inl.to(kp1).sum(dim=1)
         best_model_idx = models_score.argmax()
@@ -106,7 +120,7 @@ def verify(
         inliers_best = inl[best_model_idx]
         return model_best, inliers_best, best_model_score
 
-    def remove_bad_samples(self, kp1: torch.Tensor, kp2: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
+    def remove_bad_samples(self, kp1: Tensor, kp2: Tensor) -> Tuple[Tensor, Tensor]:
         """"""
         # ToDo: add (model-specific) verification of the samples,
         # E.g. constraints on not to be a degenerate sample
@@ -115,14 +129,14 @@ def remove_bad_samples(self, kp1: torch.Tensor, kp2: torch.Tensor) -> Tuple[torc
             return kp1[mask], kp2[mask]
         return kp1, kp2
 
-    def remove_bad_models(self, models: torch.Tensor) -> torch.Tensor:
+    def remove_bad_models(self, models: Tensor) -> Tensor:
         # ToDo: add more and better degenerate model rejection
         # For now it is simple and hardcoded
         main_diagonal = torch.diagonal(models, dim1=1, dim2=2)
         mask = main_diagonal.abs().min(dim=1)[0] > 1e-4
         return models[mask]
 
-    def polish_model(self, kp1: torch.Tensor, kp2: torch.Tensor, inliers: torch.Tensor) -> torch.Tensor:
+    def polish_model(self, kp1: Tensor, kp2: Tensor, inliers: Tensor) -> Tensor:
         # TODO: Replace this with MAGSAC++ polisher
         kp1_inl = kp1[inliers][None]
         kp2_inl = kp2[inliers][None]
@@ -132,39 +146,43 @@ def polish_model(self, kp1: torch.Tensor, kp2: torch.Tensor, inliers: torch.Tens
         )
         return model
 
-    def forward(
-        self, kp1: torch.Tensor, kp2: torch.Tensor, weights: Optional[torch.Tensor] = None
-    ) -> Tuple[torch.Tensor, torch.Tensor]:
+    def validate_inputs(self, kp1: Tensor, kp2: Tensor, weights: Optional[Tensor] = None) -> None:
+        if self.model_type in ['homography', 'fundamental']:
+            KORNIA_CHECK_SHAPE(kp1, ["N", "2"])
+            KORNIA_CHECK_SHAPE(kp2, ["N", "2"])
+            if not (kp1.shape[0] == kp2.shape[0]) or (kp1.shape[0] < self.minimal_sample_size):
+                raise ValueError(
+                    f"kp1 and kp2 should be \
+                                 equal shape at at least [{self.minimal_sample_size}, 2], \
+                                 got {kp1.shape}, {kp2.shape}"
+                )
+        if self.model_type == 'homography_from_linesegments':
+            KORNIA_CHECK_SHAPE(kp1, ["N", "2", "2"])
+            KORNIA_CHECK_SHAPE(kp2, ["N", "2", "2"])
+            if not (kp1.shape[0] == kp2.shape[0]) or (kp1.shape[0] < self.minimal_sample_size):
+                raise ValueError(
+                    f"kp1 and kp2 should be \
+                                 equal shape at at least [{self.minimal_sample_size}, 2, 2], \
+                                 got {kp1.shape}, {kp2.shape}"
+                )
+
+    def forward(self, kp1: Tensor, kp2: Tensor, weights: Optional[Tensor] = None) -> Tuple[Tensor, Tensor]:
         r"""Main forward method to execute the RANSAC algorithm.
 
         Args:
-            kp1 (torch.Tensor): source image keypoints :math:`(N, 2)`.
-            kp2 (torch.Tensor): distance image keypoints :math:`(N, 2)`.
-            weights (torch.Tensor): optional correspondences weights. Not used now
+            kp1: source image keypoints :math:`(N, 2)`.
+            kp2: distance image keypoints :math:`(N, 2)`.
+            weights: optional correspondences weights. Not used now.
 
         Returns:
             - Estimated model, shape of :math:`(1, 3, 3)`.
             - The inlier/outlier mask, shape of :math:`(1, N)`, where N is number of input correspondences.
         """
-        if not isinstance(kp1, torch.Tensor):
-            raise TypeError(f"Input kp1 is not torch.Tensor. Got {type(kp1)}")
-        if not isinstance(kp2, torch.Tensor):
-            raise TypeError(f"Input kp2 is not torch.Tensor. Got {type(kp2)}")
-        if not len(kp1.shape) == 2:
-            raise ValueError(f"Invalid kp1 shape, we expect Nx2 Got: {kp1.shape}")
-        if not len(kp2.shape) == 2:
-            raise ValueError(f"Invalid kp2 shape, we expect Nx2 Got: {kp2.shape}")
-        if not (kp1.shape[0] == kp2.shape[0]) or (kp1.shape[0] < self.minimal_sample_size):
-            raise ValueError(
-                f"kp1 and kp2 should be \
-                             equal shape at at least [{self.minimal_sample_size}, 2], \
-                             got {kp1.shape}, {kp2.shape}"
-            )
-
+        self.validate_inputs(kp1, kp2, weights)
         best_score_total: float = float(self.minimal_sample_size)
         num_tc: int = len(kp1)
         best_model_total = torch.zeros(3, 3, dtype=kp1.dtype, device=kp1.device)
-        inliers_best_total: torch.Tensor = torch.zeros(num_tc, 1, device=kp1.device, dtype=torch.bool)
+        inliers_best_total: Tensor = torch.zeros(num_tc, 1, device=kp1.device, dtype=torch.bool)
         for i in range(self.max_iter):
             # Sample minimal samples in batch to estimate models
             idxs = self.sample(self.minimal_sample_size, num_tc, self.batch_size, kp1.device)


Test patch:
diff --git a/test/geometry/test_homography.py b/test/geometry/test_homography.py
--- a/test/geometry/test_homography.py
+++ b/test/geometry/test_homography.py
@@ -9,6 +9,9 @@
 from kornia.geometry.homography import (
     find_homography_dlt,
     find_homography_dlt_iterated,
+    find_homography_lines_dlt,
+    find_homography_lines_dlt_iterated,
+    line_segment_transfer_error_one_way,
     oneway_transfer_error,
     sample_is_valid_for_homography,
     symmetric_transfer_error,
@@ -69,6 +72,41 @@ def test_shift(self, device, dtype):
         assert_close(oneway_transfer_error(pts1, pts2, H), expected, atol=1e-4, rtol=1e-4)
 
 
+class TestLineSegmentOneWayError:
+    def test_smoke(self, device, dtype):
+        ls1 = torch.rand(1, 6, 2, 2, device=device, dtype=dtype)
+        ls2 = torch.rand(1, 6, 2, 2, device=device, dtype=dtype)
+        H = utils.create_random_homography(1, 3).type_as(ls1).to(device)
+        assert line_segment_transfer_error_one_way(ls1, ls2, H).shape == (1, 6)
+
+    def test_batch(self, device, dtype):
+        batch_size = 5
+        ls1 = torch.rand(batch_size, 3, 2, 2, device=device, dtype=dtype)
+        ls2 = torch.rand(batch_size, 3, 2, 2, device=device, dtype=dtype)
+        H = utils.create_random_homography(1, 3).type_as(ls1).to(device)
+        assert line_segment_transfer_error_one_way(ls1, ls2, H).shape == (batch_size, 3)
+
+    def test_gradcheck(self, device):
+        # generate input data
+        batch_size, num_points, num_dims = 2, 3, 2
+        ls1 = torch.rand(batch_size, num_points, num_dims, 2, device=device, dtype=torch.float64, requires_grad=True)
+        ls2 = torch.rand(batch_size, num_points, num_dims, 2, device=device, dtype=torch.float64)
+        H = utils.create_random_homography(batch_size, 3).type_as(ls1).to(device)
+        assert gradcheck(line_segment_transfer_error_one_way, (ls1, ls2, H), raise_exception=True)
+
+    def test_shift(self, device, dtype):
+        pts1 = torch.zeros(3, 2, device=device, dtype=dtype)[None]
+        pts1_end = torch.ones(3, 2, device=device, dtype=dtype)[None]
+        ls1 = torch.stack([pts1, pts1_end], dim=2)
+
+        pts2 = torch.tensor([[1.0, 0.0], [2.0, 0.0], [2.0, 2.0]], device=device, dtype=dtype)[None]
+        pts2_end = pts2 + torch.ones(3, 2, device=device, dtype=dtype)[None]
+        ls2 = torch.stack([pts2, pts2_end], dim=2)
+        H = torch.tensor([[1.0, 0.0, 1.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], dtype=dtype, device=device)[None]
+        expected = torch.tensor([0.0, 1.0, 1.0], device=device, dtype=dtype)[None]
+        assert_close(line_segment_transfer_error_one_way(ls1, ls2, H), expected, atol=1e-4, rtol=1e-4)
+
+
 class TestSymmetricTransferError:
     def test_smoke(self, device, dtype):
         pts1 = torch.rand(1, 6, 2, device=device, dtype=dtype)
@@ -200,6 +238,158 @@ def test_gradcheck(self, device):
             return
 
 
+class TestFindHomographyFromLinesDLT:
+    def test_smoke(self, device, dtype):
+        points1st = torch.rand(1, 4, 2, device=device, dtype=dtype)
+        points1end = torch.rand(1, 4, 2, device=device, dtype=dtype)
+        points2st = torch.rand(1, 4, 2, device=device, dtype=dtype)
+        points2end = torch.rand(1, 4, 2, device=device, dtype=dtype)
+        weights = torch.ones(1, 4, device=device, dtype=dtype)
+        ls1 = torch.stack([points1st, points1end], dim=2)
+        ls2 = torch.stack([points2st, points2end], dim=2)
+        H = find_homography_lines_dlt(ls1, ls2, weights)
+        assert H.shape == (1, 3, 3)
+
+    def test_smoke2(self, device, dtype):
+        points1st = torch.rand(4, 2, device=device, dtype=dtype)
+        points1end = torch.rand(4, 2, device=device, dtype=dtype)
+        points2st = torch.rand(4, 2, device=device, dtype=dtype)
+        points2end = torch.rand(4, 2, device=device, dtype=dtype)
+        ls1 = torch.stack([points1st, points1end], dim=1)
+        ls2 = torch.stack([points2st, points2end], dim=1)
+        H = find_homography_lines_dlt(ls1, ls2, None)
+        assert H.shape == (1, 3, 3)
+
+    def test_nocrash(self, device, dtype):
+        points1st = torch.rand(1, 4, 2, device=device, dtype=dtype)
+        points1end = torch.rand(1, 4, 2, device=device, dtype=dtype)
+        points2st = torch.rand(1, 4, 2, device=device, dtype=dtype)
+        points2end = torch.rand(1, 4, 2, device=device, dtype=dtype)
+        weights = torch.ones(1, 4, device=device, dtype=dtype)
+        points1st[0, 0, 0] = float('nan')
+        ls1 = torch.stack([points1st, points1end], dim=2)
+        ls2 = torch.stack([points2st, points2end], dim=2)
+        H = find_homography_lines_dlt(ls1, ls2, weights)
+        assert H.shape == (1, 3, 3)
+
+    @pytest.mark.parametrize("batch_size, num_points", [(1, 4), (2, 5), (3, 6)])
+    def test_shape(self, batch_size, num_points, device, dtype):
+        B, N = batch_size, num_points
+        points1st = torch.rand(B, N, 2, device=device, dtype=dtype)
+        points1end = torch.rand(B, N, 2, device=device, dtype=dtype)
+        points2st = torch.rand(B, N, 2, device=device, dtype=dtype)
+        points2end = torch.rand(B, N, 2, device=device, dtype=dtype)
+        weights = torch.ones(B, N, device=device, dtype=dtype)
+        ls1 = torch.stack([points1st, points1end], dim=2)
+        ls2 = torch.stack([points2st, points2end], dim=2)
+        H = find_homography_lines_dlt(ls1, ls2, weights)
+        assert H.shape == (B, 3, 3)
+
+    @pytest.mark.parametrize("batch_size, num_points", [(1, 4), (2, 5), (3, 6)])
+    def test_shape_noweights(self, batch_size, num_points, device, dtype):
+        B, N = batch_size, num_points
+        points1st = torch.rand(B, N, 2, device=device, dtype=dtype)
+        points1end = torch.rand(B, N, 2, device=device, dtype=dtype)
+        points2st = torch.rand(B, N, 2, device=device, dtype=dtype)
+        points2end = torch.rand(B, N, 2, device=device, dtype=dtype)
+        ls1 = torch.stack([points1st, points1end], dim=2)
+        ls2 = torch.stack([points2st, points2end], dim=2)
+        H = find_homography_lines_dlt(ls1, ls2, None)
+        assert H.shape == (B, 3, 3)
+
+    @pytest.mark.parametrize("batch_size, num_points", [(1, 4), (2, 5), (3, 6)])
+    def test_points_noweights(self, batch_size, num_points, device, dtype):
+        B, N = batch_size, num_points
+        points1st = torch.rand(B, N, 2, device=device, dtype=dtype)
+        points1end = torch.rand(B, N, 2, device=device, dtype=dtype)
+        points2st = torch.rand(B, N, 2, device=device, dtype=dtype)
+        points2end = torch.rand(B, N, 2, device=device, dtype=dtype)
+        weights = torch.ones(B, N, device=device, dtype=dtype)
+        ls1 = torch.stack([points1st, points1end], dim=2)
+        ls2 = torch.stack([points2st, points2end], dim=2)
+        H_noweights = find_homography_lines_dlt(ls1, ls2, None)
+        H_withweights = find_homography_lines_dlt(ls1, ls2, weights)
+        assert H_noweights.shape == (B, 3, 3) and H_withweights.shape == (B, 3, 3)
+        assert_close(H_noweights, H_withweights, rtol=1e-3, atol=1e-4)
+
+    @pytest.mark.parametrize("batch_size", [1, 2, 5])
+    def test_clean_points(self, batch_size, device, dtype):
+        # generate input data
+        points_src_st = torch.rand(batch_size, 10, 2, device=device, dtype=dtype)
+        points_src_end = torch.rand(batch_size, 10, 2, device=device, dtype=dtype)
+
+        H = kornia.eye_like(3, points_src_st)
+        H = H * 0.3 * torch.rand_like(H)
+        H = H / H[:, 2:3, 2:3]
+        points_dst_st = kornia.geometry.transform_points(H, points_src_st)
+        points_dst_end = kornia.geometry.transform_points(H, points_src_end)
+
+        ls1 = torch.stack([points_src_st, points_src_end], axis=2)
+        ls2 = torch.stack([points_dst_st, points_dst_end], axis=2)
+        # compute transform from source to target
+        dst_homo_src = find_homography_lines_dlt(ls1, ls2, None)
+
+        assert_close(kornia.geometry.transform_points(dst_homo_src, points_src_st), points_dst_st, rtol=1e-3, atol=1e-4)
+
+    @pytest.mark.parametrize("batch_size", [1, 2, 5])
+    def test_clean_points_iter(self, batch_size, device, dtype):
+        # generate input data
+        points_src_st = torch.rand(batch_size, 10, 2, device=device, dtype=dtype)
+        points_src_end = torch.rand(batch_size, 10, 2, device=device, dtype=dtype)
+
+        H = kornia.eye_like(3, points_src_st)
+        H = H * 0.3 * torch.rand_like(H)
+        H = H / H[:, 2:3, 2:3]
+        points_dst_st = kornia.geometry.transform_points(H, points_src_st)
+        points_dst_end = kornia.geometry.transform_points(H, points_src_end)
+
+        ls1 = torch.stack([points_src_st, points_src_end], axis=2)
+        ls2 = torch.stack([points_dst_st, points_dst_end], axis=2)
+        # compute transform from source to target
+        dst_homo_src = find_homography_lines_dlt_iterated(ls1, ls2, None, 5)
+
+        assert_close(kornia.geometry.transform_points(dst_homo_src, points_src_st), points_dst_st, rtol=1e-3, atol=1e-4)
+
+    @pytest.mark.grad
+    @pytest.mark.skipif(torch.__version__ < '1.7', reason="pytorch bug of incopatible types: #33546 fixed in v1.7")
+    def test_gradcheck(self, device):
+        # Save initial seed
+        initial_seed = torch.random.initial_seed()
+        max_number_of_checks = 10
+
+        # Test gradients for a max_number_of_checks times
+        current_seed = initial_seed
+        for i in range(max_number_of_checks):
+            torch.manual_seed(current_seed)
+            points_src_st = torch.rand(1, 10, 2, device=device, dtype=torch.float64, requires_grad=True)
+            points_src_end = torch.rand(1, 10, 2, device=device, dtype=torch.float64, requires_grad=True)
+
+            points_dst_st = torch.rand_like(points_src_st)
+            points_dst_end = torch.rand_like(points_src_end)
+            weights = torch.ones_like(points_src_st)[..., 0]
+            ls1 = torch.stack([points_src_st, points_src_end], axis=2)
+            ls2 = torch.stack([points_dst_st, points_dst_end], axis=2)
+            try:
+                gradcheck(find_homography_lines_dlt, (ls1, ls2, weights), rtol=1e-6, atol=1e-6, raise_exception=True)
+
+            # Gradcheck failed
+            except RuntimeError:
+
+                # All iterations failed
+                if i == max_number_of_checks - 1:
+                    assert gradcheck(
+                        find_homography_lines_dlt, (ls1, ls2, weights), rtol=1e-6, atol=1e-6, raise_exception=True
+                    )
+                # Next iteration
+                else:
+                    current_seed = random.randrange(0xFFFFFFFFFFFFFFFF)
+                    continue
+
+            # Gradcheck succeed
+            torch.manual_seed(initial_seed)
+            return
+
+
 class TestFindHomographyDLTIter:
     def test_smoke(self, device, dtype):
         points1 = torch.rand(1, 4, 2, device=device, dtype=dtype)
diff --git a/test/geometry/test_ransac.py b/test/geometry/test_ransac.py
--- a/test/geometry/test_ransac.py
+++ b/test/geometry/test_ransac.py
@@ -85,6 +85,54 @@ def test_jit(self, device, dtype):
         assert_close(model(points1, points2)[0], model_jit(points1, points2)[0], rtol=1e-4, atol=1e-4)
 
 
+class TestRANSACHomographyLineSegments:
+    def test_smoke(self, device, dtype):
+        torch.random.manual_seed(0)
+        points1 = torch.rand(4, 2, 2, device=device, dtype=dtype)
+        points2 = torch.rand(4, 2, 2, device=device, dtype=dtype)
+        ransac = RANSAC('homography_from_linesegments').to(device=device, dtype=dtype)
+        torch.random.manual_seed(0)
+        H, _ = ransac(points1, points2)
+        assert H.shape == (3, 3)
+
+    @pytest.mark.xfail(reason="might slightly and randomly imprecise due to RANSAC randomness")
+    def test_dirty_points(self, device, dtype):
+        # generate input data
+        torch.random.manual_seed(0)
+
+        H = torch.eye(3, dtype=dtype, device=device)
+        H[:2] = H[:2] + 0.1 * torch.rand_like(H[:2])
+        H[2:, :2] = H[2:, :2] + 0.001 * torch.rand_like(H[2:, :2])
+
+        points_src_st = 100.0 * torch.rand(1, 20, 2, device=device, dtype=dtype)
+        points_src_end = 100.0 * torch.rand(1, 20, 2, device=device, dtype=dtype)
+
+        points_dst_st = transform_points(H[None], points_src_st)
+        points_dst_end = transform_points(H[None], points_src_end)
+
+        # making last point an outlier
+        points_dst_st[:, -1, :] += 800
+        ls1 = torch.stack([points_src_st, points_src_end], dim=2)
+        ls2 = torch.stack([points_dst_st, points_dst_end], dim=2)
+
+        ransac = RANSAC('homography_from_linesegments', inl_th=0.5, max_iter=20).to(device=device, dtype=dtype)
+        # compute transform from source to target
+        dst_homo_src, _ = ransac(ls1[0], ls2[0])
+
+        assert_close(
+            transform_points(dst_homo_src[None], points_src_st[:, :-1]), points_dst_st[:, :-1], rtol=1e-3, atol=1e-3
+        )
+
+    @pytest.mark.skip(reason="find_homography_dlt is using try/except block")
+    def test_jit(self, device, dtype):
+        torch.random.manual_seed(0)
+        points1 = torch.rand(4, 2, 2, device=device, dtype=dtype)
+        points2 = torch.rand(4, 2, 2, device=device, dtype=dtype)
+        model = RANSAC('homography_from_linesegments').to(device=device, dtype=dtype)
+        model_jit = torch.jit.script(RANSAC('homography_from_linesegments').to(device=device, dtype=dtype))
+        assert_close(model(points1, points2)[0], model_jit(points1, points2)[0], rtol=1e-4, atol=1e-4)
+
+
 class TestRANSACFundamental:
     def test_smoke(self, device, dtype):
         torch.random.manual_seed(0)