Commit 4e9a7176 authored by Jay's avatar Jay Committed by jay
Browse files

Update to significantly improve ratio test performance and provenance tracking for ratio.

parent 7772d1c2
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
{"AS15-M-0297_SML.png": ["AS15-M-0298_SML.png", "AS15-M-0299_SML.png"],
"AS15-M-0298_SML.png": ["AS15-M-0297_SML.png", "AS15-M-0299_SML.png"],
"AS15-M-0299_SML.png": ["AS15-M-0297_SML.png", "AS15-M-0298_SML.png"]}
"AS15-M-0298_SML.png": ["AS15-M-0299_SML.png", "AS15-M-0297_SML.png"],
"AS15-M-0299_SML.png": ["AS15-M-0298_SML.png", "AS15-M-0297_SML.png"]}
+16 −4
Original line number Diff line number Diff line
@@ -54,10 +54,11 @@ class Edge(dict, MutableMapping):

    @property
    def masks(self):
        mask_lookup = {'fundamental': 'fundamental_matrix'}
        mask_lookup = {'fundamental': 'fundamental_matrix',
                       'ratio': 'distance_ratio'}
        if not hasattr(self, '_masks'):
            if hasattr(self, 'matches'):
                self._masks = pd.DataFrame(True, columns=['symmetry', 'ratio'],
                self._masks = pd.DataFrame(True, columns=['symmetry'],
                                       index=self.matches.index)
            else:
                self._masks = pd.DataFrame()
@@ -110,9 +111,20 @@ class Edge(dict, MutableMapping):
        else:
            raise AttributeError('No matches have been computed for this edge.')

    def ratio_check(self, ratio=0.8):
    def ratio_check(self, ratio=0.8, clean_keys=[]):
        if hasattr(self, 'matches'):
            mask = od.distance_ratio(self.matches, ratio=ratio)

            if clean_keys:
                _, mask = self._clean(clean_keys)
            else:
                mask = pd.Series(True, self.matches.index)

            self.distance_ratio = od.DistanceRatio(self.matches)
            self.distance_ratio.compute(ratio, mask=mask, mask_name=None)

            # Setup to be notified
            self.distance_ratio._notify_subscribers(self.distance_ratio)

            self.masks = ('ratio', mask)
        else:
            raise AttributeError('No matches have been computed for this edge.')
+12 −8
Original line number Diff line number Diff line
@@ -198,7 +198,7 @@ class CandidateGraph(nx.Graph):
            node.extract_features(image, method=method,
                                extractor_parameters=extractor_parameters)

    def match_features(self, k=3):
    def match_features(self, k=None):
        """
        For all connected edges in the graph, apply feature matching

@@ -207,18 +207,22 @@ class CandidateGraph(nx.Graph):
        k : int
            The number of matches, minus 1, to find per feature.  For example
            k=5 will find the 4 nearest neighbors for every extracted feature.
            If None,  k = (2 * the number of edges connecting a node) +1
        """
        #Load a Fast Approximate Nearest Neighbor KD-Tree
        fl = FlannMatcher()
        degree = self.degree()

        self._fl = FlannMatcher()
        for i, node in self.nodes_iter(data=True):
            if not hasattr(node, 'descriptors'):
                raise AttributeError('Descriptors must be extracted before matching can occur.')
            fl.add(node.descriptors, key=i)
        fl.train()
            self._fl.add(node.descriptors, key=i)
        self._fl.train()

        for i, node in self.nodes_iter(data=True):
            if k is None:
                k = (degree[i] * 2) + 1
            descriptors = node.descriptors
            matches = fl.query(descriptors, i, k=k)
            matches = self._fl.query(descriptors, i, k=k)
            self.add_matches(matches)

    def add_matches(self, matches):
@@ -256,12 +260,12 @@ class CandidateGraph(nx.Graph):
        for s, d, edge in self.edges_iter(data=True):
            edge.symmetry_check()

    def ratio_checks(self, ratio=0.8):
    def ratio_checks(self, ratio=0.8, clean_keys=[]):
        """
        Perform a ratio check on all edges in the graph
        """
        for s, d, edge in self.edges_iter(data=True):
            edge.ratio_check(ratio=ratio)
            edge.ratio_check(ratio=ratio, clean_keys=clean_keys)

    def compute_homographies(self, clean_keys=[], **kwargs):
        """
+121 −57
Original line number Diff line number Diff line
from collections import deque

import cv2
import numpy as np
import pandas as pd


def self_neighbors(matches):
    """
    Returns a pandas data series intended to be used as a mask. Each row
    is True if it is not matched to a point in the same image (good) and
    False if it is (bad.)
class DistanceRatio(object):

    Parameters
    ----------
    matches : dataframe
              the matches dataframe stored along the edge of the graph
              containing matched points with columns containing:
              matched image name, query index, train index, and
              descriptor distance
    Returns
    -------
    : dataseries
      Intended to mask the matches dataframe. True means the row is not matched to a point in the same image
      and false the row is.
    """
    return matches.source_image != matches.destination_image
    def __init__(self, matches):

        self._action_stack = deque(maxlen=10)
        self._current_action_stack = 0
        self._observers = set()
        self.matches = matches
        self.mask = None

    @property
    def nvalid(self):
        return self.mask.sum()

def distance_ratio(matches, ratio=0.8):
    def compute(self, ratio, mask=None, mask_name=None):
        """
        Compute and return a mask for a matches dataframe
        using Lowe's ratio test.
@@ -33,12 +27,6 @@ def distance_ratio(matches, ratio=0.8):

        Parameters
        ----------
    matches : dataframe
              the matches dataframe stored along the edge of the graph
              containing matched points with columns containing:
              matched image name, query index, train index, and
              descriptor distance.

        ratio : float
                the ratio between the first and second-best match distances
                for each keypoint to use as a bound for marking the first keypoint
@@ -51,35 +39,111 @@ def distance_ratio(matches, ratio=0.8):
                default, since the ratio test will not work for them.

        """

    mask = np.zeros(len(matches), dtype=bool)  # Pre-allocate the mask
    counter = 0
    for i, group in matches.groupby('source_idx'):
        group_size = len(group)
        n_unique = len(group['destination_idx'].unique())
        # If we can not perform the ratio check because all matches are symmetrical
        if n_unique == 1:
            mask[counter:counter + group_size] = True
            counter += group_size
        else:
            # Otherwise, we can perform the ratio test
            sorted_group = group.sort_values(by=['distance'])
            unique = sorted_group['distance'].unique()

            if len(unique) == 1:
                # The distances from the unique points are identical
                mask[counter: counter + group_size] = False
                counter += group_size
            elif unique[0] / unique[1] < ratio:
                # The ratio test passes
                mask[counter] = True
                mask[counter + 1:counter + group_size] = False
                counter += group_size
        def func(group):
            res = [False] * len(group)
            if len(res) == 1:
                return res
            if group.iloc[0] < group.iloc[1] * ratio:
                res[0] = True
            return res

        if mask is not None:
            self.mask = mask.copy()
            new_mask = self.matches[mask].groupby('source_idx')['distance'].transform(func).astype('bool')
            self.mask[mask==True] = new_mask
        else:
                mask[counter: counter + group_size] = False
                counter += group_size
            new_mask = self.matches.groupby('source_idx')['distance'].transform(func).astype('bool')
            self.mask = new_mask.copy()

        state_package = {'ratio': ratio,
                         'mask': self.mask.copy(),
                         'clean_keys': mask_name
                         }
        self._action_stack.append(state_package)
        self._current_action_stack = len(self._action_stack) - 1

    def subscribe(self, func):
        """
        Subscribe some observer to the edge

        Parameters
        ----------
        func : object
               The callable that is to be executed on update
        """
        self._observers.add(func)

    def _notify_subscribers(self, *args, **kwargs):
        """
        The 'update' call to notify all subscribers of
        a change.
        """
        for update_func in self._observers:
            update_func(self, *args, **kwargs)

    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
        self._current_action_stack = idx
        state = self._action_stack[idx]
        setattr(self, 'mask', state['mask'])
        setattr(self, 'ratio', state['ratio'])
        setattr(self, 'clean_keys', state['clean_keys'])
        # Reset attributes (could also cache)
        self._notify_subscribers(self)

    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
        self._current_action_stack = idx
        state = self._action_stack[idx]
        setattr(self, 'mask', state['mask'])
        setattr(self, 'ratio', state['ratio'])
        setattr(self, 'clean_keys', state['clean_keys'])
        # Reset attributes (could also cache)
        self._notify_subscribers(self)

def self_neighbors(matches):
    """
    Returns a pandas data series intended to be used as a mask. Each row
    is True if it is not matched to a point in the same image (good) and
    False if it is (bad.)

    Parameters
    ----------
    matches : dataframe
              the matches dataframe stored along the edge of the graph
              containing matched points with columns containing:
              matched image name, query index, train index, and
              descriptor distance
    Returns
    -------
    : dataseries
      Intended to mask the matches dataframe. True means the row is not matched to a point in the same image
      and false the row is.
    """
    return matches.source_image != matches.destination_image



    return mask


def mirroring_test(matches):
@@ -104,8 +168,7 @@ def mirroring_test(matches):
                 otherwise, they will be false. Keypoints with only one match will be False. Removes
                 duplicate rows.
    """
    duplicates = matches.duplicated(keep='first').values
    duplicates.astype(bool, copy=False)
    duplicates = matches.duplicated(keep='first').astype(bool)
    return duplicates


@@ -215,6 +278,7 @@ def compute_homography(kp1, kp2, method='ransac', **kwargs):
                                                     kp2,
                                                     method_,
                                                     **kwargs)
    if mask is not None:
        mask = mask.astype(bool)
    return transformation_matrix, mask

