Commit b5d92204 authored by Jay's avatar Jay Committed by jay
Browse files

Updates to fundamental matrix to support refinement, error computation, undo,...

Updates to fundamental matrix to support refinement, error computation, undo, redo.  ABC used for all transformations.
parent a77f3497
Loading
Loading
Loading
Loading
+0 −121
Original line number Diff line number Diff line
@@ -4,126 +4,5 @@ import pandas as pd
from autocnet.utils.utils import make_homogeneous


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]

    error : dataframe
            describing the error of the points used to
            compute this homography
    """
    def __new__(cls, inputarr, x1, x2, index=None):
        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)
        obj.pd_index = index

        cls.__array_finalize__(cls, obj)

        return obj

    def __array_finalize__(self, obj):
        if obj is None:
            return
        self.x1 = getattr(obj, 'x1', None)
        self.x2 = getattr(obj, 'x2', None)
        self.pd_index = getattr(obj, 'pd_index', None)

    @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 error(self):
        if not hasattr(self, '_error'):
            self._error = self.compute_error(self.x1,
                                             self.x2,
                                             self.pd_index)
        return self._error

    def compute_error(self, a, b, index=None):
        """
        Give this homography, compute the planar reprojection error
        between points a and b.

        Parameters
        ----------
        a : ndarray
            n,2 array of x,y coordinates

        b : ndarray
            n,2 array of x,y coordinates

        index : ndarray
                Index to be used in the returned dataframe

        Returns
        -------
        df : dataframe
             With columns for x_residual, y_residual, rmse, and
             error contribution.  The dataframe also has cumulative
             x, t, and total RMS statistics accessible via
             df.x_rms, df.y_rms, and df.total_rms, respectively.
        """
        if not isinstance(a, np.ndarray):
            a = np.asarray(a)
        if not isinstance(b, np.ndarray):
            b = np.asarray(b)

        if a.shape[1] == 2:
            a = make_homogeneous(a)
        if b.shape[1] == 2:
            b = make_homogeneous(b)

        # ToDo: Vectorize for performance
        for i, j in enumerate(a):
            a[i] = self.dot(j)
            a[i] /= a[i][-1]

        data = np.empty((a.shape[0], 4))

        data[:,0] = x_res = b[:,0] - a[:,0]
        data[:,1] = y_res = b[:,1] - a[:,1]
        data[:,2] = rms = np.sqrt(x_res**2 + y_res**2)
        total_rms = np.sqrt(np.mean(x_res**2 + y_res**2))
        x_rms = np.sqrt(np.mean(x_res**2))
        y_rms = np.sqrt(np.mean(y_res**2))

        data[:,3] = rms / total_rms

        df = pd.DataFrame(data,
                          columns=['x_residuals',
                                   'y_residuals',
                                   'rmse',
                                   'error_contribution'],
                          index=index)

        df.total_rms = total_rms
        df.x_rms = x_rms
        df.y_rms = y_rms

        return df
+3 −3
Original line number Diff line number Diff line
@@ -7,7 +7,7 @@ sys.path.insert(0, os.path.abspath('..'))

import numpy.testing

from .. import homography
from .. import transformations


class TestHomography(unittest.TestCase):
@@ -27,7 +27,7 @@ class TestHomography(unittest.TestCase):
        tp = static_H.dot(fph.T)
        # normalize hom. coordinates
        tp /= tp[-1,:np.newaxis]
        H = homography.Homography(static_H,
        H = transformations.Homography(static_H,
                                  fp,
                                  tp.T[:,:2])
        self.assertAlmostEqual(H.determinant, 0.6249999, 5)
@@ -36,4 +36,4 @@ class TestHomography(unittest.TestCase):
        numpy.testing.assert_array_almost_equal(error['rmse'], np.zeros(20))

    def test_Homography_fail(self):
        self.assertRaises(TypeError, homography.Homography, [1,2,3], 'a', 'b')
        self.assertRaises(TypeError, transformations.Homography, [1,2,3], 'a', 'b')
+152 −23
Original line number Diff line number Diff line
import abc
from collections import deque

import numpy as np
@@ -7,23 +8,10 @@ import pysal as ps
from autocnet.matcher.outlier_detector import compute_fundamental_matrix
from autocnet.utils.utils import make_homogeneous

class TransformationMatrix(np.ndarray):
    __metaclass__ = abc.ABCMeta

class FundamentalMatrix(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]

    error : dataframe
            describing the error of the points used to
            compute this homography
    """
    @abc.abstractmethod
    def __new__(cls, inputarr, x1, x2, mask=None):
        obj = np.asarray(inputarr).view(cls)

