Loading autocnet/__init__.py +8 −4 Original line number Diff line number Diff line Loading @@ -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" Loading Loading @@ -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 Loading autocnet/graph/__init__.py +3 −0 Original line number Diff line number Diff line from . import edge from . import network from . import node autocnet/graph/edge.py +104 −124 Original line number Diff line number Diff line from functools import wraps import warnings from collections import MutableMapping Loading @@ -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 Loading @@ -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 Loading @@ -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 """ Loading Loading @@ -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'): Loading @@ -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): Loading @@ -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. Loading @@ -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. Loading @@ -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 Loading @@ -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']) Loading @@ -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, Loading Loading @@ -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 Loading Loading @@ -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) Loading Loading @@ -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 Loading Loading @@ -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) autocnet/graph/network.py +73 −17 Original line number Diff line number Diff line import itertools import math import os from time import gmtime, strftime import warnings Loading @@ -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): """ Loading Loading @@ -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): """ Loading Loading @@ -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): """ Loading Loading @@ -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 Loading Loading @@ -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) Loading @@ -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) Loading @@ -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) Loading @@ -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) Loading Loading @@ -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 Loading Loading @@ -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) Loading autocnet/graph/node.py +125 −74 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
autocnet/__init__.py +8 −4 Original line number Diff line number Diff line Loading @@ -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" Loading Loading @@ -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 Loading
autocnet/graph/__init__.py +3 −0 Original line number Diff line number Diff line from . import edge from . import network from . import node
autocnet/graph/edge.py +104 −124 Original line number Diff line number Diff line from functools import wraps import warnings from collections import MutableMapping Loading @@ -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 Loading @@ -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 Loading @@ -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 """ Loading Loading @@ -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'): Loading @@ -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): Loading @@ -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. Loading @@ -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. Loading @@ -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 Loading @@ -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']) Loading @@ -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, Loading Loading @@ -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 Loading Loading @@ -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) Loading Loading @@ -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 Loading Loading @@ -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)
autocnet/graph/network.py +73 −17 Original line number Diff line number Diff line import itertools import math import os from time import gmtime, strftime import warnings Loading @@ -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): """ Loading Loading @@ -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): """ Loading Loading @@ -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): """ Loading Loading @@ -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 Loading Loading @@ -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) Loading @@ -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) Loading @@ -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) Loading @@ -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) Loading Loading @@ -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 Loading Loading @@ -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) Loading
autocnet/graph/node.py +125 −74 File changed.Preview size limit exceeded, changes collapsed. Show changes