Commit 76e190e6 authored by Jay's avatar Jay Committed by jay
Browse files

Added homography class with quality and diagnostic elements

parent 59d6ca0d
Loading
Loading
Loading
Loading
+83 −13
Original line number Diff line number Diff line
@@ -2,11 +2,9 @@ import os
import warnings

import networkx as nx
import numpy as np
import pandas as pd
import cv2
from pysal.cg.shapes import Polygon
import numpy as np

from scipy.misc import bytescale

from autocnet.control.control import C
@@ -16,9 +14,9 @@ from autocnet.matcher.matcher import FlannMatcher
from autocnet.matcher import feature_extractor as fe
from autocnet.matcher import outlier_detector as od
from autocnet.matcher import subpixel as sp
from autocnet.matcher.homography import Homography
from autocnet.cg.cg import convex_hull_ratio, overlapping_polygon_area
from autocnet.vis.graph_view import plot_node, plot_edge

from autocnet.vis.graph_view import plot_node, plot_edge, plot_graph

class Edge(object):
    """
@@ -105,7 +103,35 @@ class Edge(object):
        else:
            raise AttributeError('No matches have been computed for this edge.')

    def compute_homography(self, outlier_algorithm=cv2.RANSAC, clean_keys=[]):
    def compute_fundamental_matrix(self, clean_keys=[], **kwargs):

        if hasattr(self, 'matches'):
            matches = self.matches
        else:
            raise AttributeError('Matches have not been computed for this edge')

        if clean_keys:
            mask = np.prod([self._mask_arrays[i] for i in clean_keys], axis=0, dtype=np.bool)
            matches = matches[mask]
            full_mask = np.where(mask == True)

        s_keypoints = self.source.keypoints.iloc[matches['source_idx'].values]
        d_keypoints = self.destination.keypoints.iloc[matches['destination_idx'].values]

        transformation_matrix, fundam_mask = od.compute_fundamental_matrix(s_keypoints[['x', 'y']].values,
                                                                           d_keypoints[['x', 'y']].values,
                                                                           **kwargs)

        fundam_mask = fundam_mask.ravel()
        # Convert the truncated RANSAC mask back into a full length mask
        if clean_keys:
            mask[full_mask] = fundam_mask
        else:
            mask = fundam_mask
        self.masks = ('fundamental', mask)
        self.fundamental_matrix = transformation_matrix

    def compute_homography(self, method='ransac', clean_keys=[], **kwargs):
        """
        For each edge in the (sub) graph, compute the homography
        Parameters
@@ -139,7 +165,8 @@ class Edge(object):
        d_keypoints = self.destination.keypoints.iloc[matches['destination_idx'].values]

        transformation_matrix, ransac_mask = od.compute_homography(s_keypoints[['x', 'y']].values,
                                                                   d_keypoints[['x', 'y']].values)
                                                                   d_keypoints[['x', 'y']].values,
                                                                   **kwargs)

        ransac_mask = ransac_mask.ravel()
        # Convert the truncated RANSAC mask back into a full length mask
@@ -148,7 +175,20 @@ class Edge(object):
        else:
            mask = ransac_mask
        self.masks = ('ransac', mask)
        self.homography = transformation_matrix
        self.homography = Homography(transformation_matrix,
                                     s_keypoints[ransac_mask][['x', 'y']],
                                     d_keypoints[ransac_mask][['x', 'y']])

    @property
    def homography_determinant(self):
        """
        If the determinant of the homography is close to zero,
        this is indicative of a validation issue, i.e., the
        homography might be bad.
        """
        if not hasattr(self, 'homography'):
            raise AttributeError('No homography has been computed for this edge.')
        return np.linalg.det(self.homography)

    def compute_subpixel_offset(self, clean_keys=[], threshold=0.8, upsampling=16,
                                 template_size=19, search_size=53):
