Commit 480187e5 authored by Adam Paquette's avatar Adam Paquette
Browse files

Transfered more code from the notebook to the code base. Also added tests and...

Transfered more code from the notebook to the code base. Also added tests and made some changes for pep8 compliance.
parent 387cc47a
Loading
Loading
Loading
Loading
+15 −101
Original line number Diff line number Diff line
@@ -63,6 +63,19 @@ def convex_hull(points):


def geom_mask(keypoints, geom):
    """
    Masks any points that are outside of the bounds of the given
    geometry.

    Parameters
    ----------
    keypoints : dataframe
                      A pandas dataframe of points to mask

    geom : object
                Shapely geometry object to use as a mask
    """

    def _in_mbr(r, mbr):
        if (mbr[0] <= r.x <= mbr[2]) and (mbr[1] <= r.y <= mbr[3]):
            return True
@@ -151,18 +164,14 @@ def compute_voronoi(keypoints, intersection=None, geometry=False, s=30):

        if intersection is None:
            keypoint_bounds = Polygon(vor_keypoints).bounds
            min_bounding_box = shapely.geometry.box(keypoint_bounds[0], keypoint_bounds[1],
            intersection = shapely.geometry.box(keypoint_bounds[0], keypoint_bounds[1],
                                                    keypoint_bounds[2], keypoint_bounds[3])

            scaled_coords = np.array(scale(min_bounding_box, s, s).exterior.coords)
        else:
        scaled_coords = np.array(scale(intersection, s, s).exterior.coords)

        vor_keypoints = np.vstack((vor_keypoints, scaled_coords))
        vor = Voronoi(vor_keypoints)

        # For weight computation
        # Should move to its own method
        # Might move the code below to its own method depending on feedback
        if geometry:
            voronoi_df = gpd.GeoDataFrame(data = keypoints, columns=['x', 'y', 'weight', 'geometry'])
        else:
@@ -194,98 +203,3 @@ def compute_voronoi(keypoints, intersection=None, geometry=False, s=30):
            i += 1

        return voronoi_df


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
    ----------
    source : int or object
             Node id or Node object to use as the initial reprojection space

    graph : object
            a networkx graph object

    clean_keys : list
                 Of strings used to apply masks to omit correspondences

    Returns
    -------
    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]

    source_corners = source.geodata.xy_corners
    source_poly = Polygon(source_corners)

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

    proj_list = []
    proj_nodes = []
    # Begin iterating through the nodes in the graph excluding the source node
    for n in graph.nodes_iter():
        if n == source['node_id']:
            continue

        # Define the edge, matches, and destination based on the zero node and the nth node
        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)
                    kp1 = edge.get_keypoints('destination', clean_keys=clean_keys, homogeneous=True)
                else:
                    kp2 = edge.get_keypoints('destination', clean_keys=clean_keys, homogeneous=True)
                    kp1 = edge.get_keypoints('source', clean_keys=clean_keys, homogeneous=True)

                H, mask = cv2.findHomography(kp2.values, kp1.values, cv2.RANSAC, 2.0)
                proj_corners = []
                for c in destination_corners:
                    x, y, h = utils.reproj_point(H, c)
                    x /= h
                    y /= h
                    h /= h
                    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({'geometry': proj_list, 'proj_node': proj_nodes})

    intersect_gdf = gpd.overlay(source_gdf, proj_gdf, how='intersection')
    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
+25 −209
Original line number Diff line number Diff line
@@ -8,6 +8,7 @@ import pandas as pd

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