@@ -38,12 +26,17 @@ class FundamentalMatrix(np.ndarray):
        obj._action_stack = deque(maxlen=10)
        obj._current_action_stack = 0
        # Seed the state package with the initial creation state
        if mask is not None:
            state_package = {'arr': obj.copy(),
                             'mask': obj.mask.copy()}
        else:
            state_package = {'arr':obj.copy(),
                             'mask':None}
        obj._action_stack.append(state_package)

        return obj

    @abc.abstractmethod
    def __array_finalize__(self, obj):
        if obj is None:
            return
@@ -53,32 +46,41 @@ class FundamentalMatrix(np.ndarray):
        self._action_stack = getattr(obj, '_action_stack', None)
        self._current_action_stack = getattr(obj, '_current_action_stack', None)

    @property
    @abc.abstractproperty
    def determinant(self):
        if not getattr(self, '_determinant', None):
            self._determinant = np.linalg.det(self)
        return self._determinant

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

    @property
    @abc.abstractproperty
    def error(self):
        if not hasattr(self, '_error'):
            self._error = self.compute_error()
        return self._error

    @property
    @abc.abstractproperty
    def describe_error(self):
        if not getattr(self, '_error', None):
            self._error = self.compute_error()
        return self.error.describe()

    @abc.abstractmethod
    def rollback(self, n=1):
        """
        Roll backward in the object histroy, e.g. undo

        Parameters
        ----------
        n : int
            the number of steps to roll backwards
        """
        idx = self._current_action_stack - n
        if idx < 0:
            idx = 0
@@ -89,16 +91,54 @@ class FundamentalMatrix(np.ndarray):
        # Reset attributes (could also cache)
        self._clean_attrs()

    @abc.abstractmethod
    def rollforward(self, n=1):
        """
        Roll forwards in the object history, e.g. do

        Parameters
        ----------
        n : int
            the number of steps to roll forwards
        """
        idx = self._current_action_stack + n
        if idx > len(self._action_stack) - 1:
            idx = len(self._action_stack) - 1
        state = self._action_stack[idx]
        self[:] = state['arr']
        self.mask = state['mask']
        setattr(self, 'mask', state['mask'])
        # Reset attributes (could also cache)
        self._clean_attrs()

    @abc.abstractmethod
    def compute_error(self, x1=None, x2=None, index=None):
        pass

    @abc.abstractmethod
    def recompute_matrix(self):
        pass

    @abc.abstractmethod
    def refine(self):
        pass


class FundamentalMatrix(TransformationMatrix):
    """
    A homography or planar transformation matrix

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

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

    error : dataframe
            describing the error of the points used to
            compute this homography
    """
    def refine(self, method=ps.esda.mapclassify.Fisher_Jenks, bin_id=0, **kwargs):
        """
        Refine the fundamental matrix by accepting some data classification
@@ -210,3 +250,92 @@ class FundamentalMatrix(np.ndarray):

        return error

    def recompute_matrix(self):
        raise NotImplementedError


class Homography(TransformationMatrix):
    """
    A homography or planar transformation matrix

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

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

    error : dataframe
            describing the error of the points used to
            compute this homography
    """

    def compute_error(self, a=None, b=None, index=None):
        """
        Give this homography, compute the planar reprojection error
        between points a and b.

        Parameters
        ----------
        a : ndarray
            n,2 array of x,y coordinates

        b : ndarray
            n,2 array of x,y coordinates

        index : ndarray
                Index to be used in the returned dataframe

        Returns
        -------
        df : dataframe
             With columns for x_residual, y_residual, rmse, and
             error contribution.  The dataframe also has cumulative
             x, t, and total RMS statistics accessible via
             df.x_rms, df.y_rms, and df.total_rms, respectively.
        """
        if not isinstance(a, np.ndarray):
            a = np.asarray(a)
        if not isinstance(b, np.ndarray):
            b = np.asarray(b)

        if a.shape[1] == 2:
            a = make_homogeneous(a)
        if b.shape[1] == 2:
            b = make_homogeneous(b)

        # ToDo: Vectorize for performance
        for i, j in enumerate(a):
            a[i] = self.dot(j)
            a[i] /= a[i][-1]

        data = np.empty((a.shape[0], 4))

        data[:,0] = x_res = b[:,0] - a[:,0]
        data[:,1] = y_res = b[:,1] - a[:,1]
        data[:,2] = rms = np.sqrt(x_res**2 + y_res**2)
        total_rms = np.sqrt(np.mean(x_res**2 + y_res**2))
        x_rms = np.sqrt(np.mean(x_res**2))
        y_rms = np.sqrt(np.mean(y_res**2))

        data[:,3] = rms / total_rms

        df = pd.DataFrame(data,
                          columns=['x_residuals',
                                   'y_residuals',
                                   'rmse',
                                   'error_contribution'],
                          index=index)

        df.total_rms = total_rms
        df.x_rms = x_rms
        df.y_rms = y_rms

        return df

    def recompute_matrix(self):
        raise NotImplementedError

    def refine(self):
        raise NotImplementedError