Commit 49abfae9 authored by Adam Paquette's avatar Adam Paquette
Browse files

Added tests for voronoi methods, and completed voronoi implementation in the code base.

parent bb4728df
Loading
Loading
Loading
Loading
+45 −44
Changes for autocnet/cg/cg.py: 45 added lines, 44 removed lines.
Original line number Diff line number Diff line
@@ -10,6 +10,7 @@ from scipy.spatial import ConvexHull
from scipy.spatial import Voronoi
from shapely.geometry import Polygon, Point
from shapely.affinity import scale
from shapely.ops import unary_union
import cv2

from autocnet.utils import utils
@@ -115,20 +116,9 @@ def get_area(poly1, poly2):

def vor(graph, clean_keys, s=30):
        """
        Creates a voronoi diagram for a complete graph using either the coordinate
        transformation or using the homography based around a source node.

        The coordinate transformation uses the footprint of each image to
        calculate an intersection between the image set, then transforms the vertices of
        the intersection back into pixel space.

        If a coordinate transform does not exist, use the homography to project the n - 1 images
        onto the source image, producing an area of intersection.

        The intersection vertices are then scaled by a factor of s (default 30), this accounts for the
        areas of the voronoi that would be missed if the scaled vertices were not included into the
        voronoi calculation.

        Creates a voronoi diagram for all edges in a graph, and assigns a given
        weight to each edge. This is based around voronoi polygons generated
        by scipy's voronoi method, to determine if an image has significant coverage.

        Parameters
        ----------
@@ -139,27 +129,19 @@ def vor(graph, clean_keys, s=30):
                     Of strings used to apply masks to omit correspondences

        s : int
            offset for the corners of the image

        Returns
        -------
        vor : object
              Scipy Voronoi object

        voronoi_df : dataframe
                     3 column pandas dataframe of x, y, and weights

            Offset for the corners of the image
        """
        neighbors_dict = nx.degree(graph)
        if not all(value == len(graph.neighbors(graph.nodes()[0])) for value in neighbors_dict.values()):
            warnings.warn('The given graph is not complete and may yield garbage.')

        intersection, proj_gdf, source_gdf = compute_intersection(graph.nodes()[0], graph, clean_keys)
        intersection, proj_gdf, source_gdf = compute_intersection(graph, graph.nodes()[0], clean_keys)

        intersection_ind = intersection.query('overlaps_all == True').index.values[0]

        source_node = graph.nodes()[0]
        for e in graph.edges():
            # If the source node changes, change the projection space to match it
            if e[0] != source_node:
                source_node = e[0]
                intersection, proj_gdf, source_gdf = compute_intersection(source_node, graph, clean_keys)
@@ -169,19 +151,20 @@ def vor(graph, clean_keys, s=30):
            kps = edge.get_keypoints('source', clean_keys=clean_keys, homogeneous=True)

            kps['geometry'] = kps.apply(lambda x: Point(x['x'], x['y']), axis=1)
            kps.mask = kps['geometry'].apply(lambda x: intersection.geometry.contains(x).all())
            kps.mask = kps['geometry'].apply(lambda x: intersection.geometry.contains(x).any())

            # Creates a mask for displaying the voronoi points
            # Currently erronious and produces NaN values
            # Currently erronious and produces NaN values in place
            # of true
            # matches, mask = edge.clean(clean_keys = clean_keys)
            # mask[mask] = kps.mask
            # edge.masks = ('voronoi', mask)

            keypoints = []
            kps[kps.mask].apply(lambda x: keypoints.append((x['x'], x['y'])), axis=1)
            coords = source_gdf.geometry.apply(lambda x:scale(x, s, s).exterior.coords)

            for point in coords:
                keypoints = np.vstack((keypoints, point))
            scaled_coords = np.array(scale(source_gdf.geometry[0], s, s).exterior.coords)
            keypoints = np.vstack((keypoints, scaled_coords))

            vor = Voronoi(keypoints)
            voronoi_df = pd.DataFrame(data=kps, columns=['x', 'y', 'weight'])