@@ -41,213 +42,28 @@ 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]]
    def test_geom_mask(self):
        my_gdf = pd.DataFrame(columns=['x', 'y'], data=[(0, 0), (2, 2)])
        my_poly = Polygon([(1, 1), (3, 1), (3, 3), (1, 3)])
        mask = cg.geom_mask(my_gdf, my_poly)
        self.assertFalse(mask[0])
        self.assertTrue(mask[1])

    def test_compute_voronoi(self):
        keypoints = pd.DataFrame({'x': (15, 18, 18, 12, 12), 'y': (6, 10, 15, 15, 10)})
        intersection = Polygon([(10, 5), (20, 5), (20, 20), (10, 20)])

        voronoi_gdf = cg.compute_voronoi(keypoints)
        self.assertAlmostEquals(voronoi_gdf.weight[0], 12.0)
        self.assertAlmostEquals(voronoi_gdf.weight[1], 13.5)
        self.assertAlmostEquals(voronoi_gdf.weight[2], 7.5)
        self.assertAlmostEquals(voronoi_gdf.weight[3], 7.5)
        self.assertAlmostEquals(voronoi_gdf.weight[4], 13.5)

        voronoi_inter_gdf = cg.compute_voronoi(keypoints, intersection)
        self.assertAlmostEquals(voronoi_inter_gdf.weight[0], 22.5)
        self.assertAlmostEquals(voronoi_inter_gdf.weight[1], 26.25)
        self.assertAlmostEquals(voronoi_inter_gdf.weight[2], 37.5)
        self.assertAlmostEquals(voronoi_inter_gdf.weight[3], 37.5)
        self.assertAlmostEquals(voronoi_inter_gdf.weight[4], 26.25)
        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=[])
        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=[])
        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)
+32 −23
Original line number Diff line number Diff line
@@ -4,8 +4,12 @@ from time import gmtime, strftime
import warnings

import networkx as nx
import geopandas as gpd
import pandas as pd
import shapely.affinity
import shapely.geometry
import shapely.wkt as swkt
import shapely.ops

from plio.io import io_hdf, io_json
from plio.utils import utils as io_utils
@@ -687,7 +691,7 @@ class CandidateGraph(nx.Graph):
        edges = [(u, v) for u, v, edge in self.edges_iter(data=True) if func(edge, *args, **kwargs)]
        return self.create_edge_subgraph(edges)

    def compute_cliques(self, node_id=None):
    def compute_cliques(self, node_id=None):  # pragma: no cover
        """
        Computes all maximum complete subgraphs for the given graph.
        If a node_id is given, method will return only the complete subgraphs that
@@ -696,7 +700,7 @@ class CandidateGraph(nx.Graph):
        Parameters
        ----------
        node_id : int
                  Arbitrary integer value for a given node
                       Integer value for a given node

        Returns
        -------
@@ -708,13 +712,16 @@ class CandidateGraph(nx.Graph):
        else:
            return list(nx.find_cliques(self))

    def compute_weight(self, clean_keys):
    def compute_weight(self, clean_keys, **kwargs):
        """
        Computes a voronoi weight for each edge in a given graph.
        Can function as is, but is slightly optimized for complete subgraphs.

        Parameters
        ----------
        kwargs : dict
                      keyword arguments that get passed to compute_voronoi

        clean_keys : list
                     Strings used to apply masks to omit correspondences
        """
@@ -722,12 +729,11 @@ class CandidateGraph(nx.Graph):
        if False in list(all(value == len(self.neighbors(self.nodes()[0])) for value in neighbors_dict.values())):
            warnings.warn('The given graph is not complete and may yield garbage.')

        intersect_gdf = self.compute_intersection(self.nodes()[0], self, clean_keys)
        source_node = self.node[self.nodes()[0]]
        source_node = self.nodes(data=True)[0][1]
        intersect_gdf = self.compute_intersection(self, source_node, clean_keys)

        for s, d, edge in self.edges_iter(data=True):
            # Recompute the intersection if the source node of the n + 1 edge is different from the n edge
            # I don't know if this check is necessary anymore
            if s != source_node['node_id']:
                source_node = edge.source
                intersect_gdf = self.compute_intersection(self, source_node, clean_keys)
@@ -741,11 +747,11 @@ class CandidateGraph(nx.Graph):

            kps['geometry'] = kps.apply(lambda x: shapely.geometry.Point(x['x'], x['y']), axis=1)
            kps_mask = kps['geometry'][initial_mask].apply(lambda x: reproj_geom.contains(x))
            voronoi_df = compute_voronoi(kps[initial_mask][kps_mask], reproj_geom, geometry=True)
            voronoi_df = compute_voronoi(kps[initial_mask][kps_mask], reproj_geom, **kwargs)

            edge['weights']['vor_weight'] = voronoi_df['weight']

    def compute_intersection(self, source, clean_keys=[], ax = None):
    def compute_intersection(self, source, clean_keys=[]):
        """
        Computes the intercetion of all images in a graph
        based around a given source node
