Commit 149162af authored by jay's avatar jay
Browse files

Updates edge to be more inline with networkx style using attr dict

parent f9a4995e
Loading
Loading
Loading
Loading
+86 −59
Changes for autocnet/graph/edge.py: 86 added lines, 59 removed lines.
Original line number Diff line number Diff line
from functools import wraps
import warnings
from collections import MutableMapping

@@ -8,7 +9,7 @@ from scipy.spatial.distance import cdist

import autocnet
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
@@ -17,6 +18,7 @@ from autocnet.vis.graph_view import plot_edge, plot_node, plot_edge_decompositio
from autocnet.cg import cg



class Edge(dict, MutableMapping):
    """
    Attributes
@@ -40,8 +42,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 """
@@ -69,7 +74,7 @@ class Edge(dict, MutableMapping):

        return eq

    @property
    """@property
    def masks(self):
        mask_lookup = {'fundamental': 'fundamental_matrix'}
        if not hasattr(self, '_masks'):
@@ -95,10 +100,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):

@@ -116,22 +118,44 @@ class Edge(dict, MutableMapping):
        """
        pass

    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 hasattr(self, 'matches'):
            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 hasattr(self, 'matches'):
    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.
@@ -151,9 +175,6 @@ class Edge(dict, MutableMapping):
        autocnet.transformation.transformations.FundamentalMatrix

        """
        if not hasattr(self, 'matches'):
            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
@@ -174,9 +195,38 @@ 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 compute_homography(self, method='ransac', clean_keys=[], pid=None, **kwargs):
    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)

        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
@@ -195,12 +245,6 @@ class Edge(dict, MutableMapping):
        mask : ndarray
               Boolean array of the outliers
        """

        if hasattr(self, 'matches'):
            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'])
@@ -210,7 +254,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,
@@ -288,19 +332,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
@@ -334,7 +377,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)
@@ -417,9 +460,6 @@ class Edge(dict, MutableMapping):
                                   returns the overlap area
                                   covered by the keypoints
        """
        if self.matches is None:
            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

@@ -458,24 +498,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
        Estimate a source and destination minimum bounding rectangle, in
        pixel space
        """
        self.overlap_latlon_coords, self["source_mbr"], self["destin_mbr"] = self.source.geodata.compute_overlap(self.destination.geodata, **kwargs)
+4 −6
Changes for autocnet/graph/tests/test_edge.py: 4 added lines, 6 removed lines.
Original line number Diff line number Diff line
import unittest
from unittest.mock import Mock
from unittest.mock import MagicMock
from unittest.mock import Mock, MagicMock

import ogr
import pandas as pd
@@ -47,8 +46,7 @@ class TestEdge(unittest.TestCase):


    def test_compute_fundamental_matrix(self):
        with self.assertRaises(AttributeError):
            self.edge.compute_fundamental_matrix()
        pass

    def test_edge_overlap(self):
        e = edge.Edge()
@@ -86,7 +84,7 @@ class TestEdge(unittest.TestCase):
                            [0, 3, 1, 3],
                            [0, 4, 1, 4]]

        matches_df = pd.DataFrame(data = keypoint_matches, columns = ['source_image', 'source_idx', 'destination_image', 'destination_idx'])
        matches_df = pd.DataFrame(keypoint_matches, columns = ['source_image', 'source_idx', 'destination_image', 'destination_idx'])
        e = edge.Edge()
        source_node = MagicMock(spec = node.Node())
        destination_node = MagicMock(spec = node.Node())
@@ -126,7 +124,7 @@ class TestEdge(unittest.TestCase):

        e.matches = matches_df

        self.assertRaises(AttributeError, cg.edge[0][1].coverage)
        #self.assertRaises(AttributeError, cg.edge[0][1].coverage)
        self.assertEqual(e.coverage(), 0.3)

    def test_voronoi_transform(self):
+10 −9
Changes for autocnet/io/network.py: 10 added lines, 9 removed lines.
Original line number Diff line number Diff line
@@ -51,12 +51,13 @@ def save(network, projectname):

        # Write the array node_attributes to hdf
        for n, data in network.nodes_iter(data=True):
            if data.descriptors is not None:
                grp = data['node_id']
                np.savez('{}.npz'.format(data['node_id']),
                         descriptors=data.descriptors,
                     keypoints=data._keypoints,
                     keypoints_idx=data._keypoints.index,
                     keypoints_columns=data._keypoints.columns)
                         keypoints=data.keypoints,
                         keypoints_idx=data.keypoints.index,
                         keypoints_columns=data.keypoints.columns)
                pzip.write('{}.npz'.format(data['node_id']))
                os.remove('{}.npz'.format(data['node_id']))