@@ -203,8 +186,12 @@ def vor(graph, clean_keys, s=30):
            edge['weights']['vor_weight'] = voronoi_df['weight']


def compute_intersection(source, graph, clean_keys=[]):
def compute_intersection(graph, source, clean_keys=[]):
    """
    Computes the intersections of images in a graph based on the
    connections between nodes. The method takes every node in a graph,
    sees who is connected to it, then computes an intersection based on
    those connections.

    Parameters
    ----------
@@ -219,7 +206,15 @@ def compute_intersection(source, graph, clean_keys=[]):

    Returns
    -------
    intersection :
    intersection : dataframe
                   4 column dataframe of source_node, proj_node, geometry,
                   and overlaps_all

    proj_gdf : dataframe
               2 column dataframe of proj_geom, proj_node

    source_gdf : dataframe
                 2 column dataframe of geometry, source_node
    """
    if type(source) is int:
        source = graph.node[source]
@@ -227,7 +222,7 @@ def compute_intersection(source, graph, clean_keys=[]):
    source_corners = source.geodata.xy_corners
    source_poly = Polygon(source_corners)

    source_gdf = gpd.GeoDataFrame({'source_geom': [source_poly], 'source_node': [source['node_id']]}).set_geometry('source_geom')
    source_gdf = gpd.GeoDataFrame({'geometry': [source_poly], 'source_node': [source['node_id']]})

    proj_list = []
    proj_nodes = []
@@ -240,8 +235,16 @@ def compute_intersection(source, graph, clean_keys=[]):
        destination = graph.node[n]
        destination_corners = destination.geodata.xy_corners

        try:
            edge = graph.edge[source['node_id']][destination['node_id']]

            # If the source image has coordinate transformation data
            if (source.geodata.coordinate_transformation.this is not None) and \
               (destination.geodata.coordinate_transformation.this is not None):
                proj_poly = Polygon(destination.geodata.xy_corners)

            # Else, use the homography transform to get an intersection of the two images
            else:
                # Will still need the check but the helper function will make these calls much easier to understand
                if source['node_id'] > destination['node_id']:
                    kp2 = edge.get_keypoints('source', clean_keys=clean_keys, homogeneous=True)
@@ -250,13 +253,6 @@ def compute_intersection(source, graph, clean_keys=[]):
                    kp2 = edge.get_keypoints('destination', clean_keys=clean_keys, homogeneous=True)
                    kp1 = edge.get_keypoints('source', clean_keys=clean_keys, homogeneous=True)

        # If the source image has coordinate transformation data
        if (source.geodata.coordinate_transformation.this is None) and \
           (destination.geodata.coordinate_transformation.this is None):
            proj_poly = Polygon(destination.geodata.xy_corners)

        # Else, use the homography transform to get an intersection of the two images
        else:
                H, mask = cv2.findHomography(kp2.values, kp1.values, cv2.RANSAC, 2.0)
                proj_corners = []
                for c in destination_corners:
@@ -267,14 +263,19 @@ def compute_intersection(source, graph, clean_keys=[]):
                    proj_corners.append((x, y))

                proj_poly = Polygon(proj_corners)
        except:
            continue

        proj_list.append(proj_poly)
        proj_nodes.append(n)

    proj_gdf = gpd.GeoDataFrame({'proj_geom': proj_list, 'proj_node': proj_nodes}).set_geometry('proj_geom')
    proj_gdf = gpd.GeoDataFrame({'geometry': proj_list, 'proj_node': proj_nodes})

    intersect_gdf = gpd.overlay(source_gdf, proj_gdf, how='intersection')
    intersect_gdf = intersect_gdf.rename(columns = {'geometry':'intersect_geom'}).set_geometry('intersect_geom')
    intersect_gdf['overlaps_all'] = intersect_gdf.geometry.apply(lambda x:proj_gdf.geometry.contains(scale(x, .9, .9)).all())
    intersection = intersect_gdf.query('overlaps_all == True').set_geometry('intersect_geom')
    intersect_gdf['overlaps_all'] = intersect_gdf.geometry.apply(lambda z: proj_gdf.geometry.contains(scale(z, .9, .9)).all())
    intersection = intersect_gdf.query('overlaps_all == True')

    if len(intersection) == 0:
        new_poly = unary_union(intersect_gdf.geometry)
        intersection = gpd.GeoDataFrame({'source_node': source['node_id'], 'geometry': new_poly, 'overlaps_all': True})
    return intersection, proj_gdf, source_gdf
