Commit 1788194d authored by jlaura's avatar jlaura Committed by GitHub
Browse files

Merge pull request #207 from jlaura/master

Assorted Updates
parents 1187045c 536b9f53
Loading
Loading
Loading
Loading
+8 −4
Original line number Diff line number Diff line
@@ -11,7 +11,10 @@ import autocnet.graph
import autocnet.matcher
import autocnet.transformation
import autocnet.utils
import autocnet.utils


# Patch the candidate graph into the root namespace
from autocnet.graph.network import CandidateGraph

__version__ = "0.1.0"

@@ -39,15 +42,16 @@ def cuda(enable=False, gpu=0):

            from autocnet.matcher.cuda_decompose import decompose_and_match
            Edge.decompose_and_match = decompose_and_match

        except Exception:
            warning.warn('Failed to enable Cuda')
            warnings.warn('Failed to enable Cuda')
        return

    # Here is where the CPU methods get patched into the class
    from autocnet.matcher.feature_extractor import extract_features
    from autocnet.matcher.cpu_extractor import extract_features
    Node._extract_features = staticmethod(extract_features)

    from autocnet.matcher.feature_matcher import match
    from autocnet.matcher.cpu_matcher import match
    Edge.match = match

    from autocnet.matcher.cpu_decompose import decompose_and_match
+3 −0
Original line number Diff line number Diff line
from . import edge
from . import network
from . import node
+104 −124
Original line number Diff line number Diff line
from functools import wraps
import warnings
from collections import MutableMapping

@@ -9,7 +10,7 @@ from scipy.spatial.distance import cdist
import autocnet
from autocnet.graph.node import Node
from autocnet.utils import utils
from autocnet.matcher import outlier_detector as od
from autocnet.matcher import cpu_outlier_detector as od
from autocnet.matcher import suppression_funcs as spf
from autocnet.matcher import subpixel as sp
from autocnet.transformation import fundamental_matrix as fm
@@ -18,6 +19,7 @@ from autocnet.vis.graph_view import plot_edge, plot_node, plot_edge_decompositio
from autocnet.cg import cg



class Edge(dict, MutableMapping):
    """
    Attributes
@@ -41,8 +43,11 @@ class Edge(dict, MutableMapping):
        self.destination = destination
        self['homography'] = None
        self['fundamental_matrix'] = None
        self.matches = None
        self.matches = pd.DataFrame()
        self.masks = pd.DataFrame()
        self['weights'] = {}
        self['source_mbr'] = None
        self['destin_mbr'] = None

    def __repr__(self):
        return """
@@ -70,7 +75,7 @@ class Edge(dict, MutableMapping):

        return eq

    @property
    """@property
    def masks(self):
        mask_lookup = {'fundamental': 'fundamental_matrix'}
        if not hasattr(self, '_masks'):
@@ -85,10 +90,7 @@ class Edge(dict, MutableMapping):
    def masks(self, v):
        column_name = v[0]
        boolean_mask = v[1]
        self.masks[column_name] = boolean_mask

    def decompose_and_match(*args, **kwargs):
        pass
        self.masks[column_name] = boolean_mask"""

    def match(self, k=2, **kwargs):

