Loading autocnet/graph/edge.py +34 −81 Changes for autocnet/graph/edge.py: 34 added lines, 81 removed lines. Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ from autocnet.cg.cg import convex_hull_ratio from autocnet.cg.cg import overlapping_polygon_area from autocnet.matcher import health from autocnet.matcher import outlier_detector as od from autocnet.matcher import suppression_funcs as spf from autocnet.matcher import subpixel as sp from autocnet.transformation.transformations import FundamentalMatrix, Homography from autocnet.vis.graph_view import plot_edge Loading Loading @@ -256,10 +257,9 @@ class Edge(dict, MutableMapping): without being considered an outlier """ matches = self.matches self.subpixel_offsets = pd.DataFrame(0, index=matches.index, columns=['x_offset', 'y_offset', 'correlation', 's_idx', 'd_idx']) for column in ['x_offset', 'y_offset', 'correlation']: if not column in self.matches.columns: self.matches[column] = 0 # Build up a composite mask from all of the user specified masks if clean_keys: Loading Loading @@ -287,21 +287,19 @@ class Edge(dict, MutableMapping): try: x_off, y_off, strength = sp.subpixel_offset(s_template, d_search, upsampling=upsampling) self.subpixel_offsets.loc[idx] = [x_off, y_off, strength,s_idx, d_idx] self.matches.loc[idx, ('x_offset', 'y_offset', 'correlation')] = [x_off, y_off, strength] except: warnings.warn('Template-Search size mismatch, failing for this correspondence point.') continue self.subpixel_offsets.to_sparse(fill_value=0.0) # Compute the mask for correlations less than the threshold threshold_mask = self.subpixel_offsets['correlation'] >= threshold threshold_mask = self.matches['correlation'] >= threshold # Compute the mask for the point shifts that are too large subp= self.subpixel_offsets 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 = subp.query(query_string) shift_mask = pd.Series(True, index=self.subpixel_offsets.index) sp_shift_outliers = self.matches.query(query_string) shift_mask = pd.Series(True, index=self.matches.index) shift_mask[sp_shift_outliers.index] = False # Generate the composite mask and write the masks to the mask data structure Loading @@ -310,82 +308,37 @@ class Edge(dict, MutableMapping): self.masks = ('threshold', threshold_mask) self.masks = ('subpixel', mask) def suppress(self, min_radius=1, k=100, error_k=0.1): """ Suppress subpixel registered points to that k +- k * error_k points, with good spatial distribution, remain def suppress(self, func=spf.correlation, clean_keys=[], **kwargs): if not hasattr(self, 'matches'): raise AttributeError('This edge does not yet have any matches computed.') Adds a suppression mask to the edge mask dataframe. # Build up a composite mask from all of the user specified masks if clean_keys: matches, mask = self._clean(clean_keys) else: matches = self.matches Parameters ---------- min_radius : int The lowest acceptable radius value for points domain = self.source.handle.raster_size k : int The desired number of output points # Massage the dataframe into the correct structure coords = self.source.keypoints.loc[matches['source_idx']][['x', 'y']] matches = matches.merge(coords, left_on=['source_idx'], right_index=True) matches['strength'] = self.matches.apply(func, axis=1) error_k : float [0,1) The acceptable epsilon """ xy_extent = self.source.handle.xy_extent[1] max_radius = min(xy_extent) / 4 k = 100 sp_mask = self.masks['subpixel'] sp_values = self.subpixel_offsets[sp_mask] coordinates = self.source.keypoints.iloc[sp_values['s_idx']][['x', 'y']] merged = pd.merge(sp_values, coordinates, left_on='s_idx', how='left', right_index=True).sort_values(by='correlation') previous_cell_size = 0 while True: r = (min_radius + max_radius) / 2 cell_size = int(r / math.sqrt(2)) # To prevent cycling if cell_size == previous_cell_size: break previous_cell_size = cell_size # Setup to store results result = [] # Compute the bin edges and assign points to the appropriate bins x_edges = np.arange(0,xy_extent[0], int(xy_extent[0] / cell_size)) y_edges = np.arange(0,xy_extent[1], int(xy_extent[1] / cell_size)) grid = np.zeros((len(y_edges), len(x_edges)), dtype=np.bool) xbins = np.digitize(merged['x'], bins=x_edges) ybins = np.digitize(merged['y'], bins=y_edges) for i, (idx, p) in enumerate(merged.iterrows()): x_center = xbins[i] y_center = ybins[i] cell = grid[y_center-1 , x_center-1] if cell == False: result.append(idx) if len(result) > k: # Search the lower half, the radius is too big max_radius = r break # Cover the necessary cells grid[y_center - 5: y_center + 5, x_center - 5:x_center + 5] = True # Check break conditions if k - k * error_k < len(result) < k + k * error_k: break elif len(result) < k: # Search the upper half, the radius is too small min_radius = r elif abs(max_radius - min_radius) < 5: break mask = pd.Series(False, self.masks.index) mask.iloc[np.array(result)] = True if not hasattr(self, 'suppression'): # Instantiate the suppression object and suppress matches self.suppression = od.SpatialSuppression(matches, domain, **kwargs) self.suppression.suppress() else: for k, v in kwargs.items(): if hasattr(self.suppression, k): setattr(self.suppression, k, v) self.suppression.suppress() if clean_keys: mask[mask == True] = self.suppression.mask else: mask = self.suppression.mask self.masks = ('suppression', mask) def coverage_ratio(self, clean_keys=[]): Loading autocnet/graph/network.py +4 −9 Changes for autocnet/graph/network.py: 4 added lines, 9 removed lines. Original line number Diff line number Diff line Loading @@ -211,9 +211,7 @@ class CandidateGraph(nx.Graph): Parameters ---------- k : int The number of matches, minus 1, to find per feature. For example k=5 will find the 4 nearest neighbors for every extracted feature. If None, k = (2 * the number of edges connecting a node) +1 The number of matches to find per feature. """ # Instantiate a single flann matcher to be resused for all nodes Loading @@ -224,7 +222,6 @@ class CandidateGraph(nx.Graph): if not hasattr(node, 'descriptors'): raise AttributeError('Descriptors must be extracted before matching can occur.') descriptors = node.descriptors # Load the neighbors of the current node into the FLANN matcher neighbors = self.neighbors(i) for n in neighbors: Loading Loading @@ -392,9 +389,6 @@ class CandidateGraph(nx.Graph): if clean_keys: matches, mask = edge._clean(clean_keys) if 'subpixel' in clean_keys: offsets = edge.subpixel_offsets kp1 = self.node[source].keypoints kp2 = self.node[destination].keypoints pt_idx = 0 Loading @@ -417,8 +411,9 @@ class CandidateGraph(nx.Graph): kp2y = kp2.iloc[m2_pid]['y'] if 'subpixel' in clean_keys: kp2x += offsets['x_offset'].values[i] kp2y += offsets['y_offset'].values[i] kp2x += row['x_offset'] kp2y += row['y_offset'] values.append([kp2x, kp2y, m2, Loading autocnet/graph/node.py +71 −17 Changes for autocnet/graph/node.py: 71 added lines, 17 removed lines. Original line number Diff line number Diff line Loading @@ -7,6 +7,7 @@ from scipy.misc import bytescale from autocnet.fileio.io_gdal import GeoDataset from autocnet.matcher import feature_extractor as fe from autocnet.matcher import outlier_detector as od from autocnet.matcher import suppression_funcs as spf from autocnet.cg.cg import convex_hull_ratio from autocnet.utils.isis_serial_numbers import generate_serial_number from autocnet.vis.graph_view import plot_node Loading Loading @@ -45,7 +46,6 @@ class Node(dict, MutableMapping): def __init__(self, image_name=None, image_path=None): self.image_name = image_name self.image_path = image_path self._masks = set() self._mask_arrays = {} self.provenance = {} self._pid = 0 Loading Loading @@ -79,12 +79,35 @@ class Node(dict, MutableMapping): @property def masks(self): mask_lookup = {'suppression': 'suppression'} if not hasattr(self, '_masks'): self._masks = pd.DataFrame() # If the mask is coming form another object that tracks # state, dynamically draw the mask from the object. for c in self._masks.columns: if c in mask_lookup: self._masks[c] = getattr(self, mask_lookup[c]).mask return self._masks @masks.setter def masks(self, v): self._masks.add(v[0]) self._mask_arrays[v[0]] = v[1] column_name = v[0] boolean_mask = v[1] self.masks[column_name] = boolean_mask @property def isis_serial(self): """ Generate an ISIS compatible serial number using the data file associated with this node. This assumes that the data file has a PVL header. """ if not hasattr(self, '_isis_serial'): try: self._isis_serial = generate_serial_number(self.image_path) except: self._isis_serial = None return self._isis_serial def get_array(self, band=1): """ Loading Loading @@ -130,9 +153,25 @@ class Node(dict, MutableMapping): 'parameters':kwargs} self._pid += 1 def anms(self, nfeatures=100, robust=0.9): mask = od.adaptive_non_max_suppression(self.keypoints,nfeatures,robust) self.masks = ('anms', mask) def suppress(self, func=spf.response, **kwargs): if not hasattr(self, 'keypoints'): raise AttributeError('No keypoints extracted for this node.') domain = self.handle.raster_size self.keypoints['strength'] = self.keypoints.apply(func, axis=1) if not hasattr(self, 'suppression'): # Instantiate a suppression object and suppress keypoints self.suppression = od.SpatialSuppression(self.keypoints, domain, **kwargs) self.suppression.suppress() else: # Update the suppression object attributes and process for k, v in kwargs.items(): if hasattr(self.suppression, k): setattr(self.suppression, k, v) self.suppression.suppress() self.masks = ('suppression', self.suppression.mask) def coverage_ratio(self, clean_keys=[]): """ Loading @@ -159,16 +198,31 @@ class Node(dict, MutableMapping): def plot(self, clean_keys=[], **kwargs): # pragma: no cover return plot_node(self, clean_keys=clean_keys, **kwargs) @property def isis_serial(self): def _clean(self, clean_keys): """ Generate an ISIS compatible serial number using the data file associated with this node. This assumes that the data file has a PVL header. Given a list of clean keys and a provenance id 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 ------- matches : dataframe A masked view of the matches dataframe mask : series A boolean series to inflate back to the full match set """ if not hasattr(self, '_isis_serial'): try: self._isis_serial = generate_serial_number(self.image_path) except: self._isis_serial = None return self._isis_serial No newline at end of file if not hasattr(self, 'keypoints'): raise AttributeError('Keypoints have not been extracted for this node.') panel = self.masks mask = panel[clean_keys].all(axis=1) matches = self.keypoints[mask] return matches, mask autocnet/graph/tests/test_node.py +0 −6 Changes for autocnet/graph/tests/test_node.py: 0 added lines, 6 removed lines. Original line number Diff line number Diff line Loading @@ -50,12 +50,6 @@ class TestNode(unittest.TestCase): self.assertNotEqual(find_in_dict(p0, 'nfeatures'), find_in_dict(p1, 'nfeatures')) def test_anms(self): image = self.node.get_array() self.node.extract_features(image, extractor_parameters={'nfeatures':100}) self.node.anms(nfeatures=10) self.assertIn('anms', self.node.masks) self.assertTrue(sum(self.node._mask_arrays['anms']), 10) def test_isis_serial(self): serial = self.node.isis_serial Loading autocnet/matcher/outlier_detector.py +145 −47 Changes for autocnet/matcher/outlier_detector.py: 145 added lines, 47 removed lines. Original line number Diff line number Diff line from collections import deque import math import cv2 import numpy as np import pandas as pd from autocnet.utils.observable import Observable class DistanceRatio(object): class DistanceRatio(Observable): """ A stateful object to store ratio test results and provenance. Loading Loading @@ -37,6 +40,9 @@ class DistanceRatio(object): self._observers = set() self.matches = matches self.mask = None self.clean_keys = None self.single = None self.attrs = ['mask', 'ratio', 'clean_keys', 'single'] @property def nvalid(self): Loading Loading @@ -90,66 +96,158 @@ class DistanceRatio(object): self._action_stack.append(state_package) self._current_action_stack = len(self._action_stack) - 1 def subscribe(self, func): class SpatialSuppression(Observable): """ Subscribe some observer to the edge Spatial suppression using disc based method. Parameters Attributes ---------- func : object The callable that is to be executed on update """ self._observers.add(func) df : dataframe Input dataframe used for suppressing def _notify_subscribers(self, *args, **kwargs): """ The 'update' call to notify all subscribers of a change. """ for update_func in self._observers: update_func(self, *args, **kwargs) mask : series pandas boolean series def rollforward(self, n=1): """ Roll forwards in the object history, e.g. do max_radius : float Maximum allowable point radius Parameters ---------- n : int the number of steps to roll forwards min_radius : float The smallest allowable radius size nvalid : int The number of valid points after suppression k : int The number of points to be saved error_k : float [0,1] the acceptable error in k domain : tuple The (x,y) extent of the input domain """ idx = self._current_action_stack + n if idx > len(self._action_stack) - 1: idx = len(self._action_stack) - 1 self._current_action_stack = idx state = self._action_stack[idx] setattr(self, 'mask', state['mask']) setattr(self, 'ratio', state['ratio']) setattr(self, 'clean_keys', state['clean_keys']) setattr(self, 'single', state['single']) # Reset attributes (could also cache) self._notify_subscribers(self) def rollback(self, n=1): def __init__(self, df, domain, min_radius=1, k=250, error_k=0.05): columns = df.columns for i in ['x', 'y', 'strength']: if i not in columns: raise ValueError('The dataframe is missing a {} column.'.format(i)) self._df = df.sort_values(by=['strength'], ascending=False).copy() self.max_radius = min(domain) self.min_radius = min_radius self.domain = domain self.mask = None self._k = k self._error_k = error_k self.attrs = ['mask', 'k', 'error_k'] self._action_stack = deque(maxlen=10) self._current_action_stack = 0 self._observers = set() @property def nvalid(self): return self.mask.sum() @property def k(self): return self._k @k.setter def k(self, v): self._k = v @property def error_k(self): return self._error_k @error_k.setter def error_k(self, v): self._error_k = v @property def df(self): return self._df def suppress(self): """ Roll backward in the object histroy, e.g. undo Suppress subpixel registered points to that k +- k * error_k points, with good spatial distribution, remain Adds a suppression mask to the edge mask dataframe. Parameters ---------- n : int the number of steps to roll backwards k : int The desired number of output points error_k : float [0,1) The acceptable epsilon """ idx = self._current_action_stack - n if idx < 0: idx = 0 self._current_action_stack = idx state = self._action_stack[idx] setattr(self, 'mask', state['mask']) setattr(self, 'ratio', state['ratio']) setattr(self, 'clean_keys', state['clean_keys']) setattr(self, 'single', state['single']) # Reset attributes (could also cache) df = self.df if self.k > len(df): raise ValueError('Only {} valid points, but {} points requested'.format(len(df), self.k)) min_radius = self.min_radius max_radius = self.max_radius while True: r = (min_radius + max_radius) / 2 cell_size = int(r / math.sqrt(2)) # Setup to store results result = [] # Compute the bin edges and assign points to the appropriate bins x_edges = np.arange(0,self.domain[0], self.domain[0] / cell_size) y_edges = np.arange(0,self.domain[1], self.domain[1] / cell_size) grid = np.zeros((len(y_edges), len(x_edges)), dtype=np.bool) # Bin assignment xbins = np.digitize(df['x'], bins=x_edges) ybins = np.digitize(df['y'], bins=y_edges) bounds = True for i, (idx, p) in enumerate(df.iterrows()): x_center = xbins[i] y_center = ybins[i] cell = grid[y_center - 1 , x_center - 1] if cell == False: result.append(idx) if len(result) > self.k - self.k * self.error_k: # Search the lower half, the radius is too small max_radius = r bounds = False continue # Cover the necessary cells grid[y_center - 3: y_center + 3, x_center - 3: x_center + 3] = True if bounds is False: continue # Check break conditions if self.k - self.k * self.error_k < len(result) < self.k + self.k * self.error_k: break elif abs(max_radius - min_radius) < 5: break elif len(result) < self.k: # Search the upper half, the radius is too small min_radius = r self.mask = pd.Series(False, self.df.index) self.mask.loc[np.array(result)] = True state_package = {'mask':self.mask, 'k': self.k, 'error_k': self.error_k} self._action_stack.append(state_package) self._notify_subscribers(self) self._current_action_stack = len(self._action_stack) - 1 # 0 based vs. 1 based def self_neighbors(matches): """ Loading Loading
autocnet/graph/edge.py +34 −81 Changes for autocnet/graph/edge.py: 34 added lines, 81 removed lines. Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ from autocnet.cg.cg import convex_hull_ratio from autocnet.cg.cg import overlapping_polygon_area from autocnet.matcher import health from autocnet.matcher import outlier_detector as od from autocnet.matcher import suppression_funcs as spf from autocnet.matcher import subpixel as sp from autocnet.transformation.transformations import FundamentalMatrix, Homography from autocnet.vis.graph_view import plot_edge Loading Loading @@ -256,10 +257,9 @@ class Edge(dict, MutableMapping): without being considered an outlier """ matches = self.matches self.subpixel_offsets = pd.DataFrame(0, index=matches.index, columns=['x_offset', 'y_offset', 'correlation', 's_idx', 'd_idx']) for column in ['x_offset', 'y_offset', 'correlation']: if not column in self.matches.columns: self.matches[column] = 0 # Build up a composite mask from all of the user specified masks if clean_keys: Loading Loading @@ -287,21 +287,19 @@ class Edge(dict, MutableMapping): try: x_off, y_off, strength = sp.subpixel_offset(s_template, d_search, upsampling=upsampling) self.subpixel_offsets.loc[idx] = [x_off, y_off, strength,s_idx, d_idx] self.matches.loc[idx, ('x_offset', 'y_offset', 'correlation')] = [x_off, y_off, strength] except: warnings.warn('Template-Search size mismatch, failing for this correspondence point.') continue self.subpixel_offsets.to_sparse(fill_value=0.0) # Compute the mask for correlations less than the threshold threshold_mask = self.subpixel_offsets['correlation'] >= threshold threshold_mask = self.matches['correlation'] >= threshold # Compute the mask for the point shifts that are too large subp= self.subpixel_offsets 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 = subp.query(query_string) shift_mask = pd.Series(True, index=self.subpixel_offsets.index) sp_shift_outliers = self.matches.query(query_string) shift_mask = pd.Series(True, index=self.matches.index) shift_mask[sp_shift_outliers.index] = False # Generate the composite mask and write the masks to the mask data structure Loading @@ -310,82 +308,37 @@ class Edge(dict, MutableMapping): self.masks = ('threshold', threshold_mask) self.masks = ('subpixel', mask) def suppress(self, min_radius=1, k=100, error_k=0.1): """ Suppress subpixel registered points to that k +- k * error_k points, with good spatial distribution, remain def suppress(self, func=spf.correlation, clean_keys=[], **kwargs): if not hasattr(self, 'matches'): raise AttributeError('This edge does not yet have any matches computed.') Adds a suppression mask to the edge mask dataframe. # Build up a composite mask from all of the user specified masks if clean_keys: matches, mask = self._clean(clean_keys) else: matches = self.matches Parameters ---------- min_radius : int The lowest acceptable radius value for points domain = self.source.handle.raster_size k : int The desired number of output points # Massage the dataframe into the correct structure coords = self.source.keypoints.loc[matches['source_idx']][['x', 'y']] matches = matches.merge(coords, left_on=['source_idx'], right_index=True) matches['strength'] = self.matches.apply(func, axis=1) error_k : float [0,1) The acceptable epsilon """ xy_extent = self.source.handle.xy_extent[1] max_radius = min(xy_extent) / 4 k = 100 sp_mask = self.masks['subpixel'] sp_values = self.subpixel_offsets[sp_mask] coordinates = self.source.keypoints.iloc[sp_values['s_idx']][['x', 'y']] merged = pd.merge(sp_values, coordinates, left_on='s_idx', how='left', right_index=True).sort_values(by='correlation') previous_cell_size = 0 while True: r = (min_radius + max_radius) / 2 cell_size = int(r / math.sqrt(2)) # To prevent cycling if cell_size == previous_cell_size: break previous_cell_size = cell_size # Setup to store results result = [] # Compute the bin edges and assign points to the appropriate bins x_edges = np.arange(0,xy_extent[0], int(xy_extent[0] / cell_size)) y_edges = np.arange(0,xy_extent[1], int(xy_extent[1] / cell_size)) grid = np.zeros((len(y_edges), len(x_edges)), dtype=np.bool) xbins = np.digitize(merged['x'], bins=x_edges) ybins = np.digitize(merged['y'], bins=y_edges) for i, (idx, p) in enumerate(merged.iterrows()): x_center = xbins[i] y_center = ybins[i] cell = grid[y_center-1 , x_center-1] if cell == False: result.append(idx) if len(result) > k: # Search the lower half, the radius is too big max_radius = r break # Cover the necessary cells grid[y_center - 5: y_center + 5, x_center - 5:x_center + 5] = True # Check break conditions if k - k * error_k < len(result) < k + k * error_k: break elif len(result) < k: # Search the upper half, the radius is too small min_radius = r elif abs(max_radius - min_radius) < 5: break mask = pd.Series(False, self.masks.index) mask.iloc[np.array(result)] = True if not hasattr(self, 'suppression'): # Instantiate the suppression object and suppress matches self.suppression = od.SpatialSuppression(matches, domain, **kwargs) self.suppression.suppress() else: for k, v in kwargs.items(): if hasattr(self.suppression, k): setattr(self.suppression, k, v) self.suppression.suppress() if clean_keys: mask[mask == True] = self.suppression.mask else: mask = self.suppression.mask self.masks = ('suppression', mask) def coverage_ratio(self, clean_keys=[]): Loading
autocnet/graph/network.py +4 −9 Changes for autocnet/graph/network.py: 4 added lines, 9 removed lines. Original line number Diff line number Diff line Loading @@ -211,9 +211,7 @@ class CandidateGraph(nx.Graph): Parameters ---------- k : int The number of matches, minus 1, to find per feature. For example k=5 will find the 4 nearest neighbors for every extracted feature. If None, k = (2 * the number of edges connecting a node) +1 The number of matches to find per feature. """ # Instantiate a single flann matcher to be resused for all nodes Loading @@ -224,7 +222,6 @@ class CandidateGraph(nx.Graph): if not hasattr(node, 'descriptors'): raise AttributeError('Descriptors must be extracted before matching can occur.') descriptors = node.descriptors # Load the neighbors of the current node into the FLANN matcher neighbors = self.neighbors(i) for n in neighbors: Loading Loading @@ -392,9 +389,6 @@ class CandidateGraph(nx.Graph): if clean_keys: matches, mask = edge._clean(clean_keys) if 'subpixel' in clean_keys: offsets = edge.subpixel_offsets kp1 = self.node[source].keypoints kp2 = self.node[destination].keypoints pt_idx = 0 Loading @@ -417,8 +411,9 @@ class CandidateGraph(nx.Graph): kp2y = kp2.iloc[m2_pid]['y'] if 'subpixel' in clean_keys: kp2x += offsets['x_offset'].values[i] kp2y += offsets['y_offset'].values[i] kp2x += row['x_offset'] kp2y += row['y_offset'] values.append([kp2x, kp2y, m2, Loading
autocnet/graph/node.py +71 −17 Changes for autocnet/graph/node.py: 71 added lines, 17 removed lines. Original line number Diff line number Diff line Loading @@ -7,6 +7,7 @@ from scipy.misc import bytescale from autocnet.fileio.io_gdal import GeoDataset from autocnet.matcher import feature_extractor as fe from autocnet.matcher import outlier_detector as od from autocnet.matcher import suppression_funcs as spf from autocnet.cg.cg import convex_hull_ratio from autocnet.utils.isis_serial_numbers import generate_serial_number from autocnet.vis.graph_view import plot_node Loading Loading @@ -45,7 +46,6 @@ class Node(dict, MutableMapping): def __init__(self, image_name=None, image_path=None): self.image_name = image_name self.image_path = image_path self._masks = set() self._mask_arrays = {} self.provenance = {} self._pid = 0 Loading Loading @@ -79,12 +79,35 @@ class Node(dict, MutableMapping): @property def masks(self): mask_lookup = {'suppression': 'suppression'} if not hasattr(self, '_masks'): self._masks = pd.DataFrame() # If the mask is coming form another object that tracks # state, dynamically draw the mask from the object. for c in self._masks.columns: if c in mask_lookup: self._masks[c] = getattr(self, mask_lookup[c]).mask return self._masks @masks.setter def masks(self, v): self._masks.add(v[0]) self._mask_arrays[v[0]] = v[1] column_name = v[0] boolean_mask = v[1] self.masks[column_name] = boolean_mask @property def isis_serial(self): """ Generate an ISIS compatible serial number using the data file associated with this node. This assumes that the data file has a PVL header. """ if not hasattr(self, '_isis_serial'): try: self._isis_serial = generate_serial_number(self.image_path) except: self._isis_serial = None return self._isis_serial def get_array(self, band=1): """ Loading Loading @@ -130,9 +153,25 @@ class Node(dict, MutableMapping): 'parameters':kwargs} self._pid += 1 def anms(self, nfeatures=100, robust=0.9): mask = od.adaptive_non_max_suppression(self.keypoints,nfeatures,robust) self.masks = ('anms', mask) def suppress(self, func=spf.response, **kwargs): if not hasattr(self, 'keypoints'): raise AttributeError('No keypoints extracted for this node.') domain = self.handle.raster_size self.keypoints['strength'] = self.keypoints.apply(func, axis=1) if not hasattr(self, 'suppression'): # Instantiate a suppression object and suppress keypoints self.suppression = od.SpatialSuppression(self.keypoints, domain, **kwargs) self.suppression.suppress() else: # Update the suppression object attributes and process for k, v in kwargs.items(): if hasattr(self.suppression, k): setattr(self.suppression, k, v) self.suppression.suppress() self.masks = ('suppression', self.suppression.mask) def coverage_ratio(self, clean_keys=[]): """ Loading @@ -159,16 +198,31 @@ class Node(dict, MutableMapping): def plot(self, clean_keys=[], **kwargs): # pragma: no cover return plot_node(self, clean_keys=clean_keys, **kwargs) @property def isis_serial(self): def _clean(self, clean_keys): """ Generate an ISIS compatible serial number using the data file associated with this node. This assumes that the data file has a PVL header. Given a list of clean keys and a provenance id 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 ------- matches : dataframe A masked view of the matches dataframe mask : series A boolean series to inflate back to the full match set """ if not hasattr(self, '_isis_serial'): try: self._isis_serial = generate_serial_number(self.image_path) except: self._isis_serial = None return self._isis_serial No newline at end of file if not hasattr(self, 'keypoints'): raise AttributeError('Keypoints have not been extracted for this node.') panel = self.masks mask = panel[clean_keys].all(axis=1) matches = self.keypoints[mask] return matches, mask
autocnet/graph/tests/test_node.py +0 −6 Changes for autocnet/graph/tests/test_node.py: 0 added lines, 6 removed lines. Original line number Diff line number Diff line Loading @@ -50,12 +50,6 @@ class TestNode(unittest.TestCase): self.assertNotEqual(find_in_dict(p0, 'nfeatures'), find_in_dict(p1, 'nfeatures')) def test_anms(self): image = self.node.get_array() self.node.extract_features(image, extractor_parameters={'nfeatures':100}) self.node.anms(nfeatures=10) self.assertIn('anms', self.node.masks) self.assertTrue(sum(self.node._mask_arrays['anms']), 10) def test_isis_serial(self): serial = self.node.isis_serial Loading
autocnet/matcher/outlier_detector.py +145 −47 Changes for autocnet/matcher/outlier_detector.py: 145 added lines, 47 removed lines. Original line number Diff line number Diff line from collections import deque import math import cv2 import numpy as np import pandas as pd from autocnet.utils.observable import Observable class DistanceRatio(object): class DistanceRatio(Observable): """ A stateful object to store ratio test results and provenance. Loading Loading @@ -37,6 +40,9 @@ class DistanceRatio(object): self._observers = set() self.matches = matches self.mask = None self.clean_keys = None self.single = None self.attrs = ['mask', 'ratio', 'clean_keys', 'single'] @property def nvalid(self): Loading Loading @@ -90,66 +96,158 @@ class DistanceRatio(object): self._action_stack.append(state_package) self._current_action_stack = len(self._action_stack) - 1 def subscribe(self, func): class SpatialSuppression(Observable): """ Subscribe some observer to the edge Spatial suppression using disc based method. Parameters Attributes ---------- func : object The callable that is to be executed on update """ self._observers.add(func) df : dataframe Input dataframe used for suppressing def _notify_subscribers(self, *args, **kwargs): """ The 'update' call to notify all subscribers of a change. """ for update_func in self._observers: update_func(self, *args, **kwargs) mask : series pandas boolean series def rollforward(self, n=1): """ Roll forwards in the object history, e.g. do max_radius : float Maximum allowable point radius Parameters ---------- n : int the number of steps to roll forwards min_radius : float The smallest allowable radius size nvalid : int The number of valid points after suppression k : int The number of points to be saved error_k : float [0,1] the acceptable error in k domain : tuple The (x,y) extent of the input domain """ idx = self._current_action_stack + n if idx > len(self._action_stack) - 1: idx = len(self._action_stack) - 1 self._current_action_stack = idx state = self._action_stack[idx] setattr(self, 'mask', state['mask']) setattr(self, 'ratio', state['ratio']) setattr(self, 'clean_keys', state['clean_keys']) setattr(self, 'single', state['single']) # Reset attributes (could also cache) self._notify_subscribers(self) def rollback(self, n=1): def __init__(self, df, domain, min_radius=1, k=250, error_k=0.05): columns = df.columns for i in ['x', 'y', 'strength']: if i not in columns: raise ValueError('The dataframe is missing a {} column.'.format(i)) self._df = df.sort_values(by=['strength'], ascending=False).copy() self.max_radius = min(domain) self.min_radius = min_radius self.domain = domain self.mask = None self._k = k self._error_k = error_k self.attrs = ['mask', 'k', 'error_k'] self._action_stack = deque(maxlen=10) self._current_action_stack = 0 self._observers = set() @property def nvalid(self): return self.mask.sum() @property def k(self): return self._k @k.setter def k(self, v): self._k = v @property def error_k(self): return self._error_k @error_k.setter def error_k(self, v): self._error_k = v @property def df(self): return self._df def suppress(self): """ Roll backward in the object histroy, e.g. undo Suppress subpixel registered points to that k +- k * error_k points, with good spatial distribution, remain Adds a suppression mask to the edge mask dataframe. Parameters ---------- n : int the number of steps to roll backwards k : int The desired number of output points error_k : float [0,1) The acceptable epsilon """ idx = self._current_action_stack - n if idx < 0: idx = 0 self._current_action_stack = idx state = self._action_stack[idx] setattr(self, 'mask', state['mask']) setattr(self, 'ratio', state['ratio']) setattr(self, 'clean_keys', state['clean_keys']) setattr(self, 'single', state['single']) # Reset attributes (could also cache) df = self.df if self.k > len(df): raise ValueError('Only {} valid points, but {} points requested'.format(len(df), self.k)) min_radius = self.min_radius max_radius = self.max_radius while True: r = (min_radius + max_radius) / 2 cell_size = int(r / math.sqrt(2)) # Setup to store results result = [] # Compute the bin edges and assign points to the appropriate bins x_edges = np.arange(0,self.domain[0], self.domain[0] / cell_size) y_edges = np.arange(0,self.domain[1], self.domain[1] / cell_size) grid = np.zeros((len(y_edges), len(x_edges)), dtype=np.bool) # Bin assignment xbins = np.digitize(df['x'], bins=x_edges) ybins = np.digitize(df['y'], bins=y_edges) bounds = True for i, (idx, p) in enumerate(df.iterrows()): x_center = xbins[i] y_center = ybins[i] cell = grid[y_center - 1 , x_center - 1] if cell == False: result.append(idx) if len(result) > self.k - self.k * self.error_k: # Search the lower half, the radius is too small max_radius = r bounds = False continue # Cover the necessary cells grid[y_center - 3: y_center + 3, x_center - 3: x_center + 3] = True if bounds is False: continue # Check break conditions if self.k - self.k * self.error_k < len(result) < self.k + self.k * self.error_k: break elif abs(max_radius - min_radius) < 5: break elif len(result) < self.k: # Search the upper half, the radius is too small min_radius = r self.mask = pd.Series(False, self.df.index) self.mask.loc[np.array(result)] = True state_package = {'mask':self.mask, 'k': self.k, 'error_k': self.error_k} self._action_stack.append(state_package) self._notify_subscribers(self) self._current_action_stack = len(self._action_stack) - 1 # 0 based vs. 1 based def self_neighbors(matches): """ Loading