+218 −0
Changes for autocnet/cg/tests/test_cg.py: 218 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -4,9 +4,17 @@ import unittest
sys.path.insert(0, os.path.abspath('..'))

import numpy as np
import pandas as pd

from .. import cg
from osgeo import ogr
from unittest.mock import Mock, MagicMock
from plio.io import io_gdal

from autocnet.graph.node import Node
from autocnet.graph.network import CandidateGraph
from autocnet.graph.edge import Edge
from autocnet.utils.utils import array_to_poly


class TestArea(unittest.TestCase):
@@ -33,3 +41,213 @@ class TestArea(unittest.TestCase):
        self.assertEqual(info[1], 400)
        self.assertAlmostEqual(info[0], 14.285714285)

    def test_voronoi_homography(self):
        source_keypoint_df = pd.DataFrame({'x': (15, 18, 18, 12, 12), 'y': (6, 10, 15, 15, 10)})
        destination_keypoint_df = pd.DataFrame({'x': (5, 8, 8, 2, 2), 'y': (1, 5, 10, 10, 5)})
        keypoint_matches = [[0, 0, 1, 0],
                            [0, 1, 1, 1],
                            [0, 2, 1, 2],
                            [0, 3, 1, 3],
                            [0, 4, 1, 4]]

        matches_df = pd.DataFrame(data=keypoint_matches, columns=['source_image', 'source_idx',
                                                                  'destination_image', 'destination_idx'])
        # Source and Destination Node Setup
        source_node = MagicMock(spec=Node())
        destination_node = MagicMock(spec=Node())

        source_node.get_keypoint_coordinates = MagicMock(return_value=source_keypoint_df)
        destination_node.get_keypoint_coordinates = MagicMock(return_value=destination_keypoint_df)

        source_geodata = Mock(spec=io_gdal.GeoDataset)
        destination_geodata = Mock(spec=io_gdal.GeoDataset)

        source_node.geodata = source_geodata
        destination_node.geodata = destination_geodata

        source_node.geodata.coordinate_transformation.this = None
        destination_node.geodata.coordinate_transformation.this = None

        source_corners = [(0, 0),
                          (20, 0),
                          (20, 20),
                          (0, 20)]

        destination_corners = [(0, 0),
                               (20, 0),
                               (20, 20),
                               (0, 20)]

        source_node.geodata.xy_corners = source_corners
        destination_node.geodata.xy_corners = destination_corners

        # Edge Setup
        e = Edge(source=source_node, destination=destination_node)

        e.clean = MagicMock(return_value=(matches_df, None))
        e.matches = matches_df

        def side_effect(node, clean_keys, **kwargs):
            if type(node) is str:
                node = node.lower()

            if isinstance(node, Node):
                node = node['node_id']

            if node == "source" or node == "s" or node == source_node['node_id']:
                return e.source.get_keypoint_coordinates().copy(deep=True)
            if node == "destination" or node == "d" or node == destination_node['node_id']:
                return e.destination.get_keypoint_coordinates().copy(deep=True)

        my_dict_source = {'node_id':0}

        def getitem_source(name):
            return my_dict_source[name]

        my_dict_destination = {'node_id':1}

        def getitem_destination(name):
            return my_dict_destination[name]

        source_node.__getitem__.side_effect = getitem_source
        destination_node.__getitem__.side_effect = getitem_destination

        e.get_keypoints = MagicMock(side_effect=side_effect)

        cang = MagicMock(spec=CandidateGraph())

        cang.nodes = MagicMock(return_value=([0, 1]))
        cang.node = [source_node, destination_node]
        cang.nodes_iter = MagicMock(return_value=([0, 1]))
        cang.neighbors = MagicMock(return_value=[1])
        cang.edges = MagicMock(return_value=([(0, 1)]))
        cang.edge = {0: {1: e}, 1: {0: e}}

        cg.vor(cang, clean_keys=['fundamental'])
        self.assertAlmostEqual(e['weights']['vor_weight'][0], 22.5)
        self.assertAlmostEqual(e['weights']['vor_weight'][1], 26.25)
        self.assertAlmostEqual(e['weights']['vor_weight'][2], 37.5)
        self.assertAlmostEqual(e['weights']['vor_weight'][3], 37.5)
        self.assertAlmostEqual(e['weights']['vor_weight'][4], 26.25)

    def test_voronoi_coord(self):
        source_keypoint_df = pd.DataFrame({'x': (15, 18, 18, 12, 12), 'y': (6, 10, 15, 15, 10)})
        destination_keypoint_df = pd.DataFrame({'x': (5, 8, 8, 2, 2), 'y': (1, 5, 10, 10, 5)})

        keypoint_df = pd.DataFrame({'x': (15, 18, 18, 12, 12), 'y': (6, 10, 15, 15, 10)})
        keypoint_matches = [[0, 0, 1, 0],
                            [0, 1, 1, 1],
                            [0, 2, 1, 2],
                            [0, 3, 1, 3],
                            [0, 4, 1, 4]]

        matches_df = pd.DataFrame(data=keypoint_matches, columns=['source_image', 'source_idx',
                                                                  'destination_image', 'destination_idx'])

        source_node = MagicMock(spec=Node())
        destination_node = MagicMock(spec=Node())

        source_node.get_keypoint_coordinates = MagicMock(return_value=source_keypoint_df)
        destination_node.get_keypoint_coordinates = MagicMock(return_value=destination_keypoint_df)

        source_geodata = Mock(spec=io_gdal.GeoDataset)
        destination_geodata = Mock(spec=io_gdal.GeoDataset)

        source_node.geodata = source_geodata
        destination_node.geodata = destination_geodata

        e = Edge(source=source_node, destination = destination_node)

        e.clean = MagicMock(return_value=(matches_df, None))
        e.matches = matches_df

        source_node = MagicMock(spec=Node())
        destination_node = MagicMock(spec=Node())

        cang = MagicMock(spec=CandidateGraph())

        cang.nodes = MagicMock(return_value=([0, 1]))
        cang.node = [source_node, destination_node]
        cang.nodes_iter = MagicMock(return_value=([0, 1]))
        cang.neighbors = MagicMock(return_value=([0]))
        cang.edges = MagicMock(return_value=([(0, 1)]))
        cang.edge = {0: {1: e}, 1: {0: e}}

        source_node.get_keypoint_coordinates = MagicMock(return_value=keypoint_df)
        destination_node.get_keypoint_coordinates = MagicMock(return_value=keypoint_df)
        source_node['node_id'] = 0
        destination_node['node_id'] = 1

        def side_effect(node, clean_keys, **kwargs):
            if type(node) is str:
                node = node.lower()

            if isinstance(node, Node):
                node = node['node_id']

            if node == "source" or node == "s" or node == source_node['node_id']:
                return e.source.get_keypoint_coordinates()
            if node == "destination" or node == "d" or node == destination_node['node_id']:
                return e.destination.get_keypoint_coordinates()

        e.get_keypoints = MagicMock(side_effect=side_effect)

        e.source = source_node
        e.destination = destination_node

        source_geodata = Mock(spec=io_gdal.GeoDataset)
        destination_geodata = Mock(spec=io_gdal.GeoDataset)

        e.source.geodata = source_geodata
        e.destination.geodata = destination_geodata

        source_corners = [(0, 0),
                          (20, 0),
                          (20, 20),
                          (0, 20)]

        destination_corners = [(10, 5),
                               (30, 5),
                               (30, 25),
                               (10, 25)]

        source_xy_extent = [(0, 20), (0, 20)]

        destination_xy_extent = [(10, 30), (5, 25)]

        source_poly = array_to_poly(source_corners)
        destination_poly = array_to_poly(destination_corners)

        vals = {(10, 5): (10, 5), (20, 5): (20, 5), (20, 20): (20, 20), (10, 20): (10, 20)}

        def latlon_to_pixel(i, j):
            return vals[(i, j)]

        my_dict_source = {'node_id': 0}

        def getitem_source(name):
            return my_dict_source[name]

        my_dict_destination = {'node_id': 1}

        def getitem_destination(name):
            return my_dict_destination[name]

        source_node.__getitem__.side_effect = getitem_source
        destination_node.__getitem__.side_effect = getitem_destination

        e.source.geodata.latlon_to_pixel = MagicMock(side_effect=latlon_to_pixel)
        e.destination.geodata.latlon_to_pixel = MagicMock(side_effect=latlon_to_pixel)

        e.source.geodata.footprint = source_poly
        e.source.geodata.xy_corners = source_corners
        e.source.geodata.xy_extent = source_xy_extent
        e.destination.geodata.footprint = destination_poly
        e.destination.geodata.xy_corners = destination_corners
        e.destination.geodata.xy_extent = destination_xy_extent
        cg.vor(cang, clean_keys=['fundamental'])
        self.assertAlmostEqual(e['weights']['vor_weight'][0], 22.5)
        self.assertAlmostEqual(e['weights']['vor_weight'][1], 26.25)
        self.assertAlmostEqual(e['weights']['vor_weight'][2], 37.5)
        self.assertAlmostEqual(e['weights']['vor_weight'][3], 37.5)
        self.assertAlmostEqual(e['weights']['vor_weight'][4], 26.25)