@@ -103,25 +105,60 @@ class Edge(dict, MutableMapping):
        ----------
        k : int
            The number of neighbors to find

        overlap : boolean
                  Apply the matcher only to the overlapping area defined by
                  the source_mbr and destin_mbr attributes (stored in the
                  edge dict).
        """
        pass    

    def match_overlap(self, k=2, **kwargs):
        """
        Given two sets of descriptors, apply the matcher with the
        source and destination overlaps.
        """
        overlaps = [self['source_mbr'], self['destin_mbr']]
        self.match(k=k, overlap=overlaps, **kwargs)
        
    def decompose(self):
        """
        Apply coupled decomposition to the images and
        match identified sub-images
        """
        pass

    def decompose_and_match(*args, **kwargs):
        pass

    """
    def extract_subset(self, *args, **kwargs):
        self.compute_overlap()

        # Extract the source
        minx, maxx, miny, maxy = self['source_mbr']
        xystart = (minx, miny)
        pixels=[minx, miny, maxx-minx, maxy-miny]
        node = self.source
        arr = node.geodata.read_array(pixels=pixels)
        node.extract_features(arr, xystart=xystart, *args, **kwargs)

        # Extract the destination
        minx, maxx, miny, maxy = self['destin_mbr']
        xystart = (minx, miny)
        pixels=[minx, miny, maxx-minx, maxy-miny]
        node = self.destination
        arr = node.geodata.read_array(pixels=pixels)
        node.extract_features(arr, xystart=xystart, *args, **kwargs)
    """
    def symmetry_check(self):
        if isinstance(self.matches, pd.DataFrame):
            mask = od.mirroring_test(self.matches)
            self.masks = ('symmetry', mask)
        else:
            raise AttributeError('No matches have been computed for this edge.')
        self.masks['symmetry'] = od.mirroring_test(self.matches)

    def ratio_check(self, clean_keys=[], **kwargs):
        if isinstance(self.matches, pd.DataFrame):
    def ratio_check(self, clean_keys=[], maskname='ratio', **kwargs):
        matches, mask = self.clean(clean_keys)
            distance_mask = od.distance_ratio(matches, **kwargs)
            self.masks = ('ratio', distance_mask)
        else:
            raise AttributeError('No matches have been computed for this edge.')
        self.masks[maskname] = od.distance_ratio(matches, **kwargs)

    def compute_fundamental_matrix(self, clean_keys=[], **kwargs):
    def compute_fundamental_matrix(self, clean_keys=[], maskname='fundamental', **kwargs):
        """
        Estimate the fundamental matrix (F) using the correspondences tagged to this
        edge.
@@ -141,16 +178,11 @@ class Edge(dict, MutableMapping):
        autocnet.transformation.transformations.FundamentalMatrix

        """
        if not isinstance(self.matches, pd.DataFrame):
            raise AttributeError('Matches have not been computed for this edge')
            return
        matches, mask = self.clean(clean_keys)

        # TODO: Homogeneous is horribly inefficient here, use Numpy array notation
        s_keypoints = self.source.get_keypoint_coordinates(index=matches['source_idx'],
                                                                 homogeneous=True)
        d_keypoints = self.destination.get_keypoint_coordinates(index=matches['destination_idx'],
                                                                homogeneous=True)
        s_keypoints = self.get_keypoints('source', index=matches['source_idx'])
        d_keypoints = self.get_keypoints('destination', index=matches['destination_idx'])


        # Replace the index with the matches index.
@@ -164,9 +196,42 @@ class Edge(dict, MutableMapping):
            mask[mask] = fmask

            # Set the initial state of the fundamental mask in the masks
            self.masks = ('fundamental', mask)
            self.masks[maskname] = mask

    def get_keypoints(self, node, index=None, homogeneous=True):
        node = getattr(self, node)
        return node.get_keypoint_coordinates(index=index, homogeneous=homogeneous)

    def compute_fundamental_error(self, clean_keys=[]):
        """
        Given a fundamental matrix, compute the reprojective error between
        a two sets of keypoints.

        Parameters
        ----------
        clean_keys : list
                     of string keys to masking arrays
                     (created by calling outlier detection)

    def compute_homography(self, method='ransac', clean_keys=[], pid=None, **kwargs):
        Returns
        -------
        error : pd.Series
                of reprojective error indexed to the matches data frame
        """
        if self['fundamental_matrix'] is None:
            warning.warn('No fundamental matrix has been compute for this edge.'
            )
        matches, masks = self.clean(clean_keys)

        source_kps = self.source.get_keypoint_coordinates(index=matches['source_idx'])
        destination_kps = self.destination.get_keypoint_coordinates(index=matches['destination_idx'])

        error = fm.compute_fundamental_error(self['fundamental_matrix'], source_kps, destination_kps)

        error = pd.Series(error, index=matches.index)
        return error

    def compute_homography(self, method='ransac', clean_keys=[], pid=None, maskname='homography', **kwargs):
        """
        For each edge in the (sub) graph, compute the homography
        Parameters