@@ -64,14 +65,15 @@ def save(network, projectname):
        for s, d, data in network.edges_iter(data=True):
            if s > d:
                s, d = d, s
            if data.matches is not None:
                grp = str((s,d))
                np.savez('{}_{}.npz'.format(s, d),
                         matches=data.matches,
                         matches_idx=data.matches.index,
                         matches_columns=data.matches.columns,
                     _masks=data._masks,
                     _masks_idx=data._masks.index,
                     _masks_columns=data._masks.columns)
                         masks=data.masks,
                         masks_idx=data.masks.index,
                         masks_columns=data.masks.columns)
                pzip.write('{}_{}.npz'.format(s, d))
                os.remove('{}_{}.npz'.format(s, d))

@@ -87,7 +89,6 @@ def json_numpy_obj_hook(dct):
    return dct

def load(projectname):

    with ZipFile(projectname, 'r') as pzip:
        # Read the graph object
        with pzip.open('graph.json', 'r') as g:
@@ -102,6 +103,7 @@ def load(projectname):
        for d in data['nodes']:
            n = Node(image_name=d['image_name'], image_path=d['image_path'], node_id=d['id'])
            n['hash'] = d['hash']
            n['downsample_amount'] = d.get('downsample_amount', 1)
            try:
                # Load the byte stream for the nested npz file into memory and then unpack
                n.load_features(BytesIO(pzip.read('{}.npz'.format(d['id']))))
@@ -118,11 +120,10 @@ def load(projectname):
            edge['weights'] = e['weights']
            try:
                nzf = np.load(BytesIO(pzip.read('{}_{}.npz'.format(e['source'], e['target']))))
                edge._masks = pd.DataFrame(nzf['_masks'], index=nzf['_masks_idx'], columns=nzf['_masks_columns'])
                edge.masks = pd.DataFrame(nzf['masks'], index=nzf['masks_idx'], columns=nzf['masks_columns'])
                edge.matches = pd.DataFrame(nzf['matches'], index=nzf['matches_idx'], columns=nzf['matches_columns'])
            except:
                pass
            # Add a mock edge
            cg.edge[e['source']][e['target']] = edge

    return cg
+27 −2
Changes for autocnet/vis/graph_view.py: 27 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -4,6 +4,13 @@ import networkx as nx
from matplotlib import pyplot as plt
import matplotlib

from scipy.misc import imresize

def downsample(array, amount):
    return imresize(array,
                      (int(array.shape[0] / amount),
                      int(array.shape[1] / amount)),
                      interp='lanczos')

def plot_graph(graph, ax=None, cmap='Spectral', labels=False, font_size=12, clusters=None, **kwargs):
    """
@@ -48,7 +55,7 @@ def plot_graph(graph, ax=None, cmap='Spectral', labels=False, font_size=12, clus
    return ax


def plot_node(node, ax=None, clean_keys=[], index_mask=None, **kwargs):
def plot_node(node, ax=None, clean_keys=[], index_mask=None, downsampling=1, **kwargs):
    """
    Plot the array and keypoints for a given node.

@@ -83,6 +90,11 @@ def plot_node(node, ax=None, clean_keys=[], index_mask=None, **kwargs):

    array = node.get_array(band)

    if isinstance(downsampling, bool):
        downsampling = node['downsample_amount']

    array = downsample(array, downsampling)

    ax.set_title(node['image_name'])
    ax.margins(tight=True)
    ax.axis('off')
@@ -182,7 +194,7 @@ def plot_edge_decomposition(edge, ax=None, clean_keys=[], image_space=100,

    return ax

def plot_edge(edge, ax=None, clean_keys=[], image_space=100,
def plot_edge(edge, ax=None, clean_keys=[], image_space=100, downsampling=1,
              scatter_kwargs={}, line_kwargs={}, image_kwargs={}):
    """
    Plot the correspondences for a given edge
@@ -201,6 +213,8 @@ def plot_edge(edge, ax=None, clean_keys=[], image_space=100,
    image_space : int
                  The number of pixels to insert between the images

    downsample : bool

    scatter_kwargs : dict
                     of MatPlotLib arguments to be applied to the scatter plots

@@ -227,8 +241,19 @@ def plot_edge(edge, ax=None, clean_keys=[], image_space=100,
    ax.axis('off')

    # Image plotting
    if isinstance(downsampling, bool):
        downsample_source = edge.source['downsample_amount']
    else:
        downsample_source = downsampling
    source_array = edge.source.get_array()
    source_array = downsample(source_array, downsample_source)

    if isinstance(downsampling, bool):
        downsample_destin = edge.destination['downsample_amount']
    else:
        downsample_destin = downsampling
    destination_array = edge.destination.get_array()
    destination_array = downsample(destination_array, downsample_destin)
    
    s_shape = source_array.shape
    d_shape = destination_array.shape