+0 −2
Changes for autocnet/graph/network.py: 0 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -701,7 +701,6 @@ class CandidateGraph(nx.Graph):
        : list
          A list of lists of node ids that make up maximum complete subgraphs of the given graph
        """

        if node_id is not None:
            return list(nx.cliques_containing_node(self, nodes=node_id))
        else:
@@ -718,5 +717,4 @@ class CandidateGraph(nx.Graph):
                     Strings used to apply masks to omit correspondences

        """

        vor(self, clean_keys)
+0 −121
Changes for autocnet/graph/tests/test_edge.py: 0 added lines, 121 removed lines.
Original line number Diff line number Diff line
@@ -128,124 +128,3 @@ class TestEdge(unittest.TestCase):

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

    def test_voronoi_transform(self):
        keypoint_df = pd.DataFrame({'x': (15, 18, 18, 12, 12), 'y': (5, 10, 15, 15, 10)})
        keypoint_matches = [[0, 0, 1, 0],
                            [0, 1, 1, 1],
                            [0, 2, 1, 2],
                            [0, 3, 1, 3],
                            [0, 4, 1, 4]]

        matches_df = pd.DataFrame(data=keypoint_matches, columns=['source_image', 'source_idx',
                                                                  'destination_image', 'destination_idx'])
        e = edge.Edge()

        e.clean = MagicMock(return_value=(matches_df, None))
        e.matches = matches_df

        source_node = MagicMock(spec=node.Node())
        destination_node = MagicMock(spec=node.Node())

        source_node.get_keypoint_coordinates = MagicMock(return_value=keypoint_df)
        destination_node.get_keypoint_coordinates = MagicMock(return_value=keypoint_df)

        e.source = source_node
        e.destination = destination_node

        source_geodata = Mock(spec=io_gdal.GeoDataset)
        destination_geodata = Mock(spec=io_gdal.GeoDataset)

        e.source.geodata = source_geodata
        e.destination.geodata = destination_geodata

        source_corners = [(0, 0),
                          (20, 0),
                          (20, 20),
                          (0, 20)]

        destination_corners = [(10, 5),
                               (30, 5),
                               (30, 25),
                               (10, 25)]

        source_poly = array_to_poly(source_corners)
        destination_poly = array_to_poly(destination_corners)

        def latlon_to_pixel(i, j):
            return vals[(i, j)]

        e.source.geodata.latlon_to_pixel = MagicMock(side_effect=latlon_to_pixel)
        e.destination.geodata.latlon_to_pixel = MagicMock(side_effect=latlon_to_pixel)

        e.source.geodata.footprint = source_poly
        e.source.geodata.xy_corners = source_corners
        e.destination.geodata.footprint = destination_poly
        e.destination.geodata.xy_corners = destination_corners

        vals = {(10, 5): (10, 5), (20, 5): (20, 5), (20, 20): (20, 20), (10, 20): (10, 20)}

        weights = pd.DataFrame({"vor_weights": (19, 28, 37.5, 37.5, 28)})

        e.compute_weights(clean_keys=[])

        k = 0
        for i in e.matches['vor_weights']:
            self.assertAlmostEquals(i, weights['vor_weights'][k])
            k += 1

    def test_voronoi_homography(self):
        source_keypoint_df = pd.DataFrame({'x': (15, 18, 18, 12, 12), 'y': (5, 10, 15, 15, 10)})
        destination_keypoint_df = pd.DataFrame({'x': (5, 8, 8, 2, 2), 'y': (0, 5, 10, 10, 5)})
        keypoint_matches = [[0, 0, 1, 0],
                            [0, 1, 1, 1],
                            [0, 2, 1, 2],
                            [0, 3, 1, 3],
                            [0, 4, 1, 4]]

        matches_df = pd.DataFrame(data = keypoint_matches, columns=['source_image', 'source_idx',
                                                                    'destination_image', 'destination_idx'])
        e = edge.Edge()

        e.clean = MagicMock(return_value=(matches_df, None))
        e.matches = matches_df

        source_node = MagicMock(spec=node.Node())
        destination_node = MagicMock(spec=node.Node())

        source_node.get_keypoint_coordinates = MagicMock(return_value=source_keypoint_df)
        destination_node.get_keypoint_coordinates = MagicMock(return_value=destination_keypoint_df)

        e.source = source_node
        e.destination = destination_node

        source_geodata = Mock(spec=io_gdal.GeoDataset)
        destination_geodata = Mock(spec=io_gdal.GeoDataset)

        e.source.geodata = source_geodata
        e.destination.geodata = destination_geodata

        source_corners = [(0, 0),
                          (20, 0),
                          (20, 20),
                          (0, 20)]

        destination_corners = [(0, 0),
                               (20, 0),
                               (20, 20),
                               (0, 20)]

        e.source.geodata.coordinate_transformation.this = None
        e.destination.geodata.coordinate_transformation.this = None

        e.source.geodata.xy_corners = source_corners
        e.destination.geodata.xy_corners = destination_corners

        weights = pd.DataFrame({"vor_weights": (19, 28, 37.5, 37.5, 28)})

        e.compute_weights(clean_keys=[])

        k = 0
        for i in e.matches['vor_weights']:
            self.assertAlmostEquals(i, weights['vor_weights'][k])
            k += 1
+15 −0
Changes for autocnet/graph/tests/test_network.py: 15 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -165,3 +165,18 @@ def test_apply_func_to_edges(graph):

    assert not graph[0][2].masks['symmetry'].all()
    assert not graph[0][1].masks['symmetry'].all()


def test_cliques():
    graph = network.CandidateGraph()
    # for i in range(0, 8):
    #     graph.add_node(i)
    graph.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7])
    graph.add_edges_from([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3), (3, 4), (3, 7), (4, 5), (4, 6), (5, 6)])
    cliques = graph.compute_cliques()
    limited_cliques = graph.compute_cliques(node_id=3)
    print(cliques, limited_cliques)
    assert len(cliques) == 4
    assert len(limited_cliques) == 3
    assert cliques[2][0] == 3
    assert cliques[2][1] == 7
Loading