@@ -185,12 +250,6 @@ class Edge(dict, MutableMapping):
        mask : ndarray
               Boolean array of the outliers
        """

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

        matches, mask = self.clean(clean_keys)

        s_keypoints = self.source.get_keypoint_coordinates(index=matches['source_idx'])
@@ -200,7 +259,7 @@ class Edge(dict, MutableMapping):

        # Convert the truncated RANSAC mask back into a full length mask
        mask[mask] = hmask
        self.masks = ('ransac', mask)
        self.masks['homography'] = mask

    def subpixel_register(self, clean_keys=[], threshold=0.8,
                          template_size=19, search_size=53, max_x_shift=1.0,
@@ -278,19 +337,18 @@ class Edge(dict, MutableMapping):
        threshold_mask = self.matches['correlation'] >= threshold

        # Compute the mask for the point shifts that are too large
        query_string = 'x_offset <= -{0} or x_offset >= {0} or y_offset <= -{1} or y_offset >= {1}'.format(max_x_shift,
                                                                                                           max_y_shift)
        query_string = 'x_offset <= -{0} or x_offset >= {0} or y_offset <= -{1} or y_offset >= {1}'.format(max_x_shift,max_y_shift)
        sp_shift_outliers = self.matches.query(query_string)
        shift_mask = pd.Series(True, index=self.matches.index)
        shift_mask.loc[sp_shift_outliers.index] = False

        # Generate the composite mask and write the masks to the mask data structure
        mask = threshold_mask & shift_mask
        self.masks = ('shift', shift_mask)
        self.masks = ('threshold', threshold_mask)
        self.masks = ('subpixel', mask)
        self.masks['shift'] = shift_mask
        self.masks['threshold'] = threshold_mask
        self.masks['subpixel'] = mask

    def suppress(self, suppression_func=spf.correlation, clean_keys=[], **kwargs):
    def suppress(self, suppression_func=spf.correlation, clean_keys=[], maskname='suppression', **kwargs):
        """
        Apply a disc based suppression algorithm to get a good spatial
        distribution of high quality points, where the user defines some
@@ -324,7 +382,7 @@ class Edge(dict, MutableMapping):
        smask, k = od.spatial_suppression(merged, domain, **kwargs)

        mask[mask] = smask
        self.masks = ('suppression', mask)
        self.masks[maskname] = mask

    def plot_source(self, ax=None, clean_keys=[], **kwargs):  # pragma: no cover
        matches, mask = self.clean(clean_keys=clean_keys)
