Loading autocnet/cg/cg.py +117 −0 Changes for autocnet/cg/cg.py: 117 added lines, 0 removed lines. Original line number Diff line number Diff line import pandas as pd import numpy as np from scipy.spatial import ConvexHull from scipy.spatial import Voronoi import cv2 from autocnet.utils import utils def convex_hull_ratio(points, ideal_area): Loading Loading @@ -99,3 +104,115 @@ def get_area(poly1, poly2): """ intersection_area = poly1.Intersection(poly2).GetArea() return intersection_area def vor(edge, clean_keys=[], s=30): """ Creates a voronoi diagram for an edge using either the coordinate transformation or using the homography between source and destination. The coordinate transformation uses the footprint of source and destination to calculate an intersection between the two images, then transforms the vertices of the intersection back into pixel space. If a coordinate transform does not exist, use the homography to project the destination image 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. Parameters ---------- edge : object An edge object clean_keys : list 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 """ source_corners = edge.source.geodata.xy_corners destination_corners = edge.destination.geodata.xy_corners matches, _ = edge.clean(clean_keys=clean_keys) source_keypoints_pd = edge.source.get_keypoint_coordinates(index=matches['source_idx'], homogeneous=True) destination_keypoints_pd = edge.destination.get_keypoint_coordinates(index=matches['destination_idx'], homogeneous=True) if edge.source.geodata.coordinate_transformation.this is not None: source_footprint_poly = edge.source.geodata.footprint destination_footprint_poly = edge.destination.geodata.footprint intersection_poly = destination_footprint_poly.Intersection(source_footprint_poly) intersection_geom = intersection_poly.GetGeometryRef(0) intersect_points = intersection_geom.GetPoints() intersection_points = [edge.source.geodata.latlon_to_pixel(lat, lon) for lat, lon in intersect_points] else: H, mask = cv2.findHomography(destination_keypoints_pd.values, source_keypoints_pd.values, cv2.RANSAC, 2.0) proj_corners = [] for c in destination_corners: x, y, h = utils.reproj_corner(H, c) x /= h y /= h h /= h proj_corners.append((x, y)) orig_poly = utils.array_to_poly(source_corners) proj_poly = utils.array_to_poly(proj_corners) intersection_poly = orig_poly.Intersection(proj_poly) intersection_geom = intersection_poly.GetGeometryRef(0) intersection_points = intersection_geom.GetPoints() centroid = intersection_poly.Centroid().GetPoint() voronoi_df = pd.DataFrame(data=source_keypoints_pd, columns=["x", "y", "vor_weights"]) voronoi_df["x"] = source_keypoints_pd['x'] voronoi_df["y"] = source_keypoints_pd['y'] keypoints = np.asarray(source_keypoints_pd) inters = np.empty((len(intersection_points), 2)) for g, (i, j) in enumerate(intersection_points): scaledx, scaledy = utils.scale_point((i, j), centroid, s) point = np.array([scaledx, scaledy]) inters[g] = point keypoints = np.vstack((keypoints[:, :2], inters)) vor = Voronoi(keypoints) poly_array = [] for i, region in enumerate(vor.regions): region_point = vor.points[np.argwhere(vor.point_region==i)] if -1 not in region: polygon_points = [vor.vertices[i] for i in region] if len(polygon_points) != 0: polygon = utils.array_to_poly(polygon_points) intersection = polygon.Intersection(intersection_poly) poly_array = np.append(poly_array, intersection) polygon_area = intersection.GetArea() voronoi_df.loc[(voronoi_df["x"] == region_point[0][0][0]) & (voronoi_df["y"] == region_point[0][0][1]), 'vor_weights'] = polygon_area return vor, voronoi_df autocnet/examples/Apollo15/cube_adjacency.json 0 → 100644 +3 −0 Changes for autocnet/examples/Apollo15/cube_adjacency.json: 3 added lines, 0 removed lines. Original line number Diff line number Diff line {"AS15-M-0297_crop.cub": ["AS15-M-0298_crop.cub", "AS15-M-0299_crop.cub"], "AS15-M-0298_crop.cub": ["AS15-M-0299_crop.cub", "AS15-M-0297_crop.cub"], "AS15-M-0299_crop.cub": ["AS15-M-0298_crop.cub", "AS15-M-0297_crop.cub"]} autocnet/graph/edge.py +34 −6 Changes for autocnet/graph/edge.py: 34 added lines, 6 removed lines. Original line number Diff line number Diff line Loading @@ -3,6 +3,7 @@ from collections import MutableMapping import numpy as np import pandas as pd from scipy.spatial.distance import cdist from autocnet.utils import utils Loading Loading @@ -49,6 +50,7 @@ class Edge(dict, MutableMapping): self.matches = None self._subpixel_offsets = None self.provenance = {} self.weight = {} self._observers = set() Loading @@ -63,6 +65,17 @@ class Edge(dict, MutableMapping): Available Masks: {} """.format(self.source, self.destination, self.masks) def __getitem__(self, item): attribute_dict = {'source': self.source, 'destination': self.destination, 'masks': self.masks, 'provenance': self.provenance, 'weight': self.weight} if item in attribute_dict.keys(): return attribute_dict[item] else: return super(Edge, self).__getitem__(item) @property def masks(self): mask_lookup = {'fundamental': 'fundamental_matrix', Loading Loading @@ -688,18 +701,15 @@ class Edge(dict, MutableMapping): def plot_decomposition(self, *args, **kwargs): #pragma: no cover return plot_edge_decomposition(self, *args, **kwargs) def clean(self, clean_keys, pid=None): def clean(self, clean_keys): """ Given a list of clean keys and a provenance id compute the mask of valid matches Given a list of clean keys compute the mask of valid matches Parameters ---------- clean_keys : list of columns names (clean keys) pid : int The provenance id of the parameter set to be cleaned. Defaults to the last run. Returns ------- Loading Loading @@ -768,3 +778,21 @@ class Edge(dict, MutableMapping): total_overlap_coverage = (convex_poly.GetArea()/intersection_area) return total_overlap_coverage def compute_weights(self, clean_keys, **kwargs): """ Computes a voronoi diagram for the overlap between two images then gets the area of each polygon resulting in a voronoi weight. These weights are then appended to the matches dataframe. Parameters ---------- clean_keys : list Of strings used to apply masks to omit correspondences """ if self.matches is None: raise AttributeError('Matches have not been computed for this edge') voronoi = cg.vor(self, clean_keys, **kwargs) self.matches = pd.concat([self.matches, voronoi[1]['vor_weights']], axis=1) autocnet/graph/network.py +19 −1 Changes for autocnet/graph/network.py: 19 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -14,7 +14,7 @@ from plio.io.io_gdal import GeoDataset from autocnet.graph import markov_cluster from autocnet.graph.edge import Edge from autocnet.graph.node import Node from autocnet.vis.graph_view import plot_graph from autocnet.vis.graph_view import plot_graph, cluster_plot class CandidateGraph(nx.Graph): Loading Loading @@ -569,6 +569,24 @@ class CandidateGraph(nx.Graph): """ return plot_graph(self, ax=ax, **kwargs) def plot_cluster(self, ax=None, **kwargs): """ Plot the graph based on the clusters generated by the markov clustering algorithm Parameters ---------- ax : object A MatPlotLib axes object. Returns ------- ax : object A MatPlotLib axes object. """ return cluster_plot(self, ax, **kwargs) def create_edge_subgraph(self, edges): """ Create a subgraph using a list of edges. Loading autocnet/graph/node.py +36 −4 Changes for autocnet/graph/node.py: 36 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -76,11 +76,29 @@ class Node(dict, MutableMapping): """.format(self.node_id, self.image_name, self.image_path, self.nkeypoints, self.masks, self.__class__) def __getitem__(self, item): attribute_dict = {'image_name': self.image_name, 'image_path': self.image_path, 'geodata': self.geodata, 'keypoints': self.keypoints, 'nkeypoints': self.nkeypoints, 'descriptors': self.descriptors, 'masks': self.masks, 'isis_serial': self.isis_serial} if item in attribute_dict.keys(): return attribute_dict[item] else: return super(Node, self).__getitem__(item) @property def geodata(self): if not getattr(self, '_geodata', None): if not getattr(self, '_geodata', None) and self.image_path is not None: self._geodata = GeoDataset(self.image_path) return self._geodata if hasattr(self, '_geodata'): return self._geodata else: return None @property def masks(self): Loading Loading @@ -127,6 +145,20 @@ class Node(dict, MutableMapping): else: return 0 @property def keypoints(self): if hasattr(self, '_keypoints'): return self._keypoints.copy() else: return None @property def descriptors(self): if hasattr(self, '_descriptors'): return np.copy(self._descriptors) else: return None def coverage(self): """ Determines the area of keypoint coverage Loading Loading @@ -239,7 +271,7 @@ class Node(dict, MutableMapping): kwargs passed to autocnet.feature_extractor.extract_features """ self._keypoints, self.descriptors = fe.extract_features(array, **kwargs) self._keypoints, self._descriptors = fe.extract_features(array, **kwargs) def load_features(self, in_path): """ Loading @@ -256,7 +288,7 @@ class Node(dict, MutableMapping): else: hdf = in_path self.descriptors = hdf['{}/descriptors'.format(self.image_name)][:] self._descriptors = hdf['{}/descriptors'.format(self.image_name)][:] raw_kps = hdf['{}/keypoints'.format(self.image_name)][:] index = raw_kps['index'] clean_kps = utils.remove_field_name(raw_kps, 'index') Loading Loading @@ -298,7 +330,7 @@ class Node(dict, MutableMapping): try: hdf.create_dataset('{}/descriptors'.format(self.image_name), data=self.descriptors, data=self._descriptors, compression=io_hdf.DEFAULT_COMPRESSION, compression_opts=io_hdf.DEFAULT_COMPRESSION_VALUE) hdf.create_dataset('{}/keypoints'.format(self.image_name), Loading Loading
autocnet/cg/cg.py +117 −0 Changes for autocnet/cg/cg.py: 117 added lines, 0 removed lines. Original line number Diff line number Diff line import pandas as pd import numpy as np from scipy.spatial import ConvexHull from scipy.spatial import Voronoi import cv2 from autocnet.utils import utils def convex_hull_ratio(points, ideal_area): Loading Loading @@ -99,3 +104,115 @@ def get_area(poly1, poly2): """ intersection_area = poly1.Intersection(poly2).GetArea() return intersection_area def vor(edge, clean_keys=[], s=30): """ Creates a voronoi diagram for an edge using either the coordinate transformation or using the homography between source and destination. The coordinate transformation uses the footprint of source and destination to calculate an intersection between the two images, then transforms the vertices of the intersection back into pixel space. If a coordinate transform does not exist, use the homography to project the destination image 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. Parameters ---------- edge : object An edge object clean_keys : list 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 """ source_corners = edge.source.geodata.xy_corners destination_corners = edge.destination.geodata.xy_corners matches, _ = edge.clean(clean_keys=clean_keys) source_keypoints_pd = edge.source.get_keypoint_coordinates(index=matches['source_idx'], homogeneous=True) destination_keypoints_pd = edge.destination.get_keypoint_coordinates(index=matches['destination_idx'], homogeneous=True) if edge.source.geodata.coordinate_transformation.this is not None: source_footprint_poly = edge.source.geodata.footprint destination_footprint_poly = edge.destination.geodata.footprint intersection_poly = destination_footprint_poly.Intersection(source_footprint_poly) intersection_geom = intersection_poly.GetGeometryRef(0) intersect_points = intersection_geom.GetPoints() intersection_points = [edge.source.geodata.latlon_to_pixel(lat, lon) for lat, lon in intersect_points] else: H, mask = cv2.findHomography(destination_keypoints_pd.values, source_keypoints_pd.values, cv2.RANSAC, 2.0) proj_corners = [] for c in destination_corners: x, y, h = utils.reproj_corner(H, c) x /= h y /= h h /= h proj_corners.append((x, y)) orig_poly = utils.array_to_poly(source_corners) proj_poly = utils.array_to_poly(proj_corners) intersection_poly = orig_poly.Intersection(proj_poly) intersection_geom = intersection_poly.GetGeometryRef(0) intersection_points = intersection_geom.GetPoints() centroid = intersection_poly.Centroid().GetPoint() voronoi_df = pd.DataFrame(data=source_keypoints_pd, columns=["x", "y", "vor_weights"]) voronoi_df["x"] = source_keypoints_pd['x'] voronoi_df["y"] = source_keypoints_pd['y'] keypoints = np.asarray(source_keypoints_pd) inters = np.empty((len(intersection_points), 2)) for g, (i, j) in enumerate(intersection_points): scaledx, scaledy = utils.scale_point((i, j), centroid, s) point = np.array([scaledx, scaledy]) inters[g] = point keypoints = np.vstack((keypoints[:, :2], inters)) vor = Voronoi(keypoints) poly_array = [] for i, region in enumerate(vor.regions): region_point = vor.points[np.argwhere(vor.point_region==i)] if -1 not in region: polygon_points = [vor.vertices[i] for i in region] if len(polygon_points) != 0: polygon = utils.array_to_poly(polygon_points) intersection = polygon.Intersection(intersection_poly) poly_array = np.append(poly_array, intersection) polygon_area = intersection.GetArea() voronoi_df.loc[(voronoi_df["x"] == region_point[0][0][0]) & (voronoi_df["y"] == region_point[0][0][1]), 'vor_weights'] = polygon_area return vor, voronoi_df
autocnet/examples/Apollo15/cube_adjacency.json 0 → 100644 +3 −0 Changes for autocnet/examples/Apollo15/cube_adjacency.json: 3 added lines, 0 removed lines. Original line number Diff line number Diff line {"AS15-M-0297_crop.cub": ["AS15-M-0298_crop.cub", "AS15-M-0299_crop.cub"], "AS15-M-0298_crop.cub": ["AS15-M-0299_crop.cub", "AS15-M-0297_crop.cub"], "AS15-M-0299_crop.cub": ["AS15-M-0298_crop.cub", "AS15-M-0297_crop.cub"]}
autocnet/graph/edge.py +34 −6 Changes for autocnet/graph/edge.py: 34 added lines, 6 removed lines. Original line number Diff line number Diff line Loading @@ -3,6 +3,7 @@ from collections import MutableMapping import numpy as np import pandas as pd from scipy.spatial.distance import cdist from autocnet.utils import utils Loading Loading @@ -49,6 +50,7 @@ class Edge(dict, MutableMapping): self.matches = None self._subpixel_offsets = None self.provenance = {} self.weight = {} self._observers = set() Loading @@ -63,6 +65,17 @@ class Edge(dict, MutableMapping): Available Masks: {} """.format(self.source, self.destination, self.masks) def __getitem__(self, item): attribute_dict = {'source': self.source, 'destination': self.destination, 'masks': self.masks, 'provenance': self.provenance, 'weight': self.weight} if item in attribute_dict.keys(): return attribute_dict[item] else: return super(Edge, self).__getitem__(item) @property def masks(self): mask_lookup = {'fundamental': 'fundamental_matrix', Loading Loading @@ -688,18 +701,15 @@ class Edge(dict, MutableMapping): def plot_decomposition(self, *args, **kwargs): #pragma: no cover return plot_edge_decomposition(self, *args, **kwargs) def clean(self, clean_keys, pid=None): def clean(self, clean_keys): """ Given a list of clean keys and a provenance id compute the mask of valid matches Given a list of clean keys compute the mask of valid matches Parameters ---------- clean_keys : list of columns names (clean keys) pid : int The provenance id of the parameter set to be cleaned. Defaults to the last run. Returns ------- Loading Loading @@ -768,3 +778,21 @@ class Edge(dict, MutableMapping): total_overlap_coverage = (convex_poly.GetArea()/intersection_area) return total_overlap_coverage def compute_weights(self, clean_keys, **kwargs): """ Computes a voronoi diagram for the overlap between two images then gets the area of each polygon resulting in a voronoi weight. These weights are then appended to the matches dataframe. Parameters ---------- clean_keys : list Of strings used to apply masks to omit correspondences """ if self.matches is None: raise AttributeError('Matches have not been computed for this edge') voronoi = cg.vor(self, clean_keys, **kwargs) self.matches = pd.concat([self.matches, voronoi[1]['vor_weights']], axis=1)
autocnet/graph/network.py +19 −1 Changes for autocnet/graph/network.py: 19 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -14,7 +14,7 @@ from plio.io.io_gdal import GeoDataset from autocnet.graph import markov_cluster from autocnet.graph.edge import Edge from autocnet.graph.node import Node from autocnet.vis.graph_view import plot_graph from autocnet.vis.graph_view import plot_graph, cluster_plot class CandidateGraph(nx.Graph): Loading Loading @@ -569,6 +569,24 @@ class CandidateGraph(nx.Graph): """ return plot_graph(self, ax=ax, **kwargs) def plot_cluster(self, ax=None, **kwargs): """ Plot the graph based on the clusters generated by the markov clustering algorithm Parameters ---------- ax : object A MatPlotLib axes object. Returns ------- ax : object A MatPlotLib axes object. """ return cluster_plot(self, ax, **kwargs) def create_edge_subgraph(self, edges): """ Create a subgraph using a list of edges. Loading
autocnet/graph/node.py +36 −4 Changes for autocnet/graph/node.py: 36 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -76,11 +76,29 @@ class Node(dict, MutableMapping): """.format(self.node_id, self.image_name, self.image_path, self.nkeypoints, self.masks, self.__class__) def __getitem__(self, item): attribute_dict = {'image_name': self.image_name, 'image_path': self.image_path, 'geodata': self.geodata, 'keypoints': self.keypoints, 'nkeypoints': self.nkeypoints, 'descriptors': self.descriptors, 'masks': self.masks, 'isis_serial': self.isis_serial} if item in attribute_dict.keys(): return attribute_dict[item] else: return super(Node, self).__getitem__(item) @property def geodata(self): if not getattr(self, '_geodata', None): if not getattr(self, '_geodata', None) and self.image_path is not None: self._geodata = GeoDataset(self.image_path) return self._geodata if hasattr(self, '_geodata'): return self._geodata else: return None @property def masks(self): Loading Loading @@ -127,6 +145,20 @@ class Node(dict, MutableMapping): else: return 0 @property def keypoints(self): if hasattr(self, '_keypoints'): return self._keypoints.copy() else: return None @property def descriptors(self): if hasattr(self, '_descriptors'): return np.copy(self._descriptors) else: return None def coverage(self): """ Determines the area of keypoint coverage Loading Loading @@ -239,7 +271,7 @@ class Node(dict, MutableMapping): kwargs passed to autocnet.feature_extractor.extract_features """ self._keypoints, self.descriptors = fe.extract_features(array, **kwargs) self._keypoints, self._descriptors = fe.extract_features(array, **kwargs) def load_features(self, in_path): """ Loading @@ -256,7 +288,7 @@ class Node(dict, MutableMapping): else: hdf = in_path self.descriptors = hdf['{}/descriptors'.format(self.image_name)][:] self._descriptors = hdf['{}/descriptors'.format(self.image_name)][:] raw_kps = hdf['{}/keypoints'.format(self.image_name)][:] index = raw_kps['index'] clean_kps = utils.remove_field_name(raw_kps, 'index') Loading Loading @@ -298,7 +330,7 @@ class Node(dict, MutableMapping): try: hdf.create_dataset('{}/descriptors'.format(self.image_name), data=self.descriptors, data=self._descriptors, compression=io_hdf.DEFAULT_COMPRESSION, compression_opts=io_hdf.DEFAULT_COMPRESSION_VALUE) hdf.create_dataset('{}/keypoints'.format(self.image_name), Loading