@@ -653,23 +693,33 @@ class CandidateGraph(nx.Graph):
        for s, d, edge in self.edges_iter(data=True):
            edge.ratio_check(ratio=ratio)

    def compute_homographies(self, outlier_algorithm=cv2.RANSAC, clean_keys=[]):
    def compute_homographies(self, clean_keys=[], **kwargs):
        """
        Compute homographies for all edges using identical parameters

        Parameters
        ----------
        outlier_algorithm : object
                            Function to apply for outlier detection
        clean_keys : list
                     Of keys in the mask dict

        """

        for s, d, edge in self.edges_iter(data=True):
            edge.compute_homography(clean_keys=clean_keys, **kwargs)

    def compute_fundamental_matrices(self, clean_keys=[], **kwargs):
        """
        Compute fundamental matrices for all edges using identical parameters

        Parameters
        ----------
        clean_keys : list
                     Of keys in the mask dict

        """

        for s, d, edge in self.edges_iter(data=True):
            edge.compute_homography(outlier_algorithm=outlier_algorithm,
                                    clean_keys=clean_keys)
            edge.compute_fundamental_matrix(clean_keys=clean_keys, **kwargs)

    def compute_subpixel_offsets(self, clean_keys=[], threshold=0.8, upsampling=10,
                                 template_size=9, search_size=27):
@@ -848,3 +898,23 @@ class CandidateGraph(nx.Graph):
          A list of connected sub-graphs of nodes, with the largest sub-graph first. Each subgraph is a set.
        """
        return sorted(nx.connected_components(self), key=len, reverse=True)

    # TODO: The Edge object requires a get method in order to be plottable, probably Node as well.
    # This is a function of being a dict in NetworkX
    '''
    def plot(self, ax=None, **kwargs):
        """
        Plot the graph object

        Parameters
        ----------
        ax : object
             A MatPlotLib axes object.

        Returns
        -------
         : object
           A MatPlotLib axes object
        """
        return plot_graph(self, ax=ax,  **kwargs)
    '''
 No newline at end of file
+0 −2
Original line number Diff line number Diff line
@@ -74,5 +74,3 @@ class TestNode(unittest.TestCase):
        # Convex hull computation is checked lower in the hull computation
        node = self.graph.node[0]
        self.assertRaises(AttributeError, node.coverage_ratio)

+62 −0
Original line number Diff line number Diff line
import numpy as np

from autocnet.utils.utils import make_homogeneous
from autocnet.utils import evaluation_measures


class Homography(np.ndarray):
    """
    A homography or planar transformation matrix

    Attributes
    ----------
    determinant : float
                  The determinant of the matrix

    condition : float
                The condition computed as SVD[0] / SVD[-1]

    rmse : float
           The root mean square error computed using a set of
           given input points

    """
    def __new__(cls, inputarr, x1, x2):
        obj = np.asarray(inputarr).view(cls)

        if not isinstance(inputarr, np.ndarray):
            raise TypeError('The homography must be an ndarray')
        if not inputarr.shape[0] == 3 and not inputarr.shape[1] == 3:
            raise ValueError('The homography must be a 3x3 matrix.')

        obj.x1 = make_homogeneous(x1)
        obj.x2 = make_homogeneous(x2)

        return obj

    @property
    def determinant(self):
        if not hasattr(self, '_determinant'):
            self._determinant = np.linalg.det(self)
        return self._determinant

    @property
    def condition(self):
        if not hasattr(self, '_condition'):
            s = np.linalg.svd(self, compute_uv=False)
            self._condition = s[0] / s[1]
        return self._condition

    @property
    def rmse(self):
        if not hasattr(self, '_rmse'):

            # TODO: Vectorize this for performance
            t_kps = np.empty((self.x1.shape[0], 3))
            for i, j in enumerate(self.x1):
                proj_point = self.dot(j)
                proj_point /= proj_point[-1]  # normalize
                t_kps[i] = proj_point
            self._rmse = evaluation_measures.rmse(self.x2, t_kps)
        return self._rmse
+76 −19
Original line number Diff line number Diff line
@@ -79,19 +79,6 @@ def distance_ratio(matches, ratio=0.8):
                mask[counter: counter + group_size] = False
                counter += group_size

        '''
        # won't work if there's only 1 match for each queryIdx
        if len(group) < 2:
            mask.append(True)
        else:
            if group['distance'].iloc[0] < ratio * group['distance'].iloc[1]: # this means distance _0_ is good and can drop all other distances
                mask.append(True)
                for i in range(len(group['distance']-1)):
                    mask.append(False)
            else:
                for i in range(len(group['distance'])):
                    mask.append(False)
        '''
    return mask


@@ -122,9 +109,9 @@ def mirroring_test(matches):
    return duplicates


def compute_homography(kp1, kp2, outlier_algorithm=cv2.RANSAC, reproj_threshold=5.0):
def compute_fundamental_matrix(kp1, kp2, method='ransac', reproj_threshold=5.0, confidence=0.99):
    """
    Given two arrays of keypoints compute a homography
    Given two arrays of keypoints compute the fundamental matrix

    Parameters
    ----------