@@ -407,9 +465,6 @@ class Edge(dict, MutableMapping):
                                   returns the overlap area
                                   covered by the keypoints
        """
        if not isinstance(self.matches, pd.DataFrame):
            raise AttributeError('Edge needs to have features extracted and matched')
            return
        matches, mask = self.clean(clean_keys)
        source_array = self.source.get_keypoint_coordinates(index=matches['source_idx']).values

@@ -448,86 +503,11 @@ class Edge(dict, MutableMapping):
        voronoi = cg.vor(self, clean_keys, **kwargs)
        self.matches = pd.concat([self.matches, voronoi[1]['vor_weights']], axis=1)

    def decompose(self, maxiterations=3):
        """
        Apply coupled decomposition to the images and
        match identified sub-images

        Parameters
        ----------
        maxiterations : int
                        The number of iterations. Appropriate values:

                        | Number of megapixels | k |
                        |----------------------|---|
                        | m < 10               |1-2|
                        | 10 < m < 30          | 3 |
                        | 30 < m < 100         | 4 |
                        | 100 < m < 1000       | 5 |
                        | m > 1000             | 6 |


    def compute_overlap(self, **kwargs):
        """
        pass

    def get_keypoints(self, node, clean_keys):
        """

        Returns a list of keypoint coordinates that match the specified
        paramaters

        Parameters
        ----------
        node :      str or Node
                    Can be "source" or "destination" based on which node we're
                    pulling keypoint data for; Also can pass Node obj itself

        clean_keys :    list
                        List of clean key strings

        Return
        ------
        masked_keypts : Dataframe
                        Dataframe of keypoints that match the specified masks
                        on the specified node
        Estimate a source and destination minimum bounding rectangle, in
        pixel space
        """

        # Assert parameter types are correct
        try:
            assert (isinstance(node, str) or isinstance(node, Node))
        except AssertionError:
            raise TypeError('Parameter "node" must be of type str or type Node')
        try:
            assert isinstance(clean_keys, list)
        except AssertionError:
            raise TypeError('Parameter "clean_keys" must be of type list')

        # If node param is a string, make sure it's one of the right strings
        if isinstance(node, str):
            try:
                assert (node in ["source", "destination"])
                # Define the node if str is passed as param
                if node == "source":
                    node = self.source
                elif node == "destination":
                    node = self.destination
            except AssertionError:
                raise KeyError('node" parameter must be "source"' +
                               'or "destination"')

        # Get cleaned, combined src & dst keypt df for this edge ("matches")
        matches, mask = self.clean(clean_keys)

        # Grab the keypt indices filtered by clean_keys as ints, pandas
        # complains when you use them as indicies if they're not ints
        if node == self.source:
            keypt_indices = matches["source_idx"].astype(int)
        elif node == self.destination:
            keypt_indices = matches["destination_idx"].astype(int)

        # Get all keypts for the specified node
        all_keypts = node.get_keypoints()
        # Return keypts @ masked indecies for the node
        masked_keypts = all_keypts.iloc[keypt_indices].sort_index()

        return masked_keypts
        self.overlap_latlon_coords, self["source_mbr"], self["destin_mbr"] = self.source.geodata.compute_overlap(self.destination.geodata, **kwargs)
+73 −17
Original line number Diff line number Diff line
import itertools
import math
import os
from time import gmtime, strftime
import warnings
@@ -15,6 +16,12 @@ from autocnet.graph.node import Node
from autocnet.io import network as io_network
from autocnet.vis.graph_view import plot_graph, cluster_plot

# The total number of pixels squared that can fit into the keys number of GB of RAM for SIFT.
MAXSIZE = {0:None,
           2:6250,
           4:8840,
           8:12500,
           12:15310}

class CandidateGraph(nx.Graph):
    """
@@ -80,6 +87,20 @@ class CandidateGraph(nx.Graph):
                eq = False
        return eq

    @property
    def maxsize(self):
        if not hasattr(self, '_maxsize'):
            self._maxsize = MAXSIZE[0]
        return self._maxsize

    @maxsize.setter
    def maxsize(self, value):
        if not value in MAXSIZE.keys():
            raise KeyError('Value must be in {}'.format(','.join(map(str,MAXSIZE.keys()))))
        else:
            self._maxsize = MAXSIZE[value]


    @classmethod
    def from_filelist(cls, filelist, basepath=None):
        """
@@ -197,25 +218,50 @@ class CandidateGraph(nx.Graph):

        raise NotImplementedError

    def extract_features(self, *args, **kwargs):
    def extract_features(self, band=1, *args, **kwargs):  # pragma: no cover
        """
        Extracts features from each image in the graph and uses the result to assign the
        node attributes for 'handle', 'image', 'keypoints', and 'descriptors'.
        """
        for i, node in self.nodes_iter(data=True):
            array = node.geodata.read_array(band=band)
            node.extract_features(array, *args, **kwargs),

    def extract_features_with_downsampling(self, downsample_amount=None, *args, **kwargs): # pragma: no cover
        """
        Extract interest points from a downsampled array.  The array is downsampled
        by the downsample_amount keyword using the Lanconz downsample amount.  If the
        downsample keyword is not supplied, compute a downsampling constant as the
        total array size divided by the network maxsize attribute.

        Parameters
        ----------
        method : {'orb', 'sift', 'fast'}
                 The descriptor method to be used

        extractor_parameters : dict
                               A dictionary containing OpenCV SIFT parameters names and values.

        downsampling : int
                       The divisor to image_size to down sample the input image.
        downsample_amount : int
                            The amount of downsampling to apply to the image
        """
        for i, node in self.nodes_iter(data=True):
            image = node.get_array()
            node.extract_features(image, *args, **kwargs),
            if downsample_amount == None:
                total_size = node.geodata.raster_size[0] * node.geodata.raster_size[1]
                downsample_amount = math.ceil(total_size / self.maxsize**2)
            node.extract_features_with_downsampling(downsample_amount, *args, **kwargs)

    def extract_features_with_tiling(self, tilesize=1000, overlap=500, *args, **kwargs): #pragma: no cover
        for i, node in self.nodes_iter(data=True):
            print('Processing {}'.format(node['image_name']))
            node.extract_features_with_tiling(tilesize=tilesize, overlap=overlap, *args, **kwargs)

    def extract_subsets(self, *args, **kwargs):
        """
        Extracts features from each image in those regions estimated to be
        overlapping.

        *args and **kwargs are passed to the feature extractor.  For example,
        passing method='sift' will cause the extractor to use the sift method.
        """
        for source, destination, e in self.edges_iter(data=True):
            e.extract_subset(*args, **kwargs)


    def save_features(self, out_path, nodes=[], **kwargs):
        """