+7 −4
Original line number Diff line number Diff line
@@ -36,10 +36,12 @@ class TestOutlierDetector(unittest.TestCase):
            counter += 1

        fmatcher.train()
        self.matches = fmatcher.query(fd['AS15-M-0296_SML.png'][1],'AS15-M-0296_SML.png', k=3)
        self.matches = fmatcher.query(fd['AS15-M-0296_SML.png'][1], 'AS15-M-0296_SML.png')

    def test_distance_ratio(self):
        self.assertTrue(len(outlier_detector.distance_ratio(self.matches)), 13)
        d = outlier_detector.DistanceRatio(self.matches)
        d.compute(0.9)
        self.assertEqual(d.mask.sum(), 3)

    def test_distance_ratio_unique(self):
        data = [['A', 0, 'B', 1, 10],
@@ -47,8 +49,9 @@ class TestOutlierDetector(unittest.TestCase):
        df = pd.DataFrame(data, columns=['source_image', 'source_idx',
                                         'destination_image', 'destination_idx',
                                         'distance'])
        mask = outlier_detector.distance_ratio(df)
        self.assertTrue(mask.all() == False)
        d = outlier_detector.DistanceRatio(df)
        d.compute(0.9)
        self.assertTrue(d.mask.all() == False)

    def test_self_neighbors(self):
        # returned mask should be same length as input df
Loading