@@ -134,12 +121,15 @@ def compute_homography(kp1, kp2, outlier_algorithm=cv2.RANSAC, reproj_threshold=
    kp2 : ndarray
          (n, 2) of coordinates from the destination image

    outlier_algorithm : object
    outlier_algorithm : {'ransac', 'lmeds', 'normal'}
                        The openCV algorithm to use for outlier detection

    reproj_threshold : float
                       The RANSAC reprojection threshold
                       The maximum distances in pixels a reprojected points
                       can be from the epipolar line to be considered an inlier

    confidence : float
                 [0, 1] that the estimated matrix is correct

    Returns
    -------
@@ -148,12 +138,79 @@ def compute_homography(kp1, kp2, outlier_algorithm=cv2.RANSAC, reproj_threshold=

    mask : ndarray
           Boolean array of the outliers

    Notes
    -----
    While the method is user definable, if the number of input points
    is < 7, normal outlier detection is automatically used, if 7 > n > 15,
    least medians is used, and if 7 > 15, ransac can be used.
    """

    if method == 'ransac':
        method_ = cv2.FM_RANSAC
    elif method == 'lmeds':
        method_ = cv2.FM_LMEDS
    elif method == 'normal':
        method_ = cv2.FM_7POINT
    else:
        raise ValueError("Unknown outlier detection method.  Choices are: 'ransac', 'lmeds', or 'normal'.")

    transformation_matrix, mask = cv2.findFundamentalMat(kp1,
                                                     kp2,
                                                     method_,
                                                     reproj_threshold,
                                                     confidence)
    mask = mask.astype(bool)
    return transformation_matrix, mask


def compute_homography(kp1, kp2, method='ransac', **kwargs):
    """
    Given two arrays of keypoints compute the homography

    Parameters
    ----------
    kp1 : ndarray
          (n, 2) of coordinates from the source image

    kp2 : ndarray
          (n, 2) of coordinates from the destination image

    outlier_algorithm : {'ransac', 'lmeds', 'normal'}
                        The openCV algorithm to use for outlier detection

    reproj_threshold : float
                       The maximum distances in pixels a reprojected points
                       can be from the epipolar line to be considered an inlier

    Returns
    -------
    transformation_matrix : ndarray
                            The 3x3 perspective transformation matrix

    mask : ndarray
           Boolean array of the outliers

    Notes
    -----
    While the method is user definable, if the number of input points
    is < 7, normal outlier detection is automatically used, if 7 > n > 15,
    least medians is used, and if 7 > 15, ransac can be used.
    """

    if method == 'ransac':
        method_ = cv2.RANSAC
    elif method == 'lmeds':
        method_ = cv2.LMEDS
    elif method == 'normal':
        method_ = 0  # Normal method
    else:
        raise ValueError("Unknown outlier detection method.  Choices are: 'ransac', 'lmeds', or 'normal'.")

    transformation_matrix, mask = cv2.findHomography(kp1,
                                                     kp2,
                                                     outlier_algorithm,
                                                     reproj_threshold)
                                                     method_,
                                                     **kwargs)
    mask = mask.astype(bool)
    return transformation_matrix, mask

+36 −0
Original line number Diff line number Diff line
import os
import numpy as np
import unittest

import sys
sys.path.insert(0, os.path.abspath('..'))

import numpy.testing

from .. import homography



class TestHomography(unittest.TestCase):

    def setUp(self):
        pass

    def test_Homography(self):
        nbr_inliers = 20
        fp = np.array(np.random.standard_normal((nbr_inliers,2))) #inliers

        # homography to transform fp
        static_H = np.array([[4,0.5,10],[0.25,1,5],[0.2,0.1,1]])

        #Make homogeneous
        fph = np.hstack((fp,np.ones((nbr_inliers, 1))))
        tp = static_H.dot(fph.T)
        # normalize hom. coordinates
        tp /= tp[-1,:np.newaxis]
        H = homography.Homography(static_H,
                                  fp,
                                  tp.T[:,:2])
        self.assertAlmostEqual(H.determinant, 0.6249999, 5)
        self.assertAlmostEqual(H.condition, 7.19064438, 5)
        numpy.testing.assert_array_almost_equal(H.rmse, np.array([0, 0, 0.0]))
Loading