@@ -279,6 +325,17 @@ class CandidateGraph(nx.Graph):
        """
        self.apply_func_to_edges('decompose_and_match', *args, **kwargs)

    def estimate_mbrs(self, *args, **kwargs):
        """
        For each edge, estimate the overlap and compute a minimum bounding
        rectangle (mbr) in pixel space.

        See Also
        --------
        autocnet.graoh.edge.Edge.compute_mbr
        """
        self.apply_func_to_edges('estimate_mbr', *args, **kwargs)

    def compute_clusters(self, func=markov_cluster.mcl, *args, **kwargs):
        """
        Apply some graph clustering algorithm to compute a subset of the global
@@ -374,7 +431,7 @@ class CandidateGraph(nx.Graph):

        See Also
        --------
        autocnet.matcher.outlier_detector.DistanceRatio.compute
        autocnet.matcher.cpu_outlier_detector.DistanceRatio.compute
        '''
        self.apply_func_to_edges('ratio_check', *args, **kwargs)

@@ -385,7 +442,7 @@ class CandidateGraph(nx.Graph):
        See Also
        --------
        autocnet.graph.edge.Edge.compute_homography
        autocnet.matcher.outlier_detector.compute_homography
        autocnet.matcher.cpu_outlier_detector.compute_homography
        '''
        self.apply_func_to_edges('compute_homography', *args, **kwargs)

@@ -395,7 +452,7 @@ class CandidateGraph(nx.Graph):

        See Also
        --------
        autocnet.matcher.outlier_detector.compute_fundamental_matrix
        autocnet.matcher.cpu_outlier_detector.compute_fundamental_matrix
        '''
        self.apply_func_to_edges('compute_fundamental_matrix', *args, **kwargs)

@@ -415,7 +472,7 @@ class CandidateGraph(nx.Graph):

        See Also
        --------
        autocnet.matcher.outlier_detector.SpatialSuppression
        autocnet.matcher.cpu_outlier_detector.SpatialSuppression
        '''
        self.apply_func_to_edges('suppress', *args, **kwargs)

@@ -512,7 +569,7 @@ class CandidateGraph(nx.Graph):
        """
        return plot_graph(self, ax=ax, **kwargs)

    def plot_cluster(self, ax=None, **kwargs):
    def plot_cluster(self, ax=None, **kwargs):  # pragma: no cover
        """
        Plot the graph based on the clusters generated by
        the markov clustering algorithm
@@ -644,8 +701,7 @@ class CandidateGraph(nx.Graph):

        # get all edges that have matches
        matches = [(u, v) for u, v, edge in self.edges_iter(data=True)
                   if hasattr(edge, 'matches') and
                   not edge.matches is None]
                   if not edge.matches.empty]

        return self.create_edge_subgraph(matches)

+125 −74

File changed.

Preview size limit exceeded, changes collapsed.

Loading