@@ -757,40 +763,43 @@ class CandidateGraph(nx.Graph):

        clean_keys : list
                           Strings used to apply masks to omit correspondences

        Returns
        -------
        intersect_gdf : dataframe
                               A geopandas dataframe of intersections for all images
                               that overlap with the source node. Also includes the common
                               overlap for all images in the source node.
        """
        if type(source) is int:
            source = self.node[source]

        try:
        # May want to use a try except block here, but what error to raise?
        source_poly = swkt.loads(source.geodata.footprint.GetGeometryRef(0).ExportToWkt())
        except:
            raise AttributeError()

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

        proj_list = []
        proj_nodes = []
        proj_gdf = gpd.GeoDataFrame(columns=['geometry', 'proj_node'])

        # Begin iterating through the nodes in the graph excluding the source node
        # Begin iterating through the edges in the graph that contain the source
        for s, d, edge in self.edges_iter(data=True):
            # May want to use a try except block here, but what error to raise?
            if s == source['node_id']:
                proj_poly = swkt.loads(edge.destination.geodata.footprint.GetGeometryRef(0).ExportToWkt())
                proj_gdf.loc[len(proj_gdf)] = [proj_poly, d]
            elif d == source['node_id']:
                proj_poly = swkt.loads(edge.source.geodata.footprint.GetGeometryRef(0).ExportToWkt())
                proj_gdf.loc[len(proj_gdf)] = [proj_poly, s]
            else:
                continue

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


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

        # Overlay the all geometry and find the one geometry element that overlaps all of the images
        intersect_gdf = gpd.overlay(source_gdf, proj_gdf, how='intersection')
        intersect_gdf['overlaps_all'] = intersect_gdf.geometry.apply(lambda x:proj_gdf.geometry.contains(scale(x, .9, .9)).all())
        intersect_gdf['overlaps_all'] = intersect_gdf.geometry.apply(lambda x:proj_gdf.geometry.contains(shapely.affinity.scale(x, .9, .9)).all())

        # If there is no polygon that overlaps all of the images, union all of the polygons into one large
        # polygon that does overlap all of the images
        if len(intersect_gdf.query("overlaps_all == True")) <= 0:
            new_poly = unary_union(intersect_gdf.geometry)
            new_poly = shapely.ops.unary_union(intersect_gdf.geometry)
            intersect_gdf.loc[len(intersect_gdf)] = [source['node_id'], source['node_id'], new_poly, True]

        return intersect_gdf
+3 −3
Original line number Diff line number Diff line
@@ -7,7 +7,7 @@ import pandas as pd
from plio.io.io_gdal import GeoDataset
from plio.io.isis_serial_number import generate_serial_number
from scipy.misc import bytescale
import shapley
from shapely.geometry import Polygon

from autocnet.cg import cg
from autocnet.control.control import Correspondence, Point
@@ -513,9 +513,9 @@ class Node(dict, MutableMapping):
        matches = self._keypoints[mask]
        return matches, mask

    def reproject_geom(self, coords):
    def reproject_geom(self, coords):  # pragma: no cover
        reproj = []

        for x, y in coords:
            reproj.append(self.geodata.latlon_to_pixel(x, y))
        return shapley.Polygon(reproj)
        return Polygon(reproj)
+79 −13

File changed.

Preview size limit exceeded, changes collapsed.

Loading